公開もしていない記事がいくつもあったりしますが、久しぶりにまともに書けそうです。
ソースを見るたびに怪しい部分が多々あるのでどこから手を付けるか非常に悩んでいました。
結果から言えば、現状binbcast.cppというソースを弄っている最中です。
そしてこの部分が結構なキモだったりするわけです。最初はあまり気にせず触り始めました。
で、何度も壁にぶつかっては途方に暮れつつも、3回ほどgeminiに質問したりして自分の中の思考を整理していました。gemmaでもいけそうですが、一時期ダメっぽくなったgeminiですがまた少し変わったのかな?結構間違っていない感じの回答を得られて助かりました。
やってることは、何度もループを作り替えつつ、実行して、結果を見て、実行速度や出力物をみて、また手直ししているだけなのです。
処理速度が遅くなったりめっちゃ早くなったり、喜んだり悲しんだり…もうなんか色々ですねw
出力画像が真っ白の場合は、大抵どこかでごっそり処理が抜け落ちているパターンですが、何らかのものが出力されている場合は、ほぼ正解という感じ。色々なパターンを経験することができました。それとlevel zero関連のエラーは、メモリーアクセス関連で想定外の部分を操作すると発生しやすいみたいです。所謂「アクセス例外」ってやつですかね?
一番大きい転換点としては、2日ほど前に書いた(下書きどまりの)ちょっとした説明を含んだ(愚痴)記事で、多次元配列の説明を書いていた時でした。何でこんな記事を書いていたかと言えば、今まで「次元を折りたたむ」と言う表現があまりしっくり来ていなかったのですが、ggmlの配列処理のループを書いていて突然言葉が自分の中にハマり込んだ感じがしたんです。まぁ普通はもっと簡単に理解しているのでしょうけど…w
結論だけいってしまうと、表現上2次元の物を1次元で表現する場合、2次元を1次元に折り畳むわけですが、コンピュータ上の表現では単純に[3, 5]と言う表現が [23] とかになるわけです。いきなり数字がでかくなっていますが、仮に2次元表記を[x,y]と表現する場合、このどちらかの次元の範囲を決める必要があります。逆にいえば、範囲を決定することによって、次元を折りたたむことができるわけです。この部分がイマイチ理解できていなかった重要なポイントだったりしますw
範囲を決めることによってxとyを一つの次元に落とし込むことができるわけです。
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 … という風に並んでいたとすると、これを5つづ区切って行ってみましょうか。(桁が違うと見づらいので01と言う表記にしました。)
|x0 x1 x2 x3 x4 --+-------------- y0|00 01 02 03 04 y1|05 06 07 08 09 y2|10 11 12 13 14 y3|15 16 17 18 19 y4|20 21 22 23 24 y5|25 26 27 28 29 y6|30 31 32 33 34
:|……
ここで、横方向をxとして縦方向をyとすると、先ほどの[3,5] と言う表現は[28]になります。こんどは逆にこの[28]と言う表現を2次元に展開する場合は表の形から[3,5]と置き換えられますが、計算上では28%5=3 28/5=5(少数以下切り捨て)となります。
こうすることで次元の折り畳みを行うわけですが、ggmlでは処理上の都合で多次元の配列を3次元に折りたたんで扱ったり、全てを折りたたんで1次元に折りたたんで扱っています。(結果的にPC上では1次元に収めているわけですが。)
そもそも1次元(線形表現)であれば処理を行う上でとても便利で見通しがしやすくなります。が、この1次元で表現されたものを元の次元に復元するとき、計算が必要になるわけです。できれば理論上の表現のまま処理を行いたいという思惑もあります。しかしながら、ggmlの配列表現は基本的に4次元で表現されます。実行環境を見ると、演算処理はGPUを利用しているために3次元表現しか対応していなかったり、処理上の都合で様々な制限がかかるため、そのままの理論上の表現で処理を行うために工夫が必要となります。
方針としては大きく分けて2つの方向性があり、どちらもメリットとデメリットが存在します。。
ひとつは一つ以上の次元を折りたたみ、3次元以下で処理を行い、処理単位を1要素づつとして処理する。
メリット:推奨される実装方法で、実行時の最適化の恩恵を最大限受けることができる。
デメリット:要素のアドレスをひとつづつ演算する必要があり、計算量が増える。
もう一つは、次元の一つを処理単位として処理し、3次元として処理を行う。
メリット:元の次元のまま処理するのでアドレス計算があまり増えない。
デメリット:一つの次元を一纏めにすることでメモリアクセスの幅が広がり、実行時の最適化の妨げになる。さらに一つの処理内でループ処理を行うので処理単位の時間が大きくなる可能性がある。
今回は、実際にどちらも実装してみました。もともと存在するものはこれのハイブリッド的な物で、ソースを見る限り処理単位と言うよりSYCLの実装による制限で作業単位を大きくすることで回避しようとしていたように思えます。しかし、膨大なデータを扱うので、限界を超えるために別途処理を変え実装しています。(こちらもある程度の限界はありますが。)
実装したものの実行結果を言ってしまうと、実行時間は明らかに1要素づつ処理させるものの方が圧倒的に処理時間が短くなりました。どのくらいの差が出たかと言うと、1step出力で1次元を処理単位にしたものは、1270.14s程度かかったのですが、一要素づつの処理単位の物では、287.60s程度になりました。どちらも最適化していないので一概にどうこう言えるものではありませんが、まぁ1桁差は倒的な差ですね(笑)
今回作成したものは、1つの処理単位の次元が一番次元の低い(一般的に一番データ量の多い次元)を選んで作成したので極端な結果となってはいますが…(-_-;)
アドレス変換の計算量の増加よりもループ処理による実行時の最適化ができなくなる方がデメリットが多いのは確かなようです。
もう少し突っ込んでいくと、ggml内の配列は4次元データだけではなく、3次元以下のデータ構造も多いので、もう少し手を加えていくことで処理を最適化することが可能です。
この状態で元の実装より遅いのですが、どのような処理を行っているのか実感できたのと、ggmlのデータ構造をより把握できたので収穫は大きかったかな?。さらにここから1024x1024の出力がSYCL上のvaeで可能になることにつながると良いのですが…
0 件のコメント:
コメントを投稿