`p3-s4-host-shadow` §7 follow-up の実装。`[backend] ssim_reduce = "host" | "host_lagged" | "gpu"` を追加した。<strong>gpu</strong> は `loss.metal` の `loss_l1_only` と同型の 1 dispatch reduction (`ssim_sum_reduce`、warp `simd_sum` → lane 0 `atomic_fetch_add`) に置き換え、host 仕事を 4 byte readback まで削る — pipelining / host_schedule に依存せず全 iteration で効くが bit-exact ではない (atomic 加算順非決定性、`loss_path = "gpu"` の L1 項がすでに持つ性質と同種)。gradient (`ssim_backward*`) には一切触れないため <strong>PSNR には構造的に影響しない</strong> (§5 参照)。<strong>host_lagged</strong> は `host_schedule = "backward_shadow"` の 6.38ms shadow に `read_mean_ssim` 自体を 1 iteration 遅らせて重ねる設計 (値は同一 buffer に対する読みなので bit-exact) だが、`p3-s4-host-shadow` 本体の 12.5k gate が backward_shadow を棄却した (+1〜5% 遅い) ため実用価値が下がり、**gate 対象から外したまま実装・test のみ完了させて残した**。<strong>12.5k n=3 交互 A/B gate (2026-08-30 実施) で Δ <strong>-1.4347%</strong> (閾値 max(2σ_off,1%)=1.0%)、PSNR +0.053dB (guard -0.05dB 内)、3/3 pair 一貫 (on<off) — 採用確定、`BackendConfig::default()` の `ssim_reduce` を `Gpu` に flip した</strong>。事前見積り (-2〜-4%、§2b) より実測は小さかった (host 仕事 13.66s 削減のうち wallclock 回収は 1.79s = 変換効率 13%、§7 で機構解釈)。`cargo test --workspace --release` 全 pass。
`p3-s4-host-shadow` §7 は「`read_mean_ssim` (1.92M 要素の CPU 総和、`loss = "l1_ssim"` の host 仕事 1.871ms/iter で最大の未着手候補) を動かせば、S4 の実測見積り (-1.5〜-3%) を元の -3〜-5% レンジに近づけられる可能性が高い」と申し送っていた。本 finding はその follow-up 実装 — `[backend] ssim_reduce` flag に `host` (default) / `host_lagged` / `gpu` の 3 variant を追加し、消費箇所の分類・2 案の比較・test・gate config 準備までを行う。
| 呼び出し元 | 経路 | 消費先 |
|---|---|---|
| `train_loop.rs` L266 (train_step、`pipelining = "off"`) | `wait(B_it)` 直後に同期呼び出し | `total_loss` → `StepOutcome` 相当の戻り値 → `history.push` |
| `pipeline.rs` `Depth2Pipeline::step` (`pipelining = "depth2"`) | 同上 (S4 以前は常に同期) | `loss_value` → `StepOutcome.loss` → `train_loop` の `history.push` |
| `loss_and_backward` 系 legacy host path (`forward_and_grad_buf` / `forward_and_grad`) | CB 内で完結、host が commit+wait 後に自前で総和 | parity test / host fallback 専用、production では不使用 |
`docs/strategy/2026-08-msplat-gap-sweep.md` の禁忌リスト「reduction (`!33c`)」「SSIM threadgroup 単独 (`#5.34.a`)」を実読して確認した。**`!33c` は rasterize backward の `backward = "tg"` (threadgroup-memory 版 atomic emit 分散) の話** (`config::BackwardKind::Tg` doc、`docs/findings/p2-w1-warp-sparse-skip.toml` 参照) で、SSIM とは無関係。**`#5.34.a` は SSIM forward/backward 本体を threadgroup-mem 化する試み** (`docs/findings/feat-bundle-musing-mendeleev-inventory.md`、gate 不通過で REJECT) であり、こちらも今回の対象 (SSIM forward が**すでに計算し終えた**per-window 値の総和のみを行う 1 dispatch kernel) とは別物。よって `ssim_sum_reduce` kernel は禁忌に抵触しない。
| 検証 | 結果 |
|---|---|
| `cargo test --workspace --release` | 全 pass (splat-metal 95 [新規 2 本含む]、splat-train-v1 90 [新規 2 本含む]、他クレート含め failure 0)。`cargo test -p splat-train-v1` (debug profile) も全 pass |
| 新規 test (`splat-metal/src/kernels/ssim.rs`) | `ssim_sum_reduce_matches_host_within_tolerance` (128×128 fixture、複数 simdgroup にまたがる規模で `Gpu` vs `Host` の相対誤差 <1e-4 を確認 — `new_single_texel` 級の 3 要素 1 warp では non-determinism 自体が発現せず識別力ゼロになる、advisor 指摘を踏まえた規模選定)。`read_mean_ssim_from_buf_matches_synchronous_read` (24×24、同一 command buffer 内で同期 read と deferred read が **bit-exact** — `HostLagged` の中核主張を直接 pin) |
| 新規 test (`splat-train-v1/src/train_loop.rs`) | `ssim_reduce_host_lagged_matches_host_loss_history_bit_exact` (`loss_history` が `assert_history_matches` 基準で一致、`host.len()==lagged.len()==3` で末尾 slot 欠落なしを確認)。`ssim_reduce_host_lagged_without_backward_shadow_matches_host` (`host_schedule` 未設定でも `host_lagged` が no-op で `host` と一致することを実行時 pin、validate warning の主張の裏取り) |
| **発見 1**: `train_loop` レベルでは `Host` vs `HostLagged` の全要素 bit-exact 比較は誤って fail する | 初版 test は全要素 `to_bits()` 一致を要求し、3 iteration 目 (最終 iteration、defer なし) で ULP 差により fail した。原因調査の結果、`loss = "l1_ssim"` の **L1 項自体** (`loss.metal` の `loss_l1_combine_ssim`、`atomic |
| **発見 2**: `assert_params_close` にも host_lagged 非固有の低頻度 flakiness (~1/30) が残る | `cargo test --workspace --release` を繰り返した際、新規 test (`ssim_reduce_host_lagged_matches_host_loss_history_bit_exact`) がまれに `rot_quats[2]` の rel tolerance (1e-4) をわずかに超えて fail した (rel≈1.14e-4)。30 回 stress run で 1/30。**同じ 30 回 stress run を、host_lagged と無関係な既存 test `pipelining_depth2_backward_shadow_matches_off_steady_state_chain` (同じ 3-iteration 構成) にも実施したところ、こちらも 1/30 で同じ signature (`rot_quats[2]`, rel≈1.13e-4) で fail した** — rasterize backward の per-splat grad atomic accumulate が run ごとに加算順が変わりうるという、fixture が既に持っていた性質であって S4b 固有の regression ではないことを確認。閾値は変更せず (緩めると real regression を見逃すリスクが増す)、doc に測定値を明記するに留めた |
| 1000 iter smoke #1 (Lego、`loss=l1_ssim`、`refine.every=100 start=500` → 6 tick、`host_schedule=backward_shadow` 固定 [初版、後に base 変更で非該当]、`ssim_reduce={host,host_lagged,gpu}` 3 arm) | 3 arm とも `peak_in_flight=2` / `sync_iters=6` / crash なし。`host`: wall 3.46s、final loss 6.674047e-2、splats 6809、PSNR 19.565 dB。`host_lagged`: wall 3.00s、final loss 6.542842e-2、splats 6861、PSNR 19.560 dB。`gpu`: wall 3.01s、final loss 6.622096e-2、splats 6889、PSNR 19.653 dB。loss/PSNR/splats は 3 arm とも n=1 の atomic 非決定性の範囲で近い値 |
| 1000 iter smoke #2 (base 修正版、`host_schedule` は default=forward_shadow、`ssim_reduce={host,gpu}` 2 arm、`ts_ssim_host_sum` 計装後) | 両 arm とも `peak_in_flight=2` / `sync_iters=6` / crash なし、final loss/splats/PSNR は近い値 (host: 6.572725e-2 / 6843 / 19.606dB、gpu: 6.568488e-2 / 6781 / 19.593dB)。**`ts_ssim_host_sum` 実測: host = 1.093ms/iter (TOTAL[sum] の 31.0%)、gpu = 0.030ms/iter (+ `ts_ssim_sum_reduce` encode 0.326µs、合計 <0.04ms)** — §2b の事前見積り (≈1.3ms) とほぼ整合。**busy%/wall にも明確な差が出た: host busy 76.0% / wall 3.427s → gpu busy 91.4% / wall 2.927s (Δ -14.6%)** — `p3-s4-host-shadow` の smoke (splat 数に比例するコストは splats~6.9k で不可視) と異なり、SSIM 総和は image-size-bound (`W*H*3`) で splat 数に依存しないため smoke 規模でも絶対値が変わらず可視化できた。ただしこの -14.6% は smoke 固有の per-iteration budget の小ささゆえで、12.5k 本番規模にそのまま外挿できない (§2b 参照) |
| 実行順 | arm | run | train elapsed (s) | PSNR (dB) | final_loss | run 窓内 load1 mean/max | 備考 |
|---|---|---|---|---|---|---|---|
| 1 | off (A) | s4b-12500-off-r1 | 125.094 | 36.125 | 5.6347e-3 | 3.99 / 5.12 | 採用 |
| 2 | on (B) | s4b-12500-on-r1 | 123.987 | 36.209 | 5.5052e-3 | 1.62 / 2.37 | 採用 |
| 3 | off (A) | s4b-12500-off-r2 | 124.605 | 36.154 | 5.3727e-3 | 1.01 / 1.45 | 採用 |
| 4 | on (B) | s4b-12500-on-r2 | 122.062 | 36.183 | 5.4684e-3 | 2.67 / 5.24 | 採用 |
| 5 | off (A) | s4b-12500-off-r3 | 123.966 | 36.099 | 5.4520e-3 | 1.53 / 2.38 | 採用 |
| 6 | on (B) | s4b-12500-on-r3 | 122.254 | 36.145 | 5.5190e-3 | 1.10 / 1.50 | 採用 |
| **mean (n=3)** | **off** | — | **124.5549** (σ 0.4541%) | **36.1260** | 5.35e-3 台 | — | — |
| **mean (n=3)** | **on** | — | **122.7680** (σ 0.8637%) | **36.1790** | 5.49e-3 台 | — | — |
| **Δ** | — | — | **-1.4347%** | **+0.0530** | 同じ桁で一致 (5.3〜5.6e-3、GPU 加算順で末尾桁のみ揺れる、§8 参照) | — | 閾値 max(2σ_off,1%)=1.0% を有意にクリア |
wallclock: Δ **-1.4347%** ≤ -閾値 1.0% で pass。3/3 pair とも on<off で一貫 (r1: 123.987<125.094、r2: 122.062<124.605、r3: 122.254<123.966)。**除外感度分析** (`p3-tier1-bit-exact.toml` §5 が「1 点の去就で判定が反転する」脆さを指摘したのを踏まえた事前チェック): off-r1 のみ除外 (off n=2, mean 124.286) でも Δ -1.221%、r1 pair を丸ごと除外 (n=2 vs n=2) でも Δ -1.712% — **3 通りいずれの扱いでも閾値 1.0% を超えて pass**、tier1 v2 のような脆い形式通過ではない。PSNR: Δ **+0.0530dB** ≥ -0.05dB guard で pass。§2b が明記したとおり `ssim_reduce` は gradient に一切触れないため、この +0.053dB は「改善した証拠」ではなく**背景の atomic 加算順 run-to-run noise** として読むべき値 (off 側レンジ 36.099-36.154 = 0.055dB、on 側レンジ 36.145-36.209 = 0.064dB で同程度の散らばり、系統的分離なし) — guard pass は「壊れていないことの確認」であり品質向上の主張ではない。final_loss は 6 run とも 5.3〜5.6e-3 の同じ桁に収まっており (off/on で系統的な水準差なし)、GPU 加算順の違いによる末尾桁の揺れの範囲内。**以上により採用確定**、`BackendConfig::default()` の `ssim_reduce` を `Gpu` に flip、`cargo test --workspace --release` 全 pass。
§2b は `ts_ssim_host_sum` 実測 (host 1.093ms/iter vs gpu <0.04ms/iter) から『host 仕事 1.093ms/iter × 12500 iter ≈ 13.66s の host 仕事削減のうち、forward shadow の fill 率低下に比例する分 (58%) が残差 bubble から回収される』という比例モデルで -2〜-4% (4.2〜4.7s) を見積もった。実測の wallclock 回収量は off mean 124.5549s → on mean 122.7680s = **1.787s**、host 仕事削減量 13.66s に対する**変換効率は 13.08%** — §2b の比例モデル (58%) の 1/4 程度に留まった。読み: `p3-c2-bubble-attribution` の残差 bubble 帰属モデルは『host-late (host 仕事が forward shadow からはみ出す分) が概ね 1:1 で wallclock に乗る』ことを前提にしていたが、実際には SSIM 総和 (1.093ms) の**大部分はもともと forward shadow (2.00ms) の中に収まっており** (fill 率 93.6% は 100% 未満、shadow に空きがあった)、`gpu`化で削減されるのは主に『shadow の中で消費されていた時間』であって『shadow からはみ出して直接 wallclock に乗っていた時間』ではない — 後者 (host-late overflow) はごく一部だったため、比例モデルが想定した回収量よりかなり小さい実測になったと解釈するのが整合的。この解釈が正しければ、§2b の『回収量 ∝ 動かした割合 (58%)』という比例仮定は楽観的すぎた、というのが今回のずれの説明になる (次に同種の host-shadow 系施策を見積もる際は、shadow 内 fill 率が 100% 未満である限り比例モデルは上振れしやすい点に注意すること)。ただしこの解釈は事後的な推測であり追加計装による検証はしていない — 採否判定 (採用) 自体はこの解釈の正否に依存しない (gate 自体は閾値を明確にクリアしているため)。