容器与 GPU 的问题:为什么不能开箱即用
GPU 无法在容器里“开箱即用”不是缺陷,而是架构使然:容器是内核特性的组合,而 GPU 恰恰不是内核原生资源。
容器究竟如何工作
容器不是轻量级虚拟机。它是一组 Linux 内核特性的集合(主要是命名空间 namespace 与 cgroups,前者负责进程、网络、文件系统、用户隔离,后者负责资源记账与限制),让一个进程相信自己独占整台机器。
当你运行 docker run nginx 时,容器运行时(Docker → containerd → runc)为进程创建隔离的命名空间、挂载容器镜像文件系统、配置网络,并对 CPU 和内存施加 cgroup 限制。这一切都是纯 Linux 内核,除了内核本来就管理的东西,没有任何硬件驱动的参与。
这正是容器启动快、重量轻的原因:它们共享主机内核,没有虚拟机管理器开销。但这个设计在加入 GPU 时制造了一个棘手问题。
根本问题:GPU 不是内核原生资源
与 CPU 核和系统内存不同(Linux 内核通过进程调度和虚拟内存系统原生管理它们),GPU 由 GPU 厂商加载的专有内核模块管理。对 NVIDIA 来说,就是 nvidia.ko 内核模块及其上的用户态驱动栈。
GPU 通过一组设备文件向用户态暴露自己。在只有一块 NVIDIA GPU 的节点上,你会发现:
# Device files created by the NVIDIA kernel module
/dev/nvidia0 # GPU 本体
/dev/nvidiactl # GPU 控制设备(所有 GPU 共享)
/dev/nvidia-uvm # 统一虚拟内存(UVM)驱动
/dev/nvidia-uvm-tools # UVM 调试工具
/dev/nvidia-modeset # 显示模式设置(可选)
# 同时:必须存在于容器中的 NVIDIA 库
/usr/lib/x86_64-linux-gnu/libcuda.so.1 # CUDA 运行时
/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 # NVML(管理接口)
/usr/lib/x86_64-linux-gnu/libnvidia-glcore.so # GL 加速问题就在这里:一个标准容器得不到其中任何一项。容器文件系统与主机隔离,设备文件在容器里不存在,NVIDIA 驱动库也没有被挂载。即使你在容器镜像里装了 CUDA,运行时它也无处可谈。
docker run my-cuda-app,结果是:CUDA error: no kernel image is available for execution on the device,或者干脆 nvidia-smi: command not found,更糟的是:进程正常启动但悄悄回落到 CPU。没有报错,只是推理慢了 100 倍。早期尝试:nvidia-docker v1(2016)
2016 年 NVIDIA 发布 DGX-1(世界上第一台“盒子里的 AI 超算”),随机器附带了 NVIDIA Docker,一个改造过的 Docker CLI 包装器。nvidia-docker v1 的做法是维护一个单独的 Docker 卷插件,把 GPU 库和设备文件打包进去,启动时再挂载到容器里。
这能用,但问题很深:
- 它要求一条单独的
nvidia-docker命令,破坏了标准 Docker 工作流和 CI/CD 流水线 - 卷里和主机上的驱动版本必须完全一致,维护起来是噩梦
- 它无法与 Kubernetes、Docker Swarm 或任何绕过 Docker CLI 的编排器配合
- 每次 nvidia-docker 升级都要重建卷插件,每逢内核或驱动更新都极其脆弱
AI 社区在快速膨胀,这个 hack 走不远。
nvidia-docker v2:OCI 钩子方案(2017)
2017 年,NVIDIA 推倒重来。nvidia-docker v2 不再包装 Docker CLI,而是引入了自定义 OCI 运行时的概念,在最底层钩进容器创建过程本身。
关键洞察是:开放容器倡议(Open Container Initiative,OCI)运行时规范定义了容器如何被创建。参考实现 runc 遵循该规范。规范允许预启动钩子(pre-start hook):在容器创建之后、启动之前运行的可执行文件。NVIDIA 正是利用这个钩子点把 GPU 访问注入容器。
nvidia-container-runtime)的方式与调用 runc 完全一样。Docker 无需任何改动;containerd、CRI-O 及任何兼容 OCI 的运行时同理。这一钩子机制后来被标准化为 CDI(容器设备接口),并成为 DRA 路径的注入基础,从 v2 到 CDI 再到 DRA 一脉相承。
总结
容器与 GPU 的缝隙本质上是两套世界的接口失配:容器世界只认识内核原生资源,GPU 世界由厂商内核模块和用户态库构成。nvidia-docker v2 用 OCI 预启动钩子在进程创建的最底层缝合了这道缝,其思想延续至今。下一章将拆解完成这件事的完整工具链。