ノードが6個から2個へ — 最適化が変えたものを数える
目標
最適化が手を加える材料を集めた/root/onnxq-optimize/graph.onnxを自分で組み立て、ORTの4つの最適化レベルを実行してグラフをファイルに取り出し、ノードを数えるツール/root/onnxq-optimize/optlevel.pyを作ります。レベルごとに何が消えて何ができるかを書き、答えが同じかどうかを測り、取り出したファイルをほかのマシンに移してはいけない理由を、ドメイン名で証明します。
なぜ重要なのか
onnxruntimeは、セッションを開くときにグラフを書き直します。定数だけで計算できる部分を先に畳み込み、なくてもよいノードを削除し、よく出てくる形を1つにまとめます。何がどこまで変わるかは、最適化レベルが決めます。
量子化の前後で速度を比べるときに数字が合わない原因が、ここにある場合が多くあります。2つの測定の最適化レベルが違うと、その比較は量子化を測ったのではなく、最適化を測ったことになります。ベースラインをORT_DISABLE_ALLで固定しておけば、この揺れはなくなります。
もっと高くつく事故は、最適化されたファイルを保存してデプロイすることです。そのファイルには標準ではないドメインのノードが入っているので、ほかのランタイムが開けません。ところが、onnx.checkerはそのファイルを通します。チェッカーは知らないドメインを通り過ぎるからです。そのため、移植可能かどうかの根拠は、チェッカーではなく、ノードのドメインのリストです。
このラボは時間を測りません。このマシンはエミュレーションなので、同じ作業でも時間が2倍まで揺れます。そのため、構造がどう変わったかと、答えが同じかどうかだけを判定します。
採点ツールは、皆さんが書き出した文言を信じません。一時ディレクトリに採点ツールが自分で作ったグラフを用意し、皆さんのツールを実際に実行して、同じファイルを採点ツールが4つのレベルで最適化して得た値と照合します。シェイプ・重み・乱数シードは、実行のたびに変わります。
ステップ
- /root/onnxq-optimize/build_graph.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-optimize/graph.onnx)。
- /root/onnxq-optimize/optlevel.pyに
nodesを作り、現在のグラフのノードと個数を読み取らせてください。 optimizeを追加し、指定されたレベルで最適化したグラフをファイルに取り出させてください。levelsを追加し、4つのレベルをすべて実行して、ノードを数えて並べさせてください。fusionを追加し、レベルごとに何が消えて何ができたかを書かせてください。equalを追加し、指定されたシードで作った同じ入力を4つのレベルに入れて、答えが同じかどうかを測らせてください。portableを追加し、取り出したファイルが標準の演算子だけを使っているかどうかを判定させてください。- レポートを作成してください(出力先: /root/onnxq-optimize/opt_report.json、/root/onnxq-optimize/opt_report.md)。
参考
- Pythonの場所: /opt/onnx-lab/bin/python。システムの
python3には、onnxもnumpyもありません。実行例:/opt/onnx-lab/bin/python /root/onnxq-optimize/optlevel.py levels /root/onnxq-optimize/graph.onnx - このPodにはネットワークがありません。素材は自分で作ります。
- グラフの契約: 入力
xはFLOATで、軸は[シンボル, 6]、出力yは[シンボル, 5]です。ノードにはMatMul・Add・Relu・Identity・Mulが入っている必要があり、入力がすべてinitializerであるノードが1つ以上ある必要があります(それが畳み込まれる定数です)。最適化をすべてオンにしたとき、ノード数が減る必要があります。 - 実行の契約:
optlevel.py <명령> ...(プレースホルダーはコマンドです)。答えはJSONの1つの塊として標準出力に出します。成功すれば終了コード0、未知のコマンドなら2です。 - レベル名は、
ORT_DISABLE_ALL・ORT_ENABLE_BASIC・ORT_ENABLE_EXTENDED・ORT_ENABLE_ALLの4つです。 nodes <모델>の応答は{"nodes": [{"op_type", "domain", "name"}...], "count": {op_type: 개수}, "initializers": [이름 정렬]}です(プレースホルダーは、モデル、個数、並べ替えた名前です)。nodesの順序は、ファイルに書かれた順序のままです。optimize <모델> <단계> <출력>は、SessionOptions.graph_optimization_levelをそのレベルに設定し、optimized_model_filepathを出力パスに設定してセッションを開いたあと、保存されたファイルを読み直して、{"level", "out", "nodes": [op_type...], "count", "initializers"}を出力します(プレースホルダーは、モデル、レベル、出力です)。levels <모델>(プレースホルダーはモデルです)の応答は、レベル名をキーにして{"nodes": [op_type...], "count": {...}}を入れたオブジェクト4つです。fusion <모델>(プレースホルダーはモデルです)の応答は、レベルごとに{"removed": 원본에 있었는데 사라진 op_type 정렬, "added": 새로 생긴 op_type 정렬, "total": 노드 수}です(プレースホルダーは、元のグラフにあったのに消えたop_typeを並べ替えたもの、新しくできたop_typeを並べ替えたもの、ノード数です)。equal <모델> <씨앗> <행수>は、numpy.random.RandomState(씨앗).standard_normal((행수, 입력너비))をfloat32で作り、4つのレベルすべてに入れます(プレースホルダーは、モデル、シード、行数、入力の幅です)。応答は{"seed", "rows", "levels": {단계: {"sum", "max", "max_abs_diff"}}, "max_abs_diff"}です(プレースホルダーはレベルです)。sumとmaxは、出力全体の合計と絶対値の最大で、max_abs_diffは、ORT_DISABLE_ALLの出力との最大絶対差です。小数点以下6桁まで書けば十分です。portable <모델>は、ORT_ENABLE_ALLで取り出したファイルを読み、{"nondefault_domains": 노드 도메인 가운데 표준이 아닌 것 정렬, "opset_domains": opset_import 의 도메인 정렬, "checker": "ok"|"error", "portable": 불리언}を出力します(プレースホルダーは、モデル、ノードのドメインのうち標準でないものを並べ替えたもの、opset_importのドメインを並べ替えたもの、ブール値です)。標準のドメインは、空文字列とai.onnxです。opt_report.jsonには、levels(4つのレベルのnodes)・nondefault_domains・portable・equality(seed・rows・max_abs_diff)を書きます。opt_report.mdは、## 무엇이 줄었나、## 어느 단계에서 무엇이 바뀌나、## 답은 같은가、## 이 파일을 옮겨도 되나の4つのセクションで書き(韓国語の見出しは、順に「何が減ったか」「どのレベルで何が変わるか」「答えは同じか」「このファイルを移してよいか」という意味です)、最適化前のノード数と、残ったドメイン名を本文に書きます。- ORTは、
ORT_ENABLE_ALLで取り出すとき、同じ環境でだけ使うようにという警告を標準エラー出力に出します。SessionOptions.log_severity_levelを2にするとその警告が見え、3にすると静かになります。警告は標準エラー出力に出るので、標準出力のJSONは壊れません。 - 浮動小数点: レベルが変わると、カーネルと計算の順序が変わります。float32は有効桁が7桁なので、最後の桁が揺れることがあります。同じかどうかを見るときは、相対誤差1e-5程度の許容誤差を置いて、その根拠を書いておいてください。
- 時間は測りません。このマシンはエミュレーションなので、同じ作業でも時間が2倍まで揺れます。
- 公式ドキュメント: Graph optimizations · ORT Python API · Gemm · Execution Providers
- よくある間違い: 最適化されたファイルをそのままデプロイすること、チェッカーの通過を移植可能の根拠にすること、最適化レベルが違う2つの測定を比べること、ノード数をそのまま速度として読むことです。
最適化が手を加える材料を集める
/root/onnxq-optimize/build_graph.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-optimize/graph.onnx)。入力がすべてinitializerであるノードを1つ、Identityを1つ、そしてMatMul・Add・Relu・Mulを入れる必要があります。
定数畳み込みを見るには、2つのinitializerを足すノードを入れて、その結果をあとで使えばよいです。Identityは何もしないので、なくてもよいノードの例としてぴったりです。MatMulのあとにAddを置き、その次にReluを置くと、融合される形になります。保存する前に、onnx.checker.check_model(model, full_check=True)でふるいにかけてみてください。
現在のグラフを数えておく
/root/onnxq-optimize/optlevel.pyにnodes <모델>(プレースホルダーはモデルです)を作り、{"nodes", "count", "initializers"}を出力させてください。nodesの各項目には、op_type・domain・nameを入れます。
ノードのdomainは、標準の演算子なら空文字列です。今はすべて空文字列でしょうが、最適化されたファイルを開くと、そうではないものが出てきます。そのときに比べるために、今のものを先に数えておく必要があります。順序は、ファイルに書かれたままにしてください。
最適化されたグラフをファイルに取り出す
optimize <모델> <단계> <출력>を追加し、そのレベルで最適化したグラフを出力パスに保存して、保存されたファイルを読み直してノードを数えて出力させてください(プレースホルダーは、モデル、レベル、出力です)。
SessionOptions.optimized_model_filepathにパスを渡すと、セッションを開く間に書き直したグラフが、そのパスに保存されます。セッションを開かなければ、ファイルもできません。ORT_DISABLE_ALLで取り出したものがベースラインで、元のファイルと同じノードが出るのが正常です。
4つのレベルを並べる
levels <모델>(プレースホルダーはモデルです)を追加し、4つのレベルをすべて実行して、レベルごとに{"nodes", "count"}を入れたオブジェクトを出力させてください。
一時ディレクトリにレベルごとに1つずつ取り出して、読めばよいです。終わったら削除してください。4行を並べると、どのレベルで何が起きるかが一目でわかります。ドキュメントを読むよりこちらのほうが速いです。
何が消えて何ができたか
fusion <모델>(プレースホルダーはモデルです)を追加し、レベルごとに{"removed", "added", "total"}を出力させてください。removedは元のグラフにあったのに消えたop_type、addedは新しくできたop_typeです。
元のop_typeの集合と、各レベルのop_typeの集合を比べればよいです。定数だけで計算されていたノードは消え、その結果がinitializerとして収まります。MatMulのあとのAddは、Gemm 1つにまとめられ、次のレベルでは活性化まで飲み込んだ名前が出てきます。その名前のドメインが何かにも、注目してください。
グラフは変わっても、答えは同じか
equal <모델> <씨앗> <행수>を追加し、そのシードで作った同じ入力を4つのレベルすべてに入れて、{"seed", "rows", "levels", "max_abs_diff"}を出力させてください(プレースホルダーは、モデル、シード、行数です)。
入力は、numpy.random.RandomState(씨앗).standard_normal((행수, 입력너비))をfloat32にして作ります(プレースホルダーは、シード、行数、入力の幅です)。入力の幅は、モデルの宣言の軸1から読んでください。レベルごとに、出力全体の合計と絶対値の最大を書き、ベースラインとの最大絶対差も書きます。正確に同じになることを期待せず、許容誤差を決めておいてください。float32の有効桁は7桁しかありません。
このファイルを移してよいか
portable <모델>(プレースホルダーはモデルです)を追加し、ORT_ENABLE_ALLで取り出したファイルを読んで、{"nondefault_domains", "opset_domains", "checker", "portable"}を出力させてください。
標準のドメインは、空文字列とai.onnxだけです。それ以外のドメインを持つノードが1つでもあれば、そのファイルはその環境専用です。onnx.checkerはそういうファイルも通すので、チェッカーの結果とドメインのリストを一緒に書いておいてください。2つが食い違うことが、このステップの核心です。log_severity_levelを2にすると、ORTが保存しながら出す警告も見えます。
4つのレベルを1枚で報告する
/root/onnxq-optimize/opt_report.jsonにlevels・nondefault_domains・portable・equalityを書き、## 무엇이 줄었나、## 어느 단계에서 무엇이 바뀌나、## 답은 같은가、## 이 파일을 옮겨도 되나の4つのセクションでMarkdownのレポートを書いてください(出力先: /root/onnxq-optimize/opt_report.md。韓国語の見出しは、順に「何が減ったか」「どのレベルで何が変わるか」「答えは同じか」「このファイルを移してよいか」という意味です)。
採点ツールが同じグラフを自分で4つのレベルで最適化してノードを数え、equalityに書かれたシードと行数でもう一度動かして、差を測ります。そのため、実際に測って書いてください。レポートには、最適化前のノード数と、残ったドメイン名を、数字と名前のままで書く必要があります。そうすれば、受け取る側が判断できます。