第4章:Docker环境下的驱动适配

说实话,Docker 这玩意儿刚火起来那会儿,我还有点抵触。觉得「容器?不就是个轻量级虚拟机嘛」。直到我在生产环境里被驱动版本冲突折磨得死去活来,才真正体会到容器化的价值。今天咱们聊聊 Docker 里跑 GPU 的那些坑。

4.1 nvidia-docker 的前世今生

先说说背景。早期的 Docker 是不支持 GPU 直通的。你想在容器里用显卡?得自己挂载设备文件、装驱动、配库,折腾半天还不一定能跑起来。我记得 2017 年有个项目,团队里三个人花了整整一周才把环境搭好。

后来 NVIDIA 出手了,搞了个 nvidia-docker。它的核心思路其实很简单:把宿主机的 GPU 驱动和 CUDA 运行时「透传」到容器里。说白了,容器里的进程以为自己直接操作显卡,实际上是通过宿主机的驱动栈来调度的。

核心原理:

  • 宿主机的 NVIDIA 驱动负责管理 GPU 硬件
  • 容器内只挂载必要的库文件(libcuda.so、libnvidia-ml.so 等)
  • 通过 nvidia-container-runtime 这个 hook 实现设备映射

你可能会问:「为什么不直接把驱动装容器里?」嗯,这个问题我当年也想过。答案其实很现实:驱动需要直接操作硬件,而容器共享宿主机的内核。你把驱动装容器里,它根本拿不到硬件资源。所以 NVIDIA 选择了「驱动在宿主机,库在容器内」的折中方案。

4.2 驱动透传机制详解

来,咱们画个图,把整个流程理清楚。

宿主机 GPU 硬件 NVIDIA 驱动 nvidia-container-runtime 容器 AI 训练进程 CUDA 库 libcuda.so libnvidia-ml.so /dev/nvidia* hook 注入

看到这个图你就明白了。整个流程分三层:

  1. 宿主机层:GPU 硬件 + NVIDIA 驱动 + nvidia-container-runtime
  2. 容器层:你的训练进程 + CUDA 库 + 挂载的驱动库 + 设备文件
  3. 连接层:runtime 通过 hook 机制,在容器启动时自动注入 GPU 能力

我个人习惯用 --gpus all 这个参数来启动容器。它其实是 nvidia-container-runtime 的语法糖,背后做了三件事:

  • 挂载 /dev/nvidia* 设备文件
  • 挂载 NVIDIA 驱动库到容器内
  • 设置环境变量 NVIDIA_VISIBLE_DEVICES

4.3 容器内 nvidia-smi 不可用排查

好,现在说重点。你启动了一个容器,进去之后敲 nvidia-smi,结果报错:

nvidia-smi: command not found

或者:

Failed to initialize NVML: Driver/library version mismatch

别慌。这个问题我排查过不下二十次。咱们按顺序来:

4.3.1 检查容器启动参数

首先确认你启动容器时有没有加 --gpus 参数。很多人会忘记:

# 错误示范 - 没有 GPU 支持
docker run -it pytorch/pytorch:latest bash

# 正确示范
docker run --gpus all -it pytorch/pytorch:latest bash

# 或者指定某张卡
docker run --gpus '"device=0,1"' -it pytorch/pytorch:latest bash

我曾经踩过的坑: 有次我用了 --gpus all,但容器里还是看不到显卡。查了半天发现是 Docker 版本太旧,不支持 --gpus 参数。Docker 19.03 以上才原生支持这个参数,之前的版本得用 nvidia-docker 命令。

4.3.2 检查 nvidia-container-runtime 是否安装

在宿主机上执行:

# 检查 runtime 是否可用
docker info | grep -i runtime

# 输出应该包含 nvidia
# Runtimes: nvidia runc

如果没有看到 nvidia,说明 nvidia-container-runtime 没装好。安装方法:

# Ubuntu/Debian
sudo apt-get install nvidia-container-runtime

# 或者用官方脚本
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
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-get update && sudo apt-get install -y nvidia-container-runtime

4.3.3 检查驱动版本兼容性

这是最常见的问题。容器内的 CUDA 版本和宿主机的驱动版本不匹配。你想想看,容器里装的是 CUDA 11.8,但宿主机驱动只支持到 CUDA 11.0,那肯定报错。

排查方法:

# 在宿主机上查看驱动版本
nvidia-smi

# 输出示例
# +-----------------------------------------------------------------------------+
# | NVIDIA-SMI 525.85.05    Driver Version: 525.85.05    CUDA Version: 12.0     |
# +-----------------------------------------------------------------------------+

# 在容器内查看 CUDA 版本
cat /usr/local/cuda/version.txt  # 或者
nvcc --version

我的经验: 驱动版本和 CUDA 版本有个对应关系。比如驱动 525.x 支持 CUDA 12.x,驱动 470.x 支持 CUDA 11.x。你可以在 NVIDIA 官网查兼容性矩阵。我一般会选「驱动版本比容器内 CUDA 版本高一个主版本」,这样最稳。

4.3.4 检查设备文件挂载

有时候参数对了,但设备文件没挂载成功。进容器看看:

# 在容器内检查设备文件
ls -la /dev/nvidia*

# 正常输出应该类似
# crw-rw-rw- 1 root root 195,   0 Jan 15 10:00 /dev/nvidia0
# crw-rw-rw- 1 root root 195, 255 Jan 15 10:00 /dev/nvidiactl
# crw-rw-rw- 1 root root 195, 254 Jan 15 10:00 /dev/nvidia-modeset

如果设备文件不存在,可能是 nvidia-persistenced 服务没启动:

# 在宿主机上启动服务
sudo systemctl start nvidia-persistenced
sudo systemctl enable nvidia-persistenced

4.3.5 终极排查:手动挂载

如果以上都检查了还是不行,那就手动挂载。虽然不推荐,但有时候能救命:

docker run -it \
  --device /dev/nvidia0:/dev/nvidia0 \
  --device /dev/nvidiactl:/dev/nvidiactl \
  --device /dev/nvidia-modeset:/dev/nvidia-modeset \
  -v /usr/lib/x86_64-linux-gnu/libcuda.so:/usr/lib/x86_64-linux-gnu/libcuda.so \
  -v /usr/lib/x86_64-linux-gnu/libnvidia-ml.so:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so \
  pytorch/pytorch:latest bash

注意: 手动挂载容易漏东西,而且不同驱动版本路径可能不一样。我只有在调试时才用这招,生产环境还是建议用 --gpus all。

4.4 常见问题速查表

现象 可能原因 解决方案
nvidia-smi: command not found 容器内没装 nvidia-smi 安装 nvidia-driver 或使用带驱动的镜像
Driver/library version mismatch 驱动版本与 CUDA 版本不匹配 升级驱动或降级 CUDA
Could not select device 设备文件未挂载 检查 --gpus 参数或手动挂载
CUDA error: no kernel image CUDA 架构不匹配 编译时指定正确的架构参数
Failed to initialize NVML NVML 库版本不对 检查 libnvidia-ml.so 挂载路径

嗯,基本上把 Docker 里 GPU 适配的坑都过了一遍。说实话,这些问题大多数都是配置问题,不是代码问题。你只要把驱动版本、容器参数、设备挂载这三件事搞清楚了,90% 的问题都能解决。

最后说一句:别在生产环境里用 latest 标签。我见过太多人因为镜像版本漂移导致环境不一致。固定版本号,比如 pytorch/pytorch:1.13.1-cuda11.6-cudnn8-runtime,能省去很多麻烦。

一句话总结: Docker 里的 GPU 适配,本质就是「宿主机管驱动,容器管库,runtime 管连接」。把这个三角关系理清了,排查问题就有方向了。

可可精品资料库