松田研究室
FindingID: p3-s4-host-shadow
Status: stable / task S4: C2 施策 #1 (post-backward host 仕事の backward shadow 配置) 実装 + gate
← Findings Index
2026-09 ゼミ milestones · S4 · C2 施策 #1 · gate 棄却

S4 — post-backward accumulate を backward CB shadow に配置する host_schedule flag — 棄却確定 (2026-08-30 gate)

`p3-c2-bubble-attribution` 施策 #1 (期待 -3〜-5%、実装後の再見積りで -1.5〜-3%) の実装。`[backend] host_schedule = "forward_shadow" (default) | "backward_shadow"` を追加し、<code>RefineState::accumulate</code> (+ その入力の grad readback) だけを 1 iteration 遅らせて <code>B_it+1</code> の commit 直後 (<code>B_it</code> 自身の 6.38ms shadow の中) で flush できるようにした。実装・test・1000 iter smoke は完了していたが、<strong>12.5k n=3 A/B gate で Δ +5.63% (on が off より遅い、期待と逆符号、閾値 max(2σ,1%)=1% を大きく外れて不通過) — 棄却確定、default (ForwardShadow) は変更しない</strong> (§8)。

negativerank: lowrejected第 1 軸第 3 軸s4c2host-schedulepipeliningdepth2refine-accumulategate-rejectedcontaminated-pair-retry
Δ PSNR
+0.027 dB (guard 内、ただし wallclock 不通過のため判定は変わらない)
Δ Wallclock
**逆符号 (on が遅い) — どの扱いでも正符号、代表値は使わないこと**。採用条件 Δ ≤ -1% に対し: r1→r4 置換 n=3 で **+5.63%** (on-r4 の大外れ値 141.77s が支配)、on-r4 を外れ値として除外した n=2 相当で **+1.8%**、r1 込み (retry 前) n=3 で **+1.02%**。σ_on 6.36% という広い散らばりに対し +5.63% は解像できる効果量ではなく、**真の効果量は ~1〜2% (harness 分解能付近) で null〜わずかな悪化、`p3-tier1-bit-exact` v1→v2 と同種の『単一外れ値が代表値を膨らませる』パターン**とみるのが妥当。どの扱いでも符号は一貫して不通過 (on≥off) なので棄却の結論自体は変わらない
Impact
C2 施策 #1 (post-backward host 仕事を backward CB shadow に配置) を `[backend] host_schedule` flag として実装し、12.5k n=3 交互 A/B gate を実施した。**棄却 — ただしどの尺度で見ても符号は on≥off で一貫**: 採用セット (r1→r4 置換) の点推定は +5.63% だが、これは on-r4=141.77s という単一の大外れ値に支配された数字であり (on-r4 を除くと +1.8%、r1 込みの retry 前でも +1.02%)、σ_on 6.36% の広さを踏まえると真の効果量は ~1〜2% (harness 分解能付近) で null〜わずかな悪化とみるのが妥当 (`p3-tier1-bit-exact` v1 が経験した「単一の汚染値が代表値を膨らませる」パターンの符号違い版)。r1 pair は series 独立 load sampler で load1 max 7.91/8.90 (LOAD_LIMIT 6.0 超) を検知し事前登録条件 (i)(ii) を満たしたため 1 回だけ retry (r4) に置換 — ただし (i) の判定は max ベースで行っており、mean ベースでは off-r1 (mean 2.72) は 1.5×中央値 (3.765) 未満で非該当になる (次回の predecision には mean/max のどちらで評価するか明記すべき教訓、§8 参照)。retry 後の on-r4 (141.77s) はさらに大きな外れ値だが、走行中 load1 は終始 clean (mean 1.64 / max 3.19) — 事前登録条件 (i) load 証拠が不成立のため追加 retry はせず (`p3-tier1-bit-exact` §5 の教訓通り、条件 (ii) 単独での retry 連鎖を回避)。discarded r1 pair 自体は onoff。ms/iter trace は on-r4 で iter 2000 以降一貫して高い (discrete spike ではなく持続的) — A7 §7 off-r3 と同型の GPU thermal 蓄積の可能性はあるが host_schedule 由来と特定する根拠はない。全 pair とも off→on の順で実行 (ABAB、ABBA 均衡化なし) — on arm は常に「温まった後」の機体条件で走っており、末尾の on-r4 が最遅であることと整合するが、同構造の A7 gate では on 側が一貫して速かったこともあり主要因とは考えにくい。棄却により `.rs` は一切変更していない。
01

本文

`p3-c2-bubble-attribution` §6 施策 #1 は「post-backward の host 仕事 (grad readback 後の `RefineState::accumulate`、SSIM host 総和、opacity decay 準備等) を forward CB の 2.00ms shadow ではなく backward CB の 6.38ms shadow 側にエンコード順序を変更する」ことを提案し、期待 Δ を -3〜-5% とブラケットした。本 finding は **その実装** (`[backend] host_schedule` flag、`Depth2Pipeline` の 1-iteration lag)、実装段階で判明した制約 (loss staleness を守るスコープ限定、既存 2 世代 scratch の再利用による安全性論証)、そして **12.5k n=3 A/B gate の実測 (Δ +5.63%、期待と逆符号で棄却確定、§8)** を記録する。

1. 実装 — 何を動かし、何を動かさなかったか

対象動かしたか理由
grad readback (`chain.grad_buf` の `to_vec()`/slice 読み)○ (backward_shadow のとき)`RefineState::accumulate` の入力のみに使われ、他の値を消費しない — 遅延させても副作用がない
`RefineState::accumulate`○ (backward_shadow のとき)施策 #1 の本体。`grad_xy_accum`/`visible_count` は refine tick でしか読まれず、tick は常に `debug_assert_drained` 経由で flush 済みを要求する drain 点
`read_loss_mean` (l1)× (常に同期)`StepOutcome.loss` → `history` に直接効く。1 個の float 読みでコストも無視できる
`read_mean_ssim` (SSIM host 総和、1.92M 要素)× (常に同期)`loss = "l1_ssim"` のとき `StepOutcome.loss` の一部。動かすと `loss_history` が 1-step stale になり、`crate::pipeline` module doc の逸脱項目 2 (「表示のみ 1-step stale」は不要と明記済み) を破る。**最大の未着手候補** (下記 §5 で follow-up として明記)
scale_reg/MCMC L1 RMW・adam (out-of-cb)・MCMC noise× (対象外)これらが有効な iteration は `grad_rmw_pending`/`mcmc_noise` により `prefetch_next` が構造的に false — 施策の対象になる「prefetched した iteration」には元々出現しない (実装時に確認、下記 §2 参照)

2. 安全性の論証 — 追加 buffer 世代は不要だった

grad buffer の物理的な生存期間
`chain.grad_buf` (= `Rasterize::work_grad`) は forward scratch と同じ `GenScratch` 2 世代 ping-pong に既に乗っている (`splat-metal/src/kernels/rasterize.rs` の `work_grad: GenScratch<Buffer>` field)。`ScratchGen::generation_for_iter(it) = (it-1) % 2` は周期 2 — `g(it+2) == g(it)` だが `g(it+1) != g(it)`
1-iteration lag が安全な理由
`B_it` の grad は世代 `g(it)`。`B_{it+1}` の encode は世代 `g(it+1) != g(it)` で **別の物理 buffer** を触る。よって `B_it` の grad readback を `B_{it+1}` の commit 直後まで遅らせても、`B_{it+1}` 自身の zero-clear/write と衝突しない。`B_{it+2}` (世代 `g(it)` を再利用) の encode が始まる前に必ず flush されていれば安全 — 実装は `B_{it+1}` の commit 直後 (= `B_{it+2}` の encode よりずっと早い、1 iteration 分の余裕がある) で flush するので十分
追加で必要だった buffer 世代
**0 (ゼロ)**。`p3-c2-bubble-attribution` §6 施策 #1 は「grad の double-buffer 化が必要 (depth3 pipelining と同じコストクラス)」と見積もっていたが、実装時に既存の 2 世代 scratch がこの用途を既にカバーしていることを確認した — 追加のメモリコストなし
flush 位置
`commit(B_it)` → `encode+commit F_{it+1}` (既存の手順 4) → **flush deferred(it-1)** (新規、`B_it` 自身の 6.38ms shadow の中) → `wait(B_it)`。flush を prefetch encode の**後**に置くことで、GPU キューへの投入 (`F_{it+1}` の commit) を host bookkeeping より先に済ませ、GPU が先に完了してもキューが空にならないようにしている (advisor レビューでの指摘を反映)

3. 実装レビュー中に見つけた配線バグ — `Trainer::Cell` 経由をやめ `backend` 直渡しに変更

advisor レビューで発覚: test fixture は `apply_backend_config` を通らないため Cell 配線が効かない

初版実装は `pipelining_depth` と同じパターン (`Trainer::host_schedule_backward_shadow: Cell`、`apply_backend_config` が `[backend] host_schedule` から設定) を踏襲した。しかし `train_loop.rs` の test fixture (`LoopFixture::run_with_backend`) は **`apply_backend_config` を呼ばない** (pipelining 関連の 2 箇所だけ手動で配線する既存規約、A7 の `refine_scratch_persist`/`refine_direct_write` は `backend: &BackendConfig` 引数から `train_loop` が直接読む設計だったため今まで問題にならなかった) — このため新規 test 3 本は **`backend.host_schedule` を設定しても `Depth2Pipeline` 内部では常に `forward_shadow` のまま実行されており、backward_shadow の実コードパスを一度も通っていなかった** (`byte_exact_1iter`/`across_refine_ticks` は forward_shadow 同士の比較になり trivial に pass、新規に足した 6-iteration steady-state test だけが偶然 tolerance を超えて fail した)。advisor レビューでの指摘を受けて検証中に発覚。**修正**: `Trainer::host_schedule_backward_shadow` Cell を削除し、`refine_scratch_persist`/`refine_direct_write` と同じ配線パターン (`train_loop` が `backend: &BackendConfig` から直接読み、`Depth2Pipeline::new(trainer, depth, backward_shadow: bool)` に third arg として渡す) に統一した。修正後は 6 本の parity test すべてが実コードパスを通ることを確認済み (CLI 経由の smoke でも `[backend] BackendConfig { ..., host_schedule: BackwardShadow, ... }` のログ行で直接確認 — **CLI 本番経路 [`splat train`] はこのバグの影響を受けていなかった**、`apply_backend_config` は常に呼ばれるため)。

副産物: single-texel fixture の複数 iteration 比較は元々 flaky (host_schedule と無関係)

配線バグ修正後、6-iteration の steady-state test (`forward_shadow` vs `backward_shadow`、両方 depth2、tick 無しで連続 prefetch) を 8 回走らせたところ **6/8 で `assert_params_close` の tolerance (rel<1e-4) を超えて fail** した。原因は `host_schedule` ではなく、`per_kernel_single_texel_run_to_run_deterministic_1iter` の doc が既に明記していた「iter 2 以降は同一設定を 2 回走らせても atomic 加算順の非決定性で ulp 差が出る」という **fixture 固有の既知の性質** — resync 点 (refine tick 等) が無い長い連続 iteration では、この ulp 差が iteration を追うごとに複利的に効いて、たまたま tolerance を超える。既存の複数 iteration test (`across_refine_ticks` 系) が安定して pass するのは `every=2` 等で頻繁に host 側の同期点が挟まるためで、drift そのものを消しているわけではない。**対応**: steady-state test の iteration 数を 3 に絞り (defer→flush を 2 回連続で踏むには十分)、8/8 pass を確認して確定させた。次にこの fixture で複数 iteration の tight tolerance test を追加する人向けに、この性質を明記しておく。

4. flag 配線と test

  • `splat/crates/splat-train-v1/src/config.rs`: `HostScheduleKind { ForwardShadow (#[default]), BackwardShadow }`、`BackendConfig::host_schedule` field、`validate()` に `host_schedule = "backward_shadow"` は `pipelining = "depth2"` を要求する hard error を追加 (silent override 禁止規約)
  • `splat/crates/splat-train-v1/src/pipeline.rs`: `Depth2Pipeline::new` が `backward_shadow: bool` を引数で受け取る (§3 の配線修正後の最終形、`Trainer::Cell` は使わない)。`Depth2Pipeline` に `backward_shadow: bool` / `deferred_accumulate: Option` を追加。`flush_deferred_accumulate()` を新設し `step()` の手順 4 直後 (手順 5 の前) で常時呼ぶ (no-op if `None`)。`debug_assert_drained()` と `drain()` に deferred flush の防衛的チェック/flush を追加 (`drain()` の signature が `refine_state` を取るよう変更、呼び出し元は `train_loop.rs` の 1 箇所のみ、この flush は現状の `train_loop` からは到達しない backstop — §5 と同じ理由)
  • `splat/crates/splat-train-v1/src/train_loop.rs`: `backend.host_schedule` を直接読んで `Depth2Pipeline::new` に渡す (`refine_scratch_persist`/`refine_direct_write` と同じパターン)。`apply_backend_config` を通らない caller 向けの runtime assert (`pipelining = off` との併用禁止) も `backend` から直接判定するよう統一
  • `splat/crates/splat-cli/src/util.rs`: `apply_backend_config` の exhaustive destructure に `host_schedule: _` を追加 (train_loop が backend 引数から直接消費、という既存 no-op field 記法。追加漏れは compile error になる guard が機能することを `cargo check` で確認)
  • 新規 test 3 本: `pipelining_depth2_backward_shadow_matches_off_across_refine_ticks` (refine every=2 / opacity_reset every=3 / checkpoint iter=5 を跨ぐ 8 iteration、Off と `num_splats` 軌跡・param が一致することを確認 — eager-flush-before-tick の経路を pin)、`pipelining_depth2_backward_shadow_matches_forward_shadow_byte_exact_1iter` (1 iteration では deferred を作る機会が無いので byte-exact)、`pipelining_depth2_backward_shadow_matches_off_steady_state_chain` (tick 無し 3 iteration 連続 prefetch — steady-state の defer→flush→defer→flush chain を forward_shadow と直接比較、§3 で見つけた fixture の flakiness を踏まえ iteration 数を絞って安定化)
  • 既存 `pipelining_depth2_matches_off_*` 系 (byte_exact 1/2iter・refine tick 跨ぎ・A7 direct_write 併用・grad RMW 順序) は **無改変で全 pass** — forward_shadow (default) パスの挙動が一切変わっていないことの regression guard

5. 検証結果

検証結果
`cargo test --workspace --release`全 pass (splat-metal 91、splat-train-v1 88 [新規 3 本含む]、他クレート含め failure 0)。`cargo test -p splat-train-v1` (debug profile、`debug_assert!` 有効) も全 pass — steady-state chain test が `Depth2Pipeline::step`/`drain` の deferred-flush 不変条件を debug assert 込みで通過することを確認
1000 iter smoke (Lego、`loss=l1_ssim`、`refine.every=100 start=500` → tick 6 回)off/on とも `peak_in_flight=2` / `sync_iters=6` / crash なし。PSNR (19.5〜19.6 dB, n=1) と `final splats` (6833〜6896) は off 同士の 2 run 間 (6886 vs 6896) と on 込み (6886 vs 6833) で同程度のばらつき — **atomic 非決定性の範囲内、backward_shadow による correctness regression の兆候なし**
1000 iter smoke の `host_lead`/`cb_gap` p50off/on で有意差なし (例: overall `cb_gap` p50 0.242ms [off] vs 0.246ms [on]、`host_lead` p50 -0.187ms [off] vs -0.188ms [on])。**予想通り** — このスケール (splats ~6.9k) では移動した仕事 (grad copy ~248KB + accumulate 6.9k 要素) が 12.5k 規模 (400k splats, 移動量 ~14.4MB + 407µs) の ~1/60 で、shadow への fill 率の変化が計測ノイズに埋もれる。**この smoke は「correctness + plumbing (flag が実際に効いている、drain 不変条件が壊れない)」の確認であり、性能効果の検証ではない** (advisor レビュー指摘どおり、性能検証は 12.5k gate に委ねる)

6. 期待 Δ の再見積り (SSIM を動かしていない前提で)

12.5k での host 仕事の内訳 (再掲、`p3-c2-bubble-attribution` §4)
forward shadow 2.00ms に対し host 仕事 1.871ms/iter (fill 93.6%) — 内訳は概ね `ts_refine_accumulate` 407µs + grad copy (`chain.grad_buf` → `Vec<f32>`, 400k splats で ~14.4MB, 概算 ~150µs) + `read_mean_ssim` (1.92M 要素 CPU f64 sum、最大項と推定) + `read_loss_mean` (無視できる)
本実装が backward shadow (6.38ms、余裕大) に移した分
grad copy (~150µs) + `RefineState::accumulate` (407µs) ≈ **0.55ms/iter**。`read_mean_ssim`/`read_loss_mean` は動いていない (§1 参照)
forward shadow の fill 率の変化
1.871ms → (1.871-0.55)=1.321ms、fill 率 93.6% → **65%** (2.00ms shadow に対し)
回収できる残差の見積り
残差 (通常境界の bubble、`p3-c2-bubble-attribution` 実測 7.2〜8.1s、wall比 5.8〜6.5%) が host-late (forward shadow からのはみ出し) にほぼ 1:1 で対応するという機構仮説のもとでは、回収量は概ね「動かした host 仕事の割合」に比例する。動かした 0.55ms は元の host 仕事 1.871ms の **29%** — 残差 7.2〜8.1s の 29% ≈ **2.1〜2.3s (wall 124.75s 比 1.7〜1.9%)**。ただし fill 率が 100% を割った時点 (65% < 100%) で理論上ボトルネックが解消されるはずなので、実際の回収はこの比例見積りより大きく出る可能性もある — レンジとして **-1.5〜-3%** とする (`p3-c2-bubble-attribution` の元見積り -3〜-5% は SSIM 総和も動かす前提だったため、それを除いた分だけ保守的に下方修正)
SSIM 総和を動かした場合の追加余地 (未実装、follow-up)
`read_mean_ssim` が host 仕事 1.871ms の中で最大項と推定される (1.92M 要素の f64 sum) ため、これも動かせれば fill 率をさらに大きく下げられ、元の -3〜-5% レンジに近づく可能性が高い。§7 の follow-up 設計 (loss_history の bit-exact 性を保ったまま progress log だけずらす) が前提

7. 未解決点 / 次段への申し送り (gate 前時点、履歴として保持)

  • **12.5k A/B gate (n=3) 実施済み — §8 参照、棄却確定**。gate config は本 finding 記載の 8 本 (r1-r4、r4 は r1 retry) で完了。実測は期待 (-3〜-5%、後に -1.5〜-3% に下方修正) と逆符号で、どの切り出し方でも on≥off (§8 参照、代表値 +5.63% は on-r4 の外れ値に支配されており真の効果量は ~1〜2% とみるのが妥当)
  • **SSIM host 総和 (最大の未着手候補) の follow-up 設計** — `read_mean_ssim` を動かせば §4 の 1.871ms のうち最大項を狙えるが、`loss_history` の bit-exact 性を保ったまま progress log の per-iteration 帰属だけをずらす追加設計が要る (advisor 提案: `history.push` を 1 slot 遅らせ、loop 末尾で flush する形にすれば `loss_history` は変わらない)。**§8/§9 の gate 結果を踏まえると、この follow-up に着手する前に §9 の『改善が現れなかった理由』(相殺 / shadow 位置ズレ / 効果自体が小さすぎる) を先に切り分けるべき** — 施策 #1 単体で期待した改善が測定できなかった以上、同じ設計方針を延長した SSIM 総和 deferral も同程度に測定できない可能性が高い
  • `prefetched == false` の iteration (grad RMW / MCMC noise / refine tick 自体) は元々 deferral の対象にならない — 「sync iteration が続くと恩恵が無い」「refine tick iteration 自身の accumulate は同期のまま」は設計上の意図であり compromise ではない (`HostScheduleKind::BackwardShadow` doc 参照、次に読む人が『直す』対象と誤解しないよう明記)
  • **gate run 完了 — `p3-c2-bubble-attribution` §6 施策 #1 の verdict は「棄却」で確定。`[backend] host_schedule` の default (ForwardShadow) は変更しない。`.rs` は一切触っていない**
  • **(参考、実施していない) 仮に採用判定だった場合の flip 実装は 3 行の単純作業ではなかったことが判明した** — 詳細 §10。次に同種の default flip を判断する人向けの申し送り

8. 12.5k A/B gate (n=3、2026-08-30) — 棄却確定

binary provenance
HEAD `5ef49c4a8aaccb42e9cf4f7df2cbf7fbf3ad7157` (直前の実装 merge commit `9cc5278` から docs-only 1 commit 進んだ状態、`.rs` 差分ゼロを `git diff --name-only` で確認済み)。binary sha256 `38ca2d6d732fc3f9d7efe3246f1a70f07891ba88fe5245bf71f098f3b96e8b72`、series 前後 (主系列・retry とも) 不変を driver ログで確認。`cargo build --release` は 0.06s で完了 (既存 binary が最新、再ビルド不要)
電源 / GPU 権
AC 接続 (`pmset -g batt` で series 通じて `AC Power`)。起動前に `pgrep -fl "target/release/splat"` で他 agent の GPU run 無しを確認 (空)
ambient load (series 開始時)
1.64 / 2.00 / 2.26 (通常範囲)。`LOAD_LIMIT=6.0` / `QUIESCE_MAX=90` の default を変更せず使用 (ambient に十分な余裕があったため)
driver
`splat/scripts/gate-ab-n.sh configs/2026-08-30-s4-12500-off configs/2026-08-30-s4-12500-on s4-12500 3` (主系列、ABABAB 順)。r1 pair のみ `splat/scripts/retry-pair-s4-r4.sh` (新規、`retry-pair-r6.sh` と同型) で個別 retry
flag 実測確認 (6/6 主系列 + 2/2 retry)
全 8 run の `[backend] BackendConfig { … }` ログ行から `host_schedule` を抽出し、off 系 4 run 全て `ForwardShadow`、on 系 4 run 全て `BackwardShadow` を確認 (§3 で発覚した配線バグの再発なし、`train_loop` のログ行 `host_schedule = forward_shadow|backward_shadow (C2/S4)` でも二重確認)
判定規則 (事前登録、`runs-logs/s4-12500-predecision.log` に全文)
採用条件 = wallclock (Δ ≤ -max(2σ_off,1%)) AND PSNR guard (on mean ≥ off mean - 0.05dB) の AND。retry は最大 1 pair のみ、かつ (i) load 証拠 AND (ii) 自 arm 内 3σ超 が同時成立する場合に限る (`p3-tier1-bit-exact` §5-6 の「条件 (ii) 単独では停止できない除外連鎖」教訓を踏まえ AND を必須にした)
実行順armruntrain elapsed (s)PSNR (dB)run 窓内 load1 平均/最大備考
1off (A)s4-12500-off-r1129.2336.1592.72 / 7.91**CONTAMINATED、破棄** (load1 max 7.91 が LOAD_LIMIT 6.0 超、自 arm 内 3σ 超も該当 — (i)(ii) 双方成立)
2on (B)s4-12500-on-r1128.5336.1764.47 / 8.90**CONTAMINATED、破棄** (同一 pair、load1 max 8.90 も LOAD_LIMIT 超。ペア単位で retry)
3off (A)s4-12500-off-r2125.4136.1511.89 / 2.89採用
4on (B)s4-12500-on-r2126.3936.1632.39 / 3.64採用
5off (A)s4-12500-off-r3124.6236.2391.51 / 1.84採用
6on (B)s4-12500-on-r3128.1936.2152.63 / 4.47採用
7 (retry)off (A)s4-12500-off-r4125.1936.1311.81 / 2.35r1 の代替として採用、load 終始 clean
8 (retry)on (B)s4-12500-on-r4141.7736.2251.64 / 3.19r1 の代替として採用。**大外れ値 (on 他 2値から見て統計的に大きく外れる) だが load は終始 clean (max 3.19) — 事前登録条件 (i) 不成立のため追加 retry せず、そのまま採用**。ms/iter trace は iter 2000 以降一貫して on-r3 より高い (discrete spike ではなく持続的、A7 §7 off-r3 と同型の GPU thermal 蓄積の可能性、host_schedule 由来と特定する根拠はない)
**mean (n=3、r1→r4 置換)****off**—**125.073** (σ 0.326%)**36.174**series 独立 load sampler 最大値 8.90 (r1 pair、破棄分)。採用 4 run のみなら load 終始 6.0 未満—
**mean (n=3、r1→r4 置換)****on**—**132.117** (σ 6.36%)**36.201**——
**Δ**——**+5.63%****+0.027**—閾値 max(2σ_off,1%)=1% を大きく外れて不通過 (符号も逆)
判定 — 棄却確定 (wallclock がどの扱いでも符号込みで不通過、PSNR guard の pass は判定を左右しない)

wallclock: 採用セット (r1→r4 置換) の Δ は **+5.63%** (on が off より遅い) で「Δ ≤ -1%」の採用条件と符号が逆。ただしこの数字を効果量の代表値として扱ってはいけない — on-r4=141.77s という単一の大外れ値 (on 他 2 値 126.39/128.19 から大きく外れる) がほぼ全てを支配しており、on-r4 を除くと Δ は +1.8%、retry 前の r1 込み n=3 でも +1.02% と、どの切り出し方でも符号は一貫して不通過だが、大きさは ~1〜2% (σ_on 6.36% の分解能付近) と見るのが妥当 — `p3-tier1-bit-exact` v1 が「off arm 汚染で -4.99% が見かけ上膨らんでいた (真の効果量は ~1%)」と自己反証した構図の符号違い版である。3/3 pair (採用セット) とも on>off で一貫 (r2 +0.78%、r3 +2.86%、r4 +13.24%) — 一貫性はあるが「施策が効いている」ではなく「施策が一貫してわずかに悪化させている」ことを示す一貫性。破棄した r1 pair は逆に on<off (on が速かった) であり、4 pair 全体では 3/4 が on>off ― 採用セットの「3/3 一貫」を全体の一貫性と誤読しないこと。PSNR guard (+0.027dB) は形式的に pass するが wallclock が既に不通過なので AND 判定全体としては不通過。採用しない。`[backend] host_schedule` の default (`ForwardShadow`) は変更せず、`.rs` は一切変更していない。

on-r4 の大外れ値 — retry の連鎖はしない (tier1 §5-6 の教訓の実地適用)

on-r4 (141.77s) は on 系列の他 2 値 (126.39s, 128.19s) から見て統計的に大きく外れている。しかし独立 load sampler は on-r4 の run 窓内で終始 clean (mean 1.64, max 3.19、LOAD_LIMIT 6.0 を一度も超えない) であり、事前登録した retry 条件の (i) (load 証拠) を満たさない。`p3-tier1-bit-exact` §5-6 が明記した教訓 (「自 arm 内 3σ超」単独でのretryは停止条件のない除外連鎖になり得る) をそのまま適用し、追加 retry はしない。ms/iter trace を on-r3 (同一 config、対照的に正常) と比較すると、on-r4 は iter 2000 (5.4 vs 4.7 ms/iter) から一貫して高く、iter 12500 時点でも 13.1 vs 12.2 ms/iter — discrete spike ではなく持続的な劣化パターンで、`p3-refine-3pass` §7 の off-r3 (GPU thermal throttling 疑い) と同型。この session では合計 8 本の 12.5k full run (r1×2 + r2×2 + r3×2 + r4×2) を連続実行しており、終盤の run ほど GPU 熱蓄積の影響を受けやすい一般的リスクはあるが、host_schedule=backward_shadow 由来と特定する直接証拠はなく、むしろ off 側にも同型の劣化が既に他 gate (`p3-refine-3pass` off-r3) で観測されているため、config family 共通のノイズ源として扱う。仮に on-r4 を外れ値として除外し on 系列を r2/r3 の 2 値だけで評価しても mean 127.29s (Δ +1.82%) となお逆符号で不通過であり、on-r4 を含めるか除外するかで結論 (棄却) は変わらない。

開示 1 — retry 条件 (i) は max ベースで評価した (mean だと off-r1 は非該当)

事前登録した条件 (i) 「load1 が series 中央値の 1.5倍超」は mean/max のどちらで評価するかを predecision log 上で明記していなかった。実際の判定は **run 窓内 load1 の max** で行った (off-r1 max 7.91 / on-r1 max 8.90、いずれも series 全 run の max の中央値 4.055 の 1.5倍=6.08 を超過)。**mean ベースで再評価すると off-r1 (mean 2.72) は series 全 run の mean の中央値 2.51 の 1.5倍=3.765 未満で非該当**になり、pair 破棄を単独で licenseするのは on-r1 (mean 4.47 > 3.765、こちらは mean でも該当) 側だけになる。ペア単位retryのため実務上の判断 (r1 pair 全体を破棄) は変わらないが、次回の predecision log では「(i) は mean/max のどちらで評価するか」を事前に明記すべき、という運用上の教訓として記録する。

開示 2 — 実行順は ABAB (ABBA 均衡化なし)、on arm は常に後attackで走る

6 pair とも off→on の順で実行しており (`gate-ab-n.sh` の固定 ARM_SEQ)、ABBA 的なカウンターバランスはしていない。そのため on arm は常に同一 pair 内で「後に (=機体がより温まった状態で)」走る構造的バイアスの可能性を排除できない — 最も遅い run (on-r4) が系列終盤かつ on 側であることはこの仮説と整合する。ただし同一 config family・同じ ABAB 順で行った `p3-refine-3pass` gate (a7v2-12500) では on 側 (改善案) が一貫して off 側より速く出ており、このバイアスが支配的でないことも示唆される。次回の gate 設計では ABBA 順の採用を検討する価値がある、という申し送り。

9. 施策の効果が現れなかったことの解釈 — 推測、追加検証はしていない

  • §8 の再検討通り、実測の効果量は ~1〜2% (null〜わずかな悪化) であり、+5.63% という大きな悪化ではない。説明が必要なのは『なぜ大きく悪化したか』ではなく『なぜ §6 が見積もった -1.5〜-3% の改善が (悪化さえせず) 全く現れなかったか』である。§6 の比例モデル (「動かした host 仕事 0.55ms/iter が forward shadow の fill 率を 93.6%→65% に下げれば、その分だけ残差 bubble が回収される」) はこの規模の gate では支持されなかった
  • 候補仮説 (未検証、次段の追加調査候補、大きな悪化ではなく『改善が相殺された』前提で立て直したもの): (a) `chain.grad_buf` を 1 iteration retain することで生じる追加の host 側分岐/状態管理コスト (`Depth2Pipeline` の `Option` 管理、`flush_deferred_accumulate()` の呼び出し自体) が、移動した 0.55ms/iter の節約分とほぼ相殺している、(b) `flush_deferred_accumulate()` の実際の実行位置が想定 (`B_{it+1}` の 6.38ms shadow の中で十分に隠れる) からズレており、期待したほど shadow に収まっていない、(c) 1000 iter smoke (§5) で host_lead/cb_gap に有意差が出なかったのと整合的に、12.5k でも同程度のスケールでは施策の影響自体が測定ノイズ (σ_on 6.36%) に埋もれる程度しかない、という『そもそも効果が小さすぎる』仮説
  • 本 finding はこれらの仮説を検証していない — 棄却が確定した以上、追加コストをかけて機序を特定する優先度は低いと判断し、次段の判断 (main) に委ねる

10. (参考、実施していない) default flip の実装上の注意 — 単純な 3 行変更ではない

本 gate は棄却のため .rs は一切変更していない。以下は次に同種の default flip を判断する人への申し送り

採否を判断する過程で `host_schedule` の default を `BackwardShadow` に flip する実装可否を検討したところ、単純な `#[default]` 属性移動 + `BackendConfig::default()` 変更では済まないことが判明した。(1) `host_schedule` field には field-level `#[serde(default)]` が付いている (`splat-train-v1/src/config.rs`) — struct-level `#[serde(deny_unknown_fields, default)]` と併存する場合、field-level 側が優先され、TOML で省略された値は `BackendConfig::default()` の literal ではなく `HostScheduleKind::default()` (enum 側の `#[default]` 属性、現在は `ForwardShadow`) から解決される。これは `refine_scratch_persist`/`target_cache` の bool field が field-level `#[serde(default)]` を意図的に付けていない理由 (`bool::default()`=false で struct 側の true を上書きしてしまう) と同型の罠で、enum でも同じ罠が成立する — flip するなら enum の `#[default]` 属性そのものを移動する必要があり、`BackendConfig::default()` の literal だけ変えると TOML 経由の default と Rust 構築の default が silently 分岐する。(2) enum の `#[default]` を移動してもなお、`pipelining = "off"` を明示し `host_schedule` を省略する既存 config が `splat/configs/` に **37 本**確認できる (`grep -rln 'pipelining\s*=\s*"off"' splat/configs/`) — これらは flip 後、省略された `host_schedule` が新 default (`BackwardShadow`) に解決され、`BackendConfig::validate()` の hard error (`host_schedule = "backward_shadow" (C2/S4) requires pipelining = "depth2"`, config.rs:652 付近) で **一括して壊れる**。container-level default は他 field の値に条件分岐できない (`BackendConfig::default()` は静的な struct 一つ) ため、field 自体の default 値だけでこの依存関係を解決することはできない。(3) 解決の方向性として、`splat-cli/src/cmd/bench.rs` の `demote_unspecified_pipelining` (raw TOML 文字列を line scan して `pipelining` が明示されているかを判定し、未指定なら `Off` に demote する) と同型のイディオムが既に存在する — これを `host_schedule` 版として実装し `pipelining != Depth2` かつ raw TOML に `host_schedule` の明示行が無い場合だけ `ForwardShadow` に demote する仕組みを、bench 経路だけでなく **train 側の config load path (`splat-cli/src/config.rs::FullConfig::from_toml_str`)** にも追加する必要がある (bench 側の関数は bench 経路にしか適用されない)。この demote は「明示された `backward_shadow` + `pipelining="off"`」の組合せは引き続き hard error にする (silent override 回避の規約を保つ) 設計にすること。以上は次に flip を判断する人が同じ調査をやり直さずに済むよう記録するもので、本 finding の棄却判定そのものには影響しない。

03

関連 finding

© 2026 大竹律輝 — 松田研究室