Docker 基础体系 · 第 49/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker GPU 容器:NVIDIA Runtime、设备、驱动、资源和可观测
GPU 容器不是“把一块显卡映射进容器”这么简单。一个可运行的 NVIDIA GPU 容器,至少需要同时满足以下条件:
- 宿主机存在可用的 NVIDIA GPU 和内核驱动;
- Docker 能在创建容器时请求 GPU 设备;
- NVIDIA Container Toolkit 能把设备节点、用户态驱动库和相关能力注入容器;
- 容器内的 CUDA、深度学习框架与宿主机驱动版本兼容;
- 资源限制、设备可见性和监控方式符合实际部署目标。
本文讨论 Linux 容器边界内、现代 Docker Engine、BuildKit 和 Compose 规范下的 NVIDIA GPU 容器。Windows 容器、AMD ROCm、Intel GPU 和 Kubernetes 调度不属于本文的运行时范围,但其中部分设备隔离原理是相通的。
一、先区分四个容易混淆的对象
1. NVIDIA 驱动
NVIDIA 驱动主要运行在宿主机上,包括:
- 内核模块;
/dev/nvidia*设备节点;- 用户态的 NVML、CUDA Driver API 等库;
- 负责与 GPU 交互的服务或内核接口。
驱动决定了宿主机是否能识别 GPU,也决定了宿主机能够支持的 CUDA Driver API 能力。
容器通常不携带一套独立的内核 GPU 驱动。容器中的 CUDA 用户态库通过 NVIDIA Container Toolkit 注入或使用镜像中的兼容库,但最终仍然通过宿主机内核驱动访问真实 GPU。
因此,下面这个关系成立:
容器隔离了进程、文件系统和部分设备视图,但没有把物理 GPU 的内核驱动复制成一份容器私有驱动。
2. CUDA Toolkit
CUDA Toolkit 是开发和运行 CUDA 程序的一组用户态组件,例如:
nvcc编译器;- CUDA Runtime;
- CUDA 数学库;
- cuBLAS、cuDNN、TensorRT 等上层库;
- CUDA Driver API 的用户态接口。
“镜像带 CUDA”不等于“镜像带宿主机驱动”。常见的 nvidia/cuda 镜像包含 CUDA 用户态工具和库,但仍要求宿主机安装兼容的 NVIDIA 驱动。
3. GPU 设备
Linux 中 GPU 会通过设备节点暴露给用户态程序,例如:
/dev/nvidia0
/dev/nvidiactl
/dev/nvidia-uvm
/dev/nvidia-uvm-tools
具体节点取决于驱动版本、功能和设备类型。容器必须能够:
- 看到所需的设备节点;
- 通过 devices cgroup 访问这些节点;
- 拥有相应的 NVIDIA 驱动能力;
- 找到匹配的用户态库。
只把 /dev/nvidia0 用 --device 映射进去,通常并不完整,因为 CUDA 程序还可能需要 nvidiactl、UVM 设备和多个用户态库。
4. NVIDIA Container Toolkit 与 Runtime
NVIDIA Container Toolkit 是连接 Docker 和宿主机 NVIDIA 驱动的组件。它根据 Docker 的 GPU 请求,生成或修改 OCI 容器配置,将以下内容注入容器:
- NVIDIA 设备节点;
- 设备访问规则;
- 驱动相关库;
- 用户态工具;
NVIDIA_VISIBLE_DEVICES、NVIDIA_DRIVER_CAPABILITIES等环境设置;- 必要的 OCI hook 或 runtime 配置。
这里的 NVIDIA Runtime 不应简单理解为“另一个独立的容器引擎”。它通常是 Docker 创建 OCI 容器时使用的 NVIDIA 感知运行时组件。现代 Docker 的常见入口是:
docker run --gpus all ...
而不是必须显式写:
docker run --runtime=nvidia ...
--runtime=nvidia 是较早或较底层的配置方式;--gpus 是现代 Docker Engine 的 GPU 请求接口。
二、完整数据流:从 docker run --gpus 到 CUDA 程序
一个 GPU 容器的创建过程可以抽象为:
flowchart LR
A[用户执行 docker run --gpus] --> B[Docker CLI]
B --> C[Docker Engine]
C --> D[OCI 容器配置]
D --> E[NVIDIA Container Toolkit]
E --> F[设备节点与 devices cgroup]
E --> G[驱动库与环境变量]
F --> H[容器进程]
G --> H
H --> I[CUDA Runtime / NVML]
I --> J[宿主机 NVIDIA 驱动]
J --> K[物理 GPU]
关键路径如下:
- Docker CLI 将
--gpus转换为 Docker Engine 的 GPU 请求; - Docker Engine 在创建容器时把 GPU 请求传给运行时;
- NVIDIA Container Toolkit 解析设备选择和 capability;
- Toolkit 生成设备节点挂载、库挂载和 cgroup 设备规则;
- 容器中的 CUDA 程序加载用户态库;
- 用户态库通过宿主机 NVIDIA 驱动访问 GPU。
因此,GPU 容器失败时,问题不一定在 CUDA 程序本身。失败可能发生在:
- Docker 根本没有识别 GPU 请求;
- Toolkit 没有安装或未配置;
- 设备节点没有注入;
- 驱动库没有注入;
- 宿主机驱动版本不兼容;
- CUDA 程序需要的能力没有暴露;
- 容器内应用没有权限或找不到库。
三、配置 NVIDIA Container Toolkit
3.1 宿主机先验证驱动
在安装 Docker GPU 集成前,先在宿主机直接运行:
nvidia-smi
正常时会显示:
- GPU 型号;
- Driver Version;
- CUDA Version;
- 当前进程;
- 显存使用情况。
这里的 CUDA Version 表示驱动声明支持的最高 CUDA 兼容版本范围,不代表宿主机安装了完整 CUDA Toolkit,也不代表容器一定使用这个版本。
如果宿主机上的 nvidia-smi 已失败,继续配置 Docker 没有意义。常见原因包括:
- NVIDIA 内核模块未加载;
- 驱动未正确安装;
- GPU 被系统禁用;
- 服务器处于虚拟化或直通配置错误状态;
- 驱动与当前内核不匹配;
- 权限或设备节点异常。
可以进一步检查:
ls -l /dev/nvidia*
lsmod | grep nvidia
docker version
docker info
ls -l /dev/nvidia* 用于确认设备节点存在;lsmod 用于确认内核模块已加载。设备节点存在并不等于驱动可用,但设备节点完全不存在时,容器通常不可能获得正常 GPU 访问。
3.2 配置 Docker 运行时
不同发行版的安装包名称和仓库配置有所不同,应以 NVIDIA Container Toolkit 官方文档对应发行版的安装步骤为准。安装 Toolkit 后,常见的 Docker 配置命令是:
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
这个命令通常会修改 Docker 的 daemon 配置,使 Docker 能够使用 NVIDIA 相关运行时集成。配置完成后检查:
docker info
不要仅凭 docker info 中是否出现某个固定字符串判断所有功能。不同 Toolkit 和 Docker 版本的展示方式可能不同,最终验证应使用实际 GPU 容器。
3.3 使用最小运行测试
下面的命令直接启动一个 CUDA 基础镜像:
docker run --rm --gpus all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
各部分含义是:
--rm:进程退出后删除临时容器;--gpus all:请求所有可用 GPU;nvidia/cuda:...:提供 CUDA 用户态环境;nvidia-smi:在容器内查询 GPU。
成功时,容器内的 nvidia-smi 应能看到宿主机 GPU。输出中的驱动版本通常来自宿主机,而 CUDA 工具和库来自镜像或 Toolkit 注入环境。
这个测试同时验证了:
它不能验证你的模型、训练框架或 CUDA kernel 一定正确,但能排除大量基础运行时问题。
四、--gpus 如何选择设备
4.1 使用所有 GPU
docker run --rm --gpus all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi -L
nvidia-smi -L 通常列出 GPU UUID 和型号。使用 all 只表示容器可见设备集合包含所有 GPU,不表示 Docker 会自动为多个容器做显存调度或独占分配。
4.2 按 GPU 索引选择
docker run --rm \
--gpus '"device=0"' \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi -L
选择多个设备时:
docker run --rm \
--gpus '"device=0,2"' \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi -L
这里出现嵌套引号,是因为:
- 外层 shell 需要把整个参数作为一个参数传给 Docker;
- 参数内部还包含
device=0,2这样的 GPU 请求表达式。
更稳定的生产标识通常是 GPU UUID,而不是索引。索引可能因宿主机枚举顺序、热插拔或环境变化而改变。可以先查看 UUID:
nvidia-smi --query-gpu=index,uuid,name --format=csv
然后按 UUID 选择:
docker run --rm \
--gpus '"device=GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"' \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi -L
示例中的 UUID 必须替换为真实值。
4.3 GPU 可见性不是 GPU 使用权
容器内经常能看到:
echo "$NVIDIA_VISIBLE_DEVICES"
这个变量表示 Toolkit 为容器配置的可见设备集合。但它主要影响“程序能看到哪些 GPU”,不是一个完整的安全边界,也不是显存配额。
例如,两个容器都执行:
docker run --rm --gpus '"device=0"' ...
docker run --rm --gpus '"device=0"' ...
它们都可能访问同一块物理 GPU。Docker 不会因此自动把 GPU 显存切成两份,也不会自动阻止两个进程同时提交 CUDA 工作。
五、设备、Runtime 与 Linux 隔离机制的关系
Docker 的隔离来自多个 Linux 机制共同作用:
- namespaces:隔离进程、挂载点、网络、主机名等视图;
- cgroups:限制和统计 CPU、内存、进程数、I/O 等资源;
- Mount:构造容器文件系统,并注入设备节点和驱动库;
- devices cgroup:控制进程能够访问哪些设备类型;
- OCI runtime:依据容器配置创建最终进程。
GPU 容器中的设备注入与普通的 bind mount 不同。普通文件挂载只解决“路径是否存在”,而设备访问还涉及:
- 设备节点的 major/minor 编号;
- 设备文件的权限;
- cgroup devices 访问规则;
- NVIDIA 驱动所需的辅助设备;
- 用户态库和动态链接器路径。
因此,下面这种手工方式通常不完整:
docker run --rm \
--device=/dev/nvidia0 \
ubuntu:22.04 \
...
它只尝试映射一个设备节点,不能自动完成 CUDA 所需的其他设备、库和能力注入。除非你明确知道应用只需要某个特殊设备接口,否则应优先使用:
docker run --rm --gpus all ...
5.1 --runtime=nvidia 与 --gpus
历史上常见的写法是:
docker run --rm \
--runtime=nvidia \
-e NVIDIA_VISIBLE_DEVICES=0 \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
现代 Docker 中,更推荐:
docker run --rm \
--gpus '"device=0"' \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
两者都可能依赖 NVIDIA Toolkit,但抽象层次不同:
--gpus表达“这个容器需要哪些 GPU”;--runtime=nvidia表达“使用哪个运行时创建容器”;NVIDIA_VISIBLE_DEVICES是 Toolkit 使用的环境变量接口。
不要把三者随意叠加来“增加权限”。重复配置可能造成选择结果难以推断,尤其是在旧版 Toolkit、旧版 Docker 或自定义 daemon 配置中。
5.2 CDI 设备声明
较新的 NVIDIA Container Toolkit 可以生成 CDI(Container Device Interface)规格,使设备以标准化名称暴露。例如,环境支持时可以使用类似:
docker run --rm \
--device=nvidia.com/gpu=all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
CDI 的准确可用性取决于 Toolkit、Docker Engine 和生成的 CDI 规格文件。它不是所有旧环境的默认接口。排查时应确认:
ls -l /var/run/cdi
以及 Toolkit 文档规定的 CDI 生成和刷新方式。不能因为系统安装了 NVIDIA 驱动,就假定 CDI 设备名已经存在。
六、NVIDIA Driver Capabilities:为什么 nvidia-smi 能用而应用仍失败
Toolkit 不仅选择 GPU,还会根据 capabilities 决定注入哪些驱动功能。常见能力包括:
compute:CUDA 计算;utility:NVML 和nvidia-smi等管理功能;graphics:OpenGL 等图形能力;video:视频编解码相关能力;display:显示相关能力;compat32:32 位兼容库。
例如,一个只需要 CUDA 计算和 NVML 的容器可以显式设置:
docker run --rm --gpus all \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
若只设置了 compute,某些依赖 NVML 的监控程序可能无法工作;若图形程序需要 OpenGL,却只暴露 compute,utility,则可能出现库缺失或初始化失败。
需要区分两个事实:
nvidia-smi成功说明管理路径和至少一部分驱动能力正常;nvidia-smi成功不等于 CUDA kernel、cuDNN、TensorRT、OpenGL 或视频编解码路径都正常。
常见失败表现
找不到 GPU
NVIDIA-SMI has failed because it couldn't communicate with the NVIDIA driver.
通常首先检查宿主机 nvidia-smi,然后检查 Toolkit 和容器运行参数。
找不到 NVML
Failed to initialize NVML: Driver/library version mismatch
这类错误表示用户态 NVML 库与内核驱动不匹配,或者容器中错误地优先加载了不兼容的库。容器并不是把任意版本的 NVIDIA 用户态库放进去都能运行,驱动兼容关系仍然成立。
找不到 libcuda.so
libcuda.so.1: cannot open shared object file
常见原因是:
- 没有通过
--gpus请求 GPU; - Toolkit 未正确注入驱动库;
- capability 不包含
compute; - 镜像中的动态链接器路径或库缓存异常;
- 容器使用了不匹配的手工库覆盖。
可以在容器中检查:
ldconfig -p | grep -E 'libcuda|libnvidia-ml'
echo "$NVIDIA_DRIVER_CAPABILITIES"
echo "$NVIDIA_VISIBLE_DEVICES"
七、宿主机驱动与容器 CUDA 的兼容关系
CUDA 应用通常依赖两类版本:
- 容器内的 CUDA 用户态组件版本;
- 宿主机 NVIDIA 驱动支持的 CUDA Driver API 版本。
简化表示为:
其中:
- 表示宿主机驱动提供的兼容能力;
- 表示容器用户态 CUDA 组件所需的能力;
- 不是简单的字符串大小比较,而是 NVIDIA 规定的兼容关系。
因此,“宿主机装了 CUDA 12.4”并不是严格的判断条件;真正重要的是宿主机驱动版本是否支持容器中 CUDA 版本的运行要求。
一个常见反例是:
宿主机驱动较旧
容器使用较新的 CUDA 镜像
nvidia-smi 可以运行
模型启动时报 CUDA driver version is insufficient
nvidia-smi 可能只使用了管理接口,而模型会调用更新的 CUDA Driver API 或特定库功能,因此两者结果不矛盾。
排查时应记录:
# 宿主机
nvidia-smi
# 容器
docker run --rm --gpus all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
bash -lc 'nvidia-smi; ldconfig -p | grep -E "libcuda|libnvidia-ml"'
镜像 CUDA 版本、宿主机驱动版本和框架官方兼容矩阵应一起检查,不能只看其中一个数字。
八、Compose 中声明 GPU
Compose 需要表达“服务需要哪些设备”,而不是把 GPU 选择写成普通环境变量。常见写法是:
services:
trainer:
image: nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["nvidia-smi"]
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
启动:
docker compose up --abort-on-container-exit
这里的关键字段是:
driver: nvidia:请求 NVIDIA 设备驱动;count: 1:请求一块 GPU;capabilities: [gpu]:声明设备能力。
也可以按设备 ID 选择:
services:
trainer:
image: nvidia/cuda:12.4.1-base-ubuntu22.04
command: ["nvidia-smi", "-L"]
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["0"]
capabilities: [gpu]
count 和 device_ids 表示两种不同的选择方式,不能同时指定。capabilities 是必需的,否则 Compose 无法明确设备用途。
需要注意 Compose 的版本差异:
- 现代 Docker Compose 支持 Compose Specification 中的设备预留语法;
- 旧版 Compose 对
deploy的支持范围不同; - 某些环境支持更直接的
gpus字段,但其版本要求和行为应以当前 Compose 文档为准; - Compose 只负责向 Docker Engine 提交设备请求,不替代宿主机驱动和 NVIDIA Toolkit。
验证生成的容器配置:
docker compose config
docker inspect <container-name>
重点查看容器是否真的创建了 GPU 请求、设备和相关环境,而不是只检查 YAML 能否解析。
九、GPU 资源与 cgroups:Docker 能限制什么,不能限制什么
9.1 CPU、内存和进程数仍然由 cgroups 管理
GPU 容器并不特殊到可以绕过普通资源治理。例如:
docker run --rm --gpus all \
--cpus=4 \
--memory=16g \
--pids-limit=512 \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
这些限制分别作用于:
--cpus=4:限制容器可使用的 CPU 配额;--memory=16g:限制容器进程使用的主机内存;--pids-limit=512:限制 PID 数量;--gpus all:选择可见 GPU。
它们作用于不同资源维度,不存在“加了 GPU 就自动获得更多 CPU 或内存”的关系。
9.2 主机内存不等于 GPU 显存
下面是最常见的错误理解:
docker run --gpus all --memory=8g ...
这并不表示容器最多使用 8 GiB GPU 显存。--memory=8g 限制的是容器 cgroup 的主机内存,通常包括:
- 应用堆内存;
- Python 对象;
- CPU tensor;
- 文件缓存;
- 部分 pinned memory;
- 其他用户态内存。
GPU 显存由 GPU 驱动和 CUDA 分配器管理,通常不会作为普通 Docker memory cgroup 配额被精确限制。
因此可能出现:
容器主机内存没有达到 --memory 上限
但 CUDA 报 out of memory
这意味着 GPU 显存耗尽,而不是容器 cgroup 的主机内存耗尽。
反过来也成立:
GPU 还有大量空闲显存
容器却因 --memory 限制触发 OOM
因为应用可能消耗了过多主机内存、数据集缓存或 pinned memory。
9.3 GPU 计算时间通常不是 Docker 原生 cgroup 配额
Docker 的 CPU cgroup 能表达 CPU 配额,例如:
但普通 Docker GPU 请求一般表达的是:
而不是:
也就是说,--gpus device=0 选择了 GPU 0,但通常没有表达“最多使用 GPU 0 的 30% 计算时间”或“最多使用 4 GiB 显存”。
多个容器共享同一 GPU 时,实际竞争可能包括:
- CUDA kernel 执行时间;
- GPU 显存;
- copy engine;
- NVDEC/NVENC 视频引擎;
- PCIe 和主机内存带宽;
- GPU 上下文资源。
Docker 默认不会为这些资源建立完整公平调度。
9.4 MIG 是硬件分区,不是普通 Docker 限制参数
支持 MIG 的 NVIDIA GPU 可以把物理 GPU 切分成多个硬件实例。MIG 实例通常具有较明确的计算和显存边界,容器可以选择某个 MIG 设备,而不是访问整块卡。
但必须区分:
--gpus device=0:可能选择整块 GPU;- 选择 MIG UUID:选择某个 MIG 实例;
--memory=8g:限制主机内存,不创建 MIG 分区。
MIG 的创建、配置和生命周期由宿主机 NVIDIA 工具链与硬件能力决定,不是 Docker --memory 或 --cpus 的替代品。生产环境中还要考虑重启后的 MIG 配置持久化、设备 UUID 变化和监控维度。
9.5 MPS、应用级限流和调度器
NVIDIA MPS、框架级 batch 限制、应用内部显存分配器和外部调度系统都可能影响 GPU 共享行为,但它们不等于 Docker Runtime:
- Docker 负责容器创建和设备可见性;
- 驱动负责 GPU 设备访问;
- MIG 负责部分硬件分区;
- MPS 负责特定 CUDA 进程的共享路径;
- 应用和调度系统负责更高层的工作负载调度。
把这些层次混成一个“GPU 配额”概念,会导致错误的容量规划。
十、容器中的 GPU 可观测性
10.1 docker stats 不显示完整 GPU 指标
docker stats
主要显示容器的:
- CPU 使用率;
- 主机内存;
- 网络 I/O;
- 块设备 I/O;
- PIDs。
它通常不提供 NVIDIA GPU 利用率、显存使用、编码器利用率或温度等 GPU 专属指标。
因此,GPU 容器监控至少要分成两条路径:
10.2 使用 nvidia-smi 做即时检查
宿主机:
nvidia-smi
容器:
docker exec -it <container-name> nvidia-smi
按秒刷新:
watch -n 1 nvidia-smi
查询结构化指标:
nvidia-smi \
--query-gpu=index,uuid,name,temperature.gpu,utilization.gpu,memory.used,memory.total \
--format=csv
查询进程:
nvidia-smi pmon -c 1
这些命令适合故障现场检查,但不适合直接作为长期监控系统。原因包括:
- 文本格式不稳定;
- 查询频率过高可能增加管理开销;
- 容器名、业务名和 GPU 进程之间缺乏稳定关联;
- 多实例 MIG 的指标维度需要额外处理。
10.3 NVML、DCGM 和 Prometheus
NVIDIA Management Library(NVML)提供程序化的 GPU 管理和指标访问。nvidia-smi 本身也依赖相关管理接口。
长期监控通常使用 NVIDIA DCGM 及其 exporter,将 GPU 指标导出给 Prometheus。常见指标类别包括:
- GPU 利用率;
- 显存已用和总量;
- 功耗;
- 温度;
- ECC 错误;
- PCIe 状态;
- 编码器和解码器利用率;
- MIG 实例指标。
这条路径的因果关系是:
GPU 驱动/NVML
↓
DCGM
↓
Exporter
↓
Prometheus
↓
Grafana / 告警系统
监控 DCGM exporter 自身是否拥有 GPU 访问权限同样重要。一个 exporter 容器启动成功,不代表它已经收集到有效 GPU 指标;必须检查其日志、指标内容以及容器的 GPU 设备配置。
10.4 把 GPU 指标关联到容器
GPU 驱动通常能看到 GPU 上运行的进程 PID,但这个 PID 可能属于宿主机 PID namespace,而容器内看到的是容器 namespace 中的 PID。两者不一定相同。
因此排查一个异常进程时,可以同时执行:
docker top <container-name>
nvidia-smi
必要时在宿主机记录:
ps -fp <host-pid>
cat /proc/<host-pid>/cgroup
/proc/<pid>/cgroup 可帮助判断进程属于哪个容器 cgroup。生产监控系统通常需要结合:
- 宿主机 PID;
- 容器 ID;
- cgroup 路径;
- GPU UUID 或 MIG UUID;
- 业务标签。
只记录“GPU 0 利用率 100%”而不记录容器和作业身份,通常无法完成故障归因。
十一、一个端到端诊断流程
下面假设容器启动后报告 CUDA 错误。应按层次从底向上排查,而不是直接修改模型代码。
第一步:验证宿主机驱动
nvidia-smi
如果失败,先修复宿主机驱动、内核模块和设备节点。
第二步:验证 Docker 基础 GPU 通路
docker run --rm --gpus all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
如果失败,检查:
docker info
docker version
nvidia-ctk --version
并查看 Docker daemon 日志:
journalctl -u docker --since "10 minutes ago"
第三步:验证设备选择
docker run --rm \
--gpus '"device=0"' \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi -L
如果 all 能运行而指定设备失败,重点检查索引、UUID、MIG 状态和设备表达式的 shell 引号。
第四步:验证库和能力
docker run --rm --gpus all \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
bash -lc '
echo "VISIBLE=$NVIDIA_VISIBLE_DEVICES"
echo "CAPS=$NVIDIA_DRIVER_CAPABILITIES"
ldconfig -p | grep -E "libcuda|libnvidia-ml" || true
nvidia-smi
'
如果设备可见但库不存在,检查 Toolkit 注入和镜像内动态链接环境。
第五步:验证 CUDA 应用
基础测试通过后,再使用实际框架镜像或应用镜像。此时如果失败,重点检查:
- CUDA 与驱动兼容关系;
- PyTorch、TensorFlow、JAX 或 TensorRT 的构建版本;
- cuDNN、NCCL 等库;
- 容器内 Python 环境;
- 应用是否选择了错误的 GPU;
- 显存是否已经被其他进程占用。
第六步:区分主机内存 OOM 和 GPU OOM
主机内存 cgroup 事件可通过:
docker inspect <container-name> --format '{{.HostConfig.Memory}}'
docker stats <container-name>
在使用 cgroup v2 的 Linux 系统上,也可以检查对应 cgroup 的:
memory.current
memory.max
memory.events
GPU 显存则通过:
nvidia-smi --query-compute-apps=pid,used_memory --format=csv
或者在支持的环境中使用 DCGM 指标检查。两者必须分别判断,不能看到 CUDA out of memory 就直接增加 Docker --memory。
十二、启动成功但实际不可用的边界
12.1 容器启动不等于 GPU 初始化成功
容器的 PID 1 可能只是一个 shell、Web 服务或等待进程。即使容器成功创建,也可能直到业务代码第一次调用 CUDA 时才报错。
因此健康检查应验证实际能力,而不是只检查进程是否存活。例如:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD nvidia-smi >/dev/null || exit 1
这个检查只能验证 NVIDIA 管理路径。对于训练服务,还应增加轻量 CUDA 或框架初始化测试,否则 nvidia-smi 成功仍可能掩盖 cuDNN、NCCL 或显存分配问题。
12.2 Rootless Docker 和权限边界
Rootless Docker 的容器进程不以宿主机 root 身份运行,设备访问和 runtime 配置会受到额外限制。GPU 场景中还要考虑:
- rootless daemon 是否能使用 NVIDIA Toolkit;
/dev/nvidia*的属主和权限;- cgroup v2 delegation;
- CDI 设备是否可被 rootless 环境解析;
- 宿主机安全策略是否阻止设备访问。
因此不能简单把 rootful Docker 的配置文件复制到 rootless Docker。必须在实际运行用户下执行 GPU 测试。
12.3 容器用户身份与设备权限
即使设备节点成功注入,容器内非 root 用户也可能因设备文件权限、组权限或安全策略而无法访问 GPU。可以检查:
docker run --rm --user 1000:1000 --gpus all \
nvidia/cuda:12.4.1-base-ubuntu22.04 \
nvidia-smi
如果 root 可以运行、普通用户失败,需要检查容器用户的组、设备节点权限以及宿主机的访问控制策略,而不是重复安装 CUDA。
12.4 安全边界不能被高估
GPU 设备访问意味着容器进程能够调用宿主机 GPU 驱动接口。--gpus 主要解决设备可见性和运行功能,不等于把 GPU 变成一个完全隔离的虚拟设备。
生产环境仍应谨慎处理:
- 不可信代码;
- 共享 GPU;
- 容器逃逸风险;
- 驱动漏洞;
- 容器内调试接口;
- 是否需要
--privileged。
通常不应为了“让 GPU 能用”直接添加:
--privileged
--privileged 会显著扩大容器权限,可能暴露远超 GPU 所需的主机能力。若普通 --gpus 不能工作,应先定位 Toolkit、设备规则、权限和驱动问题。
十三、BuildKit 与 GPU 构建边界
GPU 运行时和 GPU 镜像构建是两个不同问题。
普通 Dockerfile 构建步骤:
FROM nvidia/cuda:12.4.1-base-ubuntu22.04
RUN nvidia-smi
不能因为基础镜像包含 CUDA,就假定 docker build 的 RUN 阶段自动拥有宿主机 GPU。构建器通常运行在独立的 BuildKit worker 中,构建容器不会自动继承 docker run --gpus 的设备请求。
GPU 训练、推理和测试应优先作为运行阶段执行:
docker buildx build -t my-cuda-app:dev .
docker run --rm --gpus all my-cuda-app:dev python /app/smoke_test.py
这个流程把两个问题分开:
buildx build负责构建可分发的镜像;docker run --gpus负责在有 GPU 的运行节点上执行测试。
某些较新的 BuildKit 版本和实验性能力支持在构建步骤中声明设备,但这依赖构建器版本、设备提供方式和安全 entitlement,不能当作普通 Dockerfile 的普遍保证。若构建确实需要 GPU,应明确验证:
- BuildKit worker 运行在哪里;
- worker 是否安装 NVIDIA 驱动和 Toolkit;
- builder 是否支持设备挂载;
- 是否启用了所需 entitlement;
- 构建缓存是否会掩盖实际执行。
不要在没有验证的情况下把“构建阶段能访问 GPU”写进跨环境 CI 方案。
十四、生产中的状态与恢复路径
GPU 容器生命周期可以分为几个状态:
stateDiagram-v2
[*] --> HostDriverReady: 宿主机驱动正常
HostDriverReady --> RuntimeConfigured: Toolkit 配置完成
RuntimeConfigured --> ContainerCreated: Docker 接受 GPU 请求
ContainerCreated --> DeviceInjected: 注入设备与驱动库
DeviceInjected --> AppInitialized: CUDA/NVML 初始化
AppInitialized --> Running: 业务运行
AppInitialized --> Failed: 版本、权限或库错误
Running --> GPUOOM: 显存耗尽
Running --> HostOOM: 主机内存 cgroup OOM
Running --> DriverFault: 驱动或硬件故障
GPUOOM --> Restarted: 释放上下文或重启应用
HostOOM --> Restarted: 调整内存或降低负载
DriverFault --> HostRepaired: 修复驱动或重置 GPU
HostRepaired --> HostDriverReady
Failed --> RuntimeRepaired: 修复配置或镜像
RuntimeRepaired --> ContainerCreated
每个状态的恢复动作不同:
- HostDriverReady 之前失败:修复宿主机驱动、内核模块和设备节点;
- RuntimeConfigured 之前失败:修复 Docker daemon 与 Toolkit 集成;
- DeviceInjected 之前失败:检查
--gpus、Compose 设备声明和 CDI; - AppInitialized 失败:检查库、capability、驱动兼容性和用户权限;
- GPUOOM:降低 batch size、减少并发、释放 CUDA context、使用 MIG 或重新调度;
- HostOOM:调整 cgroup 内存、减少 CPU 侧缓存和 pinned memory;
- DriverFault:查看
dmesg、GPU 错误和硬件状态;必要时进行节点隔离和维护。
“重启容器”对 GPU OOM 有时有效,因为 CUDA context 会释放;但如果宿主机上仍有其他进程占用显存,重启当前容器不会解决资源竞争。如果驱动已经进入错误状态,单纯重启应用也可能无效。
十五、资源治理的一个完整算例
假设一台主机有四块 GPU,业务希望:
- 服务 A 使用 GPU 0;
- 服务 B 使用 GPU 1 和 GPU 2;
- 服务 C 使用 GPU 0,但只作为共享推理服务;
- 每个服务限制主机 CPU 和内存;
- 不要求 Docker 原生限制显存。
可以配置:
services:
service-a:
image: my-inference:latest
command: ["python", "serve.py"]
deploy:
resources:
limits:
cpus: "4"
memory: 16G
pids: 512
reservations:
devices:
- driver: nvidia
device_ids: ["0"]
capabilities: [gpu]
service-b:
image: my-trainer:latest
command: ["python", "train.py"]
deploy:
resources:
limits:
cpus: "16"
memory: 64G
pids: 2048
reservations:
devices:
- driver: nvidia
device_ids: ["1", "2"]
capabilities: [gpu]
service-c:
image: my-inference:latest
command: ["python", "serve.py"]
deploy:
resources:
limits:
cpus: "4"
memory: 16G
pids: 512
reservations:
devices:
- driver: nvidia
device_ids: ["0"]
capabilities: [gpu]
从配置可以推导出:
但不能推导出:
因为 A 和 C 仍然共享同一块 GPU 0,Docker 没有自动进行显存分区。若 A 和 C 必须互不影响,应使用不同物理 GPU、MIG 实例,或在更高层实施调度和限流。
十六、常见误解与反例
误解一:CUDA 镜像自带驱动,所以任何宿主机都能运行
反例:
容器:CUDA 12.x
宿主机:过旧 NVIDIA 驱动
结果:镜像能拉取,nvidia-smi 可能部分工作,但应用初始化失败
镜像分发的是用户态环境,不会替代宿主机内核驱动。
误解二:--gpus 1 表示使用 1 GiB 显存
实际含义通常是请求一块 GPU,而不是显存大小。它表达的是设备集合选择:
而不是:
误解三:--memory=16g 可以防止 CUDA OOM
两者属于不同资源系统。Docker memory cgroup 管理主机内存;CUDA 分配器管理 GPU 显存。一个限制不会自动转换成另一个限制。
误解四:宿主机 nvidia-smi 正常,容器就一定正常
还可能缺少:
- Docker GPU 请求;
- Toolkit 配置;
- 设备节点;
- capability;
- 容器用户权限;
- 兼容的用户态库;
- 正确的 Compose 声明。
宿主机测试只验证驱动层,不验证容器集成层。
误解五:容器看到 GPU 就拥有独占权
GPU 可见性只是访问入口。多个容器可以看到同一个 GPU,显存和计算时间仍可能互相竞争。
十七、选择接口时的取舍
在大多数现代 Docker Engine 环境中,可以按以下层次选择接口:
普通 Docker 命令
docker run --gpus all ...
适合单机运行和脚本化任务,语义直接,最容易验证。
Compose 设备声明
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
适合多服务本地开发、测试和单机部署,但必须确认当前 Compose 版本对设备预留的支持。
CDI
docker run --device=nvidia.com/gpu=all ...
适合希望使用标准化设备名称、减少特定 runtime 耦合的环境,但依赖 Toolkit 生成 CDI 规格和 Docker 对 CDI 的支持。
手工 --device 和库挂载
只适用于已经明确掌握驱动设备、库和权限要求的特殊场景。它容易遗漏辅助设备和 ABI 兼容关系,不应作为普通 CUDA 应用的默认方案。
结语
NVIDIA GPU 容器的核心不是某一个参数,而是一条跨越多个层次的运行链路:
其中:
- Runtime 负责把 GPU 请求转化为 OCI 容器配置;
- 设备决定容器能够访问哪些 GPU 接口;
- 驱动仍然由宿主机提供,并决定兼容能力;
- 资源治理可以继续限制 CPU、内存、PID 和 I/O,但普通 Docker GPU 请求通常不提供显存或 GPU 时间配额;
- 可观测性必须同时观察容器 cgroup 和 NVIDIA GPU/NVML/DCGM 指标。
只要把“设备可见性”“驱动兼容性”“资源配额”和“监控归因”分别建模,GPU 容器出现问题时就能沿着明确的数据流定位,而不会把所有故障都归因于 CUDA 或 Docker。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker IPv6、Macvlan 与 Host 网络:场景、隔离和路由边界
- 下一篇:Docker PID 1 与 Init:信号、子进程回收、Shell Form 和 Tini
- 延伸:Docker 运行时隔离:namespaces、cgroups、Mount、PID 和网络
- 延伸:Docker 资源治理:CPU、内存、PID、I/O、cgroups 与 OOM
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论