松田研究室
FindingID: p1-e-refine-gpu-smoke
Status: stable / task P1.E refine GPU 化 (axis 1 core contribution)
← Findings Index
P1 Phase E · refine GPU profile · hypothesis falsified

P1.E refine GPU 化 hypothesis を SPLAT_TIMING profile で falsified — refine 寄与は <1%、真の bottleneck は forward 60% + loss 28%

task の前提「refine の host RMW = CPU 1-thread bound = wallclock -5x の余地」を `SPLAT_TIMING=1` を導入して 5k smoke で実測したところ、refine 系 ops (compact + accumulate + opacity_decay) の合計 wallclock 寄与は **<1%** と判明。真の bottleneck は forward (60.1%) + loss_gpu (28.0%) + adam/target_upload (~10%) で、refine を完璧に GPU 化しても全体 wallclock は **誤差レベル** しか変わらない。Phase E は方針を切替えて (1) `SPLAT_TIMING=1` env で kernel timing を有効化する instrumentation を追加、(2) forward を 5 sub-kernel に分割計測して bottleneck の解像度を上げ、(3) **demo kernel として `refine_opacity_decay.metal` を実装** (kernel + Rust pipeline + `refine.gpu_path` flag の plumbing pattern を validate)、(4) GPU/CPU 経路の bit-close 検証 (max diff < 1e-3、5k full PSNR delta -0.21 dB = 許容内) を実証。卒論 narrative としては「Metal kernel 直叩きの limit を測定した負の finding」として価値あり (memory `autonomous_plan_brush_parity.md` の P1.E 想定通り)。次 step (Phase F) は forward subdivision で判明した `ts_fwd_sort` 15.5% / `ts_fwd_emit` 12.8% の tile-binning chain と、5x sequential `cmd.wait` の Adam batching が target。

negativerank: highaccepted-negative-redirect-phase-f第 1 軸p1phase-erefine-gpunegative-findingkernel-profileaxis-1-limitopacity-decay-gpukernel-plumbinglego-5k
Δ PSNR
-0.21 dB (CPU 31.92 → GPU 31.71、5k smoke、bit-close 内 RNG cascade)
Δ Wallclock
+1.4% (CPU 144.32s → GPU 146.32s、5k smoke、opacity_decay は 0.005% で誤差)
Δ Splats
-0.5% (CPU 84629 → GPU 84236、bit-close 内 refine prune cascade 差)
Impact
Phase D 30k 実測 wallclock 41m54s vs brush 9m08s = -4.6x gap の原因について、task は `splat process CPU 63.4% = 1 core only` → 「refine の host RMW loop が CPU 1-thread bound」と仮説立てた。本 Phase E ではこの仮説を SPLAT_TIMING profile で実測。5k smoke (84k splats、p1-e-profile-5k.toml) の kernel breakdown: **ts_forward 60.1% (123s) / ts_loss_gpu 28.0% (57s) / ts_adam 4.8% (9.9s) / ts_target_upload 3.9% (8.1s) / ts_project_back 2.3% (4.75s) / ts_refine_compact 0.6% (1.14s, 103ms/call × 11) / ts_refine_accumulate 0.3% (605ms) / ts_opacity_decay 0.0046% (957µs)**。**refine 全体で <1%** = refine を完璧に GPU 化しても全体 wallclock は -1% も短縮されない。代わりに demo kernel として `refine_opacity_decay.metal` を実装し、kernel + Rust pipeline + `refine.gpu_path` flag plumbing pattern を validate (CPU vs GPU max diff 1.5e-5、5k full PSNR delta -0.21 dB = 許容内)。Phase F の真の target は (a) forward subdivision で判明した tile-binning chain (`ts_fwd_sort 15.5% + ts_fwd_emit 12.8%`)、(b) Adam の 5x sequential `cmd.wait_until_completed` (1 cmd buffer 化で ~5% 削減期待)、(c) target_upload preload (~4% 削減期待) の 3 つ。
01

本文

本 Phase E は task framing「refine の host RMW loop が wallclock -5x の余地」を SPLAT_TIMING profile で falsified させた。5k smoke の breakdown で refine 系 ops の合計寄与は <1%、真の bottleneck は forward 60% + loss 28%。代わりに demo kernel として `refine_opacity_decay.metal` を実装し kernel plumbing pattern を validate、Phase F (真の bottleneck) に向けた forward subdivision profile を残した。卒論 axis 1 narrative の「Metal kernel 直叩きの limit」を測定値として確定する負の finding。

Headline — refine GPU 化 hypothesis falsified

SPLAT_TIMING profile (5k smoke, 84k splats) で refine 系 ops の wallclock 寄与は <1%: ts_refine_compact 0.6% / ts_refine_accumulate 0.3% / ts_opacity_decay 0.005%。一方 ts_forward 60.1% + ts_loss_gpu 28.0% が dominant。refine を fully GPU 化しても全体 wallclock は誤差レベル (~-1%) しか変わらない。axis 1 contribution は方針転換: (a) Phase F で tile-binning chain (sort 15.5% + emit 12.8% = 28%) を target、(b) demo kernel `refine_opacity_decay.metal` で kernel plumbing pattern を validate (CPU vs GPU bit-close 1.5e-5、PSNR delta -0.21 dB / 許容内)。

1. SPLAT_TIMING profile (5k smoke、Lego brushcompat opacity_decay 0.004)

kerneltotal wallclockcallsavg / call% total
**ts_forward (total)****123.146s**500024.629ms**60.1%**
— ts_fwd_sort33.839s50006.768ms15.6%
— ts_fwd_emit27.511s50005.502ms12.7%
— ts_fwd_rasterize11.598s50002.320ms5.4%
— ts_fwd_project3.179s5000635µs1.5%
— ts_fwd_offsets2.072s5000414µs1.0%
**ts_loss_gpu**57.374s500011.475ms**28.0%**
**ts_adam**9.893s50001.979ms**4.8%**
ts_target_upload8.053s50001.611ms3.9%
ts_project_back4.750s5000950µs2.3%
**ts_refine_compact****1.137s**11103.4ms**0.6%**
**ts_refine_accumulate****605ms**5000121µs**0.3%**
**ts_opacity_decay (CPU)****957µs**1187µs**0.005%**
TOTAL (sum)204.958s100%
Lego 5k smoke (84,329 final splats, 31.99 dB)、`SPLAT_TIMING=1 ./target/release/splat train --config configs/2026-05-25-p1-e-profile-5k.toml`。**refine 系 ops の合計は 1.74s / 204s ≈ 0.85%**。forward 内訳の tile-binning chain (sort + emit) が 28% で次の Phase F target。 (sum) は再帰 inclusion で実 wallclock 144.32s より大きい — sub-kernel が parent ts_forward に二重計上されているため。

2. 仮説 vs 実測 (refine GPU 化 ROI)

項目task 仮説実測 (5k smoke)判定
refine の host RMW = bottleneckwallclock の主因0.85% (compact + accumulate + decay)**falsified**
CPU 63.4% = 1 core onlyrefine RMW で 1-thread bound実測 ~42-51% (M4 Max 16 core 中 0.4-0.5 core)**GPU dispatch wait** で thread block 中 (CPU 使ってない)
refine fully GPU 化 → wallclock -5x42m → 9m (brush parity)上限 -1% (refine 全体が 0.85% のため)**not achievable**
axis 1 contribution = refine kernelsplit / clone / decay の Metal 化demo `refine_opacity_decay.metal` のみ**方針転換** (Phase F へ)
axis 1 真の target(未定義)forward tile-binning chain 28%**Phase F の中心**
task 仮説は `axis 1 contribution = refine kernel` を前提としていたが、SPLAT_TIMING の実測で **refine は wallclock の 1% 未満** = どれだけ最適化しても全体への影響は誤差レベル。Phase F で tile-binning chain (sort 15.5% + emit 12.8%) を target にすべき。`ps -o pcpu` 実測で splat process CPU% は 42-51% (M4 Max 16 core 中 0.4-0.5 core) で task 説明の 63.4% と整合。ただしこれは「1 thread が CPU を専有」ではなく **GPU dispatch 待ちで thread が `wait_until_completed()` ブロックしている状態** で、CPU 並列化では解決しない。

3. 実装内容 (demo kernel + instrumentation)

  1. shaders/opt/refine_opacity_decay.metal: brush 互換 opacity decay (sigmoid-space) を element-wise kernel として実装。30 行 MSL、1 thread / 1 splat。
  2. crates/splat-metal/src/kernels/refine.rs: `RefineOps` struct (Library + PSO 保持) + `dispatch_opacity_decay()` (adam.rs と同 pattern)。3 unit tests (CPU と max diff 1.5e-5、minus_opac=0 no-op、100 splat ramp 検証) all pass。
  3. crates/splat-metal/src/kernels/mod.rs: `pub mod refine;` 追加。
  4. crates/splat-train-v1/src/config.rs: `RefineConfig.gpu_path: bool` 追加 (default false で backward compat)。
  5. crates/splat-train-v1/src/trainer.rs: `Trainer.refine_ops: RefineOps` field 追加、`forward_with_state` に 5 つの timing::record 挿入 (`ts_fwd_project / _emit / _sort / _offsets / _rasterize`)。
  6. crates/splat-train-v1/src/train_loop.rs: `SPLAT_TIMING=1` env で `timing::enable()` + 終了時 `print_summary()`、refine cadence ブロックに `ts_refine_accumulate / ts_refine_compact / ts_opacity_decay[_gpu] / ts_opacity_reset / ts_mcmc_noise` を追加、`refine.gpu_path` flag で opacity_decay GPU 経路に分岐。
  7. configs/2026-05-25-p1-e-profile-{1k,5k}.toml: SPLAT_TIMING 計測用 config (gpu_path = false)。
  8. configs/2026-05-25-p1-e-gpu-decay-5k.toml: gpu_path = true smoke。CPU baseline 31.92 dB vs GPU 31.71 dB (-0.21 dB / 許容 ±0.5 dB)、wallclock 144→146s (+1.4% noise)、final splats 84629→84236 (-0.5% / bit-close prune cascade)。

4. GPU/CPU bit-close 検証 (unit tests + 5k smoke)

test条件結果判定
`opacity_decay_gpu_matches_cpu_formula`3 splat (σ=0.5/0.9/0.001), minus=0.1|GPU - 期待| < 1e-4✅ pass
`opacity_decay_gpu_zero_is_noop`2 splat, minus_opac=0|before-after| < 1e-4✅ pass
`opacity_decay_gpu_vs_cpu_100splats`100 splat ramp, minus=0.05max |CPU - GPU| 1.5e-5✅ pass (bit-close)
5k smoke PSNR deltaLego, gpu_path on/off-0.21 dB✅ 許容 ±0.5 dB 内
5k smoke wallclock deltaLego, gpu_path on/off+1.4% (144.32 → 146.32s)✅ ~unchanged (noise)
5k smoke final splats deltaLego, gpu_path on/off-0.5% (84629 → 84236)✅ bit-close RNG cascade
GPU / CPU の数値差は f32 sigmoid/logit の last-bit 程度 (1e-5 帯)。1500 iter で 15 回 cadence 適用すると refine prune の選択が微妙にずれて final splat 数が ±0.5% 変動するが PSNR への影響は ±0.5 dB の許容内。**bit-close 検証としては完全に合格**。

5. 卒論 narrative としての価値 (negative finding の axis 1 限界)

axis 1 (native Metal kernel) の正直な limit

memory autonomous_plan_brush_parity.md「失敗してもデータ自体は卒論『Metal kernel 直叩きの限界 / 成功例』として価値あり」 を Phase E 設計時に明記。本 Phase E はまさにその「限界の測定」であり、「卒論軸 1 の core contribution は refine の Metal 化ではなく、forward の tile-binning chain (Phase F target、28%) と Adam の cmd buffer batching (5%)」 という方向修正を **実測値ベース** で提示する。SPLAT_TIMING instrumentation + 5 forward sub-kernel timing + demo kernel pattern は、Phase F 以降の「真の bottleneck の最適化」のための **infrastructure 整備** として 100% 流用可能。

6. Phase F target (refine GPU から切り替える)

  1. tile-binning chain (forward 内 28%): `ts_fwd_emit 12.7% + ts_fwd_sort 15.6% = 28.3%` が 5k smoke で wallclock の 1/3 弱。brush は radix sort を GPU 完結 (CPU 16-pass exclusive prefix scan を 1 cmd buffer に統合) しているとされ、本実装は radix の 16 pass 間で CPU 介在あり (trainer.rs:154 周辺、A.7 ICB batching の対象外区間)。Phase F の中心 target。
  2. Adam 5x sequential cmd.wait_until_completed: `adam.rs::step_buf` line 200 で 5 component それぞれに `cmd.commit() + wait_until_completed()`。1 cmd buffer に統合すれば 5x → 1x wait で `ts_adam 4.8%` の 50-80% 削減見込み (-2〜-4% wallclock)。
  3. ts_target_upload preload (3.9%): 毎 iter で同じ GT image を host → GPU に upload。100 view × 800×800×4 = 256 MB と shared memory 上の cache に乗る規模、起動時 1 回の upload に変更で -4% 期待。brush も同様の preload を実装している (要確認)。
  4. Phase E で残した負の finding を卒論 axis 1 章の冒頭に配置: 「Metal kernel 直叩きの limit を測定する」セクションで p1-e-refine-gpu-smoke profile table を引用、Phase F の選択肢を実測値で justify する narrative 構成。

7. 再現手順

# 1. Build (Phase E 実装含む)
cd splat
cargo build --release -p splat-cli

# 2. 5k smoke CPU baseline (gpu_path = false) + timing
SPLAT_TIMING=1 ./target/release/splat train --config configs/2026-05-25-p1-e-profile-5k.toml
# → ts_refine_compact 0.6% / ts_opacity_decay 0.005% / ts_forward 60.1%
# → val PSNR 31.92 dB, wallclock 144s

# 3. 5k smoke GPU path (gpu_path = true, opacity_decay GPU 経由)
SPLAT_TIMING=1 ./target/release/splat train --config configs/2026-05-25-p1-e-gpu-decay-5k.toml
# → ts_opacity_decay_gpu 182µs/call (CPU 87µs より遅い、dispatch overhead で逆転)
# → val PSNR 31.71 dB, wallclock 146s, splats 84236

# 4. unit tests (kernel bit-close)
cargo test --release -p splat-metal --lib refine
# → 3 tests pass (max diff 1.5e-5)

8. 想定外 / 教訓

  • 仮説検証ステップを skip して kernel コードを書き始めなかった: advisor の初回助言で「SPLAT_TIMING で profile してから kernel 選定すべき」と指摘され実行、結果として `refine fully GPU 化` の前提が崩れることを 30 分で確認できた。実装を 1-2 日かけてから「効かない」と気付くより 1 桁効率的。
  • negative finding は積極的に文書化する: 卒論 narrative では「Metal kernel 直叩きの limit を測定値で示す」セクションがあると、axis 1 の正当性が「無闇に GPU 化」ではなく「実測根拠ベースの選択」として強化される。
  • GPU dispatch overhead は 100µs オーダー: opacity_decay (84k splat の element-wise op) でも CPU host loop 87µs より GPU dispatch 182µs の方が遅い。`cmd.commit + wait` の往復で ~100µs かかるため、N が小さい op や少呼び出しの op は CPU の方が有利。Adam (1.7ms/call) は dispatch 量に対して compute が十分大きいので GPU 妥当だが、refine_accumulate (90µs/call) は GPU 化しても改善余地が薄い。
  • CPU% ~42-51% は "1 core bound" ではなく "GPU dispatch wait で thread block": top で splat process の CPU% を測ったところ 42-51% (M4 Max 16 core 中 0.4-0.5 core)。これは「1 thread が CPU を専有」ではなく、**`cmd.wait_until_completed()` で thread が休眠している状態** (GPU 結果待ち)。15+ core idle なのは正しいが、原因は CPU 計算ではなく **GPU dispatch chain の sequential 同期** (Phase F の Adam 5x wait + radix sort 16-pass などが該当)。CPU 並列化では解決せず、cmd buffer batching か kernel fusion が真の対策。
  • plumbing pattern validation は demo kernel として有効: 本 Phase E では `refine_opacity_decay.metal` 1 個の demo kernel で `RefineOps` struct, `gpu_path` flag, bit-close unit test の 3 点セットを確立。Phase F で tile-binning kernel を書く際にも同じ pattern を流用できる。

9. 関連

  • Phase D 30k 結果 (本 Phase の前提となる wallclock 41m54s): p1-d-stage2-30k-results
  • Phase D 5k smoke (opacity_decay 数値検証): p1-d-opacity-decay-smoke
  • brush bench (target -4.6x gap source): m4-brush-bench
  • autonomous loop spec (Phase E 設計含む): memory autonomous_plan_brush_parity.md
03

関連 finding

© 2026 大竹律輝 — 松田研究室