W2 が loss chain 側を 1 CB に畳んだ後、残っていた forward 側 (project → emit → radix → extract → rasterize) の per-stage commit + wait を <strong>1 command buffer / 1 wait</strong> に統合した。radix の per-pass CPU prefix scan は <strong>hierarchical GPU scan</strong> (reduce → block sums → downsweep、整数和なので bit-exact) に、pair_count の host readback は <code>bin_indirect_args</code> + indirect dispatch に置き換えて host 往復を全廃。replay bench は <strong>final 17.833 → 15.637 ms/iter = -12.3% / iter5000 16.600 → 14.395 = -13.3%</strong> (σ ≤1.5% の clean block のみ採用)。機構は Layer 1 が直接示す: <strong>sync-tax 5.21 → 2.86 ms/iter (-45%)、GPU busy 69.1 → 79.8%</strong> の一方で <strong>busy ms/iter は +0.20 しか増えない</strong> (dispatch 26 → ~60 の overhead + scan の CPU→GPU 移動ぶん)。per-kernel 帰属も一致: <code>ts_forward</code> 8573 → 6005 µs (<strong>-30.0%</strong>) が total 差 -2.51 ms/iter をほぼ全額説明し、W2 の <code>ts_chain_commit_wait</code> は 7512 → 7549 µs で<strong>不変</strong> = S2 と W2 は直交。
counter session (p2-counter-session) が「default スタックの GPU busy は 68.9%、sync-tax 5.66 ms/iter、残る 2 大残差は forward の radix と chain_commit_wait」と確定させたのを受け、1× 工程表 v2 が本命に格上げしていた S2 を gate した。W2 (encoder/CB 統合) が backward/loss chain 側を 1 CB に畳んだのに対し、S2 は対になる forward 側を畳む。機構上の障害は 2 つあった — ① radix sort の prefix scan が per-pass で host CPU に降りていた (11 pass × 2 CB = 22 CB / 12 wait)、② rasterize の grid が pair_count に依存し host readback を 1 回強制していた。①は hierarchical GPU scan (reduce → block sums → downsweep) で、②は bin_indirect_args kernel + dispatchThreadgroupsWithIndirectBuffer で除去し、forward 全段を 1 CB / 1 encoder / 末尾 1 wait に連結した。
| 比較対象 | 結果 | 備考 |
|---|---|---|
| `tile_offsets` | **PASS — byte 完全一致** (n=2,500) | binning の最終成果物。S2 の hierarchical scan が整数 prefix sum として per-pass CPU scan と完全同値であることの直接証拠 |
| `means_grad` | PASS (atol/rtol 1e-4、max_abs_diff **2.811e-5**) | n=1,228,980 |
| `log_scales_grad` | PASS (max_abs_diff 5.808e-8) | n=1,228,980 |
| `rot_quats_grad` | PASS (max_abs_diff 2.127e-7) | n=1,638,640 |
| `raw_opacities_grad` | PASS (max_abs_diff 9.005e-8) | n=409,660 |
| `sh_coeffs_grad` | PASS (max_abs_diff 1.558e-7) | n=19,663,680 |
| loss value | baseline 0.004210922 / candidate 0.004210832 (diff **8.99e-8**) | — |
| `sorted_idx` | INFO (gate 対象外) — 2,100/969,279 = **0.217%** 相違 | **既知の baseline 水準の非決定性**。depth 量子化 key の tie 内順序が emit_pairs の atomic write-offset 競合に依存するため、同一 config の run 間でも同程度ズレる (p2-bench-harness-calibration)。S2 固有の差ではない |
| **PARITY OVERALL** | **PASS** | — |
S2 で新規に入る計算 (hierarchical prefix scan) は整数和なので bit-exactであり、それは tile_offsets の byte 完全一致が実証している。しかし end-to-end の run が bit-exact になるわけではない — rasterize backward の atomic<float> 加算順序に由来する既存の非決定性 (sorted_idx 0.217%、grad の 1e-5 級差) を S2 もそのまま継承する。つまり default (per_stage) 同士を 2 回走らせても同じ程度はズレる。だから PSNR guard (n=3 mean ±0.1) は飾りではなく実質的な検査になっている — 「bit-exact だから PSNR は見なくてよい」とは書けない。
S2 の前提条件 (radix_scan = gpu_hierarchical / binning = resident / tile_sort = global_radix / dispatch = per_stage) を hard error で強制する validate() は train config の load 経路 (splat-cli/src/config.rs) にしか配線されていない。BenchTomlConfig の [backend] は #[serde(default)] なので、bench config で field を書き忘れると黙って BackendConfig::default() で埋まり、意図と違う条件を計測してしまう。本 gate では両 arm の [backend] を全 field 明示で書き、差分を forward_submission / radix_scan の 2 つだけに閉じ込めたうえで、bench が stderr に出す backend = BackendConfig {...} の echo で実効値を毎 run 目視確認した。これは harness 側の改善余地として記録しておく (bench でも validate を呼ぶべき)。
| block | σ | 採否 | ms/iter (median) | busy ms/iter | sync-tax ms/iter | busy% |
|---|---|---|---|---|---|---|
| final / perstage pass1 | 0.18% | **採用** | 18.147 | 12.377 | 5.771 | 68.2% |
| final / perstage pass2 | 0.76% | **採用** | 17.889 | 12.282 | 5.587 | 68.8% |
| final / perstage pass3 | 0.56% | **採用** | 17.632 | 12.275 | 5.357 | 69.6% |
| final / perstage pass4 | 0.89% | **採用** | 17.664 | 12.268 | 5.375 | 69.6% |
| final / chained pass1 | 0.10% | **採用** | **15.637** | 12.509 | 3.153 | 79.9% |
| final / chained pass2 | 11.15% | 棄却 | (15.984) | 12.526 | 3.515 | 78.0% |
| final / chained pass3 | 2.66% | 棄却 (DVFS throttle: 14.89 W) | (16.896) | (13.797) | 3.191 | 80.4% |
| final / chained pass4 | 6.85% | 棄却 | (17.341) | 12.660 | 4.681 | 73.0% |
| iter5000 / perstage pass1 | 4.46% | 棄却 | (17.171) | 11.305 | 5.866 | 65.8% |
| iter5000 / perstage pass2 | 0.26% | **採用** | 16.473 | 11.155 | 5.318 | 67.7% |
| iter5000 / perstage pass3 | 0.25% | **採用** | 16.727 | 11.197 | 5.536 | 66.9% |
| iter5000 / perstage pass4 | 4.34% | 棄却 (host stall: sync-tax 10.9) | (22.546) | 11.692 | (10.854) | 51.9% |
| iter5000 / chained pass1 | 0.79% | **採用** | **14.395** | 11.284 | 3.112 | 78.4% |
| iter5000 / chained pass2 | 2.32% | 棄却 | (15.384) | 12.240 | 3.143 | 79.6% |
| iter5000 / chained pass3 | 2.09% | 棄却 | (14.753) | 11.337 | 3.416 | 76.8% |
| iter5000 / chained pass4 | 2.66% | 棄却 | (14.293) | 11.354 | 2.937 | 79.5% |
| regime | 指標 | per_stage (default) | chained (S2) | Δ |
|---|---|---|---|---|
| **final** (409,660 splats) | **total ms/iter** (clean block) | 17.833 | **15.637** | **-12.3%** |
| final | total ms/iter (min-to-min、頑健性確認) | 17.482 | 15.625 | -10.6% |
| final | **busy ms/iter** (min-to-min) | 12.265 | 12.469 | **+0.204 (+1.7%)** |
| final | **sync-tax ms/iter** (min-to-min) | 5.214 | 2.863 | **-2.351 (-45.1%)** |
| final | **GPU busy%** (median / max) | 69.1% / 70.2% | **79.8% / 83.1%** | **+10.7 pt** |
| final | GPU energy / repeat (median) | 72.57 J | 68.81 J | -5.2% |
| **iter5000** (373,933 splats) | **total ms/iter** (clean block) | 16.600 | **14.395** | **-13.3%** |
| iter5000 | total ms/iter (min-to-min) | 16.446 | 14.216 | -13.6% |
| iter5000 | **busy ms/iter** (min-to-min) | 11.154 | 11.255 | **+0.101 (+0.9%)** |
| iter5000 | **sync-tax ms/iter** (min-to-min) | 5.292 | 2.881 | **-2.411 (-45.6%)** |
| iter5000 | **GPU busy%** (median / max) | 66.7% / 67.8% | **78.9% / 79.7%** | **+12.2 pt** |
| iter5000 | GPU energy / repeat (median) | 63.42 J | 62.75 J | -1.1% |
採用 block の σ は perstage 0.18〜0.89% / chained 0.10〜0.79%、したがって有意閾値は max(2σ, 1%) = 1.0〜1.8%。実測 Δ は final -12.3% / iter5000 -13.3% で閾値の 8〜13 倍。頑健性推定 (min-to-min、-10.6% / -13.6%) でも同じ結論。期待帯 (roadmap v2: ms/iter -6〜11%) の上限を上回って着弾した。
| label | per_stage (µs/call) | chained (µs/call) | 読み方 |
|---|---|---|---|
| **`ts_forward`** (forward 全体) | **8,573.4** | **6,005.4** | **-2,568 µs = -30.0%**。total の差 -2,510 µs/iter をほぼ全額説明する |
| `ts_fwd_chain_commit_wait` | — | 5,609.9 | S2 で新設。forward 全段の GPU 実行 + 末尾 1 wait がここに集約 |
| `ts_fwd_radix_sort` | 5,191.1 | 0.0 | chained では独立 CB でなくなり label 消滅 (chain 内へ吸収) |
| `ts_fwd_rasterize` | 1,567.7 | 21.8 | **encode コストのみ**に (W2 の loss chain と同じ性質) |
| `ts_fwd_project` | 1,079.5 | 5.8 | 同上 |
| `ts_fwd_emit_pairs` | 537.8 | 0.0 | 同上 |
| `ts_fwd_extract_off` | 141.2 | 0.0 | 同上 |
| `ts_project_soa_upload` | 21.2 | 0.0 | host upload が消滅 (indirect dispatch 化の副産物) |
| **`ts_chain_commit_wait`** (W2 の loss chain) | **7,512.4** | **7,549.3** | **+0.5% = 不変** → S2 と W2 は直交していることの実測証拠 |
| `ts_refine_accumulate` | 432.5 | 435.7 | 不変 (host 側 refine、S2 の射程外) |
| `ts_backward_raster` / `ts_ssim_fwd_grad` | 94.6 / 97.7 | 99.4 / 99.6 | 不変 (chained submission 下では encode コストのみ) |
roadmap v2 が S2 の注目トレードとして挙げていた「dispatch 数 26 → ~60 の overhead vs wait 15 → 1 の削減」は、Layer 1 の 2 指標で直接判定できる。overhead 側 = busy ms/iter の増分 +0.204 (final) / +0.101 (iter5000)、削減側 = sync-tax の減分 -2.351 / -2.411。すなわち回収額は overhead の 11〜24 倍で、dispatch 数の増加は完全に飲み込まれている。なお busy ms/iter の増分は純粋な dispatch overhead だけでなく、prefix scan が CPU から GPU に移った分の実計算も含む (以前は sync-tax 側に計上されていた仕事) ため、真の dispatch overhead は +0.20 ms/iter よりさらに小さい。
実装 agent が報告していた follow-up 候補 (radix の reduce pass を histogram kernel に畳んで dispatch 数をさらに減らす) は、上記のとおり dispatch overhead の全額が +0.20 ms/iter 未満である以上、その一部しか回収できない = 総額 -1.1% 未満の施策と確定した。有意閾値 max(2σ,1%) の境界そのものであり、単独で gate を通す見込みが薄い。一方 sync-tax は S2 後でも 2.86〜2.88 ms/iter 残っている (busy 80% の残り 20% ぶん) ので、次に取るべきは S3 (iteration pipelining) であって radix の dispatch 削減ではない。follow-up は「S3 着弾後に残 overhead を再測してから判断」に降格する。
roadmap v2 の S2 行は期待を「busy 91-94%、ms/iter -6〜11%」と併記していたが、この 2 つは算術的に両立しない。baseline final は 17.833 = busy 12.27 + sync-tax 5.21 と分解されるので — ① busy 91-94% を満たすには sync-tax を ~0.8-1.2 まで落とす必要があり、その時 total は ~13.3-13.7 = -23〜-25%、② ms/iter -6〜11% (total 15.9-16.8) を満たすと sync-tax は 3.6-4.5 残り、busy は 73-77% にしかならない。さらに sync-tax を 0 にしても total は busy 下限の 12.47 = -30.1% 止まりで、これが S2 単独の理論上限である。実測は total -12.3% / busy 79.8% — ①の ms/iter 換算 (-23〜-25%) には届かないが、②の ms/iter 帯 (-6〜11%) は上回り、busy も②の予想 (73-77%) を超えた。gate 判定は W2 と同じく ms/iter 帯で行い (= PASS)、busy% は機構の証拠として扱う。
S2 後も残る sync-tax 2.86〜2.88 ms/iter は、① forward chain 末尾の 1 wait、② W2 loss chain 末尾の 1 wait、③ refine / accumulate の host 処理 (ts_refine_accumulate 436 µs は S2 で不変) の 3 つ。①②はどちらも「CB が完了するまで host が次 iter を組めない」という構造で、これは CB をさらに畳んでも消えず、iteration pipelining (S3、depth 2) でしか埋まらない。counter session が出した理論上限「busy 100% = 148.17s」に対し、S2 後の busy は ~80% なので、S3 が狙える残額は依然として大きい。
counter session の baseline 216.12s は同日夜に取得したものだが、その後機体の CPU 負荷条件が大きく変わった (§0)。S2 の効果は host 往復の除去なので CPU 競合と交絡しやすく、異なる負荷条件下の cross-session 比較は危険と判断した。そこで S2 arm と default arm を同一 session 内で ABBAAB 交互に n=3 ずつ走らせ、機体条件を arm 間で共有させた。counter session の 216.12s は参考値として併記する。
| run (実行順) | arm | driver wallclock | training elapsed | PSNR (100 views) | final splats |
|---|---|---|---|---|---|
| s2 r1 (21:53:50) | **S2** | 194.58s | 191.47s | 36.154 dB | 407,751 |
| base r1 (21:57:25) | default | 211.64s | 208.45s | 36.107 dB | 405,267 |
| base r2 (22:01:17) | default | 207.23s | 203.95s | 36.167 dB | 408,123 |
| s2 r2 (22:05:05) | **S2** | 184.46s | 181.44s | 36.192 dB | 410,502 |
| s2 r3 (22:08:30) | **S2** | 183.91s | 180.93s | 36.155 dB | 409,861 |
| base r3 (22:11:54) | default | 205.91s | 202.89s | 36.205 dB | 404,002 |
| **S2 n=3 mean** | — | **187.65s** | **184.61s** | **36.167 dB** | 409,371 |
| **default n=3 mean** | — | **208.26s** | **205.10s** | **36.160 dB** | 405,797 |
| **Δ** | — | **-9.9%** | **-10.0%** | **+0.007 dB** (guard 内) | **+0.88%** (S2 が多い) |
| (参考) counter session baseline | default | 216.12s | — | 36.174 dB | — |
2 arm の分布は完全に非重複 (S2: 183.91 / 184.46 / 194.58s vs default: 205.91 / 207.23 / 211.64s) で、最遅の S2 run すら最速の default run より 11.3s 速い。さらに S2 arm の方が final splats が +0.88% 多い — 点数が多い = 仕事量が多いので、wallclock 差が点数差で説明される余地はなくむしろ不利な条件で勝っている。PSNR は n=3 mean で +0.007 dB (guard ±0.1 内)。なお S2 の最遅 run (r1、194.58s) は最も機体負荷が高い時刻 (load 7.74) に先頭で走ったもので、交互設計が意図どおり drift を吸収している。replay・full run・per-kernel 帰属・Layer 1 busy の 4 系統がすべて同じ機構を指すため、採用して 2 つの enum default を flipした。敗者 variant (PerStage / Cpu / Gpu) は A/B baseline として enum 保持。
| iteration | default (n=3 mean elapsed) | S2 (n=3 mean elapsed) | Δ | その時点の splat 数 (概算) |
|---|---|---|---|---|
| 1,000 | 6.66s | 5.01s | **-24.7%** | ~10 万 |
| 3,000 | 28.16s | 23.40s | **-16.9%** | ~30 万 |
| 5,000 | 62.27s | 53.63s | **-13.9%** | ~37 万 |
| 8,000 | 118.55s | 103.95s | **-12.3%** | ~40 万 |
| 10,000 | 156.93s | 138.83s | **-11.5%** | ~40 万 |
| 12,500 (完走) | 205.10s | 184.61s | **-10.0%** | ~41 万 |
W1a (replay -11.2% → full -7.7%) と W2 (replay -13.7% → full -9.1%) はいずれも pass-through 比 ~0.67 だった。S2 も表面上は同じに見える (replay -12.3% → full -9.9%、比 0.80) が、内訳の形がまったく違う。上表のとおり Δ は iteration が進む (= splat 数が増える) ほど単調に縮む — iter1000 で -24.7%、完走時点で -10.0%。これは S2 が削るのが splat 数に依存しない固定コスト (per-stage の commit + wait の往復レイテンシ) だからで、GPU 仕事量が小さい序盤ほど相対的な取り分が大きい。W1/W2 は逆に GPU 仕事量に比例する施策 (backward の演算削減 / GPU chain の実時間) なので、この regime 依存は出ない。含意: ① replay bench は最終 regime (373k/409k splats) しか見ないので、固定コスト系の施策については構造的に過小評価する — harness の既知の盲点として記録する。② より大きなシーン (mipnerf 1M splats 級) では S2 の相対効果はさらに小さくなると予測でき、逆に小シーン・低 iteration 用途では大きい。卒論では「axis 3 (unified memory / submission 論) の ROI はシーン規模に反比例する」という形で一般化できる。
| 時点 | Lego 12.5k | msplat 同日実測比 (169.77s) | 備考 |
|---|---|---|---|
| W1 (warp sparse skip) | 241.41s | 1.44× (対 167.7s) | p2-w1-warp-sparse-skip |
| W2 (encoder/CB 統合) | 219.51s | 1.31× (対 167.7s) | p2-w2-chained-submission |
| counter session 当日 baseline | 216.12s | 1.273× | p2-counter-session (msplat 同日 n=4 = 169.77s) |
| 本 gate の default arm (同一 session) | 208.26s | 1.227× | 本 finding。夜間の機体条件下での default 実測 |
| **+ S2 (forward 1 CB 化)** | **187.65s** | **1.105×** | 本 finding、-9.9% — 累計 766.07 → 187.65s = **-75.5%、全て数学的同値の施策のみ** |
等価 gap は 1.105× まで縮み、1.0× が射程に入った。counter session が出した理論上限 (busy 100% = 148.17s) に対し S2 後の busy は ~80% なので、残る伸びしろは依然 20 pt。次段の優先度は本 gate の実測で明確になった — ① S3 (iteration pipelining depth 2): 残 sync-tax 2.86 ms/iter は「CB 完了まで host が次 iter を組めない」構造で、CB をさらに畳んでも消えず pipelining でしか埋まらない。単独の本命。② S2 follow-up (radix reduce の histogram fold) は総額 -1.1% 未満と判明したので S3 の後ろに defer。③ W3a/W3b/W4/W5 は busy 側 (12.5 ms/iter) を削る施策なので、busy% が上がった今こそ相対的な旨みが増している — S3 と並行して breakdown を取り直す。④ 保持 flag 全数再 gate sweep はコスト構造が 2 波 (W2・S2) 変わったので必須。なお本 flip で敗者 variant を使う旧 config 7 本 (batched_tail 4 / f3 gpu scan 1 / c0 per_iter_alloc 1 / c3 tile_counting 1) に forward_submission = "per_stage" を明示追記済み — sweep 時に validate で落ちない。
この finding が観測された / 言及している実験 run。