松田研究室
FindingID: p3-refine-nondrain
Status: stable / task C2 施策 #4: refine tick drain の non-draining 化
← Findings Index
2026-09 ゼミ milestones · C2 施策 #4 · refine tick drain non-draining 化 (12.5k gate 棄却)

C2 施策 #4 — refine tick drain の non-draining 化。drain は構造的に必須、snapshot read の GPU blit 化 (`refine_gpu_snapshot`) は 12.5k n=3 gate で棄却

`B_it` 統合 CB の Adam encoder が param/adam 全 15 buffer を書くため、<code>wait(B_it)</code> 自体は depth3 化 (buffer 2 世代化、実装コスト大) なしには消せない — refine tick drain の <strong>構造的な必要性</strong>を確認した。縮められるのは drain の中身: `refine()` の CPU snapshot read (15 buffer中 <code>sh_coeffs</code>/<code>m_sh_coeffs</code>/<code>v_sh_coeffs</code> が支配的、407k splats で ~250MB/tick) を host memcpy から <code>B_it</code> CB 末尾 (Adam encoder 直後、同一 CB 内で hazard tracking により直列化) への GPU blit に置き換える <code>[backend] refine_gpu_snapshot</code> を実装した。<code>raw_opacities</code> は brush 互換 opacity decay が blit 後に書き換えるため対象外、grad RMW / MCMC noise・relocation が同 tick で発火する場合は <code>refine_tick_wants_gpu_snapshot</code> が安全側にフォールバックする。bit-exact 性は「iter tag が一致したときだけ消費、それ以外は無条件 host snapshot」という <code>Option::take()</code> ベースの機構で保証 (誤った iteration の値を使うことは構造的に起きない)。unit test 3 本 (advisor 指摘の 2 trap + capacity overflow 生存性)・integration test 2 本 を追加、<code>cargo test --workspace --release</code> 全 pass。<strong>2026-08-30 実施の 12.5k n=3 交互 A/B gate では Δ -0.2365% (閾値 max(2σ_off,1%)=1.000%) で実質ノイズ水準の null 結果、pair 一貫性も 2/3 に留まり、advisor 試算 (-1.2〜-1.9%) の pessimistic 側にすら届かなかった。PSNR guard (+0.0553dB) は pass するが wallclock が閾値の 24% にしか届かず棄却、`refine_gpu_snapshot` の default は false のまま維持。</strong>

negativerank: lowstructural-drain-necessity-confirmed / snapshot-read-gpu-blit-implemented / gate-rejected-null-effect第 1 軸第 3 軸p3-refine-nondrainc2refinegpu-snapshotdrainnon-drainingin-flight-invariantlegogate-rejected
Δ PSNR
12.5k n=3 gate: on mean 36.1937dB (36.179/36.203/36.199) vs off mean 36.1383dB (36.113/36.187/36.115) = **+0.0553dB**、guard (-0.05dB 以内) は pass。gradient 非変更の bit-exact 設計のため理論上は差が出ないはずで、この +0.055dB は backward atomic の run-to-run noise とみなすのが妥当 (`p3-s4b-ssim-reduce` と同型の解釈)
Δ Wallclock
12.5k n=3 gate (2026-08-30, off→on×3): train elapsed off mean **122.6467s** (σ 0.330%) → on mean **122.3567s** (σ 0.301%) = Δ **-0.2365%**。有意閾値 max(2σ_off,1%)=1.000% の 24% にしか届かず不通過。advisor 試算 (-1.2〜-1.9%) を大きく下回る null 結果、pair 一貫性も 2/3 に留まる (§7 参照)
Impact
`p3-c2-bubble-attribution` §6 施策 #4 (refine tick drain の non-draining 化、期待 -1.5〜-2.5%) を検討・実装した。advisor との構造分析で「drain 自体は構造的に必須、縮められるのは中身」と reframe: `Depth2Pipeline::step` の `B_it` 統合 CB は Adam encoder で param/adam 全 15 buffer を書き換えるため、`refine()` の snapshot read はこの書き込み完了 (`wait(B_it)`) を待たないと race になる。task が提示した候補 (a) tick を `B_it` commit 直後に前倒し、(b) 候補選定だけ先に走らせる 2 段化、(c) 再充填コスト削減 は、いずれも「`B_it` の GPU 実行中に host が snapshot を読む/書く」という同じ race を踏むか、staleness (1 tick 分古い grad/param を使う意味論変更) を受け入れる必要があり、後者は `p3-refine-every-200` (densify 予算副作用で PSNR guard 8.4 倍超過、棄却) と同種のリスクを持つ。一方、advisor が提案した第 4 の道 (host memcpy を GPU blit に置き換えて `B_it` CB 自体を延長する) は staleness を一切受け入れずに host の逐次コピーコストを消せる — これを `[backend] refine_gpu_snapshot` として実装した。snapshot 対象 15 buffer のうち `raw_opacities` (opacity decay が blit 後に書き換える) を除く 14 buffer を、Adam encoder 直後に追加した 1 blit command encoder (14 `copy_from_buffer`) で GPU 側 mirror buffer にコピーし、`refine()` はその mirror を読む。安全性は 2 層: (1) encode 時点で `pipeline::refine_tick_wants_gpu_snapshot` が「Adam が同一 CB 内で完了するか (`!grad_rmw_pending`)」「`wait(B_it)` 後に MCMC noise/relocation がこれらの buffer を触らないか」を判定し、条件を満たさない tick では blit を encode しない、(2) `refine()` 側は `gpu_snapshot_ready_for.take() == Some(it)` のときだけ消費し、iteration が食い違えば (万一 (1) の判定が pipeline とズレても) 無条件に host snapshot へフォールバックする — 誤った iteration の値を使うバグが構造的に起きない設計。`cargo test --workspace --release` 全 pass (0 failed、新規 5 test 込み)。1000 iter smoke (Lego) で crash なし、`refine_gpu_snapshot: true` が実際に解決され `ts_refine_snapshot_blit` が refine tick 数 (6 回) と一致する回数だけ発火することを確認。2026-08-30、main 上で 12.5k n=3 交互 A/B gate (off→on×3) を実施: train elapsed (internal timer) off mean 122.6467s (σ 0.330%) → on mean 122.3567s (σ 0.301%) = Δ **-0.2365%**。有意閾値 max(2σ_off,1%)=1.000% の **24%** にしか届かず、pair 一貫性も 3 組中 2 組 (r1: -0.788%, r2: -0.082%) が on 優位、r3 (+0.163%) は逆方向という系統性の無い結果。事前の advisor 試算 (-1.2〜-1.9%) が明記した「pessimistic ケースでは不通過もあり得る」を超え、実測は pessimistic ケースの想定 (max(2σ,1%) 近傍での僅差) よりもさらに小さい、実質ノイズ水準の null 結果だった。PSNR は +0.0553dB で guard (-0.05dB 以内) を pass するが、wallclock が閾値に遠く及ばないため棄却確定、`refine_gpu_snapshot` の default は false のまま維持、code は変更しない。汚染判定 (`p3-tier1-bit-exact.toml` §5 の事前登録条件、load spike AND 3σ超 の AND) は off-r1 の load1 window mean が他 run よりやや高い (2.41 vs 0.80-1.38) ものの偏差 1.07σ (<3σ) で条件不成立、かつ off-r1 を除外しても Δ は -0.060% とさらに小さくなるだけで結論は変わらないため retry は実施しなかった。
01

本文

task は refine tick drain (3.0〜3.8s、wall の 2.4〜3.1%) の non-draining 化を求めた。実装に入る前に advisor と行った構造分析で、drain (`wait(B_it)`) 自体は depth3 化なしには除去できないことが判明し、task が提示した 3 候補はいずれも staleness を受け入れるか低い ceiling (6.4ms の backward shadow に押し込めるだけ) にとどまることが分かった。本 finding は (1) drain が構造的に必要な理由、(2) 3 候補 + advisor 提案の第 4 候補の比較、(3) 採用した設計 (`refine_gpu_snapshot`) の bit-exact 性・安全性、(4) test/smoke 結果、(5) gate config と期待値、の順で記録する。

1. なぜ drain が構造的に必要か — Adam encoder が B_it 内で全 15 buffer を書く

事実
`encode_loss_backward_chain` (`pipeline.rs`) は `grad_rmw_pending == false` (grad への CPU RMW が無い iter、Lego 12.5k final-stack config はこれに該当) のとき `ts_adam` (Adam step) を **`B_it` 統合 CB の内側**にエンコードする。Adam step は `param.means`/`log_scales`/`rot_quats`/`raw_opacities`/`sh_coeffs` と `adam_state` の m/v 全 10 buffer、計 15 buffer を GPU 上で書き換える。
帰結
`refine()` が `param`/`adam_state` の値を host から読む (`take_snapshot`) には、その GPU 書き込みが完了していること (= `B_it` の `wait_until_completed()`) が **race を避けるための必要条件**になる。これは A7(b) `refine_direct_write` の安全性論証 (`Depth2Pipeline::debug_assert_drained`) と同じ前提であり、既に `p3-refine-3pass.toml` で明文化済みだった。
depth3 化すれば理論上は消せる
buffer をもう 1 世代持てば、host は 1 世代前の (確定済みの) 値を読みながら GPU は次の世代に書き込める。しかし `pipeline.rs` module doc が明記する通り depth3 (in-flight CB をもう 1 本増やす) は「禁忌」スコープ — メモリコスト (2 世代化だけで Lego 407k で ~275MB、depth3 はさらに増える) と正しさの論証の複雑化に見合わないと判断されている。本 finding はこの前提を覆さない。

2. 設計候補の比較 — task 提示の 3 案 + advisor 提案の第 4 案

#案bit-exact 性ceiling採否
(a)tick を `B_it` commit 直後・`F_{it+1}` encode 前に置き、`B_it` の GPU 実行と host densify を重ねる**意味論変更** — `B_it` はこの時点でまだ実行中 (Adam 未完了) なので、host が読む grad/param は前 iteration (`it-1`) までの値になる。1 tick 分 stale6.379ms (`B_it` shadow) しかなく、advisor 試算で refine bucket (~24ms/tick) の ~27% (≈0.6s、0.5%程度) が上限。stale の代償に見合わない不採用
(b)densify の host 計算 (候補選定・index 計算) だけ先に走らせ、buffer 書き換えは drain 後に短時間で行う 2 段化**(a) と同じ race を踏む** — 候補選定は `old_raw_opacities`/`old_log_scales` 等を読む必要があり、これらも Adam 未完了の値になる。真に安全にするには (a) と同じ staleness を受け入れる必要があり、2 段化しても race 条件は変わらない(a) と同じ 6.379ms shadow 制約不採用 (机上で (a) に帰着すると判明)
(c)再充填コストの削減 (tick 直後の `F` を通常 depth2 で即 overlap)bit-exact (staleness なし)**ceiling ≈ 0** — `F_{it+1}` は post-refine の param/num_splats に依存するため refine() 完了前には encode できず、host は refine() 完了直後に `F_{it+1}` を encode するしかない (現状の実装と同じ)。オーバーラップできる相手が構造的に存在しない不採用 (効果なしと判明)
(4, advisor 提案)host memcpy による snapshot read を `B_it` 統合 CB 末尾 (Adam 直後) への GPU blit に置き換える (`B_it` の CB 自体を延長する — 何かと重ねるのではない)**bit-exact** — blit は Adam の直後・同一 CB 内 (hazard tracking で直列化) から同じバイトをコピーするだけ。staleness ゼロ6.4ms shadow の制約を受けない (「重ねる」設計ではないため)。ceiling は snapshot read が refine tick drain bucket に占める割合 (advisor 試算 ~半分)**採用**
(a)/(b) が同じ race に帰着する理由 — advisor 指摘

(a)/(b) の狙いは「host の CPU 作業を `B_it` の GPU 実行時間に重ねる」ことだが、`B_it` の Adam encoder が `refine()` の読み書き対象と**同じ 15 buffer**を書いているため、「重ねる」こと自体が race になる。狙いを達成するには「重ねる」のではなく「対象を `B_it` 完了前の世代に限定する」しかなく、それは即ち staleness の受容に等しい。(4) が (a)/(b) と質的に違うのは、`B_it` と並行して host が読み書きするのではなく、`B_it` 自身の CB に blit を追加して GPU 側で完結させる点 — 「重ねる」候補ではなく「host 作業を GPU に移す」候補であり、この違いが 6.4ms shadow の制約を受けない理由になっている。

3. 採用した設計 — `[backend] refine_gpu_snapshot`

確認内容file:symbol備考
blit 対象 14 buffer の定義・確保`splat-train-v1/src/refine.rs` `RefineGpuSnapshot` struct + `RefineGpuSnapshot::new``raw_opacities` (値そのもの) のみ除外。`m_raw_opacities`/`v_raw_opacities` は opacity decay が触らないので対象に含む
blit の encode (B_it CB 末尾)`refine.rs` `RefineState::encode_gpu_snapshot_blit` — Adam encoder 直後に呼ぶ呼び出し側 (`Depth2Pipeline::step`) が commit **前**に呼ぶ制約を守る (`pipeline.rs` の呼び出し箇所コメント参照)
安全条件の判定 (encode してよいか)`pipeline.rs` `refine_tick_wants_gpu_snapshot` — grad RMW / MCMC noise / 同 it の MCMC relocation を除外`param_mutated_after_step` の姉妹関数、「`train_loop` の実際の分岐条件と 1:1 対応させること」という同じ規約を継承
host memcpy を実際に消す (**修正済み**)`refine.rs` `refine()` 冒頭、`Cow<[f32]>` で `Cow::Borrowed`(GPU mirror への zero-copy `&[f32]`) / `Cow::Owned`(`take_snapshot` の host memcpy Vec) を分岐**gate 前に advisor レビューで発覚した実装ミスの修正箇所**。初版は `take_snapshot` の引数 (読み出し元 buffer) を差し替えただけで memcpy 自体を呼び続けており、GPU blit を「追加」しただけで host コピーを「置換」できていなかった (net で純増、下記コールアウト参照)
bit-exact な消費 (iteration tag)`refine.rs` `refine()` 冒頭 `refine_state.gpu_snapshot_ready_for.take() == Some(it)``Option::take()` で毎回無条件にクリアするため、iteration 不一致時に stale な値を誤って使うことは構造的に起きない (最適化されないだけに縮退)
`[backend]` flag / validate`config.rs` `BackendConfig::refine_gpu_snapshot` (default false) + `validate()` の `pipelining != Depth2` hard error`refine_scratch_persist`/`refine_direct_write` と同じ配線パターン (`apply_backend_config` の exhaustive destructure に追加)
gate 前に発覚した実装ミス — 「読み出し元を変える」だけでは host memcpy は消えない

初版実装は `old_means = take_snapshot(&mut ..., persist, means_buf, n)` のように、`means_buf` を `¶m.means` から `&snap.means` (GPU mirror) に差し替えるだけだった。しかし `take_snapshot` 自体は `buf.contents()` から `Vec::extend_from_slice` で **host CPU memcpy** する実装のままで、読み出し元アドレスが変わっただけで memcpy は消えていなかった — つまり tick ごとに host memcpy (~250MB) + GPU blit (~250MB) の両方を行っており、net で純増になっていた。1000 iter smoke (7k splats 規模、advisor 指摘どおりこの規模では効果は測れないが方向性は検出できる) で `ts_refine` per-call を比較したところ、初版は off (365.6µs/call) より on (618.9µs/call) の方が **遅い** — 実際に純増していたことを既存ログのみで確認した (advisor 助言「30 秒で確認できる」の通り、新規 GPU run 不要)。修正 (`Cow<[f32]>` 化、host memcpy を `Cow::Borrowed` 分岐で完全にスキップ) 後、同条件で on 370.5µs/call ≈ off 365.6µs/call まで復帰 (この小規模 regime では snapshot read 自体が `ts_refine` の小部分のため、劇的な差は出ない — 12.5k/407k splats 規模で初めて有意な差が出る想定、advisor 試算 -1.2〜-1.9%)。この bug を含む版で 12.5k gate を実施していたら、「効果なし〜わずかに悪化」という誤った結論に達していたはず — 実装完了後・gate 実施前の advisor レビューが機能した事例として記録する。

4. test / smoke 結果

test何を確認するか結果
`refine_gpu_snapshot_matches_host_snapshot_when_blitted` (unit)blit した GPU snapshot を消費した `refine()` が、host snapshot 経路と全 15 buffer bit-exact に一致することPASS
`refine_gpu_snapshot_wrong_iter_falls_back_to_host_snapshot` (unit)`gpu_snapshot_ready_for` の iteration が呼び出し時の `it` と食い違うとき、stale な blit 値ではなく live buffer (blit 後に意図的に変更した値) を読むこと — フォールバックが実際に機能する証拠PASS
`refine_grows_capacity_on_overflow_gpu_snapshot` (unit、advisor 指摘で追加)capacity overflow (`param.resize_capacity`/`adam_state.resize_capacity` が live buffer を新しい Metal buffer に差し替える) を GPU snapshot 経由で発生させ、mirror buffer への `Cow::Borrowed` スライスが resize を跨いでも有効であり続け、host snapshot 経路と bit-exact に一致することPASS
`pipelining_depth2_refine_gpu_snapshot_matches_off_across_refine_ticks` (integration)advisor 指摘の trap 1 (opacity_decay_rate > 0 が実際に発火するシナリオ) を通しつつ Off と Depth2+flag が一致することPASS
`pipelining_depth2_refine_gpu_snapshot_falls_back_with_grad_rmw` (integration)advisor 指摘の trap 2 (`scale_reg.weight > 0` で grad RMW が有効、Adam が CB 外で走る) で `refine_gpu_snapshot=true` でも安全にフォールバックし Off と一致することPASS
`cargo test --workspace --release` (全 crate)既存 test (refine 22 本、pipelining/train_loop 系含む) の regression 無し全 pass (0 failed)、新規 5 test 込み
1000 iter smoke (`2026-08-30-refine-nondrain-smoke-1000-gpu-snapshot.toml`、修正版で再実行)実バイナリで crash なし、`[backend] BackendConfig {...}` line で `refine_gpu_snapshot: true` が実際に解決、`ts_refine_snapshot_blit` の呼び出し回数が refine tick 数と一致、`ts_refine` per-call が off と同水準に復帰 (純増バグが直った証拠)PASS — `ts_refine_snapshot_blit` 6 回 (= `ts_refine`/`ts_opacity_decay` と同数、tick 500/600/700/800/900/1000)、49.8µs/call、`sync_iters=6/1000`、`ts_refine` 370.5µs/call (off 365.6µs/call とほぼ同水準)
smoke の on/off 比較は splat 数・PSNR が一致しない — これは想定通り (バグではない)

1000 iter smoke を on (`refine_gpu_snapshot=true`) / off で走らせたところ final splats (6816 vs 6766) / PSNR (19.551 vs 19.611 dB) が一致しなかった。これは実装のバグではなく、フル Lego シーン (複数カメラ view) の GPU atomic float 累積順が run ごとに非決定的という既知の特性 (`per_kernel_single_texel_run_to_run_deterministic_1iter` の doc 参照 — 決定性が保証されるのは single-texel fixture の 1 iter のみ) — 同じ off config を 2 回走らせただけでも final splats が 6766→6948、PSNR が 19.611→19.548 と同程度ばらつくことを確認して裏付けた。bit-exact 性は unit test (決定的な single-texel 相当の fixture、固定 seed) で確認済みであり、smoke の役割は crash-freedom と計装確認に限定される。

5. gate config (12.5k n=3、実施済み — §7 参照)

config
`splat/configs/2026-08-30-rnd-12500-{off,on}-r{1,2,3}.toml`、base は `2026-08-30-final-12500-r{1,2,3}.toml` ([backend] `refine_gpu_snapshot` の 1 field のみ diff、他は現行採用スタック — M4 simd_ballot + A7 refine_scratch_persist/refine_direct_write + S4b ssim_reduce=gpu (default) — と bit-exact。base config は `ssim_reduce` を明示していないため S4b flip 後の default である `Gpu` がそのまま両 arm に効くことを 6 run の `[backend] BackendConfig {...}` ログで確認済み)
PSNR guard
task 指定の相対 guard (on mean ≥ off mean - 0.05dB) で判定 (pass、§7 参照)。参考: `p3-final-stack` n=3 mean 36.219 dB を中心にした絶対 guard (±0.05dB → [36.169, 36.269]) で読むと **off (36.138) は帯外・on (36.194) は帯内**になるが、両 arm とも同程度に低く flag 由来の系統差ではなく `p3-final-stack` 基準からの baseline drift (session 間の atomic 非決定性) と読むのが妥当 — この読み替えは棄却/採用の結論を動かさない
採用閾値
A7/S4b と同じ `max(2σ_off, 1%)` 規約 (σ_off は off arm 3 run から算出)
期待 Δ (gate 前の宣言)
advisor 試算: refine tick drain bucket (2.4〜3.1%) のうち snapshot read が占める割合を概算半分とすると **-1.2〜-1.9%**。pessimistic ケース (閾値 1.612% 相当) では不通過もあり得ることを明記しておく — A7/S4 の教訓 (期待帯を事前登録し、null 結果も解釈可能にする) に従う
実施状況
**2026-08-30、main HEAD `2cb044bde530a425b89c3e90d2532d0e37673021` (binary sha256 `66c07056de004237d3c3c01d2aaa6d2cbdf019566dc1030af1339b6b4b240cdb`) 上で実施済み** — `splat/scripts/gate-ab-n.sh configs/2026-08-30-rnd-12500-off configs/2026-08-30-rnd-12500-on rnd-12500 3` を nohup+disown で detach し、DONE sentinel を待って完了。結果は §7 参照、**棄却確定**

6. 規約 / 次段への含意

  • **refine tick drain は構造的に必須** — `B_it` の Adam encoder が param/adam 全 15 buffer を書くため、`wait(B_it)` を depth3 化なしに除去することはできない。今後 refine tick drain の追加最適化を検討する際は、この制約を前提にすること (staleness を受け入れるか、depth3 化するかの二択になる)
  • **「GPU shadow に重ねる」系の最適化 (S4 host_schedule と同型) は refine には向かない** — refine の snapshot read/write は `B_it` 自身が触る buffer と同一のため、S4 (`p3-s4-host-shadow`、既に棄却確定) と同じ race 構造を持つ。今回 advisor が提案した「host 作業を GPU 側に移す」(重ねるのではない) アプローチ自体は設計として妥当だったが、**実測 Δ は -0.24% でノイズ水準に留まり、棄却**
  • **次段のアクション**: `refine_gpu_snapshot` は default false のまま維持、`p3-c2-bubble-attribution` §6 の施策 #4 を「実装済み・12.5k gate で棄却 (効果量が harness 分解能未満)」に更新すること。読みは §7 の機構解釈 (snapshot read 自体は refine tick drain bucket 内で相対的に小さく、host memcpy の絶対コストが元々小さかった可能性) を参照
  • gate 実施・棄却確定により本 finding は `status = "stable"` に更新した。code 変更は行っていない (`refine_gpu_snapshot` の default / validate は変更なし)

7. 12.5k n=3 gate (2026-08-30、rnd-12500 series) — 棄却根拠

前提
AC 接続 (`pmset -g batt` で `AC Power` 確認)、GPU 権は main から付与済み・他 agent の GPU 使用なし。`cargo build --release` (HEAD `2cb044bde530a425b89c3e90d2532d0e37673021`、binary sha256 `66c07056de004237d3c3c01d2aaa6d2cbdf019566dc1030af1339b6b4b240cdb`)。起動直前に `pgrep -fl "target/release/splat"` が空であることを確認、config 6 本は `splat check` で ok を確認済み
driver
`splat/scripts/gate-ab-n.sh configs/2026-08-30-rnd-12500-off configs/2026-08-30-rnd-12500-on rnd-12500 3` (nohup+disown で detach、`rnd-12500-gate.DONE` sentinel で完了検出)。6 run とも rc=0、series 前後で binary sha256 / git rev 不変を driver ログで確認
flag 実測確認 (6/6)
全 6 run の `[backend] BackendConfig { … }` で他の全 flag (backward=Simd, loss_path=Gpu, binning=Resident, pipelining=Depth2, host_schedule=ForwardShadow, ssim_reduce=Gpu [S4b default 追従], refine_scratch_persist=true, refine_direct_write=true 等) が off/on 間で完全同一であることを確認、差分は `refine_gpu_snapshot: false` (off 3/3) / `refine_gpu_snapshot: true` (on 3/3) のみ
汚染 / retry 判定
事前登録条件 (`p3-tier1-bit-exact.toml` §5、(i) load 証拠 AND (ii) 自 arm内 3σ超 の **AND**) を適用。off-r1 の run 窓内 load1 (mean 2.41, max 3.49) は他の off run (0.80/0.87) よりやや高いが、off-r1 の off arm内偏差は 1.07σ (<3σ) で条件 (ii) 不成立、よって retry 対象なし。感度チェックとして off-r1 を除外した場合の Δ も算出したところ **-0.060%** (さらに閾値から遠ざかる) で、いずれの扱いでも結論 (棄却) は変わらない
実行順armruntrain elapsed (s)PSNR (dB)final splatsrun 窓内 load1 mean/max備考
1off (A)rnd-12500-off-r1123.0836.113408,7982.41 / 3.49採用
2on (B)rnd-12500-on-r1122.1136.179405,2421.10 / 1.59採用
3off (A)rnd-12500-off-r2122.2836.187405,9760.80 / 1.04採用
4on (B)rnd-12500-on-r2122.1836.203406,6881.38 / 2.05採用
5off (A)rnd-12500-off-r3122.5836.115407,5620.87 / 1.17採用
6on (B)rnd-12500-on-r3122.7836.199409,9490.96 / 1.15採用
**mean (n=3)****off**—**122.6467** (σ 0.330%)**36.1383**407,445——
**mean (n=3)****on**—**122.3567** (σ 0.301%)**36.1937**407,293——
**Δ**——**-0.2365%****+0.0553**-0.037%—閾値 max(2σ_off,1%)=1.000% を大きく下回り不通過
判定 — wallclock 閾値未達 (実測は閾値の 24%)、pair 一貫性も 2/3 に留まり棄却

wallclock: Δ **-0.2365%** は有意閾値 max(2σ_off,1%)=1.000% の **24%** にしか届かず、明確に不通過。pair は r1 (-0.788%, on 優位) / r2 (-0.082%, on 優位だがごく小) / r3 (+0.163%, on が逆に遅い) で 3 組中 2 組のみ on 優位、系統的な一貫性はない。PSNR: Δ **+0.0553dB** で guard (-0.05dB 以内) は pass するが、`refine_gpu_snapshot` は snapshot read の読み出し元を変えるだけで gradient には一切触れない bit-exact 設計のため、この値は改善の証拠ではなく §2b (`p3-s4b-ssim-reduce`) と同型の背景 atomic noise と解釈すべきもの。wallclock が閾値に遠く及ばない以上、PSNR guard の pass に関わらず棄却。

8. Δ 実測 (-0.24%) が advisor 試算 (-1.2〜-1.9%) を大きく下回ったことの機構解釈

snapshot read の host memcpy コスト自体が想定より小さかった可能性

advisor 試算は「refine tick drain bucket (2.4〜3.1%) のうち snapshot read が占める割合を概算半分」という比例配分だったが、`p3-c2-bubble-attribution` のブラケットは drain bucket 全体 (snapshot read + action 決定 + compact write + zero_tail) の合算であり、snapshot read 単体の絶対コストは当時計装 (`ts_refine` のサブラベル分割) されていなかった (§4 未解決点 (2) が明記していた計装ギャップ)。**§9 の instrumentation-only 実測 (n=1、12.5k 規模) がこれを直接測定した**: 12500 iter 中の refine tick は `start_iter=500, every=100` により **121 回** (500,600,…,12500)、`ts_refine` per-call が off 20.744ms → on 16.032ms (削減 4.712ms/tick) なので、121 tick × 4.712ms/tick = **0.570s** — `ts_refine` の合計値 (2.510s − 1.940s = 0.570s) と独立に一致する。~123s の wall に対し **0.46%** であり、これが本施策の実測上限 (ceiling)。12.5k n=3 gate の Δ -0.2365% はこの 0.46% ceiling とノイズ帯の範囲で整合する — 逆算や頻度論は不要で、**advisor 試算の「drain bucket の約半分」という配分そのものが過大評価だった**、という単純な説明で足りる。

9. instrumentation-only 検証 (n=1、`profile=true`) — blit が 12.5k で実際に発火したことの直接証拠

advisor 指摘への対応 — BackendConfig ログだけでは blit 発火の証拠にならない

§7 の 6 run はいずれも `profile = false` (他の全 gate と同じ設定) で `ts_*` 計装ログが出力されないため、`[backend] BackendConfig { refine_gpu_snapshot: true }` は flag が parse されたことしか示さず、`refine_tick_wants_gpu_snapshot` が実際に true を返し `refine()` が `Cow::Borrowed` 分岐 (GPU blit 経路) を取ったことの証拠にはならない。§3 コールアウトが記録した過去の実装ミス (host memcpy が消えていないまま blit だけ追加していた) は `ts_refine` の on/off 比較でのみ検出できた前例であり、本 gate も同じ確認を経ないと「効果なし」と「計測していない」が区別できない。この懸念に対応するため、gate 6 run とは別に `profile = true` の n=1 pair (`2026-08-30-rnd-12500-prof-{off,on}.toml`、name/run_dir を gate run と衝突しないよう変更、統計には含めない instrumentation-only run) を追加で実行した。

計装off (rnd-12500-prof-off)on (rnd-12500-prof-on)備考
`ts_refine_snapshot_blit` 発火回数(該当なし)**121 / 121**`start_iter=500, every=100, stop_iter=12500` → refine tick 数 (500,600,…,12500) と完全一致。blit が全 refine tick で実際に発火したことの直接証拠
`ts_refine` (per-call)20.744ms/call (合計 2.510s, 121 call)16.032ms/call (合計 1.940s, 121 call)**-22.7%/call** — snapshot read の host memcpy 除去による `refine()` 自体の短縮は実際に起きている
`ts_refine_snapshot_blit` (per-call)—12.009µs/call (合計 1.453ms)blit 自体の GPU encode コストは極小 (1000 iter smoke の 49.8µs/call より小さい — 407k splats 規模でも支配的コストにならない)
`ts_chain_commit_wait` (`B_it` 完了待ち、drain 本体)82.855s (57.1%)87.983s (57.2%)**+5.128s** だが下記コールアウト参照 — blit のコストではなく run 単体のノイズと判断
`ts_forward` (参考、flag が一切触れないはずの経路)26.300s28.309s**+2.009s** — `refine_gpu_snapshot` は forward path に一切配線されていないにもかかわらず増加しており、on-arm instrumentation run 全体が (profiling オーバーヘッドか偶発的な system jitter で) 一般的に遅かったことを示す独立した証拠
`training done in`123.18s129.68sn=1・instrumentation-only。on 側は gate の on 3 run (122.11-122.78s) から明確に外れており、下記コールアウトの通り run 単体のノイズと判断、統計には含めない
機構解釈 — 実測上限は `ts_refine` の -0.570s (≈0.46%)、advisor 試算 (半分配分) が過大評価だった

`ts_chain_commit_wait` の +5.128s は blit の GPU コストではない、と判断する根拠は 3 点: (1) 大きさが合わない — 121 回の blit に均せば 42ms/blit 相当になるが、同じ log の `ts_refine_snapshot_blit` encode コストは 12.009µs/call、~250MB の unified memory `copy_from_buffer` の実行コストも概算 1ms 未満のオーダーで、42ms/tick は 2 桁近く合わない。(2) 同じ log 内に独立した反証がある — `refine_gpu_snapshot` は一切配線されていない `ts_forward` (count 12500) も +2.009s 増えており、`ts_chain_commit_wait` (同じく count 12500、wall の 57%) は system 全体が遅いときにそのまま増える場所である。(3) `training done in` 自体が on 側で gate の on 3 run から明確に外れている (129.68s vs 122.11-122.78s) — これは on-arm instrumentation run 単体が (profiling オーバーヘッドか偶発的な負荷で) 全体的に遅かったことの直接証拠であり、その run のバケット差分を『blit の構造的コスト』として解釈するのは誤り。したがって本施策の実測上限は `ts_refine` の削減分のみ: -0.570s ≈ wall (123s) の 0.46%。これは 12.5k n=3 gate の Δ -0.2365% (ノイズ帯) とほぼ整合する — §8 で「host memcpy コスト自体が想定より小さかった」と推測した読みが正しく、advisor 試算 (-1.2〜-1.9%、drain bucket の約半分という配分) の方が過大評価だった、という単純な説明で足りる。blit 自体の追加コストが `wait(B_it)` を有意に伸ばしているという証拠は無い (§8 のまま維持)。

未解決点

(1) ~~snapshot read が refine tick drain bucket に占める正確な割合は計装していない~~ → **§9 で解消**: 12.5k 規模 instrumentation-only run (n=1) で `ts_refine_snapshot_blit` 121/121 発火・`ts_refine` per-call -22.7% (20.744ms→16.032ms、合計 -0.570s ≈ wall の 0.46%) を実測。これが本施策の実測上限であり advisor 試算 (-1.2〜-1.9%) は過大評価だったと判明した (`ts_chain_commit_wait` の +5.128s は同 run の `ts_forward` +2.009s [flag が触れない経路] と合わせて run 単体のノイズと判定、§9 参照)。`ts_refine` のさらに細かいサブラベル分割 (`snapshot`/`action`/`compact`/`zero_tail`) は依然未実施。(2) MCMC 有効時 (grad RMW / SGLD noise / relocation) の safety fallback は integration test 1 本 (`scale_reg` 経由) のみで確認しており、`mean_noise_weight`/relocation 経由の fallback は明示的な test を追加していない。(3) 4 flag 一括ではなく単独 flag の gate だったため他施策との相互作用は未検証だが、効果量が既にノイズ水準のため優先度は低い。(4) `config.rs:511` (`refine_gpu_snapshot` field doc)・`config.rs:558` (`BackendConfig::default()` のコメント)・`refine.rs:64` (module doc) にはいずれも「default = false (gate 待ち)」の文言が残っている — 棄却が確定した以上、`p3-tier1-bit-exact.toml` §6 が指摘した規約 (「verdict が変わったら doc を同じ commit で更新する」) に照らすと doc-only で「棄却確定 (2026-08-30 gate、`p3-refine-nondrain` 参照)」に追随させるべきだが、本 task の指示 (「棄却なら code 不変」) に従い `.rs` は一切変更していない。`p3-tier1-bit-exact` 系の未処理の同種 doc (`ssim_clear`/`raster_grad_clear`/`backward_uniform` の「gate 待ち」文言、`config.rs:406/412/420/428/433`) も含め、main の判断で doc-only commit として一括処理することを推奨する。

02

関連する run

この finding が観測された / 言及している実験 run。

03

関連 finding

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