うごいた!!遺伝的プログラミングがWebGPUで!!!遅い!!!!CPUより8倍ぐらい遅い!!!俺たちの戦いはこれからだ!!!
うごいた!!遺伝的プログラミングが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しか無いのか。
for(var i=0u; i<10u; i++ ){let val = arr[i]}
みたいなことができない然るに
きえええええええええええええぇぇぇぇぇ
GPGPUあるあるの制約なのかもしれないが、世の機械学習ライブラリはこういう制約をどう御しているのか。
なぜこんな古生代みたいな制約があるのか。令和やぞ。
一部WGSLの制約かもしれない。
「遺伝的プログラミングの学習中に生成したプログラムからWebGPUのシェーダーを自動生成して実行する」みたいなことをやったらアホほど処理が遅かった。