ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

3DGS环境搭建全指南:CUDA、GCC与PyTorch版本对齐实战

3DGS环境搭建全指南:CUDA、GCC与PyTorch版本对齐实战

先说句实在话,3DGS的环境搭建,网上教程少说几十篇,但大多数能让你把代码装进环境,却不一定能让你真正跑通。高斯泼溅(Gaussian Splatting)这两年已经成了三维重建和新视角合成领域最常见的落地方案之一,我的实际体会是:它的门槛不在于Python调用,而在于两个自定义CUDA扩展子模块的编译,以及CUDA、GCC、PyTorch三个版本之间的微妙对齐关系。这篇内容就以环境搭建为主线,从原理、硬件选型到每个命令的执行细节,把整条链路讲透。我有过在四台不同配置机器上搭建的经验,从老旧的GTX 1660到24GB显存的RTX 3090都试过,踩过编译失败、显存溢出、COLMAP位姿估算失败等各种问题。这篇文章适合刚接触3DGS、准备在自己机器上跑通训练和渲染的同学,也适合那些装好了却频繁报错、不知道从哪下手的人。

1. 先别急着装环境:高斯泼溅到底在算什么

1.1 一句话理解3DGS的场景表示

3DGS的全称是3D Gaussian Splatting,解决的问题叫"新视角合成"(Novel View Synthesis):给你几十张从不同角度拍摄的同一物体或场景照片,算法重建出三维表示,之后就能从任意新角度实时渲染画面。NeRF类算法也能做这件事,但NeRF把场景藏在隐式神经场中,渲染一帧要沿光线做大量采样,1080p的单帧动辄几秒。3DGS换了个思路:把场景显式表示成数百万个三维高斯分布,每个高斯拥有自己的位置、旋转、缩放、透明度和球谐系数颜色,渲染时通过"泼溅"操作把这些高斯投到屏幕上按深度排序混合,一次前向即可出图。

这个思路和传统网格建模完全不同。网格建模需要先确定表面几何,再用纹理贴图填充细节,遇到毛发、树叶、玻璃这类复杂结构就很吃力。3DGS没有显式的"表面"概念,它就像在空间里撒了几百万个半透明的小光斑,一个物体表面实际上是由无数相互重叠的高斯叠出来的。每个高斯的透明度、大小、颜色都是可优化参数,训练过程就是让这堆光斑最终叠出你照片里看到的样子。

1.2 "实时"这两个字是有代价的

3DGS能达到实时渲染,是因为光栅化过程不是用PyTorch的自动微分硬算出来的,而是作者单独写了CUDA kernel。这意味着你每次渲染、反传梯度,实际都在调自定义的CUDA代码。PyTorch本身只是个框架外壳,底层的高斯投影、排序、alpha混合全部由编译好的C++/CUDA扩展提供。

这正是环境搭建复杂度的来源。NeRF用纯PyTorch实现也能跑,顶多慢点;但3DGS少了那几个CUDA扩展,连前向都做不了。凡是涉及CUDA自定义算子的项目,都逃不过编译环境对齐的问题:PyTorch自带的CUDA运行库要和驱动匹配,nvcc编译器要能找到PyTorch的CUDA头文件,宿主机上的GCC版本又不能超出CUDA支持范围。三个条件缺一个,你就只能在编译报错里打转。

1.3 搭建前先明确自己的目标

动工之前想清楚你要拿它做什么,这会直接决定硬件投入和配置策略。

如果你只是验证算法效果,拿一个普通室内场景复现训练和渲染,那显存8GB以上的中端显卡就够。如果你想做的是大规模城市场景或高分辨率建筑重建,显存和迭代策略都要重新考虑。还有一种场景是只做离线渲染、不在乎训练速度,那即使显卡弱一些,也可以靠调低分辨率跑通。我的建议是:第一次搭建不要追求满配,先拿一个拍摄简单的小物体跑通全流程,环境问题解决之后再换大场景。小场景训练时间短,排查问题方便,而且一旦暴露环境问题,重复验证成本低得多。

2. 硬件与驱动:显存、CUDA与GCC的三方博弈

2.1 显存需求到底多大

3DGS训练时显存消耗主要来自三部分:一是前向渲染时的中间张量,用于反向传播;二是COLMAP生成的稀疏点云转换成高斯初始状态时占用的参数存储;三是球谐系数、密度控制相关的辅助数组。分辨率越高、场景越大,显存压力越成倍增长。

我整理了一份参考分级,对应不同使用场景:

显卡规格显存能做什么实际体验
GTX 1660 Super / RTX 20606~8GB小物体、低分辨率(约1MP以内)、短迭代能跑但训练慢,建议图像缩放到720p
RTX 3060 12GB / RTX 4060 Ti12~16GB普通室内场景、中等分辨率可以完成完整流程,性价比之选
RTX 3090 / 409024GB大场景、高分辨率、不开缩放直接训练推荐配置,基本不用为显存焦虑

注意显存不是唯一指标。3DGS的训练瓶颈很大程度在光栅化kernel的并发效率上,核心数量和显存带宽同样重要。RTX 3090和RTX 4070在显存上差不多,但实际训练耗时差距明显,因为Ampere架构的大显存带宽摊薄了高斯排序和混合的开销。

2.2 nvidia-smi告诉你的是驱动上限

很多人在这一步就犯迷糊:到底要不要安装CUDA Toolkit?先记住一个关键区分:NVIDIA驱动和CUDA Toolkit是两回事。显卡驱动本身内置了一个CUDA运行库,nvidia-smi右上角显示的CUDA Version代表当前驱动能支持的最高CUDA版本,不是说你机器上装了那个版本的CUDA Toolkit。

PyTorch的CUDA版本是跟随PyTorch包走的,比如你用pip install torch==1.13.1+cu116,PyTorch内部就带着CUDA 11.6的运行时组件。这时候你不需要单独安装CUDA Toolkit也能跑PyTorch的基本张量运算。但编译CUDA扩展时,setup.py会通过PyTorch找到nvcc编译器,而nvcc又需要系统里有对应的CUDA头文件路径。所以实际经验是:要先确认驱动版本足够新,能让PyTorch的CUDA组件正常工作;其次确认系统里有nvcc且版本和PyTorch的CUDA版本接近。

可以用这两条命令快速摸底:

nvidia-smi nvcc --version

如果nvcc找不到,没关系,PyTorch会带上自己的nvcc路径。但如果nvidia-smi显示的驱动CUDA版本比PyTorch要求的低,那训练时大概率会报CUDA driver version is insufficient。

2.3 GCC版本为什么是个隐藏的坑

编译CUDA扩展时,nvcc负责把.cu文件编译成设备代码,但宿主代码的编译还是交给GCC/G++。CUDA对GCC版本有严格的上限要求:CUDA 11.6/11.7最多支持到GCC 9或10,CUDA 11.8支持到GCC 11,CUDA 12.x才支持GCC 12以上。

现在很多新装系统的默认GCC已经到12甚至13,如果你按照官方推荐的旧版本PyTorch+CUDA 11.6方案来搭,编译子模块时就会撞上GCC版本过老或过新的问题。过新的典型报错是unsupported GNU version或者一堆从标准库头文件里冒出来的编译错误,看着像代码问题,实际是编译器组合不对。

解决办法就两条路:要么把系统GCC降级(或者用conda在环境里装一个特定版本的GCC),要么直接选新版本的PyTorch+CUDA 12组合,配合新GCC。我在新版机器上更推荐后者,少折腾很多。

3. Conda与PyTorch:把版本对齐做在第一步

3.1 创建Python环境

我的习惯是用Conda管理Python环境,因为后面COLMAP也经常靠conda安装,一个环境里同时管理Python包和系统依赖比跨工具链方便。先创建独立环境,避免污染系统自带的Python。

conda create -n gaussian python=3.8 -y conda activate gaussian

Python版本选择3.8或3.9比较稳妥。3.10以上某些旧版本PyTorch的预编译包会有兼容性问题,3.7又低于官方仓库推荐的基线。选定环境后,顺手把pip升级一下,避免后面安装时版本解析出幺蛾子。

pip install --upgrade pip setuptools wheel

这一步很多人忽略。setuptools版本太旧会导致后面pip install submodules/...时解析不了setup.py里的Extension配置,报错内容还很迷惑。

3.2 PyTorch版本怎么选

官方仓库推荐的是PyTorch 1.13.1 + CUDA 11.6,这是最保守的选择,适合老机器和老系统。但如果你是新买的显卡(比如RTX 40系列),CUDA 11.6的nvcc可能不认识新GPU架构,编译时会报Unsupported gpu architecture。这时就应该选PyTorch 2.x版本搭配CUDA 12.1/12.4。

我的建议简单直接:

  • 显卡是RTX 30系及以前:PyTorch 1.13.1 + cu116,GCC降到9或10。
  • 显卡是RTX 40系及以后:PyTorch 2.1以上 + cu121,保留系统新GCC。

安装命令示例(以cu121为例):

pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu121

装完立刻验证CUDA是否可用:

python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"

这一步输出True才有意义。如果输出False,说明装成了CPU版本,后面编译自定义扩展时连CUDA头文件都找不到,所有步骤都会白费。

3.3 拉取仓库与子模块

3DGS的代码以开源仓库形式发布,仓库里通过git submodule引用了那两个关键CUDA子模块。这就意味着不能用普通的git clone,必须连子模块一起拉。

git clone --recursive <仓库地址>

如果忘了加--recursive也没关系,进目录后补一条命令即可:

git submodule update --init --recursive

拉完代码后,我习惯先看一眼子模块目录是不是空的,有时候网络原因子模块没拉全,后面编译就会因为找不到源码而失败。检查方法很简单:看submodules/目录下两个子模块文件夹里有没有.cu或.cpp文件。

4. 光栅化与KNN子模块:编译这一关怎么过

4.1 这两个模块到底干了什么

仓库里的两个核心子模块,一个叫diff-gaussian-rasterization,另一个叫simple-knn,名字偏学术,但功能并不复杂。

diff-gaussian-rasterization负责的是整个算法最核心的部分:把3D高斯投影到2D图像平面上,对每个像素做深度排序和alpha混合,同时实现反向传播。相当于一个专门为高斯泼溅写的可微分光栅化器。它在PyTorch的标准算子库里没有现成替代,所以必须单独编译成Python可导入的扩展。

simple-knn则是为自适应密度控制服务的。训练过程中算法需要决定哪些高斯应该被克隆、哪些被分裂,这依赖每个高斯周围的邻居关系,simple-knn就是用CUDA实现的快速K近邻搜索。它相对轻量,编译时间也短。

理解这一点对排查问题特别重要:以后凡是看到diff_gaussian_rasterization相关的报错,基本可以肯定问题出在光栅化kernel,而不是普通的PyTorch运算错误。

4.2 编译命令其实只有两条

环境就绪后,编译过程异常简洁:

pip install submodules/diff-gaussian-rasterization pip install submodules/simple-knn

编译时setup.py会调用torch.utils.cpp_extension.CUDAExtension,自动从PyTorch的安装目录里找CUDA工具链。这时不需要手动指定CUDA路径,但前提是PyTorch本身是带CUDA的版本。

编译时间取决于CPU性能,通常diff-gaussian-rasterization要5到15分钟,simple-knn要2到5分钟。期间会出现大段gcc/nvcc输出,看到红字不要慌,先等它完整跑完,很多warning不影响最终产物。只有出现error:开头的行才需要重视。

编译完成后验证导入:

python -c "import diff_gaussian_rasterization; import simple_knn; print('OK')"

如果没有任何输出错误,说明编译成功,这一关正式通过。

4.3 编译失败的三类典型原因

从我的经验看,编译失败几乎都集中在三类原因上。

第一类是torch/extension.h: No such file or directory。这说明PyTorch本身没有装对,最常见于直接用pip install torch装到了CPU版本。解法就是回到第3.2节,重新装带CUDA的PyTorch。

第二类是GCC版本和CUDA不兼容。报错里通常有unsupported GNU version字样。解法是给当前环境装一个低版本GCC:

conda install gcc=9 gxx=9 -c conda-forge

或者反过来说,如果系统GCC太老而装新CUDA,则要升级GCC。

第三类是显卡架构不匹配,报错形如Unsupported gpu architecture 'compute_90'。这几乎是RTX 40系显卡配了老CUDA的必现错误。解法是升级到CUDA 12.x对应的PyTorch版本。注意修改环境后需要重新激活环境,让PATH生效,不然conda里的GCC没被真正用到。

5. 数据链路:从手机视频到COLMAP位姿

5.1 视频抽帧:拍得稳比拍得多重要

3DGS训练依赖一组静态图片和对应的相机位姿。最常见的起点是一段围绕物体环绕拍摄的视频,第一步就是抽帧成图片序列。

ffmpeg -i input.mp4 -r 5 -qscale:v 1 frames/%04d.jpg

这里的-r 5表示每秒抽5帧。我建议抽帧率控制在3到10帧每秒之间。太高会导致相邻帧高度相似,浪费COLMAP匹配时间;太低又容易丢失视角连续性,让稀疏重建不稳定。视频拍摄时速度也很有讲究,慢速环绕比快速扫过的效果好得多,因为运动模糊和帧间跳变都会直接影响SIFT特征的提取质量。

抽帧完成后做一次人工筛选。删除明显失焦、过度曝光、画面被手挡住的帧。我给一个参考经验:环绕一圈下来,保留80到150张清晰的图就足够一个普通场景用了,多了反而不是好事。实测中COLMAP在最开始可能就会因为某些帧匹配不到而重建失败,这时候优先删掉那些和前后帧差异过大的图像。

5.2 COLMAP安装与运行

COLMAP负责从图片集里解算出相机内外参数和稀疏点云。官方仓库提供了一个封装好的脚本convert.py,它会自动调用COLMAP完成特征提取、匹配、稀疏重建和图像去畸变。

COLMAP本身不是Python包,安装方式推荐用conda:

conda install -c conda-forge colmap -y

安装完成后,最简单的做法是直接运行仓库自带的转换脚本:

python convert.py -s data/scene

也可以手动执行COLMAP命令,流程更透明,方便定位问题:

colmap feature_extractor --database_path data/scene/database.db --image_path data/scene/input colmap exhaustive_matcher --database_path data/scene/database.db colmap mapper --database_path data/scene/database.db --image_path data/scene/input --output_path data/scene/sparse colmap image_undistorter --image_path data/scene/input --input_path data/scene/sparse/0 --output_path data/scene

手动执行时要注意:最终的数据路径必须包含images/目录和sparse/0/目录,其中sparse/0/里要有cameras.bin、images.bin、points3D.bin这三个文件。convert.py之所以推荐,就是因为它自动完成了目录整理。

5.3 数据质量决定训练上限

COLMAP不是万能的。在环境搭建阶段,如果你把数据问题误判成代码环境问题,会浪费很多时间。我遇到过最典型的场景:对着全白墙或纯色地板拍摄,COLMAP一帧特征都提不出来,程序直接卡在特征匹配阶段。解决办法是保证场景里有足够的纹理细节,比如桌面上的杂志、墙面上的海报,这些都能提供丰富的特征点。

还有一类问题是弱纹理加运动模糊混合出现,常见于室内光线不足时手持拍摄。此时哪怕特征提取成功,匹配结果也是错误的,后期训练出来的模型会出现明显重影。我的建议是:第一次跑通流程时,宁可选一个纹理丰富、光照稳定的小物体,也不要拿大场景挑战流程稳定性。数据链路稳定了,再逐步添加复杂场景。

6. 训练、渲染与实时查看:跑通Hello World

6.1 训练命令与关键参数

数据和环境都准备好后,训练命令本身很简单:

python train.py -s data/scene -m output/demo

-s指定数据集路径,-m指定输出目录。训练过程中会看到类似Step: 10000, Loss: 0.18的日志输出。默认迭代次数是30000次,在RTX 3090级别显卡上,一个普通室内场景大概10分钟左右能跑完。

对第一次跑通来说,默认参数就是最好的参数,不需要调任何东西。但理解几个关键参数的含义,后面调优时会有用:

参数默认值作用
--iterations30000总迭代次数,越大细节越好但越慢
--sh_degree3球谐阶数,控制颜色随视角变化的精细度
--densify_until_iter15000密度控制截止迭代点,此后高斯数量不再增加
--densify_grad_threshold0.0002触发高斯克隆/分裂的梯度阈值

6.2 渲染与结果验证

训练结束后,用渲染脚本生成新视角序列:

python render.py -m output/demo -s data/scene

渲染结果会输出到output/demo/下,通常包括每个测试视角的重建图、深度图和法线图。我建议训练和渲染都跑完后,不要急着看数值指标,先滚动查看渲染出的图片序列。如果画面稳定、颜色自然、边缘没有漂浮物,说明训练基本成功。如果有明显的黑色空洞或漂移,通常不是环境问题,而是COLMAP位姿估算精度不够,或者训练迭代不够。

6.3 实时查看器:跑通闭环的最后一块拼图

3DGS的亮点是实时渲染,官方仓库配套的SIBR查看器可以加载训练好的模型,让你用鼠标拖动视角实时预览。查看器需要额外编译,依赖CMake和Qt环境。如果你只是想验证算法流程,这个环节可以跳过;但如果目标是做展示或者交互式应用,建议还是编译一下。

编译查看器最需要注意的是CMake版本要足够新,并且要让CMake找到Qt开发包。这个过程比Python扩展编译更容易出环境问题,但逻辑是一样的:先确认依赖版本,再执行编译,不要贸然把所有错误都归到代码上。我在一台系统库比较老的Ubuntu机器上编译查看器时,就因为Qt版本过旧导致生成的窗口无法正常显示,最后通过升级Qt解决了问题。

7. 踩坑清单:从报错信息反推环境问题

7.1 常见报错速查表

把我在多台机器上遇到的典型问题整理成一张表,遇到报错先对照这张表定位方向:

报错信息根因解决办法
ModuleNotFoundError: No module named 'diff_gaussian_rasterization'子模块没编译成功或没导入当前环境回到第4.2节重装子模块,确认当前conda环境激活
torch.cuda.is_available()返回False装成了CPU版PyTorch用带cu11x/cu12x的index重新安装
CUDA driver version is insufficient驱动版本低于PyTorch要求升级NVIDIA驱动
Unsupported gpu architectureCUDA版本太老,不认识新GPU换用CUDA 12.x对应的PyTorch
unsupported GNU versionGCC版本超出CUDA支持范围conda装旧版GCC或换新CUDA
CUDA error: out of memory显存不够降低图像分辨率或减少迭代
COLMAP特征提取数为0场景纹理太少换场景或增加纹理物体
训练后画面全黑梯度爆炸或NaN开启--detect_anomaly,调低学习率

7.2 一个典型排查过程的还原

我详细说一个实例,方便你建立排查思路。有一次我在新电脑上搭环境,torch.cuda.is_available()返回True,但编译diff-gaussian-rasterization时一直报gcc: error: unrecognized command-line option '-std=c++17'。表面看是编译器不认识C++17,但系统GCC明明是11,理论上支持C++17。

最后排查发现,是conda环境里残留了一个旧版本gcc,g++命令被指向了4.8版本,而nvcc调用的就是这旧的g++。解决方式是清理conda环境里的编译器包,并显式指定CXX环境变量:

conda remove gcc gxx export CXX=/usr/bin/g++

重新编译立刻通过。这个案例说明一个经验:环境搭建里大多数报错不是代码问题,而是工具链指向错误。排查时先问自己三个问题——编译用的编译器是不是我以为的那个?PyTorch有没有真的装上CUDA支持?conda环境有没有激活对?这个问题回答了,报错基本就解了一半。

7.3 环境备份与复现

当你终于在一台机器上把全流程跑通后,我强烈建议立即做一件事:导出环境依赖清单。我用的是下面两条命令:

conda env export > environment_full.yml pip freeze > requirements.txt

这样换机器或者环境被搞坏时,可以直接用导出的配置恢复。别指望能记住所有依赖版本,Python生态的版本绑定关系太复杂了,靠文字记录一定会漏。

最后补充一个个人习惯:每次更换PyTorch或CUDA版本后,都重新编译一次两个子模块,哪怕你已经编译过。编译产物会绑定到具体的PyTorch ABI版本,换版本后不重编会导致导入时神秘的段错误或者找不到符号。把这个动作当成固定的环境切换流程,能帮你省下一大堆查无头绪的排查时间。

3DGS环境搭建这件事,说穿了就是让CUDA、GCC、PyTorch、COLMAP这几样东西在同一个环境里达成一致。每一样单独装都很简单,难的是理解它们之间的依赖关系。这篇内容把我在拆解过程中积累的排查思路整理了出来,核心其实是那一句话:先确认每一个工具链的真实版本和指向,再动手编译。以后无论你在这个框架上做场景重建、实时渲染,还是往里面加自定义算子,只要环境这关过了,后面的事都会顺畅很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进