你有没有过这种体验:手里有一块还不错的NVIDIA显卡,想跑百度Apollo做点感知或规划相关的实验,结果照着网上几篇文章折腾两天,不是Docker起不来,就是编译到一半被各种报错打断。我自己第一次在Ubuntu 20.04上从源码编译Apollo时,就被“GPU环境配置”这个看似简单、实则全是坑的环节卡了很久。
这篇教程我不敢说能把所有版本的坑都踩平,但至少能给你一套可以完整走下去的路线:从Ubuntu系统准备、NVIDIA驱动安装、Docker GPU支持,到Apollo源码拉取、开发容器启动、CPU/GPU模式编译,再到最后的Dreamview验证和常见报错排查。整个流程我按“保姆级”的标准来写,每一步都解释为什么这么做,而不是只扔一串命令。
1. 为什么偏要自己编译Apollo,不直接用官方镜像?
1.1 预编译镜像和源码构建,其实是两条路线
Apollo官方确实提供了预编译好的Docker镜像,拉到本地跑起来就能看到Dreamview界面,配合官方demo数据回放一下,确实很爽。但这里有个关键区别:预编译镜像里装的是已经编好的二进制产物,你拿到的只是一个“黑盒”。你要是想改感知模块里的一个后处理参数,想在规划模块里加自己的逻辑,或者想把某个模型换成自己的训练结果,光靠改配置文件是不够的,必须重新编译相关模块。
源码编译做的事情,就是把Apollo整个工程里的C++、Python、Protobuf、CUDA相关代码,在你自己机器上从头生成一遍二进制。这个过程虽然耗时,但会把你机器上的环境、依赖、编译参数全部固化下来。简单说,预编译镜像是租来的精装房,源码编译是拿毛坯房自己装修,装修过程累,但你知道每一根管线是怎么走的。
1.2 编译这件事的真实性价比
先说结论:如果你只想跑通一个demo、看看Apollo长什么样,源码编译的性价比极低,直接拉官方镜像就完事了。但如果你属于下面这几类人,源码编译几乎是绕不过去的门槛:
- 要改Apollo内部代码,不管是感知、预测、规划还是控制模块;
- 要把Apollo和自研系统做集成,需要知道模块间的编译依赖;
- 做自动驾驶方向的研究生或工程师,面试或汇报时被问到“Apollo的编译流程是怎样的”;
- 需要在没有官方预编译支持的硬件或环境上做适配。
我个人的建议是:第一次编译,挑一个周末的上午开始,不要在工作日的晚上搞。编译过程中大概率会遇到一两个意想不到的环境问题,留足时间比留足耐心更重要。
1.3 版本选择:Ubuntu 20.04对应哪代Apollo
Apollo的迭代速度很快,不同版本的系统和Docker镜像依赖差别不小。从支持矩阵来看,Apollo 7.0及以上版本对Ubuntu 20.04的支持已经很成熟,我自己用的就是Apollo 7.x分支。如果你还在用Apollo 5.0或更早的版本,Ubuntu 20.04上会遇到不少兼容性麻烦,建议先升级。
选版本这事,我的建议是不要追最新的master分支,因为它可能每天都在变,今天能编过的代码,下周可能就新增了一个依赖。选定一个release分支,比如7.0.0、8.0.0这样的稳定tag,后面遇到问题也容易在社区里搜到答案。
2. 硬件体检与Ubuntu基础环境准备
2.1 硬件准入清单
Apollo本身的模块非常重,如果硬件不达标,后面的编译和运行体验会非常难受。我整理了一个参考表,注意是“参考”,不是绝对门槛:
| 部件 | 最低配置 | 推荐配置 | 说明 |
|---|---|---|---|
| CPU | 8核 | 16核及以上 | 编译时Bazel会并行跑编译任务,核越多越快 |
| 内存 | 16GB | 32GB及以上 | 16GB编译大模块时容易OOM |
| 磁盘 | 50GB空闲 | 100GB SSD | 源码+镜像+编译缓存会吃掉大量空间 |
| GPU | NVIDIA GTX 1060 | RTX 3070及以上 | 显存至少6GB,感知模块很吃显存 |
| 网络 | 稳定宽带 | 稳定宽带 | 拉镜像和依赖时速度很重要 |
这里要特别强调一下磁盘。很多人只盯着源码体积,却忽略了Docker镜像和Bazel缓存。Apollo的dev容器镜像通常有几个GB,编译产生的缓存和临时文件动辄几十GB,如果你系统盘只有100GB,很容易编到一半磁盘满了。
2.2 先确认GPU驱动状态
在装任何东西之前,先用两个命令确认系统状态:
# 查看系统版本 lsb_release -a # 查看显卡硬件型号 lspci | grep -i nvidia如果能看到类似NVIDIA Corporation GP104 [GeForce GTX 1080]的输出,说明系统至少识别到了显卡。接下来看驱动是否已经正常:
nvidia-smi如果显示了一大块表格,右上角有Driver Version和CUDA Version,恭喜你,驱动这关已经过了。如果提示command not found,说明驱动没装好或者完全没装。
2.3 没有驱动时的安装选择
Ubuntu 20.04上安装NVIDIA驱动常见有两种方式:
方式一:直接用系统仓库的驱动(推荐新手)
# 查看系统推荐的驱动版本 ubuntu-drivers devices # 安装推荐版本 sudo apt install nvidia-driver-545 sudo reboot这种方式省事,驱动版本经过Ubuntu测试,稳定性有保障。缺点是你拿到的不是最新驱动,但Apollo对驱动版本要求并不苛刻,只要能满足NVIDIA Container Toolkit的运行需求就行。
方式二:用NVIDIA官方runfile安装
这种方法可以精确控制版本,但安装过程需要先关闭图形界面服务,步骤多、容易出错,新手不建议一上来就搞。如果你确实需要某个特定驱动版本,再考虑这条路。
2.4 处理nouveau,把驱动冲突扼杀在源头
这是新手最容易忽略的一步。Ubuntu自带一个开源的nouveau显卡驱动,如果不禁用它,NVIDIA官方驱动装上后可能无法正常工作,表现是nvidia-smi能显示但图形界面卡死,或者开机黑屏。
禁用方法:
sudo bash -c "echo 'blacklist nouveau' >> /etc/modprobe.d/blacklist-nouveau.conf" sudo bash -c "echo 'options nouveau modeset=0' >> /etc/modprobe.d/blacklist-nouveau.conf" # 重新生成initramfs sudo update-initramfs -u # 重启 sudo reboot重启后确认nouveau已经不再加载:
lsmod | grep nouveau这条命令没有输出就说明禁用成功。之后再安装NVIDIA驱动就干净多了。
3. Docker与NVIDIA Container Toolkit:GPU进入容器的关键一跳
3.1 Docker是Apollo的开发载体,不只是部署方式
Apollo从很早的版本开始就拥抱了Docker,把整个编译环境和运行环境都封装在容器里。这样做的好处是:你不需要在宿主机上装一堆特定版本的依赖,也不怕搞坏系统环境。坏处是:如果你对Docker不熟悉,会感觉多了一层无形的墙。
编译Apollo源码时,我们并不是直接在Ubuntu里执行g++或make,而是把源码挂载进一个官方提供的开发容器,然后在容器内部执行Bazel构建命令。这个容器里已经预装好了编译需要的工具链、CUDA Toolkit、依赖库。
3.2 把NVIDIA Container Toolkit装好
要让容器里能访问到宿主机GPU,光装个Docker是不够的。Docker容器默认是没有GPU设备的,需要借助NVIDIA Container Toolkit把GPU设备映射进容器。
安装步骤通常是这样:
# 添加NVIDIA的软件源(以官方仓库为例) distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt update sudo apt install -y nvidia-container-toolkit # 配置Docker使用NVIDIA runtime sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker这里面的nvidia-ctk runtime configure很多人会漏掉,它会把NVIDIA runtime自动写进Docker的daemon配置里。不执行这一步,后面的--gpus参数可能不会生效。
3.3 用一条docker命令验证容器GPU可用
装好之后,先别急着拉Apollo镜像,用一条最简单的命令验证容器里能不能看到GPU:
docker run --rm --gpus all nvidia/cuda:11.4.0-base-ubuntu20.04 nvidia-smi如果能看到GPU信息,说明宿主机、Docker、NVIDIA Container Toolkit三者已经打通。如果报错,我建议你先在这里把问题解决,不要带着这个问题去碰Apollo,否则后面一片混乱。
碰到最常见的一个报错是:
could not select device driver "" with capabilities: [[gpu]]这个基本就是nvidia-container-toolkit没装好,或者Docker daemon没有加载NVIDIA runtime。回到上一步,重新执行sudo nvidia-ctk runtime configure --runtime=docker再重启Docker。
4. Apollo源码获取,以及工程目录怎么读
4.1 拉代码前的分支选择
Apollo的代码托管在GitHub上,项目名是ApolloAuto/apollo。拉取源码的方式:
git clone https://github.com/ApolloAuto/apollo.git cd apollo # 查看所有远程分支,选择一个稳定release git branch -r # 切换到具体的release分支 git checkout -b apollo7 origin/release/7.0.0这里有个经验:不要直接用master分支。master分支是日常开发分支,可能处于不可编译状态。release分支经过完整测试,踩坑概率小很多。
如果你只需要最新某分支的最新代码,可以用--depth 1参数做浅克隆,节省时间和磁盘:
git clone --depth 1 --branch release/7.0.0 https://github.com/ApolloAuto/apollo.git4.2 源码目录的结构地图
很多新手把源码拉下来之后不知道怎么读,我标注一下几个关键目录:
| 目录/文件 | 作用 |
|---|---|
modules/ | 核心自动驾驶模块,包括perception、prediction、planning、control等 |
cyber/ | Apollo自研的通信中间件,类似ROS |
docker/ | Docker相关脚本和Dockerfile,包括dev_start.sh等 |
scripts/ | 各种辅助启动脚本 |
apollo.sh | 顶层构建脚本,编译、测试、格式化都要用它 |
.env | 环境变量配置,比如用户UID映射 |
还有一个不显眼但很重要的目录third_party,里面放了一些第三方依赖的构建规则。编译时你会发现Bazel会频繁引用这个目录。
4.3 框架配置文件与访问凭据
在源代码根目录下有一个.env文件,里面的配置会影响后续容器的启动。我自己遇到的一个典型问题是容器内用户权限和宿主不一致,导致源码目录在容器内无法写入。解决办法就是在.env里把用户UID和GID设置成宿主用户的:
# 查看宿主机当前用户的UID和GID id -u id -g然后在.env文件里确认APOLLO_BAIDU_UID和APOLLO_BAIDU_GID和上面输出一致。这样容器内创建的文件在宿主机上也能正常读写。
另外,Apollo的构建过程需要从官方平台获取一些数据资源和依赖,你在跑dev_start.sh或后续编译前,需要先在Apollo开放平台上注册并获取访问凭证,然后按照官方文档的指引把凭证配置到环境变量里。这一步各家教程说法不太统一,但核心逻辑就是:让Apollo构建系统在下载数据时能识别你的身份。不要跳过,不然编译到一半会出现一个让你摸不着头脑的下载失败。
5. 启动开发环境容器,让编译在隔离环境进行
5.1 dev_start.sh参数及含义
Apollo提供了一组脚本来管理开发容器,核心就两个:dev_start.sh和dev_into.sh。启动开发容器的标准姿势:
cd apollo # -g表示使用GPU bash docker/scripts/dev_start.sh -g-g这个参数很关键,它告诉脚本要启动一个可以访问GPU的容器。如果你漏了它,容器就算起来了,后面编译GPU模块时也会找不到CUDA设备。
启动过程中脚本会检查本地有没有Apollo dev镜像,没有的话会自动拉取。这一步消耗时间取决于网络状况,有时候会等很久。我建议在没有其他网络占用的时候做这一步。
5.2 dev_into.sh进入容器与权限处理
容器启动之后,另一个终端执行:
bash docker/scripts/dev_into.sh正常情况下你会进入容器的shell,提示符会显示类似apollo@in-dev-docker这样的字样。注意,这时候你不是root用户,而是Apollo自动映射的普通用户。很多人在这时候用sudo发现没有权限,以为容器坏了,其实这是故意的,为了安全和权限一致。
进入容器后,先确认几个信息:
# 确认源码被挂载到容器内 ls /apollo # 确认GPU设备在容器内可见 nvidia-smi/apollo目录就是宿主机上源码挂载进来的路径。nvidia-smi有输出,说明GPU映射成功。
5.3 镜像拉取失败或启动超时的排查逻辑
第一次dev_start.sh最常见的两个问题:
问题1:镜像拉取很慢或超时
这种问题没有灵丹妙药,只能换网络环境或者配置Docker镜像加速器。Apollo镜像体积不小,耐心等是常态。如果反复拉到一半断掉,可以手动docker pull镜像的完整名称,再重新跑dev_start.sh。
问题2:容器启动后立刻退出
多半是环境变量或端口冲突。先看容器状态:
docker ps -a docker logs apollo_dev_<你的用户名>日志里通常会有明确提示,比如端口被占用或者权限不足。Apollo默认会映射一些端口到宿主机,比如8888、8889等端口。如果本机已经有程序占用这些端口,容器就起不来,需要调整映射或关掉冲突进程。
6. 源码编译全流程,CPU/GPU构建的底层差异
6.1 GPU构建比CPU构建多做了哪些事
Apollo的构建脚本同时支持CPU和GPU两种模式。很多人不理解为什么还有CPU模式,以为所有模块都必须GPU才能跑,其实不是。
- CPU模式:把代码编译成纯CPU可执行版本,感知模块里依赖CUDA的部分会被禁用或降级,适合没有NVIDIA显卡的机器做纯算法开发。
- GPU模式:在CPU模式基础上,额外编译CUDA扩展和GPU算子,感知模块能用上GPU加速推理。
GPU模式编译出的二进制文件比CPU模式大不少,因为里面嵌入了CUDA kernel代码和GPU相关依赖。如果你的机器有NVIDIA显卡,务必用GPU模式,否则后面跑感知demo时会报CUDA错误,或者模型推理速度慢到让人崩溃。
6.2 编译命令与日志解读
在容器内执行:
cd /apollo # GPU模式构建 ./apollo.sh build_gpu # CPU模式构建(无NVIDIA卡时) ./apollo.sh build_cpu有些新版本Apollo把这两种模式合并到了统一的build命令里,通过参数区分。具体以你拉取的版本里的./apollo.sh --help输出为准,但build_gpu和build_cpu这组命令在多数release版本中仍然有效。
编译开始后,你会看到Bazel输出满屏的进度信息,类似:
[52,700 / 53,200] ... Compiling modules/perception/lidar/...; 4s local [53,100 / 53,200] ... Linking modules/planning/scenario_manager; 12s local看到这种输出说明Bazel正在并行编译。整个过程可能从半小时到两三个小时不等,取决于你机器的核心数和磁盘性能。编译期间不要手动去杀Bazel进程,也不要频繁中断,否则下次重新编译要从断点继续,虽然增量构建会接着跑,但上下文切换本身也耗时。
6.3 编译真正能提速的几个点
第一次编译慢是没办法的,但后续增量编译可以明显加速,前提是你做对几件事:
第一,不要随便清理Bazel缓存。Apollo的构建缓存叫bazel-*符号链接,可以占用几十GB空间。很多人为了省空间把它删了,结果下次编译一下回到了解放前。真要清理,也等确认不会再频繁改动代码再做。
第二,控制并行作业数。Bazel默认会按CPU核心数开并行任务,但每个任务吃内存。如果编译过程中发现内存接近满,甚至出现OOM killed,可以限制作业数:
# 容器内设置 ./apollo.sh config --release export BAZEL_JOBS=8 ./apollo.sh build_gpu第三,保持源码目录在一个稳定的文件系统上。如果你把源码放在机械硬盘,编译速度会被磁盘IO拖累;如果放在网盘挂载目录或FUSE文件系统,Bazel甚至会因为文件锁问题报错。把源码放在本地SSD上是最稳的。
7. 编译成果验证与Dreamview运行
7.1 多少产出物才算编译成功
当Bazel输出类似以下的结尾时,说明编译链路基本走通了:
Build completed successfully, 53200 total actions但也有一种情况是,Bazel显示successfully,你启动Dreamview却仍然报缺少库。这是因为Apollo有些模块是动态库,有些是独立的二进制,全量编译完成后应该在/apollo/output或其他导出目录生成最终的部署产物。部分版本需要额外执行导出步骤:
./apollo.sh deploy或者用./apollo.sh build时加上--clean_output等标志来生成干净的输出目录。具体看脚本提示,但核心思路是:Bazel编译成功 ≠ 运行环境ready,你可能还需要把产物“安装”到运行目录。
7.2 启动Dreamview并回放demo数据
编译完成后,最好验证一下Dreamview能不能正常起来。通常流程是:
# 容器内启动Dreamview bash scripts/bootstrap.sh start看到类似Dreamview is running at http://localhost:8888的提示后,打开宿主机浏览器访问http://localhost:8888。如果你看到前端界面加载出来了,说明编译产物能正常运行。
接下来回放demo数据。Apollo官方会提供一批demo数据,你需要按照上述凭证流程获取并放到指定数据目录。在Dreamview界面里选择相应数据包,点击播放,如果看到可视化界面里出现了道路、车辆、障碍物框等元素,就说明整套链路通了。
7.3 自查GPU和CPU资源占用
验证阶段还有个很有用的动作:在宿主机开一个终端跑nvidia-smi,观察Apollo运行时的GPU使用率。正常的感知模块推理阶段,GPU显存使用应该明显上升。如果nvidia-smi完全没变化,很有可能是容器GPU映射出了问题,或者你跑的demo没有启用GPU推理。
8. 高频报错排查:分层定位不瞎猜
8.1 GPU层问题
症状:容器内nvidia-smi报command not found或could not access NVIDIA GPU。
先回宿主机制确认nvidia-smi正常,再确认容器启动时是否加了-g参数,以及NVIDIA Container Toolkit是否安装完整。如果宿主机驱动版本过低或损坏,容器里的CUDA即使存在也访问不了硬件,这时候卸载重装驱动比在容器里折腾有效得多。
8.2 Docker层问题
症状:dev_start.sh启动时报容器名称冲突。
用docker ps -a查看是否有之前残留的容器,删掉即可:
docker rm -f apollo_dev_<你的用户名>症状:容器可以启动,但容器内的源码目录只读。
检查.env文件中UID/GID是否和宿主用户一致。不一致的话,容器内用户对挂载的源码目录没有写权限,Bazel生成临时文件时就会失败。
8.3 Bazel、磁盘和内存问题
症状:编译过程中提示No space left on device。
先用df -h查看系统盘和源码所在分区。Apollo编译的临时目录默认在/root/.cache/bazel或/home/用户/.cache/bazel,如果你把源码放一个盘,家目录缓存却在另一个小分区,很容易爆。解决方案是把Bazel缓存目录改到空间大的分区:
export TEST_TMPDIR=/bigdisk/bazel_cache症状:编译时Killed,或者日志末尾是OutOfMemory。
这是内存不够。减少并行作业数:在容器内执行echo 4 > $(bazel info output_base)/...这种方式比较复杂,更简单的方法是直接给Docker容器增加内存限制或关掉其他占用内存的应用。如果是16GB内存的机器,建议编译时不要同时开浏览器、IDE和一堆窗口,给Apollo留足内存。
8.4 运行时层问题
症状:Dreamview页面能打开,但看不到仿真场景或数据回放卡住。
先检查数据文件路径是否放对,再检查docker logs <容器名>或Dreamview前端的控制台是否报错。很多时候是端口没映射完整,或者浏览器缓存了旧版前端,清一下缓存或换个浏览器试试。
我在实际编译Apollo的过程中最大的体会是:源码编译这件事,环境和流程比代码本身更容易卡住人。很多人编译失败不是因为不会敲命令,而是因为某一步的环境状态没有验证就往下走,等到最后报错时才发现根因在很前面的地方。
所以这套流程里我特别强调一个习惯:每做完一个阶段性的步骤,先确认输出,再进入下一步。检查GPU驱动用nvidia-smi,检查容器GPU映射也用nvidia-smi,检查编译产物就看Bazel输出。每一步都确认过了,后面真的会顺畅很多。
另外,编译Apollo是个体力活,机器好能省很多时间,但机器一般也不意味着不行——我就在一台8核16GB的老笔记本上成功编译过,只是花的时间多一些,中间还插了两次内存不足的坑,最后把并行度调低也就过了。希望你读到这里,已经对从源码编译Apollo的整个过程心里有底了。接下来,大胆试就行。