写这篇记录的时候,我的机器刚重装完系统,桌面干干净净。旁边还有一杯没喝完的咖啡,虎口上残留着上一次编译报错时拍桌子的余震。说句实话,如果不是项目需要跑一个老代码库里的推理流程,我大概率不会主动去碰Caffe2和Detectron这套组合。这套东西放在今天看,时代感很强,但你不能否认,当年确实有不少目标检测算法是跑在这套环境上的,Caffe2提供的跨平台部署能力,配合Detectron里那批经典检测模型,成了当时不少工业项目落地的首选方案。
我写这篇东西,就是想把这次从零构建Caffe2和Detectron代码环境的完整过程记录下来。包括依赖选择、踩过的坑、最终跑通骨灰级推理Demo的全链路,以及事后复盘时觉得可以少走的弯路。适合那些因为老项目维护、算法复现、学术对比等原因,不得不把目光重新投向这套老框架的人。
1. 为什么2024年还有人折腾Caffe2和Detectron
我知道第一反应是:这玩意儿不是早就停更了吗?理解这个问题,得先回到代码环境构建的原点——有时候搭建旧环境,不是为了追求新,而是为了兼容旧代码。
1.1 经典算法复现的现实需求
我这次要复现的检测流程,核心依赖恰好是Detectron库里的老牌实现。虽然现在detectron2遍地都是,但不少早期实验数据的baseline系数,跑的都是Detectron的原始结果。论文里写的是"Based on Detectron",你要公平对比,就得在同环境同版本下把对方的数据跑出来。另外,某些老模型转换到ONNX和移动端推理的链路中,Caffe2的导出格式占据了相当大的比重。朋友圈不少人都在讨论所谓"复古复现",本质上就是被这套老环境卡的。
还有一个很现实的原因:部分早期开源项目的代码格式化、算子实现、数据预处理和现在PyTorch版本之间,存在理不清的代差。与其改测试脚本去适配新框架,不如搭建一个匹配时期的旧环境,既省心又能保证代码不做逻辑层面的改动。虽然"搭环境"这个活听起来不性感,但它是所有后续工作的前置条件。
1.2 组件版本间的互相约束关系
Caffe2本身兼容的Python、CUDA、GCC版本区间比较窄,不像现在隔一两年升级一套编译器没啥感觉。Detectron的源码编译,又依赖Caffe2的算子注册机制。说白了,这是一条链路上的三个环,任何一个环节版本不对,最终跑出来的推理结果或者直接骂娘,或者无声无息地报一堆看不懂的符号错误。
典型约束我大概总结了下:
| 组件 | 推荐版本区间 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 16.04 / 18.04 | 老代码对高版本GCC不友好 |
| Python | 3.6 左右 | 高于3.7后部分Cython代码生成有问题 |
| CUDA | 9.0 / 9.2 / 10.0 | 根据GPU计算能力选,太高则opencv冲突 |
| cuDNN | 7.x 系列 | 7.1.3等特定版本更稳 |
| GCC | 5.x / 7.x | 超过8后abi兼容性风险高 |
| PyTorch | 1.0 / 1.1 | Caffe2与PyTorch合并进程中的衔接版本 |
这种版本间的"你限制我、我限制你",正是整个构建过程的隐性成本。很多人搭不成功,多半不是操作错误,而是全局层面的版本规划就没对上。有些朋友会选择用Docker镜像省事,但如果你需要改底层代码、加自定义算子,还是得老老实实从源码构建,这样后面才谈得上灵活。
2. 构建前必须搞清楚的版本规划逻辑
这部分我吃了不少教训,决定单独拎出来说清楚。很多人上来就git clone然后开始编译,这其实是把顺序搞反了。我尝试这套环境的时候,总共折腾了三个大版本组合,最后才落定。说得玄一点,版本规划决定了你后面一半的报错会不会出现。
2.1 如何根据GPU算力和驱动选择CUDA
先看你的显卡是什么年代的,再决定CUDA版本。老的GPU如果用太高的CUDA Toolkit,计算能力不满足,编译器直接告诉你"Unsupported gpu architecture"之类的话。反过来,如果你驱动装得过新而CUDA过老,驱动和Toolkit之间倒是可以兼容,但运行时会提示你缺少某些库。
我的选择思路是这样:一张多年前的中端卡,算力在6.1左右。这种算力适配的Toolkit区间很宽泛,但结合Detectron的编译脚本来看,当时官方测试最勤快的还是CUDA 9.2与cuDNN 7.1.x组合。所以我最终锁定CUDA 9.2。如果你的显卡更老,往下压到9.0即可;如果是后来入手的卡,算力大一些,可以考虑10.0,但强烈建议不要上10.1之后的版本,因为与Caffe2内部的算子实现存在编译兼容问题。
驱动就遵循"够用且稳定"原则。我当时安装的驱动对应CUDA 9.2兼容区间,不需要追最新。追最新的驱动反而会带来一个隐患——新版驱动和旧版Toolkit运行时库之间存在一些隐性的兼容缝隙,在某些算子上会触发奇怪的非法内存访问。
2.2 Python解释器与Cython代码生成的坑
Caffe2在构建过程中要用Cython生成部分算子代码。Python 3.7以上,某些生成模板的语法会有细微变化,导致生成出的.cpp文件编译时报奇怪的指针转换错误。我把Python降到3.6.9,这类问题立刻消失。建议使用虚拟环境管理Python版本,别直接动系统Python,否则后期conda包依赖会让你头疼。
另外注意Python的安装方式。建议直接用Minicoda安装指定版本Python,然后用conda管理后续依赖库。因为老项目依赖的numpy版本比较旧,用pip直接安装到系统目录的话,很容易出现OpenBLAS线程冲突之类的问题,排查起来能浪费你一下午。
2.3 依赖库的锁定清单
Detectron安装文档中会列出一批依赖,但文档的细粒度不够,有些隐性依赖不全。我在实践基础上整理了一份Lock清单,直接照着来基本不会出大岔子:
- numpy(1.16左右,别高于1.17,否则OpenCV兼容报错)
- opencv-python(3.4.x版本,4.0以后一些cv2接口行为有变)
- cython(0.28.x,高于0.29在某些环境下也会出问题)
- six、enum34等常规依赖,按setup.py需求走
- protobuf(3.6.x,这个很关键,后文单说)
- pyyaml、matplotlib、Pillow等基础库,版本宽松
这套组合是我反复试错之后得到的稳定集合。如果你机器上已经装了较新版本的numpy或者opencv,建议建一个全新的虚拟环境,别嫌麻烦。这跟做菜一个道理,调料比例不对,菜谱再好也白搭。
3. 源码获取和依赖安装的具体步骤
确认好版本版本对应的位置关系后,就可以实际操作了。这部分内容看起来像流水账,但每一步都可能成为未来的坑。
3.1 拉取源码与分支选择
先定位Caffe2的源码。这里有个容易混的地方:Caffe2后来与PyTorch合并,独立仓库的代码停更比较早。Detectron官方要求的是合并前某个状态的Caffe2。我们可以使用对应的PyTorch分支,也可以使用旧仓库冻结状态。
我当时选用的是PyTorch 1.0里内嵌的Caffe2模块。这个选择主要考虑到编译流程更完整,而且内部算子覆盖更全。具体操作是:
git clone --recursive https://github.com/pytorch/pytorch.git git checkout v1.0.0然后Detectron的源码另外拉取:
git clone https://github.com/facebookresearch/Detectron.git git checkout master两条源码的路径保持独立,编译Caffe2时会让Detectron指向这套源码。千万别用pip直接安装预编译的Caffe2包,那样算子注册机制不会完整暴露出来,后续自定义算子会很难受。
3.2 编译Caffe2前的环境变量和预处理
编译前需要准备几件事。第一个是设置环境变量,让编译系统能感知到CUDA路径:
export CUDA_HOME=/usr/local/cuda-9.2 export CUDNN_ROOT_DIR=/usr/local/cuda-9.2第二个是提前装好必要的系统依赖:
sudo apt-get install libopenblas-dev libopencv-dev libgflags-dev libgoogle-glog-dev然后开始编译。这里要注意,Caffe2的构建建议直接用Python的setup脚本,别用传统CMake流程,因为setup脚本会帮你处理大量Python扩展的绑定工作:
cd pytorch python setup.py build_deps python setup.py develop这个过程的编译时间取决于机器配置。我第一次用了大概40分钟出来,第二次因为系统干净、依赖正确,大概20多分钟就完成。期间如果有颜色标记的警告,建议不要忽略,很多"看似无关紧要"的警告到最后会变成硬错误。
3.3 构建Detectron核心扩展模块
Detectron中大量模块是用C++和CUDA扩展写的,必须单独编译。官方提供了对应的构建脚本:
cd Detectron make这里本质上是调用setuptools,把detectron包下的ops扩展编译成Python可调用的.so文件。编译前需要在make脚本里指定Caffe2的安装路径,我这样设置:
make CAFFE2_ROOT=/path/to/pytorch如果一切顺利,你会看到几个.egg-info目录生成,最终在detectron目录下出现一堆编译好的so库。验证是否成功,可以在Python环境中执行:
from caffe2.python import core import detectron如果没有报错,说明环境基本通了。
4. 编译过程中的报错排查链路
这部分是整个构建过程里最耗时的地方。我希望用一种类似"决策树"的方式,把报错现象、对应原因和解决路径写清楚,方便你按图索骥。
4.1 算子编译失败:GCC ABI版本冲突
第一次编译Detectron,报了一堆以undefined reference to开头的链接错误,指向几乎都是Caffe2算子符号。这玩意儿很迷惑人,看起来像是Caffe2没编译好,但实际上问题出在GCC的ABI兼容性上。
Caffe2编译时用的是系统GCC 7.x,但Detectron扩展编译时,如果环境里某些库的依赖指向了旧GCC ABI,或者反过来,就会出现找不到符号的问题。这个需要保证两处编译使用同一个编译器。具体操作是把老版本的GCC软链到/usr/bin/gcc、g++,然后在编译前:
export CC=/usr/bin/gcc-7 export CXX=/usr/bin/g++-7如果符号冲突还存在,可以考虑清理Detectron的build缓存重建一次。这个坑解决后,编译速度直接流畅。
4.2 cuDNN头文件路径导致的算子注册失败
我秘遇到过Detectron编译完成后,Python导入时报找不到某个算子的错误,类似No operator registered。这个非常坑,原因往往在于构建时刻的cuDNN路径和运行时刻不一致。第一次编译时我因为某种原因,把cuDNN的库文件复制到了系统目录,但头文件还保留在CUDNN目录里,导致编译链接和运行时查找的库版本出现漂移。
解决方式很直接:编译前检查/usr/local/cuda-9.2/include/cudnn.h是否存在,并确保这与libcudnn.so的软链版本一致。然后重新完整执行make。不要用软链的方式把cudnn库设置到多个目录,尽量统一路径。
4.3 protobuf版本错乱引发的序列化崩溃
环境构建成功后,我跑第一个推理测试,结果在做目标框解码时出现莫名其妙的崩溃,回溯栈全部指向protobuf。原因我在前面稍微提过。Detectron内置的caffe2算子需要加载预训练模型,模型文件内部的结构是protobuf序列化格式。如果Python环境中的protobuf版本过新,与Caffe2编译时使用的protobuf头文件版本不匹配,反序列化时会直接读取乱码或者触发断言。
这个问题的排查链路很短,解决也很直接——把protobuf锁定到3.6.x:
pip install protobuf==3.6.1值得注意的是,有些时候不是通过pip安装的protobuf,而是随conda一起安装的新版本。需要先卸载干净再安装锁定版本。我自己用conda remove protobuf清除后,才pip指定安装,问题彻底消失。
4.4 Python数据层加载图片时的OpenCV兼容问题
还有一个很折腾的问题出现在数据读取环节。Detectron的数据加载层使用opencv读取图片,然后在Python层做transpose和归一化。新版本opencv对EXIF方向的处理方式不同,导致图片被自动旋转,检测框全错位。因为Detectron的代码假定opencv读图后不做旋转,而opencv 4.x却有这个行为。
所以将opencv-python锁定为3.4.x是有实际检测效果意义的。遇到这个bug时,我还误以为是模型权重没对齐,又去重新梳理了一遍预训练模型转换流程,浪费了不少无用功。后来用一个小脚本验证读取图像的长宽和像素值与训练脚本完全一致,才发现是opencv的问题。
5. 跑通推理流程后的验证与踩坑心得
环境构建完成,只是万里长征第一步。你得跑通一个真实模型,才能证明这套环境是健康可用的。我当时使用Detectron提供的预训练模型做目标检测推理,这个过程中又遇到一些问题,顺带分享下我的验证方法。
5.1 用预训练模型快速验证环境完整性
下载对应模型的权重文件,然后把一张包含常见物体的测试图片丢进推理脚本。命令如下:
python tools/infer_simple.py \ --cfg configs/12_2017_baselines/e2e_mask_rcnn_R-50-FPN_1x.yaml \ --output-dir /tmp/detectron-visualize \ --image-dir /tmp/test-images \ --wts /path/to/model.pkl这里有一个经典坑:权重文件的后缀明明是.pkl,但内部序列化是caffe2的格式,加载过程使用pickle模块,但需要预先通过caffe2_pb2来初始化环境。如果直接在Python脚本里导入Detectron模块,包装器会自动处理这些。但如果你在notebook里先在别的位置加载了torch,再导入caffe2,大概率会遇到protobuf定义冲突。我建议所有流程都在顶层新建一个脚本来执行,不要跟其他框架混合导入。
5.2 性能评估和边界测试
跑通了基本检测后,最好做一次性能冒烟测试。用一个视频片段或者连续图片序列做推理,观察平均帧率是否在预期范围内。我这里测出来的帧率和老文档里的记录差不多,这表明整个环境不是假通,而是真能用的状态。
另外一个建议是测试不同尺度的图片。因为Detectron内部使用blob格式保存数据,图片缩放流程如果出错,不会直接崩溃,可能只是检测框偏移。我当时测试了从200像素到2000像素宽的图片,观察模型输出框的大小和位置是否平滑变化。这里是一个验证整个数据流的好方法。
6. 构建Caffe2与Detectron环境的通用建议
最后这部分,是我经过多次折腾之后的一些总结性建议,也相当于是收尾阶段的经验输出。
6.1 尽量在全新虚拟机或容器环境内操作
这套环境的依赖污染风险很大。即便你用conda新建了环境,依然可能受系统路径下某些库的影响。一个比较推荐的做法是,在一个干净的虚拟机里按步骤操作,完成后将虚拟机镜像留存。这样哪怕后续代码环境崩了,也不用从零开始重来一遍。
我当时为了Caffe2的构建,重新做了三次操作系统级别的环境,最后才整理出这篇笔记。第一次用了系统自带的Python 3.8,GCC 9,结果Cython生成的代码各种报错;第二次换了Python 3.6但忘了锁定protobuf,Opencv版本过高导致推理框错位;第三次严格按照上面的流程走,才总算顺畅。
6.2 记录编译日志,比记答案更重要
每次执行编译命令时,建议把输出完整保存到日志文件:
python setup.py build_deps 2>&1 | tee build.log当出现问题时,先看日志第一条警告,往往就是后续错误的源头。不要一上来就疯狂搜索错误码,很多报错在日志前200行就已经埋下伏笔。把日志保存下来,还可以方便你在对比不同依赖版本时,快速定位差异。
6.3 老代码环境的"时间胶囊"意识
构建这类老项目环境,本质上是在做一个时间胶囊。你装的所有依赖版本,都应该锁定在当时的时空切片里。这意味着不要轻易升级任何库,哪怕是看似无关紧要的Pillow。一些琐碎到不起眼的库更新,可能在某个深层调用路径里改变行为,触发难以发现的bug。
我个人的习惯是构建完成后,立即用pip freeze导出完整依赖清单,再配合conda环境导出功能保存整个环境状态。如果未来再遇到同样的项目,我都可以直接还原,不用再搜索资料。这也是我要写这篇笔记的初衷——环境构建是个繁琐的体力活,但也是有章法可循的事情。
写到最后,我发现这套老环境虽然年代久远,但构建思路对于今天依然有借鉴意义:理解版本约束关系、控制变量、严格验证运行链路。每一步操作背后的原因,比命令本身重要得多。