nerfstudio splatfacto — 3D Gaussian Splatting (1×L4)
Train a 3D Gaussian Splat with nerfstudio on an L4.
Hardware: gpu_1x_l4. Save the files under Source files into a folder named
nerfstudio-splatfacto, then follow the steps below.
Trains a 3D Gaussian Splatting model with nerfstudio's
splatfacto on the canonical 3DGS "truck" capture, then exports the .ply
splat and renders a fly-through video + a still. It's a self-contained workload
that proves the gsplat CUDA stack — JIT kernel compilation and the
forward/backward rasterizer — works end to end on supercomputer. About 15–25 min on a
single L4 (dominated by the one-time gsplat kernel compile and the dataset
download; the actual 2000-iter train is ~1 min).
No docker: torch + nerfstudio are baked into the image via supercomputer.json, and
training/export/render all run inside the session.
Files
supercomputer.json—nvidia/cuda:12.1.1-develbase; image-build step pip-installs the verified stack (torch 2.1.2+cu121,nerfstudio 1.1.5,gsplat 1.4.0,numpy<2), SKUgpu_1x_l4. SetsTORCH_CUDA_ARCH_LIST=8.9(L4 = Ada) and the GPU-env fixups below.rescale_colmap.py— the tandt dataset ships full-res COLMAP intrinsics with half-res images; this rescalescameras.binto match so the native images load without upscaling.run.sh— verifies the GPU, downloads the dataset, rescales intrinsics, trains splatfacto, exports the splat, and renders. Writes outputs to/workspace/out:splat/splat.ply— the Gaussian splat (~460k Gaussians)nerfstudio_truck.mp4— fly-through of the trained modelnerfstudio_render.jpg— a single rendered still (the smoke pass-signal, written last so a partial run never looks successful)
Run
cd examples/nerfstudio-splatfacto
super create --sku gpu_1x_l4 # → s-xxxx
super run . --script run.sh --on s-xxxx
super download s-xxxx /workspace/out/splat/splat.ply ./splat.ply
super download s-xxxx /workspace/out/nerfstudio_truck.mp4 ./truck.mp4
super stop s-xxxxOpen splat.ply in any 3DGS viewer (e.g. https://superspl.at/editor).
Notes
- Tune iterations via env:
ITERS=7000 super run . --script run.sh --on s-xxxx(7000 is a noticeably sharper splat; 2000 is the smoke default). - GPU env fixups (set in
supercomputer.json, re-exported inrun.sh):TORCHDYNAMO_DISABLE=1because Triton/inductor can't find the runtime-injectedlibcuda.so; andLD_LIBRARY_PATH=/opt/nvidia/lib64so the driver lib resolves to the live host driver rather than a stale stub. - gsplat compiles its kernels on first use (~10–15 min) into
TORCH_EXTENSIONS_DIR. Smoke sessions are ephemeral so this recurs each run; baking a precompiled extension into the image is a worthwhile follow-up. - Dataset is Inria's public
tandt_db.zip— no HF token, no gdown quota issues.
Source files
rescale_colmap.py
"""Rescale a COLMAP cameras.bin so its intrinsics match the actual on-disk image
resolution.
The 3DGS tandt/db datasets ship COLMAP intrinsics computed on full-resolution
images but an images/ folder containing half-resolution copies, so nerfstudio's
data loader aborts with "size of image ... does not match the camera parameters".
This scales each camera's focal length + principal point by actual/colmap so the
native (smaller) images are used as-is — no upscaling, no quality loss.
"""
from __future__ import annotations
import sys
from pathlib import Path
from PIL import Image as PILImage
from nerfstudio.data.utils.colmap_parsing_utils import (
Camera,
read_cameras_binary,
read_images_binary,
write_cameras_binary,
)
def main(scene: Path) -> None:
sparse = scene / "sparse" / "0"
images_dir = scene / "images"
cams = read_cameras_binary(sparse / "cameras.bin")
imgs = read_images_binary(sparse / "images.bin")
# One representative image filename per camera id, to read true pixel dims.
cam_to_file: dict[int, str] = {}
for im in imgs.values():
cam_to_file.setdefault(im.camera_id, im.name)
new_cams = {}
for cid, cam in cams.items():
w_actual, h_actual = PILImage.open(images_dir / cam_to_file[cid]).size
sx = w_actual / cam.width
sy = h_actual / cam.height
if abs(sx - 1) < 1e-6 and abs(sy - 1) < 1e-6:
new_cams[cid] = cam
print(f"cam {cid}: already matches ({w_actual}x{h_actual})")
continue
p = list(cam.params)
if cam.model in ("SIMPLE_PINHOLE", "SIMPLE_RADIAL", "RADIAL"):
# f, cx, cy, [k...]
p[0] *= sx
p[1] *= sx
p[2] *= sy
elif cam.model in ("PINHOLE", "OPENCV", "FULL_OPENCV", "OPENCV_FISHEYE"):
# fx, fy, cx, cy, [dist...]
p[0] *= sx
p[1] *= sy
p[2] *= sx
p[3] *= sy
else:
raise SystemExit(f"unhandled COLMAP camera model: {cam.model}")
new_cams[cid] = Camera(id=cam.id, model=cam.model, width=w_actual, height=h_actual, params=p)
print(f"cam {cid} [{cam.model}]: {cam.width}x{cam.height} -> "
f"{w_actual}x{h_actual} (scale {sx:.4f},{sy:.4f})")
write_cameras_binary(new_cams, sparse / "cameras.bin")
print(f"wrote rescaled {sparse / 'cameras.bin'}")
if __name__ == "__main__":
main(Path(sys.argv[1]))run.sh
#!/usr/bin/env bash
# Train a 3D Gaussian Splatting model (nerfstudio splatfacto) end-to-end INSIDE an
# supercomputer session — proves the gsplat CUDA stack (JIT kernel compile + rasterization)
# works on supercomputer. Downloads the canonical 3DGS "truck" capture, trains splatfacto,
# exports the .ply splat, and renders a fly-through video + a still. See README.md.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT=/workspace/out
SCENE=/workspace/data/tandt/truck
ITERS="${ITERS:-2000}" # smoke: enough for a coherent splat in ~1 min of compute
# Soak knob (stress tier): when SUPERCOMPUTER_STRESS_DURATION_S is set, keep training
# fresh splatfacto rounds until that many seconds have passed since this script
# started; only the last round is exported/rendered, so the pass-signal
# artifact appears only at the end. Unset/0 = one round (daily smoke).
STRESS_END=$(( $(date +%s) + ${SUPERCOMPUTER_STRESS_DURATION_S:-0} ))
# gsplat JIT-compiles its CUDA kernels on first use; Triton/torch.compile is disabled
# (it can't find the runtime-injected libcuda.so) and the driver lib path is fixed up.
# These are also baked into supercomputer.json's image env — re-exported here so a manual
# `bash run.sh` outside the smoke harness behaves identically.
export TORCHDYNAMO_DISABLE=1
export TORCH_EXTENSIONS_DIR=/workspace/.torch_ext
export LD_LIBRARY_PATH="/opt/nvidia/lib64:${LD_LIBRARY_PATH:-}"
export TRITON_LIBCUDA_PATH=/opt/nvidia/lib64
# Bound the gsplat JIT build: torch cpp_extension defaults MAX_JOBS to the
# visible CPU count (10), and parallel nvcc jobs over torch headers OOM-kill
# the build on a gpu_1x_l4 box (ninja exit 137, nvcc "Killed") — that host
# has 16 GB RAM and the heavy backward kernels peak ~3.5 GB per job,
# so even 4 jobs died. Two jobs stay well inside memory; the 26-file build
# takes a few minutes longer and the run still fits its timeout.
export MAX_JOBS="${MAX_JOBS:-2}"
mkdir -p "$OUT" /workspace/data
# Fail loudly if torch can't see the GPU (the whole point of this example).
python3 -c "import torch; assert torch.cuda.is_available(), 'CUDA not available'; \
print('gpu:', torch.cuda.get_device_name(0))"
# 1. Dataset: the 3DGS tandt 'truck' capture — BAKED into the image at build
# (supercomputer.json), so there is NO run-time dependency on repo-sam.inria.fr, whose
# TLS cert expired 2026-08-30 (and whose host was dropping the 682 MB transfer).
# The bake extracts only tandt/truck to /opt; copy it in writable because
# rescale_colmap.py edits the COLMAP intrinsics in place.
BAKED_TRUCK=/opt/tandt-data/tandt/truck
if [ ! -d "$SCENE/images" ]; then
[ -d "$BAKED_TRUCK/images" ] || {
echo "FATAL: baked truck dataset missing at $BAKED_TRUCK — rebuild the image (supercomputer.json bakes it)" >&2
exit 1
}
mkdir -p "$(dirname "$SCENE")"
cp -a "$BAKED_TRUCK" "$SCENE"
fi
# 2. tandt ships full-res COLMAP intrinsics with half-res images — rescale to match.
python3 "$HERE/rescale_colmap.py" "$SCENE"
# 3. Train splatfacto (gsplat compiles kernels on first run, then trains). The
# soak loop trains round after round — dataset and compiled kernels are
# reused — pruning each finished round so hours of soak can't fill the disk.
round=0
while :; do
ns-train splatfacto \
--data "$SCENE" \
--output-dir /workspace/outputs \
--experiment-name truck --timestamp "smoke-r$round" \
--max-num-iterations "$ITERS" \
--vis tensorboard --viewer.quit-on-train-completion True \
colmap --colmap-path sparse/0
if [ "$round" -gt 0 ]; then
rm -rf "/workspace/outputs/truck/splatfacto/smoke-r$((round - 1))"
fi
CONFIG="/workspace/outputs/truck/splatfacto/smoke-r$round/config.yml"
round=$((round + 1))
if [ "$(date +%s)" -ge "$STRESS_END" ]; then break; fi
done
if [ "$round" -gt 1 ]; then echo "soak: trained $round rounds of $ITERS iterations"; fi
# 4. Export the Gaussian splat (.ply) — open it in any 3DGS viewer (e.g. supersplat).
ns-export gaussian-splat --load-config "$CONFIG" --output-dir "$OUT/splat"
# 5. Render the trained model from every training view and stitch a fly-through mp4.
ns-render dataset --load-config "$CONFIG" --output-path "$OUT/frames" \
--split train --rendered-output-names rgb
ffmpeg -y -framerate 20 -pattern_type glob -i "$OUT/frames/train/rgb/*.jpg" \
-vf scale=978:546 -c:v libx264 -pix_fmt yuv420p -crf 20 "$OUT/nerfstudio_truck.mp4"
# 6. Primary artifact LAST (the smoke runner's pass signal): a single rendered still
# of the trained splat. Written only after everything above succeeds.
frames=("$OUT"/frames/train/rgb/*.jpg)
cp "${frames[$((${#frames[@]} / 2))]}" "$OUT/nerfstudio_render.jpg" # a mid-sequence view
echo "done: $(ls -1 "$OUT"/splat/*.ply "$OUT"/nerfstudio_truck.mp4 "$OUT"/nerfstudio_render.jpg)"supercomputer.json
{
"sku": "gpu_1x_l4",
"image": {
"base": "nvidia/cuda:12.1.1-devel-ubuntu22.04",
"apt": ["git", "build-essential", "ninja-build", "ffmpeg", "wget", "python3", "python3-pip", "python3-dev",
"libegl1", "libgl1", "libgomp1"],
"run": [
"python3 -m pip install --upgrade pip",
"python3 -m pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121",
"python3 -m pip install numpy==1.26.4 nerfstudio==1.1.5",
"mkdir -p /opt/tandt-data && cd /tmp && wget --no-check-certificate --continue --tries=30 --waitretry=5 --timeout=60 -q -O tandt_db.zip https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/datasets/input/tandt_db.zip && echo '816e62f22a161abbfe841d2a6b10cdf036e297c9fa289b3bfeee9c6ec526d7e1 tandt_db.zip' | sha256sum -c - && python3 -c \"import zipfile; z=zipfile.ZipFile('tandt_db.zip'); [z.extract(n, '/opt/tandt-data') for n in z.namelist() if n.startswith('tandt/truck/')]\" && rm -f tandt_db.zip && test -d /opt/tandt-data/tandt/truck/images"
],
"env": {
"PYTHONUNBUFFERED": "1",
"CUDA_HOME": "/usr/local/cuda",
"TORCH_CUDA_ARCH_LIST": "8.9",
"TORCH_EXTENSIONS_DIR": "/workspace/.torch_ext",
"TORCHDYNAMO_DISABLE": "1",
"LD_LIBRARY_PATH": "/opt/nvidia/lib64",
"TRITON_LIBCUDA_PATH": "/opt/nvidia/lib64"
}
}
}