Phase F.1 emit_simd / f16_fwd に続く audit Tier 2 (radix_sort GPU prefix scan、別 mechanism = CPU-GPU sync 除去) の empirical 検証。GPU 上で per-group histograms の exclusive scan を計算する 16-thread single-threadgroup kernel を実装、bit-exact (100k random keys / 500k packed keys / edge cases 6 種) 確認。**5k Lego smoke で wallclock 115.83s → 124.41s (+7.4%、sanity rerun 118.73s で再現)**、**ts_fwd_radix_sort 4.768 → 6.733 ms/call (+41%)**、PSNR は parity (31.604 → 31.635 dB、bit-exact 経路なので drift なし)。**Likely mechanism**: StorageModeShared buffer での back-to-back compute encoder 間で Metal が implicit fence を挿入 (hist→scan で buf_hist、scan→scatter で buf_offsets の read-after-write hazard)、TBDR pipeline stall。旧 CPU 経路は buf_hist→buf_offsets を host で行うため GPU 内 memory dependency が無く、「除去した wait_until_completed」は実は **CPU prefix scan と overlap していた active work** だった。kernel + tests は env `SPLAT_RADIX_GPU_SCAN=1` で opt-in、別 workload (>>100k splats 等) で再評価余地のみ残す。
SPLAT_RADIX_GPU_SCAN=1 で opt-in (future workload hedge)。audit (p1-axis1-metal-opt-audit) で Tier 2 = 「Phase E scope の構造改善、別 mechanism (CPU-GPU sync 除去)、-0.5-0.8% wallclock、LOW PSNR risk」 と分類した radix_sort GPU prefix sum を 4-5h で実装、5k Lego smoke で empirical 検証。bit-exact 実装は完成、PSNR parity (drift 0 期待、観測 ±0.03 dB は session noise)、しかし wallclock +7.4% / ts_fwd_radix_sort +41% の net regression。Phase F.1 (emit_simd / f16_fwd) に続く Tier 2 別 family の audit-overestimate 観測 5 例目。
audit Tier 2 "-0.5-0.8% wallclock、CPU-GPU sync 除去" 予測は empirical で大幅 falsified。5k Lego smoke で wallclock 115.83s → 124.41s (+7.4%)、ts_fwd_radix_sort 4.768 → 6.733 ms/call (+41%、sanity re-run 6.402ms で再現)、PSNR は bit-exact 経路で parity。Likely mechanism (advisor 2026-05-25): StorageModeShared buffer での back-to-back compute encoder 間で Metal が implicit fence を挿入 (hist→scan の buf_hist、scan→scatter の buf_offsets で read-after-write hazard)、TBDR pipeline stall。旧 CPU 経路は buf_hist→buf_offsets を host で行うため GPU 内 memory dependency が無く、「除去した wait_until_completed」は CPU prefix scan と overlap していた active work だった。**結論**: default 経路は legacy CPU scan 維持、GPU scan kernel + tests は env SPLAT_RADIX_GPU_SCAN=1 で opt-in (future workload hedge、>>100k splats / 高 tile_per_splat scene で dispatch overhead vs fence trade-off が逆転する可能性)。Tier 2 同 family (backward TBDR、tile-local accumulator + Kahan summation で global atomic 除去) は **同 mechanism 影響を受けるため skip 判断強化**。
| metric | F.3 CPU scan baseline | F.3 GPU scan | GPU scan rerun (sanity) | delta vs baseline | audit 予測 |
|---|---|---|---|---|---|
| total wallclock | **115.83s** | 124.41s (**+7.4%**) | 118.73s (+2.5%) | +7.4% (first) / +2.5% (rerun) | -0.5〜-0.8% (audit) / -3〜-14% (advisor optimistic) |
| TOTAL kernel sum | 174.943s | 191.236s (+9.3%) | 186.0s (est.) | +9.3% | |
| ts_fwd_radix_sort ms/call | 4.768 | **6.733 (+41.2%)** | **6.402 (+34.5%)** | **+35〜41%** | -0.5〜-3.0 ms/call expected |
| ts_fwd_radix_sort share | 13.6% | 17.6% | 17.5% | +4.0 pt | |
| ts_fwd_emit_pairs ms/call | 5.157 | 4.775 (-7.4%) | — | -7.4% (noise、orthogonal) | |
| mean val PSNR (100 view) | **31.604** | 31.635 (**+0.031**) | 31.869 (+0.265) | ±0 (bit-exact) | ±0 dB |
| final splats | 84,840 | 83,644 (-1.4%) | — | -1.4% |
hist[g*16 + d] を全 group で sum → threadgroup memory の bucket_totals[d]。phase 2: simd_prefix_exclusive_sum を 16 lane で 1 SIMD group (Apple width 32) として実行 → bucket_starts[d]。phase 3: thread `d` が column 内 serial scan で offsets[g*16 + d] = cursor; cursor += hist[g*16 + d] を g=0..num_groups で実行。num_groups 数千でも単 threadgroup の serial scan は 8000 add 程度で trivial。use_gpu_prefix_scan flag (env SPLAT_RADIX_GPU_SCAN=1 で ON、default OFF) で 2 path 分岐。GPU 経路は 1 command buffer / pass で hist → scan → scatter を 3 encoder で chain、intra-pass の wait_until_completed を完全除去、final pass のみ wait。CPU 経路 (legacy) は intact 保持。num_groups u32 buffer (scan kernel 引数) を pre-allocate。radix_sort_gpu_vs_cpu_scan_100k (100k 一様乱数 u64、num_groups ≈ 391)、radix_sort_gpu_scan_packed_keys_500k (500k packed key = tile_id[0,2500) << 32 | depth_bits[0.01,100.0)、runtime distribution mimic、num_groups ≈ 1954)、radix_sort_gpu_scan_edge_cases_match_cpu_scan (n=1, 255, 256, 257, 1024, 2048 の boundary num_groups)、すべて (a) GPU 経路 vs CPU 経路 byte-for-byte 一致 (b) 両者 vs cpu_stable_sort reference 一致。Phase F.3 で PSNR drift は理論的に 0 (sort 順序保存)、観測 ±0.03 dB は session noise。histogram.commit() → wait → CPU prefix scan (~64KB 配列の 2 pass、~100µs) → scatter.commit()。host 側で wait している間、GPU は scatter dispatch 待ちで idle だが、wait 時間そのものは CPU prefix scan と overlap していた。新経路で除去したのは idle time ではなく overlap time、純粋な improvement にならない。buf_hist write → encoder 2 (scan) が buf_hist read + buf_offsets write → encoder 3 (scatter) が buf_offsets read で 2 つの fence が必須。TBDR pipeline では fence が tile flush + driver-level scheduling barrier を強制、stall 発生。# Build (worktree)
cd splat
cargo build --release
# Bit-exact validation (9 tests, ~0.1s)
cargo test --release -p splat-metal --lib tile_bin
# A/B (5k Lego smoke、~2 min each)
# baseline = default (legacy CPU scan、env 不要、本 commit の現状)
./target/release/splat train \
--config configs/2026-05-25-2030-lego-phase-f3-baseline-5k.toml
# → ts_fwd_radix_sort ~4.77 ms/call、total ~115s、PSNR ~31.6 dB
# GPU scan opt-in (env で明示的に enable)
SPLAT_RADIX_GPU_SCAN=1 ./target/release/splat train \
--config configs/2026-05-25-2030-lego-phase-f3-gpu-scan-5k.toml
# → ts_fwd_radix_sort ~6.4-6.7 ms/call (+35-41%)、total ~118-124s (+2.5-7.4%)、PSNR parity
p1-axis1-metal-opt-audit (Tier 2 radix GPU prefix scan -0.5-0.8% wallclock 予測)p1-profiling-clean (radix_sort 13.6% share)p1-axis1-phase-f1-emit-simd-falsified (emit_simd / f16_fwd の 2 連続 falsified)p1-e-refine-gpu-smoke (refine GPU 化 5x → 0.2%)、p1-axis1-target-cache (5.5% → 0.23% async overlap で 1/25)、a-6-f16-packed-rebench (-50% bandwidth → -1%)p1-d-multi-scene-rechainchapter-5-4-negative-findings (axis 1 audit predictions section、5 連続 overestimate)この finding が観測された / 言及している実験 run。