用 Docker 运行 GPU 工作负载
容器是 GPU 与 Kubernetes 之间的通用语。学会让容器合法地拿到 GPU,后面的调度、切分、共享才有的可谈。
上一章的结论在实战中的形态是:镜像带着工具链与框架走,宿主机只负责驱动。本章把这句话变成操作——装好 NVIDIA Container Toolkit,理解 --gpus 的语义,选对基础镜像,最后跑通你的第一个 GPU 容器。这一章也是通往 Kubernetes 的最后一级台阶:K8s 里 GPU Pod 的注入机制与本章完全同源。
GPU 容器与普通容器的差别
普通容器的隔离靠 namespace 与 cgroup,进程需要的资源(CPU、内存、文件系统)都由内核直接仲裁。GPU 打破了这个假设:GPU 设备不属于容器默认可见的 /dev,访问 GPU 需要的驱动库(libcuda 等)也在容器 rootfs 之外;而 cgroup 也管不住显存(详见显存管理)。
所以“GPU 容器”的本质是:在容器启动时,把指定的 GPU 设备节点与配套的驱动用户态库安全地注入进去。这件事由 NVIDIA Container Toolkit 完成。它为什么必须做成运行时钩子而不是普通的 volume 挂载、以及与 CDI 的关系,在Container Toolkit 与 CDI有完整拆解,本章直接走可用路径。
安装与配置 NVIDIA Container Toolkit
在宿主机上(Ubuntu/Debian 为例,其他发行版与最新命令以官方文档为准):
# 1. 配置软件源
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -sL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
# 2. 安装并注册到 Docker
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
# 3. 重启 Docker 使配置生效
sudo systemctl restart docker前置条件只有一条:上一章验证过的、工作正常的宿主机驱动。Toolkit 不含也不替代驱动。
--gpus 的语义
--gpus 回答“给这个容器哪些卡、给几张”:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi # 全部卡
docker run --rm --gpus 2 nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi # 前两块
docker run --rm --gpus '"device=0,2"' nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi # 指定序号--gpus all 是实验环境的默认;指定序号是一次性的调试用法。--runtime=nvidia 加 NVIDIA_VISIBLE_DEVICES 环境变量的旧写法仍能见到,语义等价。
docker run 里用 device=0,2 无妨,但把 GPU UUID/序号写进应用的 YAML 是反模式:硬件更换、节点迁移都会让它失效。“哪块卡给谁”应当交给调度器决定,这是控制面整部分的主题。选对基础镜像
NVIDIA CUDA 镜像一个名字有三种 tag,区别常被忽视:
| Tag | 内容 | 体积 | 适用 |
|---|---|---|---|
base | 驱动兼容层与最小 CUDA 库 | 最小 | 只跑已编译好的程序、nvidia-smi 冒烟 |
runtime | base + CUDA 运行时库 | 中 | 运行 CUDA 应用(多数推理场景够用) |
devel | runtime + nvcc、头文件 | 大 | 需要在容器内编译 CUDA 代码(如源码装 flash-attention) |
两条选择建议:优先用框架官方镜像(如 pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime),它替你解决了 CUDA/cuDNN/框架的版本配套;必须自建镜像时,tag 钉住完整版本号,禁止 latest——镜像里的 CUDA 版本要满足准备 GPU 运行环境一章的规则 3(≤ 宿主机驱动支持上限)。
常见故障速查
| 现象 | 常见原因 | 第一反应 |
|---|---|---|
could not select device driver "" with capabilities: [[gpu]] | Toolkit 未安装、未注册或 Docker 未重启 | 重做配置三步,确认 systemctl restart docker |
容器内 CUDA driver version is insufficient | 宿主机驱动低于镜像 CUDA 要求 | 升级宿主机驱动,或换低 CUDA tag |
容器能起但 torch.cuda.is_available() 为 False | 忘了 --gpus | 检查启动命令;容器内 ls /dev/nvidia* 确认设备可见 |
容器内 nvidia-smi 与宿主机驱动版本不一致的报错 | 镜像与驱动错配 | 核对上一章基线卡的 CUDA 上限 |
| rootless Docker / Podman 行为异常 | 运行时路径不同 | 参照所用运行时的官方 GPU 文档 |
动手实验:从裸 Docker 到第一个 GPU 容器
实验目标
在一台宿主机驱动正常的机器上完成 Toolkit 安装配置,跑通两类冒烟测试(系统级 nvidia-smi、框架级 PyTorch 断言),并用一次矩阵乘法建立单卡算力的量级直觉。
前置条件与成本预估
- 上一章体检通过的机器(本地或云实例);已装 Docker。
- 磁盘空间约 5–10 GiB(镜像下载);网络流量产生少量费用,计算成本为零(本地)或已含在实例小时价内(云)。
操作步骤
# 步骤 1:安装并配置 Toolkit(见上文三步命令),然后做最小验证
docker info | grep -i nvidia # 预期看到 nvidia 运行时已注册
# 步骤 2:系统级冒烟——容器内应看到与宿主机相同的 GPU 与驱动版本
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
# 步骤 3:框架级冒烟——PyTorch 真正完成一次 CUDA 调用
docker run --rm --gpus all pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime \
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# 步骤 4:性能直觉——单精度大矩阵乘法,折算 TFLOPS
docker run --rm --gpus all pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime python -c "
import torch, time
x = torch.randn(8192, 8192, device='cuda')
torch.cuda.synchronize(); t = time.time()
for _ in range(10): y = x @ x
torch.cuda.synchronize()
dt = (time.time() - t) / 10
print(f'achieved TFLOPS: {2 * 8192**3 / dt / 1e12:.1f}')
"验证清单
-
docker info能看到 nvidia 运行时 - 容器内
nvidia-smi的Driver Version与宿主机完全一致 - PyTorch 输出
True与正确的卡型号 - 步骤 4 输出的 TFLOPS 达到该卡标称 FP32 算力的 50%–80%(对照上一章基线卡的型号查标称值;明显偏低通常是落到了 PCIe 供电限制或占用干扰,而不是“GPU 慢”)
- 把实测 TFLOPS 记入基线卡——后续章节验证调度是否引入性能损失时,以它为参照
原理速览
nvidia-container-runtime 在容器创建前通过钩子检查 --gpus 声明,把对应设备节点(/dev/nvidia0 等)与匹配宿主机驱动版本的用户态库挂进容器。容器内的 CUDA 应用因此“以为”自己装了完整驱动,实际复用的是宿主机那一份——版本一致性由注入机制保证,这正是规则 3 的实现基础。
清理与止损
docker rmi nvidia/cuda:12.4.1-base-ubuntu22.04 pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime云实例若不再进入下一章实验,执行云上第一台 GPU 机器一章的 terminate 流程。
总结
本章跨过了 GPU 工程的第一道门槛:容器。你掌握了 --gpus 的语义、镜像 tag 的选择逻辑,并把实测 TFLOPS 记入基线卡——从此“环境有问题”和“代码有问题”可以被区分开。更重要的伏笔是:容器里的 GPU 访问靠运行时注入,而 Kubernetes 的 GPU 方案(device plugin、GPU Operator)正是把这套注入搬进 kubelet 的调度体系。下一章单机 Kubernetes 与 GPU Operator完成最后一步:让 GPU 变成 nvidia.com/gpu: 1 这样一行可调度的资源请求。