転送と実行の重ね合わせ
この章でわかること:
- GPU処理の全体時間を決める「同期点」という視点
- 同じ計算・同じ転送量でも、同期の形だけで2.7倍変わる実測
- パイプライン化とダブルバッファリングという設計パターン
- 実務のGPUコードが「送信して待たない」形を基本にする理由
- なぜ体系に必須か: 11章と12章は 「1回計算して1回待つ」形でした。実務のGPU利用は連続する処理の 列であり、その設計原則がなければ単発の実験より先に進めません
この章の実験はリポジトリのexamples/ch24-overlapです。
cd examples
cargo run --release -p ch24-overlap
「待つ」がすべてを直列にする
11章の実測を思い出してください。GPUの計算自体は マイクロ秒でも、コマンド送信と完了待ちの往復にミリ秒かかりました。 1回だけの計算ならこれは避けられない固定費です。しかし、 処理が連続してある場合は事情が変わります。
データを16個のチャンク(塊)に分けて処理する状況を考えます。 各チャンクは「計算→読み戻し」の2段階です(データは事前に GPU側に置いてある想定です。転送も毎回行う場合は、この章の 効果がさらに大きくなります)。単純に書くと、 チャンク1のコマンドを送り、完了を待ち、読み戻してから 次のチャンクに進む形になります。この待ちの間、CPUは完了を待つだけで、 GPUは次のコマンドが来るのを待つだけです。 どちらかが常に停止しています。
3章のパイプラインとまったく同じ構図です。 CPUが命令を段階に分けて重ねたように、GPU処理も 「チャンクiの計算」と「チャンクi+1の準備」を重ねられるはずです。
実験: 同期の形だけを変える
16チャンク(各4MB)に同じ計算(要素ごとのハッシュ反復)を適用し、 結果をすべて読み戻します。計算量も読み戻しの量もまったく同じで、 同期の形だけを変えます。
- (A) 1チャンクごとに同期: 各チャンクで
submit→map_async→poll(待機)→結果回収を繰り返します - (B) 全投入→一括回収: 16チャンクぶんのコマンドを
待たずにすべて
submitし、読み戻しのマップもすべて登録してから、 最後に1回だけ待って全結果を回収します
筆者の実測(Apple M4)です。
(A) 1チャンクごとに同期 : 30〜34ms
(B) 全投入→一括回収 : 約11.8ms
2.7倍の差です。計算とデータは同じままで、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章の規則はここでも同じです。
PCIe構成では効果がさらに大きい
この実験はユニファイドメモリ(10章)のMacで 行ったため、重なったのは主に同期の待ち時間でした。PCIe接続の 外付けGPUでは、転送そのものに時間がかかるため、 「チャンクiの計算とチャンクi+1の転送を重ねる」効果が さらに加わります(CUDAではストリーム、専用転送エンジンとして 明示的にサポートされています)。原則は同じで、 直列に見える処理から独立な部分を見つけて重ねます。
まとめ
- GPU処理の全体時間は、計算と転送だけでなく「同期点の数」で 決まります。実測では同期を16回→1回にしただけで2.7倍でした
- 基本形は「早く送信し、遅く待つ」です。submitの後は待たず、 結果は必要になる時点まで回収しません
- メモリと引き換えに重なりを作るのがダブルバッファリングで、 読み戻し自体をなくすのがGPU上のバッファでの直接接続です
- この章の原理は3章のパイプライン、19章の非同期I/Oと同型です。 待ち時間を他の処理と重ねて隠蔽することは、本書全体で繰り返し現れる 最も重要なパターンです
次章は、現在のGPUの計算能力の中心である行列エンジンと混合精度を 扱います。自作カーネルの限界を超える部分で、ハードウェアとライブラリが 何を提供するのかを実測します。