radix_scatter (tile_bin.metal) の local rank 計算を、256 thread の threadgroup memory を O(lid) 逐次に読む既存実装から、simdgroup 内 <code>simd_ballot</code> + popcount (段1) と simdgroup 間の小さい exclusive prefix (段2) に置換した。sentinel <code>RADIX_BUCKETS</code> (=16) が <code>RADIX_BITS</code> (=4) の下位ビットのみでは digit 0 と衝突する罠 (16 & 0xF == 0) は、同一 digit マスクの初期値を <code>simd_ballot(valid)</code> にすることで回避した。<strong>sort parity は unit test (境界サイズ 255/256/257 の sentinel collision ケース込み) + 本番規模 407,267 splats の <code>splat bench --parity</code> の両方で bit exact</strong>。<strong>12.5k Lego 同一 session 交互 A/B n=3 (off-r1→on-r1→off-r2→on-r2→off-r3→on-r3) で train elapsed 149.883 → 127.530s (-14.9%)</strong>、3 pair 個別 (-14.72% / -15.15% / -14.87%) が高い一貫性を示し、有意閾値 max(2σ<sub>off</sub>,1%)=1.18% の <strong>12.6 倍</strong>。PSNR guard も pass (on 36.143 dB ≥ off 36.121 dB − 0.05、Δ +0.022 dB)。<strong>採用、`RadixScatterKind` の default を `SimdBallot` に flip</strong>、`cargo test --workspace --release` 全 pass。
tasklist A5 (`tile_bin.metal:464-475`) が指摘した「radix scatter の local rank 計算が O(lid) 逐次 threadgroup memory scan で latency 律速」を、simdgroup 内 simd_ballot + popcount (段1) と simdgroup 間の小さい exclusive prefix (段2、GROUP_SIZE/32=8 simdgroup) に置換した新 kernel radix_scatter_ballot で検証した。罠 (tasklist 記載どおり): sentinel RADIX_BUCKETS (=16=0b10000) は RADIX_BITS (=4) の下位ビットだけを見ると digit 0 (0b0000) と区別がつかない (16 & 0xF == 0)。同一 digit マスクの初期値を simd_ballot(valid) にして無効 lane を明示的に除外することでこれを回避した (実装コメント + unit test の境界サイズ 255/256/257 が pin)。2026-08-30 の 12.5k full run GPU gate で採用、default flip 済み。
| test / 経路 | 何を pin するか | 結果 |
|---|---|---|
| `radix_sort_ballot_scatter_matches_linear_rank` | n ∈ {0,1,255,256,257,1234,100000} で keys/idx が既存実装と bit exact。255/256/257 は sentinel collision trap (partial group で無効 lane が混じる境界) を明示カバー | **PASS (全 7 サイズ)** |
| `radix_sort_ballot_scatter_stability_ties` | n=5000, key %100 の ties 多発ケースで stability | **PASS** |
| `radix_sort_ballot_scatter_matches_across_scan_kinds` | 3 call site (Cpu/GpuFlat/GpuHierarchical scan) すべてで flag が一貫して効くこと | **PASS (3 scan kind 全て)** |
| `splat bench --parity` (本番規模、407,267 splats、Lego 12.5k checkpoint) | `tile_offsets` byte 一致 + grad 5 種 tol PASS | **PASS** (下表) |
| PARITY 行 (本番規模 407,267 splats) | 結果 |
|---|---|
| loss_value | baseline=0.004296378 candidate=0.004296381 diff=3.260e-9 |
| sorted_idx | INFO (非 gate、既知の tie-order 非決定性 — 2105/969838 = 0.217% 差、`p2-bench-harness-calibration` と同じ既存の性質で本変更由来ではない) |
| tile_offsets | **PASS (exact byte equality, n=2500)** |
| means_grad | PASS (max_abs_diff=1.080e-5) |
| log_scales_grad | PASS (max_abs_diff=4.642e-8) |
| rot_quats_grad | PASS (max_abs_diff=1.611e-7) |
| raw_opacities_grad | PASS (max_abs_diff=2.539e-8) |
| sh_coeffs_grad | PASS (max_abs_diff=2.618e-7) |
| **PARITY OVERALL** | **PASS** |
default flip 後の `cargo build --release` (workspace) / `cargo test --release --workspace` の両方が全 pass (splat-metal 86 + 2 ignored、splat-train-v1 70、colmap-reader 8、splat-cli 8、splat-io 7、splat-summary 1、他 crate 0、失敗 0)。既存 sort parity test にも regression なし。
| arm (`splat bench`, iters=200, warmup=20, n=3) | median ms/iter | mean ms/iter | σ | GPU busy% (median) | busy ms/iter | sync-tax ms/iter |
|---|---|---|---|---|---|---|
| off (`radix_scatter=linear_rank`) | 15.545 | 15.556 | 0.18% | 82.3% | 12.785 | 2.749 |
| on (`radix_scatter=simd_ballot`) | 13.536 | 13.489 | 0.82% | 79.4% | 10.749 | 2.787 |
| **Δ** | **-12.9%** | **-13.3%** | — | -2.9 pt | **-15.9%** | +1.4% |
旧版 finding はこの n=3 を「off×3→on×3 の逐次 run で GPU 専有未確認、σ 非対称 4.5×」を理由に directional 参考値にとどめていた。§0 の 12.5k full run gate (同一 session 交互 A/B、独立 load sampler つき) は -14.9% を計測し、この replay bench の -12.9%〜-13.3% と符号・大きさの両方で整合する (12.5k full run のほうがやや大きいのは、407,267 splats 固定 checkpoint の pair 数分布が 12.5k を通した平均よりやや大きい可能性がある、という下記 callout の予想どおり)。したがって本節の数値はもはや未確定ではなく、独立した full run gate によって裏付けられた一致証拠として読む。
`splat bench` は `train_step` を N 回呼ぶだけの fixed-workload replay で、refine/MCMC tick を含まない (S3 finding と同じ制約)。今回の測定は 407,267 splats (Lego 12.5k 到達後の checkpoint) を固定して回しているため、radix の pair 数分布は 12.5k 全体を通した平均的な pair 数 (splat 数が 0 → 407k へ増える過程の平均) よりやや大きめに出ている可能性があった — §0 の 12.5k full run gate (-14.9%) はこの replay bench (-12.9%〜-13.3%) を上回る短縮を示しており、この予想と整合する。
| arm (forward_submission=per_stage、iters=200、n=1) | ts_fwd_radix_sort total_ms | gpu_ms/call | ms/iter (全体) |
|---|---|---|---|
| off (`radix_scatter=linear_rank`) | 717.94 | 3.32 | 16.468 |
| on (`radix_scatter=simd_ballot`) | 286.63 | 1.21 | 14.077 |
| **Δ** | **-60.1%** (total) | **-63.7%** | -14.5% |
production default (`forward_submission=chained`, S2) では radix の全 pass が forward 全体の 1 CB / 1 wait に畳まれ、`ts_fwd_radix_sort` のような単独 label は取れない (`ts_forward` に project/emit/radix/extract/rasterize がまとめて計上される)。radix だけの gpu_ms を切り出すため、この節だけ legacy `forward_submission=per_stage` (S2 以前の経路、production では使わない) で計測している。ts_fwd_radix_sort は histogram + hierarchical scan + scatter の全 pass (11〜16 pass) を含む合計であり、scatter 単体の寄与ではない点に注意 — とはいえ scatter が O(lid) 逐次 scan の中で最も高コストな kernel だった (tasklist の latency 律速の主張と整合) ため、この -63.7% の大半は scatter 由来と推定される。chained mode の `ts_forward` gpu_ms は 5.452 → 3.336 ms/iter (-38.8%) で、上記 per_stage の縮小方向と整合する。この n=1 計測も §0 の 12.5k full run gate (-14.9%) と方向が一致する。
| 実行順 | arm | config | train elapsed (s) | PSNR (dB) | final splats | run 窓内 load1 平均 / 最大 |
|---|---|---|---|---|---|---|
| 1 | off (A) | m4-12500-off-r1 | 150.90 | 36.19775 | 408,928 | 17.45 / 21.30 |
| 2 | on (B) | m4-12500-on-r1 | 128.69 | 36.15227 | 408,408 | 13.85 / 18.37 |
| 3 | off (A) | m4-12500-off-r2 | 149.47 | 36.01755 | 411,049 | 15.40 / 17.98 |
| 4 | on (B) | m4-12500-on-r2 | 126.82 | 36.05730 | 409,074 | 12.78 / 17.36 |
| 5 | off (A) | m4-12500-off-r3 | 149.28 | 36.14695 | 408,113 | 13.38 / 15.64 |
| 6 | on (B) | m4-12500-on-r3 | 127.08 | 36.21933 | 406,770 | 14.26 / 17.66 |
| **mean** | **off** | — | **149.883** (σ 0.591%) | **36.1207** | 409,363 | 15.41 |
| **mean** | **on** | — | **127.530** (σ 0.794%) | **36.1430** | 408,084 | 13.63 |
| **Δ** | — | — | **-14.914%** | **+0.0223** | -0.31% | — |
off 側 sample σ (n=3, n-1 分母) は 0.591%、on 側は 0.794%。有意閾値は max(2σ_off, 1%) = 1.182%。実測 Δ は -14.914%、閾値の 12.6 倍という大差。3 pair 個別の Δ は -14.72% (r1) / -15.15% (r2) / -14.87% (r3) でレンジわずか 0.43 pt — p2-s3 (採用、レンジは pair 間で比較的広かった) や M2 Tier1 (棄却、閾値に肉薄) と比べても際立って一貫している。σ の非対称比は 1.34× (off < on) で、汚染の典型署名とされる 3〜4.5× (M2 Tier1 4.66×、p3-radix-ballot 旧版 replay bench 4.5×) を大きく下回り、on 側が GPU 律速でむしろ host jitter に鈍いという機構と整合する健全な非対称。
on の PSNR 3 値 (36.15227 / 36.05730 / 36.21933) と off の 3 値 (36.19775 / 36.01755 / 36.14695) は完全分離しておらず (on-r1 < off-r1、on-r2 > off-r2、on-r3 > off-r3 と方向が混在)、mean Δ は +0.022 dB。task 指定 guard (on mean ≥ off mean − 0.05 dB) に pass。config header の絶対 guard (36.202 ± 0.1 dB) にも on/off とも帯内で pass。M2 Tier1 gate (p3-tier1-bit-exact) が観測した n=3 完全分離・系統的 PSNR 低下 (-0.065 dB、guard 抵触) とは対照的。sort parity は `tile_offsets` で byte-exact だが `sorted_idx` は既知の tie-order 非決定性 (0.217%、§2 表) を含み、これが rasterize/atomic 加算順序に伝播するため parity の byte-exactness は end-to-end PSNR を pin しない — 観測された小さい非方向性の散らばりはこの既知の非決定性の範囲内として想定どおり。
series 開始時の 1 分 load average は 18.86〜22.8 と、p2-s3 が round 1 を全破棄した汚染クラス (Spotlight `mediaanalysisd`/`mds_stores` 再インデックス) と同型だった。`sudo` 権限が無く `mdutil` での一時停止はできなかったため、①交互 A/B 設計で drift を両 arm に均等に配る、②quiesce 待ちを ambient に合わせて短く保ち run を back-to-back にする (長い quiesce 待ちのほうが「後半 run ほど有利」という別種のドリフトを生む、advisor レビュー指摘)、③独立 load sampler で run 単位の load を事後記録する、の 3 点で対応した。r3 pair で on の load (14.26) が off (13.38) より高いにもかかわらず r3 の Δ (-14.87%) が他 2 pair と同水準だったことは、観測された Δ が load 差の artifact ではなく実効果であることの直接証拠になる。ただし load sampler は 10s 間隔のスナップショットであり、run 内のより短い時間スケールの CPU バーストまでは捉えていない点は限界として残す。
この finding が観測された / 言及している実験 run。