worktree (`worktree-agent-a61d27ad0ed372418`) で実装済みの C3 (count → CPU exclusive scan → exact-size scatter → per-tile bitonic sort、msplat 風) を main へ統合し GPU gate を完遂。<code>splat bench --parity</code> で正しさを確認 (tile_offsets byte-exact、grad buffer 5 種すべて tol 1e-4 以内 PASS)、12.5k full run の PSNR guard も通過 (36.226→36.190 dB、-0.036 dB)。しかし fixed-workload replay bench (final regime n=10) では <strong>+3.15% (むしろ僅かに遅い、閾値 max(2σ,1%)=4.45% 未満で非有意)</strong>、radix_sort kernel 単体も +7.5% で speedup 仮説は不成立。CPU 側 exclusive scan が GPU パイプラインに同期点を挟むことが原因と推定。full run 単体では -8.8% と matching しない結果が出たが、これは既知の full-run 分散 (~10%) の confound と判断し gate 判定には使わない — bench harness (p2-bench-harness-calibration) 導入の価値をそのまま裏付けた。<strong>default flip 見送り、flag として保持</strong>。副次効果として sort key に <code>splat_idx</code> を含めたことで depth-tie 非決定性が解消 (sorted_idx tie-order ambiguity が原理的に消える)。
splat bench --parity で数学的正しさを確認 (tile_offsets byte-exact、5 grad buffer tol 1e-4 以内 PASS)。しかし fixed-workload replay (final checkpoint n=10) では +3.15% ― 閾値 max(2σ,1%)=4.45% 未満で有意な高速化なし。radix_sort kernel 単体も +7.5% 遅い。12.5k full run の PSNR guard は通過 (-0.036 dB) したが、同じ full run の wallclock は -8.8% と replay の非有意判定に矛盾しており、これは既知の full-run 分散 confound と判断し gate 判定には採用しない (むしろ bench harness の存在意義を裏付ける実例)。default flip は見送り、TileCounting は BackendConfig flag として保持 (正しさは証明済みなので将来 CPU scan を GPU 化する等の改良余地があれば再挑戦可能)。副次効果として sort key (depth_bits<<32)|splat_idx により depth-tie 非決定性を解消した。Phase 2 完了時点 (finding: p2-last-contributor) で radix_sort が 13.5% (絶対) 残っていた。別 worktree (worktree-agent-a61d27ad0ed372418) で先行実装されていた C3 (msplat 風 per-tile counting sort: count → CPU exclusive scan → exact-size scatter → per-tile bitonic sort、threadgroup memory 32KB = 4096 要素上限、超過 tile がある frame は正しさ優先で GlobalRadix に全面フォールバック) を main へ統合し GPU gate を完遂した。
| 項目 | 内容 |
|---|---|
| worktree | `worktree-agent-a61d27ad0ed372418` (branch 同名)、分岐元 c8e5881 (main は fe68cfa / b053e35 の bench harness commit 2 本分先行) |
| conflict 要因 | `apply_backend_config` が `cmd/train.rs` の private fn から `crates/splat-cli/src/util.rs` の pub fn に抽出済みだった (bench harness 実装時)。tile_bin.rs / tile_bin.metal / config.rs は main 側で無変更だったため diff がそのまま apply、train.rs の C3 差分 (`tile_sort` destructuring + `use_tile_counting_sort` 配線 + `build_config_items` 2 行) のみ util.rs / train.rs へ手で移植 |
| smoke compile | MSL は runtime compile のため Rust build では未検証だった。`cargo test -p splat-metal tile_counting` (= `GpuTileBinner::new` 経由) で shader 構文を検証、6 test 一発で PASS (shader 構文エラーなし) |
| unit test | 5 本 (`tile_counting_matches_legacy_synthetic` / `_random_500k` / `_fallback_on_oversized_tile` / `_biased_single_tile_under_threshold` / `_empty_input_routes_to_legacy`) + panic guard 1 本 (`resident_sorted_bufs_panics_after_tile_counting_sort`、BatchedTail 併用の防衛) 全 PASS。既存 106 test も無傷 |
| build | `cargo build --release` warning 0、`cargo check --tests` clean |
`splat bench --config c3-parity-baseline.toml --parity c3-parity-candidate.toml` (final checkpoint、409,660 splats、GPU loss path) で baseline (GlobalRadix) と candidate (TileCounting) の 1 iteration を比較。
| 項目 | 結果 |
|---|---|
| loss_value | baseline=0.004210821 candidate=0.004210905 diff=8.4e-8 |
| tile_offsets | **PASS (exact byte equality, n=2,500)** — per-tile count/scan は完全一致 |
| means_grad / log_scales_grad / rot_quats_grad / raw_opacities_grad / sh_coeffs_grad | **全 5 種 PASS** (tol atol=1e-4 rtol=1e-4、max_abs_diff は 2.8e-5〜5.8e-8 の範囲) |
| sorted_idx (informational) | 2,197 / 969,279 要素差 = **0.227%** — sort key layout の違い (legacy `(tile_id<<32)|depth_bits` vs C3 `(depth_bits<<32)|splat_idx`) による tie-order 差であって誤りではない (既存 harness 規約により gate 対象外) |
| PARITY OVERALL | **PASS** |
`splat bench` で final checkpoint (409,660 splats) / iter5000 checkpoint (373,933 splats) の 2 regime を各 n=3 で計測。final regime は有意性が閾値付近だったため n=10 に escalation した (n=3/n=5 の σ 推定自体がノイズを持つという p2-bench-harness-calibration の運用ガイドラインに従う)。
| regime | n | variant | median ms/iter | mean | σ_pct | delta (vs GlobalRadix) | 閾値 max(2σ,1%) | 判定 |
|---|---|---|---|---|---|---|---|---|
| final (409,660 splats) | 3 | GlobalRadix | 30.496 | 31.053 | 3.16% | — | — | — |
| final | 3 | TileCounting | 32.479 | 32.569 | 0.53% | +4.88% | 6.32% | 非有意 (境界付近) |
| final | 10 | GlobalRadix | 32.503 | 32.383 | 2.23% | — | — | — |
| final | 10 | TileCounting | 33.833 | 33.403 | 4.60% | **+3.15%** | **4.45%** | **非有意 (n=10 escalation で確定)** |
| iter5000 (373,933 splats) | 3 | GlobalRadix | 30.313 | 30.526 | 2.15% | — | — | — |
| iter5000 | 3 | TileCounting | 31.185 | 30.641 | 3.66% | +0.38% | 4.30% | 非有意 (ほぼゼロ) |
| regime | n | GlobalRadix avg_us/call | TileCounting avg_us/call | delta |
|---|---|---|---|---|
| final | 10 | 5,721.4 | 6,150.9 | +7.5% |
| final | 3 | 5,771.6 | 5,846.0 | +1.3% |
| iter5000 | 3 | 5,578.3 | 5,726.8 | +2.7% |
同一 recipe (`lego-bench-calib-checkpoints` = 現行 default スタック resident C0/C1 + last_contributor 2-BWD) で `tile_sort = "tile_counting"` のみ変えた 12.5k full run を 1 本実施。
| metric | baseline (lego-bench-calib-checkpoints) | C3 (lego-c3-tile-counting-12500) | delta |
|---|---|---|---|
| PSNR (mean val, 100 views) | 36.226 dB | 36.190 dB | **-0.036 dB (±0.1 dB guard 内、PASS)** |
| final splats | 409,660 | 405,293 | -1.1% |
| training wallclock (log 実測) | 360.96s | 329.11s | -8.8% |
full run 単体の wallclock delta (-8.8%) は、n=10 replay bench が示した「非有意 (+3.15%、むしろ僅かに遅い)」と方向が逆転している。p2-bench-harness-calibration finding が実測した通り full run 間には ~10% の分散 (dataset load / refine cadence / relocation 等の交絡) があり、n=1 の full run 比較はこの分散に完全に飲み込まれる規模の delta。今回の C3 は replay bench (σ で統制された比較) が唯一の信頼できる速度判定材料であり、full run 単体の数値だけで判断していれば誤って +8.8% の高速化を採用していたことになる — bench harness 導入の価値をそのまま裏付ける実例。
この finding が観測された / 言及している実験 run。