松田研究室
FindingID: p3-brush-sameday-lego-30k
Status: stable / task seminar05 brush 同日比較
← Findings Index
2026-09 第 5 回ゼミ · brush 同日比較 · Lego 30k

brush 同日比較 Lego 30k — 本実装 392s vs brush 417s (0.94×)、PSNR は評価器次第で ±1.9 dB 反転

5 月の binary をそのまま使った brush (wgpu→Metal) と本実装の現行最終スタックを、2026-09-23 に同じ M4 Max で直列に 1 本ずつ Lego 30k 学習。wallclock は本実装が brush の 0.912× (process) / 0.941× (trainer 内部値)。PSNR は、brush PLY と本実装 PLY を互いの評価器にかけると、どちらの評価器でもその renderer で学習した側が約 1.8〜1.9 dB 有利 (brush 評価器: brush 37.41 / 本実装 35.58、splat eval q8: brush 34.57 / 本実装 36.47)。「同じ評価器で測った 1 つの PSNR」では優劣を決められない。

mixedrank: highsame-day-speed-parity-plus-psnr-is-renderer-coupled第 1 軸第 2 軸brushsame-daylego-30kwgpufinal-stackcross-evalrenderer-couplingseminar05
Δ PSNR
splat eval q8: 本実装 +1.895 dB (36.465 vs 34.570)。brush 評価器: 本実装 -1.824 dB (35.582 vs 37.406)。各実装の自己評価: 本実装 -0.996 dB (36.418 vs 37.414)
Δ Wallclock
本実装 / brush = 0.941× (trainer 内部 392.39s / 417s) / 0.912× (process 395.07s / 433.23s)
Δ Splats
485,825 vs 284,151 (1.71×)
Impact
同日・同機・直列・load 記録付きで Lego 30k を brush / 本実装 1 本ずつ実行。学習時間は本実装 392.39s vs brush 417s (trainer 内部値、0.941×)、process wallclock では 395.07s vs 433.23s (0.912×)。PSNR は評価器依存が大きい: brush PLY を brush 評価器で測ると 37.41 dB、splat eval q8 で測ると 34.57 dB (-2.84 dB)。本実装 PLY は splat eval q8 で 36.47 dB、brush 評価器で 35.58 dB (-0.88 dB)。同じ評価器の中で比べると、splat eval では本実装 +1.90 dB、brush 評価器では本実装 -1.82 dB。どちらの評価器を『公式』にするかで優劣が入れ替わる。splats は brush 284,151 / 本実装 485,825 (1.71×)。
01

本文

5 月の brush 記録 (m4-brush-bench: 9m08s / 37.40 dB) は load 記録がなく、日付も違ったので、2026-09-23 に同じ M4 Max で brush と本実装を直列に 1 本ずつ Lego 30k 学習させた。brush は 5 月と同じ binary と同じ引数。本実装は 2026-08-31-final-lego-30k.toml を run 名・run_dir・日付だけ変えて複製した config。学習時間は本実装が brush の 0.94× (trainer 内部値) / 0.91× (process)。PSNR は どちらの renderer で評価するかで本実装 +1.90 dB 〜 -1.82 dB と符号が反転した。

0. 計測環境

機体 / 電源
M4 Max 36GB。`pmset -g batt` は driver 開始前・各 run 前後とも `AC Power` (作業開始時はバッテリー駆動だったが、smoke 前に AC 接続を確認)
GPU 専有
`pgrep -fl "splat|brush|msplat"` 空を確認してから開始。本 driver 以外の GPU run なし。Docker の Virtualization VM が常駐しているが CPU 0.5% 程度で idle
disk
`df -h /` 空き 16GiB (開始時) → 15GiB (終了時)
本実装 binary
`splat/target/release/splat` sha256 `c804b349223ffc68e9e1101d79c5ea9f058dfa8d34620504b66aa0a3eb10aa13` (p3-final-stack-v2 と同一)。`cargo build --release -p splat-cli` は no-op (0.37s)。git rev e7fc835 (1565fcc 以降 splat/crates・shaders に変更なし)。`[backend] BackendConfig` ログで radix_scatter=SimdBallot 等の default を確認
brush binary
`~/dev/brush/target/release/brush` sha256 `c460b12aac29a8308bb9d0af7e2628e33da45c0320e2adda9d5be0b80e0c1616`、HEAD ce6ef3f、working tree clean、mtime 2026-05-23 01:43 (5 月 bench の 2 分前 = 5 月と同じ binary)。**再 build していない** (rustc 更新などで 5 月との比較条件が変わるのを避けるため)
driver
`splat/scripts/brush-sameday-lego-30k.sh` (新規、gate-ab-n3.sh の流儀: AC 確認 → load1<6.0 が 60s 続くまで quiesce → meta pre/post → 10s load sampler (epoch load1 top3) → sentinel .DONE)。`nohup … < /dev/null & disown` で detach し、foreground の until loop で sentinel/meta を polling
load (開始時)
`uptime` 2.79 2.64 2.42 (22:14)

1. 手順

# (a) brush smoke (500 / 3000 iter): eval 出力・PLY header・splat eval での読込を事前確認
#     500 iter: brush 13.758 / splat eval q8 13.747 / raw 13.740 (差 0.01 dB)
#     3000 iter: brush 27.070 / splat eval q8 26.821 / raw 26.655 (差 -0.25 dB、学習が進むほど開く — §4)
# (b) 本番 (直列、driver 1 本)
nohup sh splat/scripts/brush-sameday-lego-30k.sh 2026-09-23-brush-sameday \
  > splat/runs-logs/2026-09-23-brush-sameday-driver.log 2>&1 < /dev/null & disown
#   ARM brush: 5 月 (m4-brush-bench.md) と同じ引数。export-path だけ /tmp から変更
RUST_LOG=info ~/dev/brush/target/release/brush /Users/otkrickey/dev/3dgs-workspace/datasets/nerf_synthetic/lego \
  --total-train-iters 30000 --eval-every 5000 --export-every 30000 --max-resolution 800 \
  --export-path /Users/otkrickey/dev/3dgs-workspace/brush-sameday-20260923/lego-30k/ --export-name 'export_{iter}.ply'
#   ARM splat:
target/release/splat train --config configs/2026-09-23-brush-sameday-lego-30k.toml

# (c) splat eval (本実装側の評価器、q8 が公式値) を両 PLY に
DS=/Users/otkrickey/dev/3dgs-workspace/datasets/nerf_synthetic/lego
for P in /Users/otkrickey/dev/3dgs-workspace/brush-sameday-20260923/lego-30k/export_30000.ply \
         runs/2026-09-23-brush-sameday-lego-30k/final.ply; do
  ./target/release/splat eval --ply $P --dataset $DS --split val --convention brush --quantize-8bit
  ./target/release/splat eval --ply $P --dataset $DS --split val --convention brush
done   # → splat/runs-logs/2026-09-23-brush-sameday-eval.log

# (d) brush 評価器 (brush-train/src/eval.rs) を両 PLY に: dataset を symlink で複製し、
#     init.ply に評価したい PLY を置いて start-iter=29999 / total=30000 で起動 (log は Took 0ns、最大 1 step の更新の後に eval 1 回)
#     brush 自身の PLY で 37.406 dB (学習中の値 37.414 と差 0.008 dB) を確認し、この方法が使えることを確かめた
W=/Users/otkrickey/dev/3dgs-workspace/brush-sameday-20260923
RUST_LOG=info ~/dev/brush/target/release/brush $W/xeval-{brushply,splatply}/lego \
  --start-iter 29999 --total-train-iters 30000 --eval-every 30000 --export-every 1000000 \
  --max-resolution 800 --export-path $W/xeval-*/out/

2. 結果 (Lego 30k、n=1 ずつ、2026-09-23 同日・同機・直列)

実装学習時間 (trainer 内部)process wallclock自己評価 PSNRsplat eval q8 (本実装評価器)splat eval rawbrush 評価器final splatsrun 中 load1 (10s sampler)
brush (wgpu→Metal, ce6ef3f)417s (`Took FormattedDuration(417s)`、6 回の eval を含む)433.23s37.414 (brush 内蔵 eval、val 100 views、q8)34.57032.16337.406 (§1(d) の再評価)284,1510.88–2.20 (mean 1.52、n=43)
本実装 (最終スタック)392.39s (`training done in`)395.07s36.418 (trainer 内蔵 `mean val`、premultiplied)**36.465**36.41835.582485,8251.79–2.88 (mean 2.19、n=39)
本実装 / brush、差分**0.941×****0.912×**-0.996 dB (raw 36.418 基準。本実装の q8 36.465 で揃えると -0.949 dB)**+1.895 dB**+4.255 dB**-1.824 dB**1.71×—
val split 100 views (brush は `transforms_val.json` を優先して読み、log に `100 eval views` と出る。本実装も `--split val`)。brush の『Took』は整数秒で丸められており、学習中の eval 6 回 (1 回あたり約 1s) を含む。本実装の `training done in` には最終 val eval を含まない。比の分子と分母で含むものが違うので、両方の比を載せた。自己評価列は brush が q8 (clamp なし)、本実装が trainer 内蔵の raw。process wallclock は driver の meta (`date +%s.%N` の pre/post 差) で、brush は起動・autotune cache 読込・PLY export も含む。

3. 学習時間比の読み方

  • trainer 内部値の比 0.941×、process wallclock の比 0.912×。どちらで見ても本実装が 6〜9% 速い。n=1 ずつなので、この差は run 間ばらつき (p3-final-stack-v2 の 12.5k n=3 で σ1.7%、30k n=2 で r1/r2 差 16.8%) と同じくらいの大きさ。『同等〜わずかに速い』までは言えるが、『速い』とは断定できない
  • 本実装 392.39s は p3-final-stack-v2 §10 の r1 (413.56s) より 5.1% 速い。1000-iter 区間ごとに比べると 0–12k と 24k–30k は ±2% で一致し、差は 12k–24k の区間に集中している (8/31 r1 の方が 6〜19% 遅い)。8/31 r1 のこの区間には load 汚染があったと推定される。PSNR (self 36.418 vs 36.457、q8 36.465 vs 36.501) と splats (485,825 vs 490,866) は a-10 の σ±0.32 dB 以内で一致
  • splat 数は本実装が brush の 1.71× なので、splat あたりの throughput に直すと本実装側がさらに有利になる。ただし avg splats (台形積分) は brush 側で出していないので、throughput 比は算出していない

4. PSNR: 評価器と学習元の組み合わせで決まる (renderer coupling)

評価器 \ PLYbrush PLY本実装 PLY差 (本実装 - brush)
brush 評価器 (brush-train eval.rs、bg=0、q8)**37.406**35.582-1.824
splat eval --convention brush --quantize-8bit34.570**36.465**+1.895
splat eval --convention brush (raw)32.16336.418+4.255
その評価器で学習した側から見た、他方の評価器にかけたときの低下-2.836 (brush→splat q8)-0.883 (splat q8→brush)—
両評価器とも GT は premultiplied、bg=0、8-bit roundtrip、val 100 views。違いは (a) renderer 本体 (brush: cubecl/wgpu kernel、本実装: Metal kernel) と (b) clamp の有無 — splat eval の q8 は r.clamp(0,1) してから丸める (splat-cli/src/cmd/eval.rs:250) のに対し、brush の eval.rs は clamp せずに (x*255).round()/255 する (brush の render 出力の内部で clamp されているかは未確認)。本実装 PLY の brush 評価器 35.58 dB には、clamp しないことで overshoot 分が罰される効果が一部含まれている可能性がある。どちらの評価器でも、その renderer で学習した PLY が約 1.8〜1.9 dB 有利になる。brush PLY を splat eval にかけると raw と q8 の差が 2.4 dB ある (本実装 PLY では 0.05 dB) ことから、本実装 renderer で brush PLY を描くと [0,1] 外に大きく overshoot する画素が多いとわかる。smoke では差が 500 iter 0.01 dB → 3000 iter 0.25 dB → 30k 2.84 dB と学習が進むほど開いた。原因は renderer の差 (小さい / 高 SH 次数の splat の扱いなど) と clamp の有無の組み合わせと考えられる。cov2d blur は両者 0.3 で一致していることを確認済み。原因の特定は本 task の範囲外で、コードは変更していない。
『同じ評価器で測れば公平』は成り立たない

依頼時の方針は『本実装の splat eval q8 を brush PLY にも適用すれば同一評価器比較になる』だった。しかし実測すると、評価器を brush 側に替えれば優劣が逆転する (本実装 +1.90 dB ↔ -1.82 dB)。p1-a-3 の convention coupling (学習と評価の convention が結びつく) と同じ形で、今回は renderer の実装差 (と eval 時の clamp の扱い) も学習と評価で結びつくことがわかった。過去 finding の『brush 自身 val 32.038 (q8: 34.484)』(p1-d-stage2 ほか) も、brush PLY を splat eval で測った値だと推定される (今回 32.163 / 34.570 とほぼ一致)。『本実装が brush を val で +x dB 上回った』という既存の主張は、本実装の評価器に有利な測り方であることに注意が必要。ゼミ資料では 2×2 表 (§4) をそのまま出すか、各実装の自己評価 (brush 37.41 / 本実装 36.42、-1.0 dB) を併記するのが誠実。

5. 5 月の brush 値との差

記録brush buildwallclockPSNRsplatsload 記録
2026-04-24 (brush-eval.md 試行 3)v0.3.0 (3dgs-workspace/brush、別 checkout)384s (6m24s)37.38—なし
2026-05-23 (m4-brush-bench)ce6ef3f、本日と同一 binary548s (9m08s)37.397284,323 (toml 側は 282,103 と記載、md と不一致)なし
**2026-09-23 (本 finding)**ce6ef3f、同一 binary (sha256 c460b12a…)**433.23s** (process) / 417s (Took)**37.414****284,151**あり (0.88–2.20)
5 月→今日で wallclock -21% (548→433s)。PSNR (+0.017 dB) と splats (-0.06%) はほぼ同じなので、学習の軌跡は同じで throughput だけが違う。5 月の 548s が process wallclock か brush の Took 値かも記録されていない。
  • binary は同じ: sha256 と mtime から 5 月と同じ binary と確認した。コード差はない
  • 考えられる要因 (いずれも検証していない): (1) 5 月の run 中の同時実行 load / 熱状態 / 電源状態 — load 記録がなく判定できない。5 月の session は複数 job を並行することが多かった。(2) macOS 25.3→25.6 での Metal driver / コンパイラ更新。(3) cubecl autotune cache の状態差 — ただし 5 月も m4-brush-bench.md の手順 3 で 1k smoke を先に走らせているので cache は温まっていたはずで、有力な説明ではない
  • 4 月の 384s (別 checkout の v0.3.0) と 5 月の 548s も 43% 違っており、brush の wallclock は条件記録なしでは ±20〜40% 振れる。5 月値は条件管理の弱い参考値とし、今日の load 記録付きの値 (433s / 417s) を比較の基準にするのが妥当

6. 汚染・信頼度

  • brush window (22:15:10–22:22:23): load1 0.88–2.20、pre 2.23 / post 1.80。途中に Spotlight.app (UI、17%CPU) と日本語 IME が一時的に top3 に出た (ユーザーの操作)。mds_stores の重い indexing は検出されなかった
  • 本実装 window (22:23:13–22:29:48): load1 1.79–2.88、pre 2.59 / post 2.97。終盤約 65s (≈iter 25k 以降) で XProtectRemediator* (macOS のマルウェアスキャン、1 本あたり約 90%CPU、trustd 併走) が動いた。1000-iter 区間の ms/iter は 27k–30k で 8/31 r1 と -0.8〜-1.5% 一致しており、目に見える遅延はなかった (CPU 側の処理で GPU-bound の学習とは競合が小さい)。ただし最大 1% 程度の影響は否定できない
  • 両 run とも quiesce gate (load1<6.0 が 60s) を通過。load の最大は 2.88 で、過去の汚染事例 (load 5.77〜24) より十分低い
  • PSNR / splats は load に依存しない量で、再評価 (§1(d)) でも brush 37.406 と学習中の値 37.414 が 0.008 dB で一致した。wallclock は n=1 ずつで σ が出せない。比 0.91〜0.94× は『同等〜わずかに速い』と読む
  • brush 学習開始前に smoke を 2 本 (500 / 3000 iter、計 20s ほど) 走らせたため、brush の autotune cache は温まった状態で本番を始めている。本実装側に同様の cache はない

7. 成果物

config
`splat/configs/2026-09-23-brush-sameday-lego-30k.toml` (2026-08-31-final-lego-30k.toml から name / description / run_dir / created とヘッダ comment だけ変更。`splat check` ok)
driver
`splat/scripts/brush-sameday-lego-30k.sh`
logs
`splat/runs-logs/2026-09-23-brush-sameday-{driver,brush,splat,eval}.log`、`-{brush,splat}.meta`、`-loadsamples.log`、`.DONE`
本実装 run
`splat/runs/2026-09-23-brush-sameday-lego-30k/{final.ply,result.toml,config.toml}`
brush 成果物 (repo 外)
`/Users/otkrickey/dev/3dgs-workspace/brush-sameday-20260923/` — `lego-30k/export_30000.ply`、`smoke/` (500/3000 iter)、`xeval-{brushply,splatply}/` (brush 評価器による再評価の symlink dataset と log)
03

関連 finding

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