NVIDIA 容器运行时
深入理解 GPU 如何在容器中工作,从底层原理到生产实践
本章目标
- 理解容器访问 GPU 的底层原理
- 掌握 NVIDIA Container Toolkit 架构
- 深入分析 nvidia-container-runtime 工作机制
- 学会配置和调试 GPU 容器环境
1. 容器访问 GPU 的挑战
1.1 问题背景
在传统容器中,进程运行在隔离的 Namespace 中,无法直接访问宿主机的硬件设备。GPU 作为 PCI 设备,需要解决以下问题:
┌─────────────────────────────────────────────────────────────┐
│ 容器访问 GPU 的挑战 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 设备文件隔离 │
│ - /dev/nvidia* 设备不在容器 namespace 中 │
│ - 需要将设备文件映射到容器内 │
│ │
│ 2. 驱动依赖 │
│ - GPU 驱动运行在宿主机内核 │
│ - 容器内需要匹配的用户态库 │
│ - 版本兼容性问题 │
│ │
│ 3. 资源隔离 │
│ - GPU 显存如何隔离? │
│ - 计算资源如何分配? │
│ - 多容器共享 GPU 如何处理? │
│ │
│ 4. 运行时集成 │
│ - Docker/containerd 如何感知 GPU? │
│ - Kubernetes 如何调度 GPU? │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 GPU 驱动架构
理解 NVIDIA 驱动架构是理解容器 GPU 访问的基础:
┌─────────────────────────────────────────────────────────────┐
│ NVIDIA 驱动架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户空间 (User Space) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 应用程序 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ PyTorch │ │TensorFlow│ │ CUDA │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ └───────────┬───────────┘ │ │
│ │ ┌────▼────┐ │ │
│ │ │libcuda.so│ CUDA Driver API │ │
│ │ └────┬────┘ │ │
│ │ ┌────▼────┐ │ │
│ │ │libnvidia-│ 辅助库 │ │
│ │ │ ml.so │ (监控、诊断等) │ │
│ │ └────┬────┘ │ │
│ └───────────────────┼─────────────────────────────────┘ │
│ │ ioctl() │
│ ────────────────────┼───────────────────────────────── │
│ ▼ │
│ 内核空间 (Kernel Space) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ nvidia.ko │ │
│ │ (NVIDIA 内核驱动模块) │ │
│ │ │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │nvidia-uvm │ │nvidia-modeset│ │nvidia-drm │ │ │
│ │ │ (统一内存) │ │ (显示) │ │ (DRM) │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ────────────────────┼───────────────────────────────── │
│ ▼ │
│ 硬件层 (Hardware) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ NVIDIA GPU │ │
│ │ (通过 PCIe 连接到主机) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.3 关键设备文件
# GPU 相关的设备文件
$ ls -la /dev/nvidia*
crw-rw-rw- 1 root root 195, 0 Dec 1 00:00 /dev/nvidia0 # GPU 0
crw-rw-rw- 1 root root 195, 1 Dec 1 00:00 /dev/nvidia1 # GPU 1
crw-rw-rw- 1 root root 195, 255 Dec 1 00:00 /dev/nvidiactl # 控制设备
crw-rw-rw- 1 root root 195, 254 Dec 1 00:00 /dev/nvidia-modeset
crw-rw-rw- 1 root root 510, 0 Dec 1 00:00 /dev/nvidia-uvm # 统一内存
crw-rw-rw- 1 root root 510, 1 Dec 1 00:00 /dev/nvidia-uvm-tools
# 设备文件的作用
# /dev/nvidia0-N : 每个 GPU 一个设备文件,用于与特定 GPU 通信
# /dev/nvidiactl : 控制设备,用于驱动初始化和管理
# /dev/nvidia-uvm : 统一虚拟内存(Unified Virtual Memory)
# /dev/nvidia-modeset: 显示相关(容器中通常不需要)
2. NVIDIA Container Toolkit 架构
2.1 组件概览
┌─────────────────────────────────────────────────────────────────────┐
│ NVIDIA Container Toolkit 组件架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 容器运行时层 │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Docker │ │containerd │ │ CRI-O │ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │
│ │ └───────────────┼───────────────┘ │ │
│ └────────────────────────┼────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ nvidia-container-runtime │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ nvidia-container-runtime-hook │ │ │
│ │ │ (OCI prestart hook) │ │ │
│ │ │ │ │ │
│ │ │ 1. 解析环境变量 NVIDIA_VISIBLE_DEVICES │ │ │
│ │ │ 2. 调用 libnvidia-container │ │ │
│ │ │ 3. 注入 GPU 设备和驱动库 │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ libnvidia-container │ │
│ │ │ │
│ │ 核心功能: │ │
│ │ - 发现宿主机 GPU 和驱动 │ │
│ │ - 创建设备节点 │ │
│ │ - 挂载驱动库到容器 │ │
│ │ - 配置 cgroup 设备访问权限 │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ nvidia-container-cli │ │ │
│ │ │ (命令行工具,供调试和手动操作) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 宿主机驱动和设备 │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ /dev/nvidia*│ │ libcuda.so │ │ nvidia.ko (内核) │ │ │
│ │ └─────────────┘ └─────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 各组件详解
nvidia-container-runtime
// nvidia-container-runtime 是 runc 的封装
// 位置: github.com/NVIDIA/nvidia-container-runtime
// 主要功能:
// 1. 拦截 OCI 规范的容器配置
// 2. 注入 prestart hook
// 3. 调用底层 runc 执行容器
// 工作流程伪代码
func main() {
// 1. 解析容器 OCI 配置
spec := loadOCISpec()
// 2. 检查是否需要 GPU
if needsGPU(spec) {
// 3. 注入 nvidia-container-runtime-hook
spec.Hooks.Prestart = append(spec.Hooks.Prestart, Hook{
Path: "/usr/bin/nvidia-container-runtime-hook",
Args: []string{"nvidia-container-runtime-hook", "prestart"},
})
}
// 4. 调用 runc 执行
exec("runc", args...)
}
// 判断是否需要 GPU 的逻辑
func needsGPU(spec *OCI.Spec) bool {
for _, env := range spec.Process.Env {
if strings.HasPrefix(env, "NVIDIA_VISIBLE_DEVICES=") {
value := strings.TrimPrefix(env, "NVIDIA_VISIBLE_DEVICES=")
return value != "" && value != "void"
}
}
return false
}
libnvidia-container
这是核心库,负责实际的 GPU 设备注入:
// libnvidia-container 核心数据结构
// 位置: github.com/NVIDIA/libnvidia-container
struct nvc_context {
char *root; // 容器 rootfs 路径
uid_t uid; // 容器用户 ID
gid_t gid; // 容器组 ID
struct nvc_device *gpus; // GPU 设备列表
struct nvc_driver_info *driver; // 驱动信息
};
struct nvc_device {
char *path; // 设备路径 /dev/nvidia0
char *uuid; // GPU UUID
unsigned int minor; // 次设备号
bool mig_enabled; // 是否启用 MIG
};
// 主要操作流程
int nvc_container_configure(struct nvc_context *ctx,
struct nvc_container *cnt,
const struct nvc_container_config *config) {
// 1. 创建设备节点
for each gpu in config->gpus {
// 在容器的 /dev 下创建设备节点
mknod(cnt->rootfs + gpu->path, S_IFCHR, makedev(195, gpu->minor));
}
// 2. 挂载驱动库
// 将宿主机的 NVIDIA 库 bind mount 到容器
mount("/usr/lib/x86_64-linux-gnu/libcuda.so.XXX",
cnt->rootfs + "/usr/lib/x86_64-linux-gnu/libcuda.so.XXX",
NULL, MS_BIND, NULL);
// 3. 配置 cgroup 设备权限
// 允许容器访问指定的 GPU 设备
write(cnt->cgroup_path + "/devices.allow",
"c 195:minor rwm"); // nvidia 设备
// 4. 设置 LD_LIBRARY_PATH
// 确保容器能找到 NVIDIA 库
return 0;
}
2.3 运行时工作流程
┌─────────────────────────────────────────────────────────────────────────┐
│ GPU 容器启动完整流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 用户执行命令 │
│ docker run --gpus all nvidia/cuda:12.0-base nvidia-smi │
│ │
│ 2. Docker 解析 --gpus 参数 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 设置环境变量: │ │
│ │ NVIDIA_VISIBLE_DEVICES=all │ │
│ │ NVIDIA_DRIVER_CAPABILITIES=compute,utility │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 3. Docker 调用 containerd │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ containerd 调用 nvidia-container-runtime 代替 runc │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4. nvidia-container-runtime 处理 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ a. 读取 OCI spec (config.json) │ │
│ │ b. 检测 NVIDIA_VISIBLE_DEVICES 环境变量 │ │
│ │ c. 注入 prestart hook │ │
│ │ d. 调用 runc create │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 5. runc 创建容器 (进程暂停在 prestart) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ a. 创建 namespaces │ │
│ │ b. 设置 cgroups │ │
│ │ c. 准备 rootfs │ │
│ │ d. 容器进程创建但未执行 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 6. 执行 prestart hook (nvidia-container-runtime-hook) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ a. 读取容器 PID 和配置 │ │
│ │ b. 解析 NVIDIA_VISIBLE_DEVICES (all → GPU 0,1,2...) │ │
│ │ c. 调用 libnvidia-container: │ │
│ │ - 在容器 mount namespace 中创建 /dev/nvidia* 设备 │ │
│ │ - Bind mount NVIDIA 驱动库 │ │
│ │ - 配置 cgroup 设备白名单 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 7. 容器进程开始执行 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ nvidia-smi 正常运行,可以访问 GPU │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
3. 环境配置实战
3.1 安装 NVIDIA Container Toolkit
# Ubuntu/Debian 安装
# 1. 配置软件源
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.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. 安装
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
# 3. 配置 Docker
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# 4. 验证安装
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
# RHEL/CentOS 安装
# 1. 配置软件源
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo | \
sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
# 2. 安装
sudo yum install -y nvidia-container-toolkit
# 3. 配置 Docker
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
3.2 配置文件详解
# Docker daemon 配置
# /etc/docker/daemon.json
{
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
},
"default-runtime": "nvidia" # 可选:设为默认运行时
}
# nvidia-container-runtime 配置
# /etc/nvidia-container-runtime/config.toml
[nvidia-container-cli]
# 调试模式
debug = "/var/log/nvidia-container-runtime.log"
# ldconfig 路径
ldconfig = "@/sbin/ldconfig"
[nvidia-container-runtime]
# 日志级别: debug, info, warn, error
debug = "/var/log/nvidia-container-runtime.log"
log-level = "info"
# 运行时模式
# "auto": 自动检测 (推荐)
# "legacy": 使用 prestart hook
# "csv": 使用 CDI (Container Device Interface)
mode = "auto"
[nvidia-container-runtime.modes.csv]
# CDI 模式配置
mount-spec-path = "/etc/nvidia-container-runtime/host-files-for-container.d"
[nvidia-container-runtime.modes.cdi]
# CDI 规范文件路径
spec-dirs = ["/etc/cdi", "/var/run/cdi"]
default-kind = "nvidia.com/gpu"
3.3 环境变量详解
# 核心环境变量
# NVIDIA_VISIBLE_DEVICES: 指定容器可见的 GPU
# 可选值:
# - "all" : 所有 GPU
# - "none" : 不使用 GPU
# - "void" : 不使用 GPU (与 none 相同)
# - "0,1,2" : 指定 GPU 索引
# - "GPU-xxxx-..." : 指定 GPU UUID
# - "MIG-xxxx-..." : 指定 MIG 实例
docker run --gpus '"device=0,1"' ...
# 等价于
docker run -e NVIDIA_VISIBLE_DEVICES=0,1 ...
# NVIDIA_DRIVER_CAPABILITIES: 指定需要的驱动能力
# 可选值 (逗号分隔):
# - compute : CUDA 和 OpenCL
# - compat32 : 32 位兼容库
# - graphics : OpenGL 和 Vulkan
# - utility : nvidia-smi 等工具
# - video : 视频编解码
# - display : 显示输出
# - all : 以上全部
docker run -e NVIDIA_DRIVER_CAPABILITIES=compute,utility ...
# NVIDIA_REQUIRE_*: 指定驱动/CUDA 版本要求
docker run -e NVIDIA_REQUIRE_CUDA="cuda>=12.0" ...
docker run -e NVIDIA_REQUIRE_DRIVER=">=525" ...
# CUDA_VISIBLE_DEVICES: CUDA 层面的 GPU 可见性
# 注意: 这是 CUDA 环境变量,不是 NVIDIA Container Toolkit 的
# 在容器内使用,重新编号 GPU (从 0 开始)
# 使用示例
# 使用所有 GPU
docker run --gpus all nvidia/cuda:12.0-base nvidia-smi
# 使用特定 GPU
docker run --gpus '"device=0"' nvidia/cuda:12.0-base nvidia-smi
# 使用多个指定 GPU
docker run --gpus '"device=0,2"' nvidia/cuda:12.0-base nvidia-smi
# 限制 GPU 数量
docker run --gpus 2 nvidia/cuda:12.0-base nvidia-smi # 使用前 2 个 GPU
# 使用 GPU UUID
docker run --gpus '"device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a"' ...
# 完整示例:PyTorch 训练
docker run --gpus all \
-v /data:/data \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
pytorch/pytorch:2.0-cuda12.0-runtime \
python train.py
4. 底层原理深入分析
4.1 设备节点创建过程
// nvidia-container-cli 创建设备节点的过程
// 位置: github.com/NVIDIA/libnvidia-container/src/nvc_container.c
// 简化的设备创建流程
func createDeviceNodes(containerPid int, devices []Device) error {
// 1. 进入容器的 mount namespace
containerNsFd, _ := os.Open(fmt.Sprintf("/proc/%d/ns/mnt", containerPid))
defer containerNsFd.Close()
// 保存当前 namespace
hostNsFd, _ := os.Open("/proc/self/ns/mnt")
defer hostNsFd.Close()
// 切换到容器 namespace
unix.Setns(int(containerNsFd.Fd()), unix.CLONE_NEWNS)
defer unix.Setns(int(hostNsFd.Fd()), unix.CLONE_NEWNS)
// 2. 创建设备节点
for _, dev := range devices {
// /dev/nvidia0, /dev/nvidia1, ...
path := filepath.Join("/dev", dev.Name)
// mknod 创建字符设备
// 主设备号: 195 (nvidia)
// 次设备号: 根据 GPU 索引
mode := uint32(unix.S_IFCHR | 0666)
devNum := unix.Mkdev(195, uint32(dev.Minor))
if err := unix.Mknod(path, mode, int(devNum)); err != nil {
return err
}
}
// 3. 创建控制设备
// /dev/nvidiactl (minor=255)
// /dev/nvidia-uvm (major=510, minor=0)
return nil
}
4.2 驱动库挂载机制
# 查看容器内挂载的 NVIDIA 库
docker run --rm --gpus all nvidia/cuda:12.0-base \
cat /proc/self/mountinfo | grep nvidia
# 输出示例 (简化):
# /usr/lib/x86_64-linux-gnu/libcuda.so.535.104.05
# /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.535.104.05
# /usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.535.104.05
# ... 更多库文件
// 驱动库挂载的实现
// 位置: github.com/NVIDIA/libnvidia-container/src/nvc_mount.c
type DriverLibrary struct {
HostPath string // 宿主机路径
ContainerPath string // 容器内路径
Type string // 库类型
}
// NVIDIA 驱动库列表
var nvidiaLibraries = []DriverLibrary{
// CUDA Driver API
{"/usr/lib/x86_64-linux-gnu/libcuda.so.XXX", "/usr/lib/libcuda.so.1", "cuda"},
// NVML (监控库)
{"/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.XXX", "/usr/lib/libnvidia-ml.so.1", "nvml"},
// PTX JIT 编译器
{"/usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.XXX", "/usr/lib/libnvidia-ptxjitcompiler.so.1", "ptx"},
// 更多库...
}
func mountDriverLibraries(containerRootfs string, libs []DriverLibrary) error {
for _, lib := range libs {
target := filepath.Join(containerRootfs, lib.ContainerPath)
// 确保目标目录存在
os.MkdirAll(filepath.Dir(target), 0755)
// 创建空文件作为挂载点
f, _ := os.Create(target)
f.Close()
// Bind mount
if err := unix.Mount(lib.HostPath, target, "", unix.MS_BIND|unix.MS_RDONLY, ""); err != nil {
return fmt.Errorf("mount %s failed: %v", lib.HostPath, err)
}
}
return nil
}
4.3 Cgroup 设备访问控制
# 查看 cgroup 设备白名单
# Cgroup v1
cat /sys/fs/cgroup/devices/docker/<container_id>/devices.list
# 示例输出:
# c 195:* rwm # 允许所有 nvidia 设备 (主设备号 195)
# c 510:* rwm # 允许 nvidia-uvm 设备 (主设备号 510)
# Cgroup v2 中没有专门的 devices controller
# 使用 BPF 程序来控制设备访问
// Cgroup v1 设备控制
func configureDeviceCgroup(cgroupPath string, devices []Device) error {
// 默认拒绝所有设备
ioutil.WriteFile(
filepath.Join(cgroupPath, "devices.deny"),
[]byte("a"),
0644,
)
// 允许基本设备
allowBasicDevices(cgroupPath)
// 允许指定的 GPU 设备
for _, dev := range devices {
// c: 字符设备, 195: nvidia 主设备号, minor: 具体 GPU
rule := fmt.Sprintf("c 195:%d rwm", dev.Minor)
ioutil.WriteFile(
filepath.Join(cgroupPath, "devices.allow"),
[]byte(rule),
0644,
)
}
// 允许 nvidia 控制设备
ioutil.WriteFile(cgroupPath+"/devices.allow", []byte("c 195:255 rwm"), 0644) // nvidiactl
ioutil.WriteFile(cgroupPath+"/devices.allow", []byte("c 510:0 rwm"), 0644) // nvidia-uvm
ioutil.WriteFile(cgroupPath+"/devices.allow", []byte("c 510:1 rwm"), 0644) // nvidia-uvm-tools
return nil
}
4.4 OCI Hook 机制
// OCI 规范中的 hooks 定义
// config.json (容器配置)
{
"hooks": {
"prestart": [
{
"path": "/usr/bin/nvidia-container-runtime-hook",
"args": ["nvidia-container-runtime-hook", "prestart"],
"env": []
}
],
"poststart": [],
"poststop": []
}
}
// Hook 执行时机
// 位置: runc 源码 libcontainer/process_linux.go
type HookState struct {
Version string `json:"ociVersion"`
ID string `json:"id"`
Pid int `json:"pid"`
Root string `json:"root"`
Bundle string `json:"bundle"`
}
// Hook 的调用顺序:
// 1. createRuntime hooks - runc create 开始时
// 2. createContainer hooks - namespace 创建后
// 3. startContainer hooks - 容器进程执行前 (这是 nvidia 使用的)
// 4. poststart hooks - 容器进程启动后
// 5. poststop hooks - 容器停止后
func runHooks(hooks []configs.Hook, state *HookState) error {
for _, h := range hooks {
// 将状态信息通过 stdin 传递给 hook
stateJson, _ := json.Marshal(state)
cmd := exec.Command(h.Path, h.Args[1:]...)
cmd.Stdin = bytes.NewReader(stateJson)
cmd.Env = h.Env
if err := cmd.Run(); err != nil {
return err
}
}
return nil
}
5. CDI (Container Device Interface)
5.1 CDI 概述
CDI 是 CNCF 提出的容器设备接口标准,用于统一不同硬件设备在容器运行时中的访问方式:
┌─────────────────────────────────────────────────────────────────────┐
│ CDI 架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 传统方式 (Hook-based) CDI 方式 │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ 容器运行时 │ │ 容器运行时 │ │
│ │ (Docker/containerd)│ │ (Docker/containerd)│ │
│ └─────────┬──────────┘ └─────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ nvidia-container- │ │ CDI Spec Files │ │
│ │ runtime │ │ (/etc/cdi/*.json)│ │
│ └─────────┬──────────┘ └─────────┬──────────┘ │
│ │ │ │
│ ▼ │ │
│ ┌────────────────────┐ │ │
│ │ nvidia-container- │ │ │
│ │ runtime-hook │ │ │
│ │ (prestart hook) │◄───────────────────────┘ │
│ └─────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ libnvidia-container│ │
│ └────────────────────┘ │
│ │
│ CDI 优势: │
│ 1. 标准化接口,不依赖特定运行时 │
│ 2. 声明式配置,更容易管理 │
│ 3. 支持多种设备类型统一管理 │
│ 4. 与 Kubernetes Device Plugin 更好集成 │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 CDI 规范文件
# 生成 CDI 规范文件
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.json
# 查看生成的规范
cat /etc/cdi/nvidia.json
// CDI 规范示例 (/etc/cdi/nvidia.json)
{
"cdiVersion": "0.5.0",
"kind": "nvidia.com/gpu",
"devices": [
{
"name": "0",
"containerEdits": {
"deviceNodes": [
{
"path": "/dev/nvidia0",
"hostPath": "/dev/nvidia0",
"type": "c",
"major": 195,
"minor": 0,
"permissions": "rw"
}
],
"hooks": [
{
"hookName": "createContainer",
"path": "/usr/bin/nvidia-ctk",
"args": ["nvidia-ctk", "hook", "create-symlinks", "--link", "..."]
}
],
"mounts": [
{
"hostPath": "/usr/lib/x86_64-linux-gnu/libcuda.so.535.104.05",
"containerPath": "/usr/lib/x86_64-linux-gnu/libcuda.so.1",
"options": ["ro", "nosuid", "nodev", "bind"]
}
],
"env": [
"NVIDIA_VISIBLE_DEVICES=0"
]
}
},
{
"name": "all",
"containerEdits": {
"deviceNodes": [
{"path": "/dev/nvidia0", ...},
{"path": "/dev/nvidia1", ...}
],
"env": [
"NVIDIA_VISIBLE_DEVICES=all"
]
}
}
],
"containerEdits": {
"deviceNodes": [
{
"path": "/dev/nvidiactl",
"hostPath": "/dev/nvidiactl"
},
{
"path": "/dev/nvidia-uvm",
"hostPath": "/dev/nvidia-uvm"
}
]
}
}
5.3 使用 CDI
# Docker (需要 Docker 25+)
docker run --device nvidia.com/gpu=0 nvidia/cuda:12.0-base nvidia-smi
# Podman
podman run --device nvidia.com/gpu=all nvidia/cuda:12.0-base nvidia-smi
# containerd (通过 ctr)
ctr run --device nvidia.com/gpu=0 docker.io/nvidia/cuda:12.0-base test nvidia-smi
# Kubernetes (通过 nvidia-device-plugin)
# 自动处理,无需手动指定
6. containerd 集成
6.1 containerd 配置
# /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "nvidia"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
SystemdCgroup = true
6.2 CRI-O 配置
# /etc/crio/crio.conf.d/99-nvidia.conf
[crio.runtime]
default_runtime = "nvidia"
[crio.runtime.runtimes.nvidia]
runtime_path = "/usr/bin/nvidia-container-runtime"
runtime_type = "oci"
runtime_root = "/run/nvidia/runc"
7. 调试与故障排查
7.1 常用调试命令
# 1. 检查 NVIDIA 驱动状态
nvidia-smi
# 2. 检查 nvidia-container-cli
nvidia-container-cli info
nvidia-container-cli list
# 输出示例:
# NVRM version: 535.104.05
# CUDA version: 12.2
# Device Index: 0
# Device Minor: 0
# Model: NVIDIA A100-SXM4-40GB
# GPU UUID: GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 3. 手动测试设备注入
nvidia-container-cli --load-kmods configure \
--device=0 \
--compute \
--utility \
$(pwd)/test-rootfs
# 4. 查看运行时日志
journalctl -u docker -f
# 或
cat /var/log/nvidia-container-runtime.log
# 5. 检查容器内的 GPU 访问
docker run --rm --gpus all nvidia/cuda:12.0-base bash -c '
echo "=== Device Files ==="
ls -la /dev/nvidia*
echo -e "\n=== NVIDIA Libraries ==="
ldconfig -p | grep nvidia
echo -e "\n=== nvidia-smi ==="
nvidia-smi
'
7.2 常见问题排查
# 问题 1: "could not select device driver"
# 原因: Docker 未配置 nvidia 运行时
# 解决:
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker
# 验证配置
cat /etc/docker/daemon.json | grep nvidia
# 问题 2: "nvidia-container-cli: initialization error"
# 原因: NVIDIA 驱动未正确加载
# 解决:
# 检查驱动
lsmod | grep nvidia
dmesg | grep -i nvidia
# 重新加载驱动
sudo modprobe nvidia
sudo modprobe nvidia-uvm
# 问题 3: "failed to create device node"
# 原因: 权限问题或 cgroup 配置错误
# 检查 cgroup
cat /sys/fs/cgroup/devices/docker/<container_id>/devices.list
# 检查设备权限
ls -la /dev/nvidia*
# 问题 4: 容器内看不到所有 GPU
# 原因: NVIDIA_VISIBLE_DEVICES 配置
# 检查环境变量
docker inspect <container_id> | jq '.[0].Config.Env'
# 问题 5: CUDA 版本不兼容
# 原因: 驱动版本低于容器内 CUDA 要求
# 检查兼容性
nvidia-smi # 查看 CUDA Version (最高支持)
docker run --rm --gpus all nvidia/cuda:12.0-base nvcc --version # 查看容器 CUDA
# CUDA 版本 vs 驱动版本对应:
# CUDA 12.x → Driver 525+
# CUDA 11.8 → Driver 520+
# CUDA 11.0 → Driver 450+
7.3 性能调试
# 检查 PCIe 带宽
nvidia-smi topo -m
# GPU 到 GPU 的 P2P 访问
nvidia-smi topo -p2p r
# 检查 GPU 时钟频率
nvidia-smi -q -d CLOCK
# 在容器内运行 CUDA 示例程序测试
docker run --rm --gpus all nvidia/cuda:12.0-devel bash -c '
cd /usr/local/cuda/samples/1_Utilities/bandwidthTest
make
./bandwidthTest
'
# 使用 nvidia-smi dmon 监控
docker run -d --gpus all --name gpu-test nvidia/cuda:12.0-base sleep infinity
nvidia-smi dmon -i 0 -s u
8. 生产环境最佳实践
8.1 安全配置
# 1. 使用非 root 用户
docker run --gpus all \
--user 1000:1000 \
nvidia/cuda:12.0-base nvidia-smi
# 2. 只授予必要的 capabilities
docker run --gpus all \
--cap-drop ALL \
nvidia/cuda:12.0-base nvidia-smi
# 3. 限制可见 GPU
docker run --gpus '"device=0"' \
nvidia/cuda:12.0-base nvidia-smi
# 4. 使用只读文件系统
docker run --gpus all \
--read-only \
--tmpfs /tmp \
nvidia/cuda:12.0-base nvidia-smi
# 5. 限制驱动能力 (只计算,不允许显示)
docker run --gpus all \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
nvidia/cuda:12.0-base nvidia-smi
8.2 资源限制
# GPU 显存限制 (需要 GPU 支持 MPS 或使用 MIG)
# 注意: 原生 NVIDIA 运行时不支持显存软限制
# 方法 1: 使用 MIG (Multi-Instance GPU)
# 在支持 MIG 的 GPU (A100, H100) 上预先配置
nvidia-smi mig -cgi 9,9,9 -C # 创建 3 个 GPU 实例
docker run --gpus '"device=MIG-xxxxx"' ...
# 方法 2: 使用 CUDA 环境变量限制
docker run --gpus all \
-e CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps \
-e CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-mps-log \
nvidia/cuda:12.0-base nvidia-smi
# 方法 3: 应用层限制 (PyTorch 示例)
docker run --gpus all nvidia/cuda:12.0-base python3 -c "
import torch
torch.cuda.set_per_process_memory_fraction(0.5) # 限制使用 50% 显存
"
8.3 镜像构建最佳实践
# 基础镜像选择
# runtime: 只有运行时库,体积最小
# devel: 包含编译工具,用于构建
# base: 最小安装,需要手动安装额外组件
# 生产环境推荐:多阶段构建
FROM nvidia/cuda:12.0-devel AS builder
WORKDIR /app
COPY . .
RUN make
FROM nvidia/cuda:12.0-runtime
COPY /app/binary /app/binary
CMD ["/app/binary"]
# 设置正确的环境变量
ENV NVIDIA_VISIBLE_DEVICES=all
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
# 使用非 root 用户
RUN useradd -m appuser
USER appuser
# 镜像大小对比
docker images | grep cuda
# nvidia/cuda:12.0-base ~120MB
# nvidia/cuda:12.0-runtime ~870MB
# nvidia/cuda:12.0-devel ~4.5GB
9. 与 Kubernetes 集成概览
GPU 在 Kubernetes 中的调度将在下一章详细介绍,这里简要说明运行时集成:
# Kubernetes 使用 GPU 的流程
# 1. Device Plugin 发现 GPU 并上报给 kubelet
# 2. Pod 请求 GPU 资源
# 3. kubelet 调用 containerd/CRI-O
# 4. containerd 调用 nvidia-container-runtime
# 5. GPU 注入到容器
# Pod 示例
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: cuda-container
image: nvidia/cuda:12.0-base
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1 # 请求 1 个 GPU
10. 本章总结
10.1 核心知识点
┌─────────────────────────────────────────────────────────────────────┐
│ NVIDIA 容器运行时知识图谱 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 组件架构 │
│ ├── nvidia-container-toolkit # 工具包总称 │
│ ├── nvidia-container-runtime # 运行时 (runc 封装) │
│ ├── nvidia-container-runtime-hook # OCI prestart hook │
│ ├── libnvidia-container # 核心库 │
│ └── nvidia-container-cli # 命令行工具 │
│ │
│ 工作原理 │
│ ├── 设备注入: mknod 创建 /dev/nvidia* 设备节点 │
│ ├── 驱动挂载: bind mount NVIDIA 库到容器 │
│ ├── Cgroup 控制: 设备白名单 (c 195:* rwm) │
│ └── 环境变量: NVIDIA_VISIBLE_DEVICES 控制可见 GPU │
│ │
│ 关键环境变量 │
│ ├── NVIDIA_VISIBLE_DEVICES # 可见 GPU 列表 │
│ ├── NVIDIA_DRIVER_CAPABILITIES # 驱动能力 (compute/utility/...) │
│ └── NVIDIA_REQUIRE_* # 版本要求 │
│ │
│ 配置文件 │
│ ├── /etc/docker/daemon.json # Docker 运行时配置 │
│ ├── /etc/nvidia-container-runtime/config.toml # 运行时配置 │
│ ├── /etc/containerd/config.toml # containerd 配置 │
│ └── /etc/cdi/nvidia.json # CDI 规范文件 │
│ │
└─────────────────────────────────────────────────────────────────────┘
10.2 面试要点
容器如何访问 GPU?
- 通过 nvidia-container-runtime 的 prestart hook
- 在容器 namespace 中创建设备节点
- Bind mount NVIDIA 驱动库
- 配置 cgroup 设备白名单
NVIDIA_VISIBLE_DEVICES 是如何工作的?
- nvidia-container-runtime-hook 解析该环境变量
- 转换为具体的 GPU 设备列表
- 只将指定设备注入到容器
CDI 相比传统 Hook 方式有什么优势?
- 标准化接口,不依赖特定运行时
- 声明式配置,更容易管理和审计
- 支持多种设备类型统一管理
如何排查容器 GPU 访问问题?
- 检查驱动状态:
nvidia-smi - 检查工具链:
nvidia-container-cli info - 检查运行时日志
- 验证 cgroup 设备权限
- 检查驱动状态:
10.3 下一章预告
下一章我们将深入探讨 GPU 共享与隔离技术:
- MIG (Multi-Instance GPU) 原理与实践
- vGPU 虚拟化技术
- 时分复用方案
- GPU 共享方案对比与选型