較正データなしで — 動的量子化を実測する
目標
dyntool.pyを作って自分のモデルに動的量子化をかけ、グラフがどう変わるか、ファイルがどこで減りどこがそのままか、入力の分布が変わってもなぜ耐えるか、そしてこの方式が合わない場合がどんな姿かを、すべて数字で残します。
なぜ重要なのか
静的量子化は、代表的な入力を集めないと始められず、そのデータを手に入れる作業が現場で最も時間がかかります。動的量子化は、活性化値の範囲を事前に決めず、推論のたびにそのテンソルを見て決めるので、モデルファイルが1つあればできます。そのため、何をするかを決める前に、まずかけてみるベースラインになります。 その代わり、何を得て何を失うかを知ってこそ選べます。得るものは、分布が揺れても耐える性質です。バッチごとに物差しを作り直すので、キャリブレーションのときに見た範囲の外に出ることがありません。失うものは再現性です。スケールがそのバッチ全体の最大値で決まるため、外れ値1行が同じバッチのほかの行まで引き下げます。単独で入れたときは問題なかった入力が、隣のせいで悪くなるのです。 ファイルサイズも、測ってみないとわかりません。減るのは行列だけで、バイアスはfloat32のまま残り、減った行列の隣には、スケールとゼロポイントが付いてきます。重みが小さいモデルでは、新しく入ったノードのほうが減った量より大きく、ファイルがかえって大きくなります。 採点ツールは、皆さんが書き出した数字を信じません。毎回異なるシードとシェイプでモデルを新しく作り、皆さんのツールを実際に実行して、DynamicQuantizeLinear演算子の実際の結果、そして採点ツールが自分で動かした推論結果と照合します。
ステップ
- /root/onnxq-dyn/gen_model.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-dyn/fp32.onnx)。
- /root/onnxq-dyn/dyntool.pyに
quantizeを作り、ファイルを作ってください(出力先: /root/onnxq-dyn/dyn.onnx)。 sizesを追加し、initializerごとのバイト数を数えて書いてください(書き込み先: /root/onnxq-dyn/size.json)。dqparamsを追加し、DynamicQuantizeLinearが推論のたびに行う計算を自分で実装してください。compareを追加し、入力の倍率を変えながら誤差を測って書いてください(書き込み先: /root/onnxq-dyn/batches.json)。outlierを追加し、外れ値1行がバッチを揺らすかどうかを測って書いてください(書き込み先: /root/onnxq-dyn/outlier.json)。- ごく小さなモデルを作って量子化し、結果を書いてください(作成スクリプト: /root/onnxq-dyn/gen_small.py、書き込み先: /root/onnxq-dyn/unfit.json)。
- 1枚にまとめてください(出力先: /root/onnxq-dyn/report.json、/root/onnxq-dyn/report.md)。
参考
- Pythonは
/opt/onnx-lab/bin/pythonです。システムのpython3には、numpyもonnxもありません。実行例:/opt/onnx-lab/bin/python /root/onnxq-dyn/dyntool.py sizes /root/onnxq-dyn/fp32.onnx。 - 実行の契約: 成功すれば終了コードは0で、標準出力にJSONの1つの塊を出します。引数の数が合わなければ2です。
quantize <입력.onnx> <출력.onnx>の応答は{"out": 경로, "bytes": 정수, "ops": {연산자: 개수}, "removed": [사라진 연산자], "added": [새로 든 연산자]}です(プレースホルダーは、入力ファイル、出力ファイル、パス、整数、演算子、個数、消えた演算子、新しく入った演算子です)。重みはQInt8です。sizes <모델.onnx>の応答は{"file_bytes": 정수, "initializers": {이름: {"dtype": 문자열, "elements": 정수, "bytes": 정수}}, "initializer_bytes": 정수}です(プレースホルダーは、モデルファイル、整数、名前、文字列です)。dqparams <입력.npy> <출력.npy>の応答は{"scale": 실수, "zero_point": 정수, "min": 실수, "max": 실수, "count": 정수}です(プレースホルダーは、入力ファイル、出力ファイル、実数、整数です)。量子化したuint8配列を、出力パスに保存します。compare <fp32.onnx> <int8.onnx> <씨앗> <행> <배율>の応答は{"rows", "span", "max_abs_output", "max_abs_error", "relative"}です(プレースホルダーは、シード、行数、倍率です)。relativeは、誤差をfp32の出力の絶対値の最大で割った値です。outlier <fp32.onnx> <int8.onnx> <씨앗> <행> <튀는값>の応答は{"rows", "spike", "clean", "with_spike", "ratio"}です(プレースホルダーは、シード、行数、外れ値です)。- バッチを作る規則(採点ツールが同じバッチを作り直します): 特徴量の数は、モデルの入力の最後の軸のサイズです。
rng = numpy.random.default_rng(씨앗)を使い、(rng.normal(0, 1, (행, 특징)) * 배율).astype(numpy.float32)で作ります(プレースホルダーは、シード、行数、特徴量の数、倍率です)。掛けたあとでfloat32に落とします。 outlierの規則: 上の規則を倍率1.0で使ってバッチを作り、cleanは、そのバッチだけを入れたときの最大絶対誤差です。with_spikeは、numpy.full((1, 특징), 튀는값, dtype=numpy.float32)の1行を下に付けて入れたあと、最初の行数分で測った最大絶対誤差です(プレースホルダーは、特徴量の数と外れ値です)。ratioは、with_spikeをcleanで割った値です。- DynamicQuantizeLinearの定義: 範囲は
[min(x, 0), max(x, 0)]で、スケールはその幅を255で割った値であり、ゼロポイントは、実数の0.0が収まる整数の位置を、0から255の間に切り詰めたものです。丸めは、numpyのnp.rintと同じ、偶数への丸めです。 - スケールは
np.float32(...)で作り、割り算もfloat32で行ってください。Pythonのfloatで割ると、境界にかかった要素で、ランタイムと1目盛りずれます。 fp32.onnxのバッチ軸は、名前だけの動的軸である必要があります(["N", 20])。数字で固定すると、採点ツールが別の行数を入れられません。- ステップ5では、倍率0.05・1.0・50.0を、シード20260917・行数32で測ります。ステップ6では、シード20260917・行数12・外れ値60.0です。
- 公式ドキュメント: ONNX Runtime 量子化 · DynamicQuantizeLinear · MatMulInteger · ONNX Concepts
quantize_dynamicを呼ぶと、前処理を勧める警告が出ます。このラボのモデルはすでに単純なので、前処理なしでもそのまま動作します。- よくある間違い: ファイルが減っただろうと推測して測らないこと、バイアスまでint8になったと書くこと、バッチの規則でfloat32に落とす順序を変えること、外れ値の実験で外れ値の行まで含めて誤差を測ることです。
測る対象を手に入れる
/root/onnxq-dyn/gen_model.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-dyn/fp32.onnx)。バッチ軸は名前だけの動的軸で、行列2つとバイアス2つを持つMLPです。
onnx.helperでグラフを組み立て、onnx.checkerで確認してから保存してください。入力シェイプの最初の軸は、数字ではなく名前(「N」)にします。initializerは、numpy_helper.from_arrayで作ります。作ったあとで、onnxruntimeで一度動かしてみると確実です。
コマンド1行で重みだけを固定する
/root/onnxq-dyn/dyntool.pyにquantize <입력.onnx> <출력.onnx>を作り、ファイルを作ってください(出力先: /root/onnxq-dyn/dyn.onnx。プレースホルダーは入力ファイルと出力ファイルです)。応答には、変わったあとの演算子数と、消えた演算子、新しく入った演算子が必要です。
onnxruntime.quantizationのquantize_dynamicに、weight_typeをQInt8として渡せばよいです。キャリブレーションデータは入れません。入れる場所がありません。変わったグラフは、onnx.loadで開いて、nodeのop_typeを数えれば見えます。MatMulがどこへ行ったかを確認してみてください。
どこが減ったかを名前で言う
sizes <모델.onnx>を追加して、initializerごとにデータ型・要素数・バイト数を数えさせ、2つのモデルを比べた結果を、fp32_bytes・dyn_bytes・shrunk・kept_float・weight_bytes_before・weight_bytes_afterとして書いてください(書き込み先: /root/onnxq-dyn/size.json。プレースホルダーはモデルファイルです)。
onnx.loadのgraph.initializerを順に見て、numpy_helper.to_arrayで変換すれば、dtypeとnbytesが出ます。shrunkは、fp32にはあるのに動的モデルでは名前が消えたinitializerで、kept_floatは、そのままfloat32で残ったものです。バイアスがどちらにあるかを見てください。
推論のたびに物差しを作り直す計算
dqparams <입력.npy> <출력.npy>を追加し、DynamicQuantizeLinearが推論のたびに行う計算を自分で実装してください(プレースホルダーは入力ファイルと出力ファイルです)。スケール・ゼロポイント・範囲を出力し、量子化したuint8配列を保存します。
範囲には、必ず0を入れます。最小値が正なら0に下げ、最大値が負なら0に上げます。uint8なので目盛りは255個で、ゼロポイントは、実数の0.0が収まる整数の位置です。すべて正の配列と、すべて負の配列を入れてみると、ゼロポイントが両端に行くのが見えます。
入力を1000倍まで振ってみる
compare <fp32.onnx> <int8.onnx> <씨앗> <행> <배율>を追加し、シード20260917・行数32で、倍率0.05・1.0・50.0を測って、runsとrelative_spreadとして書いてください(書き込み先: /root/onnxq-dyn/batches.json。プレースホルダーは、シード、行数、倍率です)。
バッチを作る規則は、参考のセクションに固定されています。採点ツールが同じバッチを作り直す必要があるので、そのとおりに従ってください。relative_spreadは、3つのrelativeの値の最大値を最小値で割った値です。絶対誤差は入力の大きさに従って大きくなりますが、相対誤差はどうなるかを見てください。
隣の1行がバッチを揺らす
outlier <fp32.onnx> <int8.onnx> <씨앗> <행> <튀는값>を追加し、シード20260917・行数12・外れ値60.0で測って書いてください(書き込み先: /root/onnxq-dyn/outlier.json。プレースホルダーは、シード、行数、外れ値です)。
同じ12行を2回入れます。1回はそのまま、もう1回は下に外れ値の行を付けて。誤差は、最初の12行だけで測ります。外れ値の行そのものの誤差は数えません。そうしてこそ、「隣のせいで悪くなった」という言い方が成り立ちます。
小さなモデルではかえって大きくなる
ごく小さなモデルを作って量子化し(作成スクリプト: /root/onnxq-dyn/gen_small.py、小さなモデル: /root/onnxq-dyn/small.onnx、量子化後: /root/onnxq-dyn/small_dyn.onnx)、結果をsmall_fp32_bytes・small_dyn_bytes・grew・growth_bytes・dynamic_quantize_nodes・kept_float_opsとして書いてください(書き込み先: /root/onnxq-dyn/unfit.json)。
重みが数百バイトしかなければ、新しく入ったノードとスケール・ゼロポイントのinitializerのほうが、減った量より大きくなります。kept_float_opsは、fp32モデルと動的モデルの両方にある演算子です。変わらずに残ったものですね。growは文字列ではなく、ブール値です。
得たものと失ったものを1枚にまとめる
/root/onnxq-dyn/report.jsonにgraph_change・file・relative_error_spread・outlier_ratio・small_model_grewを書き、## 그래프가 어떻게 바뀌나、## 어디가 줄고 어디가 안 줄었나、## 보정 자료가 필요 없는 이유、## 동적 양자화가 맞지 않는 경우の4つのセクションでMarkdownのレポートを書いてください(出力先: /root/onnxq-dyn/report.md。韓国語の見出しは、順に「グラフがどう変わるか」「どこが減ってどこが減らなかったか」「キャリブレーションデータが不要な理由」「動的量子化が合わない場合」という意味です)。
前のステップで作ったJSONを読んで組み立てればよいです。fileのratioは、動的モデルのバイト数をfp32のバイト数で割った値です。レポートには、数字と一緒に、その数字が何を指すのかを1行ずつ書いてください。読む人は、皆さんの実験を見ていません。