
2026/09/09 4:46
GPU がメモリに書き込むとどうなるのか?
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
核心メッセージは、NVIDIA RTX 4090 ハードウェアが複雑なタイミング制約および特定のキャッシュポリシーを有するグローバルストア命令を管理し、データが DRAM に到達することを保証することにある。ロード操作と異なり、ストア操作は 2 つのレジスタではなく 3 つのレジスタを読み取る必要があるため、ワープあたり約 6.1 クロック周期ごとに 1 命令 throughput のパイプライン性能となる。重要なボトルネックとして、L1 キャッシュがライトスルーモードで動作している一方、ストリーミングマルチプロセッサ(SM)からのエグジットはサイクル当たり 32 バイトの維持能力しかないため、すべてのアクティブなワープから同時にストアを発行するとこの上限を超え、遅延を引き起こす。
これらの問題に対処するため、システムは小規模なアクセスを大きなセクターにマージしてメモリトラフィックを最適化するクオアレスラ(coalescer)を採用している。L1 キャッシュ(仮想インデックス/タグ付き、4-way セット関連性)は、ヒット与否に関わらずデータを L2 へ直接送出する。L2 キャッシュは、複雑なスライシング構造(36 スライス、各スライスは 16-way セット関連性で物理アドレスでハッシュされた 1024 セットを有する)およびエビクジョンルールを用いてディルティラインを管理する。L2 からエビクションされると、ディルティラインは即座に削除されるのではなく、先にメモリーコントローラ内の FIFO ワイトバックバッファへ送信され、その後でクリーニングされる。特定のアンドルティ規則(「8-dirty rule」)では、8 以上のディルティラインを有するセットにおいて、最も古くからストアされたディルティラインを前向きにクリーニングし、トラフィックのバーストを防ぐ。開発者は、ストア操作が DRAM にデータがヒットするまで永続的ではないため、システム全体での可視性を確保するため明示的なメモリーバリア(例:
membar.sys)が必要であること、システムスコープフェンス(membar.sys)には約 1 µs かかることを認識しなければならない。高並行性ワークロードは、維持限界を超えたり、システムスコープフェンス後に必要なマイクロ秒単位の PCIe 転送レイテンシを考慮しなかったりすると、パフォーマンス劣化のリスクがある。将来的には、カウントが 8 を超えた場合にトラフィックバーストを防ぐためディルティラインの前向きなクリーニングを行うメカニズムが導入される可能性がある。また、cudaMemcpyDeviceToHost は明示的なシステムスコープフェンスをスケジュールするものであり、PCIE への転送前に L2 を介してデータがグローバルに可視となるのを待ってから転送を行う。メモリーコントローラは 3 スライス間で共有され、コントローラごとの総 DRAM ライトスルーputthrough は約 55 GB/s(スライスあたり約 19.7 GB/s)で飽和する。本文
RTX 4090 の「STG.E(ストア)」経路追跡:ロードからのデータ永続化と可視性まで
前回の投稿では、グローバルメモリへのロード(LDG.E)の経路を追跡しました。今回は、その逆方向である**ストア(STG.E)**の処理フローを詳細に解説します。計算結果をレジスタからグローバルメモリへ書き出すプロセスにおいて、各ステーションで何が起きるか、またデータがいつ永続化するかを明らかにします。
1. ストア指令の実行とワープからの脱出
アセンブリコードでの処理
ベクトル加算カーネルでは、レジスタ計算後、結果を書き出す
STG.E 指令が実行されます。
/*00c0*/ IMAD.WIDE R6, R6, R7, c[0x0][0x170] ; // &c[i] /*00d0*/ FADD R9, R4, R3 ; // a[i] + b[i] /*00e0*/ STG.E [R6.64], R9 ; // c[i] = ... /*00f0*/ EXIT ;
- FADD で計算された合計値(
)を、アドレスレジスタ(R9
,R6
)で指定された場所へ書き込みます。R7 - この指令は**ロード/ストアユニット(LSU)**に発行されます。
- LSU はオペコード、アクティブなレーンのマスク、計算されたアドレスを受信します。
データ転送量の比較
- 読み込み:レジスタファイルから 2 ライン分を読み出す必要がありました。
- 書き込み:アドレス情報 2 つ+データそのもの合計で3 ラインを読む必要があります。
ワープのステータス変化
LDG.E が戻り、FADD が完了した後、サブパーティション内のワープは実行可能な状態(eligible)となります。レーンが STG.E の実行を開始します。
2. パフォーマンスボトルネックとコエリサ
ストアの頻度
- 1 ワープは、約6.1 サイクル(2.6GHz で換算すると約 2.3 ns)ごとに新しい
指令を LSU に通過させます。STG.E
SM の出口制限とボトルネック
- SM の出口:1 サイクルあたり32 バイト分のストア(またはロード)を維持できます。
- ワープがすべてのスロットリング能力を発揮しようとする場合、この点でボトルネックが発生します¹。
次の停留所:コエリサ(Coalescer)
- 役割:32 の 4 バイトアクセスを受け取り、最小数の 32 バイトセクタにまとめる。
- 当カーネルは 128 バイト連続書き込みのため、これは**4 セクタ(1 ライン)**に相当します²。
- ロードとの違い:
- ロード:常に 1 セクタあたり 32 バイト読み込み後、LSU でフィルタリング。
- ストア:各セクタリクエストにはバイトマスクが付き、「セクタ内のどのバイトを書き込むか」を指定します。
3. L1 キャッシュと L2 メモリへの移動
L1 を通過する
- 4 セクタが L1 キャッシュに到達します。L1 は**書き込みスルー(Write-through)**方式³です。
- データフロー:
- セクタ、マスク、データはそのまま L2 へ進みます。
- ラインのヒット・ミスに関わらず、この流れは維持されます。
L1 の置換と翻訳
- 空きスロットの確保:セットに空きがあれば、古いスロットが厳格な LRU 順で明け渡されます。
- アドレス翻訳:L1 の下層ではストアの仮想アドレスが翻訳され、クロスバーを介してラインを所有する L2 スライスへ送られます。
L2 への到着と処理
- リクエストはクロスバーを経由して、36 つの L2 スライスのうち 1 つに送られます。
- これは物理アドレスに基づき選択されたスライスです(前回のリバースエンジニアリング結果と同じ関数)。
- L2 内での動作:
- ヒット時:指定されたバイトがスロットに書き込まれ、「dirty(変更)」とマークされます。
- ミス時:スロットを探す必要があり、他のラインを DRAM へ追い出す場合があります。
- 完了信号:スロットが見つかりデータ書き込み後、L2 はクロスバーを介して SM へ確認応答(ACK)を送り返します。
4. 書き込みの永続化と「死後生」
データは実際に DRAM に到達しているか?
- ストア指令は完了し ACK が SM と LSU で消費されていますが、データは未だに DRAM に到達していません。
- このカーネルでは
により、ホスト側で L2 から PCIe を経て読み込まれました。cudaMemcpyDeviceToHost
- このカーネルでは
なぜデータは DRAM へ到達する必要があるのか?
- ハードウェアはデータを L2 で「dirty」として保持していますが、ストア指令は発射後放置(fire-and-forget)です。
- データを永続させるためには、別のカーネルがそのラインを読み取ったり書き込んだりする場合、あるいはキャパシティを使い果たし追い出される必要があります。
追従予測値(RRPV)と置換ポリシー
データがいつ DRAM へ到達するかは、L2 の置換ポリシーによって決まります。
ラインの状態管理
- RRPV(再参照予測値):00, 11, 2 のいずれかに設定されます。低い数字ほど「間もなく再使用する」と判定されます。
- 新規データは RRPV=11 で挿入されます。
- Dirty マスク:L2 がそのラインの唯一の存在場所であることを示します。
置換プロセス(Replacement Policy)
新しいラインが入る場合の動作:
- 検索:セット内の 16 ウェイをスキャンし、RRPV=2 のラインを探します。見つからない場合は全 RRPV を増やし再スキャン。
- 犠牲者の選定:最も最近使用されていない(LRU)ラインを選びます。
- dirty ラインの扱い:
- dirty であれば即座に追い出さず、FIFO ワイバックバッファへ入れ「書き戻し中」にします。
- 2 つの充填(fill)ごとに古いラインがポップされ、ウェイが解放されます。
- 次にクリーンな犠牲者を探して空けたウェイに挿入します。
「8-dirty ルール」:能動的なクリーンアップ
- dirty ラインでいっぱいのセットからミスが発生すると、DRAM への大規模書き戻しが発生し、容量不足とキューイング問題を引き起こします。
- これを避けるため、ストア前に**最も最近ストアされていない dirty ラインを見つけ、プロアクティブに「clean」に変換(ワイバック)**するルールがあります。
L2 セットでの動作シミュレーション
- 16 ライン分の出力を書き込んだ後、32 ライン分の入力ストリームが来ます。
- 書き戻しはメモリーコントローラのバッフルがいっぱいになるまで非同期的に行われます。その後はストアがストールします。
5. DRAM への書き込みと制御機構
セクタの送達
- ラインはセットから FIFO ワイバックバッファへ送り込まれ、そこからメモリコントローラへ渡されます。
- 書き込み処理:
- コントローラーはラインをアクティブにし、1 セクタずつ 32 バイト+バイトマスク付きで送信します¹²。
- RTX 4090 の GDDR6X はバイトマスクを直接適用しますが、ECC を備えた HBM はコードワード単位での処理となるため異なる挙動を示します。
6. ストアの可視性(Visibility)とフェンス
結論:ストア指令の時間的特性
- LDG.E(ロード):約255 ns。ワープはこの間待機。
- STG.E(ストア):約6 サイクルで完了。しかし、データはキャッシュ内に留まり、永続化には時間がかかります。
フェンス(membar)の役割
ストアが永続的に書き込まれるまでのタイミングを知るための指令です。スコープごとに 3 つあります。
| 種類 | PTX 命令例 | 動作範囲 | 備考 |
|---|---|---|---|
| CTA 内 | | ワープ自身のブロック内 | 共有 L1 で可視化(約 1 ns / 3 サイクル) |
| チップ全体 | | チップ上の全 SM | L2 で合流し、グローバルに可視(約 140 ns) |
| システム全体 | | ホスト含む全体 | NVLink/PCIe での整合性確保(約 1 µs) |
内部状態と展開
- CTA 内:ストアが L1 に引き渡された時点で定義上可視。
を含む展開が可能ですが、ERRBAR ; CCTL.IVALL
はそれ自体で十分です¹⁴。membar.cta - チップ全体:L2 で ACK 受信時点でグローバルに可視。読み取り側が L2 から取得する必要があるため意図的動作が必要¹⁵。
はmembar.gl
に展開されます。MEMBAR.SC.GPU ; ERRBAR ; CCTL.IVALL
- システム全体:順序付け問題のため時間がかかります。
に展開され、全スレッドのロードが自身の L1 を無効化する指令も含まれます¹⁵。MEMBAR.ALL.SYS
データ転送の実例
- カーネル内で
を明示的に使用しなくても、ドライバはカーネル終了時にシステムスコープのフェンスをスケジューリングします¹⁶。membar.sys - これは
がデータを永続化させるセマンティクスと合致しています。cudaMemcpyDeviceToHost
補足:脚注と詳細データ
¹ SM の出口制限:16 ウェイの L2 現行ラインへのストアループ周りで計測。ポート数字はブロック当たりワープ数とレーン当たりバイト数のスイーププラトー(84 GB/s)です。
² セクタ数報告:
ncu はストアリクエストあたりのセクタ数を報告。ストライド 32 バイトの場合、各スパンに対応する 32 セクタとなります。
³ L1 の書き込みポリシー判定: - Write-around: L1 に割り当てず(除外)。 - Write-back: ストア時に転送しないが dirty マーク(除外)。 - Write-through: ミス時に転送し、ヒット時にも更新・保持。SM 側のカウンターがこれを確認。
⁴ アドレス翻訳とプロファイリング:512 MB のスイープにおいて、4,194,304 の書き込みリクエストが確認された(各リクエスト 4 セクタ)。
⁵ 置換アルゴリズムの特定方法: - ワイバックバッファ: dirty ラインの数と FIFO キューの挙動を確認。 - LRU タイブレイク: 17 番目のラインを挿入した時の退避動作で LRU を確認。 - RRPV 値: 生存曲線から RRPV がどう変動するかを予測モデルに適合させ、アルゴリズムを検証。
⁶ メモリーコントローラ飽和:各コントローラが保持できる 55 GB/s の限界を超えるとストールが発生する可能性。
⁷ 書き込みバイトの整合性:1 バイト書き込みスイープと全ラインスイープで送信される DRAM バイト量は同じ。
⁸ フェンスコスト計測方法:ループ内 N 個のストアを発行し、フェンス後にもう一度発行的比較(スロープ)。
⁹ システムスコープフェンスのオーバーヘッド:
CWD_MEMBAR_TYPE = L1_SYSMEMBAR を使用。待機コストは membar.sys と同等。事前フェンスすると待機が見えなくなる(同じ状態を待っているため)。