NW' Blog
August 13, 2026 · Content page   |   📝Edited on August 13, 2026

3DGS Training Full Pipeline Notes
(NVIDIA P104-100)

RealityScan 2.2 mobile photogrammetry → export to CSV / COLMAP format → 3D Gaussian Splatting (3DGS) modeling. A complete record of why the NerfStudio route is a dead end on the P104, how to set up the original 3DGS (INRIA) environment, plus a full step-by-step tutorial from data preparation to training verification and the measured results.

3DGS training preview

1. Preface

Project background: RealityScan 2.2 mobile photogrammetry → export to CSV / COLMAP format → 3D Gaussian Splatting (3DGS) modeling. This article covers: why the NerfStudio route is a dead end on the P104, how to set up the original 3DGS (INRIA) environment, a complete step-by-step tutorial from data preparation to training verification, and the actual measured results on the P104.

Companion document: "RealityScan2.2-NerfStudio-CSV Debugging Notes" (troubleshooting during the data conversion stage).
Companion scripts: fix_rc_csv.py, colmap_txt2bin.py, prepare_gs_data.py, build_all.bat, build_dgr.bat.

1.1 Data pipeline

Data flow
RealityScan 2.2 alignment (16 mobile photos)
  ├─ export 3.csv          → camera poses / intrinsics / distortion (Internal/External camera parameters)
  ├─ export images/        → matching COLMAP images (00000.png, already renamed)
  └─ export sparse/0/      → COLMAP sparse reconstruction (text format txt)

Final output of this pipeline: the trained 3DGS Gaussian point cloud (point_cloud.ply) + rendered images

1.2 Environment

ItemValue
GPUNVIDIA P104-100 (8 GB, Pascal architecture sm_61)
Driver / CUDA582.53 / system CUDA Toolkit 11.8 (nvcc 11.8.89)
CompilerVisual Studio 2022 Community (MSVC 14.44)
Python3.10.11
torch / torchvision2.4.1+cu118 / 0.19.1+cu118
Training frameworkgaussian-splatting (INRIA, main + diff-gaussian-rasterization dr_aa branch)
Training data16 images, 0.5x resolution (≈2250×1000)

1.3 Document structure

  • Chapter 2: Problems (why the NerfStudio route is blocked, all the issues hit during training)
  • Chapter 3: Solution approach and option selection
  • Chapter 4: Step-by-step tutorial (can be followed as-is)
  • Chapter 5: Measured training results on the P104
  • Chapter 6: Next steps; Chapter 7: Pitfall list; Chapter 8: Appendix

2. Problems

2.1 Core problem: NerfStudio's splatfacto cannot run on the P104

At the core of 3D Gaussian Splatting is a custom CUDA rasterizer; NerfStudio 1.1.5's splatfacto uses the open-source library gsplat. Investigation confirmed:

  • The gsplat source build configuration explicitly comments: build against architectures >= 7.0 (required by cooperative_groups::labeled_partition in gsplat);
  • cooperative_groups::labeled_partition is an independent thread scheduling feature introduced with Volta (sm_70) and does not exist on Pascal (sm_60/61) hardware;
  • The official prebuilt wheels only contain cubins for sm_70/75/80/86/90, verifiable via cuobjdump -elf gsplat/csrc.pyd; even building it yourself cannot bypass the code-level dependency.

Conclusion: splatfacto requires a GPU architecture ≥ sm_70 (e.g. T4, V100, RTX 20 series+); the P104-100 (sm_61) has no choice but to switch approaches.

2.2 Problems encountered during training (overview)

Even with the original 3DGS, setting up the training environment was a chain of cascading problems (see the pitfall list in Chapter 7):

#ProblemCause in one line
1torch.cuda.is_available()=FalseThe CPU build of torch was installed
2gsplat no kernel image availableSee 2.1; hardware not supported
3CUDA version (11.8) mismatches PyTorch (12.4)torch's CUDA version does not match the system nvcc
4STL1002: expected CUDA 12.4 or newerThe new STL in MSVC 14.44 rejects the older CUDA
5unsupported Microsoft Visual Studio versionCUDA 11.8 does not recognize MSVC 14.44
6cv2 _ARRAY_API not foundnumpy was accidentally upgraded to 2.x
7colmap images.bin parsed as garbageqvec/tvec mistakenly read as float32, should be float64
8antialiasing parameter errorThe rasterizer branch does not match the main repo code
9Backslashes lost from pathsA bash escaping issue

3. Solutions (option selection)

OptionFeasibilityVerdict
splatfacto (NerfStudio) + P104sm_61 not supported by the hardware
splatfacto + cloud GPU (T4 etc.)Feasible, but requires extra resourcesFallback
Original 3DGS (INRIA gaussian-splatting)The CUDA rasterizer code is Pascal-compatible, widely verified by the community on GTX 10 series cards✅ Adopted

Reasons for choosing the original 3DGS:

  1. Its diff-gaussian-rasterization rasterizer was written in 2023 (the CUDA 11 era) and supports Pascal well;
  2. Our data already came in the COLMAP format the original 3DGS needs (exported from RealityScan), no need to redo feature matching/reconstruction;
  3. The sparse point cloud (23513 points) can directly initialize the Gaussians, making training faster and more stable.

3.1 Overall approach

Pipeline
Environment alignment (torch cu118 ↔ nvcc 11.8, numpy<2)
   → Get the source (main + submodules, dr_aa branch + glm)
   → Build the three CUDA extensions (MSVC + nvcc, bypassing the two-way version checks)
   → Data preparation (colmap txt→bin; images and intrinsics scaled 0.5x)
   → Training (7000-step verification) → Render verification (PSNR)

4. Step-by-Step Tutorial

4.0 Environment checks

bash
# GPU and architecture
nvidia-smi --query-gpu=name,compute_cap,memory.total --format=csv
# Expected output: NVIDIA P104-100, 6.1, 8192 MiB

# CUDA toolchain (nvcc must exist)
nvcc --version

# Python dependency status
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
python -c "import numpy; print(numpy.__version__)"

# MSVC toolchain (just confirm cl.exe exists; the path is inside the VS install directory)
ls "/c/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/"

4.1 Aligning the Python environment

Key rule: torch's CUDA version must match the nvcc version, otherwise torch cpp_extension refuses to compile outright.

bash
# The system nvcc is 11.8 → install the cu118 build of torch (must use the official PyTorch index; the default index installs the CPU build!)
python -m pip install torch==2.4.1 torchvision==0.19.1 \
  --index-url https://download.pytorch.org/whl/cu118

# numpy must be <2 (binary extensions such as open-cv depend on numpy 1.x)
python -m pip install "numpy==1.26.4"

# Verify
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"   # 2.4.1+cu118 True
python -c "import numpy; print(numpy.__version__)"                              # 1.26.4

⚠️ pip install --force-reinstall also upgrades dependencies from the default index along the way (numpy→2.x, torch→CPU build) — a trap hit twice. Always double-check the versions afterwards.

4.2 Getting the source

Direct GitHub access (especially over the git protocol) is extremely unstable in this environment; downloading zips through the ghfast.top mirror is recommended:

bash
cd /d/temp

# Main repo (main branch)
curl -L --retry 10 -o gs.zip "https://ghfast.top/https://github.com/graphdeco-inria/gaussian-splatting/archive/refs/heads/main.zip"
unzip -q gs.zip && mv gaussian-splatting-main gaussian-splatting

# rasterizer: must use the dr_aa branch (the main repo code passes the antialiasing parameter, which the main branch does not expose)
curl -L --retry 10 -o dgr.zip "https://ghfast.top/https://github.com/graphdeco-inria/diff-gaussian-rasterization/archive/refs/heads/dr_aa.zip"
unzip -q dgr.zip
mkdir -p gaussian-splatting/submodules/diff-gaussian-rasterization
mv diff-gaussian-rasterization-dr_aa/* gaussian-splatting/submodules/diff-gaussian-rasterization/

# simple-knn lives on the INRIA GitLab (not on GitHub!)
curl -L --retry 10 -o sknn.zip "https://gitlab.inria.fr/bkerbl/simple-knn/-/archive/main/simple-knn-main.zip"
unzip -q sknn.zip
mkdir -p gaussian-splatting/submodules/simple-knn
mv simple-knn-main/* gaussian-splatting/submodules/simple-knn/

# fused-ssim (used by metrics)
curl -L --retry 10 -o fssim.zip "https://ghfast.top/https://github.com/rahul-goel/fused-ssim/archive/refs/heads/main.zip"
unzip -q fssim.zip
mkdir -p gaussian-splatting/submodules/fused-ssim
mv fused-ssim-main/* gaussian-splatting/submodules/fused-ssim/

# glm (math library dependency of the rasterizer)
curl -L --retry 10 -o glm.zip "https://ghfast.top/https://github.com/g-truc/glm/archive/refs/tags/1.0.1.zip"
unzip -q glm.zip
mkdir -p gaussian-splatting/submodules/diff-gaussian-rasterization/third_party
cp -r glm-1.0.1/* gaussian-splatting/submodules/diff-gaussian-rasterization/third_party/glm/

4.3 Building the three CUDA extensions

Build prerequisites (already satisfied in 4.0/4.1): vcvars environment, nvcc 11.8, torch cu118, numpy<2.

Modify the three setup.py files: prepend two arguments to the start of the extra_compile_args["nvcc"] list (MSVC 14.44 is too new and CUDA 11.8 too old; each flag bypasses one side's version check):

python
"-allow-unsupported-compiler", "-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH"

The finished modified script can be found in readme/build_all.bat (the setup.py of all three submodules has already had the arguments added):

build_all.bat
@echo off
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
set PYTHONIOENCODING=utf-8
set DISTUTILS_USE_SDK=1
cd /d D:\temp\gaussian-splatting\submodules\diff-gaussian-rasterization
python setup.py install
cd /d D:\temp\gaussian-splatting\submodules\simple-knn
python setup.py install
cd /d D:\temp\gaussian-splatting\submodules\fused-ssim
python setup.py install

Execution and verification:

bash
cd /d/temp/ReasonixChat/3/readme && cmd //c build_all.bat
cd /d/temp/gaussian-splatting
python -c "from diff_gaussian_rasterization import GaussianRasterizationSettings, GaussianRasterizer; print('dgr OK')"
python -c "import simple_knn; print('simple_knn OK')"
python -c "import fused_ssim; print('fused_ssim OK')"

Build time: the three extensions together take roughly 5-10 minutes (single-core ninja build).

4.4 Data preparation

The sparse/0 exported by RealityScan is in COLMAP text format, while the original 3DGS only reads the binary format, so a conversion is needed:

bash
# 1) txt → bin (16 cameras / 16 images / 23513 points)
cd /d/temp/ReasonixChat/3
python readme/colmap_txt2bin.py

# 2) Scale the images and intrinsics together to 0.5x (to fit 8GB VRAM and training speed)
#    Output: D:\temp\ReasonixChat\3\gs_data (images/ + sparse/0/*.bin)
python readme/prepare_gs_data.py 0.5

Key points of colmap_txt2bin.py:

  • cameras.bin: [id(int32), model(int32), w(int64), h(int64), params(double...)]; PINHOLE=1 has 4 parameters (fx, fy, cx, cy);
  • images.bin: qvec/tvec are float64 (double) — the easiest part to get wrong (COLMAP's official ReadImagesBinary uses 8-byte doubles); followed by camera_id(int32), name(ASCII+\0), num_points(uint64), (x,y double, point3D_id int64)...;
  • points3D.bin: [id(int64), xyz(3×double), rgb(3×uint8), error(double), track_len(int64), (image_id int32, point2D_idx int32)...].

Key points of prepare_gs_data.py: 3DGS reads images at their original resolution and has no built-in scaling; when scaling the images, fx/fy/cx/cy and w/h must be scaled by the same factor, otherwise the camera geometry breaks.

4.5 Training

bash
cd /d/temp/gaussian-splatting
PYTHONIOENCODING=utf-8 python train.py \
  -s "D:/temp/ReasonixChat/3/gs_data" \
  -m "D:/temp/ReasonixChat/3/gs_output" \
  --iterations 7000
ParameterMeaning
-sData directory (containing images/ and sparse/0/); use forward slashes in paths (backslashes get swallowed under bash)
-mOutput directory (model, checkpoints, logs)
--iterationsNumber of iterations, default 30000; the verification run uses 7000 first
--evalOptional: splits off 1/8 as a test set and reports test metrics

Excerpt from the training log (the first run prompts you to convert points3D.bin to points3D.ply; this is normal):

log
Loading Training Cameras
Number of points at initialisation :  23513
Training progress: 100%|██████████| 7000/7000 [11:21<00:00, 9.49it/s, Loss=0.0155550]
[ITER 7000] Evaluating train: L1 0.009710725769400597 PSNR 36.59599494934082
[ITER 7000] Saving Gaussians
Training complete.

4.6 Render verification

bash
cd /d/temp/gaussian-splatting
PYTHONIOENCODING=utf-8 python render.py \
  -m "D:/temp/ReasonixChat/3/gs_output" \
  -s "D:/temp/ReasonixChat/3/gs_data" \
  --iteration 7000

Output: gs_output/train/ours_7000/{renders,gt}/ and gs_output/test/ours_7000/.

Rendered results vs ground truth
Rendered results (left) vs ground truth (right) — average PSNR 37.24 dB over 16 frames

Compute the PSNR by comparing the rendered images against GT with a script:

python
import numpy as np
from PIL import Image
from pathlib import Path

r = Path("D:/temp/ReasonixChat/3/gs_output/train/ours_7000")
psnrs = []
for p in sorted((r / "renders").glob("*.png")):
    gt = np.asarray(Image.open(r / "gt" / p.name).convert("RGB"), dtype=np.float32) / 255.0
    pred = np.asarray(Image.open(p).convert("RGB"), dtype=np.float32) / 255.0
    mse = np.mean((pred - gt) ** 2)
    psnrs.append(10 * np.log10(1.0 / (mse + 1e-12)))
print(f"PSNR: mean={np.mean(psnrs):.2f} min={np.min(psnrs):.2f} max={np.max(psnrs):.2f}")

5. Measured Training Results on the P104

5.1 Training data

ItemValue
Images16, 0.5x ≈ 2250×1000 (original 4500×2000)
Sparse point initialization23513 points
Iterations7000
Training time11 min 21 s (about 9.5 it/s)
Loss convergence0.1606 → 0.0156
Training set evaluationPSNR 36.6 dB, L1 0.0097

5.2 Render verification (16 frames, compared against the input images)

MetricValue
Average PSNR37.24 dB
Min / Max30.17 / 39.92 dB

Note: this is a render comparison from training-set viewpoints (pipeline verification); the numbers are already at a good level. To evaluate generalization, split off a test set with --eval.

Training input photo
Training input: mobile photo aligned by RealityScan 2.2 (one of the 16)

5.3 Outputs

gs_output/
gs_output/
├── point_cloud/iteration_7000/point_cloud.ply   ← trained Gaussian splatting point cloud (core artifact)
├── input.ply                                     ← sparse point initialization
├── cameras.json / exposure.json                  ← camera and exposure parameters
├── train/ours_7000/{renders,gt}/                 ← rendered images and ground truth
├── test/ours_7000/
└── events.out.tfevents.*                         ← TensorBoard logs

6. Next Steps

6.1 Full training (recommended, about 50 minutes)

bash
cd /d/temp/gaussian-splatting
PYTHONIOENCODING=utf-8 python train.py \
  -s "D:/temp/ReasonixChat/3/gs_data" \
  -m "D:/temp/ReasonixChat/3/gs_output_full" \
  --iterations 30000

Output: gs_output_full/point_cloud/iteration_30000/point_cloud.ply.

6.2 Viewing and exporting

  • SIBR viewer (the official real-time viewer; needs a separate build, see the repo's SIBR_viewers/);
  • Render video directly: render.py supports --skip_train/--skip_test and custom trajectories;
  • The point cloud ply can be opened in CloudCompare / MeshLab to check quality.

6.3 Switching to a more suitable GPU (if you need the NerfStudio ecosystem)

gsplat requires sm_70+; after renting a T4 / RTX 20 series or newer GPU, output/transforms.json is ready to go, just run:

bash
PYTHONIOENCODING=utf-8 ns-train splatfacto --data output --output-dir runs \
  --experiment-name myscan --max-num-iterations 30000 --vis tensorboard \
  nerfstudio-data --downscale-factor 2

7. Pitfall List (in order of occurrence)

#SymptomCauseFix
1torch.cuda.is_available()=FalseThe default index installed the CPU build of torchInstall the cu118/cu124 wheel from the official PyTorch index
2gsplat no kernel image is available for execution on the devicegsplat does not support sm_61 (see 2.1)Switch to the original 3DGS
3DLL load failed while importing gsplat.csrcThe torch version does not match gsplat's prebuilt wheel (pt24)This path is abandoned (gsplat is no longer needed); if you insist, downgrade torch to the version the wheel expects
4The detected CUDA version (11.8) mismatches ... PyTorch (12.4)torch's CUDA does not match nvccSwitch torch to cu118 to align with nvcc 11.8
5unsupported Microsoft Visual Studio versionCUDA 11.8's host_config.h rejects MSVC 14.44Add -allow-unsupported-compiler to nvcc
6STL1002: Unexpected compiler version, expected CUDA 12.4 or newerThe new STL of MSVC 14.44 rejects the older CUDA in turnAdd -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH to nvcc
7cv2 _ARRAY_API not found / numpy.core.multiarray failed to importnumpy was upgraded to 2.x by pip along the waypip install "numpy==1.26.4"
8glm/glm.hpp: No such file or directoryThe rasterizer's third_party/glm sub-dependency is missingDownload glm and place it in third_party/glm/
9colmap binary reading throws UnicodeDecodeErrorThe qvec/tvec in images.bin were mistakenly read as float32Correct to float64 (<4d/<3d)
10GaussianRasterizationSettings ... unexpected keyword 'antialiasing'The rasterizer was built from the main branch, but the main repo code needs dr_aaSwitch to the dr_aa branch and rebuild
11Could not recognize scene typePath backslashes were swallowed by bash, so sparse detection failedPass arguments with forward slashes D:/temp/...
12A DISTUTILS_USE_SDK warning aborts the buildtorch cpp_extension requires the SDK environment to be declared explicitlyAdd set DISTUTILS_USE_SDK=1 in the bat file
13After --force-reinstall, torch became the CPU build / numpy became 2.xpip reinstalled the dependencies from the default indexUse --no-deps, or double-check the versions after installing

8. Appendix

8.1 File list

File / DirectoryDescription
readme/3DGS训练全流程记录.mdThis document
readme/RealityScan2.2-NerfStudio-CSV调试记录.mdTroubleshooting notes for the data conversion stage
readme/fix_rc_csv.pyRealityScan 2.2 CSV column name mapping (includes --zero-k4)
readme/colmap_txt2bin.pyCOLMAP text → binary conversion
readme/prepare_gs_data.pyScales images + intrinsics together, generating the 3DGS data directory
readme/build_all.batOne-click build of the three CUDA extensions
readme/build_dgr.batBuilds the rasterizer alone
gs_data/3DGS input data (0.5x)
gs_output/Training artifacts (7000 steps)
output/NerfStudio conversion output (transforms.json, kept as backup)

8.2 Theory notes

Why does the original 3DGS support Pascal while gsplat does not?

The rasterization core of 3D Gaussian Splatting organizes Gaussians per tile and performs parallel sorting/blending. The original diff-gaussian-rasterization (2023) is implemented with classic warp-level synchronization (__syncwarp / shared memory) — primitives that Pascal already has; gsplat, by contrast, brings in cooperative_groups::labeled_partition (Volta's independent thread scheduling) to achieve more efficient dynamic parallelism, at the cost of dropping Pascal. This is the classic trade-off of "the newer library is faster but pickier about hardware".

Note on the COLMAP coordinate system exported by RealityScan

  • CSV export: (E, N, U) right-handed system (x east, y north, z up / altitude);
  • COLMAP export: (E, -U, N) right-handed system (camera center = (x, -alt, y)); the two differ by a fixed rotation, verified geometrically consistent via PnP reprojection;
  • The original 3DGS consumes COLMAP poses directly, with no need to care about the absolute coordinate system (it normalizes internally during training).

8.3 Reference links


Happy Reconstructing! 🎉

Comments Leave your thoughts
Guide