握ったものを数える — 実行プロバイダと誤差の蓄積
目標
要求した実行プロバイダーと、セッションが実際に手にしたプロバイダーを測り、サポートする演算子のリストに応じてグラフが何個の断片に分かれるかを数え、層の境界ごとに量子化誤差がどう育つかを表にするツールeprun.pyを作成します。
なぜ重要なのか
実行プロバイダーは、要求するものであって、保証されるものではありません。この環境で実際に測ると、存在しないプロバイダーをリストに入れても、例外は出ず、そのままCPUに落ちて動きます。そのため、「アクセラレーターを使っている」という思い込みが、ログに何の痕跡も残さないまま、長く続きます。確認する方法は1つだけです。セッションに尋ねることです。 プロバイダーは、モデル全体を担当するのではなく、ノードごとに担当できるものだけを引き取ります。そのため、サポートのリストがグラフの順序の上で途切れると、モデルが複数の断片に分かれ、断片の間ごとに値を受け渡します。断片の分かれ方が違えば、計算の順序が変わり、同じモデルに同じ入力を入れても、マシンごとに違う数が出ることがあります。 誤差の側にも、同じ姿勢が必要です。量子化誤差は、層ごとに新しく生まれ、前の層の誤差が次の層に入ります。ただし、層ごとに均等に育つわけではないので、話で済ませず、層の境界ごとに測って表にして初めて、どの層が問題なのかが見えます。 このPodにはプロバイダーが2つしかありません(AzureExecutionProvider・CPUExecutionProvider)。存在しないアクセラレーターをあるかのように扱わず、ここで観察できることだけを扱います。採点ツールは、毎回違うモデル・シード・サポートリストで皆さんのツールを実際に実行し、同じ計算をやり直して照合します。
ステップ
- /root/ep/gen_ep.pyを作成して実行し、モデルを2つ作ってください(出力先: /root/ep/fp32.onnx、/root/ep/int8.onnx)。層は5層以上です。
- /root/ep/eprun.pyに
providersを作成し、要求リストごとの結果を書いてください(出力先: /root/ep/providers.json)。 partitionを追加して、サポートする演算子のリストが与えられたとき、グラフが何個の断片に分かれるかを数えさせてください。exposeを追加して、層の境界のテンソルをグラフの出力として取り出したバージョンを作り、保存してください(出力先: /root/ep/fp32_exposed.onnx、/root/ep/int8_exposed.onnx)。layersを追加して、層の境界ごとに、最大絶対誤差・相対誤差・最小コサインを出力させてください。growthを追加して、層を重ねるにつれて誤差が育つ比率を出力させ、皆さんのモデルの結果を書いてください(出力先: /root/ep/growth.json)。settingsを追加して、同じモデル・同じ入力を、設定だけ変えて動かした結果を出力させ、書いてください(出力先: /root/ep/settings.json)。- /root/ep/ep_report.mdを4つのセクションで書いてください。
参考
- Pythonは
/opt/onnx-lab/bin/pythonです。システムのpython3には、onnxruntimeがありません。 - 実行の契約:
/opt/onnx-lab/bin/python /root/ep/eprun.py <명령> <인자...>(プレースホルダーは、コマンドと引数です)。成功すれば終了コードは0、ファイルがなければ3、使い方が間違っていれば2です。答えはJSONを1つの塊で標準出力に出します。EPのリストとサポートする演算子のリストは、カンマでつないで、1つの引数として渡します。 - モデルは、入力名が
x、入力の形が[None, 특징수]で、MatMul・Add・Reluを5層以上積みます(プレースホルダーは特徴量の数です)。int8.onnxはquantize_static(quant_format=QDQ)で作ります。 providers <model> <목록> [...]の応答は{"model", "available", "cases"}です(プレースホルダーはリストです)。availableは、onnxruntime.get_available_providers()そのままです。casesの各項目は{"requested", "actual", "missing", "fell_back", "error"}で、actualはセッションのget_providers()、missingは要求したのにavailableにない名前、fell_backはactualがrequestedと違うかどうか、errorはセッションを作れなかったときの例外名(そうでなければnull)です。- 存在しないプロバイダーを要求すると、onnxruntimeがフォールバックの案内を標準出力に出力します。そのままにすると、答えのJSONに混ざるので、セッションを作る間は、標準出力を標準エラー出力に向けておくか、少なくともJSONが最後の行に来るようにしてください。
providers.jsonは、上の応答にnoteを1行足したものです。存在するプロバイダーだけを書いた要求と、存在しないプロバイダーを混ぜた要求を、どちらも入れてください。partition <model> <지원연산자목록>の応答は{"model","supported","nodes","claimed","partition_count","handoffs","partitions"}です(プレースホルダーは、サポートする演算子のリストです)。グラフに書かれたノードの順に見ていき、サポートのリストにある演算子はep、そうでなければcpuと記し、同じ担当者が続く区間を1つの断片にまとめます。partitionsの各項目は{"owner","nodes","size"}で、handoffsは断片の数から1を引いた値(断片がなければ0)です。これは実際の割り当てではなく、シミュレーションです。expose <model> <out.onnx>の応答は{"model","out","boundaries","outputs"}です。層の境界は、MatMul・Gemmノードが受け取る最初の入力テンソルをグラフの順に集め、最後にグラフの最初の出力を付けたものです。すでに出力になっている名前は、もう一度入れません。整数バージョンで整数テンソルをfloatとして取り出そうとすると、セッションが開かないので、受け取る側のテンソルを境界にする規則を守ってください。layers <fp32> <int8> <seed>の応答は{"seed","rows","layer_count","layers"}です。入力はnumpy.random.default_rng(seed).standard_normal((64, 특징수)).astype(numpy.float32)で、rowsは64です(プレースホルダーは特徴量の数です)。セッションはproviders=["CPUExecutionProvider"]で作り、ほかの設定はデフォルトのままにします。layersの各項目は{"index","fp32","int8","max_abs_error","scale","rel_error","cos_min"}で、scaleはFP32側のテンソルの最大絶対値、rel_errorは最大絶対誤差をscaleで割った値(scaleが0なら0.0)、cos_minは行ごとに求めたコサインの最小値(分母が0ならその行は1.0)です。2つのグラフの境界は、位置の順で対応させます。growth <fp32> <int8> <seed>の応答は{"seed","layer_count","rel_error","ratios","monotone","total_growth","max_ratio"}です。ratiosのk番目は、k番目の層の相対誤差を、その前の層の相対誤差で割った値で、前の層が0ならnullです。monotoneは相対誤差が一度も減らなかったかどうか、total_growthは最後を最初で割った値(最初が0ならnull)、max_ratioは最も大きい比率の{"index","ratio"}で、同数なら前の位置です。settings <model> <seed>の応答は{"model","seed","baseline","cases","all_equal"}です。基準は、ORT_DISABLE_ALLでintra/interのスレッドが1で、casesは、(ORT_ENABLE_ALL, 1)・(ORT_DISABLE_ALL, 2)・(ORT_ENABLE_ALL, 2)の3通りを、この順に入れます。各項目は{"level","intra_op","max_abs_diff","bitwise_equal"}で、baselineは{"level","intra_op","max_abs_output","mean_output"}です。growth.jsonとsettings.jsonは、皆さんのモデルに対する上の応答そのものです。シードは皆さんが選び、その値がファイルに入ります。ep_report.mdは## 무엇을 요청했고 무엇을 쥐었나## 그래프가 쪼개지는 자리## 층을 거듭하며 자란 오차## 이 기계에서만 참인 것の4つのセクションです(韓国語の見出しは、順に「何を要求して何を手にしたか」「グラフが分かれる場所」「層を重ねて育った誤差」「このマシンでだけ真であること」という意味です)。- 公式ドキュメント: Execution Providers · Python API · グラフ最適化 · スレッド管理 · ONNX Concepts
- よくある間違い: 要求リストをそのままログに残すこと、存在しないEPを入れれば例外が出ると信じること、整数バージョンの中間テンソルをfloatとして取り出そうとして、セッションが開かないこと、誤差が層ごとに単調に大きくなると仮定することです。
- このラボは、時間・スループットを測りません。このMacはエミュレーションなので、レイテンシが2倍まで揺れます。
層を何重にも積んだモデルと、その整数バージョンを作る
/root/ep/gen_ep.pyを作成して実行し、モデルを2つ作ってください(出力先: /root/ep/fp32.onnx、/root/ep/int8.onnx)。線形演算子が5つ以上必要です。
層が2つや3つだと、誤差が育つ様子が見えません。5層以上積んでください。静的量子化にはCalibrationDataReaderが必要で、キャリブレーションデータは、テスト入力と同じ分布から取ればよく、同じサンプルである必要はありません。
要求したものと手にしたものを並べる
/root/ep/eprun.pyにproviders <model> <목록> [...]を作成し(プレースホルダーはリストです)、存在するプロバイダーだけを書いた要求と、存在しないプロバイダーを混ぜた要求の結果を書いてください(出力先: /root/ep/providers.json)。
存在しないプロバイダーを入れると例外が出そうですが、この環境ではそうなりません。自分で入れてみて、何が起きるかを測ってください。session.get_providers()が答えで、要求リストは答えではありません。例外を出すバージョンがあるかもしれないので、例外名も入れておいてください。そして、フォールバックの案内が標準出力に出るので、答えが混ざらないように止めてください。contextlib.redirect_stdoutが使えます。
グラフが何個の断片に分かれるかを数える
partition <model> <지원연산자목록>を追加して(プレースホルダーは、サポートする演算子のリストです)、その演算子だけをサポートするプロバイダーがあるとき、グラフがどう分かれるかをシミュレーションさせてください。断片の数と受け渡しの数を一緒に出力します。
実際の割り当てを真似るのではなく、サポートのリストがグラフの順序の上で途切れる場所を数えるのです。同じ担当者が続く区間が、1つの断片です。整数バージョンでMatMulだけをサポートすると置けば、Q/DQのせいで断片がどれだけ増えるかがすぐに見えます。
層の境界のテンソルを取り出す
expose <model> <out.onnx>を追加して、層の境界のテンソルをグラフの出力として付け足したバージョンを保存させてください(出力先: /root/ep/fp32_exposed.onnx、/root/ep/int8_exposed.onnx)。
整数バージョンでは、QuantizeLinearの出力はint8です。それをfloatとして取り出すと書けば、セッションが開きません。線形演算子が受け取る側のテンソルは、両方のグラフともfloatなので安全です。保存したバージョンを実際に開いて動かしてみてから、先に進んでください。
層の境界ごとに誤差を測る
layers <fp32> <int8> <seed>を追加して、シードで作った同じ入力を2つのバージョンに入れ、層の境界ごとに最大絶対誤差・大きさ・相対誤差・最小コサインを出力させてください。
絶対誤差だけを見ると、後ろの層が常に悪く見えます。値そのものが大きくなるからです。そのため、その層のテンソルの最大絶対値で割った相対誤差も一緒に出します。2つのグラフの境界の名前は違うので、位置の順で対応させてください。
育つ比率を表にする
growth <fp32> <int8> <seed>を追加して、層ごとの相対誤差とその比率、単調かどうか、全体の増加倍率、最も大きく跳ねた層を出力させ、皆さんのモデルの結果を書いてください(出力先: /root/ep/growth.json)。
測ってみると、比率が1より小さい区間も出てきます。活性化関数が一部を切り捨て、層ごとにスケールが新しく決まるからです。単調ではないこと自体が結果なので、そのまま書いてください。前の層の相対誤差が0なら割れないので、nullにします。
設定だけ変えて同じ入力を動かしてみる
settings <model> <seed>を追加して、同じモデル・同じ入力を、最適化レベルとスレッド数だけ変えて動かし、基準との差を出力させてください。皆さんのモデルの結果を書いてください(出力先: /root/ep/settings.json)。
このPodでは、4つのケースがすべて同じ値を出しました。その結果をそのまま書いてください。違わなければならないと取り繕いません。大事なのは、同じだったという事実ではなく、同じか違うかを測る手順が整っていることです。ほかのマシンでは、違う答えが出るかもしれません。
何がこのマシンでだけ真なのかを書く
/root/ep/ep_report.mdを、## 무엇을 요청했고 무엇을 쥐었나 ## 그래프가 쪼개지는 자리 ## 층을 거듭하며 자란 오차 ## 이 기계에서만 참인 것の4つのセクションで書いてください(韓国語の見出しは、順に「何を要求して何を手にしたか」「グラフが分かれる場所」「層を重ねて育った誤差」「このマシンでだけ真であること」という意味です)。使えるプロバイダーの数と、全体の誤差の増加倍率が、数字で入っている必要があります。
最後のセクションが、このレポートの核心です。ここで測った値のうち、どれがこのPodのプロバイダー構成とこの設定に結びついているかを書いてください。時間を測っていないことも書きます。