从云原生走向 AI 原生:一套面向未来的架构方法论 → 阅读《AI 原生基础设施》

用 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=nvidiaNVIDIA_VISIBLE_DEVICES 环境变量的旧写法仍能见到,语义等价。

不要把 GPU UUID 写进应用配置
一次性 docker run 里用 device=0,2 无妨,但把 GPU UUID/序号写进应用的 YAML 是反模式:硬件更换、节点迁移都会让它失效。“哪块卡给谁”应当交给调度器决定,这是控制面整部分的主题。

选对基础镜像

NVIDIA CUDA 镜像一个名字有三种 tag,区别常被忽视:

Tag内容体积适用
base驱动兼容层与最小 CUDA 库最小只跑已编译好的程序、nvidia-smi 冒烟
runtimebase + CUDA 运行时库运行 CUDA 应用(多数推理场景够用)
develruntime + nvcc、头文件需要在容器内编译 CUDA 代码(如源码装 flash-attention)
表 1: nvidia/cuda 镜像三种 tag

两条选择建议:优先用框架官方镜像(如 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 文档
表 2: 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-smiDriver 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 这样一行可调度的资源请求。

创建于 2026/09/14 更新于 2026/09/14 2161 字 阅读约 5 分钟