Stage 1 brushcompat 5k baseline (31.308 dB / 93,948 splats / 2m 5s) に対し、brush 互換 sigmoid-space opacity decay (rate=0.004、refine cadence で全 splat に適用) を追加し 5k smoke を実行。結果: PSNR 31.689 dB (+0.38 dB)、splats 83,093 (-10,855、-11.6%)、wallclock 2m 34s (+29s、+23% — Adam 後 host RMW のオーバヘッドだが許容範囲)。4-way eval は brush conv +q8 で 31.706 dB、legacy conv で 1.595 dB (Stage 2 row 1 と同じ対称崩壊 pattern、convention bridge は健全動作)。PSNR と splats が同時改善 (trade-off 不発) しており、30k full bench に進む価値あり。実装は refine.rs:`apply_opacity_decay` 1 関数 (33 行 CPU host RMW、brush の sigmoid-space formula を忠実移植) + train_loop.rs 1 ブロック (12 行) + config.rs 1 field のみで、kernel 改変なし。
P1.B+F Stage 2 (Lego 30k brushcompat = 35.18 dB) は brush parity を +3.20 dB 上回ったが、splats が 846,689 (brush 282k の 3 倍)、wallclock 1h 2m (brush ~22m の 2.8 倍) と効率面で劣る。P1.D では brush の opacity decay (`opac_decay=0.004` default) を sigmoid-space で忠実移植し、5k smoke で splats -11.6% + PSNR +0.38 dB の同時改善 を確認。30k full bench に進む go 判定。
splats と PSNR の trade-off が不発、両方改善。 brush の sigmoid-space opacity decay を refine cadence (100 iter 毎) で applies すると、(1) 低 opacity splat が物理的に prune されやすくなり, (2) 全 splat 一律 shift で残った splat は より高 opacity で精度寄与する。raw-space multiplicative (`raw *= 1-decay`) では sigmoid=0.5 付近で no-op になる落とし穴があるため、sigmoid-space subtraction が必須。5k で +0.38 dB なら 30k では +1〜+2 dB shift も期待できる (要実測)。
| metric | Stage 1 baseline 5k | opacity decay 5k (Phase D) | Δ |
|---|---|---|---|
| PSNR (training-time、brush conv) | 31.308 dB | **31.689 dB** | **+0.38 dB** |
| PSNR (eval、brush conv + q8) | (未測) | **31.706 dB** | — |
| PSNR (eval、legacy conv) | 1.6 dB 相当 | 1.595 dB | ≈ same (対称崩壊、Stage 2 row 1 と同 pattern) |
| splats (final) | 93,948 | **83,093** | **-10,855 (-11.6%)** |
| wallclock | 2m 5s | 2m 34s | +29s (+23%) |
| ms/iter (median window) | ~ 25 | ~ 34 | +9 ms (refine cadence で host RMW) |
| final loss | — | 1.468e-2 | — |
| iter | loss | splats | ms/iter | 経過 |
|---|---|---|---|---|
| 1 | 5.94e-1 | 5,207 (init) | 162 | 0.16s |
| 500 | 8.62e-2 | 842 | 21.8 | 11.1s |
| 1000 | 5.45e-2 | 11,363 | 16.5 | 19.3s |
| 1500 | 3.12e-2 | 83,093 | 23.8 | 31.2s |
| 2000 | 1.95e-2 | 83,093 | 35.6 | 49.0s |
| 3000 | 1.63e-2 | 83,093 | 33.8 | 82.5s |
| 5000 | 1.47e-2 | 83,093 | 33.5 | 154.5s (2m 34s) |
// crates/splat-train-v1/src/refine.rs — Phase D 追加関数
pub fn apply_opacity_decay(param: &Param, decay_rate: f32, train_t: f32) {
let n = param.num_splats as usize;
if n == 0 || decay_rate == 0.0 { return; }
let t_clamped = train_t.clamp(0.0, 1.0);
let minus_opac = decay_rate * (1.0 - t_clamped);
if minus_opac == 0.0 { return; }
unsafe {
let ptr = param.raw_opacities.contents() as *mut f32;
for i in 0..n {
let raw = *ptr.add(i);
let cur = sigmoid(raw);
let new_opac = (cur - minus_opac).clamp(1e-12, 1.0 - 1e-12);
*ptr.add(i) = logit(new_opac);
}
}
}
// crates/splat-train-v1/src/train_loop.rs — refine() の **直前** に挿入
if refine_cfg.opacity_decay_rate > 0.0
&& it >= refine_cfg.start_iter
&& it <= refine_cfg.stop_iter
&& refine_cfg.every > 0
&& it % refine_cfg.every == 0
{
let train_t = it as f32 / cfg.max_steps as f32;
refine::apply_opacity_decay(param, refine_cfg.opacity_decay_rate, train_t);
}
minus_opac = rate × (1 - train_t)、train_t = iter/max_steps。終盤 (t→1) で decay→0、後半は刈り込みなし。raw *= 1-decay) は raw=0 (sigmoid=0.5) で no-op になり brush と非互換。load_record(record) で m/v を維持しており、refine cadence で resetすると momentum が壊れる。raw_opacity の 絶対値 shift なので m_raw_opacity の方向情報は引き続き有効。decay_rate > 0.0 で初めて関数 body に入る (early return)。default 0.0 で完全な backward compat。| # | trainer | eval convention | quant 8-bit | PSNR (dB) | min | median | max |
|---|---|---|---|---|---|---|---|
| 1 | Phase D opacity decay 5k | legacy | — | 1.595 | 0.991 | 1.615 | 1.942 |
| 2 | Phase D opacity decay 5k | brush | q8 | **31.706** | 25.122 | 31.925 | 35.947 |
| 3 | (reference) training-time eval | brush | raw | 31.689 | — | — | — |
5k smoke で PSNR +0.38 dB / splats -11.6% の同時改善。trade-off 不発の理想形。30k full bench で Stage 2 35.18 dB / 846k splats を上回り、かつ splats を 282k 帯 (brush 並) に近づけられる 期待が高い。即 30k に進む価値あり。
| 役割 | path:line |
|---|---|
| 新規 関数 apply_opacity_decay | splat/crates/splat-train-v1/src/refine.rs:365-394 |
| train_loop 統合 (refine 前 shift) | splat/crates/splat-train-v1/src/train_loop.rs:316-329 |
| RefineConfig.opacity_decay_rate field | splat/crates/splat-train-v1/src/config.rs:135-141 |
| unit test opacity_decay_subtracts_in_sigmoid_space | splat/crates/splat-train-v1/src/refine.rs:tests |
| 新 config (5k smoke) | splat/configs/2026-05-24-2000-lego-brushcompat-opacdecay-5k.toml |
| smoke run 結果 | splat/runs/lego-brushcompat-opacdecay-5k/result.toml |
| brush 元実装 (sigmoid-space formula) | /Users/otkrickey/dev/brush/crates/brush-train/src/train.rs:611-619 |
| brush opac_decay default 0.004 | /Users/otkrickey/dev/brush/crates/brush-train/src/config.rs:82-84 |
この finding が観測された / 言及している実験 run。