全 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 で許容範囲)。
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 計算と既に重なっていた。
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 上の位置付けのみ。
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 増えるだけ" 採用)。&[f32] も従来通り渡す (advisor: "両方 signature 保持" 採用)。| 項目 | 値 | 備考 |
|---|---|---|
| scene | nerf_synthetic/lego | brush convention (premultiplied) |
| max_steps | 5000 | Phase D opacdecay-5k baseline と同条件 |
| config | configs/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 参照 |
| seed | 42 | 両方 identical |
| concurrent load | materials 30k brushcompat 1 本 (PID 86708) | 両 run 共通、GPU contention 同レベル |
| host | M4 Max 36 GB | macOS、Metal HW counter 不使用 |
| metric | A (cache OFF) | B (cache ON) | delta |
|---|---|---|---|
| wallclock | 145.58 s | 145.24 s | **-0.23%** |
| ts_target_upload total | 3.284 s (5000 calls × 657 µs) | **absent (0 calls)** | **-100%** |
| ts_target_upload share | 1.5% / 2.26% (sum/wallclock) | 0.0% | -1.5pt / -2.26pt |
| ts_forward | 79.293 s (36.2%) | 81.322 s (36.9%) | +2.6% (noise) |
| ts_fwd_radix_sort | 34.585 s (15.8%) | 35.982 s (16.3%) | +4.0% (noise) |
| ts_backward_raster | 26.672 s (12.2%) | 26.025 s (11.8%) | -2.4% (noise) |
| ts_ssim_fwd_grad | 15.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 loss | 1.428949e-2 | 1.480875e-2 | +3.6% (RNG drift) |
| final splats | 84,665 | 84,595 | -0.08% |
| **val PSNR (100 views)** | **31.631 dB** | **31.770 dB** | **+0.14 dB** |
ts_target_upload の host Instant 計測 (cumulative) を wallclock で割った値。 これは "GPU が host を 5.6% 待っていた" ではなく "host 側で 5.6% の時間を消費していた" の意味。 後者は GPU が同時に他の kernel を走らせていれば wallclock には現れない。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 に乗らない。StdRng sequence は完全に identical。 だが refine::split 内で rng.gen() を呼ぶ 順序と回数 は decay/prune の通過率に依存し、 通過率は loss grad に依存し、 loss grad は floating-point atomic accumulation order に依存する。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 ごとに微妙に変わる。SPLAT_DISABLE_TARGET_CACHE=1)、 ただし production / bench / 卒論評価は cache ON で問題なし。| 項目 | 値 | 備考 |
|---|---|---|
| view 数 | 100 | lego train split |
| view あたり buffer size | 800 × 800 × 4 × 4 byte = 10.24 MiB | RGBA f32 |
| 合計 GPU buffer | 976.6 MiB | 実測 (start log: "uploaded 100 view buffers 976.6 MiB") |
| upload 時間 | 105.7 ms | 起動時 1 回、wallclock 145s に対し 0.07% |
| M4 Max unified memory | 36 GB | 1 GB cache = 2.7% で trivial |
| scale-up (例: tnt 21k view) | 10.24 MiB × 21,000 = 215 GiB | **警告**: large dataset では cache 全 view は不可、 chunk / LRU が必要 |
本 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 で検証が必要。
SPLAT_BATCHED_FORWARD=1) を拡張する形で実装、 期待 -5〜6%。# 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 内)