Phase 2c。graphdeco-inria/gaussian-splatting を V100 (sm_70) で 30k 学習し PSNR 28.384 dB / 10m37s / 237k splats。同時に codebase eval (28.4 dB) vs paper-standard eval (14.6 dB) の 12 dB 乖離が判明、A.5 三層対比の eval convention 問題を確定。
V100 (c32 GPU1) で graphdeco-inria/gaussian-splatting (54c035f) を Lego 30k 学習し PSNR 28.384 dB / 10m37s / 237,920 splats。同 V100・同 30k で brush wgpu→Vulkan が 8m24s / 37.46 dB と原著 CUDA を上回る (wgpu 抽象は 21% 速い)。加えて codebase eval (28.4 dB) vs paper-standard eval (14.6 dB) で 12 dB の convention 乖離が発覚、A.5 三層対比表に eval convention 注記が必須となった。
| 項目 | 値 |
|---|---|
| PSNR @ 30k (codebase eval) | 28.384 dB (val 100 frame、α マスク render vs raw gt) |
| PSNR @ 30k (paper-std eval) | 14.598 dB (val 100 frame、raw render vs white-composited gt) — 詳細 §6 |
| wallclock @ 30k | 10m 37s (train、init pcd 含む) / 全体 11m 07s |
| final splats | 237,920 |
phase 2a/2b 結果対比: gsplat (V100) PSNR 32.94 / 5m03s / 108,704 splats、brush wgpu→Vulkan (V100) PSNR 37.46 / 8m24s / ~260k splats、brush wgpu→Metal (M4 Max) PSNR 37.40 / 9m08s / ~284k。
PSNR が 2 通り出るのは、原著 3DGS 上流の train.py 損失が image *= alpha_mask + raw RGB gt の混成領域 で実装されており、paper 想定の white-bg eval (gt を白でアルファ合成) で測ると訓練中見たことのない off-object floater が penalize されて 14 dB 台に下がるため。A.5 表では codebase eval (28.4 dB) を使うが、両方を併記する。
orig3dgs-env の 3 拡張 wheel が sm_70 で上書き済。c33 (A6000, sm_86) で再使用するには TORCH_CUDA_ARCH_LIST='7.0;8.6' でマルチ sm 再 build か、8.6 単独で上書きが必要 (将来作業)。
phase 1 で c33 に build した orig3dgs-env は NFS 共有なので、c32 側でも conda activate するだけで Python パッケージは即動く。問題は CUDA 拡張の sm 互換性:
TORCH_CUDA_ARCH_LIST=7.0 を export すれば V100 用 .so を初回 build cache する (c32-gsplat-smoke.md 参照、93s)。-gencode arch=compute_86,code=sm_86 のみで、sm_70 (V100) で kernel launch すると symptom が "OOM 21 TB" という形で発現 (kernel ABI mismatch の garbage launch parameter)。TORCH_CUDA_ARCH_LIST=7.0 + --force-reinstall --no-build-isolation --no-deps で 3 サブモジュールを再 build。ssh matsudalab-c32
source ~/miniconda3/etc/profile.d/conda.sh && conda activate orig3dgs-env
cd ~/repos/gaussian-splatting
export TORCH_CUDA_ARCH_LIST='7.0'
export CUDA_HOME=/usr/local/cuda
pip install --force-reinstall --no-build-isolation --no-deps \
submodules/simple-knn \
submodules/diff-gaussian-rasterization \
submodules/fused-ssim
副作用: NFS 共有 env なので、これで wheel が sm_70 上書きされた状態になる。c33 (A6000、sm_86) で同 env を使う場合、現状 simple-knn の distCUDA2 が no kernel image で落ちる。対処案 2 通り:
# 案 1: c33 側で 8.6 上書き (c32 で再度使うとまた 7.0 上書き必要)
TORCH_CUDA_ARCH_LIST='8.6' pip install --force-reinstall --no-build-isolation --no-deps submodules/*
# 案 2: マルチ sm wheel (build 時間 2 倍、安定)
TORCH_CUDA_ARCH_LIST='7.0;8.6' pip install --force-reinstall --no-build-isolation --no-deps submodules/*
phase 2d 以降 c33 で原著 3DGS を再実行する場合は要対処 (今 session はスコープ外)。
NeRF Synthetic Lego の test/ 200 frame のうち 164 frame がローカル NFS rsync で欠損していた (phase 2a の gsplat smoke 時点で既知)。原著 3DGS の readNerfSyntheticInfo は eval flag に関係なく test 用 readCamerasFromTransforms を呼び、200 frame 全部 Image.open するため、欠損 = train.py が起動できない。
対処: transforms_val.json を transforms_test.json に substitute。これで --eval 評価は val 100 frame に対して走る。
cd ~/datasets/nerf_synthetic/lego
cp transforms_test.json transforms_test.json.bak
cp transforms_val.json transforms_test.json
加えて、ローカル copy の transforms_*.json は file_path に既に .png を含む変則的書式 (./train/r_0.png)。readCamerasFromTransforms が path + extension で .png.png を作って FileNotFound するため、3 split 全部から .png を strip。
for fr in d["frames"]:
if fr["file_path"].endswith(".png"):
fr["file_path"] = fr["file_path"][:-4]
(transforms_*.json.bak で原本は保持。./val/r_0 の path は OK、loader は path/val/r_0.png を開く。)
gsplat ↔ 原著 3DGS の eval 公平性: どちらも val=100 frame で eval する状態に揃った (gsplat smoke も val 評価。c32-gsplat-smoke.md §2 参照)。brush は brush 自体の eval pipeline で別マスキング規約だが、A.5 表ではこのまま 3 層対比に使う。
scene/dataset_readers.py:259 で Image.fromarray(np.array(arr*255.0, dtype=np.byte), "RGB") が TypeError: Cannot handle this data type: (1, 1, 3), |i1。np.byte = np.int8 (signed) であり、numpy 2.4 + Pillow 12.2 で signed int8 が RGB image として拒否される。
# 1 行修正
# np.byte → np.uint8
image = Image.fromarray(np.array(arr*255.0, dtype=np.uint8), "RGB")
これは原著 3DGS の eval 実装が --white_background Blender 系に対し未対応:
readCamerasFromTransforms は alpha compose で白背景合成した RGB PIL image を作るが、utils/camera_utils.py:21 の loadCam は Image.open(image_path) で原 PNG を直接再 open、RGBA を読む。scene/cameras.py:42-46 で gt_image = resized_image_rgb[:3, ...] (RGB チャネル = 黒背景の raw RGB) + alpha_mask = resized_image_rgb[3:4, ...] (アルファ別保持)。train.py:114-116) では image *= alpha_mask で render を α マスクしてから loss を取るため正しく学習が進む。train.py:230-241) で α マスクが当たっておらず、render (白背景) と gt_image (黒背景) を直接比較。1k 段階で PSNR 3.05 dB と異常値 (実態は loss 0.05 で学習自体は順調)。# train.py line 231 直後に挿入
image = torch.clamp(renderFunc(viewpoint, scene.gaussians, *renderArgs)["render"], 0.0, 1.0)
if viewpoint.alpha_mask is not None:
image = image * viewpoint.alpha_mask.cuda() # added: match training loss masking
gt_image = torch.clamp(viewpoint.original_image.to("cuda"), 0.0, 1.0)
これで 1k 段階 PSNR が 23.58 dB (paper 軌跡: 1k で ~25 dB、30k で ~35 dB) に揃った。ただし完成形ではない — §6 参照。
CUDA_VISIBLE_DEVICES=1 python train.py \
-s ~/datasets/nerf_synthetic/lego \
--model_path ~/runs/orig3dgs-lego-1k-smoke \
--iterations 1000 --eval --white_background \
--test_iterations 500 1000 --save_iterations 1000 \
--disable_viewer
| iter | L1 (test=val) | PSNR (test=val) |
|---|---|---|
| 500 | 0.0423 | 19.984 dB |
| 1000 | 0.0225 | 23.583 dB |
wallclock 1k iter ≈ 30s (含む dataset load + random 100k init pcd の knn)。
CUDA_VISIBLE_DEVICES=1 nohup python -u train.py \
-s ~/datasets/nerf_synthetic/lego \
--model_path ~/runs/orig3dgs-lego-30k \
--iterations 30000 --eval --white_background \
--test_iterations 5000 10000 15000 20000 25000 30000 \
--save_iterations 30000 --disable_viewer \
> ~/runs/orig3dgs-lego-30k/train.log 2>&1 &
| iter | L1 (test) | PSNR test (dB) | PSNR train sample (dB) |
|---|---|---|---|
| 5000 | 0.00895 | 27.785 | 27.415 |
| 10000 | 0.00751 | 28.342 | 28.012 |
| 15000 | 0.00722 | 28.402 | 28.194 |
| 20000 | 0.00722 | 28.386 | 28.279 |
| 25000 | 0.00715 | 28.417 | 28.329 |
| 30000 | 0.00720 | 28.384 | 28.353 |
→ 15k 以降ほぼ完全に plateau。densify_until_iter=15000 (default) で refine 停止後は flat。
| 区分 | 時刻 |
|---|---|
| Output folder 作成 | 03:06:20 |
| Train start (init pcd 完了) | 03:06:50 |
| Train end (iter 30000 save) | 03:17:24 |
| プログラム終了 | 03:17:27 |
~/runs/orig3dgs-lego-30k/point_cloud/iteration_30000/point_cloud.plyiter 30000 checkpoint の保存後、4 通りの eval ストラテジで val 100 frame の PSNR を比較したところ:
| Variant | 説明 | PSNR (dB) | L1 |
|---|---|---|---|
| A. codebase 修正版 | render *= alpha mask、gt 生 RGB | 28.384 | 0.00720 |
| B. paper-standard | render 生 (bg=white)、gt += (1-α)*white | 14.598 | 0.08848 |
| C. 両方白合成 | render 生 + gt 白合成 (= B と同じ、render bg=white なので) | 14.598 | 0.08848 |
| D. 上流 unmodified | render 生 vs gt 生 RGB (train.py original eval) | 2.512 | 0.61280 |
paper 提示 (Kerbl Table 2) の Lego 30k = 34.5 dB に対し、本実装の A は -6.1 dB、B は -19.9 dB。
原因解析 (iter 30000 の test camera 0 の中央エリア統計):
| エリア | render 平均 RGB | gt 平均 RGB (raw) | gt 平均 RGB (white-composite) |
|---|---|---|---|
| object 内側 (α>0.5) | (0.58, 0.50, 0.30) | (object color) | (object color) |
| 外側 (α<0.1) | (0.93, 0.93, 0.85) | (0.002, 0.002, 0.001) (≒0、premultiplied) | (1.0, 1.0, 1.0) |
→ object 外側で render は 白に近いが完全に白ではない (0.93)。原因は train loss が image *= alpha_mask で render の外側を 0 にされてから gt と比較しており、モデルが 外側に何を描くかを学習する圧がかからないため、低 opacity の "floater" gaussian が散らばって rendering bg (白) と混ざる結果。
これが visible に: render イメージに yellow speck が散らばる 形で確認できる (/tmp/render_30k.png を eval debug で出力)。
A (28.4 dB) は upstream codebase の自己整合的な数値 だが、paper 想定の "white-bg comparison" とは厳密に異なる。Kerbl らの paper 35 dB と直接比較不可。
B (14.6 dB) が paper-standard 評価 だが、本実装の trainer が paper-standard で訓練していないため公平不可。
D (2.5 dB) は上流 train.py が printf する数値 で、--white_background Blender path で実質壊れている。
A.5 表での扱い: A の 28.4 dB を載せ、本 .md の本セクションを reference して "eval convention 注記" を付ける。3 層対比 (gsplat 32.94 / brush 37.46 / orig 28.4) は 同 val=100 frame、同 V100、ただし orig は codebase-internal eval の枠で並べる。
「Apple Silicon native vs CUDA 三層対比」の比較を取る際に、ナイーブに gsplat / brush / orig 3DGS を並べてしまうと:
実際には eval convention の選択 で 28.4 〜 14.6 〜 2.5 dB の 12 dB 幅の振れが生じる、というのが本来の事実。卒論 Chapter 4 ではこの convention 依存性 を最初に明示し、3 層対比は「同 convention 内での比較」に限定する必要がある。これは A.3 phase 2 全体の methodological prerequisite finding と位置付けられる。
| 自作 (M4 Max) | brush wgpu→Vulkan (V100) | orig CUDA (V100) | gsplat CUDA (V100) | |
|---|---|---|---|---|
| PSNR (dB) (codebase-internal eval) | 24.842 | 37.460 | 28.384 | 32.940 |
| wallclock | 23m40s | 8m24s | 10m37s | 5m03s |
| final splats | 79,239 | ~260k | 237,920 | 108,704 |
| GPU (TDP) | M4 Max (40W) | V100 (250W) | V100 (250W) | V100 (250W) |
| 抽象レイヤ | native Metal | wgpu→Vulkan | CUDA native (paper) | CUDA native (PyTorch ext) |
| Eval convention | (val 100) white-bg | brush internal | α-masked render vs raw gt | val 100 white-bg |
(1) 純粋抽象差分の対比対象が初めて揃った: orig 3DGS (CUDA native、paper baseline) と brush (wgpu→Vulkan) は同 V100 GPU・同 30k iter で動かしたので、原理的には wallclock の差 = wgpu 抽象オーバーヘッド を抽出可能。実測:
wgpu (brush) が CUDA native (orig) より 約 21% 速い。これは「wgpu 抽象は重い」という素朴予想を再度反証。原因はおそらく trainer 側の実装最適化レベル差 (brush は kernel fusion / dispatch overhead 削減が orig より進んでいる)。即ち m4-brush-bench.md で M4 Max 上で観察したのと同じ「抽象コスト ≪ 実装最適化レベル」が V100 でも観察された。
(2) PSNR は brush > gsplat > orig (同 V100、同 val 100、同 30k): 抽象レイヤの薄さと PSNR の絶対値は無相関どころか 逆相関 している。原因: trainer recipe (densification、refine schedule、capacity bound、eval methodology) が dominant。orig 3DGS が最低なのは off-object floater が white-bg eval 下で penalize されて codebase eval (28.4 dB) にも影響、§6 参照。
(3) gsplat vs orig 3DGS (同 V100、同 CUDA native): 両者とも CUDA native だが gsplat は simple_trainer minimal、orig は l1+ssim+fused_ssim+exposure compensation。それでも gsplat が +4.5 dB 上回るのは gsplat の DefaultStrategy が NeRF Synthetic の eval (white-bg) と整合的に train するため。orig 3DGS 上流の image *= alpha_mask 損失は eval/train で convention 差を作る (本 .md §6)。
(4) 卒論 Chapter 4.1/4.2 への含意:
/home/otake_26/
├── miniconda3/envs/orig3dgs-env/ ← c33 構築、c32 で sm_70 再 build 済 (c33 で要再 build)
├── repos/gaussian-splatting/ ← graphdeco-inria HEAD 54c035f
│ ├── scene/dataset_readers.py ← np.byte → np.uint8 修正済 (in-place、.bak 保持)
│ ├── train.py ← eval ブロックに alpha_mask 適用 patch (§3.2)
│ └── submodules/{diff-gaussian-rasterization,simple-knn,fused-ssim}/
├── datasets/nerf_synthetic/lego/
│ ├── transforms_{train,val,test}.json ← .png strip 済
│ ├── transforms_{train,val,test}.json.bak ← オリジナル保持
│ └── (test/ 実体は 36/200 frame しか無いが、transforms_test.json は val を copy)
├── runs/orig3dgs-lego-1k-smoke/ ← smoke 結果
└── runs/orig3dgs-lego-30k/ ← 本 bench
├── point_cloud/iteration_30000/point_cloud.ply (237,920 vert, 17 MB)
├── cameras.json / cfg_args / exposure.json / input.ply
└── train.log
| # | 失敗 | 原因 | 対処 |
|---|---|---|---|
| F1 | MemoryError: std::bad_alloc cudaErrorMemoryAllocation (simple_knn distCUDA2) | c33 で build した sm_86 .so を V100 (sm_70) で load → kernel ABI mismatch、distCUDA2 への引数が garbage に解釈されて 21 TB alloc 要求 | TORCH_CUDA_ARCH_LIST=7.0 で 3 拡張を --force-reinstall --no-build-isolation --no-deps 再 build |
| F2 | TypeError: Cannot handle this data type: (1, 1, 3), |i1 (PIL fromarray) | numpy 2.4 + Pillow 12.2 で np.byte (signed int8) が RGB image として拒否 | dtype=np.byte → np.uint8 |
| F3 | FileNotFoundError: ./train/r_0.png.png | dataset 配布元差異で file_path に既に .png が含まれ、loader が再度 .png を append | 3 split 全部から .png を strip |
| F4 | smoke 1k で PSNR 3.05 dB (loss 0.05 / 学習自体は順調) | eval ブロックで render に α マスク未適用、白背景 render vs 黒背景 gt の直接 L2 | eval ブロックに image *= alpha_mask 2 行追加 |
| F5 (finding) | 30k で codebase eval 28.4 dB vs paper-std eval 14.6 dB の乖離 | train loss が α-mask render なので off-object floater が学習中 penalize されず、white-bg eval で目立つ | §6 で finding として記録、修正は scope 外 |
ssh matsudalab-c32 << 'EOF'
source ~/miniconda3/etc/profile.d/conda.sh
conda activate orig3dgs-env
export CUDA_VISIBLE_DEVICES=1
cd ~/repos/gaussian-splatting
python train.py \
-s ~/datasets/nerf_synthetic/lego \
--model_path ~/runs/orig3dgs-lego-30k \
--iterations 30000 --eval --white_background \
--test_iterations 5000 10000 15000 20000 25000 30000 \
--save_iterations 30000 --disable_viewer
EOF
image *= alpha_mask を除去し bg=white で raw 比較、PSNR が 35 dB まで上がるか検証 (A.3 phase 2e 候補)final-ablation-table.mdc32-gsplat-smoke.mdm4-brush-bench.mdc33-cuda-setup-notes.md