テンソル
テンソルとは、1次元、または2次元の、タプルまたはスカラーの配列です。
MFGのプログラムとは、複数のテンソルの定義を並べる事で行われます。 定義したテンソルは他の所で使う事が出来ます。
テンソルはMFGに特有の概念で、他の言語での関数に似た機能を提供しつつ、 GPUプログラムの重要な要素であるカーネルとグローバルメモリに深く結びついています。
テンソルの例
以下が典型的なテンソルの例です。
result_u8の例
def result_u8 |x, y|{
u8[0, 0, 0xff, 0xff]
}
中間テンソルの例
@bounds(640, 480)
def red_tensor |x, y| {
u8[0, 0, 0xff, 0xff]
}
テンソルの定義と参照
テンソルは通常、定義と参照、という2つの側面があります(inputとresultは例外で片方だけになります)。
定義というのはテンソルを作るルールを記述して、MFGがそのルールをもとに実際にテンソルのデータを構成します。 テンソルの定義は例えば以下になります。
@bounds(640, 480)
def red_tensor |x, y| {
u8[0, 0, 0xff, 0xff]
}
テンソルの定義についての詳細は後述の「テンソルの定義」で扱います。
定義したテンソルは参照する事が出来ます。 参照は対象のテンソルに収められた要素にアクセスする事です。
テンソルの参照はカッコで行います。
let v = red_tensor(10, 20)
テンソルの形は、次元と要素の型の2つで決まります。
テンソルの範囲外への参照
範囲外への参照は、返る値は不定ですが参照自体は可能です。 内部的には範囲外のインデックスは範囲内に強制的に変更しています。
GPUプログラムでは結局その値は使われないが値を参照してしまう、 という事を避けるのが難しいので、仕様として明確に参照を許しています。 将来の実装で値は変わりうるので、範囲外アクセスした時の値に依存したコードは避けてください。
テンソルの参照とsplat演算子
テンソルの要素の参照にはsplat演算子*を使えます。 以下のv1とv2は同じ意味になります。
let tup = [10, 20]
let v1 = red_tensor(10, 20)
let v2 = red_tensor(*tup)
splat演算子については式とベクトライズ演算を参照ください。
テンソルの種類
テンソルには、大きく以下の種類があります。
- inputテンソル
- resultテンソル
- テンソルリテラル
- 通常のテンソル(中間テンソル)
- ローカルテンソル
inputテンソル
inputテンソルはフィルターを掛ける対象となるレイヤーのピクセル値を保持しているテンソルです。 このテンソルはユーザーが定義する事が無く、参照するだけ、という特殊な性質があります。
inputテンソルにはinput_u8とinput_u16の二種類があります。 u8とu16については、後述の「inputとresultのu8とu16について」で扱いますが、 端的に言えばu8がBGRAを8bitで、u16はBGRAをそれぞれ16bitで扱うものです。
input_u8テンソル
input_u8は、BGRAをu8の4次元ベクトルを要素として保持するテンソルです。 u8の4次元ベクトルとu16の4次元ベクトルは良く使われるので、 それぞれu8v4、u16v4という呼び名がついています。
テンソルとしての次元はいつも2次元です。入力レイヤーのxとyに対応します。 左上が0で右下に向かって正の座標系です。
例
let [b, g, r, a] = input_u8(32, 24)
input_u16テンソル
input_u16はBGRAをu16の4次元ベクトルとして保持するテンソルです。 そのほかはinput_u8と同様です。
「inputとresultのu8とu16について」でu8とu16については扱います。
他レイヤーの参照
MFGでは、対象とするレイヤーの他に、その上、下のレイヤーを相対的に参照出来ます。 他のレイヤーは大括弧で指定します。
# 1つ下のレイヤー
input_u8[-1](x, y)
# 1つ上のレイヤー
input_u8[1](x, y)
マイナスが下、プラスが上のレイヤーを表します。input_u16にも同様の仕組みがあります。
存在しないレイヤーは、すべて0が入っているとみなされます。今の所存在しないレイヤーかどうかを区別する方法はありません。
resultテンソル
resultテンソルには、result_u8とresult_u16の2種類があります。 MFGプログラムは、一度にこのどちらか一つだけを必ず含んでいる必要があります。 両方定義するのはエラーです。
resultテンソルも、プログラムに一つだけ存在する、という点で特別なテンソルです。 また、幅と高さの指定が存在せず、いつもinputテンソルと同じサイズになります。 そして定義だけが存在し、参照する事は出来ません。
テンソルの要素はresult_u8がu8v4で、result_u16がu8v16で保持します。
このresultの結果が、最終的にMFGのフィルタを適用した結果のレイヤーになります。
result_u8テンソル
結果をBGRAの8bitずつで保持するテンソルです。 結果はu8v4でなくてはいけません。
例えば以下が、すべてを赤にするresult_u8の例です。
def result_u8 |x, y|{
u8[0, 0, 0xff, 0xff]
}
result_u16テンソル
結果をu16v4で保持するresultテンソルです。
inputとresultのu8とu16について
inputとresultには、その末尾に _u8 か _u16 がつきます。
これはBGRAの色の要素をそれぞれ u8(符号無し8bit)で持つかu16(符号無し16bit)で持つかの違いがあります。 (u8やu16は型を参照)。
MFGはレイヤーにフィルターを掛ける言語です。 対象のレイヤーは32bpp、64bpp、その他、に分けられます。
現時点では32bppと64bppだけを対象として、それ以外への実行はサポートしていません。 将来的には8bppのグレースケールへの実行もサポート予定です。
32bppレイヤーに対してu8を指定する場合、及び64bppレイヤーに対してu16を指定する場合は、 それぞれ変換されることなくそのまま渡されます。
32bppレイヤーに対してu16を指定したり、64bppレイヤーに対してu8を指定すると、 内部で自動で色を変換して渡します。 u8かu16のどちらかで書いておけば、32bppのレイヤーでも64bppのレイヤーでも動作します。
一般には、u16で実装しておく方が、キレイな色のフィルタになります。ただしGPUのメモリ使用量は倍になります。 この辺はレイヤーの32bppと64bppの使い分けと同様ですね。
テンソルリテラル
小さな定数のテンソルを定義したい事が良くあります。 例えば畳み込み系のフィルタのweightなどです。
そうしたものは、テンソルリテラル、という記法を使って定義する事が出来ます。
以下がテンソルリテラルの例です。
def tensorName by [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
byとタプルのネストのような記法で表現します。
テンソルリテラルは、全ての要素が同じでなくてはいけません。 要素としては、現在の所数値のみ対応していて、タプルは未サポートです。
通常のテンソル
通常のテンソルとは、ここまで述べた特殊なテンソルで無いテンソルの事です。 中間テンソルとして使う事になります。
通常のテンソルは定義の所で @bounds でサイズを指定する必要があります。
@bounds(640, 480)
def red_tensor |x, y| {
u8[0, 0, 0xff, 0xff]
}
ローカルテンソル
ローカルテンソルは、グローバルなメモリと結びついていなく、生成も一つのスレッドで行われる、という特殊なテンソルです。 また、MFGで現在の所、唯一副作用で値を更新する事が出来るものです。
ローカルテンソルの例: ヒストグラム
典型的な使用例としては、ヒストグラムなどを求めるのに使います。例えば以下の _hist がローカルテンソルです。
def weight by [[1, 2, 1],
[2, 3, 2],
[1, 2, 1]]
@bounds(input_u8.extent(0)-2, input_u8.extent(1)-2)
def median |x, y| {
# ローカルテンソルの定義
@bounds(256, 4)
def _hist |i, col| { 0 }
# ローカルテンソルの副作用での更新
weight.for_each |ix, iy, wval| {
let [b, g, r, a] = input_u8(ix+x, iy+y)
mut! _hist(b, 0) += wval
mut! _hist(g, 1) += wval
mut! _hist(r, 2) += wval
mut! _hist(a, 3) += wval
}
# 以下_histを使って何かする
}
以下、このコードの詳細を見ていきます。
ローカルテンソルの定義
定義は通常のテンソルと同様で、以下のようになります。
@bounds(256, 4)
def _hist |i, col| { 0 }
ただし、他のテンソル定義の中で定義されている所だけが違います。
また、このboundsは定数でなくてはいけません。グローバルなテンソルでは変数も使用可能ですが、 ローカルテンソルはコンパイルの時点で値が確定している必要があるため、このような強い制約を設けています。
実際の動きとしては、単一のスレッドでこの初期化が実行される所が大きな違いです。 また、内部的にもグローバルメモリでは無くレジスタになります(シェーダーのローカル配列と同様)。
なお、後述するtensor reduceで定義する事も可能です。
ローカルテンソルの副作用での更新
MFGでは副作用はなるべく存在しないように設計されています。 現時点での唯一の例外がローカルテンソルです。
副作用はシンタックス的に特別に見えるように、必ず mut! で始まります。
先ほどの例では、以下の += が副作用での更新です。
mut! _hist(b, 0) += wval
これは _hist(b, 0) の値を、 _hist(b, 0)+wval で更新する、という意味です。 他の言語では普通に出来る事なので、むしろこの限定した方法でしか行えない事に驚かれる人もいるでしょう。
mut! による演算は非常に限定的なものしか提供していません。
mut! は、現時点では以下の大きく2つの種類しか存在していません。
+=transファミリー- cumsum!
- sort!
trans ファミリーについては別途、transとreduce で扱います。
ローカルテンソルは必要最小限に
ローカルテンソルは通常のプログラム言語の配列のように振る舞いますが、それはGPU的には多くの欠点をはらむ事にもなります。
まず、ローカルテンソルはレジスタに展開されるため、使えるサイズの制限がグローバルなテンソルに比べて非常に強い制限があり、 また、デバイスの依存も大きくなります。 特定のデバイスで動いたフィルタがほかのデバイスでは動かない、というリスクが上がります。
また、動いたとしても、並列実行性能を大きく下げるので、親のカーネルの生成が大きく速度低下する事になります。 この度合いもデバイス依存が大きくなるため、フィルタのポータビリティに悪影響があります。
ヒストグラムなどのようにどうしてもローカルテンソルが必要なケースは存在しますが、使用は最小限に抑えるようにしましょう。
テンソルの定義
(通常の)テンソルを定義する方法には、大きく2つの方法があります。
- 通常のテンソルの定義
- 他のテンソルからtensor reduce系の機能で定義(ローカルテンソルのみ)
テンソルの定義は def で行います。
通常のテンソル定義
通常のテンソル定義は、@boudsで幅と高さを指定し、ブロックで各位置の要素を返します。
@bounds(640, 480)
def red_tensor |x, y| {
u8[0, 0, 0xff, 0xff]
}
defの次はテンソルの名前です。その次には各位置を表す値が引数としてやってきます。
この場合、xは0から639まで、yは0から479までの値が来ます。 テンソルの次元は@boudsに渡される数値の数で決まります。 この個数と |x, y| などの数が不一致の場合はパースエラーとなります。
tensor reduceを用いた、他のテンソルからのテンソル定義
他のテンソルをもとに新しいテンソルを作る、という場合に使える、tensor reduceというものがあります。 これはローカルテンソルのみに使える機能です。
シンタックスは以下になります。
def テンソル名 by reduce<元となるテンソル>.メソッド名(引数) ブロック
引数の部分は「名前付き引数」を使う事が出来、引数の内容は「メソッド名」によって決まります。
具体例としては、例えば以下があります。
def med by reduce<hist>.accumulate(dim=0, init=-1) |i, col, val, accm| {
ifel(accm != -1, accm, ...)
elif(val < _hist(255, col)/2, -1, i)
}
これは上級者向けの機能なので、transとreduceで別途詳しく扱います。
テンソルの値
テンソルの値としては、数値とそのタプルを使う事が出来ます。 タプルはベクトルでなくても構わず、例えばi32とf32を混ぜたものを返す事が出来ます。
@bounds(640, 480)
def red_tensor |x, y| {
# i32とf32のタプルを返す
[3, 1.2]
}
タプルのネストは許されていません。
内部的にはタプルは別々のグローバルメモリの配列となります。
u8v4とu16v4のテンソルの最適化
一般にMFGでは、u8が必ず8bitとは限らず、内部的には32bitが使われる事も多くなります。 ですが、u8v4のテンソルだけは、必ず32ビットのサイズになるように最適化する事を保証しています。
これはu8v3やu8v2では保証していないため、かえってu8v4の方がu8v3やu8v2より効率的になる事もあります。
また、u16v4も同様の最適化が行われ、必ず64bitしか使わない事が保証されています。
テンソル関連のメソッド
テンソルは中の要素を参照するのが主な使い方ですが、 そのほかにメソッドもあります。
テンソルには以下のメソッドがあります。
- extent
- extentf
- is_inside
- to_ncoord
- sum
- for_each
メソッドは、input_u8.extent(0) などのように、
テンソル名.メソッド名(引数)
の形で呼び出します。(ただしto_ncoordだけは例外。後述)
sumとfor_eachはブロック引数を取る、ループ系の機能を提供します。
ts.extent(dim)
テンソルの幅や高さを取得します。引数で次元を指定します。
let w = input_u8.extent(0)
let h = input_u8.extent(1)
wやhはinput_u8の「最大のインデックス+1」となります(0オリジンなので)。
なお、引数を指定しないと全ての値をベクトルで返します。
let [w, h] = input_u8.extent()
ts.extentf(dim)
(since v1.0.08)
exntetをf32にキャストした結果を返します。 extentと同様に引数指定無しでベクトルを返す機能もあります。
let fsize = max(*input_u8.extentf())
ts.is_inside(x, y)
x, yがtsの範囲内ならノンゼロを、範囲外なら0を返します。
以下と同様です。
x < ts.extent(0) && y < ts.extent(1)
1次元の場合は引数は一つになります。
to_ncoord([x, y])
テンソルの定義内で使われて、0.0〜1.0にノーマライズされた座標を返します。引数は2次元のタプル。
def result_u8 |x, y| {
let [fx, fy] = to_ncoord([x, y])
...
}
テンソルは暗黙のうちに現在定義中のテンソルが指定されたものとして振る舞い、指定する事は出来ません。
なお、引数の整数座標がextent以上の値の場合は1.0以上を返すことになります。
ts.sum ブロック
tsの全ての要素に対してブロックを実行し、結果を全て足し合わせたものを返します。
例えば2次元のweightというテンソルに対して、それを全て合計したwsumという変数を求めるには以下となります。
let wsum = weight.sum |_, _, val| { val }
ブロックの引数はテンソルのインデックス(座標)とそのインデックスの要素の値です。
ts.for_each ブロック
ts.for_each はMFGのループ系の機能で唯一値を返さないループとなります。 値を返さないので、これはいつも副作用のある機能、具体的にはローカルテンソルと mut! の += と併用する事になります。
ts.for_each はtsの各要素に対してブロックを実行します。
weight.for_each |ix, iy, wval| {
let [b, g, r, a] = input_u8(ix+x, iy+y)
mut! _hist(b, 0) += wval
mut! _hist(g, 1) += wval
mut! _hist(r, 2) += wval
mut! _hist(a, 3) += wval
}
ブロック引数の仮引数は、sumと同様インデックスとそのインデックスの要素の値です。
サポートされているテンソルの次元
現時点ではテンソルは1次元と2次元のみをサポートしています。 3次元は将来的にはサポートする可能性がありますが、現時点ではサポートしていません。
グローバルブロック
グローバルなテンソルの定義の外側は、グローバルブロックと呼ばれる領域で、 ここでも計算を行う事が出来ます。
# ここがグローバルブロック
let a = 3*2
# ここより下はテンソル定義なのでグローバルブロックでは無い
def result_u8 |x, y| {
u8[y, x, 0, a]
}
グローバルブロックではアトリビュートでUI入力ウィジェットに関する情報を書いたり、 @print_expr で式をデバッグ目的で出力したり出来ます。
詳細はアトリビュートと入力ウィジェットを参照してください。