S2 が forward を 1 CB に畳んだ後も残っていた sync-tax は「CB 完了まで host が次 iteration を組めない」実行形状そのものだった。iteration N の loss/backward CB を commit した直後に <strong>wait せず</strong> iteration N+1 の forward CB を組んで commit し、in-flight command buffer を常に 2 本以下に保つ depth-2 pipelining を入れた。host の重い仕事 (grad 14.7 MB readback / <code>RefineState::accumulate</code> / SSIM の host 総和) はすべて F<sub>N+1</sub> の GPU 実行の影に入る。12.5k Lego の<strong>同一 session 交互 A/B n=3 で 185.21 → 151.70s = -18.1%</strong> (process wallclock 188.33 → 154.0s = -18.2%)、機構は Layer 1 が直接示す — <strong>GPU busy 77.5 → 91.3%、sync-tax 3.244 → 1.136 ms/iter (-65%)、<code>ts_fwd_chain_commit_wait</code> 5.195 → 2.636 ms/iter</strong>。PSNR は n=3 mean 36.202 dB (off 36.185、guard 内)。<strong>msplat 同日等価 gap は 1.105× → 0.907×</strong>。
wait(BN) の後の host readback / accumulate を FN+1 の GPU 実行の影に置く。12.5k 同一 session 交互 A/B n=3 で 185.21 → 151.70s = -18.1% (閾値 max(2σ,1%) = 6.7% の 2.7 倍)。GPU busy 77.5 → 91.3% / sync-tax 3.244 → 1.136 ms/iter (-65%) で counter session が置いた「busy 100% = 理論上限」への距離は 22.5 pt → 8.7 pt に縮んだ。1k smoke では busy 65.8 → 72.0% にしか上がらず判定不能だった — smoke の splat 数 6.8k では forward の GPU 影 (1.06 ms/iter) が image size 固定の host 仕事 (~1.7 ms/iter) を隠しきれないためで、これは 「pipelining の gate は smoke では代替できない」という harness 上の教訓として残す。残 bubble の内訳も確定した: refine tick の drain 6.04s (43%) > encode(B) 4.66s (33%) > 残差 3.28s (23%) — 次の一手が roadmap の「即効ペア (refine capacity スコープバグ修正)」であることが実測で決まった。msplat 同日等価 gap 1.105× → 0.907×、Phase 0 からの累計 766.07 → 154.0s = -79.9% (全て数学的同値の施策のみ)。counter session (p2-counter-session) と S2 gate (p2-s2-forward-chained) は「残る sync-tax 2.86 ms/iter は command buffer が完了するまで host が次 iteration を組めないという実行形状そのもので、CB をこれ以上畳んでも消えない」と結論していた。S3 はその結論を実装したもの — iteration N の loss/backward CB を commit したら wait せずに iteration N+1 の forward CB を組んで commit し、in-flight command buffer を常に 2 本以下に保つ。host の重い仕事 (grad 14.7 MB readback / RefineState::accumulate / SSIM の host 総和 1.9M 要素) はすべて FN+1 の GPU 実行の影に入る。param / adam state の double buffering は不要 — command queue が 1 本の in-order queue なので BN-1 ≺ FN ≺ BN ≺ FN+1 が保証され、BN の adam write は FN+1 の project read より必ず前に起きる。二重化が要るのは scratch だけ (cross-CB の write-after-read を buffer の同一性ごと消す)。
| test | 何を pin するか | 結果 |
|---|---|---|
| `pipelining_depth2_matches_off_byte_exact_1iter` | 1 iteration の全 param が Off と byte 一致 | **PASS** |
| `pipelining_depth2_matches_off_byte_exact_2iter` | 2 iteration (= 世代 ping-pong が 1 周) で byte 一致 | **PASS** |
| `pipelining_depth2_matches_off_across_refine_ticks` | refine tick (= 逐次点) を跨いでも byte 一致 | **PASS** |
| `pipelining_depth2_grad_rmw_order_preserved_scale_reg` | grad への CPU RMW がある iteration で Off と同じ逐次順序 | **PASS** |
| `pipelining_off_never_leaves_scratch_generation_zero` | `pipelining = off` は slot 0 固定 (S2 と bit-exact) | **PASS** |
| `pipeline::tests` 8 本 | 逐次点 cadence (refine / relocation / opacity reset / checkpoint / grad RMW / MCMC noise) と in-flight limiter | **PASS** |
| workspace 全体 | debug (`-p splat-train-v1` 70 本、`debug_assert` 有効) + release 全 crate | **PASS / warning 0** |
S3 は新しい計算を 1 つも足していないので、test 5 本が示すとおり同一 seed・同一 iteration での結果は Off と byte 一致する。しかし end-to-end の 12.5k run が run 間で bit-exact になるわけではない — rasterize backward の atomic<float> 加算順序に由来する既存の非決定性を S3 もそのまま継承する (p2-s2-forward-chained と同じ)。実際 1k smoke の final loss は off 6.582768e-2 / depth2 6.568347e-2 と一致せず、12.5k の final splats も off 408,874 / depth2 406,439 とズレる。だから PSNR guard (n=3 mean ±0.1) は実質的な検査として機能している。
| 指標 (1k smoke、profile=true、最終 6,826 splats) | off | depth2 | 読み方 |
|---|---|---|---|
| GPU busy% | 65.8% | 72.0% | **期待した 95% 圏に遠く届かない** |
| sync-tax (ms/iter) | 1.573 | 1.270 | -19% しか減らない |
| wallclock (s) | 4.601 | 4.545 | **-1.2% = 判定不能** |
| `ts_fwd_chain_commit_wait` (ms/iter) | 1.252 | 0.176 | **-86% — 機構自体は完全に効いている** |
| `ts_chain_commit_wait` (ms/iter) | 2.135 | 2.411 | +13% (forward 待ちが loss chain 待ちへ移動しただけ) |
1k smoke の最終 splat 数は 6,826 (12.5k run は 407k)。forward の GPU 実行時間はおよそ 1.06 ms/iter しかなく、これが「host 仕事を隠す影」の全長になる。一方 host 側で隠したい仕事のうち SSIM の host 総和 (1.9M 要素) は image size にしか依存せず splat 数で縮まない (~1.7 ms/iter)。したがって smoke では影より仕事のほうが長く、原理的に隠しきれない。12.5k (407k splats) では forward の影が 4〜5 ms/iter に伸びるので全部入る。この構造から「iteration pipelining は smoke で gate できない」という harness 上の一般則が出る — S3 は 12.5k を 2 本 (profile=true の 1 対 1) 追加で走らせて判定に進んだ。
| 実行順 | arm | train elapsed (s) | process wallclock (s) | PSNR (dB) | final splats | run 窓内 load1 平均 / 最大 |
|---|---|---|---|---|---|---|
| 1 | off (A) | 179.87 | 183 | 36.178 | 406,400 | 8.03 / 13.28 |
| 2 | depth2 (B) | 149.63 | 152 | 36.195 | 405,084 | 3.57 / 4.22 |
| — | depth2 (破棄) | (165.67) | (169) | (36.280) | (407,650) | **7.95 / 15.16** |
| 3 | off (A) | 191.95 | 195 | 36.192 | 409,011 | 3.10 / 4.04 |
| 4 | depth2 (B) | 152.70 | 155 | 36.179 | 407,677 | 2.14 / 2.58 |
| 5 | off (A) | 183.80 | 187 | 36.186 | 411,211 | 3.62 / 4.34 |
| 6 | depth2 (B) | 152.77 | 155 | 36.231 | 406,555 | 4.96 / 6.94 |
| **mean** | **off** | **185.21** (σ 3.33%) | **188.33** | **36.185** | 408,874 | 4.92 |
| **mean** | **depth2** | **151.70** (σ 1.18%) | **154.00** | **36.202** | 406,439 | 3.56 |
| **Δ** | — | **-18.09%** | **-18.23%** | **+0.016** | -0.6% | — |
σ は off 3.33% / depth2 1.18%、したがって有意閾値は max(2σ, 1%) = 6.7%。実測 Δ -18.1% はその 2.7 倍。加えて σ の非対称自体が S3 の機構を裏づける — depth2 は GPU busy 91.3% でほぼ GPU 律速なので host 側 jitter に鈍く、off は host 往復が critical path に乗っているので jitter をそのまま拾う。同一 session の off arm (train 185.21s / process 188.33s) は S2 gate 当日の S2 arm (184.61 / 187.65) を +0.3% で再現しており、session の健全性も別途確認できている。期待帯 (roadmap v2 再見積り: busy 96% 圏 / ms/iter -13〜16%) に対しては busy は下振れ (91.3%) / wallclock は上振れ (-18.1%) で着弾した。
23:27〜23:45 の ABBAAB は off 182.32 / 188.90 / 200.06s (mean 190.43、σ 4.71%)、depth2 166.74 / 162.49 / 171.19s (mean 166.81、σ 2.61%)、Δ -12.4%。この間に別プロジェクトの tsc / Gradle / wrangler が立ち上がり 1 分 load が 2 → 23 まで上がった。破棄の理由は Δ の大小ではなく arm 間で σ が非対称になったことで、S3 のように「host 仕事を影に隠す」施策では CPU 競合が depth2 側を選択的に殴るため、汚染下の Δ (-12.4%) は真値より小さく出る。実際 clean 条件で取り直すと -18.1% に伸びた。同じ現象は単一 run 単位でも観測できる — depth2-r2 は load 平均 7.95 で 165.67s、load 平均 2.14 で 152.70s (+8.5%)。
| 指標 (12.5k、profile=true、汚染前に取得) | off | depth2 | Δ |
|---|---|---|---|
| **GPU busy%** | 77.5% | **91.3%** | **+13.8 pt** |
| **GPU busy (s)** | 139.854 | 148.436 | +6.1% |
| **sync-tax (s / ms per iter)** | 40.556 / 3.244 | **14.206 / 1.136** | **-65.0%** |
| train elapsed (s) | 180.49 | 162.72 | -9.85% |
| `[pipelining]` 計器 | — | `peak_in_flight=2` / `sync_iters=121 / 12500` (99.0% で先行 encode) | in-flight CB が仕様どおり 2 本以下 |
| final splats | 407,489 | 410,011 | +0.6% |
| self-eval PSNR (dB) | 36.127 | 36.247 | +0.120 |
先行 encode を止める iteration 数は 121。refine tick は start_iter=500 / every=100 / stop_iter=12500 なので (12500 − 500) / 100 + 1 = 121 で完全一致する。1k smoke でも sync_iters = 6 = (1000 − 500) / 100 + 1 で一致。param_mutated_after_step が refine tick でだけ発火し、それ以外の想定外の drain が 1 度も起きていないことの直接証拠になる (本 config は MCMC noise / relocation / opacity reset / checkpoint をすべて無効にしているので、期待される逐次点は refine tick のみ)。
| label (µs/call、12.5k profile=true) | off | depth2 | 読み方 |
|---|---|---|---|
| **`ts_fwd_chain_commit_wait`** (forward CB の wait) | **5,195.0** | **2,636.0** | **-49.3%** — S3 の機構そのもの。forward の待ちが B_{N-1} の実行と重なって消える |
| `ts_forward` (forward 全体) | 5,509.0 | 2,640.0 | -52.1% (上の wait が支配的) |
| **`ts_chain_commit_wait`** (loss/backward CB の wait) | **6,684.0** | **7,109.0** | **+6.4%** — forward 待ちが減ったぶん GPU 律速がここに集まる (期待どおり) |
| `ts_refine_accumulate` | 323.4 | 493.2 | +52.5% だが **gpu_ms 0 → 1,871.9** = F_{N+1} の影で走っていることの計器上の証拠 |
| `ts_ssim_fwd_grad` (encode) | 83.3 | 127.1 | +52.6% |
| `ts_backward_raster` (encode) | 72.8 | 115.1 | +58.2% |
| `ts_raster_bwd_alloc` | 56.6 | 88.9 | +57.1% |
| `ts_zero_grads` | 8.6 | 18.6 | +115.7% |
| `ts_raster_bwd_upload` | 8.6 | 13.9 | +60.9% |
| `ts_adam` (encode) | 2.4 | 3.5 | +49.7% |
| `ts_refine` (µs / tick) | 43,066 | 49,888 | +15.8% (121 tick) |
depth2 では すべての encode scope が 1.5〜2.2 倍に膨らむ (1k smoke でも同じ比率で観測できたので splat 数に依らない構造的なコスト)。原因は「GPU が別の CB を実行している最中に host が encode している」ことで、driver 側の lock 競合と unified memory の帯域競合の両方が効いていると考えられる。総額は encode(B) 合計で 0.236 → 0.373 ms/iter (+0.137)。module doc が置いていた「残る bubble は encode(B) のみ ~0.3 ms/iter」という見積りはこの膨張を織り込んでいなかったので、実測値 0.373 に訂正する。同様に GPU busy 時間そのものが +6.1% (139.85 → 148.44s) 増えている — splat 数の差 (+0.6%) では説明できず、① 2 世代化した scratch (+275 MB) の ping-pong による SLC/L2 局所性の悪化、② CB を隙間なく詰めたことで GPUStartTime/GPUEndTime の union に scheduling latency が算入されるようになった計測上の artifact、の 2 つが候補。どちらであっても回収額 (sync-tax -26.35s) が代金 (+8.58s) を 3 倍以上上回るので判定は変わらないが、切り分けは Xcode capture の残件に積む。
| 残 sync-tax の内訳 (depth2、12.5k、profile=true、合計 14.206s) | s | ms/iter | share | 解消手段 |
|---|---|---|---|---|
| **refine tick の drain** (`ts_refine` + `ts_opacity_decay`、121 tick) | **6.26** | 0.501 | **44%** | roadmap の**即効ペア** (`refine()` capacity スコープバグ修正 ~2.1GB/tick → ~0.85GB)。tick 中は先行 encode を止めるので GPU が完全に空く |
| **encode(B)** (zero_grads → ssim → l1 → backward → project_back → adam) | **4.66** | 0.373 | **33%** | 構造上ここは埋まらない (depth 3 以上か encode の並列化が必要)。ただし上の「encode 膨張」を抑えられれば 0.236 相当まで下がる余地 |
| 残差 (影に収まりきらない readback / driver overhead) | 3.28 | 0.263 | 23% | Xcode capture で CB 内 occupancy を見る残件 |
counter session は「busy 100% なら 216.12 → 148.17s」という上限を置いていたが、これは S2 時点の busy ms/iter を固定した計算だった。S3 は busy 時間自体を +6.1% 押し上げているので上限も動く。本 gate の実測 (profile=true で busy 91.3%) をそのまま profile=false の 151.70s に当てると 残 bubble 8.7% = 約 13.2s、busy 100% の床は ~138.5s。ただし profile=true / false で busy 時間が同じという保証は無い (計装は host 側にも GPU 側にも乗る) ため、これは推定であって実測ではない。確定させるには profile=false 相当の busy を別経路で取る必要があり、Xcode capture の残件と同じ袋に入れる。
splat bench は train_step を直接叩く replay harness で、iteration 境界を持たない。したがって pipelining は原理的に測れない (黙って Off 相当で走ると「S3 の replay 差分が 0」という誤った測定になる)。そこで build_trainer が pipelining != off を hard error にしてある。一方で本 gate の default flip 後は、pipelining を書いていない既存 bench config 47 本 + parity candidate 11 本が全部この hard error に落ちるため、raw TOML に pipelining キーが無い config は Off に落とす demote を BenchTomlConfig::from_path と run_parity の両方に入れた (validate() より前に走らせる必要がある — 敗者 variant を pin した config は validate 側で先に落ちるため)。明示的に depth2 と書いた場合だけ hard error に到達する。
| 段階 | Lego 12.5k (s) | msplat 169.77s 比 | 備考 |
|---|---|---|---|
| Phase 0 | 766.07 | 4.51× | — |
| S1 | 261.55 | 1.54× | — |
| W1 | 241.41 | 1.42× | warp sparse skip |
| W2 | 219.51 | 1.29× | chained submission (loss chain 1 CB) |
| S2 | 187.65 | 1.105× | forward 1 CB + hierarchical scan |
| **S3** | **154.00** | **0.907×** | **iteration pipelining depth 2** |
process wallclock 154.00s は msplat 同日実測 169.77s の 0.907× で、数字の上では初めて 1.0× を割った。しかしこれは matched 断面ではない — 本実装 Lego 12.5k の PSNR は 36.202 dB、msplat 30K の native val は 36.539 dB で 0.34 dB 低い。roadmap v2 も最終段に「**msplat 等価比較の再実施 (matched 断面込み)**」を残しており、そこを通すまでは「等価 gap 0.907×」という指標値としてのみ扱う。卒論本文に「速い」と書けるのは matched 断面を取った後。
内訳は BinArena 172 MB (うち radix keys/idx が 156 MB)、SSIM work 56 MB、projector 16 MB、rasterize forward 15 MB、splats_grad 15 MB。P2 調査時の見積り「~81 MB」は radix の keys/idx を数え落としていたので 3 倍以上の乖離がある。実効的には pair capacity = num_splats × 16 の係数がそのままメモリ代価に効くので、将来 capacity hint を下げれば同じ割合で減る。Lego 12.5k で実際に peak RSS が問題になった場面は無い。
この finding が観測された / 言及している実験 run。