転送と実行の重ね合わせ
この章でわかること:
- GPU処理の全体時間を決める「同期点」という視点
- 同じ計算・同じ転送量でも、同期の形だけで2.7倍変わる実測
- パイプライン化とダブルバッファリングという設計パターン
- 実務のGPUコードが「投げっぱなし」を基本形にする理由
- なぜ体系に必須か — 11章と12章は 「1回計算して1回待つ」形でした。実務のGPU利用は連続する仕事の 流れであり、その設計原則がなければGPUの知識は単発実験止まりです
この章の実験はリポジトリのexamples/ch24-overlapです。
cd examplescargo run --release -p ch24-overlap「待つ」がすべてを直列にする
Section titled “「待つ」がすべてを直列にする”11章の実測を思い出してください。GPUの計算自体は マイクロ秒でも、コマンド送信と完了待ちの往復にミリ秒かかりました。 1回きりの計算ならこれは仕方のない固定費です。しかし、 仕事が連続してあるなら話が変わります。
データを16個のチャンク(塊)に分けて処理する状況を考えます。 各チャンクは「計算→読み戻し」の2段階です(データは事前に GPU側に置いてある想定です。転送も毎回行う場合は、この章の 話がさらに強く効きます)。素朴に書くとこうなります—— チャンク1のコマンドを送り、完了を待ち、読み戻して、 次のチャンクへ。この「待ち」の間、CPUは結果を眺めているだけ、 GPUは次の仕事が来るのを待っているだけ。 どちらかが常に手待ちです。
3章のパイプラインとまったく同じ構図です。 CPUが命令を段階に分けて重ねたように、GPU処理も 「チャンクiの計算」と「チャンクi+1の準備」を重ねられるはずです。
実験 — 同期の形だけを変える
Section titled “実験 — 同期の形だけを変える”16チャンク(各4MB)に同じ計算(要素ごとのハッシュ反復)を適用し、 結果をすべて読み戻します。計算量も読み戻しの量もまったく同じで、 同期の形だけを変えます。
- (A) 1チャンクごとに同期 — 各チャンクで
submit→map_async→poll(待機)→結果回収、を繰り返す - (B) 全投入→一括回収 — 16チャンクぶんのコマンドを
待たずに全部
submitし、読み戻しのマップも全部仕掛けてから、 最後に1回だけ待って全結果を回収する
筆者の実測(Apple M4)です。
(A) 1チャンクごとに同期 : 30〜34ms(B) 全投入→一括回収 : 約11.8ms2.7倍。計算もデータも1バイトも変わっていません。 変わったのは「CPUがGPUを待つ回数」——16回が1回になっただけです。
(B)が速い理由は2つに分解できます。第一に、GPUの待ち行列が 絶えず埋まっていること。(A)ではチャンクの合間にGPUの手持ちが 毎回尽きます(コマンド往復のレイテンシがそのまま隙間になる)。 (B)では、キューに次の仕事が常に積まれているため、GPUは チャンクからチャンクへ切れ目なく進みます。第二に、CPU側の コマンド記録がGPUの実行と自然に重なることです(結果の集計は 今回の実装では完了後にまとめて行っています)。どちらも、 特別な仕掛けではなく「早く投げて、遅く待つ」だけで手に入ります。
設計パターンとしての整理
Section titled “設計パターンとしての整理”この原理を実務の形に整理します。
投げっぱなしを基本形にする。 queue.submitは非同期です
(11章)。結果が必要になる瞬間まで、待たない。
待つ場所(同期点)はプログラムの構造で決まるので、
「どこで待っているか」を数えることが、GPUコードの
プロファイリングの第一歩です(26章で
計器を導入します)。
ダブルバッファリング。 この章の実験は全チャンクぶんの バッファを用意しましたが、メモリが厳しければ2組で十分です—— GPUがバッファ組1を処理している間に、CPUが組2へ次のデータを 書き込み、交互に入れ替える。ダブルバッファリング (double buffering)と呼ばれる古典で、画面描画(フロント/バック バッファ)から科学計算まで、あらゆる場所に現れます。 18章のバッファ再利用の教えと同じで、 確保し直すのではなく使い回すのが要点です。
連続する処理はGPUに置いたまま繋ぐ。 10章の原則の再確認です。「計算A→計算B」と 続くなら、Aの結果をCPUへ読み戻してBに再アップロードするのではなく、 Aの出力バッファをそのままBの入力に束ねます(バインドグループを 差し替えるだけです)。読み戻しは本当に必要な最後の1回だけ。
CPU側の並行との合成。 19章のasyncと
組み合わせる場合、map_asyncの完了をFutureに包めば、
GPUの完了待ちも「待ちの多重化」に乗ります。橋渡しの基本形は
「コールバックでチャネルの送信側を発火させ、受信側をawaitする」
です(wgpu公式のexamplesにこのパターンがあります)。
ブロッキングのpoll(wait)をtokioのワーカーで呼ばない——
19章の規律はここでも同じです。
- GPU処理の全体時間は、計算と転送だけでなく「同期点の数」で 決まります。実測では同期を16回→1回にしただけで2.7倍でした
- 基本形は「早く投げて、遅く待つ」。submitは待たない、 結果は必要になる瞬間まで回収しない
- メモリと引き換えに重なりを作るのがダブルバッファリング、 読み戻し自体を消すのが「GPUに置いたまま繋ぐ」です
- この章の原理は3章のパイプライン、19章の非同期I/Oと同型です。 「待ちを重ねて隠す」は本書を貫く最重要パターンです
次章は、GPUの計算能力の現在の中心——行列エンジンと混合精度——を 扱います。自作カーネルの限界の先で、ハードウェアとライブラリが 何をしてくれるのかを実測します。