モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
GWAW.JP / WEBGPU × FINANCE / MONTE CARLO ②
SPEED WEBGPU で学ぶモンテカルロ法

CPU vs WebGPU
並列化の威力を測る

基本編で実装したモンテカルロ法を、CPUとWebGPUで計測して比較します。 数百万パスでGPUが圧勝する様子と、その鍵となる「並列リダクション」を学びます。

CPU WebGPU 並列リダクション ベンチマーク atomics
← 前回:① 基本編 ヨーロピアン・オプションと Black-Scholes
01

概要

シリーズ第2回(速度編)です。基本編では「乱数が理論値に収束する」ことを確認しました。 今回は同じ計算をCPUとWebGPUで実行し、速度差を測定します。 さらに、全パスの平均をGPU上で高速に集計する並列リダクションを実装します。

💡 この記事で学べること
CPU vs WebGPU:パス数を変えた時の実行時間・スループット比較
並列リダクション:全パスの合計をGPU上で求める手法(atomics / 多段集計)
最適化の効果:ナイーブ実装(CPU集計)との速度差
🎯 シリーズ内での対比
記事⑤⑥では「小さいモデルはCPUが速い」という結果でした。 しかしモンテカルロ法は並列性が極めて高いため、今回はWebGPUが明確に勝ちます。 「どんな計算でGPUが効くのか」を、対照的な事例で理解できます。

02

動作環境・注意事項

Chrome 113+✔ 推奨。CPU/WebGPU 両方計測可能
Edge 113+✔ 動作します
Firefox✘ WebGPU 未対応。CPU のみ計測
Safari 18+△ 実験的対応
計測精度△ 他タブ・他アプリの負荷で変動。傾向を見てください
大きいパス数△ CPUは1000万パスで数秒かかります
03

背景:なぜ計算速度が重要か

オプション価格評価の速度は、実務で直接的な価値を持ちます。 「正確だが遅い」計算は、変化の速い市場では使い物になりません。

実務 01

リアルタイム評価

マーケットメイカーは刻々と変わる市場で、無数のオプションを瞬時に再評価し続ける必要があります。

実務 02

大量シナリオ計算

リスク管理(VaR等)では、数千〜数万のシナリオ × 多数の銘柄を反復計算します。

実務 03

キャリブレーション

モデルパラメータを市場価格に合わせ込む作業は、価格計算を何百回も繰り返します。

04

速度比較コンソール

▶ SPEED BENCHMARK CONSOLE 確認中...
対象
初期化中...
MAX SPEEDUP
CPU比(最速GPU)
PEAK THROUGHPUT
REDUCE vs NAIVE
集計方式の差
RUNS DONE

EXECUTION TIME vs PATHS(LOG-LOG, LOWER IS BETTER)

THROUGHPUT(PATHS/SEC, HIGHER IS BETTER)

PATHS CPU (ms) GPU Naive (ms) GPU Reduce (ms) SPEEDUP
RUN BENCHMARK を押して計測を開始してください
※ CPU は 500万パスを超えると計算に数秒かかり UI が固まるため、SKIP (>5M) として計測を省略します。GPU は全パス数で計測します。
05

動作原理:並列リダクション

基本編では各パスのペイオフをバッファに書き出し、CPU側で合計していました。 しかしパスが数百万になると、この「GPUからCPUへの全データ転送 + CPU集計」がボトルネックになります。 そこで合計をGPU上で求めるのが並列リダクションです。

3141 5926
↓ 隣接ペアを加算(並列)
45148 ····
↓ さらに加算
922 ··
31
8要素の合計を log₂(8)=3 ステップで計算。要素が増えても段数は対数的にしか増えない。
方式 A

ナイーブ(CPU集計)

GPUで全ペイオフを計算 → 全データをCPUに転送 → CPUでforループ合計。基本編の方式。転送量が大きいと遅い。

方式 B

並列リダクション

各ワークグループ内で部分和を計算し、atomicAdd で全体の合計に足し込む。CPUに戻すのは「合計値1つだけ」。転送がほぼゼロ。

鍵となる技術

workgroup 共有メモリ

ワークグループ内のスレッドが共有メモリで部分和を集計。最後に1スレッドが atomicAdd で全体に反映する。

06

コードのポイント解説

並列リダクションの実装を中心に解説します。基本編の乱数生成・株価計算はそのまま使います。

workgroup 共有メモリでの部分和
// ワークグループ内で共有するメモリ(64スレッド分)
var<workgroup> partialSum: array<f32, 64>;

// 各ワークグループの部分和を書き出す配列(要素数=ワークグループ数)
@group(0) @binding(0) var<storage, read_write> partials: array<f32>;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u,
        @builtin(local_invocation_id)  lid: vec3u,
        @builtin(workgroup_id) wid: vec3u,
        @builtin(num_workgroups) nwg: vec3u) {
  let totalThreads = nwg.x * 64u;

  // ① このスレッドが担当する全パスのペイオフを合計(grid-stride)
  var localSum = 0.0;
  var i = gid.x;
  loop {
    if (i >= p.numPaths) { break; }
    localSum = localSum + computePayoff(i);  // 乱数→株価→ペイオフ
    i = i + totalThreads;
  }
  partialSum[lid.x] = localSum;

各スレッドはまず自分の担当パスのペイオフを localSum に集めます。 grid-stride で複数パスを担当するので、ここで既にかなりのデータが圧縮されます。

ツリー状の集計とバリア同期
  // ② ワークグループ内でツリー状に合計(64→32→16→...→1)
  workgroupBarrier();
  var stride = 32u;
  loop {
    if (stride == 0u) { break; }
    if (lid.x < stride) {
      partialSum[lid.x] = partialSum[lid.x] + partialSum[lid.x + stride];
    }
    workgroupBarrier();  // 各段で全スレッドの完了を待つ
    stride = stride / 2u;
  }

  // ③ 代表スレッド(lid.x==0)が全体合計に加算
  if (lid.x == 0u) {
    // f32 を u32 にスケール変換して atomicAdd(後述のハマりどころ)
    atomicAdd(&globalSum, u32(partialSum[0] * 1000.0));
  }
}

ハイライト部分がツリー状リダクションの核心です。 stride を半分ずつ減らしながら、各段で隣接要素を加算します。 workgroupBarrier() で全スレッドの足並みを揃えないと、 まだ書かれていない値を読んでしまい結果が壊れます。

ワークグループ部分和の書き出し(atomics回避)
  // ③ 代表スレッドが「このワークグループの部分和」を配列に書き出す
  if (lid.x == 0u) {
    partials[wid.x] = partialSum[0];  // atomic不要・f32精度を維持
  }
}

// CPU側: 部分和の配列を合計(要素数 = ワークグループ数 ≤ 65535)
let sum = 0;
for (let i = 0; i < numWG; i++) sum += partials[i];
const price = (sum / numPaths) * Math.exp(-r * t);

当初は atomicAdd で1つの合計に足し込む設計でしたが、 WGSLのatomicは整数型のみで、f32を固定小数点化すると大パス数で u32オーバーフローが起きます(平均50・2000万パスで合計1e12 ≫ 43億)。
そこで各ワークグループの部分和を配列の別スロットに書き出し、 CPU側で合計する方式にしました。書き出すのは最大65,535個だけなので、 CPU合計も一瞬です。GPUで大部分を圧縮し、最後の僅かな集計だけCPUが担う実用的な折衷案です。

07

ハマりどころまとめ

PITFALL 01

f32 の atomic 不可

WGSLのatomicは整数型のみ。f32合計をatomicでやろうと固定小数点化すると、大パス数でu32オーバーフローします。部分和を配列に書き出す方式が安全です。

PITFALL 02

workgroupBarrier 忘れ

リダクションの各段でバリアを忘れると、未書き込みの値を読んで結果が壊れます。エラーは出ません。

PITFALL 03

計測の同期

submit() 直後の時刻はコマンド送信時刻。mapAsync または onSubmittedWorkDone を await して実際の完了を待ちます。

PITFALL 04

ワークグループ数の上限

dispatchWorkgroups は最大65,535。これを超えるパス数は grid-stride で1スレッドが複数パスを担当して対応します(基本編参照)。

PITFALL 05

ウォームアップ

初回実行はシェーダコンパイルを含むため遅い。複数回実行の中央値/最小値で評価します。

PITFALL 06

CPUの巨大パス

CPUは1000万パスで数秒かかり、UIが固まります。大パス数ではCPUを自動スキップする配慮が必要です。

⚠️ 免責事項
本デモは WebGPU とモンテカルロ法の学習・技術実験を目的としたものです。 計測値はデバイス・ブラウザ・負荷状況により変動します。 価格モデルは理論上の仮定に基づく簡略化されたものであり、 実際の投資判断・オプション取引の根拠として使用しないでください。
NEXT — シリーズ第3回
③ 応用編:エキゾチック・オプション — 解析解のない世界へ

『WebGPUで学ぶモンテカルロ法② 速度編 CPU vs WebGPU』を公開しました。