あれ
うごいた!!遺伝的プログラミングがWebGPUで!!!遅い!!!!CPUより8倍ぐらい遅い!!!俺たちの戦いはこれからだ!!!
あれ
goから越境してrustやるの嫌じゃな~~~。
そもそも、wgpuのレポジトリ見てても動かし方がよくわからん。
なんとなくIssueをcompute shader
で検索して眺めてみる。すでにIssueが出てれば幸甚。
なかったわ。一般的には巨大シェーダーを動かす動機がないらしい。
wgpuのレポジトリにExampleを発見したので動かしてみる。
wgpuでboidsが動いた。やったねたえちゃん!
巨大シェーダー実証の元としてhello-computeが良さそう。コードが簡単。見覚えもある。
サクッとシェーダーを書き換えてみたら動いた。いいぞ。
条件分岐が1万行の巨大シェーダーを試してみた結果、wgpuにおいてはそんなに時間がかからない。500msとかそんなもん。じゃあRustでwgpu使って遺伝的プログラミングすればええやんという判断が浮かび上がってくるが、Rust難しすぎてあれなのであれ。覚悟を決めるのに1週間ぐらいかかるやつ。
とはいえ現状は wgpu → wgpu-native → go-webgpu という依存をしており、wgpuを直接ぶったたくよりも隔靴搔痒となる。依存しているコードがどこでどう変わるかもわからない。wgpuはブラウザでも使われているらしいので、多分大丈夫だろう。
本丸の遺伝的プログラミングのコードは2000行程度なので、1週間もあれば行ける気がする。Rustを理解できれば。
その前にgo-webgpuをアップデートすべく、go get -u
を叩く。よし、最新版でも直ってない。wgpuに乗り換えだ。
遺伝的プログラミングのコード全部を書き換えるのは骨が折れる。泣く。なので、wgpuを叩く部分だけRustで書けばええんちゃうんかという判断が生えてくる。『wgpu-go』や。
念のためにgoでもrustで動かしたのとまったく同じシェーダーを実行してみる。いや、普通に動く。先生……!これは……!条件分岐の数がCreateComputePipelineの処理時間に影響していると思われていたが、実はそうではなかった可能性が高い。Rustのwgpuで行った検証も不十分だった可能性が高い。
配列アクセスが悪いのではないかと思い至り、配列利用型巨大WGSLシェーダーを動かしてみたが普通に動く。機序がわからん。キショい。
関数実行が悪いのではないかと思い至り、配列及関数利用型巨大WGSLシェーダーを動かしてみたが普通に動く。静的だと最適化してないかこいつ。
関数がインライン展開されている気がしてきた。多分。わからんけど。そう考えると、関数の再起呼び出しができないのも、配列の添え字に変数が使えないのもしっくりくる。
「気晴らしにcompute.toysとか言うので遊んでみるかぁ」つって、何気なく配列の添え字に変数を入れてみたところ、なんと動いた。はぁ?じゃあっつって、最新版のgo-webgpuだとどうなんのよって試したら配列の添え字に変数を入れても動く。CreatePipelineも300ms以内に終わる。はぁ???今までの苦労は何だったんだ。3日返してほしい。
なお、最新版が出てきたのは5 days agoである。タイムリー。
あれ
nodes_outputにアクセスするコードを並び立てて、switch文で添字を定数にしてしまえば、汎用的なシェーダーで遺伝的プログラミングができそう。
つまり以下のような擬似コードになる
fn nodes_output_access(i:u32, nodes_output:array<i32>) i32 {
switch(i) {
case 0 { return nodes_output[0]}
case 1 { return nodes_output[1]}
...
}
}
fn nodes_exec(node:Node, nodes_output:array<i32>) {
let i1=nodes_output_access(node.index1, nodes_output);
let i2=nodes_output_access(node.index2, nodes_output);
return node_function_exec(node.function, i1, i2)
}
fn main(){
nodes_output[0] = node_exec(nodes[0], nodes_output);
nodes_output[1] = node_exec(nodes[1], nodes_output);
...
}
個体を作るごとにコンパイルしなくて良いので、学習データ数が少なくても早くなるはず。
やってみたところ、条件分岐の数が2倍になると、コンパイル、というかcompute pipelineの作成にかかる時間が10倍になった。ばかやろうじゃん。
条件分岐の数が1000にもなると、もはや待っていられない時間がかかる。
力技で解決するなら、wgpuに踏み込み、compute pipelineの作成を高速化するように書き換え、pull requestを出すと良いのだろう。
あれ
WGSLのコード内の条件分岐の数が2倍になると、compute pipelineの作成にかかる時間が10倍になった現象は、Macにおいてのみ低速な可能性がある。
MTLCompilerServiceがCPU100%に張り付いている。
WGSL → SRIP−V → Metal Shading Languageと変換される部分の、SRIP−V → Metal Shading Language が時間を取っている、ような気がする。
Windowsでも再現することが確認できた。
200個の条件分岐があるとpipelineの作成が200秒程度かかる。
あれ
遺伝的プログラミングで生成したプログラムをWebGPUでまるっと実行することは諦めて、配列に対して基礎的な四則演算をするぐらいにするのが良さそう。
処理とデータがCPUとGPUを行ったり来たりすることになるが、実装は極度に簡単になる。
そのように思われて試してみた結果、基本的な四則演算ではGPUよりもCPUのほうが早い。for文で100回とか演算させるとやっとGPUが早くなってくる。
あれ
あれ
WGSLからはSRIP−Vに変換されるので、これを直接生成すればいいじゃんという厄介な案が浮かぶ。
WebGPU-nativeのgo版ではGLSLが使える雰囲気あったので試そうかと思ったが、GLSLも関数の再帰呼び出しや、変数での配列の値取得はできないらしい。
腹くくってSRIP-Vしか無いのか。
WebGPU(WGSL?)で遺伝的プログラミングをするにあたっての制約
- 一つのBufferが128MBまでしか作れない。
- Deviceの取得時にmaxBufferSizeとか設定すれば限定解除できそうな雰囲気があるが、やり方がわからない
- というかやってみたけど(RequiredLimitsを設定)Deviceが取得できなくなった
- Deviceの取得時にmaxBufferSizeとか設定すれば限定解除できそうな雰囲気があるが、やり方がわからない
- Shaderの中で宣言したarrayは添え字が定数でしか取得できない
for(var i=0u; i<10u; i++ ){let val = arr[i]}
みたいなことができない- 正気か?
- 関数の再帰呼び出しは不可
- 「SIMDやぞ」ということで、まあわからなくはない
- 「Stackを作って関数の再帰呼び出しをfor文で再現」と思うも、「Shaderの中で宣言したarrayは添え字が定数でしか取得できない」
- 関数型がなさそう
- 「配列に関数を入れて処理を出しわける」みたいなことができない
- 無理やりやるとif文のお化けになってしまう
- SIMDでif文めっちゃ使うのはなんかヤダ
然るに
- 遺伝的プログラミングで生成したProgramを構成するNode群を実行できない
- NodeからNodeを呼び出したい
- → 「関数の再帰呼び出しは不可」
- Node群を入力側から順次実行し、Nodeの出力値をNodesOutputみたいなarrayに格納
- → 「Shaderの中で宣言したarrayは添え字が定数でしか取得できない」
- StorageBufferは変数の添え字が使えるので、それでNodesOutputを作る
- → 「一つのBufferが128MBまでしか作れない」
- 並列処理の数だけNodesOutputが必要なので、1GBぐらいほしい
- 手元にある1070 TiはCUDA Coreが2432基あるので
- 並列処理の数だけNodesOutputが必要なので、1GBぐらいほしい
- → 「一つのBufferが128MBまでしか作れない」
- 並列処理したい数だけStorageBufferを作る(シェーダーを自動生成する)
- → 処理が複雑過ぎて3日もたてば何やってるかわからないコードになる
- NodeからNodeを呼び出したい
- どれか一つでも制約をなんとかできれば遺伝的プログラミングはできそう。
きえええええええええええええぇぇぇぇぇ
GPGPUあるあるの制約なのかもしれないが、世の機械学習ライブラリはこういう制約をどう御しているのか。
なぜこんな古生代みたいな制約があるのか。令和やぞ。
一部WGSLの制約かもしれない。
あれ
「遺伝的プログラミングの学習中に生成したプログラムからWebGPUのシェーダーを自動生成して実行する」みたいなことをやったらアホほど処理が遅かった。
- プログラムを普通に実行するのに比べて、50倍実行に時間がかかってすごくすごくすごく遅い
- 自動生成されたプログラムなので1KB~10MB程度となって大きく、これのコンパイルは極めて時間がかかる
- Step数でいうと1000 step~100000 stepみたいな感じ
- 本当はプログラムをもっとデカくして、100MBぐらいにして推論の精度を上げたい
- 学習中、個体が発生する毎にシェーダープログラムのコンパイルが走る
- 自動生成されたプログラムなので1KB~10MB程度となって大きく、これのコンパイルは極めて時間がかかる
あれ
WebGPUをネイティブで動かす、wgpu-nativeという得体の知れないライブラリを使っている。
WebGPUならWindowsでもMacでも動いてお得なので。
そして、それを使って遺伝的プログラミングを動かそうとしている。おそらくWebGPUで遺伝的プログラミングは世界初だろう。ニッチすぎてやる人がいない。