コンテンツにスキップ

GPU で計算する ONNX モデル(試験中)

この章は試験中です。 [OnnxGpu("…")] は、画像をバッファへ入れ、結果を別のバッファで受け取ります。

モデルのファイル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
組み込む手順組み込む手順のとおりです
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);
}
}

層の中身を省いた抜き書きです。

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);
// …(層の数だけ続きます)…
}
}
生成される名前説明
FilterInput入力のバッファ(GPU カーネルの実行と同じ型)。セルに代入する値の範囲は、生成されたファイルの行き先の欄に書いてあります
FilterOutput出力のバッファ。8 ビットのモデルではセルを 0 から 1 で、実数のモデルでは値をそのまま読み出します
LoadFilter()バッファを作成し、重みを読み込みます。Start から 1 度だけ呼びます
RunFilter()1 回ぶん計算します
FilterSteps1 回ぶんの計算の段の数です
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 組のままで、重みだけを並べた数だけ持ちます

量子化の演算を 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 に沿います
MatMulNBitscom.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;
受け取る形全部の入力の軸 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 で書き出して件ごとに計算できます。元のモデルと同じ結果になるのは、元のモデルが件ごとに独立に計算するときだけで、件を混ぜる層があると結果が違います
エラーになる形対処理由
一部だけが量子化されている全体を量子化するか、量子化の演算を 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 が必要です。生成されたファイルが、そのクラスの残りになります