コンテンツにスキップ

転送と実行の重ね合わせ

この章でわかること:

  • GPU処理の全体時間を決める「同期点」という視点
  • 同じ計算・同じ転送量でも、同期の形だけで2.7倍変わる実測
  • パイプライン化とダブルバッファリングという設計パターン
  • 実務のGPUコードが「投げっぱなし」を基本形にする理由
  • なぜ体系に必須か — 11章12章は 「1回計算して1回待つ」形でした。実務のGPU利用は連続する仕事の 流れであり、その設計原則がなければGPUの知識は単発実験止まりです

この章の実験はリポジトリのexamples/ch24-overlapです。

Terminal window
cd examples
cargo run --release -p ch24-overlap

「待つ」がすべてを直列にする

Section titled “「待つ」がすべてを直列にする”

11章の実測を思い出してください。GPUの計算自体は マイクロ秒でも、コマンド送信と完了待ちの往復にミリ秒かかりました。 1回きりの計算ならこれは仕方のない固定費です。しかし、 仕事が連続してあるなら話が変わります。

データを16個のチャンク(塊)に分けて処理する状況を考えます。 各チャンクは「計算→読み戻し」の2段階です(データは事前に GPU側に置いてある想定です。転送も毎回行う場合は、この章の 話がさらに強く効きます)。素朴に書くとこうなります—— チャンク1のコマンドを送り、完了を待ち、読み戻して、 次のチャンクへ。この「待ち」の間、CPUは結果を眺めているだけ、 GPUは次の仕事が来るのを待っているだけ。 どちらかが常に手待ちです

3章のパイプラインとまったく同じ構図です。 CPUが命令を段階に分けて重ねたように、GPU処理も 「チャンクiの計算」と「チャンクi+1の準備」を重ねられるはずです。

16チャンク(各4MB)に同じ計算(要素ごとのハッシュ反復)を適用し、 結果をすべて読み戻します。計算量も読み戻しの量もまったく同じで、 同期の形だけを変えます。

  • (A) 1チャンクごとに同期 — 各チャンクで submitmap_asyncpoll(待機)→結果回収、を繰り返す
  • (B) 全投入→一括回収 — 16チャンクぶんのコマンドを 待たずに全部submitし、読み戻しのマップも全部仕掛けてから、 最後に1回だけ待って全結果を回収する

筆者の実測(Apple M4)です。

(A) 1チャンクごとに同期 : 30〜34ms
(B) 全投入→一括回収 : 約11.8ms

2.7倍。計算もデータも1バイトも変わっていません。 変わったのは「CPUがGPUを待つ回数」——16回が1回になっただけです。

(B)が速い理由は2つに分解できます。第一に、GPUの待ち行列が 絶えず埋まっていること。(A)ではチャンクの合間にGPUの手持ちが 毎回尽きます(コマンド往復のレイテンシがそのまま隙間になる)。 (B)では、キューに次の仕事が常に積まれているため、GPUは チャンクからチャンクへ切れ目なく進みます。第二に、CPU側の コマンド記録がGPUの実行と自然に重なることです(結果の集計は 今回の実装では完了後にまとめて行っています)。どちらも、 特別な仕掛けではなく「早く投げて、遅く待つ」だけで手に入ります。

この原理を実務の形に整理します。

投げっぱなしを基本形にする。 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の計算能力の現在の中心——行列エンジンと混合精度——を 扱います。自作カーネルの限界の先で、ハードウェアとライブラリが 何をしてくれるのかを実測します。