
2026/06/29 22:11
CUDA カーネルを実行すると何が起きるのか?
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
元の要約は主要なポイントと明確に一致しており、簡潔で明瞭です。完全性、忠実性、明瞭性、または簡潔性を満たすために改善や再作成は必要ありません。
本文
CUDA カーネルの実行全体像:コンパイルから計算への追跡
このドキュメントでは、シンプルな CUDA ベクトル加算プログラムが、ソースコードから実際の計算結果へ至るまでの全プロセスを追跡します。 専門的な用語は適切に翻訳し、技術的なニュアンスを損なわず、読みやすい構成としています。
1. サンプルプログラム
ここでは、2 つのベクトルを足し合わせる簡単な CUDA プログラムを示します。 結果として、$1+1=2$ が 100 万回正しく計算されます。
Cuda コード (vadd.cu)
このカーネルは、各スレッドが配列の 1 つの要素を担当する形で作成されています。
__global__ void vadd(const float* a, const float* b, float* c, int n) { // グローバルインデックスの計算:ブロック内での位置 + ブロックのオフセット int i = blockIdx.x * blockDim.x + threadIdx.x; // インデックス範囲内であれば、対応する要素を足す if (i < n) c[i] = a[i] + b[i]; }
ホスト実行プログラム (main)
- メモリ確保(ホスト側とデバイス側)
- 初期化(値を 1.0f に設定)
- GPU コードの読み込み (
的な呼出し)cuLaunchKernel - 結果の転送と確認
int main() { // n = 1,048,576 (2^20) 個の要素 int n = 1 << 20; size_t bytes = n * sizeof(float); // ホストメモリ確保と初期化 float *a = (float*)malloc(bytes), *b = (float*)malloc(bytes), *c = (float*)malloc(bytes); for (int i = 0; i < n; i++) a[i] = b[i] = 1.0f; // デバイスメモリ確保 float *da, *db, *dc; cudaMalloc(&da, bytes); cudaMalloc(&db, bytes); cudaMalloc(&dc, bytes); // ホストからデバイスへデータ転送 cudaMemcpy(da, a, bytes, cudaMemcpyHostToDevice); cudaMemcpy(db, b, bytes, cudaMemcpyHostToDevice); // カーネル起動:グリッド=4096ブロック、ブロック=256スレッド // 総スレッド数 = 4096 * 256 = 1,048,576 (= n) vadd<<<4096, 256>>>(da, db, dc, n); // デバイスからホストへ結果転送 cudaMemcpy(c, dc, bytes, cudaMemcpyDeviceToHost); // 確認出力 printf("c[0]=%f c[n-1]=%f\n", c[0], c[n-1]); // 両方とも 2.000000 }
2. コンパイルとコードの展開 (nvcc
)
nvccnvcc コンパイラは、単一のコマンドで複数のコンパイラを実行し、結果を組み合わせたドライバプログラムです。
コンパイルオプション
RTX 4090 (Ada アーキテクチャ: sm_89) をターゲットにします。
$ nvcc --keep -arch=sm_89 -o vadd vadd.cu && ./vadd
--keep オプションを使用すると、中間ファイルがディスクに残り、各段階を確認できます。
生成される出力ファイル
コンパイルプロセスにより以下のファイル群が生成されます。
| ファイル名 | 内容・用途 | 作成ツール |
|---|---|---|
| PTX (仮想 ISA)。 デバイス非依存の仮想的なコード形式。将来の互換性用。 | |
| CUBIN (固定 ISA)。 アーキテクチャ固有の実装コード。SASS。 | |
| Fat Binary。 PTX と CUBIN を 1 つのファイルにバンドルしたもの。 | Linker |
| ホスト起動スタブとカーネル登録用コード。 | CUDA Front-end |
| リンカ後の最終ホストオブジェクト。 fatbin を埋め込んだ ELF ファイル。 | Linker |
PTX から SASS への流れ
-
PTX (仮想 ISA):
- 型付きレジスタ(
,%rd
など)を持ち、アーキテクチャ非依存。%f - アドレス指定などに冗長な表現を使う(例:3 つの命令でアドレス形成)。
でポインタの領域変換を行う必要がある。cvta.to.global
- 型付きレジスタ(
-
SASS (実装 ISA):
が PTX を RTX 4090 用の物理命令に最適化・圧縮。ptxas- 特別レジスタ (
,SR_CTAID
) から一般レジスタへコピー(SR_TID
)。S2R - 命令数大幅削減:例として
,LDG.E
,FADD
などの簡略化された命令セット。STG.E
/*00d0*/ FADD R9, R4, R3 ; // a[i] + b[i] /*00e0*/ STG.E [R6.64], R9 ; // c[i] = ...
3. ホストによる GPU のトリガー (cuLaunchKernel
)
cuLaunchKernelコンパイルされたバイナリは単なるファイルです。GPU に実際にコードを走らせるには、以下のブリッジプロセスが必要です。
-
フロントエンドコンパイラ (
):cudafe++- ホストコード内に起動スタブ(
など)を挿入。__cudaLaunchPrologue - カーネル関数
とファットバイナリ内のシンボルを紐付ける登録処理を行う。vadd
- ホストコード内に起動スタブ(
-
動的ライブラリの読み込み (
):libcuda.so.1- CUDA ランタイムは、最初の GPU 呼び出し時に
を動的に開く。libcuda.so.1 - ここからドライバーのユーザー空間側へのアクセス経路が確保される。
- CUDA ランタイムは、最初の GPU 呼び出し時に
-
起動シーケンス:
- コンパイラで生成された登録コード(コンストラクタ)が実行され、ファットバイナリが登録される。
が呼ばれ、起動構成 (QMD) が構築される。cuLaunchKernel- CUDA 12.2 以降: デフォルトは「怠慢 (Lazy)」読み込み(ロード時に PTX を JIT コンパイル)。
4. GPU メモリへのコード転送と通信経路 (GPFIFO
/ ドアベル)
GPFIFOGPU は PCIe バスの向こう側にあり、直接関数呼び出しを受け入れられません。ホストは**命令ストリーム(メソッド)**を書き込む必要があります。
コミュニケーションの仕組み
- プッシュバッファ: ドライバーが GPU への命令を記述するホスト RAM 上の領域。
- メソッド:GPU のネイティブコマンド(レジスタセット+動作)。
- QMD (Queue Meta Data): カーネルの起動 descriptor(グリッドサイズ、レジスタ数など)を格納。
- GPFIFO (Graphics Pipeline FIFO):
- CPU と GPU で共有されるポインタリングバッファ。
: ドライバーが書いたカーソル(新しい仕事の追加)。GP_PUT
: GPU が消費したカーソル。GP_GET
起動トリガーの進化
- 古式 CUDA (Turing 以前):
スクロールによって直接ポーティング。USERD - 現行 CUDA (Turing / Ada):
- CPU のホストエンジンは GPFIFO を常時監視する機能がない(または無効化)。
- ドライバーはMMIO ドアベルを使用。
- ドライバーが
を更新し、特定のレジスタ領域に書き込むと、GPU はその変化を検知して起動処理を開始する。GP_PUT
5. 指令ごとの進行 (SM 内での分散)
QMD が計算ワークディストリビュータ(GigaThread エンジン)に渡されると、以下のプロセスが開始されます。
VRAM × 128 SMs | 指令ストリーム
- コンパイル済みの SASS は VRAM に単一の線形リストとして存在する。
- 計算ワークディストリビュータ: この長列を 128 個の SM(Multiprocessor)に分布。
SM (マルチプロセッサ) の内部構造
- RTX 4090 は物理的な 144 SM を持つが、効率的な動作のために16 SM が無効化されている(実質 128 SM)。
- スループット制限:
- レジスタ容量: 各 SM は最大 1,536 スレッド(48 ワープロ)を保持。
- 割り当て: この例のカーネルは「ブロックあたり 256 スレッド」を使用するため、1 SM あたり**6 ブロック(48 ワープロ)**しか稼働させられない。
- スケジューリング:
- 1 SM は 4 つのサブパーティションに分かれ、それぞれが独立した実行パイプラインを維持する。
- サイクルごとに最大 1 つの命令を発行可能(スロット制約)。
ワープロの有効化とスケジューリング制御
PTX の冗長さを除去し、ハードウェア最適化のために
ptxas が以下の制御ビットを埋め込んだ SASS を生成します。
| ディレクティブ | 役割 | 効果 |
|---|---|---|
| 静的停止数 (Stall) | 固定遅延の推測 | ALU による計算中は、命令を発行せずにスレッドを駐車(パース)。 |
| 降伏ヒント (Yield) | 優先度制御 | このワープロを待機させるべきか否かの指示。ボトルネック時は他のワープロに処理を譲る。 |
| 依存関係バリア | データ同期 | グローバルメモリ読み書き(, )による遅延の同期管理。 |
これにより、GPU は並列計算による潜伏遅延を隠蔽し、効率的なスループットを実現します。
6. メモリ階層とデータ転送 (Coalescing)
カーネルは浮動小数点数(4 バイト)の配列を操作します。
ロード/ストアの合体 (Coalescing)
- 要求: ワープロ内の 32 スレッドがそれぞれ 1 バイトアクセスすると、計 128 バイトの連続ブロックとして処理される (
)。32 * 4 bytes - SM ロードユニット: この連続パターンを検出し、4 つの「扇区リクエスト」に結合。
- メリット: メモリ帯域幅を最大化。単一トランザクションでデータを読み取れる。
データパス (Data Path)
- L1 データキャッシュ: 最優先でチェック(ミスする場合のみ次のステップへ)。
- L2 キャッシュ: 72 MB で、全 SM に共有。
- VRAM (GDDR6X): L2 ミス時にアクセス。RTX 4090 は HBMではなく GDDR6X を使用。
- 読み込み: 入力データ(12 バイト/要素)が DRAM から読み込まれる。
- 計算:
により加算が行われる。FADD - 書き出し (ストア): 結果
は、同じパスを経由して L2 を経由するか、あるいは L2 にキャッシュされる(場合によっては VRAM とは直接やり取りしない)。STG.E
パフォーマンス計測 (ncu
)
ncu$ ncu --metrics launch__grid_size, sm__warps_active.avg.pct_of_peak, \ dram__throughput.avg.pct_of_peak, gpu__time_duration.sum ./vadd
- sm__warps_active: 82.77%(充分な計算リソース確保)。
- dram__throughput: 79.65%(メモリ帯域使用率が高いが、入力転送のみで計算量が少ないため)。
- 推定スループット: $780 \text{ GB/s}$ 附近。
7. CPU への戻り (結果の同期)
GPU は非同期に計算するが、ホストは最終結果が必要だ。
- 完了セマフォ: グリッドの最後のブロックが終了したことを示すシグナル(QMD の Fence)。
- DMA コピー:
は GPU のコピーエンジンによって実行され、セマフォによるゲート制御を受ける。cudaMemcpy - データ転送: 結果が L2 キャッシュにある場合、DRAM を経由せず PCIe バスのみで CPU メモリへ移動する(高速)。
最終的にホスト関数がブロックされ、
printf によって計算結果が表示される:
c[0]=2.000000 c[n-1]=2.000000
8. 補遺: ランチの詳細確認方法 (診断ツール)
より詳細な動作を確認するための、いくつかの技術的なアプローチ(
LD_PRELOAD, ioctl など)が存在します。
インターポジションフック (Memory Inspection)
ドライバーが GPU メモリ領域をマップする際 (
/dev/nvidia*) を監視し、プッシュバッファの内容を読み取る。これにより、QMD やメソッドバーストを確認できる。
# プッシュバッファの内容を出力するシムプログラム LD_PRELOAD=./shim.so ./vadd
起動コマンドの解析
- プッシュバッファコマント:
(QMD の格納場所) とSET_INLINE_QMD_ADDRESS_A
(データ読み込み)。LOAD_INLINE_QMD_DATA- Ampere/Ada ヘッダー (
) を参照し、オフセットを解析することで起動パラメータを取得可能。clc9c0.h
- Ampere/Ada ヘッダー (
ドライバー通信 (ioctl
)
ioctllibcuda の内部処理を確認するため、strace で nv_escape.h に定義されたコマンド(NV_ESC_RM_CONTROL など)を追跡する。
$ strace -f -e trace=ioctl ./vadd # /dev/nvidiactl, /dev/nvidia-uvm に対するコマンド確認
これらを通じて、コンパイル済みのバイナリ内の「暗黙の契約」や、ドライバーの動作メカニズムを深く理解できる。