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

AIダイエット失敗事件

較正データが精度を決める — 狭く・合わせて・広く、三度

TT Labで続きを見る

目標

CalibrationDataReaderを自分で実装し、同じモデルを、狭い・適切・広いの3通りのキャリブレーションデータで静的量子化して、グラフに固定される入力スケールと、自分で測った最大絶対誤差を並べて残します。そして、ずれたキャリブレーションはサンプル数では直せないことと、QDQとQOperatorが同じ計算の別の形であることを、測って確かめます。

なぜ重要なのか

静的量子化は、活性化値のスケールを変換の時点で固定し、そのスケールはキャリブレーションデータが決めます。そのため、キャリブレーションデータは設定値ではなく入力値です。同じモデルと同じコマンドでも、キャリブレーションデータが違えば、まったく別のモデルができます。ところが、そのデータはコードにもログにも残りません。 崩れる方向は2つあります。キャリブレーションで見た範囲が実際より狭いと、外に出た値が整数データ型の端に張り付いてしまい、逆量子化しても元に戻りません。広いと、切り落とされはしませんが、使われもしない範囲に目盛りを割り振ることになるので、実際に値が存在する区間の解像度が落ちます。どちらなのかは、誤差1つだけでは分からず、3通りで測ってみて初めて分かれます。 そして、サンプル数では直せない種類があります。キャリブレーションデータの分布そのものがずれていると、そのデータを512倍に増やしても、観察範囲はわずかしか広がりません。数ではなく分布が問題になる場合を区別できてこそ、時間を見当違いの場所に使わずに済みます。 採点ツールは、皆さんが書き出した数字を信じません。皆さんのリーダーを自分で動かして契約を守っているかを見て、採点ツールが作ったモデルに皆さんのツールをかけてみて、書き出された誤差を皆さんのモデルファイルで再計算して照合します。

ステップ

  1. /root/onnxq-calib/gen_model.pyを作成して実行してください(出力先: /root/onnxq-calib/fp32.onnx)。
  2. /root/onnxq-calib/reader.pyにmake_reader(arrays, input_name)を作成してください。
  3. /root/onnxq-calib/calibtool.pyにquantizeとscalesを作成してください(出力先: /root/onnxq-calib/int8_match.onnx、/root/onnxq-calib/match.json)。
  4. errorを追加し、狭いキャリブレーションで量子化してください(出力先: /root/onnxq-calib/int8_narrow.onnx、/root/onnxq-calib/narrow.json)。
  5. 広いキャリブレーションで量子化してください(出力先: /root/onnxq-calib/int8_wide.onnx、/root/onnxq-calib/wide.json)。
  6. 3つを1か所にまとめてください(出力先: /root/onnxq-calib/calib_report.json)。
  7. 狭いキャリブレーションを1・8・64・512バッチに増やして試し、結果を書いてください(出力先: /root/onnxq-calib/samples.json)。
  8. 同じキャリブレーションでQOperator形式も作ってください(出力先: /root/onnxq-calib/format.json、/root/onnxq-calib/report.md)。

参考

測る対象の用意

/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つのセクションの見出しは、課題の文にあるとおりに残してください。