松田研究室
FindingID: c32-orig3dgs-bench
Status: stable / task A.3
← Findings Index
Phase 2c · orig 3DGS V100 bench

c32 V100 原著 3DGS 30k bench — A.5 三層対比表の最終 row & eval convention 乖離 finding

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 問題を確定。

mixedrank: highinvestigative第 2 軸phase-2original-3dgsv100c32cudabencheval-conventionabstraction-cost
Δ PSNR
28.384
Δ Wallclock
10m37s
Δ Splats
237,920
Impact
原著 3DGS を V100 で 30k 学習 (PSNR 28.38 dB / 10m37s / 237k splats)。同 V100・同 30k で brush (wgpu→Vulkan) 8m24s / 37.46 dB を上回れず、抽象コスト ≪ 実装最適化レベル を CUDA 機でも再確認。さらに codebase eval と paper-standard eval で 12 dB 乖離 (28.4 vs 14.6) を発見、A.5 表は eval convention 注記必須。
01

本文

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 注記が必須となった。

実施日
2026-05-23
対象機
matsudalab-c32 (compute-0-32, V100-PCIE-32GB × 2, driver 555.42.02, CUDA 12.1)
GPU 占有
GPU0 = yamada さん diffusion train (100% util)、GPU1 = free / 32 GB → CUDA_VISIBLE_DEVICES=1
repo
graphdeco-inria/gaussian-splatting @ 54c035f
env
~/miniconda3/envs/orig3dgs-env (torch 2.4.1+cu121、Python 3.11)
extensions
diff-gaussian-rasterization / simple-knn / fused-ssim を sm_70 で再 build
dataset
~/datasets/nerf_synthetic/lego (NFS、test split 物理欠損 164/200 → val を test に substitute)

TL;DR

項目
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 @ 30k10m 37s (train、init pcd 含む) / 全体 11m 07s
final splats237,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) を使うが、両方を併記する。

c33 NFS 副作用

orig3dgs-env の 3 拡張 wheel が sm_70 で上書き済。c33 (A6000, sm_86) で再使用するには TORCH_CUDA_ARCH_LIST='7.0;8.6' でマルチ sm 再 build か、8.6 単独で上書きが必要 (将来作業)。

1. setup 経緯と c33 (A6000) 構築済 env からの差分

phase 1 で c33 に build した orig3dgs-env は NFS 共有なので、c32 側でも conda activate するだけで Python パッケージは即動く。問題は CUDA 拡張の sm 互換性:

  • gsplat は JIT build なので TORCH_CUDA_ARCH_LIST=7.0 を export すれば V100 用 .so を初回 build cache する (c32-gsplat-smoke.md 参照、93s)。
  • orig 3DGS の 3 拡張 (diff-gaussian-rasterization / simple-knn / fused-ssim) は AOT build wheel。c33 で build したものは -gencode arch=compute_86,code=sm_86 のみで、sm_70 (V100) で kernel launch すると symptom が "OOM 21 TB" という形で発現 (kernel ABI mismatch の garbage launch parameter)。
  • 対処: c32 側で 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 の distCUDA2no 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 はスコープ外)。

2. dataset 問題と val 代替

NeRF Synthetic Lego の test/ 200 frame のうち 164 frame がローカル NFS rsync で欠損していた (phase 2a の gsplat smoke 時点で既知)。原著 3DGS の readNerfSyntheticInfoeval flag に関係なく test 用 readCamerasFromTransforms を呼び、200 frame 全部 Image.open するため、欠損 = train.py が起動できない。

対処: transforms_val.jsontransforms_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)。readCamerasFromTransformspath + 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 層対比に使う。

3. 上流コードの非互換 2 件 (smoke で踏んだ)

3.1 PIL の np.byte 拒否 (numpy 2.x + Pillow 12.x)

scene/dataset_readers.py:259Image.fromarray(np.array(arr*255.0, dtype=np.byte), "RGB")TypeError: Cannot handle this data type: (1, 1, 3), |i1np.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")

3.2 --white_background モードでの eval PSNR が誤計算

これは原著 3DGS の eval 実装が --white_background Blender 系に対し未対応:

  • readCamerasFromTransforms は alpha compose で白背景合成した RGB PIL image を作るが、utils/camera_utils.py:21loadCamImage.open(image_path) で原 PNG を直接再 open、RGBA を読む。scene/cameras.py:42-46gt_image = resized_image_rgb[:3, ...] (RGB チャネル = 黒背景の raw RGB) + alpha_mask = resized_image_rgb[3:4, ...] (アルファ別保持)。
  • 訓練ループ (train.py:114-116) では image *= alpha_mask で render を α マスクしてから loss を取るため正しく学習が進む。
  • しかし eval ブロック (train.py:230-241) で α マスクが当たっておらず、render (白背景) と gt_image (黒背景) を直接比較。1k 段階で PSNR 3.05 dB と異常値 (実態は loss 0.05 で学習自体は順調)。

Patch 3.2

# 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 参照。

4. smoke (1k iter) 結果

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
iterL1 (test=val)PSNR (test=val)
5000.042319.984 dB
10000.022523.583 dB

wallclock 1k iter ≈ 30s (含む dataset load + random 100k init pcd の knn)。

5. 30k full bench

コマンド

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 &

Eval history (test=val、α マスク render vs raw gt)

iterL1 (test)PSNR test (dB)PSNR train sample (dB)
50000.0089527.78527.415
100000.0075128.34228.012
150000.0072228.40228.194
200000.0072228.38628.279
250000.0071528.41728.329
300000.0072028.38428.353

→ 15k 以降ほぼ完全に plateau。densify_until_iter=15000 (default) で refine 停止後は flat。

Wallclock

区分時刻
Output folder 作成03:06:20
Train start (init pcd 完了)03:06:50
Train end (iter 30000 save)03:17:24
プログラム終了03:17:27
  • 30k 純 train wallclock = 10m 32s (tqdm 最終 line)
  • train + init = 10m 37s
  • total (dataset load 込み) = 11m 07s
  • iter rate: 30〜50 it/s (densify 中) → 50〜52 it/s (refine 停止後)

Final splats

  • saved point cloud: ~/runs/orig3dgs-lego-30k/point_cloud/iteration_30000/point_cloud.ply
  • vertex count = 237,920 (random 100k init から +138k densify)

6. 重要 finding — codebase eval vs paper-standard eval の乖離

iter 30000 checkpoint の保存後、4 通りの eval ストラテジで val 100 frame の PSNR を比較したところ:

Variant説明PSNR (dB)L1
A. codebase 修正版render *= alpha mask、gt 生 RGB28.3840.00720
B. paper-standardrender 生 (bg=white)、gt += (1-α)*white14.5980.08848
C. 両方白合成render 生 + gt 白合成 (= B と同じ、render bg=white なので)14.5980.08848
D. 上流 unmodifiedrender 生 vs gt 生 RGB (train.py original eval)2.5120.61280

paper 提示 (Kerbl Table 2) の Lego 30k = 34.5 dB に対し、本実装の A は -6.1 dB、B は -19.9 dB

原因解析 (iter 30000 の test camera 0 の中央エリア統計):

エリアrender 平均 RGBgt 平均 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 の枠で並べる。

この finding の卒論への価値

Apple Silicon native vs CUDA 三層対比」の比較を取る際に、ナイーブに gsplat / brush / orig 3DGS を並べてしまうと:

  • orig 3DGS が paper 値より大きく下がる (28.4 vs 34.5、gsplat 32.94 が orig より高い)
  • 読者は「原著実装が gsplat より悪い」と誤読する

実際には eval convention の選択 で 28.4 〜 14.6 〜 2.5 dB の 12 dB 幅の振れが生じる、というのが本来の事実。卒論 Chapter 4 ではこの convention 依存性 を最初に明示し、3 層対比は「同 convention 内での比較」に限定する必要がある。これは A.3 phase 2 全体の methodological prerequisite finding と位置付けられる。

7. 三層対比表 (A.5 §第 1 軸 完成)

自作 (M4 Max)brush wgpu→Vulkan (V100)orig CUDA (V100)gsplat CUDA (V100)
PSNR (dB) (codebase-internal eval)24.84237.46028.38432.940
wallclock23m40s8m24s10m37s5m03s
final splats79,239~260k237,920108,704
GPU (TDP)M4 Max (40W)V100 (250W)V100 (250W)V100 (250W)
抽象レイヤnative Metalwgpu→VulkanCUDA native (paper)CUDA native (PyTorch ext)
Eval convention(val 100) white-bgbrush internalα-masked render vs raw gtval 100 white-bg

第 1 軸 + 第 2 軸 finding

(1) 純粋抽象差分の対比対象が初めて揃った: orig 3DGS (CUDA native、paper baseline) と brush (wgpu→Vulkan) は同 V100 GPU・同 30k iter で動かしたので、原理的には wallclock の差 = wgpu 抽象オーバーヘッド を抽出可能。実測:

  • brush wgpu→Vulkan: 8m24s
  • orig CUDA: 10m37s
  • gsplat CUDA: 5m03s
意外な順序

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 への含意:

  • 表は「3 軸のキャプチャレベル + eval convention をどう取るかで 4.5 dB / 5 分単位で結果が変わる」という事実を最初に明示する
  • 純抽象差分を取りたい場合は「同 GPU + 同 trainer + 同 convention で渡したい kernel API だけを差し替える」という新しい control variable が必要 (例: brush の trainer を CUDA backend で動かすか、gsplat の trainer を wgpu に移植する)
  • 本卒研の M-3.x baseline (自作 native Metal、24.84 dB) は「同 eval convention で比較すれば brush native Metal 37.40 dB に対し -12.6 dB のギャップが本研究の trainer 最適化余地」という Chapter 4 の主張は変わらない

8. リモートファイル (c32 / NFS 共有)

/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

9. 失敗箇所と対処 (sm_70 ABI 含む)

#失敗原因対処
F1MemoryError: 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
F2TypeError: 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
F3FileNotFoundError: ./train/r_0.png.pngdataset 配布元差異で file_path に既に .png が含まれ、loader が再度 .png を append3 split 全部から .png を strip
F4smoke 1k で PSNR 3.05 dB (loss 0.05 / 学習自体は順調)eval ブロックで render に α マスク未適用、白背景 render vs 黒背景 gt の直接 L2eval ブロックに 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 外

10. 再現コマンド (one-liner)

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

11. 残課題

  1. c33 (A6000) で orig3dgs-env を sm_86 再 build — 副作用解消 (or マルチ sm wheel に統一)
  2. paper-standard eval (B 方式) で trainer を回す ablationimage *= alpha_mask を除去し bg=white で raw 比較、PSNR が 35 dB まで上がるか検証 (A.3 phase 2e 候補)
  3. 3 層対比表の wallclock 解釈 — brush 8m24 / orig 10m37 / gsplat 5m03 のうち、純粋抽象コストを取り出す制御実験 (Chapter 4.2 詳細課題)

12. 関連

  • A.5 final ablation: final-ablation-table.md
  • gsplat V100 bench: c32-gsplat-smoke.md
  • brush M4 Max bench: m4-brush-bench.md
  • CUDA env setup: c33-cuda-setup-notes.md
  • 関連 memory: [[research_direction]], [[research_plan_doc]]
03

関連 finding

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