このファイルは何を約束しているのか — エクスポートで固まるもの
目標
onnx.helperでMLPを自分で組み立てて/root/onnxq-export/mlp.onnxを作り、ONNXファイルに固定されているものを読み取るツール/root/onnxq-export/modelmeta.pyを作ります。バージョン番号を差し替えながらonnx.checkerとonnxruntimeに順に尋ね、チェッカーは通るのにランタイムが拒否する箇所を自分で見つけて、文書に残します。
なぜ重要なのか
ONNXファイルはグラフだけを持っているのではありません。どの演算子セットのバージョンで書いたか、IRバージョンはいくつか、誰が作ったか、何が入力で何がすでに決まった重みかが、エクスポートした瞬間に一緒に固定されます。受け取る側はその決定を変えられず、変えるには、もう一度エクスポートし直す必要があります。
そのため、「私たちのサーバーで開かない」という報告の原因は、たいてい変換オプションではなく、ファイルの冒頭にあります。バージョン番号を下げて保存し直しても、演算子の定義がそれに合わせて下がるわけではないので、チェッカーは通すのにランタイムだけが拒否するファイルができてしまいます。エラーメッセージが「バージョンが低い」ではなく「この演算子の実装がない」と出るため、原因がさらに見えにくくなります。
onnx.checkerも1層ではありません。デフォルトの検査は構造だけを見て、full_check=Trueを渡して初めてシェイプ推論まで実行されます。行列積が成り立たないMatMulは、デフォルトの検査をそのまま通過します。そして、2つの検査のどちらも、登録されていないドメインの演算子を防げません。
採点ツールは、皆さんが書き出した文言を信じません。一時ディレクトリに採点ツールが自分で作ったONNXファイルを用意し、皆さんのツールを実際に実行して、同じファイルを採点ツールが読んで得た答えと照合します。シェイプ・名前・バージョン番号・活性化関数は、実行のたびに変わります。
ステップ
- /root/onnxq-export/build_mlp.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-export/mlp.onnx)。
- /root/onnxq-export/modelmeta.pyに
infoを作り、バージョン番号・producer・入出力・initializer・ノードを読み取らせてください。 infoにinput_overridesとruntime_inputsを追加し、initializerと入力の境界が見えるようにしてください。checkを追加し、onnx.checkerをデフォルトとfull_check=Trueの2回実行して、判定を別々に書かせてください。loadを追加し、onnxruntimeがセッションを開けるか、開けないならどんな例外かを書かせてください。stampを追加し、演算子はそのままにして、バージョン番号だけを差し替えて保存し直させてください。scanを追加し、このランタイムが開けるバージョンの範囲を実際に測らせ、自分のモデルの範囲を書き込んでください(書き込み先: /root/onnxq-export/opset_range.json)。- 引き継ぎ文書を作成してください(出力先: /root/onnxq-export/export_report.json、/root/onnxq-export/export_report.md)。
参考
- Pythonの場所: /opt/onnx-lab/bin/python。システムの
python3には、onnxもnumpyもありません。実行例:/opt/onnx-lab/bin/python /root/onnxq-export/modelmeta.py info /root/onnxq-export/mlp.onnx - このPodにはネットワークがありません。インストールはできず、あらかじめ用意されたモデルもありません。素材は自分で作ります。
- モデルの契約: 入力
xはFLOATで、軸は2つ、軸0はシンボル名(文字列)、軸1は8です。出力yはFLOATで、軸1は4です。ノードにはMatMul・Add・Reluが入り、重みはinitializerを2つ以上使って固定します。producer_nameは空にしません。opsetは7から26の間、ir_versionは13以下にします。 - 実行の契約:
modelmeta.py <명령> ...(プレースホルダーはコマンドです)。答えはJSONの1つの塊として標準出力に出します。成功すれば終了コード0、未知のコマンドなら2です。onnxruntimeが標準エラー出力に出す警告は答えではないので、標準出力だけをきれいに保ってください。 info <모델>(プレースホルダーはモデルです)の応答は次のとおりです。ir_versionは整数、producer_nameは文字列、opsetsはドメインをキーにしたオブジェクト、inputs・outputsは{"name", "elem_type", "dims"}のリスト、initializersは名前を並べ替えたリスト、nodesはop_typeのリストです。dimsの各軸は、固定なら整数、シンボルなら文字列、何もなければnullです。elem_typeは、onnx.TensorProto.DataType.Name(...)が返す名前(例: FLOAT)です。- ステップ3から、
infoの応答にinput_overrides(graph.inputとinitializerの両方にある名前を並べ替えたリスト)とruntime_inputs(セッションが実際に求める入力名、セッションを開けなければnull)が加わります。inputsには、initializerで埋められる名前を入れません。 check <모델>(プレースホルダーはモデルです)の応答は{"checker": "ok"|"error", "full_check": "ok"|"error", "message": 문자열}です(プレースホルダーは文字列です)。失敗した場合、messageには、捕まえた例外の最初の1行をそのまま入れます。load <모델>(プレースホルダーはモデルです)の応答は{"load": "ok"|"error", "error_type": 예외 클래스 이름 또는 null, "message": 문자열}です(プレースホルダーは、例外クラス名またはnullと、文字列です)。stamp <모델> <opset> <ir> <출력>は、デフォルトドメインのopsetとir_versionだけを変えて、別のファイルに保存します(プレースホルダーはモデルと出力です)。ノード・重み・producer_nameには手を触れません。応答は{"out", "opset", "ir_version", "nodes"}です。scan <모델>(プレースホルダーはモデルです)の応答は{"min_ok": 정수 또는 null, "max_ok": 정수 또는 null, "ok": 정수 목록, "failed": 정수 목록}です(プレースホルダーは、整数またはnullと、整数のリストです)。バージョン番号を1から27まで差し替えながら、セッションが開くかどうかだけを見ます(チェッカーの結果ではなく、セッションを開けるかどうかで判定します)。opset_range.jsonには、最低限min_okとmax_okを書きます。export_report.jsonには、model・ir_version・producer_name・opset・nodes・initializers・runtime_inputs・min_ok_opset・max_ok_opsetと、チェッカーは通るのにランタイムが拒否するバージョンを入れたchecker_ok_runtime_errorオブジェクト(opset・checker・load)を書きます。export_report.mdは、## 무엇을 내보냈나、## 판이 굳는 자리、## 검사기가 못 잡는 것、## 다음 사람에게 넘길 것の4つのセクションで書き(韓国語の見出しは、順に「何をエクスポートしたか」「バージョンが固定される箇所」「チェッカーが捕まえられないもの」「次の担当者に渡すもの」という意味です)、開けるバージョンの下限と上限を数字で書きます。- 公式ドキュメント: ONNX Concepts · ONNX Versioning · ONNX IR · ORT Compatibility · ORT Python API
- よくある間違い:
graph.inputを数えて入力の個数だと言うこと、デフォルトの検査だけを実行して通過したと報告すること、バージョン番号を下げれば演算子もそれに合わせて下がると信じること、ランタイムの警告を標準出力に混ぜてJSONを壊すことです。
モデルを自分で組み立てる
/root/onnxq-export/build_mlp.pyを作成して実行し、ファイルを作ってください(出力先: /root/onnxq-export/mlp.onnx)。入力xは[シンボル, 8]、出力yは[シンボル, 4]で、MatMul・Add・Reluで2層を積みます。重みはinitializerとして固定します。
helper.make_tensor_value_infoのシェイプのリストに文字列を入れるとその軸はシンボル名になり、整数を入れると固定されます。重みはnumpy_helper.from_array(배열, 이름)で作り(プレースホルダーは配列と名前です)、make_graphの5番目の引数に入れます。その名前をノードの入力に使い、make_graphの入力リストには入れないでください。保存する前に、onnx.checker.check_model(model, full_check=True)で一度ふるいにかけてみてください。
ファイルに固定されているものを読み取る
/root/onnxq-export/modelmeta.pyにinfo <모델>(プレースホルダーはモデルです)を作り、ir_version・producer_name・opsets・inputs・outputs・initializers・nodesをJSONで出力させてください。
onnx.load(path)がModelProtoを返します。model.opset_importはドメインとバージョン番号のリストで、デフォルトのドメインは空文字列です。軸は、dim_paramがあればシンボル、dim_valueがあれば整数、どちらもなければnullとして書きます。inputsからは、initializerの名前を除いてください。その値は、ファイルの中にすでに入っています。
重みは入力ではない
infoの応答に、input_overrides(graph.inputとinitializerの両方にある名前)とruntime_inputs(セッションが実際に求める入力名)を追加してください。セッションを開けなければ、runtime_inputsはnullです。
IR 4より前は、initializerを必ずgraph.inputにも宣言する必要がありました。そのため、古いツールが作ったファイルには、重みが入力リストにも一緒に書かれていて、その名前は「デフォルト値のある入力」という意味です。ランタイムに尋ねると、その名前を必須入力として要求しないことがすぐにわかります。セッションを開くコードは、例外を握りつぶしてnullを返してください。開けないファイルも、infoでは読み取れる必要があります。
チェッカーは1層ではない
check <모델>(プレースホルダーはモデルです)を追加し、onnx.checkerをデフォルトとfull_check=Trueでそれぞれ実行して、{"checker", "full_check", "message"}を出力させてください。失敗した場合は、捕まえた例外の最初の1行をそのままmessageに入れてください。
デフォルトの検査は構造だけを見ます。full_check=Trueを渡すとシェイプ推論まで実行され、行列積が成り立たないMatMulや、宣言した出力シェイプと推論されたシェイプが違う場合を捕まえます。例外のメッセージを作り上げずに、str(exc)の最初の1行をそのまま写してください。採点ツールが、自分で仕込んだ名前がその中にあるかどうかを見ます。
ランタイムに直接尋ねる
load <모델>(プレースホルダーはモデルです)を追加し、onnxruntimeのセッションを開いてみて、{"load", "error_type", "message"}を出力させてください。error_typeは捕まえた例外のクラス名で、開けたらnullです。
チェッカーを通ったファイルでも、ランタイムは拒否することがあります。登録されていないドメインの演算子、バージョン番号が低すぎてそのバージョンに定義がない演算子、ランタイムがまだ開けない高いバージョンがそうです。拒否の種類がそれぞれ違うので、例外のクラス名まで書いておけば、次の人が原因をすぐに切り分けられます。
バージョン番号だけを差し替える
stamp <모델> <opset> <ir> <출력>を追加し、デフォルトドメインのopsetとir_versionだけを変えて、別のファイルに保存させてください(プレースホルダーはモデルと出力です)。ノード・重み・producer_nameはそのままにしておく必要があります。
model.opset_importを順に見て、ドメインが空文字列の項目のversionを書き換え、なければ新しく追加します。これは変換ではなく、スタンプを押す作業です。演算子の定義が、それに合わせて下がることも、上がることもありません。その事実を、次のステップで目で確かめることになります。
このランタイムが開けるバージョンの範囲を測る
scan <모델>(プレースホルダーはモデルです)を追加し、バージョン番号を1から27まで差し替えながら、セッションが開くかどうかを測って、{"min_ok", "max_ok", "ok", "failed"}を出力させてください。そして、自分のモデルの範囲をmin_ok・max_okとして書き込んでください(書き込み先: /root/onnxq-export/opset_range.json)。
前のステップのstampとloadを、そのままつなげればできます。一時ディレクトリに出力して開いてみるだけでよいので、元のファイルには手を触れないでください。下限は、モデルが使う演算子がどのバージョンから定義されたかによって決まり、上限は、ランタイムがどこまで開けるかによって決まります。2つの数字の出どころが違うという点が、このステップの核心です。
引き継ぎを1枚に残す
/root/onnxq-export/export_report.jsonに、モデルから読み取った値とバージョンの範囲、そしてチェッカーは通るのにランタイムが拒否するバージョンを入れたchecker_ok_runtime_errorを書き、Markdownのレポートを4つのセクションで書いてください(出力先: /root/onnxq-export/export_report.md)。
checker_ok_runtime_errorのopsetは、開ける下限より低いバージョンであればかまいません。そのバージョンで出力したファイルをcheckとloadにそれぞれ尋ねて、実際に出た判定を書いてください。採点ツールも、同じファイルを作って確認し直します。レポートには下限と上限を数字で書く必要があります。そうすれば、受け取る側が自分のランタイムと見比べられます。