HN 日本語サマリー

← 一覧へ戻る
プログラミング

GPUがメモリに書き込む際に何が起こるか

What happens when a GPU writes memory (blog.doubleword.ai)

60 pointsby ibobev1 コメント

要約

この記事では、GPUの命令であるSTG.Eがグローバルメモリにデータを書き込むプロセスを詳細に追跡します。命令がワープから発行され、LSU、Coalescer、L1キャッシュ、クロスバーを経てL2キャッシュに到達するまでの各段階での処理と、L2キャッシュでのデータの保持、そして最終的にDRAMへの書き戻し(エビクション)のメカニズムについて解説しています。

全文翻訳

GPUがメモリに書き込むとき何が起こるか

前回の投稿(こちら)では、1つのLDG.E命令がRTX 4090のL1、翻訳、クロスバーを経てL2スライス、そしてDRAMへとどのように到達するかを追跡しました。リクエストはその結果を取得し、ウェイステーションを戻り、ワープに結果を返してカーネルの実行を続行しました。

そのカーネルはベクトル加算を実行していました。同じカーネルは、両方のベクトルの要素をロードした後、それらを加算し、結果をストアします。

/*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 ; 

STG.Eは、計算された合計をグローバルメモリに書き戻す責任を持つ命令です。この記事では、STG.Eが同じウェイステーションをどのように通過するかを追跡し、各ステップで何が起こるかを解明します。前回と同様、この情報はすべて公開されているわけではありません。公開されていない場合は、新しい実験を行います。

状況設定:LDG.Eはワープに戻り、FADDはR4とR3の内容をR9に加算しました。そして今、そのレジスタの内容をストアする必要があります。ワープはそのサブパーティション内で実行可能になり、そのレーンはSTG.Eの実行を開始します。

ワープからの出発

STG.E [R6.64], R9は、レジスタR9の32ビットデータを64ビットアドレス(R6とR7で構成)にグローバルストアします。LDG.Eでは2行のレジスタファイルを読みましたが、STG.Eでは3行読み込む必要があります。ストアするデータと、そのデータのアドレスを構成する2つのレジスタです。

その後、命令はロード/ストアユニット(LSU)に発行されます。LSUは、オペコード(「これらのアドレスにストアせよ」)、アクティブなレーンの32ビットマスク、および計算された32個のアドレスを送信します。

SMはどのくらいの頻度でストアを発行できるか

1つのワープは、レーンの発行数に関係なく、約6.1サイクル(2.6 GHzで2.3 ns)ごとに新しいSTG.E命令をレジスタ/LSU/コレアサー/L1にプッシュできます。

SMからの出口は、サイクルあたり32バイトのストア(またはロード)を維持できるため、すべてのワープが発行している場合、ここでボトルネックが発生します1。

次の停止地点はコレアサーです。その仕事は、32個の4バイトアクセスを受け取り、それを達成可能な最小数の32バイトセクターに変換することです。私たちのカーネルは128バイト連続して書き込むため、それは4つのセクター、つまり1つのラインになります2。

ロードは常にセクターあたり32バイトをプルインし、LSUで結果がフィルタリングされて、SASSが実際に要求したものをレジスタに書き込みます。ストアの場合、各セクターリクエストにはバイトマスクが発行され、この命令がセクターのどのバイトを書き込んでいるかを示します。

4つのセクター、4つのマスク、および128バイトのデータがL1に進みます。

L1を通過する

4つのセクターはL1キャッシュに到達します。前回示したように、L1キャッシュはSMごとに4-wayセットアソシアティブで、仮想インデックス、仮想タグ付けされています。ラインが存在するかどうかに関わらず、セクター、そのマスク、およびデータはL2に向かってまっすぐ進みます。L1はライトスルーです3。

ストアがそのセットにスペースを必要とする場合、古いスロットは厳密なLRU順に空けられます。

L1の下で、ストアの仮想アドレスが変換されます(読み取り投稿の翻訳セクションを参照)。そして、4つのセクターはクロスバーを横切り、そのラインを所有するL2スライスに移動します。

L2に到着

リクエストはクロスバーを介して36個のL2スライスの1つに送信されます。スライスは、前回リバースエンジニアリングした物理アドレスの同じ関数によって選択されます。

それはラインのアドレス、最大4つのセクターのデータ、およびそれらのセクターのマスクを運びます。ストアはラインごとに1つのリクエストを発行します4。

スライス内では、各キャッシュは1024セットを持つ16-wayセットアソシアティブで、物理アドレスによってハッシュされます。ラインが既に存在する場合、バイトマスクによって選択されたバイトがスロットに書き込まれ、セクターはダーティとしてマークされます。ラインが存在しない場合、スライスはそのためのスロットを見つける必要があります。それを行うには、他のラインのバイトをDRAMに追い出す必要があるかもしれません。場所が見つかると、バイトがスロットに書き込まれ、マスクはセクターのどのバイトが有効かを示します。バイトがスロットに入ると、スライスはSMに確認応答をクロスバー経由で返します。

確認応答の後、4つのセクターはそのスロットにダーティとしてマスクの下に配置されます。

書き込みの完了

確認応答はクロスバーを介して、ストアを送信したSMに戻ってきます。ストアを発行したワープはとっくに先に進んでいます。実際、ここではカーネル全体が終了しています。そのため、確認応答はLSUに到達し、そこで消費されます。

実際、このカーネルでは、データはDRAMに到達しません!私たちの元の投稿のカーネルは、これらの書き込まれた結果をcudaMemcpyDeviceToHostで読み戻し、L2からPCIeバスを経由してホストDRAMに直接取得します。それ自体が興味深いスレッドであり、別の日に追うべきものです。

ハードウェアは現在、L2にダーティなデータを保持しています。STG.E命令は完了しました。これが意味を持つためには、後続のカーネルからのデータへのポインタがそこでデータを見つける必要があります。しかし、それはキャッシュ内のダーティな状態であり、まだDRAMにヒットしていません。どのようにしてDRAMに到達するのでしょうか?いつ旅行するのでしょうか?

ストア命令のその後

L2はチップのすべてのトラフィックのシリアライゼーションポイントとして機能しますが、ある時点でスペースがなくなると、何かを追い出す必要があります。

私たちのデータはL2にあります。別のカーネルが私たちのカーネルの後に実行されます。そのカーネルはデータを読み取ったり書き込んだりするかもしれません。私たちのラインを必要とするかもしれませんし、他のラインを必要とするかもしれません。そのカーネルが実行されるにつれて、私たちのデータはDRAMに到達する必要があります。

データがDRAMに到達する方法

私たちのデータの各ラインは、2ビットの「再参照予測値」(RRPV)5とともに格納され、00、11、または22のいずれかに設定されます。これは模式的なもので、ハードウェアに実際に0、1、または2という数値が現れるわけではありませんが、タイミング実験により3つのレベルがあることが判明しています。数値が低いほど、キャッシュはそのラインがすぐに再度使用されると予測していることを意味します。私たちのキャッシュラインは、挿入されたばかりの11にあります。

各ラインには「ダーティマスク」があり、L2がこのバージョンのラインが存在する唯一の場所であるかどうかを示します。私たちのラインはすべて完全にダーティです。各ラインには、最後の使用と最後のストアのカウンターもあります。

ラインに対する新しいロードまたはストアが発生した場合、どうなるでしょうか?

  1. レジデントヒットの場合:ロードが私たちのラインの1つを探して入ってくると、それを受け取ります。ラインのRRPVは00に設定されます。ストアが入ってきてラインにヒットした場合、そのセクターがラインにヒットし、「ダーティマスク」が更新されます。
  2. ミスの場合:L2は、ミスした値をキャッシュにロードする必要があります。それを行うには、犠牲者を見つける必要があります。

置換ポリシーは次のようになります。

まず、セット内のすべての16ウェイをスキャンして、RRPV=2のものを探します。つまり、キャッシュがもう使用されないと考えているものです。見つからない場合は、すべてのRRPV値をインクリメントし、再度スキャンします。RRPV=2の値の中から、最も最近使用されていないものを選択します。

次に、犠牲者をどうするかを決定します。犠牲者がダーティな場合、つまりL2が唯一存在する場所であり、それを追い出すとすぐにDRAMと通信する必要がある場合、まだキャッシュから追い出しませんが、それをクリーンアップし始めます。唯一の方法は、ダーティセクターをメモリコントローラーに渡してDRAMに書き戻すことです。ラインはセットのFIFOライトバックバッファに入り、読み取り可能のままになります。その後、クリーンな犠牲者を探してウェイを再度スキャンします。1つ見つかると、それを追い出してウェイを奪います。

新しいラインはそのウェイにスロットインされ、RRPVは1に設定されます。

セットをクリーンに保つ

上記で説明したポリシーは、新しいデータのためにスペースを空けるための追い出し方法を管理します。しかし、ダーティなレジデントラインにヒットし続けると、ポリシーはそれらを書き戻す方法がなくなります。

セットに≥8個のダーティライン(16個中)が含まれているストアの場合、上記のポリシーを実行する前に、まずセット内の最も最近ストアされたダーティラインを見つけ、それをクリーンアップします。ダーティセクターをメモリコントローラーに送信し、クリーンとしてマークします。

このような追加ルールが必要な理由を理解するために、それがなければどうなるかを想像してみてください。ダーティラインでいっぱいのセットを考えます。ミスが発生し、DRAMからのフィルが必要になると想像してください。上記のポリシーによれば、最も古いダーティなものを選択し、ライトバックバッファに送信します。次に、次に古いダーティなものを選択し、ライトバックバッファに送信します。次に、さらに古いもの、すべてダーティなもの、すべて書き戻しを開始し、すべてライトバックバッファに配置されます。すべての16ラインが一度に書き戻され、DRAMトラフィックの単一のバーストになります。バッファがドレインするまで、このセットは次の数回のミスに対してほとんど容量がありません。さらに悪いことに、書き戻しのバーストはメモリコントローラーでキューイングされるため、そのコントローラーの他の読み取りまたは書き込みはすべてキューの後ろに並びます。

8-ダーティルールは、クリーンな犠牲者を見つけ、DRAMへのトラフィックの流れをスムーズにするために、ラインを積極的にクリーンアップする清掃員として機能します。

上記ポリシーによって実行される1つのL2セット。16行の出力を書き込むカーネルと、32行の入力をストリーミングするカーネルを実行します。ラインをクリックして読み取ります。ラインの下の点はRRPVです。

dirty 0/16 written back 0

書き戻し

これらの書き込みは、メモリコントローラーの書き込みバックログがいっぱいになるまで、L2への受信ストアとは非同期に実行できます。その時点で、新しいストアは停止する必要があります6。

書き込みが実行されると、セクターはメモリコントローラーに送られ、そこからDRAMチップに書き込みとして送られます。コントローラーは行をアクティブにし、ロードの場合と同じように、セクターあたり32バイトを同じ16ピンを逆方向に発行します。バイトマスクにより、書き込まれたバイトのみが格納されます7。4090のGDDR6Xはバイトマスクを直接適用します。ECC付きHBMは、コードワードで動作するため直接はできません。そのため、読み取り、マージ、書き戻しによって部分的な書き込みを行います。

結論

LDG.Eは255 nsかかり、ワープは最後まで待機しました。STG.Eはわずか6サイクルで、ストアを開始するのに十分な時間です。

ストアには長い余生があります。ワープがそれを読み戻したい場合を除き、ワープは幸せに無関心なまま、ユニットを通過していきます。L1を通過し、L2に到達すると、発行したSMに信号を返します。L2では、ダーティとして配置され、新しく登場するストアやロードによって出口に向かって押されます。やがてミスが犠牲者としてそれを選び、メモリコントローラーを介してDRAMに送り出され、書き込んだワープが消えたずっと後に着地します。

カーネルが128 MBの出力を書き込み、次に96 MBの入力をストリーミングするカーネルの下でのダイ。L2はセットごとに1ピクセルで、16ラインのうち何ラインがダーティかによって色付けされています。

SM (128)

クロスバー

L2 (36スライス、コントローラーあたり3つ)、セットごとに1ピクセル:16ラインのうち何ラインがダーティか

メモリコントローラー:書き込みバックログ、読み取りバックログ

GDDR6X、12チップ:現在何をしているか

t 0 µs stored 0 MB dirty in L2 0.0 MB written to DRAM 0 MB SMs throttled 0/128

付録:可視性

書き込みが可視になるのはいつか

私たちのSTG.Eは「ファイア・アンド・フォーゲット」でした。ワープはそれを発行し、プログラムは無造作に終了しました。これは一般的に便利な機能です。ストアを発行したカーネルは、ストアが完了するのを待たずに他の作業を進めることができます。しかし、注意が必要です。命令が完了してもデータが永続的に書き込まれるわけではないため、書き込みがいつ完了したかを知る方法が必要です。その答えはフェンスです。

フェンスは、発行したすべてのストアが可視になるまでワープを待機させる命令です。スコープごとに3つあります。membar.ctaはストアがワープ自身のブロックに対して可視になるのを待ちます。membar.glはチップ上のすべてのSMに対して待ちます。membar.sysはホストおよび他のデバイスに対しても待ちます。これらはPTX命令であり、複数のSASS命令に展開されます。

私たちのブロックへ:L1で合流

ワープ上のmembar.ctaは、同じCTA内のストアが可視になるまで待機します。SMとの間で出入りするすべてのメモリトラフィックのコヒーレンスポイントは共有L1です。したがって、membar.ctaは、ストアがL1で可視になるのを待つだけで済みます。L1に正常に引き渡された時点で、定義上、可視になります。membar.ctaは、STG.Eに約1 nsまたは3サイクルを追加します8。

すべてのSMへ:L2で合流

membar.glは、ストアがチップ上のすべてのSMに対して可視になるのを待つ必要があります。これが可能であるためには、これらのSMに何もブロードキャストする必要はありません。なぜなら、SM自体と同様に、すべてのトラフィックが流れるボトルネックがあるからです。したがって、L2に書き込みを配置し、確認応答を受け取ると、それがグローバルに可視であることがわかります。membar.glは約140 nsで完了します。これはL2と同じ時間です。

これは原理的にのみグローバルに可視です。それを取得するために、リーダーはL2から取得する必要があります(自身のL1は古いラインを保持する可能性があります)。これは意図的に行う必要があります。LDG.E.STRONG.GPUのような命令はL1をバイパスします。ld.acquire.gpuのような命令は、このような可視性ネゴシエーションのために設計されており、SASSにコンパイルされるとCCTL.IVALLが含まれ、そのSM全体のL1を無効にします。

フェンス自体は双方向です。それらは、そのスレッドの後続のロードがそのスレッドが書き込んだデータを見ることを保証する責任もあります。そのため、L1を無効にする命令も含まれています。cuobjdump -sassでプローブすると、membar.glはMEMBAR.SC.GPU ; ERRBAR ; CCTL.IVALLにコンパイルされ、membar.sysはMEMBAR.SC.SYS ; ERRBAR ; CCTL.IVALLに、fence.acq_rel.gpuはMEMBAR.ALL.GPU ; ERRBAR ; CCTL.IVALLに、ld.acquire.gpuはLDG.E.STRONG.GPUに続いて同じCCTL.IVALLにコンパイルされます。membar.ctaは、単なるMEMBAR.SC.CTAです。

全世界へ:membar.sys

membar.sysは、ストアを世界の他の部分(NVLink経由のピア、PCIeまたはNVLink-C2C経由のホストなど)に対して順序付けます。より狭いスコープでは、コヒーレンスポイントを見るだけで達成できましたが、ここでは順序付けの問題に答えるのが難しいため、時間がかかります。membar.sysは約1 µsかかります。

私たちのストアの読み取りは何を待っていたか?

私たちのカーネルのPTXにはフェンスが含まれていません。データを書き込んで終了しただけです。必要なセマンティクスはmembar.sysとまったく同じです。cudaMemcpyDeviceToHostは、データがシステム全体に可視になるのを待つ必要があります。

実際、ドライバはカーネルの最後に同様のシステムスコープのメンバをスケジュールしました9。そのメンバが解決すると、データはホストに可視になり、画面に表示するために送信できます。

脚注

  1. ここでのタイミングは、L2に常駐するラインへの16個(アンロールされた)ストアのループの周りに%globaltimerを使用したものです。結果がレーンの発行数に依存しないことは容易にわかります。ポート数は、ブロックあたりのワープ数とレーンあたりのバイト数のスイープのプラトーです:84 GB/s、またはサイクルあたり32バイト。↩
  2. ncuは、l1tex__t_sectors_pipe_lsu_mem_global_op_st.sum対l1tex__t_requests_pipe_lsu_mem_global_op_st.sumとして、ストアリクエストあたりのセクター数を報告します。レーンあたり4バイトをストライド1でストアする1ワープカーネルは、リクエストあたり4セクターを生成します。ストライド32バイトでは、レーンが触れる個別のスパンごとに1つずつ、ライン全体に丸められることなく32を生成します。↩
  3. ここには3つの合理的な選択肢があります:1. ライトアラウンド:データはL1キャッシュを通過しますが、そこにアロケーションされることはありません。ヒットした場合、L1のデータを無効にするか、更新します。L1スロットはデータの読み取り時にアロケーションされます。2. ライトスルー:ミスの場合、転送し、スロットに書き込みます。ヒットの場合、転送し、スロットを更新します。3. ライトバック:ミスの場合、L1にアロケーションし、スロットをダーティとしてマークします。転送しません。ヒットの場合、コピーを更新し、ダーティとしてマークし、転送しません。その後、追い出し、フェンス、またはカーネル終了時に、すべてのダーティセクターを転送します。

それを判断するために、3つの質問に答えます。

ミス時のストアはL1にアロケーションするか?:コールドL1を用意し、いくつかのラインをストアしてから、同じラインをポインタチェイスします。アロケーションする場合、すぐにL1ヒットが発生します。アロケーションするので、ライトアラウンドではありません。

L1ヒット時、ストアはL1コピーを保持するかドロップするか?:64ラインのポインタチェイスをウォームアップし、約15 ns(L1)で実行されるようにします。各ホップの前に、直前にアクセスしたラインの異なる単語にストアします。そのラインは64ホップ後に再びラウンドしてきますが、まだ約15 nsです。ストアがコピーをドロップした場合、約114 ns(L2)になります。したがって、ヒット時にはコピーを保持し、更新します。

ストアはいつL2に送信されるか?書き込み時か、追い出し時か?:SM側の書き込みリクエストカウンターlts__t_requests_srcunit_tex_op_writeは、あらゆる種類のラインへの書き込み中に、ストアごとに1つのリクエストをカウントします。

したがって、私たちは2を行っています:アロケーションと更新を伴うライトスルー。↩
4. 512 MBのスイープで、ncuはSMからL2に到着する書き込みリクエスト(lts__t_requests_srcunit_tex_op_write)を4,194,304件、セクター(リクエストあたり4つ)を16,777,216件カウントします。これは、バイトレイアウトに関係なく発生します。ワープあたりの全ライン、セクターあたりの1つの4バイトワード、またはセクターあたりの1バイトです。↩
5. このステートマシンの全体像を把握するのは非常に複雑な作業です。一般的なプローブは次のとおりです。既知のラインでセットを埋めます。次に、ターゲットラインを挿入し、そのターゲットラインに干渉するラインをセットにストリーミングし、元のラインがミスするまでのラインのストリーミング数をカウントします。それを使用して、置換アルゴリズムのさまざまな側面を次のように特定できます。

  1. ライトバックバッファ:セットをDD個のダーティラインで埋め、ターゲットラインをそれらの隣に配置します。次に、ダーティラインは、クリーンなラインに対するミスが発生する前に、追加の2D-12D-1個の干渉フィルを生き残ります。DDは11から88までです。これは、ダーティラインがセットごとのキューに入り、2回のフィルごとに1つのエントリが進むことで説明できます。クリーンなラインはすぐに追い出されます。アイデアは、DDラインもターゲットラインの前にダーティキューに入るということです。因子22は、ドレインレートが2回のフィルあたり1であることを意味します。
  2. LRUによるタイブレーク:16個の異なるラインを順番にセットに埋め込み、17番目を挿入して、何が追い出されるかを調べます。次に、セット内のさまざまなラインにタッチし、再度実行します。決定論的に、FIFOではなく、最も最近タッチされたラインが追い出されます。
  3. RRPV値はどこに挿入されるか:異なるRRPV値は、ロードミスおよびストアミス下での新しいラインの異なる動作を予測します。RRPV=0で挿入されたラインはより長く持続し(より多くのエイジングラウンドを経る)、RRPV=2は短く持続します(すぐに追い出し対象になります)。ストリーミングテストを構築すると、結果はロードとストアが1で挿入されることと一致します。

次に、楽器を外科的に使用するのではなく、サバイバルカーブを構築できます。データを挿入し、次に他のデータを大量に挿入し、意味のあるパラメータ(セット内のダーティラインの数、後続のフィルの間隔、ストリーミング開始前のターゲットへのヒット数)を変更します。次に、L2のアナログモデルを構築することで、それらのサバイバルカーブに適合させることができます。テキストのアルゴリズムは、このデバイスで構築したすべてのサバイバルカーブに適合するものです。↩
6. ncuはLSU入力キューでのストールを検出しますが、メモリコントローラーでのバックアップがそれをトリガーすると考えています。同じメモリコントローラーを共有するスライス間のストアは、各コントローラーのDRAMが維持できる55 GB/sで飽和します。↩
7. 512 MBのスイープでセクターあたり1バイトを書き込む場合と、全ラインを書き込む場合で、同じDRAM書き込みバイトが送信されることをncuは示しています。↩
8. フェンスコストを取得するには、%globaltimerの呼び出しで囲まれたループでN個のストアを発行し、次にフェンスで区切られた同じN個のストアを発行し、スロープを比較します。↩
9. 以前の投稿のテクニックを使用して、プッシュバッファからCWD_MEMBAR_TYPE = L1_SYSMEMBARを読み取ることができます。その待機時間は、membar.sysで測定した時間とほぼ同じです。グリッドの終わりに約0.74 µsのオーバーヘッドがありますが、カーネルがシステムスコープのトラフィック(ストア、またはピン留めされたホストメモリのロード)を持っていた場合にのみ発生します。カーネル内でmembar.sysで事前にフェンスする場合、待機時間は表示されません。これは、内部的にはこれらの2つのフェンスが同じ状態を待機していることを示唆しています。↩