CUDA カーネルを実行したときに起こること: ソースから SASS まで

CUDA カーネルを実行したときに起こること: ソースから SASS まで

CUDA カーネルの起動は、ホスト CPU、NVIDIA ユーザーモードドライバ、カーネルモードドライバ、そして GPU ハードウェア間の複雑なオーケストレーションを伴います。このプロセスは、高レベルの C++ コードをハードウェア固有のコマンドと SASS 命令のストリームに変換し、数千もの並列スレッド全体で実行をトリガーするために特殊なメモリマップド・ドアベルシステムを利用します。

コンパイルパイプライン: PTX と SASS

CUDA プログラムを実行するには、デバイス非依存コードとハードウェア固有の機械語命令の間のギャップを埋めるために、複数のコンパイル段階が必要です。

nvcc はいくつかのコンパイラを調整するドライバプログラムとして機能します。ホストコードは標準のホストコンパイラに送られ、デバイスコードは特定のパスをたどります: cicc(LLVM ベースのコンパイラ)が PTX(Parallel Thread Execution) を生成し、続いて ptxas がそれを SASS(Streaming Assembler) に変換します。

PTX と SASS の違い

  • PTX は仮想命令セットアーキテクチャ(ISA)です。デバイスに依存せず、無限に近い数の型付きレジスタを使用します。これは前方互換性のフォールバックとして機能し、バイナリが事前にコンパイルされた SASS がカバーしていない GPU アーキテクチャ上で実行された場合、ドライバはロード時に PTX を JIT コンパイルして新しい SASS を生成できます。
  • SASS は実際に GPU 上で動作する機械語です。仮想レジスタを有限の物理レジスタにマッピングし、複雑な PTX 操作を単一のハードウェア命令(例: IMAD.WIDE)に融合します。

最終的なバイナリは "fatbin" と呼ばれ、SASS(ELF コンテナ内)と圧縮された PTX の両方を束ねています。この fatbin はホスト実行ファイルの .nv_fatbin セクションに埋め込まれます。

ホストから GPU へのトリガー

GPU は PCIe バス越しに存在するため、CPU が単に "GPU 関数を呼び出す" ことはできません。代わりに、ドライバが管理するワークキューを介して通信する必要があります。

ホスト側ランチスタブ

nvcc がカーネル起動(例: vadd<<<4096, 256>>>)をコンパイルすると、式は生成されたホスト側ランチスタブに置き換えられます。このスタブはカーネル引数をホストメモリ上のバッファへ特定のバイトオフセットで詰め込み、続いて __cudaLaunch を呼び出します。__cudaLaunch はホスト側関数ポインタを検索キーとして使用し、fatbin 内の対応するデバイス側シンボルを見つけます。

ドライバブリッジ

CUDA ランタイムはクローズドソースのユーザーモードドライバ(libcuda.so)とやり取りし、さらにそれがカーネルモードドライバ(nvidia.ko)と /dev/nvidiactl などのデバイスファイル上の ioctl 呼び出しで通信します。

CUDA 12.2 以降、モジュールのロードはデフォルトで 遅延(lazy) です。ドライバは特定のカーネルが初めて起動されるまで、SASS の cubin を GPU メモリへアップロードしません。

ハードウェア側の起動メカニズム

GPU の実行は、ホストメモリから読み取られるコマンドストリームによって駆動され、主に 3 つの構造体からなる "チャネル" を通じて調整されます:

  1. Pushbuffer: ドライバが GPU の動作を定義する "メソッド"(レジスタアドレスと値)を書き込むホストメモリ領域。
  2. GPFIFO: Pushbuffer のどの領域を読むかを GPU に指示するポインタのリングバッファ。
  3. USERD: デバイスメモリ上の小さな構造体で、GP_GETGP_PUT カーソルを保持し、ワークの消費状況を追跡します。

ドアベル

Turing 以降の最新 GPU は GP_PUT カーソルをスヌープしません。その代わり、ドライバは ドアベル と呼ばれるメモリマップドレジスタにワークサブミットトークンを書き込みます。これにより GPU のホストエンジンが更新された GP_PUT を取得し、DMA 経由で Pushbuffer からメソッドをプルします。

キュー メタ データ (QMD)

最も重要なメソッドの一つは Queue Meta Data (QMD) のストリーミングです。QMD は GPU に対して次の情報を伝える起動ディスクリプタです:

  • グリッドとブロックの次元(例: 4096 ブロック × 256 スレッド)
  • スレッドあたりに必要なレジスタ数と共有メモリ量
  • SASS コードのメモリアドレス
  • カーネル引数を保持する定数バンクのアドレス

ストリーミング マルチプロセッサ (SM) 上での実行

QMD がコンピュート ワーク ディストリビュータ(GigaThread Engine)に渡されると、GPU は線形の SASS 命令を利用可能なストリーミング マルチプロセッサ(SM)へマッピングします。

リソース制約と占有率 (Occupancy)

RTX 4090(AD102)では、SM はスレッド容量とレジスタファイルサイズで制限されます。スレッドあたり 16 レジスタ、ブロックサイズ 256 スレッドのカーネルの場合:

  • レジスタ容量: $65,536 / (256 \times 16) = 16$ ブロック
  • スレッド容量: $1,536 / 256 = 6$ ブロック

スレッド容量がボトルネックとなり、各 SM は最大で 6 ブロック(48 ワープ)しか保持できません。

ワープの実行適格性とレイテンシ隠蔽

CPU とは異なり、GPU は複雑なアウトオブオーダー実行ロジックを使用しません。その代わり、多数のレジデントワープ間で切り替えることでレイテンシを隠蔽します。ワープは ptxas が SASS 命令に埋め込む制御コードペイロードに基づいて 実行適格 と判断されます:

  • 静的ストールカウント: 固定レイテンシ演算に対して、コンパイラはワープが何サイクル待機すべきか正確にエンコードします。
  • Yield ヒント: スケジューラに他のワープを優先させることを示すビット。
  • 依存バリアインデックス: 可変レイテンシ演算(例: グローバルメモリロード LDG)用に、ハードウェアは 6 つの物理スコアボードバリアを使用します。ワープは待機中のバリアがクリアされるまで実行不可です。

メモリ階層とデータ転送

ワープがロード(LDG.E)を発行すると、SM のロード/ストアユニットは リクエスト合成 を行います。たとえば 32 スレッドが連続した 4 バイトの float にアクセスすると、ハードウェアはこれらを 4 つの 32 バイトセクタリクエストにまとめ、バス交通を最小化します。

データの流れは L1 データキャッシュ → L2 キャッシュ → メモリコントローラ → GDDR6X VRAM です。ベクトル加算のように演算強度が低いカーネルでは、性能は通常 DRAM バス帯域幅に制限され、計算能力よりもメモリ転送がボトルネックになります。

結果を CPU に戻す

最終ブロックがリタイアすると、GPU は QMD で定義された完了セマフォをポストします。その後、GPU のコピーエンジンが L2 キャッシュ(または VRAM)からホストメモリへ結果を DMA 転送します。コピーが完了すると、コピーエンジン自身がセマフォをポストし、ホスト側の cudaMemcpy 呼び出しが復帰でき、CPU は(例: printf を呼び出す)実行を再開します。

Sources