在 20.1 节中,我们完成了 GPU 驱动、CUDA、cuDNN、NCCL 等基础环境的手动部署。但生产环境中直接“裸机安装”会带来两个致命问题:
- 环境漂移:A 节点 CUDA 12.1,B 节点 CUDA 12.2,同一个训练脚本跑出不同结果甚至直接崩溃;
- 依赖地狱:PyTorch 版本、Python 版本、系统库版本互锁,运维人员每天在“装环境”和“修环境”之间疲于奔命。
容器化是算力中心软件栈的第一层胶水。它把模型训练/推理所需的完整运行时(OS 用户态、CUDA 库、Python 依赖、框架版本)打包成一个镜像,确保开发环境、测试集群、生产集群完全一致。本节从零开始,覆盖 Docker 基础、NVIDIA 容器生态、GPU 透传原理,以及一个可落地的 Dockerfile 最佳实践。
一、为什么 GPU 容器不是普通 Docker 那么简单?
标准 Docker 容器共享宿主机的 Linux 内核,本身看不到 GPU。因为 GPU 不属于操作系统标准设备模型(像 CPU、内存那样天然可见),而是通过内核驱动模块(nvidia.ko)暴露的设备文件。
要让容器内能调用 nvidia-smi、跑 CUDA 代码,必须解决三层穿透:
- 设备文件穿透:将
/dev/nvidia*等设备节点映射进容器; - 驱动库穿透:将宿主机的 CUDA 驱动库(libcuda.so)挂载进容器;
- 运行时约束:容器进程必须能调用 GPU 驱动 ioctl 接口。
早期方案(如 --device /dev/nvidia0 手动挂载)极其脆弱——驱动版本一升级,容器镜像就得重做。
NVIDIA Container Toolkit 正是为此而生。它提供了一套标准化的容器运行时,让 docker run --gpus all 一行命令就能完成所有穿透。
二、核心组件:nvidia-docker2、nvidia-container-runtime、nvidia-container-toolkit
这套工具的演进史就是从“外壳脚本”到“OCI 运行时钩子”的标准
化之路。当前生产环境推荐直接使用 nvidia-container-toolkit ,历史组件 nvidia-docker2 作为过渡。
部署步骤(Ubuntu 22.04,适用绝大多数算力节点):
# 1. 添加 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
# 2. 安装
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
# 3. 重启 Docker 服务
sudo systemctl restart docker
验证命令:
docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi
如果输出显示 GPU 信息和驱动版本,说明穿透成功。
原理简述:nvidia-container-toolkit 通过一个 OCI prestart hook,在容器启动瞬间自动挂载所需文件。底层利用 libnvidia-container 库解析 GPU 驱动版本和 CUDA 库依赖关系,自动注入环境变量(如 NVIDIA_VISIBLE_DEVICES)和设备挂载。
三、镜像选型:Base、Runtime、Devel 的差异与选择
NVIDIA 官方在 Docker Hub 上提供了三类 CUDA 镜像,选错会多出几个 GB 的无用层:
| 镜像类型 | Tag 示例 | 包含内容 | 适用场景 |
|---------|---------|---------|---------|
| base | nvidia/cuda:12.1.0-base-ubuntu22.04 | CUDA 运行时库(libcuda.so),不含编译工具 | 部署推理服务(仅运行) |
| runtime | nvidia/cuda:12.1.0-runtime-ubuntu22.04 | base + 共享 CUDA 库 + NCCL | 训练和推理的推荐基线 |
| devel | nvidia/cuda:12.1.0-devel-ubuntu22.04 | runtime + nvcc 编译器 + 头文件 + 静态库 | 编译 CUDA 扩展(如 flash-attention) |
实用建议:
- 训练节点直接选 runtime:现代框架(PyTorch、TF)自带预编译 CUDA 算子,不需要 nvcc 编译器;
- 需要编译 CUDA kernel 时才用 devel:例如安装 flash-attention、vLLM 源码编译,或调试自定义 CUDA 算子;
- 推理服务用 base 或 runtime 即可:配合 PyTorch 官方 Docker 镜像,进一步瘦身。
如何看镜像花了多少空间? base: 12.1.0 约 130 MB,runtime 约 500 MB,devel 超过 3 GB。别让“用 devel 省事”的思维浪费集群存储。
四、实战 Dockerfile:一个可直接复用的训练镜像模板
以下是一个经过生产验证的 PyTorch 训练镜像 Dockerfile,包含中文支持、SSH 无密码登录(多节点通信)、常用工具和 NVML 库:
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
# 基础系统依赖
RUN apt-get update && apt-get install -y \
wget curl git vim htop net-tools openssh-server \
&& apt-get clean
# 配置中文环境(防止日志乱码)
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# 安装 Miniconda(环境隔离器)
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh \
&& bash ~/miniconda.sh -b -p /opt/conda \
&& rm ~/miniconda.sh
ENV PATH=/opt/conda/bin:$PATH
# 安装 PyTorch(CUDA 12.1 对应版本)
RUN pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# 配置 SSH(多节点训练需要免密登录)
RUN mkdir /var/run/sshd \
&& echo 'root:YOUR_PASSWORD' | chpasswd \
&& sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ''
# 启动 SSH 服务(容器内需要 supervisor 或直接启动脚本)
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
注意事项:
- 生产环境不要硬编码密码,改用 SSH key 注入;
- 训练脚本通常通过
docker run -v /data:/data挂载数据卷,不要在镜像里打包数据; - 多节点训练时,所有节点必须基于 相同镜像 ID,否则 OpenMPI/ NCCL 可能因库版本微小差异而报错。
五、GPU 资源隔离:--gpus vs 环境变量 vs MIG
启动容器时,你可以精确控制哪些 GPU 对容器可见:
# 所有 GPU
docker run --gpus all ...
# 仅使用编号 0,2 的 GPU
docker run --gpus '"device=0,2"' ...
# 通过环境变量控制(与 --gpus 结合)
docker run --gpus all -e NVIDIA_VISIBLE_DEVICES=0,2 ...
对于 A100/H100 等支持 MIG(多实例 GPU) 的硬件,还可以直接在容器内启用 MIG 配置:
# 宿主机先切分 MIG
nvidia-smi mig -cgi 9,9,9 # 将一块 A100-40GB 切为 3 个 13GB 实例
# 启动容器挂载其中一个实例
docker run --gpus '"device=0:0"' ... # 使用第一个 MIG 实例
这为多租户算力切分提供了硬件级隔离,大幅减少彼此的显存争抢。
六、完整工作流:从镜像到集群调度
在 20.3 节我们会集成 Kubernetes 或 Slurm,但
纯 Docker 的运行闭环如下:
- 构建镜像
docker build -t my-train:latest -f Dockerfile .
- 推送到私有仓库(如 Harbor)
docker tag my-train:latest harbor.internal/my-project/my-train:latest
docker push harbor.internal/my-project/my-train:latest
- 任意节点拉取并运行
docker run --gpus all -v /data/datasets:/data -it my-train:latest
- 进入正在运行的容器
docker exec -it <container_id> /bin/bash
- 训练失败时,通过日志排错;成功后,打包新的镜像版本进行迭代
七、常见陷阱与解药
陷阱 1:容器内 nvidia-smi 显示 “Failed to initialize NVML: Unknown Error”
- 原因:Docker 没有正确加载
nvidia-container-runtime;可能在/etc/docker/daemon.json中未配置默认运行时。 - 解法:编辑
daemon.json,加入:
{
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
},
"default-runtime": "nvidia"
}
然后 sudo systemctl restart docker。
陷阱 2:容器内 CUDA 版本与驱动不匹配
- 容器内的 CUDA Toolkit 版本必须 ≤ 宿主机驱动支持的 CUDA 版本。例如驱动 525.60.13 最高支持 CUDA 12.0,如果容器用
cuda:12.1就会报cuda driver version is insufficient。 - 检查驱动兼容性:
nvidia-smi右上角 “CUDA Version” 是驱动支持的最高版本。
陷阱 3:镜像体积膨胀
- 将
apt-get install和pip install放在一层,并清理缓存:rm -rf /var/lib/apt/lists/*。 - 使用
.dockerignore排除数据、log、.git 文件夹。
陷阱 4:多节点 NCCL 通信失败
- 容器内必须挂载宿主机的网络接口,使用
--network=host模式可避免端口映射问题,但会失去网络隔离。 - 更推荐的做法:显式暴露 NCCL 通信端口(如 29400)并使用
--ipc=host(共享内存对 NCCL 至关重要)。
八、小结
容器化是算力中心软件栈 一致性、可迁移性和效率 的基石。本节要点:
- NVIDIA Container Toolkit 让 GPU 透明穿透 Docker,告别手动挂载驱动的野蛮时代;
- 镜像选型 要克制:训练用 runtime,推理用 base,切勿无脑上 devel;
- 资源隔离 结合 MIG 与
--gpus标志,实现多租户安全共享; - 标准化 Dockerfile 是跨节点免密通信、环境复现的源头。
在下一节 20.3 中,我们将把这些容器接入集群调度系统(Slurm/Kubernetes),实现从“单机 docker run”到“千卡自动化编排”的工程跨越。