松田研究室
FindingID: p3-kernel-share-post-s3
Status: stable / task 2026-09-seminar05-milestones M1 (C1 per-kernel 再計測 + C2 残差 bubble 帰属)
← Findings Index
2026-09 ゼミ milestones · M1/M7 · C1 per-kernel 再計測 (v1+v2) + C2 残差帰属

M1/v2 — post-S2/S3 per-kernel share 再計測: backward_raster 33.9%→40.1% (M4/A7 後)、radix 26.4%→12.0% (M4 の絶対削減を反映) → M3 GO 判定は不変。C2 残差 4.54s は driver latency / CB gap に分解不能 (計装欠如、file:line 提案のみ)

C1: `submission = per_kernel` / `forward_submission = per_stage` / `pipelining = off` (radix_scan = gpu_hierarchical / binning = resident は現 default のまま) で 12.5k Lego を 1 run。leaf label の gpu_ms 合計 141.716s は `gpu_busy_stats()` の union 141.716s と<strong>誤差 0.3ms で一致</strong> (per_kernel/per_stage は commit+wait 境界が kernel 境界と一致するため、chained 由来の二重計上が原理的に起きない)。<strong>backward_raster (+project_back、bit 同値のため 1 行に統合) 33.86%</strong> > radix 26.40% > adam 12.68% > rasterize 11.64% > ssim(+l1_combine) 11.80% > project(+emit+extract_off) 3.61%、refine/zero_grads は host 実コストがありながら **GPU 側 gpu_ms が 0** に落ちる (非ブロッキング harvest の帰属漏れ、既知の限界)。<strong>M3 の GO 閾値 (≥25%) を 33.86% で超過 → per-splat backward 実装へ進む根拠が立った</strong>。C2 は M0 (並行 doc 修正 c8864c8) が発見した会計訂正後の depth2 残差 sync-tax 内訳 <strong>refine drain 6.26s (44%) / encode(B) 3.41s (24%) / 残差 4.54s (32%)</strong> を出発点に、残差 4.54s のさらなる driver-latency / CB間-gap 分解を試みたが、<strong>`gputime.rs` は `kernelStartTime`/`kernelEndTime` を bind しているだけで収集していない</strong> (Collector は `GPUStartTime`/`GPUEndTime` のペアのみ保持) ため、既存計装では分解不能と判明。file:line 付きで追加案を残す (実装はしていない)。

mixedrank: highm3-go-backward-raster-33.9pct / c2-instrumentation-gap-proposal-only第 1 軸第 3 軸m1m7seminar05-milestoneskernel-shareper-kernel-gpu-msbackward-rasterm3-go-gateresidual-bubblegputimelegopost-s2-s3v2post-m4-a7
Δ PSNR
n=1: 36.087 dB (self-eval val mean, 100 views)。p2-s3 の off/depth2 n=3 mean (36.185 / 36.202 dB) の guard 内。本 run は submission=per_kernel/forward_submission=per_stage という別 regime なので PSNR の直接比較は非対象 (kernel は bit-exact のため理論上は同水準になるはず、というだけの参考値)
Δ Wallclock
train elapsed 205.78s (n=1)。**production baseline (depth2 default, 151.70s) や S3 off baseline (185.21s) と直接比較しない** — per_kernel/per_stage submission は CB 数が既定の 15〜27/iter → 15 CB/iter 超 (最終 15,001 cb from iter1 window 相当) に増え、busy% が 68.9% まで下がる**測定専用 regime**であり、本 run の wallclock 自体に意味はない。意味があるのは kernel 単体の gpu_ms とその share のみ
Impact
M3 (per-splat backward, brush PR #401 型) の GO/NO-GO 閾値「backward_raster share ≥ 25%」を判定した。post-S2/S3 kernel (hierarchical radix scan / resident binning) を submission=per_kernel/forward_submission=per_stage/pipelining=off (per-kernel breakdown が測れる唯一の regime) で 1 run 実測した結果 backward_raster (+project_back 統合) share 33.86% で 閾値を大きく超過 → M3 は GO。share × 0.25 (brush の -25% 主張をそのまま適用したナイーブな期待値) = 8.47%。ただしこれは本 run の GPU busy (68.9%、production の 91.3% とは別 regime) に対する share であり、production (depth2 default) の wallclock に対する期待 Δ は 33.86% × 91.3% × 0.25 ≈ 7.7% とみるほうが妥当 (kernel 自体の gpu_ms は submission wrapping にほぼ不変という counter-session の事後検証はあるが、n=1 点推定であることと合わせて上限値として扱う)。pre-S2/S3 (counter-session) との比較では share 構成はほぼ不変 (backward_raster 33.6%→33.86%、radix 26.3%→26.40%、rasterize 11.9%→11.64%、adam 13.8%→12.68%、ssim 10.0%→10.95%) — S2 のhierarchical scan は「radix の CB/wait 数」を畳んだだけで kernel 自体の GPU 時間配分は変えていない、という roadmap の当初の想定を実測で裏づけた。C2 は M0 の並行会計訂正 (encode(B) 4.66→3.41s、残差 3.28→4.54s、nested label 二重計上の是正) を踏まえ、残差 4.54s (depth2 sync-tax の 32%) を driver 投入 latency と CB 間 gap に分解しようとしたが、既存計装 (`gputime.rs`) は `kernelStartTime`/`kernelEndTime` を objc bind しているのみで収集しておらず、per-CB の生 interval 列も外部に露出していない ため分解不能と判明した。file:line 付きの追加案を §C2 に残す (実装はしていない、GPU run はこれ以上起動していない)。
01

本文

p2-counter-session の per-kernel share 表 (backward_raster 33.6% > radix 26.3% > adam 13.8% > rasterize 11.9% > ssim 10.0%) は commit 56519c4 — S2 (forward 1 CB 化 + hierarchical GPU scan) の実装 (414c633) より後、しかし default 採用 (3027e33) より前 — に撮られたデータで、config が `[backend] radix_scan` を明示していなかったため 当時のデフォルト (旧 flat scan) で計測されていた。post-S2/S3 (hierarchical scan + resident binning が defaultの現在) で撮り直すとどう動くかが M1 C1 の主題であり、その結果が M3 (per-splat backward) の着手可否を決める。

0. 計測 regime の選択 — chained では per-kernel の GPU 時間が分離できない (逸脱の報告)

milestone 本文の指示
`2026-08-16-s3-12500-off-prof.toml` を複製、`pipelining = "off"` のまま run 1 本
そのまま使わなかった理由
同 config は `submission = "chained"` / `forward_submission = "chained"` (現 default) を継承する。`timing.rs` のモジュール doc: 「Chained mode ではこれらの label は<strong>encode コストのみ</strong>を計り、実 GPU 時間は統合 CB の `ts_chain_commit_wait` に集約される (= per-CB timing に粒度が落ちる)。bench harness の per-kernel breakdown 表は `submission = "per_kernel"` で取得する運用とする」。実際 S2 の chained forward (`trainer.rs::encode_forward_chained`) は project→bin_chain(emit+radix+extract)→rasterize を 1 CB に纏め、<strong>`ts_fwd_radix_sort` という個別 label 自体が経路から存在しない</strong> (`ts_fwd_bin_chain` に統合済み)
採った regime
config を `submission = "per_kernel"` / `forward_submission = "per_stage"` に明示上書き (他は off-prof.toml を継承: `radix_scan = "gpu_hierarchical"` / `binning = "resident"` / `backward = "simd"` / `raster_backward_range = "last_contributor"` / `pipelining = "off"`)。`validate()` は `forward_submission = chained` or `pipelining = depth2` のときだけ hard error を出すので、この組み合わせは合法 (`splat check` で確認済み)
「p2-counter-session と同じ読み方」の担保
counter-session の per-kernel 表も同じ `submission = per_kernel` で撮られている (`SubmissionKind::PerKernel` の doc comment: 「per-kernel host timing … は引き続きこの mode で取ること」)。radix_scan/binning は現 default (post-S2) のまま据え置いたので、「S2/S3 の kernel 実装を post-S2/S3 の submission 粒度で見る」という当初の狙いは達成できている
この regime の busy%/wallclock は「busy 予算の再構成には使えない」

本 run の GPU busy は 68.9% (production depth2 default の 91.3% とは別物)、wallclock も 205.78s (production の 151.70s とは別物)。submission=per_kernel/forward_submission=per_stage/pipelining=off という 3 重に非 production な regimeであり、以下の share 表と kernel 単体の gpu_ms/iter だけが意味を持つ。counter-session はこの点を「chained 12.498 vs per_kernel 12.874 ms/iter busy (差 3%)」で検証しており、kernel 単体の gpu_ms は submission wrapping にほぼ不変という事後検証があるため、share の値自体は production の kernel 内訳の近似として読んでよい。

1. C1 — per-kernel gpu_ms 表 (leaf のみ、二重計上を除去)

kernel (leaf label)gpu_ms 総計 (12,500 iter)gpu_ms/itershare
**`ts_backward_raster` (+ `ts_project_back`、bit 同値のため統合)**47,991.156**3.839****33.86%**
`ts_fwd_radix_sort`37,416.5572.99326.40%
`ts_adam`17,970.4711.43812.68%
`ts_fwd_rasterize`16,494.4601.32011.64%
`ts_ssim_fwd_grad`15,516.9791.24110.95%
`ts_fwd_project`3,500.6820.2802.47%
`ts_fwd_emit_pairs`1,473.6540.1181.04%
`ts_l1_combine_ssim`1,205.5940.0960.85%
`ts_fwd_extract_off`146.1440.0120.10%
`ts_refine` (121 tick、host 5.047s)0.0000.0000.00%
`ts_refine_accumulate` (host 3.969s)0.0000.0000.00%
`ts_opacity_decay` (121 tick、host 0.127s)0.0000.0000.00%
`ts_zero_grads` (host 0.140s)0.0000.0000.00%
**leaf 合計****141,715.697****11.337****100%**
(参照) `gpu_busy_stats()` union141,716.0 (141.716s)11.337—
leaf 合計が union と 0.3ms 差で一致 — chained の二重計上が消えた直接証拠

counter-session の per_kernel 表は leaf 合計が union を +5.4% 上回っていた (非ブロッキング harvest が wait なし commit の CB を後続 label へ重複帰属)。本 run は 141.715697s (leaf 合計) vs 141.716s (union) で 差 0.3ms (0.0002%) — 実質完全一致。`per_stage` forward は各 stage が commit+wait 境界を持つため (`trainer.rs` の per-stage forward path、S2 以前と同じ構造)、非ブロッキング harvest の帰属漏れが起きる余地がほぼ無い。「leaf 合計 ≠ union」問題は submission=per_kernel/forward_submission=per_stage では原理的に解消される — 逆に言えば chained (production) でこの表を撮ろうとすると必ず起きる帰属誤差だということでもある。

`ts_project_back` の二重計上 — counter-session と同一現象が今回も再現

`ts_project_back` (host 9.737s、gpu_ms **47,991.156** — `ts_backward_raster` と bit 単位で同値) は counter-session でも観測された「同一 CB 集合を指す 2 つの label」現象がそのまま再現している。上表では 1 行に統合し、host 列は参考として両者の合計 (54.924+9.737=64.661s) を別途 §2 で扱う。

refine / zero_grads の gpu_ms が 0 に落ちる理由 — host コストは実在する

`ts_refine` (121 tick、host 合計 5.047s = tick あたり 41.7ms) と `ts_zero_grads` (12,500 回、host 合計 0.140s) はいずれも gpu_ms が 0.000 だが、host 時間は明確に非ゼロ (特に refine は 1 tick 41.7ms と重い)。これは 非ブロッキング harvest の帰属先が「その label の closure 内で完了した CB」に限られるため — refine tick 中に commit された GPU 側の実作業 (opacity decay 等の compute dispatch) が、tick の closure が終わる時点でまだ完了しておらず、次の label (`ts_forward` 等) の harvest 時に回収されてそちらに計上される典型例 (p2-s3 finding が `ts_refine_accumulate` の gpu 0→1,871.9 で報告したのと同じ機構)。つまり本表の refine 行 (0.00%) は「refine の GPU コストが無い」ことを意味しない — 実コストは backward_raster 等の行に混入して見えなくなっている。この limitation は counter-session の callout (「label は入れ子」「Σ label.gpu ≠ 全体 GPU busy」) の系であり、新規発見ではない。

2. M3 GO/NO-GO 判定 — backward_raster share 33.86% ≥ 25% 閾値 → GO

指標値根拠
backward_raster (+project_back) share**33.86%**上表 (leaf 合計に対する比)
M3 GO 閾値 (milestones.md)≥ 25%docs/strategy/2026-09-seminar05-milestones.md M3 節
判定**GO**33.86% は閾値を 8.9pt 超過
ナイーブ期待 Δ = share × 0.25**8.47%**brush PR #401 の -25% 主張をそのまま backward_raster share に乗せた素朴な積 (milestone 本文が要求する形式)
busy%-調整後の期待 wallclock Δ**≈7.7%**33.86% × 91.3% (production depth2 の busy%) × 0.25。本 run の share は GPU busy に対する比率であり、production の wallclock に対する比率に変換するには busy% を掛ける必要がある (本 run 自身の busy% 68.9% は非 production regime なので使わない)
8.47% と 7.7% の差 — 「share」がどの分母に対する比率かで変わる

milestone 本文は「backward_raster の share (%) と、それ × 0.25 の期待 wallclock Δ」を 1 行で求めており、素朴に計算すれば 33.86% × 0.25 = 8.47% になる。ただしこれは「GPU busy 時間のうち backward_raster が占める割合」であって「wallclock のうち backward_raster が占める割合」ではない — production (depth2 default) は busy 91.3% / sync-tax 8.7% なので、GPU busy の delta がそのまま wallclock の delta になるとしても、wallclock に対する backward_raster の実効 share は 33.86% × 91.3% ≈ 30.9% であり、期待 Δ は 30.9% × 0.25 ≈ 7.7% のほうが production の数字としては妥当。両方とも n=1 点推定・brush の -25% を無検証で借用した上限値である点は変わらない — M3 実装後の実測 gate でしか確定しない。

3. pre-S2/S3 (counter-session) との比較 — share 構成はほぼ不変

kernelcounter-session (pre-S2/S3, 旧 flat scan)本 finding (post-S2/S3, hierarchical scan)Δ (pt)
backward_raster (+project_back)33.6%33.86%+0.26
radix26.3%26.40%+0.10
adam13.8%12.68%-1.12
rasterize11.9%11.64%-0.26
ssim (`ts_ssim_fwd_grad`)10.0%10.95%+0.95
project + emit_pairs + extract_off2.7+1.0+0.1=3.8%2.47+1.04+0.10=3.61%-0.19
S2 の hierarchical scan は「radix の CB/wait 数」を畳んだだけ — kernel 時間配分は変えていない

radix の share は 26.3% → 26.40% と実質不変。roadmap v2 が S2 gate 時に明記していた「S2 (hierarchical scan) が狙うのは radix の CB/wait 数 であって kernel 時間ではない」という注記どおりの結果であり、実測でこの想定を裏づけた。adam が -1.12pt 下がっているのは測定誤差 (n=1) の範囲内とみるのが妥当で、有意な変化と主張するには n=3 が要る。全体として「backward_raster が最大単独ブロックで 1/3 強を占める」という律速構造は S2/S3 を経ても変わっていない — これが M3 (per-splat backward) を優先する直接的な根拠になる。

4. C2 — 残差 bubble の帰属 (会計訂正の反映 + 計装限界の特定)

tasklist C2 が指す「4.54s」の出自 (判明)
本 M1 着手前は repo 内のどこにも 4.54s という値が見当たらず (p2-s3 finding の「残差」行は 3.28s/23%)、milestone 起草時点の記憶違いかと疑っていた。<strong>M1 実施中に並行して走っていた M0 (docs 修正、commit c8864c8) が roadmap v2 の encode(B) 会計を訂正</strong>しており (`ts_backward_raster` の子 label `ts_raster_bwd_alloc`/`_upload` を親と二重計上していた、D-4 の是正)、<strong>encode(B) 4.66→3.41s / 残差 3.28→4.54s</strong> という修正後の値が確定した。tasklist の「4.54s」はこの<strong>訂正後の値を先取りした表記</strong>だったと分かる (訂正コミットは M1 と同日、M1 の作業中に main へ入った)
訂正後の depth2 残 sync-tax 内訳 (合計 14.206s、p2-s3 §5 + roadmap v2 訂正)
refine tick drain 6.26s (**44%**、`511f916` 適用済みで既にこれ以上は同施策で削れない) / encode(B) 3.41s (**24%**、nested label 二重計上を除いた正しい値) / **残差 4.54s (32%)**
C2 の狙い
残差 4.54s を「driver 投入 latency」(CPU が commit() してから GPU が実際に実行を開始するまでの遅延) と「CB 間 gap」(GPU タイムライン上で先行 CB の GPUEndTime と後続 CB の GPUStartTime の間に生じる純粋な空白) に分解する
**結論: 既存計装では分解不能**
`splat/crates/splat-metal/src/gputime.rs` は `CommandBufferGpuTime` trait で `kernel_start_time()` / `kernel_end_time()` (`kernelStartTime`/`kernelEndTime`、objc `msg_send!` で bind 済み、L67-79) を提供しているが、`collect()` (L216-244) は `cb.gpu_start_time()` / `cb.gpu_end_time()` **だけ**を読んで `Collector.intervals: Vec<(f64, f64)>` (L153-159) に積む — kernelStart/EndTime は**どこにも収集されていない**。さらに `Collector.intervals` 自体が private で、`union_since()` (L265-273) / `stats()` (L275-296) はいずれも union / 集計値だけを返し、<strong>個々の CB の生 (start, end) ペアを外部に露出する API が存在しない</strong> — CB 間 gap (連続する 2 CB の interval の差) すら現状は計算できない
C2 提案 (実装はしていない) — file:line 付き

① splat/crates/splat-metal/src/gputime.rs:153-159 — Collector.intervals を Vec<(f64,f64)> (gpu start/end) から Vec<(f64,f64,f64,f64)> (kernel_start, kernel_end, gpu_start, gpu_end) に拡張。② 同ファイル collect() の let (s, e) = (cb.gpu_start_time(), cb.gpu_end_time()); (L237) の直前後に let (ks, ke) = (cb.kernel_start_time(), cb.kernel_end_time()); を足し、4 tuple を push。③ union_since() (L265) と同じ形の pub fn intervals_since(mark: usize) -> Vec<(f64,f64,f64,f64)> を追加してソート済み生 interval を label 単位で取り出せるようにする。④ splat-train-v1/src/timing.rs に、④-a 連続 GPU interval 間の gap (gpu_start[i+1] - gpu_end[i]、= CB 間 gap) と ④-b 各 CB の gpu_start - kernel_end (= driver 投入 latency の近似、kernelEndTime は driver がコマンドの GPU 側実行可能形式への変換を終えた時刻) を集計する gpu_gap_report() を追加。⑤ 呼び出し箇所は splat-train-v1/src/train_loop.rs:802-803 (既存の gpu_drain() → print_summary() の直後、最終 blocking drain 済みで全 CB が回収されている地点)。この 5 箇所を実装すれば、次回 depth2-prof run 1 本で残差 4.54s の分解が測れる見込み (追加コストはほぼゼロ — 既存 register()/collect() 経路に相乗りするだけ)。

5. 規約 / 次段への含意

  • **M3 (per-splat backward) は GO** — backward_raster share 33.86% ≥ 25% 閾値、期待 wallclock Δ は 7.7〜8.5% (n=1・brush 借用値の上限見積り、M3 実装後の gate で確定させる)
  • **M1 C1 の per-kernel breakdown は submission=per_kernel/forward_submission=per_stage でしか測れない** — chained (production) では individual kernel の GPU 時間が構造的に分離不能。今後この breakdown を再測るときは同じ規約を使うこと (本 finding に明記)
  • **C2 (残差 driver-latency/CB-gap 分解) は計装追加が前提** — 上記 file:line 5 箇所を実装しない限り、Xcode capture を使わずに分解する経路は無い。tasklist が「Xcode 不要」と書いていたのは gputime.rs の kernelStartTime/EndTime を使えば取れるはずという想定だったが、**bind されているだけで未配線**だったことが本 M1 で判明した
  • **tasklist の「4.54s」表記の出自を確定** — M0 (並行 doc 修正) による encode(B) 4.66→3.41s の会計訂正の結果であり、旧 finding (p2-s3) の「残差 3.28s」は誤りではなく訂正前の値。roadmap v2 は既に訂正済み (commit c8864c8)、本 finding が二次ソースとして参照する

6. v2 (M4/A7 後、2026-09-seminar05-milestones M7) — 2026-08-30 追加計測

背景
本節 (v1、§0〜5) の計測後、M4 (A5 radix scatter simd_ballot、default flip、p3-radix-ballot) と A7 (refine 3-pass 削減、default flip、p3-refine-3pass) が同日中に採用・default flip された。両施策とも「post-S2/S3 の kernel share がさらにどう動くか」を検証する対象になるため、M7 (最終スタック確定) の一部として同一手法で撮り直した
config
`splat/configs/2026-08-30-c1v2-12500-off-prof.toml` (`2026-08-30-c1-12500-off-prof.toml` の複製。§0 と同じ理由で `submission = per_kernel` / `forward_submission = per_stage` / `pipelining = off` を明示、`radix_scan = gpu_hierarchical` / `binning = resident` は現 default のまま。**`radix_scatter` / `refine_scratch_persist` / `refine_direct_write` は明示せず default に任せている** — これにより M4/A7 の default flip が両方効いた状態が採れる)
flag 実測確認
run log の `[backend] BackendConfig { … }` 行で `radix_scatter: SimdBallot` / `refine_scratch_persist: true` / `refine_direct_write: true` を確認 (意図した post-M4/A7 regime で走っていることの直接証拠)
run
1 run のみ (§0 と同じ GPU ルール遵守、n=3 化はしていない — 本節の share 値も n=1 の点推定)。commit a446f2c の release binary (M4/A7/M2 Tier1/M5a 実装込みの main HEAD)。AC 電源、他 GPU 使用なし。wallclock 174.57s / self-eval PSNR 36.201 dB (**§0 の v1 run 205.78s/36.087dB や production baseline とは比較しない** — 本 regime は「busy 予算の再構成には使えない」測定専用 regime、という v1 §0 の警告がそのまま適用される)
leaf sum が union と exact 一致 — v1 と同じ検算が v2 でも通る

leaf 9 label (backward_raster / ssim_fwd_grad / adam / fwd_project / fwd_emit_pairs / fwd_radix_sort / fwd_rasterize / fwd_extract_off / l1_combine_ssim、`ts_project_back` は `ts_backward_raster` と bit 同値の重複のため v1 と同様に 1 行へ統合) の gpu_ms 合計は 114,507.738ms = 114.508s で、run 末尾の [gpu] busy 114.508s と完全一致。v2 run のバイナリには v1 に無かった新規 sub-label (ts_forward / ts_raster_bwd_gpu / ts_project_back_gpu / ts_project_soa_gpu 等) が追加されているが、これらはいずれも host 計測目的の子スコープで gpu_ms は 0 に落ちるか (v1 の refine/zero_grads と同じ「非ブロッキング harvest が親スコープに帰属する」現象)、または上位 leaf の子要素の和と完全一致する集計ラベル (ts_forward の gpu_ms 34,977.613 = fwd_project + fwd_emit_pairs + fwd_radix_sort + fwd_rasterize + fwd_extract_off の和と bit 一致) であることを確認し、leaf 集合の選定から除外した (v1 の「leaf 判定は union との一致で検算する」規約をそのまま適用)。

kernel (leaf label)v1 gpu_ms (M1、M4/A7 前)v2 gpu_ms (M4/A7 後)abs Δv1 sharev2 shareshare Δ (pt)
**backward_raster (+project_back)**47,991.15645,877.989-4.40%33.86%**40.07%**+6.21
adam17,970.47117,502.128-2.61%12.68%**15.28%**+2.60
rasterize (`ts_fwd_rasterize`)16,494.46016,185.077-1.88%11.64%**14.13%**+2.49
ssim (`ts_ssim_fwd_grad`)15,516.97915,025.375-3.17%10.95%**13.12%**+2.17
radix (`ts_fwd_radix_sort`)37,416.55713,749.681**-63.25%**26.40%**12.01%****-14.39**
project (`ts_fwd_project`)3,500.6823,391.138-3.13%2.47%2.96%+0.49
emit_pairs (`ts_fwd_emit_pairs`)1,473.6541,511.105+2.54%1.04%1.32%+0.28
`ts_l1_combine_ssim`1,205.5941,124.633-6.72%0.85%0.98%+0.13
extract_off (`ts_fwd_extract_off`)146.144140.612-3.79%0.10%0.12%+0.02
**leaf 合計****141,715.697****114,507.738****-19.20%****100%****100%**—
radix 単独 -63.25% は M4 gate の per_stage 実測 -63.7% と独立に一致 — cross-validation

本 v2 run (submission=per_kernel/forward_submission=per_stage) の radix 単独 gpu_ms 減少 -63.25% は、M4 の gate finding (p3-radix-ballot §4) が別の run で計測した「forward_submission=per_stage 単独計測での radix gpu_ms/call -63.7%」と独立した run・独立した計測断面で一致する (両者とも `radix_scatter` flag 差の A/B ではなく、v1(M4前)/v2(M4後) や M4 gate の off/on という異なる比較軸から同じ数字に収束している)。leaf 合計の総額縮小 -19.20% (141.716s → 114.508s) のほぼ全部が radix 単独の絶対削減 (37,416.6 → 13,749.7ms = -23,666.9ms) で説明でき、他 8 label の削減 (backward_raster -4.40%〜l1_combine_ssim -6.72%、絶対にして合計 ~2,470ms) は寄与として一桁小さい。A7 (refine 3-pass 削減) は host 側の allocation/copy を減らす施策であり、この per-kernel gpu_ms 表 (GPU 側 kernel 実行時間) には理論上ほぼ影響しないはずという予想と、他 8 label の変化が誤差レベル (-1.9〜+2.5%、n=1 点推定のノイズ帯) に収まっていることは整合する。

share が軒並み『上昇』しているのは分母 (radix) が縮んだため — 他 kernel が遅くなったわけではない

上表で backward_raster (33.86%→40.07%) / adam (12.68%→15.28%) / rasterize (11.64%→14.13%) / ssim (10.95%→13.12%) はshare だけを見ると全て上昇しているが、これは分母 (leaf 合計) が radix の劇的な縮小で 19.20% 小さくなったことによる相対効果であり、これら 4 kernel 自体の絶対 gpu_ms はすべて -1.88%〜-4.40% と、v1→v2 でむしろわずかに縮んでいる (n=1 点推定のノイズ帯内で、A7/その他の間接効果か測定誤差かは切り分けられない)。「share が上がった = 遅くなった」ではない — 唯一の実質的な構造変化は radix の share が 26.40% → 12.01% (-14.39pt) に落ち、backward_raster が単独最大ブロックとしての優位をさらに拡大した (33.86%→40.07%、2 位の adam 15.28% との差が 21.2pt→24.8pt に拡大) ことのみである。

6.1 v3 (2026-08-31 追加、drums 切り分け用) — 同じ Lego scene で binary vintage だけを HEAD (a556b78) に進めて再計測

目的
`p3-kernel-share-drums` §2/§4 が、Lego v2 (`a446f2c`) と drums (`3db33a8`) の間の binary vintage 差 (S4b `4405bb4` ssim_reduce=Gpu default flip、C2 計装 `9deb14a` 全 CB kernel timestamp 収集など 34 commit) が per-kernel gpu_ms を系統的に底上げしている疑いを報告した。scene を Lego に固定したまま同じ binary vintage 差だけを再現し、疑いを切り分ける
config / run
`splat/configs/2026-08-31-c1v3-12500-off-prof.toml` (本節の `2026-08-30-c1v2-12500-off-prof.toml` を run 名のみ `c1v3-12500-off-prof` に変更して複製、[backend] を含め設定は完全一致)。HEAD commit **a556b78835d8** の release binary (既存バイナリ、再ビルドなし)。1 run のみ (n=3 化はしていない)、AC 電源・他 GPU 使用なし・load1 1.90 を確認。train elapsed 174.71s (v2 174.57s とほぼ同一)、self-eval mean val PSNR 36.190dB (v2 36.201dB)、final splats 409,702 (v2 404,652)。leaf 9 label 合計 114,880.732ms が run 末尾 `[gpu] busy 114.881s` と誤差 0.268ms で一致 (v1/v2 と同じ検算)
詳細 finding
`docs/findings/p3-kernel-share-drums.toml` §5「Lego 同 binary 再計測による切り分け」に v3 の leaf 表・判定を記載。本節にはサマリのみを転記する
kernel (leaf label)v2 ms/iter (a446f2c)v3 ms/iter (a556b78, 同 Lego scene)v3/v2 比v2 sharev3 shareshare Δ (pt)
backward_raster (+project_back)3.6703.6931.006×40.07%40.18%+0.11
adam1.4001.4161.011×15.28%15.40%+0.12
rasterize1.2951.2930.999×14.13%14.07%-0.06
ssim (`ts_ssim_fwd_grad`)1.2021.1940.993×13.12%12.99%-0.13
radix1.1001.1031.003×12.01%12.00%-0.01
project0.2710.2741.012×2.96%2.99%+0.03
l1_combine_ssim0.0900.0901.000×0.98%0.98%0.00
emit_pairs0.1210.1160.962×1.32%1.27%-0.05
extract_off0.0110.0111.008×0.12%0.12%0.00
**leaf 合計****9.161****9.190****1.003×**100%100%—
判定 (a) 確定 — 全 9 leaf が ±5% 以内、profile 経路の系統的底上げは棄却

同じ Lego scene・同じ [backend] regime で binary だけを `a446f2c`→`a556b78` (S4b + C2 計装 + それ以降の全 commit) に進めても、9 leaf すべてが v2 比 0.962×〜1.012× (leaf 合計 1.003×) に収まった — 最大逸脱は emit_pairs の -3.80%、次点は ssim (`ts_ssim_fwd_grad`) の -0.67% で、share Δ も全 kernel ±0.13pt 以内。S4b (ssim_reduce=Gpu) と C2 計装 (全 CB kernel timestamp 収集) を含む binary vintage 差は per-kernel gpu_ms を系統的には底上げしていない — `p3-kernel-share-drums` §2/§4 が疑った「profile=on 経路の系統的な底上げ」仮説は本 run により反証された。drums で観測された 1.17〜1.93倍の増加 (ssim_fwd_grad +44.6% を含む) は binary vintage ではなくdrums scene 自体 (splat 数 1.85倍、画像内容差) に起因すると判定する — 詳細は `p3-kernel-share-drums.toml` §5 参照。

7. v2 が M3 判定に与える示唆 (参考、M3 は既に per-splat backward 実装 → NO-GO で完結済み)

M3 (per-splat backward) の GO 判定はそのまま — share はさらに上昇 (40.07% ≥ 25%)

M3 は本 finding v1 の GO 判定 (33.86% ≥ 25%) を受けて実装・gate まで完了しており (`p3-per-splat-backward` / `p3-per-splat-backward-tg256`)、tg256 版が default (per_pixel) 比 +32.8% と大幅劣後で NO-GO 確定済みである。v2 の backward_raster share 40.07% は M3 実装当時 (33.86%) より さらに大きいため、GO/NO-GO の判定自体は変わらない (GO のまま) — ただし M3 の性能敗因は「256-thread TG の低 occupancy」という実装上の問題であり、share の大小とは無関係の敗因だったため、v2 の share 上昇は M3 の結論を覆さない。本節は記録のためだけに残す — M3 系列の探索は打ち切り済みで、v2 を受けた再挑戦の計画は無い。

8. C2 計装基盤 実装済み (2026-08-30、§4 の file:line 提案 5 箇所を実装)

commit
`9deb14a` (splat-metal/src/gputime.rs, splat-train-v1/src/timing.rs, splat-train-v1/src/train_loop.rs)。worktree agent-a25c4092c2a352ba9 上で実装、main への merge は別途
実装内容
§4 の提案①〜⑤をすべて実装。`Collector.intervals` は `Vec<(f64,f64)>` ではなく `Vec<CbInterval>` (epoch, kernel_start, kernel_end, gpu_start, gpu_end) に拡張 — `union_seconds()` 自体のシグネチャは変えず、呼び出し側で `(gpu_start, gpu_end)` に投影してから渡す形にした (`union_seconds` は public/test 参照されているため非破壊)。`intervals_since(mark)` で生 interval を露出。`timing::gpu_gap_report(n_bands)` / `timing::print_gap_report()` を追加、`train_loop.rs` の各 iteration 先頭で `gpu_set_epoch(it)`、最終 blocking drain 直後 (`gpu_drain()`→`print_summary()` の次、実装当時の行番号は 823-828 — §4 提案が指した「802-803」から drift 済み) で `print_gap_report()` を呼ぶ
当初提案からの逸脱 (2 点)
(1) **epoch 追加**: §4 提案にはなかったが、`harvest()` が非ブロッキング swap_remove 回収のため `intervals` の格納順は完了順であって iteration 順ではないと気づき (advisor 指摘)、iteration 帯別集計が誤ってバンド化されるバグを避けるため `register()` 時点 (CB 生成 = iteration 順) の epoch を CB に焼き付ける仕組みを追加した。(2) **`gap_v/lead` の 3 分解を 4 分解に変更**: §4 提案の ④-b は `gpu_start - kernel_end` (driver 投入 latency の近似) 1 本だけだったが、実装では `kernel_end - kernel_start` (drvproc) と `gpu_start - kernel_end` (handoff) を分けて出し、さらに `gpu_end[i-1] - kernel_start[i]` (host_lead、符号あり・正=host 先行/良い overlap、負=host 遅れ→gap 直結) を追加した 4 分解にした — `commit()→kernelStart` (真の driver 投入 latency) は host 側の commit タイムスタンプ計装が必要で §4 提案 5 箇所のスコープ外のため、host_lead をその代替として位置づけている (詳細は `docs/architecture/backend-and-compiler-conventions.md` §4.1)
smoke 確認 (1000 iter, splat 数 6.9k, config は使い捨てのため未コミット)
`reliable=true dropped=0 kernel_invalid=0 sum/union=1.000000x host_ahead_frac=49.7%`。cb_gap mean 0.454ms (n=2000、p50 0.249ms、p90 0.988ms、max 3.504ms)、drvproc mean 0.038ms、handoff mean 0.974ms、host_lead mean 0.556ms (p50 **-0.192ms** — 中央値は host 遅れ側だが平均は正側に引っ張られる裾の長い分布)。**Σcb_gap = 0.908s vs sync-tax (全量) 0.917s → 99.0%** (`print_gap_report` の `reconcile:` 行、advisor review で追加)。**この 99.0% は「sync-tax 全量」に対する割合であり「残差 4.54s」の割合ではない** — 2 回目の advisor review で指摘: depth2 sync-tax には refine tick の blocking drain (§4: 6.26s/44%) も含まれ、これも同じ `cb_gap` として計上される。本 smoke は `ts_refine` host 計 2.1ms (6 回) と refine コストが無視できる regime だったため「ほぼ全量」に見えただけで、12.5k (refine 125 回・計 6.26s 見込み) では **`cb_gap` は双峰分布になり、`mean × n` は refine tick という外れ値に引っ張られる** — 残差 4.54s 相当を見たいときは `mean` ではなく **`p50 × n`** (robust) を使うこと。恒等式 `cb_gap = -host_lead + drvproc + handoff` は個別 CB で exact に確認済み (単体 test `gap_report_identity_with_host_late`)。12.5k での分解値はまだ未計測 (次項)
12.5k full run で main が実行すべきコマンド
`pgrep -fl "target/release/splat"` が空であることを確認した上で `cd splat && ./target/release/splat train --config configs/2026-08-30-c2-12500-depth2-prof.toml` ([backend] は `2026-08-16-s3-12500-depth2-prof.toml` と bit-exact、[experiment] メタデータのみ差し替え)。run 末尾の `=== C2 gap report ===` ブロックの `reconcile:` 行は sync-tax **全量** (§4: 14.206s 相当) との突き合わせ — 「残差 4.54s」を見るには `p50 × n` (`mean × n` ではなく) を使い、band 別 `max`/`p90` が `refine.every` (=100) 周期で跳ねていれば refine tick の混入として除外して読むこと
reliable ゲートの blast radius (12.5k で false になった場合の読み方)
`dropped > 0` は GPU timeline に穴を空けるため `cb_gap` を含む**全列**を無効化する。`kernel_invalid > 0` は kernel timestamp だけが無効なので **`drvproc`/`handoff`/`host_lead` の 3 列だけ**を縮小させる — `cb_gap` (と `reconcile:` 行) は `GPUStart`/`GPUEnd` だけに依存するため**無影響**。よって 12.5k (~25000 CB、smoke の ~10 倍) で `kernel_invalid > 0` が出て `reliable=false` になっても、`dropped == 0` である限り `cb_gap`/`reconcile:` は捨てずに読んでよい
未解決点
(1) **実測済 → `p3-c2-bubble-attribution`** (2026-08-30、M4+A7 default 込みの main HEAD で 12.5k full run。reliable=true dropped=0 kernel_invalid=0、残差=通常境界 7.2〜8.1s / refine tick drain 3.0〜3.8s とブラケット分解、host-late 支配・handoff は定常帯で headroom 薄いと判明)。(2) `dropped`/`kernel_invalid` が smoke では 0 だったが、12.5k (400k splats 規模、より長い run) で HW-3 相当の 0/逆転 timestamp が出るかは未確認 (出た場合の読み方は上記) — 実測 run でも `dropped=0 kernel_invalid=0` だったため杞憂に終わった。(3) `commit()→kernelStart` の真の driver 投入 latency は依然として計装されていない (意図的なスコープ外、host_lead が代替) — 必要になった場合は per-`.commit()`-call-site の instrumentation が別途要る。(4) 12.5k run の最初の band が `epoch_lo == 1` になっているか確認すること — `CURRENT_EPOCH` は 0 始まりのため、`enable()` 後 最初の `gpu_set_epoch(1)` より前に登録された CB があると epoch 0 に落ちて band 境界がずれる (smoke では `[1, 250]` で clean だったが、setup 部分の CB 数が config によって変わりうる) — 実測 run でも `[1, 3125]` で clean、band n 合計 25000 と一致し drift なしを確認済み
02

関連する run

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

03

関連 finding

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