float の島を数える — 演算子ごとの対応と置き換え
目標
同じモデルを5通りで出力したあと、グラフの中でint8に包まれた演算子と、floatのまま残った島を数える分析ツールgraphscan.pyを作成します。対象範囲を絞る2つのオプションの効果を数字で確認し、モデルをGemmに直して、Q/DQの往復を減らします。
なぜ重要なのか
量子化ツールは、自分が知っている演算子だけをint8の形に変えます。QDQ形式では、演算子名がそのまま残り、テンソルごとにQuantizeLinearとDequantizeLinearが挟まれるだけです。そのため、「量子化した」と「int8で動く」は別の話で、両者を分けるには、グラフを数える必要があります。 このラボは、判定の規則を次のように固定します。すべての入力がDequantizeLinearから来て、すべての出力がQuantizeLinearにだけ行く演算子を、int8に包まれたものと見なします。残りはfloatのまま残り、つながったものどうしをまとめると、float島になります。 島は、数より境界が問題です。島ごとに、入ってくるDQと出ていくQが付き、境界ごとに、値を詰め替える作業と丸めが1回ずつ増えます。島を減らす道は2つです。対象範囲を絞って、外れるノードをそもそも除くか、モデルを直して、量子化される形にするかです。 採点ツールは、皆さんが書いた数字を信じません。毎回違う層の数・形・除外ノードで自分のモデルを一時ディレクトリに作り、皆さんの分析ツールを実際に実行して、採点ツールが同じ規則で数えた値と照合します。ステップ6・7は、皆さんが作ったモデルファイルを採点ツールが直接読んで、数え直します。
ステップ
- /root/ops/gen_models.pyを作成して実行し、
/root/opsの下にモデルを5つ作ってください。 - /root/ops/graphscan.pyに
nodesを作成して、演算子のcensusと、初期値のデータ型を数えさせてください。 qdqを追加して、QuantizeLinear・DequantizeLinearと、Q/DQの往復を数えさせてください。islandsを追加して、int8に包まれた演算子とfloat島を分けさせてください。boundaryを追加して、島ごとに出入りする境界を数えさせてください。diffを追加して、2つのモデルの差を出力させ、対象範囲を絞った結果を書いてください(出力先: /root/ops/scope.json)。- MatMulとAddをGemmにまとめたモデル(/root/ops/gemm.onnx)と、それを量子化したモデル(/root/ops/gemm_full.onnx)を作り、結果を書いてください(出力先: /root/ops/fuse.json)。
- /root/ops/ops_report.mdを4つのセクションで書いてください。
参考
- Pythonは
/opt/onnx-lab/bin/pythonです。システムのpython3には、onnxがありません。 - 実行の契約:
/opt/onnx-lab/bin/python /root/ops/graphscan.py <명령> <모델...>(プレースホルダーは、コマンドとモデルです)。成功すれば終了コードは0、ファイルがなければ3、使い方が間違っていれば2です。答えはJSONを1つの塊で標準出力に出します。 - ステップ1が作る5つのファイル:
fp32.onnx(オリジナル)、full.onnx(静的QDQで、対象範囲を絞っていないバージョン)、matmul_only.onnx(op_types_to_quantize=["MatMul"])、excluded.onnx(nodes_to_excludeでMatMulを1つ除いたバージョン)、dynamic.onnx(quantize_dynamic)です。オリジナルは、MatMul・Add・Reluで層を3つ以上積み、ノードごとに名前を付けます。入力名はxです。 nodesの応答は{"model", "nodes", "op_types", "initializers"}です。op_typesは演算子名をキーにした個数で、initializersは初期値のデータ型名(onnx.TensorProto.DataType.Nameが返すFLOAT・INT8のような名前)をキーにした個数です。qdqの応答は{"model", "quantize", "dequantize", "round_trips", "weight_dequantize"}です。round_tripsは、出力がDequantizeLinearにそのまま入るQuantizeLinearの数で、weight_dequantizeは、最初の入力が初期値であるDequantizeLinearの数です。islandsの応答は{"model", "compute", "wrapped", "float", "island_count", "islands"}です。computeは、QuantizeLinear・DequantizeLinearを除いた演算子の数です。islandsの各項目は{"nodes", "size", "op_types"}で、nodesとop_typesは並べ替えます。島のリストは、最初のノード名の順に並べ替えます。名前が空のノードは、연산자이름_자리번호と呼びます(プレースホルダーは、演算子名と位置番号です)。- 包まれているかの判定: ノードのすべての入力がDequantizeLinearの出力で、すべての出力がQuantizeLinearにだけ使われていれば、包まれています。その出力を使うノードが1つもない出力があれば、包まれていません。
boundaryの応答は{"model", "island_count", "crossings", "per_island"}です。per_islandの各項目は{"nodes", "in_dequantize", "out_quantize"}で、同じノードを何度も数えません。crossingsは、すべてを足した値です。diff <a> <b>の応答は{"a", "b", "op_delta", "quantize_delta", "dequantize_delta", "round_trip_delta", "island_delta", "float_delta"}です。各deltaは、bからaを引いた値で、op_deltaは0でない項目だけを入れます。scope.jsonの形は{"excluded_node": 이름, "models": {"full": {...}, "matmul_only": {...}, "excluded": {...}}, "diff_matmul_only": {...}, "diff_excluded": {...}}です(プレースホルダーは名前です)。modelsの各項目は{"quantize","dequantize","round_trips","island_count","float","crossings"}で、2つのdiffは、aをfull.onnxとして出したdiffの応答そのものです。fuse.jsonの形は{"before": {...}, "after": {...}, "removed_quantize": 정수, "removed_round_trips": 정수, "max_abs_error": 실수}です(プレースホルダーは、整数と実数です)。beforeはfull.onnx、afterはgemm_full.onnxについての{"op_types","quantize","dequantize","round_trips"}です。max_abs_errorは、fp32.onnxとgemm.onnxを同じ入力で動かしたときの最大絶対誤差です。- Gemmにまとめる条件:
MatMulの出力が、そのAddでだけ使われていて、Addのもう一方の入力が初期値のときだけ、まとめます。Gemm(A, B, C)は、デフォルト値のalpha=1.0、beta=1.0、transA=0、transB=0でA @ B + Cです。まとめたあとでonnx.checker.check_modelを通る必要があり、オリジナルと同じ答えを出す必要があります。 - 動的量子化のバージョンは、形式が違います。
MatMulIntegerとDynamicQuantizeLinearが出てきて、QuantizeLinear・DequantizeLinearはありません。島と境界を測る物差しはQDQバージョンのためのものなので、動的バージョンにはnodesだけを使うのが誠実です。 - 公式ドキュメント: 量子化 · グラフ最適化 · QuantizeLinear · DequantizeLinear · MatMulInteger · Gemm
- よくある間違い: ファイルサイズだけを見て量子化できたと言うこと、QDQバージョンでMatMulが見えるからといって量子化されていないと言うこと、同じDequantizeLinearを何度も数えること、動的バージョンにQDQの物差しを当てることです。
- このラボは、時間・スループットを測りません。数えることだけを扱います。
同じモデルを5通りで出力する
/root/ops/gen_models.pyを作成して実行し、/root/opsの下にfp32.onnx・full.onnx・matmul_only.onnx・excluded.onnx・dynamic.onnxを作ってください。
onnx.helperでMatMul・Add・Reluをつなげ、ノードごとに名前を付けてください。名前があってこそ、あとでnodes_to_excludeで1つを除けます。静的量子化にはCalibrationDataReaderが必要で、同じキャリブレーションデータを何度も使うときは、リーダーをそのたびに新しく作ってください。一度読み切ったリーダーは、もう一度は返してくれません。
何が入っているかから数える
/root/ops/graphscan.pyにnodes <model>を作成して、演算子のcensusと、初期値のデータ型のcensusを出力させてください。
初期値のデータ型は、onnx.TensorProto.DataType.Name(init.data_type)で名前が得られます。静的QDQバージョンと動的バージョンを並べて数えると、形式がどれほど違うかがすぐにわかります。動的バージョンには、MatMulがそもそもありません。
Q/DQの往復を数える
qdq <model>を追加して、quantize・dequantize・round_trips・weight_dequantizeを数えさせてください。
重みに付くDequantizeLinearには、対になるQuantizeLinearがありません。重みはすでにint8でファイルに入っているからです。そのため、DequantizeLinearの数からその分を引いて初めて、活性化値の往復数と合います。
floatのまま残った島を見分ける
islands <model>を追加して、int8に包まれた演算子とfloatのまま残った演算子を分け、つながったものどうしをまとめて、島として出力させてください。
先に、生産者・消費者の表を作っておくと、残りが楽になります。包まれているかの判定は、参考のセクションの規則をそのまま使ってください。島をまとめるときは、floatノードの間のつながりだけをたどります。QやDQを通ったら、別の島です。
島ごとに境界を数える
boundary <model>を追加して、島ごとに入ってくるDequantizeLinearの数と、出ていくQuantizeLinearの数を数え、全体の境界の数を出力させてください。
同じDequantizeLinearが、1つの島の2つのノードに入ることがあります。ノードごとに数えず、集合にまとめて数えてください。島の出力がグラフの出力にそのまま出ていく場合、出ていくQuantizeLinearが0になることがあります。
対象範囲を絞ると何が変わるのか
diff <a> <b>を追加して、2つのモデルの差を出力させ、3つのバージョンの数値と2つのdiffを、参考のセクションの形で書いてください(出力先: /root/ops/scope.json)。
オプションを渡したからといって、そのとおりになったと信じないでください。ノード名で除いたバージョンでは、そのノードの重みがfloatの初期値として残っていて、演算子の種類で絞ったバージョンでは、その種類以外の演算子が丸ごと島になります。2つの絞り込みは、グラフに残す痕跡が違います。
モデルを直して往復を減らす
MatMulとAddをGemm1つにまとめて、モデルを作ってください(出力先: /root/ops/gemm.onnx)。それを同じ設定で量子化したモデルも作り(出力先: /root/ops/gemm_full.onnx)、2つのバージョンの数値と最大絶対誤差を書いてください(出力先: /root/ops/fuse.json)。
まとめる条件を守ってください。MatMulの出力が、そのAddでだけ使われていて、Addのもう一方の入力が初期値のときだけ、まとめます。まとめたあとは、onnx.checker.check_modelを通る必要があり、オリジナルと同じ入力で動かして、答えが同じかどうかを必ず確認してください。値が変わったなら、まとめ方が間違っています。
グラフで答えられるように書く
/root/ops/ops_report.mdを、## 무엇이 int8 로 갔나 ## float 로 남은 섬 ## 범위를 좁히면 무엇이 달라지나 ## 모델을 고쳐 얻은 것の4つのセクションで書いてください(韓国語の見出しは、順に「何がint8に行ったか」「floatのまま残った島」「対象範囲を絞ると何が変わるか」「モデルを直して得たもの」という意味です)。島の数と、消えたQ/DQの数が、数字で入っている必要があります。
このレポートは、「なぜ期待したほど減らなかったのか」という質問に答える文章です。数えてみた5つの数字(演算子のcensus・Q/DQの往復・島の数・島のサイズ・境界の数)をそのまま書けば、答えになります。時間を測っていないことも書いてください。