supercomputer
Examples

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-devel base; image-build step pip-installs the verified stack (torch 2.1.2+cu121, nerfstudio 1.1.5, gsplat 1.4.0, numpy<2), SKU gpu_1x_l4. Sets TORCH_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 rescales cameras.bin to 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 model
    • nerfstudio_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-xxxx

Open 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 in run.sh): TORCHDYNAMO_DISABLE=1 because Triton/inductor can't find the runtime-injected libcuda.so; and LD_LIBRARY_PATH=/opt/nvidia/lib64 so 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"
        }
    }
}

On this page