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

gwaw.jp
 
GPU COMPUTE × RENDER · 技術ノート

計算が、そのまま絵になる
— WebGPU compute→render 直結

これまでの実験ノートは、WGSLで「計算」し、結果をCPUに読み戻してグラフ化してきた。今回は別の道を行く。GPUで計算した結果を、CPUに一度も戻さず、そのまま画面に描く。数千本のモンテカルロ経路が光の川となって広がり、オプション価格が立ち上がる——その実装を解説する。

WebGPUcompute shaderrender pipeline storage bufferprimitive restartGPUパーティクル

01計算の、その先へ — compute だけでなく render

これまでの実験ノートは、一貫して「計算」の話だった。モンテカルロ、行列積、エージェント市場——WGSLのコンピュートシェーダーでGPUに計算させ、その結果をCPUに読み戻して、Chart.jsでグラフにしていた。GPUは強力な「計算機」として使い、絵を描くのはCPU側のライブラリの仕事だった。

これまで:WGSL compute → CPUに読み戻し → Chart.js(CPU描画)

だがWebGPUには、もう半分の顔がある。render(描画)だ。GPUは計算だけでなく、グラフィックスを描くための装置でもある。そして両者は同じデバイス上にあるから、計算した結果をCPUに戻すことなく、そのまま描画に渡せる。これがGPUグラフィックスの王道であり、本稿で踏み込む新しい領域だ。

今回:WGSL compute → GPUバッファ共有(CPUを経由しない) → WGSL render

読み戻しが無いことの意味は大きい。数十万の点を毎フレームCPUに転送していては、リアルタイムなアニメーションは難しい。計算結果がGPUに留まったまま描画されるからこそ、数千本の経路が滑らかに動く絵が成立する。

02何を描くのか — モンテカルロ経路とオプション価格

題材は、これまで計算してきたモンテカルロを「見せる」ことだ。株価の将来は無数に枝分かれする。その数千本のシナリオ(幾何ブラウン運動の経路)を、1本ずつ線として描く。時間とともに経路は扇状に広がり、不確実性の拡散が目に見える。

さらにオプション価格を重ねる。行使価格のラインを引き、満期にそれを超えた経路(価値を持つ=イン)を緑に、超えない経路(アウト)を暗く色分けする。無数の経路のうち、価値が出るものの集積が、オプション価格になる——金融工学の本質を、計算結果そのものの絵として示す。計算と可視化が、ひとつのGPUパイプラインの中で融合する。

03直結の核心 — 計算したバッファを、描画がそのまま読む

最大のポイントがこれだ。経路の価格を格納するストレージバッファを、1つだけ用意する。コンピュートシェーダーがそこに価格を書き込み、レンダーシェーダーが同じバッファを読んで描画する。CPUへのコピーは無い。

鍵はバッファのusage指定だ。計算で書き込むためのSTORAGEと、描画で読むためのVERTEXを、同じバッファに与える。

JavaScript — バッファを compute と render で共有
const trajBuf = device.createBuffer({ size: P * STEPS * 4, // 経路数 × ステップ数 × f32 usage: GPUBufferUsage.STORAGE // ← compute が書き込む | GPUBufferUsage.VERTEX // ← render が頂点として読む | GPUBufferUsage.COPY_DST, });

コンピュート側は、各スレッドが1経路を担当し、前ステップの価格から1ステップ進めてTRAJに書く(幾何ブラウン運動)。

WGSL — compute:経路を1ステップ進めて書き込む
@group(0) @binding(1) var<storage, read_write> TRAJ : array<f32>; @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid: vec3<u32>) { let p = gid.x; // 経路インデックス let s = U.curStep; let prev = TRAJ[p*U.steps + (s-1u)]; // ... GBM で乱数 z を作り ... TRAJ[p*U.steps + s] = prev * exp(drift + vol*z); }

レンダー側は、そのTRAJそのまま読む。頂点シェーダーで頂点インデックスから(経路, ステップ)を復元し、価格を画面座標に変換する。computeが書いた値が、CPUを経由せず描画に流れ込む。

WGSL — render:同じ TRAJ を読んで頂点位置に
@group(0) @binding(1) var<storage, read> TRAJ : array<f32>; // ← 同じバッファ @vertex fn vs(@builtin(vertex_index) vi: u32) -> VOut { let s = vi % U.steps; // ステップ let p = vi / U.steps; // 経路 let price = TRAJ[p*U.steps + s]; // computeが書いた値をそのまま let xN = (f32(s)/f32(U.steps-1u))*2.0 - 1.0; let yN = (price - U.ymin)/(U.ymax - U.ymin); out.pos = vec4<f32>(xN, yN*2.0-1.0, 0.0, 1.0); return out; }
なぜ速いか もし計算結果をCPUに戻すなら、毎フレーム mapAsync でGPU→CPU転送し、JS配列を作り、Chart.jsに渡す——という重い往復が要る。直結ではこれがすべて無くなる。バッファはGPUメモリに留まったまま、computeパスとrenderパスを連続して流すだけ。数千本×数百ステップの点を、毎フレーム描き直せる。

04軌跡をどう描くか — line-strip と primitive restart

各経路は、ステップを結んだ折れ線として描く。トポロジはline-strip(連続した点を線で繋ぐ)。だが素朴にやると問題が起きる。全頂点を1本のline-stripで繋ぐと、経路の終わりと次の経路の始まりまで線で繋がってしまう

かといって経路ごとにdraw()を呼ぶと、経路数ぶんのドローコールが要る。数千〜数万本では、このドローコール過多が描画のボトルネックになる。

解決策がprimitive restartだ。インデックスバッファに「ここで線を切れ」という特別な値(0xFFFFFFFF)を挟むと、GPUがそこでline-stripを区切る。経路の区切りごとにこの値を入れれば、1回のドローコールで、繋がらない数千本の線を描ける。

JavaScript — primitive restart のインデックスを組む
const RESTART = 0xFFFFFFFF; // 線を切る特別値 const idx = new Uint32Array(P * (STEPS + 1)); let k = 0; for (let p = 0; p < P; p++) { for (let s = 0; s < STEPS; s++) idx[k++] = p*STEPS + s; idx[k++] = RESTART; // 経路の区切り }
JavaScript — uint32 インデックスで restart を有効化、1回で描画
// パイプライン:stripIndexFormat に 'uint32' を指定すると restart が効く primitive: { topology: 'line-strip', stripIndexFormat: 'uint32' }, // 描画:全経路を1回のドローコールで pass.setIndexBuffer(indexBuf, 'uint32'); pass.drawIndexed(indexCount); // ドローコール1回
ドローコールという観点 GPU描画では「何を描くか」だけでなく「何回 draw を呼ぶか」が効く。1回のdrawは大量の頂点を一気に処理できるが、drawを何千回も呼ぶと、その発行コストが積み上がる。primitive restart は、見た目(経路ごとに独立した線)を保ったまま、ドローコールを1回に畳む手法だ。これはエージェント市場(協調計算)で見た「同期回数が律速」という話と同じ構図——GPUでは呼び出しの回数が、しばしば本質的なコストになる

05計算を色にする — 満期判定とブレンド

オプションのイン/アウトは、レンダーシェーダーの中で判定する。各経路の満期価格(最終ステップの値)を読み、行使価格と比較して色を決める。計算結果が、そのまま色になる。

WGSL — render:満期価格で色分け
let finalPrice = TRAJ[p*U.steps + (U.steps-1u)]; // 満期価格 var inMoney = false; if (U.optType == 0u) { inMoney = finalPrice > U.strike; } // コール else { inMoney = finalPrice < U.strike; } // プット if (inMoney) { out.col = vec4<f32>(0.2, 1.0, 0.7, a*1.6); } // イン:緑 else { out.col = vec4<f32>(0.5, 0.35, 0.15, a*0.6); } // アウト:暗橙

色は加算ブレンドで重ねる。経路が密集する価格帯ほど色が積み重なって明るくなり、光の濃淡が確率密度を表す。中央が白く輝くのは、そこが最も経路の通る=起こりやすい価格帯だからだ。静的なヒストグラムが示す確率分布が、無数の経路の重なりとして生きた光になる。

最後に、オプション価格の集計だけはCPUを使う。満期列の価格を一度読み戻し、ペイオフの平均を割り引く。これは1回きりの集計なので、毎フレームの描画とは切り離せる。描画は毎フレームGPU直結で高速に、価格集計は満期到達時に1回だけCPUで——役割分担だ。

06試してみる

下のデモは、ここまで解説したパイプラインがそのまま動いている。再生すると、computeが経路を生成し、renderが直結で描画する。数千本の経路が扇状に広がり、行使価格を境に色が分かれる様子を見てほしい。経路数を変えても、primitive restart のおかげでドローコールは常に1回だ。

▶ compute→render 直結デモ
モンテカルロ経路の生成(compute)と軌跡描画(render)が、GPUバッファ共有で直結
WebGPU 判定中...
緑=満期で行使価格超(イン)、橙=未達(アウト)、黄線=行使価格。中央が明るいほど経路密=確率高。

07結論 — 計算と描画が融合するとき

これまでの実験ノートは「計算」だけを見てきた。GPUは速い計算機で、絵を描くのはCPUの仕事——そう切り分けていた。だがWebGPUの本質は、計算と描画が同じ装置の上で地続きであることだ。計算結果をCPUに戻さず、そのまま頂点として描く。それだけで、数千本の経路がリアルタイムに動く絵が生まれる。

そして、その絵はただ美しいだけではない。モンテカルロの確率分布が光の濃淡として、オプション価格が緑の経路の集積として、計算の意味そのものを語っている。「計算が見える」とは、数字が絵になることではなく、計算の構造が視覚の構造と一致することだ。

compute だけを追ってきたシリーズに、render が加わった。計算の速さを測る旅から、計算を見せる旅へ。WebGPUの両輪が揃ったとき、ブラウザは「その場で計算し、その場で描く」場所になる。手のひらの中で、無数の未来が光になる——それがこの技術の到達点だ。

『計算が、そのまま絵になる — WebGPU compute→render 直結』を公開しました。