松田研究室
FindingID: p1-axis1-target-cache
Status: stable / task P1 axis 1 target upload cache
← Findings Index
P1 axis 1 · target upload cache · low ROI 確定

P1 axis 1 target_upload cache — kernel 除去は成功、wallclock ROI は host/GPU overlap で予想の 1/25

全 100 train view (976 MiB) を train_loop 起動時に GPU shared buffer へ 1 度 upload して cache。per-iter `ts_target_upload` を 5000 → 0 calls に落とし、kernel timing summary から消去。だが apples-to-apples A/B 計測 (env `SPLAT_DISABLE_TARGET_CACHE=1` toggle、同一 binary・同一 contention regime) では wallclock は 145.58s → 145.24s で **-0.23%** に留まる。 ts_target_upload host 計算 3.28s は GPU forward / backward と Metal driver の async dispatch で完全に overlap していたのが真因 (advisor 事前指摘)。実装コストは小、PSNR は seed RNG 経由で +0.14 dB drift (refine.split の浮動小数点 ordering 起因、5k smoke で許容範囲)。

neutralrank: lowaccepted-cleanup-keep-merged第 1 軸p1axis-1target-cachekernel-removalhost-gpu-overlaplow-roiapples-to-apples-ablego-5k
Δ PSNR
+0.14 dB (seed同一、RNG drift、許容範囲)
Δ Wallclock
-0.23% (apples-to-apples A/B、env toggle 同一 binary)
Δ Splats
-0.08% (84665 → 84595)
Impact
Per-iter target upload kernel を完全除去 (5000 calls → 0)、構造的には kernel 一つ消えた成果。 だが wallclock ROI は **予想 -5.5% に対し実測 -0.23%** (-1/25)、profile baseline の "5.6% share" は GPU contention 3x 環境での host stall 値で、平常 contention では host upload は既に GPU 計算と overlap していた。 PSNR は seed 同一でも +0.14 dB drift (Metal driver の buffer 配置順序差 → atomic ordering 差 → refine.split RNG 経由)、5k smoke の noise floor 内。 実装は trivial (train_loop entry で `Vec` 構築、train_step に `Option<&Buffer>` 追加)、commit 残しておく価値はあるが、roadmap 上の位置付けは "deprioritize / cleanup level" に修正。 **真の優先順位は radix_sort 改善 (27% share) と A.7 ICB batching tail に集中すべき**。
01

本文

p1-profiling-baseline の優先順位 #1 (target_upload cache、即時 -5.5% 期待) を実装。全 100 train view (RGBA 800×800×4 f32 = 10.24 MiB/view、合計 976 MiB) を train_loop 起動時に Vec<Buffer> として GPU shared memory に 1 度 upload、per-iter は idx で参照するだけにした。 構造的成果: kernel timing summary から ts_target_upload 行が消える (5000 calls → 0 calls、3.28s GPU work が完全除去)。実 wallclock ROI: -0.23% (env toggle で apples-to-apples 計測、predicted -5.5% の 1/25)。 advisor 事前 "<3% なら overlap 疑え" 警告と一致、host upload は Metal の async dispatch で GPU 計算と既に重なっていた。

Headline (kernel 除去は成功 / wallclock ROI は 1/25)

p1-profiling-baseline の "ts_target_upload 5.6%" は host-side wallclock proxy 値であり、 "GPU が host upload を待つ純 stall" ではなかった。 contention 3x の状況下では host upload 1.33 ms × 5000 = 6.6s が見かけ上 5.6% を占めるが、平常 contention では Metal driver の async commit で GPU forward kernel と overlap し、 wallclock 直接削減量は -0.2% に留まる。 profile baseline で "低 hanging fruit" と分類した #1 は実は ROI 最小、roadmap の優先順位を radix_sort (27%) / A.7 batching に集中させる根拠が固まった。 cache 実装自体は trivial・後悔なし・PSNR 影響なしで残す、deprioritize は roadmap 上の位置付けのみ。

1. 実装 (1 file、~30 行)

  • splat/crates/splat-train-v1/src/train_loop.rs: train_loop() entry で cameras.iter().map(|(_, t)| upload_buffer(...))Vec<Buffer> 構築 (106 ms / 976 MiB)、env SPLAT_DISABLE_TARGET_CACHE=1 で legacy 経路に戻せる A/B 計測 hook。
  • train_step() 引数追加: target_buf_cache: Option<&Buffer>Some なら L1 / L1Ssim の GPU loss path が cache を直接参照、ts_target_upload::record は呼ばない。 None なら従来通り per-iter upload_buffer + record。
  • cmd/train.rs 改変なし (cache は train_loop 内で完結、cli は &cameras を渡すだけ)。 config flag も追加なし (常時 ON、 36 GB unified memory で 1 GB は trivial、 advisor 指摘の "flag は code path 増えるだけ" 採用)。
  • L2 / force_host fallback 経路は target を host-side で読むため &[f32] も従来通り渡す (advisor: "両方 signature 保持" 採用)。

2. A/B 計測 setup (apples-to-apples)

項目備考
scenenerf_synthetic/legobrush convention (premultiplied)
max_steps5000Phase D opacdecay-5k baseline と同条件
configconfigs/2026-05-25-1500-lego-target-cache-5k.toml[backend] profile=true (kernel timing 取得)
A (cache OFF)`SPLAT_DISABLE_TARGET_CACHE=1` で env toggle同一 binary、cache 構築 skip + per-iter upload (legacy)
B (cache ON)default (env unset)100 view を起動時 upload、per-iter は idx 参照
seed42両方 identical
concurrent loadmaterials 30k brushcompat 1 本 (PID 86708)両 run 共通、GPU contention 同レベル
hostM4 Max 36 GBmacOS、Metal HW counter 不使用
env toggle 1 つで A/B。 binary は同一なので Metal driver 経路・config 解釈・kernel pipeline は完全互換、唯一の違いは cache build と per-iter upload 有無。 seed 同一でも GPU buffer 配置順序差で refine.split の浮動小数点 ordering が変わり、 splats 数と最終 PSNR が seed-noise レベルで差が出る (後述 §3 表)。

3. 結果 (5000 iter 完走)

metricA (cache OFF)B (cache ON)delta
wallclock145.58 s145.24 s**-0.23%**
ts_target_upload total3.284 s (5000 calls × 657 µs)**absent (0 calls)****-100%**
ts_target_upload share1.5% / 2.26% (sum/wallclock)0.0%-1.5pt / -2.26pt
ts_forward79.293 s (36.2%)81.322 s (36.9%)+2.6% (noise)
ts_fwd_radix_sort34.585 s (15.8%)35.982 s (16.3%)+4.0% (noise)
ts_backward_raster26.672 s (12.2%)26.025 s (11.8%)-2.4% (noise)
ts_ssim_fwd_grad15.520 s (7.1%)16.890 s (7.7%)+8.8% (noise)
**TOTAL kernel sum****219.006 s****220.547 s****+0.7% (noise)**
final loss1.428949e-21.480875e-2+3.6% (RNG drift)
final splats84,66584,595-0.08%
**val PSNR (100 views)****31.631 dB****31.770 dB****+0.14 dB**
kernel 除去は完全成功: ts_target_upload は B の timing summary から完全に消失 (record 呼ばれず)、5000 calls × 657 µs = 3.28s の GPU work 削減。 だが wallclock は ~0.3s しか縮まない: GPU 全体 200+s の中で host upload は async commit でほぼ完全 overlap していた。 PSNR drift +0.14 dB は seed noise 範囲、loss 3.6% diff も最終 PSNR でほぼ均される (refine.split で異なる positions を生成、 5k smoke 解像で 0.1 dB 程度の差は通常)。

4. なぜ wallclock ROI が予想の 1/25 だったのか

  • profile baseline の "5.6% share"ts_target_upload の host Instant 計測 (cumulative) を wallclock で割った値。 これは "GPU が host を 5.6% 待っていた" ではなく "host 側で 5.6% の時間を消費していた" の意味。 後者は GPU が同時に他の kernel を走らせていれば wallclock には現れない。
  • Metal の new_buffer_with_data + StorageModeShared: macOS Metal は shared memory model で、 host 書き込みは contents() ポインタへの memcpy だけ、 GPU commit までは GPU は触らない。 1 つ前の iter の GPU backward / adam が走っている間に次 iter の target upload を完了できる構造で、 critical path に乗らない。
  • contention 3x 環境 (p1-profiling-baseline の "並行 3 本") では GPU が busy で overlap window が狭く、 host upload が浮き出して見える。 平常 contention (本 A/B では並行 1 本のみ) では GPU が空いている時間が短く、 host upload は完全に隠れる。
  • 真の bottleneck は GPU 全体: kernel SUM 219s に対し wallclock 145s、 残り 74s は ts_forward の子の double-count + scheduling overhead。 ts_forward parent 79.3s と children sum (radix_sort 34.6 + emit_pairs 27.7 + rasterize 11.8 + project 3.1 + extract_off 2.1 ≈ 79.3s) はほぼ一致、 forward だけで wallclock の 36% を占有 = 純 GPU work である。
  • 結論: target_upload を消すことは kernel 除去としては正しい が、 wallclock 改善には別の bottleneck (radix_sort 27% / ssim 8% / backward 12%) を狙う必要がある。

5. PSNR drift +0.14 dB の原因 (seed 同一でも非決定論)

  • seed 42 同一 なので StdRng sequence は完全に identical。 だが refine::split 内で rng.gen() を呼ぶ 順序と回数 は decay/prune の通過率に依存し、 通過率は loss grad に依存し、 loss grad は floating-point atomic accumulation order に依存する。
  • Metal の non-deterministic atomics: atomic_fetch_add_explicit の commit 順序は GPU scheduler が決め、 thread block 起動順は buffer 配置・cache state・前 dispatch の終了タイミングに左右される。 cache を build すると Metal driver の heap allocator state が変わり、 backward の per-pixel atomic accumulation 順序が iter ごとに微妙に変わる。
  • iter 1 (loss 5.9385e-1) は両方 4 sig fig 一致、 iter 500 (8.6218 vs 8.6219) もほぼ一致、 iter 1000 で 0.6% diverge し以後 compound。 これは典型的な GPU non-determinism の出方。
  • 5k smoke の PSNR noise floor: multi-scene chain の同 config 再実行で ±0.2 dB は通常観測される (refine.split 確率的経路の影響)。 +0.14 dB は noise floor 内、 "PSNR 不変" の要件を満たすと判断。
  • Determinism が必要な test (snapshot diff、CI regression gate) では env で cache を OFF にする option を残してある (SPLAT_DISABLE_TARGET_CACHE=1)、 ただし production / bench / 卒論評価は cache ON で問題なし。

6. memory footprint

項目備考
view 数100lego train split
view あたり buffer size800 × 800 × 4 × 4 byte = 10.24 MiBRGBA f32
合計 GPU buffer976.6 MiB実測 (start log: "uploaded 100 view buffers 976.6 MiB")
upload 時間105.7 ms起動時 1 回、wallclock 145s に対し 0.07%
M4 Max unified memory36 GB1 GB cache = 2.7% で trivial
scale-up (例: tnt 21k view)10.24 MiB × 21,000 = 215 GiB**警告**: large dataset では cache 全 view は不可、 chunk / LRU が必要
NeRF Synthetic (lego/chair/...) は 100 view 固定で 1 GB に収まる。 Tanks & Temples の Truck (21,612 view) や Mip-NeRF 360 では cache サイズが unified memory を超えるため、 view-batch chunk + LRU eviction が必要 (本 finding の scope 外、 将来 Stage 4 で別途検討)。

7. roadmap 含意 (本 finding の位置付け)

p1-profiling-baseline の優先順位 table 更新

本 finding により target_upload cache の wallclock ROI は -5.5% (期待) → -0.2% (実測) に修正。 profile baseline の Table 5."ROI 見積もり" 行は contention 3x 環境での見積もり として記録、 本番 contention での実 ROI と区別する。 残りの優先順位は (1) radix_sort 改善 (27% share、 -14〜17% 期待)(2) A.7 ICB batching forward+backward (-6% 期待)(3) SSIM fusion (-5% 期待) の順で変更なし。 これらも当然 "contention 高い時の見積もり" であり、 実装後に同様の A/B で検証が必要。

  1. commit を保持: kernel 除去は構造的成果、 future contention 高負荷時 (multi-scene chain など) では再評価で ROI が出る可能性がある。 cache build cost 106 ms は 5k 145s に対し 0.07%、 trade-off は negligible。
  2. radix_sort 改善に集中: 27% share = 35s / 145s、 ここを 2x 高速化 (Apple MPS / kernel fusion) すれば -14% wallclock = 真の高 ROI。 Metal Counter Sample で 16-pass の中身分解を次 step に提案。
  3. A.7 ICB batching: forward tail (extract_off + rasterize + project_back + loss) を 1 cmd buffer 化、 commit barrier を 4→1 に。 既存 PoC (SPLAT_BATCHED_FORWARD=1) を拡張する形で実装、 期待 -5〜6%。
  4. 本 finding は "低 hanging fruit と思った実は ROI 1/25" の cautionary taleとして roadmap 文書に追記、 profile baseline の "host Instant proxy は overlap を見られない" 注記を強調する。

8. 想定外 / caveat

  • predicted -5.5% に対し実測 -0.23% (= 1/25): advisor 事前警告 "<3% なら overlap 疑え" がそのまま的中。 profile baseline で観測した "5.6% host stall" は contention 3x の特殊環境値、 平常運用での真の host stall は ~1.5% で、 そのうち overlap を引いた残りが 0.2% という構造。
  • PSNR +0.14 dB drift: seed 同一でも GPU buffer 配置順序差で atomic ordering が変わり、 refine.split の RNG 経路が compound。 5k smoke では noise floor 内だが、 30k full run で同 A/B したときに差が広がる可能性は残る (本 finding の scope 外、 30k bench で別途確認推奨)。
  • GPU contention 環境依存: 本 A/B は materials 30k 1 本並行 (中 contention)、 profile baseline は 3 本並行 (高 contention)。 contention が 0 (single trainer) のときに ROI がさらに低下する可能性あり、 逆に contention が 5+ になると ROI が回復する可能性あり (host stall window が広がる)。
  • large dataset では cache が hold できない: Tanks & Temples / Mip-NeRF 360 では 100→数千 view、 RAM cache 不可。 本実装は NeRF Synthetic scope に限定、 Stage 4+ で chunk + LRU が必要。

9. 再現手順

# 1. Build (target cache 実装込み binary)
cd splat
cargo build --release -p splat-cli

# 2a. A: cache OFF baseline (env で disable、apples-to-apples)
SPLAT_DISABLE_TARGET_CACHE=1 \
  ./target/release/splat train \
  --config configs/2026-05-25-1500-lego-target-cache-5k.toml

# 2b. B: cache ON (default)
./target/release/splat train \
  --config configs/2026-05-25-1500-lego-target-cache-5k.toml

# 3. 期待出力:
#   A: timing summary に `ts_target_upload  3.284s  5000 calls  657µs  1.5%`
#   B: timing summary から ts_target_upload 行が消失、起動時に
#      `[target_cache] uploaded 100 view buffers (976.6 MiB total) in 105ms`
#
# 4. wallclock 差: A 145.58s / B 145.24s = -0.23% (本番 contention 環境)
#    PSNR: A 31.631 dB / B 31.770 dB (+0.14 dB drift、noise floor 内)

10. 関連

  • Parent: p1-profiling-baseline (target_upload 5.6% share を観測した profiling)
  • Phase D 30k baseline: p1-d-stage2-30k-results (Lego brushcompat opacdecay 30k 36.106 dB / 42 min)
  • 次 step 提案: radix_sort sub-pass profiling (Metal Counter Sample で 16-pass 分解)、 A.7 ICB batching tail 完全化
  • Multi-scene chain (PID 81416) / Phase D decay sweep (PID 36644) と並行起動互換 (本 A/B 計測時に materials 30k 1 本のみ並行)
03

関連 finding

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