較正データが精度を決める — 狭く・合わせて・広く、三度
目標
CalibrationDataReaderを自分で実装し、同じモデルを、狭い・適切・広いの3通りのキャリブレーションデータで静的量子化して、グラフに固定される入力スケールと、自分で測った最大絶対誤差を並べて残します。そして、ずれたキャリブレーションはサンプル数では直せないことと、QDQとQOperatorが同じ計算の別の形であることを、測って確かめます。
なぜ重要なのか
静的量子化は、活性化値のスケールを変換の時点で固定し、そのスケールはキャリブレーションデータが決めます。そのため、キャリブレーションデータは設定値ではなく入力値です。同じモデルと同じコマンドでも、キャリブレーションデータが違えば、まったく別のモデルができます。ところが、そのデータはコードにもログにも残りません。 崩れる方向は2つあります。キャリブレーションで見た範囲が実際より狭いと、外に出た値が整数データ型の端に張り付いてしまい、逆量子化しても元に戻りません。広いと、切り落とされはしませんが、使われもしない範囲に目盛りを割り振ることになるので、実際に値が存在する区間の解像度が落ちます。どちらなのかは、誤差1つだけでは分からず、3通りで測ってみて初めて分かれます。 そして、サンプル数では直せない種類があります。キャリブレーションデータの分布そのものがずれていると、そのデータを512倍に増やしても、観察範囲はわずかしか広がりません。数ではなく分布が問題になる場合を区別できてこそ、時間を見当違いの場所に使わずに済みます。 採点ツールは、皆さんが書き出した数字を信じません。皆さんのリーダーを自分で動かして契約を守っているかを見て、採点ツールが作ったモデルに皆さんのツールをかけてみて、書き出された誤差を皆さんのモデルファイルで再計算して照合します。
ステップ
- /root/onnxq-calib/gen_model.pyを作成して実行してください(出力先: /root/onnxq-calib/fp32.onnx)。
- /root/onnxq-calib/reader.pyに
make_reader(arrays, input_name)を作成してください。 - /root/onnxq-calib/calibtool.pyに
quantizeとscalesを作成してください(出力先: /root/onnxq-calib/int8_match.onnx、/root/onnxq-calib/match.json)。 errorを追加し、狭いキャリブレーションで量子化してください(出力先: /root/onnxq-calib/int8_narrow.onnx、/root/onnxq-calib/narrow.json)。- 広いキャリブレーションで量子化してください(出力先: /root/onnxq-calib/int8_wide.onnx、/root/onnxq-calib/wide.json)。
- 3つを1か所にまとめてください(出力先: /root/onnxq-calib/calib_report.json)。
- 狭いキャリブレーションを1・8・64・512バッチに増やして試し、結果を書いてください(出力先: /root/onnxq-calib/samples.json)。
- 同じキャリブレーションでQOperator形式も作ってください(出力先: /root/onnxq-calib/format.json、/root/onnxq-calib/report.md)。
参考
- Pythonは
/opt/onnx-lab/bin/pythonです。システムのpython3には、numpyもonnxもありません。実行例:/opt/onnx-lab/bin/python /root/onnxq-calib/calibtool.py scales /root/onnxq-calib/int8_match.onnx。 - 実行の契約: 成功すれば終了コードは0で、標準出力にJSONを1つの塊で出します。引数の数が合わないときは2です。
quantize <입력.onnx> <출력.onnx> <씨앗> <배율> <묶음수> [형식]の応答は{"out", "bytes", "batches", "span", "format", "ops", "input_scale", "input_zero_point"}です(プレースホルダーは、入力ファイル、出力ファイル、シード、倍率、バッチ数、形式です)。形式はqdq(デフォルト)またはqoperatorです。活性化値も重みもQInt8です。scales <모델.onnx>の応答は{"input_scale", "input_zero_point", "observed_span", "quantize_nodes", "dequantize_nodes"}です(プレースホルダーはモデルファイルです)。observed_spanは、input_scaleに255を掛けた値です。error <fp32.onnx> <int8.onnx> <씨앗> <행>の応答は{"rows", "max_abs_error", "mean_abs_error", "max_abs_output"}です(プレースホルダーは、シードと行数です)。- キャリブレーションデータを作る規則(採点ツールが同じデータを作り直します): 1バッチは16行です。特徴量の数は、モデル入力の最後の軸の大きさです。
rng = numpy.random.default_rng(씨앗)でバッチごとに(rng.normal(0, 1, (16, 특징)) * 배율).astype(numpy.float32)を作り、バッチ数だけ出します(プレースホルダーは、シード、特徴量の数、倍率です)。掛けたあとでfloat32に落とします。 - 評価データを作る規則:
numpy.random.default_rng(씨앗).normal(0, 1, (행, 특징)).astype(numpy.float32)です(プレースホルダーは、シード、行数、特徴量の数です)。倍率は掛けません。これが実際の分布です。 - このラボが使う値: キャリブレーションのシードは4242、評価のシードは777、評価の行数は256、バッチ数は64です。倍率は、狭いが0.2、適切が1.0、広いが10.0です。
make_reader(arrays, input_name)は、onnxruntime.quantization.CalibrationDataReaderを継承したオブジェクトを返す必要があります。get_next()は、{입력이름: 배열}の辞書を1つずつ返し、データがなくなればNoneを返します(プレースホルダーは、入力名と配列です)。終わったあとにまた呼ばれても、Noneを返し続ける必要があります。例外を投げると、変換器がキャリブレーションの途中で死にます。- 使い切ったリーダーは再利用できません。量子化のたびに新しく作ってください。
- モデルに固定された入力スケールは、グラフで入力テンソルを最初の入力として受け取る
QuantizeLinear(QOperator形式ならQLinearMatMul)を見つけて、そのスケールのinitializerを読めば取り出せます。 calibtool.pyは、同じディレクトリのreader.pyを読み込んで使います。スクリプトがあるディレクトリをsys.pathに入れてください。- ステップ7が作るモデルは、/root/onnxq-calib/samplesの下に
narrow_n1.onnx・narrow_n8.onnx・narrow_n64.onnx・narrow_n512.onnx・match_n8.onnxとして保存します。512バッチは少し時間がかかります。 quantize_staticを呼ぶと、前処理を勧める警告が出ます。このラボのモデルはすでに単純なので、前処理なしでもそのまま動作します。- 公式ドキュメント: ONNX Runtimeの量子化 · QuantizeLinear · DequantizeLinear · ONNX Concepts
- よくある間違い: リーダーを1つ作って複数の変換で使い回すこと、終わったあとに例外を投げること、評価データに倍率を掛けること、形式が違えば精度も違うだろうと決めつけることです。
測る対象の用意
/root/onnxq-calib/gen_model.pyを作成して実行し、/root/onnxq-calib/fp32.onnxを作ってください。バッチ軸は名前だけを持つ動的な軸にし、行列2つとバイアス2つを含むMLPにしてください。
onnx.helperでグラフを組み立て、onnx.checkerで確認してから保存してください。入力の形状の最初の軸は、数字ではなく名前(「N」)にします。作ったあとで、onnxruntimeで1回実行してみれば確実です。
変換器にデータを流し込む契約
/root/onnxq-calib/reader.pyにmake_reader(arrays, input_name)を作成してください。CalibrationDataReaderを継承したオブジェクトを返し、get_next()は入力の辞書を1つずつ出して、データがなくなったあとはNoneを返し続ける必要があります。
契約は短いですが、終わりの扱いが肝心です。変換器はNoneが出るまで呼び続けるので、データがなくなったあとに例外を投げると、キャリブレーションの途中で死にます。イテレーターを1つ持ってnext(it, None)で出す方法も、リストを先頭から取り出す方法もあります。一度使い切ったオブジェクトは再利用できないことを覚えておいてください。
適切なキャリブレーションでの1回
/root/onnxq-calib/calibtool.pyにquantizeとscalesを作成し、シード4242・倍率1.0・64バッチで、/root/onnxq-calib/int8_match.onnxを作ってから、/root/onnxq-calib/match.jsonにspan・batches・input_scale・input_zero_point・observed_span・quantize_nodes・dequantize_nodesを書いてください。
quantize_staticに、quant_formatにはQDQを、activation_typeとweight_typeにはQInt8を指定してください。キャリブレーションデータの規則は、参考のセクションに明記されています。採点ツールが同じデータを作り直すので、そのとおりに従ってください。scalesは、グラフで入力テンソルを最初の入力として受け取るQuantizeLinearを見つけて、そのスケールのinitializerを読みます。observed_spanがキャリブレーションデータの範囲の幅とおおよそ合っているか、確認してみてください。
狭いキャリブレーション: 切り落とし
errorを追加し、シード4242・倍率0.2・64バッチで、/root/onnxq-calib/int8_narrow.onnxを作ってから、/root/onnxq-calib/narrow.jsonにspan・batches・input_scale・observed_span・max_abs_error・match_max_abs_errorを書いてください。誤差は、評価のシード777・256行で測ります。
評価データには倍率を掛けません。それが実際の分布です。キャリブレーションで見た範囲が実際より5倍狭いと、評価入力のかなりの部分がデータ型の端に張り付きます。適切なキャリブレーションの誤差も一緒に書いておくと、次のステップで比較しやすくなります。
広いキャリブレーション: 潰れ
シード4242・倍率10.0・64バッチで、/root/onnxq-calib/int8_wide.onnxを作り、/root/onnxq-calib/wide.jsonにnarrow.jsonと同じ形で書いてください。
今回は切り落とされません。キャリブレーションが実際よりはるかに広く見たからです。それでも誤差は、適切なキャリブレーションより大きくなります。256個の目盛りを、使われもしない範囲に割り振った結果です。observed_spanを、適切なキャリブレーションのものと比べてみてください。
3通りの比較
/root/onnxq-calib/calib_report.jsonにeval・narrow・match・wide・best・clipping_worse_than_coarseを書いてください。3つの項目にはspan・input_scale・observed_span・max_abs_errorが必要で、bestは誤差が最も小さいものの名前です。
clipping_worse_than_coarseは、狭いほうの誤差が広いほうの誤差より大きいかどうかを入れたブール値です。3つの誤差を並べるとU字が見えます。真ん中が最も低く、両側へ上がります。どちらの壁がより急なのか、確認してみてください。
サンプル数では直せないもの
狭いキャリブレーション(倍率0.2)を1・8・64・512バッチで4回量子化して、/root/onnxq-calib/samplesの下にnarrow_n1.onnx・narrow_n8.onnx・narrow_n64.onnx・narrow_n512.onnxとして保存し、適切なキャリブレーションの8バッチもmatch_n8.onnxとして保存したあとで、/root/onnxq-calib/samples.jsonにnarrow_runs・match_small・narrow_best・narrow_gain・beats_narrowを書いてください。
narrow_bestは、4回のうち最も小さい誤差で、narrow_gainは、1バッチの誤差をnarrow_bestで割った値です。サンプルを512倍に増やして得られた改善、ということです。beats_narrowは、適切なキャリブレーションの8バッチのほうが、狭いキャリブレーションの512バッチより良いかどうかを入れたブール値です。512バッチは少し時間がかかります。
同じ計算、別の形
適切なキャリブレーション(シード4242・倍率1.0・64バッチ)でQOperator形式を作り(出力先: /root/onnxq-calib/int8_qop.onnx)、/root/onnxq-calib/format.jsonにqdq・qoperator・same_input_scale・max_abs_difference・difference_vs_errorを書いてから、/root/onnxq-calib/report.mdを4つのセクションで書いてください。
qdqとqoperatorの項目には、bytes・ops・max_abs_errorを入れます。max_abs_differenceは、2つの形式のモデルに同じ評価バッチを入れて出た出力の最大絶対差で、difference_vs_errorは、それをQDQの誤差で割った値です。2つの形式の誤差の大きさは同じ桁なのに、両者の差は0ではありません。QDQは実行方法をランタイムに任せるからです。レポートの4つのセクションの見出しは、課題の文にあるとおりに残してください。