すべての開発者が知っておくべきGPUコンピューティング
(codeconfessions.substack.com)- GPUは、低い単一命令レイテンシよりも大規模並列スループットを優先するアーキテクチャのため、ディープラーニング・グラフィックス・数値計算のように、同種の演算を大量に実行する処理に強い
- CPUは、パイプライニング、アウトオブオーダー実行、投機実行、多段キャッシュによって逐次実行のレイテンシを減らす一方、GPUは多数のALUとスレッドでレイテンシを隠蔽し、スループットを高める
- 32ビット精度の基準ではNvidia Ampere A100は19.5 TFLOPS、2021年のIntel 24コアプロセッサは0.66 TFLOPSで、数値計算スループットの差は拡大し続けている
- CUDAカーネルは、CPUのホストコードが実行準備を担当し、GPUのデバイスコードがgrid・block・thread構造で実行され、スレッドは32個単位のwarpにまとめられてSIMT方式で処理される
- 実際の性能は、SMのレジスタ、共有メモリ、ブロックスロット、スレッドスロットをどう分割するかに大きく左右され、occupancyが低いとレイテンシを隠しにくくなり、最大スループットにも届かないことがある
CPUとGPUの設計目標の違い
- CPUは主に逐次命令実行を高速に処理するよう設計されている
- 命令実行レイテンシを減らすために、instruction pipelining、out-of-order execution、speculative execution、multilevel cacheのような機能を使う
- 2つの数値を足す単一演算や短い演算フローは、GPUよりCPUのほうが低レイテンシで処理できる
- GPUは大規模並列性と高スループットを中心に設計されている
- ビデオゲーム、グラフィックス、数値計算、ディープラーニングのように、多くの線形代数・数値演算を高速に実行する必要がある処理がこの構造に適している
- 数百万・数十億個の同種演算では、GPUは大規模並列性によってCPUよりはるかに高速に処理できる
- 数値計算性能は、1秒あたりの浮動小数点演算数であるFLOPSで測定する
- Nvidia Ampere A100は32ビット精度で19.5 TFLOPSのスループットを提供する
- 2021年時点のIntel 24コアプロセッサは、32ビット精度で0.66 TFLOPS水準である
- GPUとCPUのスループット差は年々拡大している
GPUがレイテンシを隠す仕組み
- GPUは、個々の命令レイテンシが高くても、多数のスレッドと計算資源を活用してレイテンシ耐性を確保する
- あるスレッドが命令結果を待っている間、GPUは待機不要な別のスレッドを実行する
- このスケジューリングにより、計算ユニットは可能な限り動作を続け、高スループットを維持できる
GPUコンピュートアーキテクチャ
- GPUは複数の**ストリーミングマルチプロセッサ(SM)**の配列で構成される
- 各SMは複数のstreaming processor、core、threadを含む
- Nvidia H100は132個のSMを持ち、各SMに64コアがあるため、合計8,448コアを備える
- 各SMには、すべてのコアが共有する限定的なオンチップメモリがある
- このメモリはshared memoryまたはscratchpadと呼ばれる
- SMの制御ユニット資源もコア間で共有される
- 各SMは、スレッド実行のためのハードウェアベースのスレッドスケジューラを備える
- ワークロードに応じて、tensor core、ray tracing unitのような特殊機能ユニットや加速計算ユニットも含まれることがある
GPUメモリ階層
-
レジスタ
- 各SMには多数のレジスタがある
- Nvidia A100とH100は、SMごとに65,536個のレジスタを持つ
- レジスタはコア間で共有され、スレッドの要求に応じて動的に割り当てられる
- 実行中に特定スレッドへ割り当てられたレジスタはそのスレッド専用であり、他のスレッドは読み書きできない
-
constant cache
- SMで実行されるコードが使用する定数データをキャッシュする
- プログラマがコード内でオブジェクトを明示的に定数として宣言しなければ、GPUはconstant cacheに保持できない
-
shared memory
- 各SMにある、小さく高速で低レイテンシなオンチップのプログラマブルSRAM
- 同じSMで実行されるスレッドブロックが共有する
- 複数スレッドが同じデータ片を使う場合、1つのスレッドだけがglobal memoryから読み込み、残りが共有すれば重複ロードを減らせる
- スレッドブロック内のスレッド間同期メカニズムとしても使われる
-
L1 cacheとL2 cache
- 各SMには、L2 cacheから頻繁にアクセスされるデータをキャッシュするL1 cacheがある
- L2 cacheは全SMで共有され、global memoryから頻繁にアクセスされるデータをキャッシュしてレイテンシを減らす
- L1とL2 cacheはSMに対して透過的に動作するため、SMから見るとglobal memoryからデータを受け取っているように見える
-
global memory
- GPUにはオフチップのglobal memoryがあり、大容量・高帯域幅のDRAMである
- Nvidia H100は80GB HBMと3000GB/sの帯域幅を持つ
- global memoryはSMから離れているためレイテンシは高いが、オンチップメモリ階層と多数の計算ユニットがこのレイテンシの隠蔽を助ける
CUDAカーネルとスレッド構造
- CUDAはNvidia GPU向けプログラムを書くためのプログラミングインターフェースである
- GPUで実行する計算は、C/C++関数に似た形の**カーネル(kernel)**として表現される
- 例として、2つのベクトルを入力に受け取り、要素ごとに加算して結果を3つ目のベクトルに書き込むベクトル加算カーネルがある
- カーネル実行時には複数のスレッドを起動し、この全体のまとまりをgridと呼ぶ
- gridは1つ以上のthread blockで構成される
- 各thread blockは1つ以上のthreadで構成される
- ブロック数とスレッド数は、データサイズと必要な並列性によって変わる
- 256次元ベクトル加算では、256スレッドのブロックを1つ構成し、各スレッドがベクトルの1要素を処理できる
- より大きな問題では、GPUで利用可能なスレッド数が不足することがあり、その場合は各スレッドに複数のデータポイントを処理させることができる
- CUDA実装は2つの部分に分かれる
- host codeはCPU上で実行され、データ読み込み、GPUメモリ割り当て、設定したスレッドgridでのカーネル実行を担当する
- device codeはGPU上で実行され、実際のカーネル関数を定義する
GPUでカーネルが実行される段階
-
ホストからデバイスへのデータコピー
- カーネル実行前に必要なデータをCPUメモリからGPU global memoryへコピーする必要がある
- 最新のGPUハードウェアでは、unified virtual memoryを使ってhost memoryから直接読み取ることもできる
-
スレッドブロックをSMへスケジューリング
- GPUメモリ内に必要なデータの準備ができたら、thread blockをSMへ割り当てる
- 1つのブロック内のすべてのスレッドは、同じSMで同時に処理される
- GPUは実行前に、そのスレッドに必要なSM資源を確保しなければならない
- 実際には、複数のthread blockが同じSMへ同時に割り当てられることがある
- SM数には限りがあり、大きなカーネルではブロック数が非常に多くなることがあるため、すべてのブロックが即座に実行されるわけではない
- GPUは待機中のブロック一覧を保持し、あるブロックが終了すると待機ブロックの1つを実行へ割り当てる
-
SIMTとwarp
- SMに割り当てられたスレッドはさらに32個単位にまとめられ、このまとまりをwarpと呼ぶ
- 現行世代のNvidia GPUのwarpサイズは32だが、将来のハードウェアでは変わる可能性がある
- SMはwarp内の全スレッドに対して同じ命令をフェッチして発行する
- スレッドは同じ命令を同時に実行するが、それぞれ異なるデータ部分を処理する
- このモデルはsingle instruction multiple threads(SIMT) と呼ばれ、CPUのSIMD命令に似ている
- Volta以降の最新GPUには、warpに依存せずスレッド間の完全な同時性を許すindependent thread schedulingもある
-
warpスケジューリングとレイテンシ耐性
- SM内のすべてのprocessing blockがwarpを処理していても、ある瞬間に実際に命令を実行しているwarpは一部にとどまる
- これは、SMの実行ユニット数が限られているためである
- あるwarpが時間のかかる命令結果を待っているとき、SMはそのwarpを待機状態にして、待つ必要のない別のwarpを実行する
- 各warpの各スレッドはそれぞれ専用のレジスタ集合を持つため、warp間の切り替えに追加のオーバーヘッドはない
- CPUのプロセスコンテキストスイッチは、レジスタをメインメモリへ保存し、別プロセスの状態を復元する必要があるためコストが高い
-
結果データをデバイスからホストへコピー
- カーネルの全スレッド実行が終わったら、結果をhost memoryへ再びコピーする
資源分割とoccupancy
- GPU資源の活用はoccupancyという指標で測定する
- occupancyは、SMに割り当てられたwarp数を、そのSMがサポートできる最大warp数で割った比率である
- 最大スループットのためには100% occupancyが望ましいが、さまざまな制約により常に可能とは限らない
- SMには、レジスタ、shared memory、thread block slot、thread slotといった実行資源が固定されている
- これらの資源は、スレッド要件とGPUの制限に応じて動的に分割される
- Nvidia H100の例
- 各SMは32個のblock、64個のwarp、すなわち2048個のthreadを処理できる
- 1 blockあたり最大1024個のthreadをサポートする
- blockサイズを1024 threadで実行すると、2048個のthread slotは2個のblockに分割される
- 動的分割は固定分割よりも計算資源を効率よく使える
- 固定分割では、各thread blockが固定量の実行資源を受け取る
- 場合によっては、スレッドが必要以上の資源を割り当てられ、資源の無駄とスループット低下が生じることがある
- occupancyを下げる例
- blockサイズを32 threadとし、合計2048 threadが必要なら64個のblockが生じる
- しかし各SMは一度に32個のblockしか処理できないため、実際に実行されるのは1024 threadだけで、occupancyは50%になる
- SMあたりのレジスタが65,536個あるとき、2048 threadを同時実行するにはthreadあたり最大32個のレジスタしか使えない
- カーネルがthreadあたり64個のレジスタを必要とすると、SMあたり1024 threadしか実行できず、occupancyは再び50%になる
- occupancyが低いと、レイテンシを十分に隠せなくなり、ハードウェアの最大スループットに到達するために必要な計算スループットも低下しうる
- 効率的なGPUカーネルを書くには、高いoccupancyを維持しつつレイテンシを減らせるよう、資源を慎重に配分する必要がある
- レジスタを多く使うとコード自体は高速化できる場合があるが、occupancyが下がる可能性もあり、最適化のバランスが重要である
1件のコメント
Hacker News の意見
誰かがこの記事について抗議メールを送ったとのこと: https://twitter.com/abhi9u/status/1715753871564476597
これは HN ルール違反です。実際、サイトのガイドラインと FAQ の両方に載るほど重要な唯一の項目で、HN ユーザーはこの問題に非常に敏感です。
Q: 自分の投稿への投票をお願いしてもよいですか?
A: だめです。ユーザーは、誰かに宣伝したいコンテンツがあるからではなく、自分が知的に興味深いと感じたときに投票すべきです。このルールに違反すると、投稿、アカウント、サイトにペナルティを科したりブロックしたりします。やめてください。
https://news.ycombinator.com/newsfaq.html
投票、コメント、投稿を依頼しないこと。ユーザーは宣伝目的ではなく、自分で出会った何かを個人的に興味深いと感じたときに投票し、コメントすべきです。
https://news.ycombinator.com/newsguidelines.html
今は分かったので、二度としません。
「ホストからデバイスへのデータコピー」の部分に 非同期コピー がないのに驚いた。GPU を最大限活用するには、ホストと GPU の間でデータをコピーしている間に GPU を遊ばせておくべきではない。
多くのフレームワークは、非同期の作業投入と並行して実行できる非同期コピーのスケジューリング機構を提供している。記事自体は GPU 入門に近いが、実際の GPU プログラミングで高価な GPU を限界まで絞り出そうとすると、その先にあらゆるコツやテクニックがある。最近の最適化の多くがそうであるように、隠れた崖や非線形性が多いので、プロファイリングツール が大いに役立つ。
ただし FP64 ユニット が多い GPU を使えば、大きく高速化できる可能性がある。通常こういうものはゲーミング GPU ではないが、4060 が手元にあるだけなら、FP64 性能は約 300 GFLOPS なので CPU より高い可能性が高い。現代の CPU もこの領域では強力で、コアごとにクロック当たり複数の FP64 演算を発行できる。
「ほとんどのプログラマーは CPU を深く理解している」という冒頭の一文があまりにも露骨に事実ではないので、記事は素晴らしいのかもしれないが、残りを真剣に受け取るのが難しくなる。
大学で面白半分に哲学の授業を取ったのだが、そこで、ある文をすぐに捨てずに、より良い形に直して読む能力を身につけた。今では脳が自動的に、過度な一般化や明らかな誤りも、合理的に近い真の命題へと翻訳してくれる。論旨が進むにつれてそれらのアイデアを再構成し、文章全体を論理的に一貫したものとして評価できるようになった。
そのおかげで、ひどい文章を読んでも、関心のあるテーマについて新しい真偽の前提や主張が残り、その分だけ自分の精神世界が広がる。
大学で CPU アーキテクチャの基本的な事実を学び、全体像を非常に基礎的には知っていて、時々限られた知識のアップデートにも触れるが、それを「深い理解」とは言わないだろう。むしろ「CPU がどのように動作し、設計され、使われるかについての基本的な理解」くらいが妥当に見える。
アセンブリに熟達していれば、低レベルで CPU を使う方法を「深く理解している」と言えるかもしれないが、それでも少し大げさに聞こえる。CPU/GPU 設計の専門家であることとも違う。
なので同意する。それでも記事は興味深く、特に図が良い。
「実行中のスレッドに割り当てられたレジスタはそのスレッド専用なので、他のスレッドは読み書きできない」という部分には例外がある。
HLSL の wave intrinsic と CUDA の類似機能は、現在の wavefront 内にある他のスレッドのレジスタを読むことができる。またメモリアーキテクチャの段落には、キャッシュは同じ dispatch/grid 内のスレッド間で一貫性を保証しないが、チップ全体にグローバルに存在する特殊な機能ブロックがグローバルメモリのアトミック演算を実装している、という点も入れてよい。
SIMD プログラミング は本当に荒々しい。
画面上のすべてのピクセルに計算を走らせたい? 問題なし。
分岐条件を入れたい? 痛い。
なぜ今でも GPU と呼ぶのだろう? PPU(並列処理装置)のほうが良い名前に聞こえる。
drone と quad-copter の関係も似ている。
すばらしい記事。そしてGPUは、自分が担う仕事に関しては、私が思いつくどんなものよりも進歩していて性能も高い。
ただしSIMDは、もっと柔軟な別のパラダイムを学ぶと、必須ではないカテゴリに入れたくなる。私は MIMD とクラスタ/トランスピュータのほうを好むが、2000年代ごろに姿を消したように見える。現在の状況は、開発者にデータを手作業で移動させ、同時にアクセスできるメモリ位置の数に恣意的な制限がある中でシェーダを書かせ、GPU/CPU向けに別々の言語を使って作業を重複させ、レイトレーシングのような機能にどんなハードウェアがあるのかを把握させ、OpenGL/Metal/Vulkanのような主張の強いフレームワークに縛られることを要求している。GPUは私が行きたい場所へは絶対に連れていってくれない脇道なので、この25年は間違った時間線に生きている人のような経験だった。
大まかに言えば、ムーアの法則が終わったという制約の中でスケールできる汎用CPUは、ローカルメモリを持つマルチコアであり、コピーオンライトのコンテンツアドレス指定メモリや別のキャッシュ方式でデータを共有し、ユーザーがデスクトップコンピューティング環境であらゆる計算方式を自由に探索できるよう、単一の統合アドレス空間を提供すべきだ。標準的なアセンブリ言語を使うが、通常はErlang/Go、Octave/MATLAB、理想的にはJuliaのような関数型プログラミング言語でプログラムする。3DレンダリングやAIライブラリはその上の階層であって、根本要素ではない。
興味深いことに、GPUは私が述べたマルチコア構成におおよそ到達しているが、ドライバが汎用MIMDに必要なベアメタルアクセスからユーザーを切り離している。GPU優位を崩す方法はFPGAしかないと思っていたが、もしかするとGPUハードウェアを統合メモリを持つMIMDのように見せるドライバを書く機会があるのかもしれない。GPUコアが整数演算をどれほど上手く処理できるのかは分からないが、64ビット浮動小数点の32ビット整数部分で近似できそうだ。そうした妥協のためにMIMDマシンはGPUより10〜100倍遅くなるかもしれないが、CPUよりは10〜100倍速くなり得る。それでいて、モバイル市場が主導権を握り、性能より価格と電力効率が優先されるようになった2007年ごろ以降CPUを停滞させた、大型キャッシュと高速バスへの過度な依存なしにスケールできる。MIMDマシンはクラスタリングして、コード変更なしでSETI@homeのような分散計算ネットワークも作れる。一般ユーザーにとってどれほど力になるかをつかむには、データではなく 計算におけるBitTorrent対FTP の比較に近い。
Apple SiliconのアーキテクチャがNVIDIAとどう違うのか、よく理解できていない。
「Nvidia H100 GPUはSMが132個、SMあたりコア64個で合計8448コア」という文を見ると、8448コアは確かに印象的だ。ところがApple M2 Ultraは76コアしかない?
NVIDIA H100 GPUはどうして110倍を超える数のコアを持てるのか? 明らかにM2 Ultraより性能が110倍高いわけではないのに、ここでは何が起きているのか?
NVIDIAブログのこの図を参照: https://developer-blogs.nvidia.com/wp-content/uploads/2021/g...
(https://developer.nvidia.com/blog/nvidia-ampere-architecture...)
もちろん、レーンごとに別個のプログラムカウンタをサポートしているという点で、それを「スレッド」と呼ぶ口実があると感じるかもしれないが、結局重要なのはALUの速度とスループットだ。
これで、なぜ機械学習が精度に浮動小数点を使うのか理解できた。選択ではなく、グラフィックスのコードがそう使うからだった
「なぜ機械学習はこんなに非効率なのか」というパズルの、もう一つのピースだ
実環境でメモリコピーのオーバーヘッドがどの程度なのか気になる。普通の仕事のように動くなら、かなり厳しいはずだ。TCP 処理をハードウェアに逃がしてまでそれを避けるくらいなのだから。ここではデータ量ははるかに多いが、より大きな塊で処理される
つまり、浮動小数点画像のミニバッチをコピーする程度なら、今でも十分に速い。勾配/SGD の反復が遅く、計算量が非常に大きいからだ。混合精度を使っても同じだ
浅いネットワークでは、元の圧縮データだけを GPU メモリにコピーし、展開などを GPU 上で行う利点があるかもしれない。しかし現代の GPU がまだ PCIe 5 を採用していないのは、生の計算性能のほうが重要だからだ
最後に、Tensor Coreの影響も大きく、ネットワークによっては速すぎて利用率が非常に低くなることもある
学習の数学も、数値が連続的であることを仮定している
ただ、CPU ベースの LLM がなぜ量子化を行うのか気になっていた。理解している限りでは、重みの精度を下げてメモリ使用量を減らすプロセスだ
精度不足が違いを生むのかは不明だ。だとすれば、そもそもなぜ浮動小数点を使うのか? 精度が重要でないなら、追加の精度は実質的な理由なしにリソースを余計に使わせるだけで、おそらく必要以上に何桁も多くのリソースを使う可能性が高い
この分野は、性能を理解している人たちが始めたものではなかった。ツールを使って何かを作ったが、「なぜ」はない。ツールがそうするから、そうしたのだ
これが重要な理由はこうだ。一般的な CPU でも、データにアクセスする方法の一つが別の方法より何桁も速いことがあるが、それを知っている必要がある。LLM のコストを何桁も削減したくはないのか?
数年前の、CPU と GPU の厄介な部分を扱ったこの発表とスライドも見る価値がある
Alexander Titov — Know your hardware: CPU memory hierarchy https://youtu.be/QOJ2hsop6hM
https://github.com/alexander-titov/public/blob/master/confer...
Know Your Hardware - CPU Memory Hierarchy -- Alexander Titov -- C%2B%2B Moscow Meetup March 2019.pdf
https://github.com/alexander-titov/public/blob/master/confer...
GPGPU - what it is and why you should care -- Alexander Titov -- CoreHard 2019.pdf