較正データが物差しを作る — 狭ければ切られ、広ければ潰れる
一言でいうと
静的量子化では、スケールを決めるのはキャリブレーションデータです。キャリブレーションで見た範囲が実際の分布より狭いと値が切り落とされ、広いと目盛りが粗くなります。どちらにしても、精度はキャリブレーションデータの関数です。
なぜここで何度も崩れるのか
静的量子化は、活性化値のスケールを変換の時点で固定します。固定するには、活性化値がどの範囲にあるかを知る必要があり、それを教えるのがキャリブレーションデータです。変換器はこのデータを流し込み、層ごとに最小値と最大値を観察して、その範囲からスケールとゼロポイントを作ります。
そのため、キャリブレーションデータは設定値ではなく入力値です。同じモデル、同じ変換コマンドでも、キャリブレーションデータだけを変えると、まったく別のモデルができます。ところが、このデータはコードにも残らず、ログにも残りません。数か月後に「このモデルの精度はなぜこうなのか」を調べようとしても、あるのはモデルファイルと変換スクリプトだけで、答えを決めたのはその日に使った配列たちです。
崩れる方向は2つあります。狭いと切り落とされます。 キャリブレーションで見た最大値より大きい値が入ってくると、整数データ型の端に張り付いてしまい、逆量子化しても元に戻りません。誤差が往復誤差の上限の数十倍にまで膨らみます。広いと潰れます。 実際には使われない広い範囲に256個の目盛りを割り振るため、データが実際に存在する区間には、わずかな目盛りしか置かれません。切り落とされはしませんが、解像度を失います。
どう動くのか
ONNX Runtimeの量子化ドキュメントは、静的量子化にCalibrationDataReaderを渡すよう求めています。契約は短いものです。
class 내_공급기(CalibrationDataReader):
def get_next(self):
더 줄 것이 있으면 -> {"입력이름": 배열} 사전 하나
더 없으면 -> None
変換器は、Noneが出るまでget_next()を呼び続けます。そのため、よくある失敗が2つあります。1つは、終わったあとにNoneの代わりに例外を投げることで(キャリブレーションの途中で死にます)、もう1つは、一度使い切ったリーダーを再利用することです(2回目の変換は、キャリブレーションデータを1つも受け取れません)。
キャリブレーションが残した痕跡は、モデルの中にそのまま残っています。グラフを開いて、入力のすぐ後ろにあるQuantizeLinearを見つけ、そのスケールのinitializerを読むと、スケールに目盛りの数を掛けた値が、キャリブレーションで観察した範囲の幅です。この数字1つで、「キャリブレーションデータがどこまで見たのか」を、あとから確認できます。
形式も2つあります。QDQは、元の演算子をそのまま残し、前後にQuantizeLinearとDequantizeLinearを挟み込みます。グラフにMatMulがそのまま見え、整数化はランタイムが実行時に融合します。QOperatorは、QLinearMatMulのような整数演算子に丸ごと置き換えます。ノード数が少なくファイルも小さくなりますが、グラフは読みにくくなります。
ここで、よくある誤解を1つ指摘しておく必要があります。同じキャリブレーションから作った2つの形式は、同じスケールとゼロポイントを使い、誤差の大きさも同じ桁ですが、要素ごとに見ると完全には一致しません。QDQは元の演算子をグラフに残し、実行方法をランタイムに任せるからです。ランタイムがQ/DQを畳んで整数カーネルで動かすこともあれば、逆量子化した値のまま浮動小数点で計算することもあります。そのため、「形式を変えたら出力が少し変わった」のは正常で、桁が変わったのであれば、そのときはキャリブレーションを疑います。
現場での姿
1つ目は、サンプルを増やしても直らない種類があることです。キャリブレーションデータの分布そのものがずれていると、そのデータを何百倍に増やしても、観察範囲はわずかしか広がりません。狭いキャリブレーションを512倍に増やして得られる改善は、適切な分布で数バッチだけ与えたときの改善に及びません。数ではなく分布が問題になる場合があることを、測って区別する必要があります。
2つ目は、静的が動的より常に正確とは限らないことです。「静的のほうが良い」という話は、キャリブレーションが正しく行われたときの話です。キャリブレーションがずれると、動的量子化よりはるかに悪いモデルができます。そして、その事実は評価してみるまで、どこにも現れません。
3つ目は、切り落としと潰れは症状が異なることです。切り落としは、大きな入力でだけ大きく外れ、小さな入力では問題ありません。潰れは、すべての入力で均等に少しずつ外れます。誤差を入力の大きさ別に分けて見れば、どちらなのかが分かれます。
4つ目は、キャリブレーションデータを記録として残すことです。どのサンプルを何個使ったか、そのデータの範囲がいくらだったかを、モデルの横に書き留めておきます。モデルの中のスケールと照合できてこそ、あとで原因を探せます。
実務で本当に大切なこと
- キャリブレーションデータは、デプロイ時に入ってくるデータと同じ分布から取ります。これがサンプル数より先です。
- モデルに固定されたスケールを取り出して確認します。スケール掛ける目盛りの数が、キャリブレーションで見た範囲です。
- 3通りで測ってみます。狭く・適切に・広くの3回量子化してみると、自分のデータの感度が見えます。
- 形式は表現の違いであり、実行はランタイムが決めます。形式を変えて誤差が桁で変わったなら、キャリブレーションを疑います。
次のラボですること
CalibrationDataReaderを自分で実装し、同じモデルを、狭い・適切・広いの3通りのキャリブレーションデータで量子化して、グラフに固定された入力スケールと、自分で測った最大絶対誤差を並べて書きます。次に、狭いキャリブレーションを1バッチから512バッチまで増やしながら、それでは直らないことを測り、最後にQDQとQOperatorを同じキャリブレーションから作って、2つの形式の誤差の大きさと、両者の差を一緒に測ります。採点ツールは、皆さんのリーダーを自分で動かしてみて、書き出された誤差を、皆さんのモデルファイルで再計算して照合します。