GPU で計算する ONNX モデル(試験中)
この章は試験中です。 [OnnxGpu("…")] は、画像をバッファへ入れ、結果を別のバッファで受け取ります。
例にするモデル
Section titled “例にするモデル”| モデルのファイル | Sharpen.cs と同じフォルダの filter.onnx |
| 入力 | image — 64×64 の画像 3 成分(形は [1,3,64,64]) |
| 出力 | out — 同じ大きさの画像 3 成分(形は [1,3,64,64]) |
| 層 | 畳み込みが 4 つ。間の channel は 8 → 16 → 16 と増えます |
| 生成されるファイル | Sharpen.Filter.Model.cs |
| 組み込む手順 | 組み込む手順のとおりです |
属性を書いたスクリプト
Section titled “属性を書いたスクリプト”using Tsukimi;using UnityEngine;
// 属性の引数は、このスクリプトから見たモデルのファイルの経路です。[OnnxGpu("filter.onnx")]public partial class Sharpen : TsukimiBehaviour{ // 通す元の画像と、結果を映す面。Inspector で挿します。 public Texture source; public Renderer display;
void Start() { // バッファを作ります。大きさはモデルの側から決まるので、指定しません。 LoadFilter();
// 元の画像を入力のバッファへ入れます。大きさが違うときは伸び縮みして入ります。 Gpu.Load(FilterInput, source); }
void Update() { // 1 回ぶん計算します。層の数だけカーネルが順に走ります。 RunFilter();
// 結果は FilterOutput に残るだけなので、そのまま面へ映します。 Gpu.Show(FilterOutput, display); }}生成されたファイル
Section titled “生成されたファイル”層の中身を省いた抜き書きです。
using Tsukimi;using UnityEngine;
public partial class Sharpen{ /// Input. Write values from 0 to 1 (mapped to 0 to 1). public GpuBuffer2D FilterInput; /// Output. Read values from 0 to 1 (corresponding to -2.39134 to 1.3269). public GpuBuffer2D FilterOutput;
// 層と層の間のバッファ。名前はモデルの名前で始まります。 private GpuBuffer2D Filterb1; // …(層の数だけ続きます)…
// 層ごとの重みの表。 public Vector4[] Filterw0; // …(層の数だけ続きます)…
/// Loads the weights and creates the buffers. Call once from <c>Start</c>. private void LoadFilter() { // 入口は 64×64。channel が 4 以下なので、横に伸びません。 FilterInput = Gpu.Buffer(64, 64); // 次の層は channel が 8=4 成分の帯が 2 本なので、横が 2 倍になります。 Filterb1 = Gpu.Buffer(128, 64); // …(層の数だけ続きます)… FilterOutput = Gpu.Buffer(64, 64); }
// 層ごとに 1 つ、カーネルが生まれます。 [Kernel] static Color4 FilterLayer0(KernelId id, GpuBuffer2D prev, [Capacity(128)] Vector4[] t) { // …(畳み込みの積和が並びます)… }
/// Runs the model once. Write <c>FilterInput</c> before calling, then read <c>FilterOutput</c>. private void RunFilter() { Gpu.Run(nameof(FilterLayer0), Filterb1, FilterInput, Filterw0); // …(層の数だけ続きます)… }}生成される名前の一覧
Section titled “生成される名前の一覧”| 生成される名前 | 説明 |
|---|---|
FilterInput | 入力のバッファ(GPU カーネルの実行と同じ型)。セルに代入する値の範囲は、生成されたファイルの行き先の欄に書いてあります |
FilterOutput | 出力のバッファ。8 ビットのモデルではセルを 0 から 1 で、実数のモデルでは値をそのまま読み出します |
LoadFilter() | バッファを作成し、重みを読み込みます。Start から 1 度だけ呼びます |
RunFilter() | 1 回ぶん計算します |
FilterSteps | 1 回ぶんの計算の段の数です |
RunFilter(int first, int count) | first 番から first + count - 1 番の段だけを計算します。全部の段を順に計算すれば、何フレームに分けても RunFilter() と同じ結果になります。FilterInput は最後の段の後に書き、FilterOutput も最後の段の後に読みます。1 つの段は描画 1 回なので、段をフレームに分けるだけで、1 つの段の重さは変わりません |
FilterLayer0 | 層ごとのカーネル。Gpu.Run から起動されるので、直接は呼びません |
FilterInput1 / FilterInput2 … | 入力が 2 本以上ある実数のモデルだけで生成されます。番号はモデルの入力の並び順です |
SetFilterTokens(int[] ids) | トークンの番号を受け取るモデルだけで生成されます。FilterInput の代わりに、番号を位置ごとに 1 つずつ書き込みます |
FilterStyles | 経路を 2 本以上並べたときだけ生成されます。並べた数です |
UseFilter(int style) | 経路を 2 本以上並べたときだけ生成されます。使う重みを、並べた順の番号(0 から)で選びます。LoadFilter() の直後は 0 です |
| 値の範囲 | 8 ビットのモデルでは、セルに 0 から 1 の値を代入すれば、そのまま正しく渡ります。8 ビットの刻みがモデルの受け取る範囲とちょうど重なるためです。入力を 16 ビットの小数で受け取るモデルと実数のモデルでは、値をそのまま代入します。どちらになるかはモデルが決め、生成されたファイルの入力の欄に書いてあります |
| 出力の意味 | 出た値が実際にどの範囲を指すかは、生成されたファイルに書いてあります |
| 8 ビットの形式 | 層の間の値は符号なし(uint8)でも符号つき(int8)でもかまいません。重みは符号つきです |
| 8 ビットのモデルの入力 | [N, C, H, W] の画像です(N は束で、1 から 16・束が 2 以上のモデル)。軸の数が 4 でない入力は、入力を名指してエラーになります(実数のモデルの入力は実数のモデルの「束と形状」にあります) |
| 8 ビットの畳み込みの尺度 | 入力か出力の尺度が channel ごとの畳み込みはエラーです。重みの尺度は出力の channel ごとのもの(重みの DequantizeLinear の axis が 0)だけを受け取り、入力の channel ごとのもの(axis が 0 以外)はエラーです。axis の既定は 1 なので、出力の channel ごとの尺度には axis=0 を書きます |
| 読み出し | 結果はバッファに出ます。数として使うときは Gpu.Texture で取り出し、実行環境の非同期の読み戻しへ渡します |
| 重みの差し替え | 同じグラフで重みだけが違うモデルは、[OnnxGpu("a.onnx", "b.onnx")] のように経路を並べると 1 つにまとまります。バッファとカーネルは 1 組のままで、重みだけを並べた数だけ持ちます |
実数のモデル
Section titled “実数のモデル”量子化の演算を 1 つも含まないモデルも、GPU で計算する属性で焼き込めます。
| 数の保持形式 | 16 ビットの小数で計算します。重みも、ファイルを生成するときに 16 ビットへ丸めます |
| 入力と出力 | 値をそのまま代入し、そのまま読み出します。バッファは GpuFormat.Half です |
| 値の並び | セル 1 つに channel が 4 つ入ります。どの軸がどの行とセルに入るかは、生成されたファイルの入力と出力の宣言の上に書いてあります |
| トークンの番号 | 語彙の表を番号で参照するモデル(言葉のモデル)は、番号を SetFilterTokens で書き込みます。位置の数は書き出したときに決まり、文字列と番号の変換は含みません |
| 入力が 2 本以上 | FilterInput・FilterInput1・FilterInput2 と、モデルの入力の並び順に生成されます |
| 重みだけ 8 ビットか 4 ビット | 重みを DequantizeLinear で戻す形なら、重みをその大きさのまま持ちます。重みの大きさは 8 ビットで半分、4 ビットでおよそ 4 分の 1 になり、組ごとの尺度を使うと組ごとに 16 ビットの尺度が加わります。層の間の値は 16 ビットのままで、ビット数は層ごとに違ってもかまいません |
| 重みの整数と尺度 | 整数は int8・uint8・int4・uint4 です。尺度は全体で 1 つか、出力の channel ごとか、block_size(4 の倍数)個の組ごとです。組は MatMul・Gemm では入力の軸に、Conv では入力の channel に沿います |
MatMulNBits | com.microsoft の MatMulNBits は、同じ 4 ビットの重みの積として読みます。accuracy_level は読まず、掛ける値は 16 ビットのまま計算します。float のゼロ点・g_idx・bias の入力・4 以外の bits はエラーになります |
| 16 ビットで持つ重み | 層がその大きさで使えない重み(出力の channel に沿った組・group のある Conv・ConvTranspose・Gather の表など)は、実数へ戻して 16 ビットで持つので、重みの大きさは減りません。コンパイルは通り、TUKI0123 の警告が層ごとに 1 件、層の名前と理由を示します |
| 束と形状 | 軸の長さは書き出したときに決まっている必要があります。入力の軸の数は 2 から 4([N, C, H, W]・[N, T, D]・[N, D]。N は束で、1 から 16・束が 2 以上のモデル)で、MatMul・Gemm が重み(2 本目の入力)としてだけ読む入力は 1 行です。外れる入力は、軸の数か行の数を示してエラーになります。形の注釈が 1 つも無いモデルは、形の推論を通してから保存してください。モデルの層は実行する順に並んでいなくてもかまいません(元の実行環境と同じく、変換が並べ直します)。互いの出力を輪になって読む層はそのように並べられないので、層を示してエラーになります(元の実行環境もそのようなモデルをエラーにします) |
| 形・軸・大きさを渡す入力 | Reshape・Expand の shape、Squeeze・Unsqueeze・縮約の axes、Tile の repeats、Resize の scales・sizes、Slice の starts・ends・axes・steps は定数です。モデルの入力か、そこから計算した値を渡すものは、層と入力を名指してエラーになります。これらの値から Reshape・Squeeze・Unsqueeze・Flatten・Expand・Tile・Slice・Split・最近傍の Resize が出す出力の形は、モデルがその出力に記録した形と一致する必要があります(書き出し元の実行環境は値に従い、違う記録を捨てるため)。一致しないモデルはエラーで、形の推論をかけ直すと記録が書き直されます |
| 読まれない入力 | 層が読むモデルの入力を、変換したモデルが読まないことになる場合は、入力を名指してエラーになります |
| 要素の型 | テンソルは float32 です(Gather が読む整数のトークンの番号は除きます)。ほかの型(整数・float16・float64 など)のテンソルは、型を名指してエラーになります。畳み込みの重みは定数で、モデルの入力や計算した値を重みにしたものはエラーになります |
| 16 ビットの上限 | 16 ビットの小数が持てる大きさは 65504 までで、超える値は無限大でなく 65504(負なら -65504)になります。超えうる層(ReduceProd・2 乗の和と分散・累乗・Exp のように入力より速く大きくなる関数・大きさの分からない値どうしの積・計算した値で割る層・入力の範囲が分かっていても超える層)は、コンパイルは通り、TUKI0135 の警告が名指します。同じ理由の層は 1 件にまとめ、入力がどこまで大きくなると超えるかを示します |
| 値が跳ぶ演算 | 入力と層の間の値は、GPU が 16 ビットへ丸めて書き込みます(PC のグラフィックスカード 1 台で測ると、0 の方向への切り捨てでした)。値が跳ぶ点のすぐ先の値は丸めで跳ぶ点の手前へ戻ることがあり、Ceil・Floor・Round・Sign・Mod・ThresholdedRelu・Shrink は元のモデルと違う答えになります(演算ごとの違いはONNX モデルの演算(試験中)にあります)。すぐ先とは 16 ビットの 1 刻みの内で、1 刻みは 5 の近くでおよそ 0.004、100 の近くでおよそ 0.06、1024 から 2048 では 1 です |
| 重みの別ファイル | 重みをモデルの隣の別ファイルに書いたモデル(最近の PyTorch の既定の書き出し)は、そのファイルをモデルの隣に置けば読みます |
| NaN | 入力の NaN は、元のモデルのようにはそのまま出力へ伝わりません。表で読む層は 0 の位置の値(Sigmoid なら 0.5)になり、Relu・Max・Min・Clip の比較はシェーダーのコンパイラが max と min の命令にするので、NaN の代わりにもう一方の値か上下限になることがあります。生成するコードでは変えられないので、入力に NaN を入れないでください |
| テンソルの定数 | 計算した値と組み合わせる、値が 2 つ以上の定数(Add・Sub・Mul・Div の重みのテンソルと、それに読み替えた BatchNormalization)も 16 ビットで持ちます。値が 1 つの定数はそのまま持ちます。掛ける定数と、計算した値で割られる定数が、65504 を超えるか 16 ビットで 0 になる(およそ 6e-8 未満)のに結果は 16 ビットで持てる形は、層と値を名指してエラーになります(割る定数は、その逆数で見ます)。足し算と引き算の定数は、大きさが 65504 を超えるとエラーです |
Softmax の前の覆い | Softmax の直前で足す -65504 以下の覆いは、Softmax が正規化するどの行にも 16 ビットで持てる値が 1 つ以上残るなら受け取ります(覆った位置はどちらでも 0 になります)。1 行を全部覆うものはエラーです(元のモデルはその行を一様にするため)。16 ビットで表せないほど小さい足し算と引き算の定数と、Max・Min・Where の定数は受け取ります(結果の違いは 16 ビットの 1 刻み未満です) |
並びの行(生成されたファイルの抜き書き)
Section titled “並びの行(生成されたファイルの抜き書き)” /// Input. Write the values as they are (not mapped to 0 to 1). /// Layout (shape [1, 4, 8, 8]): y is the index along axis 2, x the index along axis 3; channel c is the index along axis 1; the value is component c % 4 of cell (x + (c / 4) * 8, y). public GpuBuffer2D FilterInput;束が 2 以上のモデル
Section titled “束が 2 以上のモデル”| 受け取る形 | 全部の入力の軸 0 が同じ束(1 から 16)で、どの層も束の 1 件ずつを独立に計算するモデルです。実数のモデルと 8 ビットのモデルの両方です |
| 計算のしかた | 生成されたコードは、束の 1 件ごとに全部の段を順に計算し、件ごとに入力と出力のバッファを持ちます |
| 件ごとのバッファ | FilterInput と FilterOutput は件 0、FilterInputItem1 と FilterOutputItem1 は件 1 で、以下同じです(2 本目の入力の件 1 は FilterInput1Item1) |
FilterItems | 束の数です |
| 費用 | 1 回の計算は、段の数も GPU の仕事も 1 件の束の数倍です。FilterSteps は全部の段を数えるので、RunFilter(first, count) で同じようにフレームに分けられます |
| 件を混ぜる層 | 層を名指してエラーになります。軸 0 に沿う Softmax・LayerNormalization・RMSNormalization・Concat・Split・Gather・ArgMax・縮約(axes の無い縮約は軸 0 も縮めます)、既定の軸(0・2・3)の MeanVarianceNormalization、軸 0 を動かす Transpose、束をほかの軸とまとめる Reshape・Flatten、軸 0 を変える Squeeze・Unsqueeze・Tile・Pad・Slice・Resize、軸 0 に沿って掛けるかブロードキャストで束を軸 0 から動かす MatMul・Gemm、入力より軸の多い shape の Expand(足した軸が前に付き、束が軸 0 から動くため)、学習の形の BatchNormalization です |
| ほかにエラーになる形 | 件ごとに計算するか分からない層・件ごとに値が違う定数・束の違う入力・束が 2 以上のトークンの番号です |
| 軸の数え方 | 書き出したときの束のままのモデルで数えます。形の記録が無い値の、後ろから数えた軸(-1 など)はエラーです(軸 0 かどうか分からないため。形の推論をかけると記録されます)。axes の無い Squeeze もエラーです(束が 1 だと軸 0 も取り除くため。axes を書きます) |
| 束 1 で書き出して件ごとに回す | エラーになるモデルも、束 1 で書き出して件ごとに計算できます。元のモデルと同じ結果になるのは、元のモデルが件ごとに独立に計算するときだけで、件を混ぜる層があると結果が違います |
受け取れるモデル
Section titled “受け取れるモデル”| エラーになる形 | 対処 | 理由 |
|---|---|---|
| 一部だけが量子化されている | 全体を量子化するか、量子化の演算を 1 つも含めずに書き出す | 量子化の演算が 1 つでもあれば 8 ビットのモデルとして読むので、尺度の付いていない値がエラーになります。重みだけを 8 ビットか 4 ビットにした実数のモデルは、この形に当たりません |
| 尺度が実行時に決まる | 書き出す時点で決まる形にする | 焼き込むには、値の刻みがコンパイルの時点で決まっている必要があります |
| 8 ビット整数でない | 8 ビットの整数へ量子化する | 同じ 8 ビットでも、小数を表す形式は受け取れません |
| 比較や整数の番号の出力を使う | 条件を定数にする。TopK などの番号は、スコアを出力して Udon で選ぶ | 前の演算の出力どうしの比較と、整数の番号を返す演算は受け取れません。16 ビットの値が整数を正確に持てるのは 2048 までです。実数のモデルの ArgMax・ArgMin は、選ぶ軸の長さが 2048 までで、出力をモデルの出力としてだけ使う形を受け取ります |
16 ビットで表せない下限の Clip で割る | 下限を 16 ビットで表せる値に上げる | 16 ビットの最も小さい値はおよそ 6e-8 なので、1e-10 のような下限は表せず、下限のかかる所で商が大きくずれます。Div が割る数として直接読む形と、形だけを変える演算(Reshape・Transpose・Unsqueeze・Squeeze・Identity・Flatten)を挟んで読む形がエラーになります。Log・Reciprocal がそれらの演算か Expand を挟んで読む形も同じです(Clip の直後の Log・Reciprocal は Clip と一緒に計算するので、エラーになりません) |
| 16 ビットで表せない小さな値を足す | 16 ビットで表せる値を使う | 1e-8 を足す Add・Sum や、その値との大きい方を取る Max は、出力が 1e-8 になるはずの所で 16 ビットでは 0 になり、そこを割ると無限大や NaN が出ます。その出力を Div が割る数・Reciprocal・Log・負の指数の Pow(Pow(x, -0.5) など)が読む形がエラーになります。間に 0 を 0 のまま運ぶ層(形だけを変える演算と Expand・Sqrt・Abs・Neg・Relu などの関数・ReduceSum・ReduceMean などの縮約・Add・Sub・Sum・Mean・Max・Min・積でほかの計算した値と組み合わせる層)を挟んでも同じです(例: (x + 1e-8) + y・Sqrt(ReduceSum(Relu(x) + 1e-8)))。途中の MatMul・Gemm・畳み込みは、読む値が全部 0 のときだけ 0 になるので、その先はたどりません。要素ごとの演算の連なり(小さな下限の Clip の後の Add・Sqrt など)で、ある入力の結果が 16 ビットで 0 になるか 65504 を超える形も同じです |
| 標準でないドメインの演算 | 標準の ONNX の演算で書き出す | 名前が同じでも、別のドメインの演算は受け取れません。com.microsoft の MatMulNBits だけは読みます |
| 形状が動的 | 入力の形状を固定してから書き出す | 軸の長さが実行時に決まるものは、テクスチャの大きさを決められません |
| 並べたモデルが 1 本目と重み以外で違う | 同じグラフのまま、重みだけを変えて書き出す | どの層が違うかを名指しします。計算に焼き込む尺度が違うモデルも、ここでエラーになります |
| 未対応の演算が含まれる | その演算を使わない形へ変更する | 何が含まれていたかを名指しします。省いたまま通すことはありません |
エラーはモデルの側の言葉で出るので、書き出した側を修正してください。
| 1 セルは 4 成分 | channel が 4 を超えるモデルは、テクスチャが横に伸びます |
| 重みの格納先 | 小さい層は定数として、大きい層はテクスチャとして保持します。どちらにするかは層ごとに決まります |
| 刻み | Conv と MaxPool の strides は 1 以外も扱えます |
| 大きさ | 各層の出力のテクスチャは、幅と高さのどちらも 16384 texel までです。1 次元の長さは高さに置くので、長さも 16384 までです |
| クラスの宣言 | partial が必要です。生成されたファイルが、そのクラスの残りになります |