私のGPUは、置き換えたはずのCPUより遅かった
かつて私は、重い計算をCPUからGPUへ移して、飛ぶように速くなることを期待した。ところが、それは這うように遅くなった——置き換えるはずだった普通のコードより、劇的に遅かったのだ。私は速さを買ったつもりが、どういうわけか減速を仕込んでしまっていた。
ハードウェアがボトルネックだったわけではない。現代のGPUは、コンピュータに載せられるものの中でも最速の部類に入る。問題は、そのGPUに、ほとんど唯一無二と言っていいほど不得手な形をした仕事を与えておきながら、それがうまく走らないことに驚いていた、という点だった。
ダウングレードだったアップグレード
私の期待は単純で、しかも安全だと思っていた。GPUは速い、私の処理は遅い、だからその処理をGPUに載せれば速くなるはずだ、と。これは加速というものについての俗流の理解であり、そして特定の、示唆に富んだ形で間違っている。
「加速した」バージョンを計測してみると、少し遅いどころではなかった。何倍も遅かった——それは、問題が最適化の不足ではなく、根本的なミスマッチであることを告げる類いの結果だ。私のやっていたことの形のどこかが、ハードウェアと真っ向から喧嘩していたのである。
「GPUに載せる」が実際に意味していたこと
私が本当にやっていたことは、こうだ。既存のループがあった——ひとつの作業単位を取り、それを計算し、次へ移り、それを何千回も繰り返す。「GPUを使う」ために、私は個々の作業単位をGPU上で走らせるようにしただけで、ループそのものは元の場所にそのまま残しておいた。
つまり何千回も連続して、私のプログラムはGPUの方を向き、小さなタスクをひとつ手渡し、答えを待ち、次のために向き直っていた。仕切っていたのは依然としてループだった。GPUは、打ち込みたい釘一本ごとに、その都度電話で呼びつけていた、とても速い請負業者にすぎなかった。
隠れたコスト——頼むだけでコストがかかる
GPUに仕事をひとつ手渡すたびに、それをセットアップして発行するための固定コストがかかる——単体では小さく、一度払うだけなら見えもしない。私はそれを何千回も何千回も払っていた。よく見てみると、実行時間のほとんどは計算ですらなかった。それはオーバーヘッドだった——GPUが何をすべきか指示されるのを待ちながら遊んでいる一方で、私のプログラムは、ちっぽけな質問を次から次へと投げかけることに時間を費やしていたのだ。
私が作り上げていたのは、ほぼまるごと「頼む」という行為でできていて、実際の仕事は丸め誤差のようなものでしかないプロセスだった。CPUバージョンは遅かったが、少なくとも忙しく働き続けてはいた。GPUバージョンが遅かったのは、その生のほとんどを待つことに費やしていたからだ。
GPUはタクシーではなくバスだ
最終的にそれを解決してくれた心的モデルはこうだ。GPUは、ひとつのことをより速くやる手段ではない。膨大な数の同じことを一度にやる手段なのだ。それはスポーツカーではなく、五十人乗りのバスである。
アイテムごとに一回ずつ呼び出すのは、そのバスを、毎回乗客一人だけ乗せて街を行ったり来たりさせるようなものだ。バス自体は本当に速い。だがスループットは惨憺たるものになる——そしてそれが惨憺たるものになるのは、まさに、群衆を運ぶために作られた乗り物を使って、人を一人ずつ運んでいるからだ。落ち度はバスにあるのではない。配車のしかたにあるのだ。
解決策——ループをやめて、バッチ化を始める
直す手立ては、より速いカーネルでも、より良いカードでもなかった。それはループの組み直しだった。「各アイテムごとに、それをGPUへ送る」のではなく、私はGPUが多数のアイテムを一度に受け取ってまとめて処理できるように作業を形作り直した——何千もの小さな発行を、ひと握りの大きな発行に変えたのだ。何度も何度も払っていたオーバーヘッドは、何千回ではなく数回だけ払えばよいものへと収束した。
これは、人々が「ベクトル化」やバッチ化という言葉で意味するものの、よくある一形態だ。多数のよく似た処理を、小さなステップの長い連なりとしてではなく、まとめて表現する、ということ。それはしばしば、CPUを赤面させるGPUと、CPUに赤面させられるGPUとの違いになり、両者の隔たりは途方もなく大きくなりうる。
なぜこれがハードウェアの存在意義そのものなのか
なぜ構造がそこまで重要なのか、それをチューニングの細部として扱うのではなく腑に落とすまでには、しばらくかかった。ここでのGPUの優位は、どれかひとつの処理が速いことから来ていたのではない。膨大な数のよく似た処理を同時にやることから来ていた。その並列性こそが、価値提案のすべてなのだ。もしあなたのコードが、自らの仕事を「これらすべてを同時にやる」と表現できないなら、並列性が掴むものは何もない——そして直列に、一度にひとつずつ仕事をするGPUは、高価で、不格好で、ひどい形をしただけのCPUにすぎない。
だから「GPUに載せる」は、はじめから本当の指示ではなかったのだ。本当の指示は「仕事を、一度にまとめてできるように形作り直せ」だった。ハードウェアは、問題の形が許す速さにしか、けっしてなりようがなかったのである。
道具は間違った形を救えない
この教訓は、GPUをはるかに越えて一般化する。あなたの計算の構造がそのハードウェアと喧嘩しているなら、より速いハードウェアに手を伸ばしてもほとんど役に立たない。こうした類いの仕事では、パフォーマンスは、それを走らせるものの名目上の速さよりも、計算の形——それがどう構成され、バッチ化され、表現されているか——のほうに宿っていた。遅いアルゴリズムを速いマシンに載せれば、得られるのは速い失望だ。
私はあの減速を、ひとつの戒めとして心に留めている——「私はそれを速くした」と「私はそれを速いハードウェアに移した」は、同じ文ではない、と。前者は仕事を理解することについての話だ。後者は、シリコンが自分の代わりに仕事を理解してくれることを願うことについての話だ。シリコンは、そうしてはくれない。
— シグナルもリターンもなし、投資助言ではありません。