エクスポートの瞬間に固まるもの — opset・IR・initializer
一言でいうと
ONNXファイルはグラフだけを持っているのではなく、どのバージョン(opset)で書いたか、どのIRバージョンで記したか、何が入力で何がすでに決まった値かを一緒に固定します。この4つはエクスポートした瞬間に決まり、受け取る側はその決定を変えられません。
なぜ必要なのか
量子化が失敗したという報告は、たいてい変換コマンドから始まりません。「私たちのサーバーではこのファイルが開きません」という報告から始まります。ファイルは問題なさそうに見え、onnx.checkerも何も言わずに通します。それなのに、ランタイムはセッションを開く段階で止まります。
こういう場面で最も時間を取られるのは、原因をファイルの外に探すことです。変換オプションを変えてみて、ランタイムのバージョンを上げてみて、ハードウェアを疑います。実は、ファイルの冒頭の数バイトに答えが書かれています。
ファイルに固定される4つのもの
1つ目は演算子セットのバージョンとドメインです。モデルはopset_importに、ドメインごとにバージョン番号を書いておきます。ドメインが空文字列なら標準の演算子(ai.onnx)で、com.microsoftのような別のドメインは、特定のランタイムの拡張です。バージョン番号は「このファイルの中の演算子を、どのバージョンの定義で読むか」という指示であって、性能の設定ではありません。
2つ目はIRバージョンです。グラフを入れる器の形式のバージョンです。ONNX VersioningがopsetとIRを別々に番号付けする理由がここにあります。演算子の定義が変わる速度と、ファイル形式が変わる速度は違うからです。実際、IR 4より前はinitializerを必ずgraph.inputにも一緒に宣言する必要がありましたが、それ以降はその必要がありません。そのため、古いツールが作ったファイルには、今でも重みが入力リストに一緒に書かれています。
3つ目はproducer情報です。producer_nameとproducer_versionは、誰がこのファイルを作ったかを残します。何の機能も果たしませんが、事故が起きたときに最初に見る行です。空のファイルが届いたら、まずそれが問題です。
4つ目はinitializerと入力の境界です。重みはgraph.initializerに入り、実行時に人が入れる値はgraph.inputに残ります。両方に名前が書かれている場合は、それは「デフォルト値のある入力」という意味で、ランタイムはその名前を必須入力として要求しません。そのため、ファイルだけを見て入力の個数を数えると間違えます。ONNX Conceptsが、この2つを分けて説明しています。
チェッカーが捕まえるものと捕まえられないもの
onnx.checker.check_model(model)は、デフォルトでは構造を見ます。ノードがトポロジカル順に並んでいるか、指している名前が存在するか、必須フィールドがあるか、といったことです。ここにfull_check=Trueを渡すと、シェイプ推論まで実行します。この違いは実務で大きく効きます。行列積が成り立たないシェイプのMatMulは、デフォルトの検査をそのまま通過し、full_checkでようやく引っかかります。
そして、2つの検査のどちらも捕まえられないものがあります。登録されていないドメインの演算子は、チェッカーが通します。チェッカーは、知らないドメインを「誰かの拡張」と見なして通り過ぎるからです。バージョン番号を低くして保存したファイルも、同じように通ります。どちらの場合も、ランタイムで止まります。
onnx.checker(기본) 구조만
onnx.checker(full) 구조 + 모양 추론
onnxruntime 세션 열기 구조 + 모양 + 이 런타임에 그 커널이 있는가
このコードブロックの韓国語の3行は、順に、構造だけを見る、構造とシェイプ推論を見る、構造とシェイプに加えてこのランタイムにそのカーネルがあるかを見る、という意味です。3行目が最も狭く、デプロイが実際に求めているのは3行目です。
現場での姿
1つ目は「バージョンを下げてください」という依頼です。受け取る側のランタイムが古いからと、opsetの番号だけを下げて保存し直すことがよくあります。しかし、バージョン番号はスタンプにすぎず、演算子の定義がそれに合わせて下がるわけではありません。下げたバージョンにその演算子の定義がなければ、チェッカーは通し、ランタイムが拒否します。エラーメッセージも「バージョンが低い」ではなく「この演算子の実装が見つからない」と出るので、原因が見えません。
2つ目は、エクスポートする側が使えるバージョンが、実行する側が開けるバージョンより広いことです。ライブラリは最新のバージョンで書けるのに、ランタイムはまだそのバージョンを開けません。ONNX Runtime Compatibilityが、ランタイムのバージョンごとに開けるopsetの範囲を表にしている理由です。そのため、「最新のバージョンでエクスポートしてください」というアドバイスは、それ自体が危険です。
3つ目は、重みが入力に見えることです。古いツールが作ったファイルを開くと、入力が5つに見えるのに、ランタイムは1つだけを求めます。この違いを知らずに5つの入力を埋めるコードを書くと、そのコードは重みを呼び出しのたびに新しく押し込むことになります。
4つ目は、チェッカーの通過をデプロイの根拠にすることです。「checker通過」をリリース条件に入れているチームは多くあります。その条件は必要条件であって、十分条件ではありません。デプロイのゲートには、受け取る側と同じランタイムのバージョンで、セッションを実際に開いてみるステップが入る必要があります。
実務で本当に大切なこと
- ファイルから読み取った値で話します。 opset・ir_version・producer・入力リストは、推測せずに開いて書きます。
- 入力の個数はランタイムに聞きます。
graph.inputを数えず、セッションが求める名前を数えます。 - バージョンの範囲を測って渡します。 このファイルが開けるバージョンの下限と上限を自分で測り、引き継ぎ文書に書きます。
- ゲートはセッションを開くところまで行います。 チェッカーだけで通しません。
次のラボですること
onnx.helperで2層のMLPを自分で組み立て、そのファイルに固定されているものを読み取るツールmodelmeta.pyを、ステップごとに育てます。initializerを入力としても宣言した古い形式のファイルに出会って、入力と重みを分けてみて、デフォルトの検査とfull_checkの違いを自分で測り、バージョン番号だけを差し替えながら、ランタイムが開ける範囲の下限と上限を探します。採点ツールは、毎回異なるシェイプ・名前・バージョン番号・活性化関数で自分のファイルを作り、皆さんのツールを実際に動かして答えを照合します。