すべての演算子が int8 になるわけではない
一言でいうと
量子化ツールは、自分が知っている演算子だけをint8の形に変えます。残りはfloatのまま残り、その境界ごとにQ/DQの往復が生まれます。
なぜこれが問題になるのか
quantize_staticを実行してファイルサイズが小さくなったなら、仕事が終わったように見えます。ところが、グラフを開いてみると、半分は元のままです。量子化ツールには、どの演算子をどう扱うかを書いた表があり、その表にない演算子には手を付けません。手を付けなかった演算子はfloatで計算する必要があるので、前でint8にした値をもう一度floatに戻し、後ろで再びint8に詰めます。
そのため、「量子化した」と「int8で動く」は別の話です。この2つを同じだと思い込むと、期待したほど速くも小さくもならないのに、理由が見つかりません。理由はグラフの中にすべて書いてあります。数えればわかります。
QDQバージョンの読み方
静的量子化のQDQ形式は、演算子を置き換えません。代わりに、テンソルごとにQuantizeLinearとDequantizeLinearを挟み込みます。グラフには、依然としてMatMulがfloatのまま書かれていて、ランタイムがDQ -> 연산자 -> Q(プレースホルダーは演算子です)のまとまりを見つけて、int8カーネルに置き換えて動かします。
そのため、グラフだけを見て「何がint8に行けるのか」を判定するには、規則が必要です。このラボは、次のように決めます。すべての入力がDequantizeLinearから来て、すべての出力がQuantizeLinearにだけ行く演算子を、int8で包まれたものと見なします。1つでも外れれば、その演算子はfloatのまま残ります。
감싸인 모양 남은 모양
DQ -> MatMul -> Q DQ -> MatMul -> Add -> Relu -> Q
(int8 커널로 접힌다) (Add·Relu 는 float 섬)
floatのまま残った演算子どうしがつながっていれば、1つの塊です。これをfloat島と呼びましょう。島は、数も重要ですが、境界の数のほうがもっと重要です。島1つごとに、入ってくるDequantizeLinearと、出ていくQuantizeLinearが付きます。島が増えるほど境界が増え、境界ごとに、値を詰め替える作業と丸めが1回ずつ増えます。
実測で、もう1つ目につくことがあります。対象範囲を絞らずに量子化すると、グラフからReluが消えます。驚くことではなく、境界1つに畳み込まれたのです。後続のQuantizeLinearのゼロポイント(zero_point)を、int8の下端である-128にすると、詰められる値の範囲がちょうど0から始まり、負の値は詰められた瞬間に0に切り捨てられます。Reluがしていたことを、境界が代わりに行うのです。このラボのモデルで測ってみると、隠れ層のゼロポイントは実際に-128で、表現範囲は0.0から始まります。数えなければ、censusでReluが0になる理由を説明できません。
もう1つ。動的量子化は、まったく別の形です。MatMulがMatMulIntegerに置き換わり、DynamicQuantizeLinearが付きます。QDQバージョンを測っていた物差しを、ここにそのまま当てると、見当違いの答えが出ます。物差しは、形式ごとに決め直す必要があります。
現場でする3つのこと
1つ目は、対象範囲を絞ることです。どこかの層で精度が崩れるなら、そのノードだけを除けばよいです。ツールは、nodes_to_excludeでノード名を、op_types_to_quantizeで演算子の種類を受け取ります。名前で除くと、そのノードの重みがfloatのまま残り、種類で絞ると、その種類以外の演算子が丸ごとfloat島になります。どちらの場合も、何が変わったのかを数えて確認する必要があります。オプションを渡したからといって、そのとおりになったと信じません。
2つ目は、モデルを直して、量子化される形にすることです。MatMulの次にAddが来る形はとてもよくありますが、この2つをGemm1つにまとめると、間にあった中間テンソルが消えます。中間テンソルが消えると、そのテンソルに付いていたQ/DQ1組も消えます。値はそのままで、境界だけが減るのです。実測では、3層のモデルでQuantizeLinearが7個から4個に減りました。
対象範囲を絞るときに、よく見落とすことが1つあります。絞るのはタダではありません。あるノードを除くと、そのノードはfloatで動きますが、その前後にあったQ/DQは、たいていそのまま残ります。つまり、int8の利点は失い、境界のコストは払う場所ができるのです。そのため、「精度が揺れるからとりあえず除こう」という判断も、数えてから行います。除いたあとの島の数と境界の数を、絞っていないバージョンと並べれば、何を失い何を得たのかが数字で残ります。
3つ目は、数えて報告することです。演算子のcensus、Q/DQの数と往復数、島の数とサイズ、境界の数。この5つの数字があれば、「なぜ期待したほど減らなかったのか」という質問に、グラフで答えられます。時間を測る前に、グラフを先に読むほうがずっと速いです。
実務で本当に大切なこと
- 量子化したあとは、グラフを開いて数えます。ファイルサイズだけを見ません。
- 判定の規則を文章で書いておきます。何をint8に行ったと見なすのかは、決めるものであって、与えられるものではありません。
- 対象範囲を絞るオプションは、渡したあとで差を数えて確認します。
- 形式が違えば、物差しも違います。動的バージョンとQDQバージョンを、同じ物差しで測りません。
- モデルを直せるなら直します。オプションよりそちらのほうが、効果が大きい場合が多いです。
次のラボですること
同じモデルを5通りで出力したあと、グラフを数える分析ツールを1ステップずつ育てます。演算子のcensus、Q/DQの往復、float島、島ごとの境界の数を順に数え、対象範囲を絞った2つのバージョンと元のバージョンの差を出します。最後に、MatMulとAddをGemmにまとめて量子化し直し、Q/DQがいくつ消えたかを数えます。採点ツールは、毎回違う層の数と違う除外ノードで自分のモデルを作り、皆さんの分析ツールを実際に動かして、同じ数を数えて照合します。