binning 経路 55% の主犯 (per-iter ~1.25GB buffer 確保 + Vec 三重往復) を grow-only resident arena + Buffer 直結で撤廃し、radix pass を 16 → 11 に削減。Lego 12.5k gate run で <strong>wallclock 766.07 → 395.59s = -48.4%</strong>、PSNR 36.216 → 36.183 dB (-0.033、atomic guard ±0.1 内)。emit_pairs 23.47 → 0.47ms/call (<strong>-98%</strong>)、radix_sort 10.19 → 4.79ms (-53%)、extract 757 → 145µs (-81%)。Day 0 attribution で機構を一段修正: Metal new_buffer は lazy なので host 計測 alloc は 42µs しかなく、<strong>1.25GB/iter の実コストは first-touch page fault として emit の GPU wait に現れていた</strong> (fresh-alloc 26.5ms vs buffer 再利用 0.61ms = 43×)。数学的同値 (演算順序・数値不変) を parity test 群で証明し、default を Resident に flip。
binning 経路 55% (p2-gap-sweep-baseline) の主犯を grow-only resident arena + Buffer 直結 (C0) と radix 定数桁 pass skip 16 → 11 (C1) で是正。演算順序・数値を一切変えない bit-exact 施策のみで Lego 12.5k が 766.07 → 395.59s (-48.4%)。Day 0 attribution が機構を一段修正した: 主犯は host alloc 時間ではなく、毎 iter 新規確保される ~1.25GB buffer への first-touch page fault が GPU wait に化けていた。
| 計測 | regime | emit 内訳 | 値 |
|---|---|---|---|
| 1k smoke (profile) | 6.8k splats / cap 1.7M pairs | gpu / readback / upload / alloc | 523 / 87 / 24 / 9 µs (host 系 18.5%) |
| micro-bench fresh-alloc | 407k splats / cap 104M pairs | gpu / readback / upload / alloc | 26.50ms / 2.09ms / 824µs / **42µs** (gpu 90%) |
| micro-bench buffer 再利用 | 同上 (同一 dispatch 列) | gpu のみ | **0.61ms** (fresh-alloc の 1/43) |
設計調査の仮説「emit の主犯は host alloc + Vec 往復」は方向として正しかったが、attribution の宛先が違った。Metal の new_buffer は lazy なので host 計測 alloc は 42µs しかない — 毎 iter 新規の keys 833MB + idx 417MB へ GPU が最初に書き込む瞬間の first-touch page fault (zero-fill) が wait_until_completed に紛れ込み、emit の「GPU 時間」26.5ms の 97% を占めていた。文言通りの gate (host 系 ≥50%) は 1k smoke で 18.5% と不発だが、regime 不一致 (6.8k vs 407k splats = capacity 60×) を micro-bench で補正すると非 kernel コストは ~98% で gate は決定的に通過。C0 本実装を GO とした。
| 項目 | 内容 | 同値性の証明 |
|---|---|---|
| **C0 resident arena** (`BinningKind::Resident`) | splats/depths 入力・keys/idx ping-pong・hist/offsets・scalar uniform 群・tile_offsets を grow-only arena として iter 間再利用。emit → radix → extract を Buffer 直結、host に降ろすのは sorted_idx と tile_offsets のみ。capacity = max(1<<16, n×16, 前 iter 実績×1.5)、overflow は arena grow + 再 emit (12.5k run で発火 1 回 = iter 1 のみ)。batched_tail (A.7) も arena 直結対応 | resident vs per_iter_alloc で sorted keys / sorted idx / tile_offsets の byte equality (synthetic + 40k splats ~515k pairs + arena 再利用 + 空入力) + forward 1-frame bit-exact (per-stage / batched_tail) |
| **C1 radix pass skip** (`radix_passes_for_tiles`) | sort key = (tile_id<<32)|depth_bits で tile_id < num_tiles → 上位の all-zero 4-bit digit pass は stable radix の恒等置換。pass 数 = min(16, 8 + ceil(bits(num_tiles-1)/4))、Lego 2500 tiles で 11。enum flag なしの直接置換 (legacy 経路にも適用)、奇数 pass の ping-pong 出力先も検証 | radix_sort_binned vs 16-pass 版 vs CPU stable sort の byte equality (2500 tiles = 11 pass 奇数 / 40000 tiles = 12 pass 偶数 / u32::MAX = 16 pass、各 200k keys) |
| metric | per_iter_alloc | resident | Δ |
|---|---|---|---|
| wallclock (1k) | 11.36s | 10.66s | -6.2% |
| emit_pairs | 588µs/call | 231µs/call | **-60.7%** |
| radix_sort | 2.470ms/call | 2.196ms/call | -11.1% |
| extract_off | 233µs/call | 132µs/call | -43.3% |
| PSNR (100 views) | 19.480 dB | 19.440 dB | -0.04 (refine 非決定性圏内) |
Day 0 run (C1 前、16 pass) の radix 3.685ms/call → C1 適用 per_iter_alloc arm で 2.470ms/call = -33%。pass 数 16 → 11 (-31%) とほぼ整合。1k regime は splats 6.8k で C0 の効果が小さく出る (capacity 60 分の 1) ため、C0 の本判定は 12.5k gate で行った。
| metric | baseline (per_iter_alloc, 7e5ca2d) | resident (C0+C1, 0abdaa2) | Δ |
|---|---|---|---|
| **wallclock (training)** | 766.07s | 395.59s | **-48.4%** |
| PSNR (100 views) | 36.216 dB | 36.183 dB | -0.033 dB (guard ±0.1 内) |
| final splats | 407,267 | 407,901 | +0.16% (軌道等価) |
| ts_fwd_emit_pairs | 293.3s (23.47ms/call, 38.2%) | 5.9s (0.47ms/call, 1.2%) | **-98.0%** |
| ts_fwd_radix_sort | 127.4s (10.19ms/call, 16.6%) | 59.8s (4.79ms/call, 11.7%) | **-53.1%** |
| ts_fwd_extract_off | 9.5s (757µs/call) | 1.8s (145µs/call) | -80.8% |
| ts_backward_raster | 164.5s (13.16ms/call, 21.5%) | 160.4s (12.83ms/call, **31.4%**) | -2.5% (share は最大化) |
| ts_fwd_rasterize | 43.3s (3.47ms/call) | 41.2s (3.30ms/call) | -4.9% |
flip 条件 (-15% & parity 通過) を大幅超過。PSNR -0.033 dB は bit-exact 実装下の run-to-run atomic 非決定性 (backward の atomic<float> → refine 軌道 ±、splats +0.16%) で説明され、guard ±0.1 dB 内。PerIterAlloc は敗者 variant として enum に保持 (A/B baseline 用)。eval / render など apply_backend_config を通らない caller も binner Cell default の同期 flip で resident を踏む。
| 候補 | C0 前 | C0 後 | 判定 |
|---|---|---|---|
| 2-BWD last_contributor | backward 21.5% (trigger 25% 未達で保留) | **31.4%** | **trigger 初超過 — 次の最有力候補** |
| radix sort 本体 (GPU compute) | 16.6% (alloc 混在) | 11.7% (compute 主体) | #5.40 8-bit radix の再評価余地はあるが優先度低 |
| STP (feat.F) default 再判定 | -7.45% (gate -8% に僅差未達) | 要再 bench | Phase 1a の -7.45% は emit の page fault 削減と重複していた可能性が高い (emit share 38.2% → 1.2% で寄与源が消滅)。C0 後 baseline で paired A/B を取り直してから default 判定 |
| separable SSIM / SH-Adam fusion | 5.2% / 6.1% | 6.6% / ~9.5% | 不発維持 (相対 share は上昇したが trigger 未達) |
per-kernel wallclock で「GPU が遅い」ように見える区間に host のメモリ管理コスト (lazy alloc の first-touch) が化けることがある。F.1 / F.3 / #5.40 が kernel 側の最適化で 3 連敗した理由も遡って整合する — 遅かったのは kernel ではなく buffer lifecycle だった。今後の kernel 最適化判断は「buffer 再利用 micro-bench で kernel 本体時間を分離してから」を必須手順とする。
この finding が観測された / 言及している実験 run。