TT Lab
はじめる
学ぶ 学習パス コース

AIダイエット失敗事件

要求した実行プロバイダと実際に握ったものは違う

TT Labで続きを見る

一言でいうと

実行プロバイダー(EP)は、要求するものであって、保証されるものではありません。存在しないEPを入れても、エラーではなく静かなフォールバックになり、セッションに尋ねて初めて、何を手にしたのかがわかります。

なぜこれが問題になるのか

アクセラレーターを使うつもりでリストに書いたのに、実際にはCPUで動いていることがよくあります。問題は、そのとき何も起きないことです。例外も出ず、終了コードも0です。警告が1行、標準エラー出力を通り過ぎるだけで、ログを集める場所では、その1行が埋もれます。

このラボの環境で実際に測ると、こうなります。存在しないCUDAExecutionProviderをリストの先頭に入れても、セッションは作られ、推論もできます。session.get_providers()に尋ねると、CPUExecutionProviderが1つだけ返ってきます。まったく存在しない名前をでっち上げて入れると、「Unknown Provider Type」という案内が出て、やはりCPUに落ちて動きます。

そのため、規則は1つです。要求リストを信じず、セッションに尋ねます。デプロイのログに残すべきものは、要求したリストではなく、セッションが返したリストです。

EPは何を決めるのか

EPは、「このモデルをどこで動かすか」を選ぶ仕組みではなく、ノードごとに誰が担当するかを決める仕組みです。ランタイムは、要求リストを順にたどって、各EPに「このノードを担当できますか」と尋ね、担当すると答えたノードだけを引き取らせます。残ったノードは次のEPへ、最後まで誰も担当しなければ、CPUが担当します。

そのため、1つのモデルが複数の断片に分かれて、別々の場所で動きます。断片と断片の間では、値を受け渡す必要があります。サポートする演算子のリストが、グラフの順序の上で何度も途切れると、断片の数が増え、受け渡しの回数も増えます。

지원: MatMul 만               조각 5개 · 넘김 4번
  [MatMul] -> Add -> Relu -> [MatMul] -> Add
    EP        CPU    CPU        EP       CPU

リストの順序がそのまま優先順位です。先に書いたプロバイダーが先に尋ね、担当すると答えたノードを引き取ります。そのため、同じ2つのプロバイダーを、順序だけ変えて書いても、割り当てが変わることがあります。そして、CPUは書かなくても、最後に付いてきます。この環境でAzureExecutionProviderだけを書いてセッションを作ると、実際のリストはそれとCPUExecutionProviderの2つになります。担当する者が誰もいないノードを残しておくことはできないからです。要求したリストと実際のリストが違うのが、エラーではなく正常な動作である理由も、ここにあります。

ここから、重要な結論が1つ出てきます。同じモデルに同じ入力を入れても、マシンごとに違う数が出ることがあります。断片の分かれ方が違えば、計算の順序が変わり、浮動小数点の足し算は、順序が変わると結果がわずかに変わります。そのため、「うちのノートPCでは合っていた」は、根拠にはなりにくいです。

層を重ねると誤差はどうなるのか

量子化誤差は、層ごとに新しく生まれ、前の層で生まれた誤差は、次の層の入力に入ります。そのため、誤差は積み重なります。ただし、実際に測ってみると、層ごとに均等に育つわけではありません。このラボで5層のモデルを測ったとき、層の境界の相対誤差は、0.099、0.074、0.063、0.214、0.167、0.369でした。減ったり増えたりしながら、全体では3.7倍になりました。

減る区間がある理由は、層ごとに値の大きさが変わり、活性化関数が一部を切り捨て、スケールが新しく決まるからです。そのため、「層ごとに少しずつ大きくなる」という話で終わらせてはならず、層の境界ごとに測って表にして初めて、どこが問題なのかが見えます。ある1つの層で比率が3倍に跳ね上がるなら、その層が、対象範囲を絞る候補です。

誤差が育つからといって、精度がその分だけ崩れるわけでもありません。最後の境界の相対誤差が0.37でも、判断が変わるサンプルは、わずかかもしれません。出力ベクトル全体が少しずつずれることと、順位がひっくり返ることは、別のことだからです。そのため、層ごとの誤差の表は、どこに手を入れるかを選ぶために使い、デプロイするかどうかは、タスクの指標で別に判断します。2つを1つの数字にまとめようとすると、どちらもぼやけます。

層の境界を測るには、中間テンソルを見られる必要があります。ONNXのグラフでは、見たいテンソルを、グラフの出力リストに付け足せばよいです。ただし、整数バージョンでは、整数テンソルをfloatとして取り出せないので、線形演算子が受け取る側のテンソルを境界にするのが安全です。その位置は、両方のグラフとも、floatです。

現場でやるべきこと

次のラボですること

存在するEPと存在しないEPを混ぜて要求し、何を手にするのかを自分で測り、サポートする演算子のリストが与えられたとき、グラフが何個の断片に分かれるかを数えるシミュレーションを作ります。次に、層の境界のテンソルをグラフの出力として取り出し、FP32のバージョンと整数のバージョンを同じ入力で動かして、層ごとの誤差を測り、育つ比率を表にします。最後に、同じモデル・同じ入力を、最適化レベルとスレッド数だけ変えて動かし、結果が変わるかを見ます。採点ツールは、毎回違うモデル・シード・サポートリストで皆さんのツールを実際に動かし、同じ計算をやり直して照合します。