ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

nerf-slam复现编译排错:依赖、CMake与CUDA/torch

nerf-slam复现编译排错:依赖、CMake与CUDA/torch

简介:面向希望从零复现 NeRF-SLAM 的研究者与三维重建方向的学习者,这份 docx 文档以实操记录加原理解读的方式,梳理了神经辐射场与 SLAM 结合的完整链路。包内共 1 个 docx 文件,约 11.01MB,内容兼顾公式推导与工程细节,可作为论文阅读之外的补充素材与复现模板。文档沿 Tracking 与 Mapping 两条主线展开:Tracking 部分以 Droid SLAM 的稠密单目框架为基础,梳理 RAFT 由特征编码器、相关层与 convGRU 更新算子构成的光流估计流程,以及 DBA 层如何借助舒尔补与 Cholesky 分解求取位姿与逆深度修正;Mapping 部分围绕 Instant-NGP 的哈希编码加速、概率体素化 NeRF 与深度—颜色平衡的 Mapping Loss 展开,并对比有无精确位姿、深度时的收敛表现。文中还记录了复现时遇到的环境安装、数据集准备与运行问题及解决思路,已有 2327 人学习,适合作复现入门与关键模块理解参考。

1. 从Could NOT find GLEW说起:编译期报错为什么最耗人

最先跳出来的往往是一条看起来很无辜的报错:Could NOT find GLEW (missing: GLEW_INCLUDE_DIRS GLEW_LIBRARIES)。搜索一圈,照着别人说的装完libglew-dev,重新 cmake,报错原样再来一遍。这并不是你装错了包,真正的原因多半不在这一行:nerf-slam 这类把 instant-ngp、gtsam、Droid SLAM 拼在一起的项目,构建链路上有子模块完整性、CMake 版本、Boost、CUDA/torch 版本、Python ABI 五道关卡,任何一道没有对齐,最后都表现为“某个库找不到”。这篇排错笔记按复现时的实际顺序展开:先把依赖拉齐,再处理两处最高频的缺库报错,最后给一套由外向内定位的顺序。手里拿着这份 nerf-slam 复现文档、机器是 Ubuntu + CUDA 11.3、卡在编译或 import 阶段的人,可以对照。

2. 依赖没对齐就开跑:ModuleNotFoundError和undefined symbol的根子都在 clone 那一步

复现这类项目,最常见的开头是:clone 下来,pip install -r requirements.txt一片 Successfully installed,心里一松,结果一跑脚本,要么ModuleNotFoundError: No module named 'pyngp',要么ImportError: undefined symbol: _ZN3c10...。这类报错看着像代码问题,实际上百分之九十是环境层面的三条线没对齐:源码没拿全、torch 的 CUDA 变体不对、两份 requirements 的安装顺序颠倒。编译期的问题会直接给你一句红字,运行期的问题则像个黑匣子,只在某个时刻突然炸开,所以先把这一层处理干净,后面的编译报错才有意义。

2.1 子模块没拉全,thirdparty目录下是空的

复现文档里的第一条命令就是带--recurse-submodules的,很多人图快直接git clone了主仓,然后发现thirdparty/instant-ngp和thirdparty/gtsam两个目录要么不存在,要么里面只有一两个文件。这两个目录不是普通文件夹,是 git submodule,主仓只记录了一个 commit 指针,不拉子模块等于只拿到一半代码。

# 一步到位:clone 的同时把子模块按记录的 commit 拉下来 git clone <复现文档中给出的仓库地址> --recurse-submodules # 如果已经 clone 过了,或者上一次网络中断只拉了一半,用这条补 git submodule update --init --recursive # 检查状态:行首是 - 表示该子模块还没初始化,是空格或 + 才正常 git submodule status

--recurse-submodules的作用是递归:子模块里面如果还嵌套了子模块,一并拉下来。git submodule update --init --recursive里的--init是把.gitmodules里登记但还没写进.git/config的条目注册上,--recursive同样是递归。两条命令不是重复,第一条负责首次拉取,第二条负责补救,尤其在国内网络环境下大仓库拉到一半断掉是常态,断完再clone一遍不如直接update来得快。判断有没有拉全,看git submodule status行首字符就够了,带减号的一律要补。

注意:子模块被拉到某个固定 commit 上,不要手动git checkout到别的分支去“更新一下”,版本错位之后 cmake 能找到文件,但编译会报一堆莫名其妙的符号缺失。

2.2 torch 必须是+cu113,默认装下来的大概率不对

复现文档里钉死了torch==1.12.1+cu113。那个+cu113后缀不是装饰,它是 wheel 的本地版本标识,代表这个包里静态链接了 CUDA 11.3 的运行时。直接pip install torch装到的是最新版,可能是 cu117、cu118,甚至是 CPU 版本;而 instant-ngp 和 gtsam 的编译过程都会去find_package(Torch),拿到的头文件路径、库路径、ABI 全都要和运行期一致,不一致就是编译期报错或者运行期undefined symbol。

# 严格按文档给的组合装,--extra-index-url 指向带 CUDA 变体的官方 wheel 源 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 \ --extra-index-url https://download.pytorch.org/whl/cu113 # 装完必须验三件事:版本、CUDA 变体、能不能真正加载 python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"

输出应该是1.12.1+cu113 11.3 True。如果第三项是False,先别急着往下编译,去查显卡驱动能不能支撑 CUDA 11.3(nvidia-smi右上角的 CUDA Version 要 ≥ 11.3),驱动太老的话 torch 装对了也起不来。第二项如果是None,说明装成了 CPU 版,卸载重装,用pip install --force-reinstall别只pip install,缓存里的旧包会让它“已满足依赖”直接跳过。

这里还有一个容易出错的地方:装 torch 时用了sudo pip,或者建了虚拟环境但编译 instant-ngp 时用的是系统 python。编译时find_package(Torch)走的是 CMake 的查找逻辑,它认的是CMAKE_PREFIX_PATH和当前解释器的site-packages,你sudo装到/usr/lib/python3/dist-packages,虚拟环境里编译自然找不到。从 clone 到跑通,全程用同一个虚拟环境、同一个python,不加sudo。

2.3 两份 requirements 的安装顺序,决定了会不会互相覆盖

复现文档里要装两次:主仓的requirements.txt和 gtsam 的 python wrapper 依赖。这两份文件里会有重叠的包(numpy、scipy 这类基础库最常见),版本约束不一定一致。

# 先主仓,把整体版本基线定下来 pip install -r requirements.txt # 再装 gtsam 的 python 端依赖 pip install -r ./thirdparty/gtsam/python/requirements.txt # 装完做一次冲突检查,把 Broken requirements 挑出来 pip check

顺序反过来会出事:子模块的 requirements 往往写得比较宽松(numpy>=1.20这种),先装它会把 numpy 拉到最新,再装主仓 requirements 时,如果主仓钉死了一个较老的版本,pip 会回退,但回退过程可能牵连 scipy、opencv 一起重装,中间任何一步失败都会留下半残状态。先主后子,是让主仓的强约束先落地,子包只做增量。pip check是我每次装完依赖都会跑的一条命令,它会明确告诉你哪个包要求的版本和实际装的对不上,比等到运行时AttributeError: module 'numpy' has no attribute 'float'再去猜要快得多。

提示:这一层的血泪经验是,装依赖之前先pip freeze > before.txt,装完pip freeze > after.txt对比一下,能立刻看出哪个包被悄悄升级了。

依赖这一层过了,剩下的才是真正意义上的“编译报错”。下面两条,是复现 nerf-slam 时几乎人人都会撞上的。

3. CMake 与 instant-ngp:cmake --version已经是 3.22 却仍报版本不够

instant-ngp 对 CMake 的要求是 > 3.22,而复现文档里前后出现过三个版本号:3.22、3.9.2、3.26.3。这不是文档写得乱,是因为 gtsam 那侧和 instant-ngp 这侧对工具链的容忍度不一样,实际操作中很容易演变成“装了新的、旧的又把它盖回去”。接着就会出现一个非常反直觉的现象:cmake --version明明显示 3.22,configure 时仍然告诉你版本不足。

3.1sudo make install会把新版本覆盖回旧版本

源码或二进制包安装 CMake,最后一步基本都是往/usr/local/bin里写cmake、ctest、cpack这几个可执行文件。问题在于/usr/local/bin只有一个cmake,谁最后make install,谁就赢。复现文档里为了兼容某个环节先装了 3.9.2,又装 3.26.3,如果顺序颠倒,或者装完新版本又回头补了一次旧版本,最终生效的就是旧的。

# 看清楚现在到底有几个 cmake、分别在哪、各自什么版本 which -a cmake /usr/local/bin/cmake --version /usr/bin/cmake --version # 二进制包方式安装新版本(比源码编译快很多) wget https://cmake.org/files/v3.22/cmake-3.22.0-rc2-linux-x86_64.tar.gz --no-check-certificate tar -zxvf cmake-3.22.0-rc2-linux-x86_64.tar.gz sudo cp -r cmake-3.22.0-rc2-linux-x86_64/bin/* /usr/local/bin/ sudo cp -r cmake-3.22.0-rc2-linux-x86_64/share/* /usr/local/share/

which -a会按 PATH 顺序把所有同名命令列出来,第一行就是实际生效的那个。二进制包解压后自带bin和share两个目录,bin里是可执行文件,share里是 CMake 自己的模块文件(FindXXX.cmake那一堆),两个都要拷,只拷bin会出现在 configure 时找不到内置模块的怪问题。拷完之后cmake --version显示新版本了,但cmake .执行起来还是旧的——往下看。

3.2hash -r:命令换了,shell 还在用老路径

bash 会把已经执行过的命令路径缓存在哈希表里,换掉二进制文件之后,同一终端里再敲cmake,bash 可能还是直接走缓存里的旧路径,根本不重新查 PATH。

# 清掉命令哈希缓存,强制重新查找 hash -r # 确认当前 shell 认为 cmake 在哪 type -a cmake # 终极保险:直接用绝对路径调用,绕开一切缓存 /usr/local/bin/cmake --version

hash -r执行完不会有任何输出,这是正常的,它的作用就是把缓存表清空。判断有没有生效,看type -a cmake打出来的第一个路径是不是你刚装的那个。如果which -a cmake第一行是/usr/bin/cmake而你想要的是/usr/local/bin/cmake,说明 PATH 顺序有问题,/usr/bin排在前面了,临时办法是export PATH=/usr/local/bin:$PATH,长期办法是改~/.bashrc里的 PATH 顺序。这一步看着很小,但它是“版本明明对却报版本错”的最高频原因,我见过不止一次有人在这里重装了三遍 CMake。

3.3Could NOT find GLEW:缺的不是 GLEW 这个名字,是开发包

把 CMake 摆平之后,第一条命令是给 instant-ngp 做 configure:

# 在 nerf-slam 根目录执行,-B 指定独立的构建目录,不污染源码树 cmake ./thirdparty/instant-ngp -B build_ngp # 报错:Could NOT find GLEW (missing: GLEW_INCLUDE_DIRS GLEW_LIBRARIES) # 原因是只装了运行库或压根没装,头文件和链接库都不在 sudo apt install libglew-dev # 装完重新 configure cmake ./thirdparty/instant-ngp -B build_ngp

-B build_ngp的意思是构建目录放在build_ngp,源码目录保持干净,好处是编译失败想推倒重来时,rm -rf build_ngp一句就够,不用去thirdparty里清理生成物。报错里括号内的部分是重点:GLEW_INCLUDE_DIRS缺失说明找不到头文件(GL/glew.h),GLEW_LIBRARIES缺失说明找不到libGLEW.so。Ubuntu 上libglew-dev这一个包同时提供两者,所以装它是对的。

真正容易出问题的是装完之后:如果上一次 configure 已经在build_ngp/CMakeCache.txt里把GLEW_INCLUDE_DIRS-NOTFOUND缓存下来了,重新跑 cmake 有时会沿用缓存值。稳妥做法是删掉构建目录重来,或者至少删CMakeCache.txt。另外,instant-ngp 在带 GUI 的构建里还依赖 OpenGL、X11 相关的一串开发包,机器上没有显示环境时,configure 阶段可能报的不止 GLEW 一个。这种机器上我一般会先确认 X11 开发包齐不齐(libx11-dev、libxrandr-dev、libxi-dev、libgl1-mesa-dev这一类),一次装全,比一条一条报错一条一条补要省事。

注意:装完系统库一定要重新执行 configure,不要指望cmake --build会自己重新探测依赖,它只负责构建,不会重新跑一遍查找逻辑。

3.4cmake --build build_ngp --config RelWithDebInfo -j的实际产物

configure 干净通过之后就是构建:

# RelWithDebInfo 介于 Release 和 Debug 之间,性能接近 Release 又保留调试符号 # -j 后面不带数字会按 CPU 核数吃满内存,编译大文件时容易 OOM,建议先 -j4 cmake --build build_ngp --config RelWithDebInfo -j4 # 构建完确认 Python 扩展是否生成 ls -lh build_ngp/*.so

--config这个参数只对多配置生成器(Visual Studio、Xcode、Ninja Multi-Config)有意义,在 Linux 默认的 Unix Makefiles 生成器下它基本是空转,写上去没坏处但别指望它切换优化级别——真正决定编译选项的是 configure 阶段传的-DCMAKE_BUILD_TYPE。-j4里的数字是并行任务数,instant-ngp 里有个别源文件编译时内存占用很高,-j不带数字在 8 核机器上能直接吃到十几个 G,触发 OOM Killer 之后报的错是c++: fatal error: Killed signal terminated program cc1plus,看着像编译器崩了,其实是被系统杀的。遇到这个报错,第一反应就是降并行度,而不是去查编译器。

ls build_ngp/*.so这一步用来确认 Python 绑定是否产出。如果目录里只有一堆.o和可执行文件,没有.so,说明 Python 扩展那部分没被编进去,回头看 configure 输出里关于pybind11、Python的探测结果。这一步确认完,Tracking 那侧的 Droid SLAM 基础组件才算准备好。

4. gtsam 报Could NOT find Boost:Required is at least version "1.65"两种解法

第二条命令是编译 gtsam 并打开 Python wrapper:

cmake ./thirdparty/gtsam -DGTSAM_BUILD_PYTHON=1 -B build_gtsam

然后就是一整屏里最长的那条:

Could NOT find Boost (missing: Boost_INCLUDE_DIR serialization system filesystem thread program_options date_time timer chrono regex) (Required is at least version "1.65")

4.1 把这条报错拆成三段读

这条报错信息量很大,只是被一行塞满了。拆成三段看,指向三个不同的排查方向。

报错片段含义先查什么
Boost_INCLUDE_DIR缺失找不到 Boost 头文件目录(boost/version.hpp)ls /usr/include/boost/version.hpp、ls /usr/local/include/boost/version.hpp
一串组件名找不到编译好的libboost_serialization等链接库ls /usr/local/lib/libboost_*、ldconfig -p | grep boost
Required is at least version 1.65找到了,但版本低于门槛,或压根没找到编译一个打印BOOST_VERSION的小程序看实际版本

关键在于第二段。很多人看到Could NOT find Boost就去apt install libboost-dev,这个包只带头文件,不带你需要的serialization、filesystem、system、thread、program_options、date_time、timer、chrono这些组件库,装完报错照旧,只是括号里的缺失列表少了几项——这恰恰说明 apt 路线装不全。要么装libboost-all-dev(一次装全,版本跟着发行版走,可能不满足 gtsam 的具体要求),要么按复现文档走源码编译,版本自己说了算。

提示:判断“到底找没找到”,直接看 CMake 生成的build_gtsam/CMakeCache.txt,搜Boost_INCLUDE_DIR和Boost_LIBRARY_DIR,是-NOTFOUND还是具体路径,一目了然,比反复读 configure 输出快。

4.2 两条路的取舍:apt 一把梭还是源码编译

选择哪种方式,本质上是时间和可控性的取舍:

方式优点代价适用情况
apt install libboost-all-dev一条命令,几分钟搞定版本由发行版决定,可能低于门槛或高于 gtsam 兼容上限发行版自带的 Boost 刚好满足要求
源码编译指定版本版本完全可控,组件可裁剪编译时间长,装到/usr/local后还要处理查找路径发行版版本不合适、或机器上已有多个 Boost

复现文档走的是第二条,以 1.73.0 为例。这条路适合系统里已经有别的软件依赖一个老版本 Boost、又不能随便被覆盖的场景,因为源码编译可以指定--prefix装到独立目录,只在编译 gtsam 时通过-DBOOST_ROOT指过去。

4.3 源码编译 Boost 1.73:bootstrap.sh和b2的参数怎么给

# 1. 下载源码包 wget https://boostorg.jfrog.io/artifactory/main/release/1.73.0/source/boost_1_73_0.tar.bz2 --no-check-certificate # 2. 解压 tar xvf boost_1_73_0.tar.bz2 cd boost_1_73_0 # 3. 生成 b2 构建工具和 project-config.jam ./bootstrap.sh # 4. 编译并安装。--buildtype=complete 编全部变体,保证 debug/release 都可用 sudo ./b2 --buildtype=complete install

bootstrap.sh干的事是根据当前环境探测编译器,生成b2(Boost.Build 的可执行文件)和project-config.jam(记录编译器路径、Python 路径等配置)。后面所有编译参数都是给b2的。

--buildtype=complete的意思是编译并安装所有构建变体,不只是默认的 release。代价是时间,好处是后面任何环节要链接 debug 版 Boost 都不用回头重编。文档里明确说了这是为了省事的选择,实际如果只要跑通 nerf-slam,用--with只编需要的几个组件会快很多:

# 只编 gtsam 需要的那几个组件,装到独立目录,避免污染系统 Boost ./bootstrap.sh --prefix=/opt/boost_1_73_0 --with-python=python3 sudo ./b2 --with-serialization --with-filesystem --with-system \ --with-thread --with-program_options --with-date_time --with-timer \ --with-chrono --with-test install

--prefix指定安装根目录,编译产物会落到/opt/boost_1_73_0/include和/opt/boost_1_73_0/lib。用独立 prefix 之后,必须在编译 gtsam 时显式告诉 CMake 去哪找,否则它还是按默认路径搜,报错原样不动。这一点很关键,很多人在这一步以为编译失败了,其实是装到了“CMake 不会主动搜”的地方。

--with-python=python3是给需要 Python 绑定的组件用的,系统里有多个 Python 时(比如 3.8 和 3.10 并存),不指定的话bootstrap.sh会挑一个默认的,装完之后 gtsam 的 wrapper 编出来是给你不用的那个 Python 用的,运行时报ImportError: dynamic module does not define module export function。默认路径安装(不加--prefix)的话,库会落到/usr/local/lib,头文件落到/usr/local/include,装完顺手跑一下sudo ldconfig刷新动态库缓存,能避免一部分“明明装了却加载不到”的玄学问题。

4.4 装完还报同样的错:CMake 的查找路径与缓存

重新 configure:

# 默认路径安装的情况,直接重来 cmake ./thirdparty/gtsam -DGTSAM_BUILD_PYTHON=1 -B build_gtsam # 独立 prefix 安装的情况,显式指定 Boost 根目录,并禁止回退到系统路径 cmake ./thirdparty/gtsam -DGTSAM_BUILD_PYTHON=1 -B build_gtsam \ -DBOOST_ROOT=/opt/boost_1_73_0 \ -DBoost_NO_SYSTEM_PATHS=ON

BOOST_ROOT是 FindBoost 模块认的环境变量/缓存变量,告诉它从哪个根目录开始找include和lib。Boost_NO_SYSTEM_PATHS=ON的作用是禁止它再去/usr、/usr/local这些系统路径里翻——这一条在有多个 Boost 版本的机器上几乎是必需的,否则 CMake 很可能先找到系统里那个版本不够的老 Boost,然后继续报Required is at least version 1.65,让你误以为新版本没装上。

另一个高频原因是缓存没清。build_gtsam/CMakeCache.txt里如果已经写着Boost_INCLUDE_DIR:PATH=Boost_INCLUDE_DIR-NOTFOUND,重新跑 cmake 时它会把缓存值当默认值继续用,除非显式传参覆盖。遇到“确定装好了但报错不动”,最省事的验证方式是rm -rf build_gtsam从零 configure 一次,如果这次过了,就说明是缓存问题;如果照旧,再回头查版本。

4.5GTSAM_BUILD_PYTHON=1之后,怎么确认 wrapper 真的能用

configure 过了不等于能 import。构建并验证:

# 构建 gtsam,同样注意并行度 cmake --build build_gtsam --config RelWithDebInfo -j4 # 验证 Python 端能不能加载。这一步会暴露 ABI 和路径问题 python -c "import gtsam; print(gtsam.__file__)" # 如果 import 失败,先看 python 版本和 wrapper 编的是不是同一个 python -c "import sys; print(sys.version)"

gtsam.__file__打印出来的路径应该落在build_gtsam/python或安装目录下,不设置PYTHONPATH的情况,也可以在环境变量里加上那个路径。import gtsam报ModuleNotFoundError时,八成是路径没加;报undefined symbol或者dynamic module does not define module export function,基本就是 Python 版本错配——wrapper 是对着 3.8 编的,你拿 3.10 去 import。gtsam 的 CMake 支持通过-DGTSAM_PYTHON_VERSION=3.8指定版本,多 Python 环境下这个参数比事后改 PATH 靠谱得多。

5. 避坑与常见问题:五条复现现场记录

这一节把复现过程中最常撞上的五条整理出来,每条都按“现象—原因—解决”走一遍。它们分布在不同的阶段,但共同点是:报错信息都不指向真正的原因。

5.1 现象:thirdparty/instant-ngp里没有CMakeLists.txt

先说现象。git clone完主仓,按照文档敲cmake ./thirdparty/instant-ngp -B build_ngp,返回The source directory ... does not appear to contain CMakeLists.txt,ls一下那个目录,只有零星几个文件,甚至完全是空的。

原因在于子模块只登记了指针,没拉实体。主仓的.gitmodules里记录了两个子模块的 URL 和对应 commit,clone 时不加--recurse-submodules,git 不会去拉。中断过的 clone 更麻烦,目录被创建了、.git文件也写了,但工作区是空的,看起来像“拉过但是文件丢了”。

解决方式是回到仓库根目录执行git submodule update --init --recursive,跑完用git submodule status确认行首没有减号,再ls thirdparty/instant-ngp/CMakeLists.txt确认文件存在。如果update卡住不动,多半是网络问题,先git submodule deinit -f thirdparty/instant-ngp清掉,再重新update,比反复 clone 整个仓库快。判断标准很简单:在cmake之前,先ls一下要编译的目录里有没有CMakeLists.txt,没有就别往下走。

5.2 现象:装了新 CMake,cmake --version还是老版本

现象是刚把 3.26 从官网下下来按文档装好,/usr/local/bin/cmake --version显示 3.26,但只要不在命令前写绝对路径,cmake --version就是 3.9.2。

原因有两层。一层是 PATH 顺序:/usr/bin排在/usr/local/bin前面,系统包管理器装的老版本会被优先命中。另一层是 bash 的命令哈希缓存,同一个终端里只要执行过一次cmake,后续调用直接走缓存路径,不管 PATH 变没变、文件换没换。

解决方式是三条命令连打:which -a cmake看全部路径,type -a cmake看 shell 实际会走哪个,hash -r清缓存。如果which -a第一行不是你要的,改 PATH 顺序或者直接用绝对路径。长期方案是在~/.bashrc里把/usr/local/bin前置,改完source ~/.bashrc并对所有开着的终端补一次hash -r。这个坑的特点是不会报错,只会让你怀疑人生:明明装了、明明查了版本、偏偏 configure 时说版本不够。

5.3 现象:装完 Boost,gtsam 还是报Could NOT find Boost

现象很好认,b2 install跑完最后一行显示安装成功,ls /usr/local/lib/libboost_serialization*看得到.so,但重新 configure gtsam,那条报错一动不动。

原因通常是三个之一。第一种,build_gtsam目录里的CMakeCache.txt把-NOTFOUND缓存下来了,重新 configure 沿用旧值。第二种,系统里/usr/lib下有一个发行版自带的老 Boost,FindBoost 按默认搜索顺序先找到了它。第三种,用--prefix装到了非标准路径,CMake 根本不会主动去搜那里。

对应的解决方式:rm -rf build_gtsam从零 configure;加-DBoost_NO_SYSTEM_PATHS=ON禁止回退系统路径;用-DBOOST_ROOT=/你的/prefix显式指路。三者可以一起上,不冲突。还有一个容易被忽略的收尾动作是sudo ldconfig,/usr/local/lib如果不是标准搜索路径,动态链接器在运行期找不到库,编译能过、运行报error while loading shared libraries。经验是:只要报错文字完全没变,先怀疑缓存和路径,别怀疑装的包有问题。

5.4 现象:编译到一半报Killed signal terminated program cc1plus

现象是构建跑到某个.cpp时进程被终止,日志里出现c++: fatal error: Killed signal terminated program cc1plus,或者干脆终端会话断开、机器卡死几秒。

原因不是编译器有 bug,是内存被吃光了,系统的 OOM Killer 出手杀了编译进程。instant-ngp 里有几个模板展开很重的文件,gtsam 里也有大文件,cmake --build ... -j不带数字时并行度按 CPU 核数算,16 核机器同时编 16 个大文件,内存瞬间打满。

解决方式是把并行度降到-j4甚至-j2重试,构建目录不用删,增量编译会接着上次的进度走。如果单文件本身就很大,可以加-DCMAKE_BUILD_TYPE=Release减少调试符号带来的内存占用。机器交换分区太小的话,临时扩 swap 也管用。这条被很多人当成“编译器坏了”,实际上一行-j4就解决,属于典型的对症下药五分钟、误判方向两小时。

5.5 现象:import gtsam正常,跑流程时 Python 侧报符号错误

现象是单独python -c "import gtsam"没问题,一旦进入完整流程,加载别的扩展模块时抛undefined symbol,符号名看起来是一串跟torch或gtsam相关的 C++ 修饰名。

原因是 ABI 不一致。Python 扩展在编译时链接了特定版本的 libstdc++、特定版本的 torch、特定 Python 解释器的 ABI,运行期如果换了其中任何一个,符号就对不上。复现现场最常见的触发方式是:编译用的是系统 Python 或者 A 虚拟环境,运行用的是 B 虚拟环境;或者编译完之后又pip install升级了 torch。

解决方式是保证编译期和运行期严格同环境:同一个虚拟环境、同一个解释器路径、不重装 torch。验证办法是对比python -c "import sys; print(sys.executable)"和编译时 CMake 输出里打印的 Python 路径,两者必须一致。已经错配的,rm -rf build_*重新编译,靠打补丁是补不回来的。从这以后我养成一个习惯:虚拟环境建好之后先打印一次解释器绝对路径,贴到笔记里,后面所有编译都在这个路径下做,避免中途切了终端、切了 conda 环境还不知道。

6. 快速定位:把“编译失败”拆成四层查

复现这类多依赖缝合的项目,最耗时间的不是解决问题,是判断问题在哪一层。我的经验是从外往里查,四层,顺序不能乱:

层级先跑什么看什么大概率问题
1. 源码完整性git submodule status、ls thirdparty/*/CMakeLists.txt行首有无减号、文件在不在子模块没拉全
2. 工具链版本which -a cmake、cmake --version、nvcc --version、nvidia-smi实际生效路径与版本PATH 顺序、命令缓存、驱动低于 11.3
3. 依赖查找configure 输出、build_*/CMakeCache.txtXXX_INCLUDE_DIR是路径还是-NOTFOUND系统库缺失、多版本抢先命中、缓存未清
4. Python ABIpython -c "import sys; print(sys.executable)"与编译期打印的解释器路径是否一致跨环境编译与运行

顺序的意义在于排除法:第一层不过,后面三层全是白费;第二层不稳,第三层的报错可能是假的——CMake 版本不够时,它探测依赖的行为本身就不可靠,你会看到一堆莫名其妙的缺失。第三层出问题时,CMakeCache.txt是最有价值的线索,它记录了每一次探测的结果,-NOTFOUND就是明确的信号,比对着滚动的 configure 输出找关键词快得多。

还有几个具体技巧值得记下来。其一,cmake支持-DCMAKE_FIND_DEBUG_MODE=ON,打开之后会把每个find_package搜过的路径全部打印出来,判断“为什么找到了错的那个版本”极其有效,用完记得关掉,输出量很大。其二,任何“装了但没生效”的报错,第一反应都是删构建目录重来,缓存导致的假报错占比高得超出直觉。其三,ldconfig -p | grep 库名能直接告诉你动态链接器认不认这个库,编译过了运行报cannot open shared object file时,先跑这条。

这套顺序我是在被同一个Could NOT find Boost折腾了一下午之后固定下来的。从那以后,每次拿到一个新的复现仓库,不管文档写得多清楚,我都会先跑一遍环境体检脚本,把 cmake、nvcc、torch、python 的版本和路径一次性打印出来存成日志,编译报错时先对照这份日志,能省掉一大半“到底是哪个版本不对”的猜测。前面那条Could NOT find GLEW也一样,它从来不是 GLEW 的问题,只是第一个撑不住倒下的环节。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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