RustではじめるCPUとGPU

GPUの計測

この章でわかること:

  • GPU自身の時計で計測するタイムスタンプクエリの実装(wgpu)
  • 「カーネル4マイクロ秒、往復190マイクロ秒」という壁時計との乖離の実測
  • GPU計測に固有の注意点(周波数の変動、非同期性)
  • GPUプロファイラという上位の計測手段
  • なぜ体系に必須か: 8章で「計測なき最適化は 推測」と学びました。GPUの計測は壁時計では原理的に不正確で (12章の注記)、専用の方法を知らなければ Part VIの最適化を進められません

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

cd examples
cargo run --release -p ch26-timestamp

壁時計では不正確な理由

これまでのGPU計測は「submitしてから完了を待ち終えるまで」の 壁時計でした。この値には、コマンドの転送、キューの待ち、 完了通知、CPU側の同期(24章)がすべて 含まれています。大きなカーネルならこれらは誤差の範囲ですが、 小さなカーネルでは測定値の大半がカーネル以外の時間になります。

解決策は、GPU自身に時刻を記録させることです。

タイムスタンプクエリの実装

WebGPUのタイムスタンプクエリ(timestamp query)は、 コマンド列の特定の位置でGPUの内部時計を記録する仕組みです。 wgpuでの手順は4つです(完全なコードはリポジトリ参照)。

// 1. 機能を要求する(対応確認つき)
//    Features::TIMESTAMP_QUERY

// 2. 記録先のQuerySetを作る(タイムスタンプ2個ぶん)
let query_set = device.create_query_set(&wgpu::QuerySetDescriptor {
    ty: wgpu::QueryType::Timestamp,
    count: 2,
    label: None,
});

// 3. コンピュートパスの開始と終了で記録するよう指定する
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
    timestamp_writes: Some(wgpu::ComputePassTimestampWrites {
        query_set: &query_set,
        beginning_of_pass_write_index: Some(0),
        end_of_pass_write_index: Some(1),
    }),
    label: None,
});

// 4. 結果(u64×2)をバッファへ解決→読み戻し、目盛りをns換算する
encoder.resolve_query_set(&query_set, 0..2, &buf_resolve, 0);
// 経過ns = (t1 - t0) × queue.get_timestamp_period()

記録される値はGPU固有の「目盛り(tick)」なので、 queue.get_timestamp_period()(1目盛りが何ナノ秒か)を掛けて 時間に直します。

実測: 乖離を数値で確認する

同じカーネルを、大きな入力(1677万要素)と極小の入力(256要素)で 実行し、「GPU時計のカーネル時間」と「壁時計」を並べます。 筆者の実測(Apple M4)です。

16M要素 カーネル(GPU時計):  9.9〜11.3ms | 壁時計: 10.3ms
256要素 カーネル(GPU時計):    0.0043ms | 壁時計:  0.19ms

2行目が重要です。カーネルの実行は4.3マイクロ秒ですが、 壁時計は190マイクロ秒で、45倍の差があります。 11章で述べた、ベクトル加算の実行時間がAPIの往復時間に 比べて無視できるほど小さいという事実が、そのまま数値で確認できました。 壁時計だけを見て「このカーネルは0.2ミリ秒かかる」と結論して 最適化を始めたら、存在しない45倍分の遅さを削減しようとすることになります。 一方、大きな入力ではカーネル時間と壁時計がほぼ一致し、 往復のコストは誤差の範囲に収まります。

測る対象に合わせて計測手段を選びます。スループットを知りたいなら タイムスタンプクエリ、エンドツーエンドのレイテンシを知りたいなら 壁時計です。8章の「何を測っているかを知る」の原則の、GPU版です。

GPU計測の注意点

実験の生データには、もう1つ注意すべき現象が含まれていました。 16M要素の計測1回目だけ、カーネル時間(26.6ms)が壁時計(13.5ms)を 上回るという物理的にありえない値が出たのです(2回目以降は正常)。

原因は特定できていませんが、有力な候補は2つあります。 1つはGPUの周波数の変動です(GPUもCPUと同じく負荷に応じて クロックを上下させます。8章で見た 計測の注意点のGPU版です)。もう1つは、目盛りをns換算する係数 (get_timestamp_period)がバックエンドの近似値である可能性です。 いずれにしても実務の対策は同じで、ウォームアップを行い、 複数回測り、異常値を疑って除外します8章の 規則はGPUでもそのまま有効です。

もう1つの注意点は非同期性です。タイムスタンプは「パスの開始と終了」を 記録しますが、GPUは複数のパスを重ねて実行することがあります。 隣のパスと重なった時間も含まれるため、正確に測りたいカーネルは 単独のsubmitで測るのが安全です(24章の 重ね合わせは性能を改善しますが、計測の正確さを損ないます)。

プロファイラ: より詳細な分析の手段

タイムスタンプクエリは8章Instantに 相当する基本的な計測手段です。その先には、CPUのプロファイラに相当する 専用ツールがあります。

  • Xcode GPUデバッガ / Metal System Trace(macOS): パスごとの時間、占有率、帯域、シェーダのホットスポットまで GUIで分析できます。wgpuのMetalバックエンドの計測にも使えます
  • NVIDIA Nsight Systems / Nsight Compute: CUDA/Vulkan環境の 標準的なプロファイラです。Nsight Computeはカーネル1個を ルーフライン(10章)上にプロットする機能を持ちます
  • wgpu-profilerクレート: この章のタイムスタンプ処理をスコープ単位に自動化した既製品です

16章のtop-down分析に相当する体系 (演算律速か、帯域律速か、占有率不足か)は、これらのプロファイラの メトリクスで組み立てます。本書で学んだ用語(占有率、 コアレッシング、算術強度)が、そのままツールの表示を読むための 語彙になります。

まとめ

  • GPUの正確な計測はGPU自身の時計(タイムスタンプクエリ)で行います。 wgpuではQuerySet、timestamp_writesresolve_query_setを組み合わせる定型です
  • 実測では、極小カーネルの壁時計はカーネル実行時間の45倍でした。 スループットはGPU時計、レイテンシは壁時計と、目的で計測手段を選びます
  • GPUにも周波数変動があり、ウォームアップと複数回計測という 8章の規則がそのまま必要です(実測で異常値も観測しました)
  • 詳細な分析にはXcode GPUデバッガやNsightを使います。本書の用語が そのまま画面の読み方になります

Part VIはここまでです。最後のPart VIIでは、プログラムの外側にある OSの層のコストと、実務で性能を運用する方法論を扱い、体系を完成させます。