机器学习平台技术栈之 NVIDIA Device Plugin:打通云原生与 AI 算力的任督二脉
机器学习平台技术栈之 NVIDIA Device Plugin:打通云原生与 AI 算力的任督二脉
对于现代机器学习平台(如 Kubeflow、PAI 等)而言,Kubernetes (K8s) 几乎已经是事实上的底层控制平面标准。然而,原生的 Kubernetes 调度器在设计之初,主要关注的是 CPU 和内存(Memory)这类可以任意切分的标准化资源。
随着深度学习的爆发,AI 训练和推理对 GPU、TPU 甚至 NPU 等异构加速硬件的依赖达到了前所未有的高度。Kubernetes 如何能够“看见”、“管理”并“分配”这些长在物理机 PCIe 插槽上的昂贵显卡?容器中的进程又是如何越过操作系统的层层隔离,直接调用底层 GPU 驱动的?
这背后最大的功臣,就是 NVIDIA Device Plugin 以及与其配套的容器工具链。本文将为你极其硬核地拆解 NVIDIA Device Plugin 以及底层 GPU 透传的技术栈,从 K8s 异构资源抽象模型,到 gRPC 交互的源码级工作流,再到 GPU 共享(Time-slicing/MIG)的高阶玩法,带你全面看透 AI 基础设施的“任督二脉”。
1. 痛点与核心概念解析
1.1 Kubernetes 扩展资源与 Device Plugin 框架
在 K8s 早期的版本(1.8 之前),由于缺乏统一的异构硬件管理标准,各个厂商不得不把包含自身硬件支持的代码直接硬编码(Hardcode)进 Kubernetes 核心代码库中(即 In-Tree 模式)。这种做法导致了极严重的耦合,K8s 的发版会被英伟达的驱动更新节奏拖累。
为了解耦,Kubernetes 提出了 Device Plugin (设备插件) 框架(Out-of-Tree 模式)。
Device Plugin 是一套标准的基于 gRPC 的对外接口规范。硬件厂商(如 NVIDIA、AMD、Intel 等)只需按照这套规范编写一个 DaemonSet 部署在各个 Node 上。Kubelet 会通过本地 Unix Socket 与之通信,了解该节点上有多少特殊硬件。
并且,引入了 Extended Resource(扩展资源)的概念。我们在 Pod YAML 中经常看到的 nvidia.com/gpu: 1 就是一种典型的扩展资源名称。
1.2 核心概念与组件
要完全掌握 GPU 在容器里的透传,你需要理清以下几个关键概念及其所属范畴:
- NVIDIA driver: 宿主机的底层操作系统上安装的真实硬件驱动(如
nvidia-smi所依赖的内核模块)。 - CUDA Toolkit: 供应用(如 PyTorch、TensorFlow)调用的并发计算编程库。
- NVIDIA Container Toolkit (原 nvidia-docker2): 一个位于容器运行时(Containerd / Docker)和低级运行时(runc)中间的钩子(Hook)工具链(包含
libnvidia-container等)。它负责在容器启动前,强行进入容器的 Namespace,将宿主机的 GPU 设备文件和相关的 Driver 库文件映射进去。 - NVIDIA Device Plugin: 部署在 K8s 上的 Pod(DaemonSet),专门跟 Kubelet 打交道。它本身不负责具体的底层设备挂载动作,而是作为一个情报员和配置生成器,告诉 K8s “选哪张卡”以及“需要容器运行时注入什么环境变量参数”。
2. 核心概念之间的关系图谱
在整个生态中,Device Plugin 扮演的是 K8s 调度系统与底层容器运行时的“翻译官”。这三者的联动构成了完整的算力生命周期:
1 | graph TD |
从图中可以看出核心协作链:
- Device Plugin 扫描宿主机发现了设备,告诉 Kubelet(例如:我有 4 张 GPU)。
- Kubelet 上报给 API Server,Scheduler 根据资源请求把任务分配到这个 Node。
- Kubelet 拿着指定的 GPU ID 去请求 Device Plugin,Plugin 会回传一套供容器使用的环境参数和挂载表。
- Kubelet 将这些参数交给 Containerd,在由
runc初始化容器的最后关头,触发 NVIDIA Hook。 - Hook 根据环境变量,直接把
/dev/nvidia0设备文件等暴力的“塞”进容器的 Namespace 中。
3. 架构设计:gRPC 交互工作流揭秘
Kubelet 是如何与 Device Plugin 沟通的?整个 Device Plugin 框架提供了两个极其重要的 gRPC 接口声明:ListAndWatch 和 Allocate。
3.1 监听并注册:ListAndWatch
当 NVIDIA Device Plugin 所在的 Pod 启动时,它首先会通过挂载的宿主机路径向本地的 Kubelet 的设备插件管理器 /var/lib/kubelet/device-plugins/kubelet.sock 发起注册请求。
注册成功后,Kubelet 会反向建立一个持久的 gRPC 连接,调用 ListAndWatch:
1 | // k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1/api.proto |
这个方法返回一个数据流(Stream)。Plugin 会调用底层的 NVML(NVIDIA Management Library)API 获取当前机器上所有的 GPU 状态,生成如下列表:
- Device
GPU-xxxx-xxxx-xxxx: 健康状态 Healthy - Device
GPU-yyyy-yyyy-yyyy: 健康状态 Unhealthy
当显卡发生硬件级故障、掉卡时,Plugin 会立刻在此 Stream 推送新的状态给 Kubelet。此时 Kubelet 会标记该资源不可用,后续调度器便不会将新 Pod 分配至该卡。
3.2 资源分配协议:Allocate
这是最为关键的一步。当你的 Pod spec.containers.resources.requests 中声明了 nvidia.com/gpu: 2 并且被调度到当前 Node。
Kubelet 这时充当包工头的角色,但它不知道应该具体分配 GPU 0、1 还是 GPU 2、3。于是 Kubelet 整理好要分配的 Device IDs 列表,通过 gRPC 发送给 Device Plugin 的 Allocate 接口。
1 | rpc Allocate(AllocateRequest) returns (AllocateResponse) {} |
NVIDIA Device Plugin 对这个接口做了什么?
它非常“克制”。在默认配置下,如果 Kubelet 请求的是物理卡的分配,Plugin 构建的 AllocateResponse 核心动作仅仅是:
- 取出请求的 GPU UUID 集合。
- 生成一个关键环境变量:将上述集合转化为注入容器内的
NVIDIA_VISIBLE_DEVICES(例如:NVIDIA_VISIBLE_DEVICES=0,1)。 - 返回给 Kubelet。
这解释了一个重要的事实:NVIDIA Device Plugin 几乎不亲自干“挂载”的脏活累活。挂载动作被非常优雅地转移给了下一层的 nvidia-container-toolkit。这保证了即使没有 K8s,仅用 Docker 命令行也能拥有相同一致的注入逻辑。
4. 关键技术细节剖析(硬核深水区)
我们继续向下下潜深入,看看在应用容器启动前的最后一瞬间,容器运行时到底在操作系统的 Cgroups 和 Namespace 维度干了什么。
4.1 神秘的 /dev/nvidia* 字符设备
Linux 下一切皆文件。容器想要调用 GPU,本质上必须在它的隔离环境内能“看到并读写”宿主机的特定字符设备。主要有以下几个:
/dev/nvidiaX: 代表具体的第 X 张独立的物理/逻辑显卡。/dev/nvidiactl: 控制驱动的总调度设备。/dev/nvidia-uvm: UVM(Unified Virtual Memory,统一虚拟内存)驱动设备,它支持 CUDA 的统一内存特性,使 CPU 和 GPU 可以共享同一块地址空间池。
libnvidia-container (由 Kubelet + Containerd 触发的 hook) 会读取由 Device Plugin 下发的 NVIDIA_VISIBLE_DEVICES,然后:
- 在 Pod 的 Mount Namespace 内,创建一个伪装的设备节点挂载这些字符设备。
- 更改当前 Pod 的 **Cgroup Device Rules (设备控制组限制)**,允许当前容器进程在沙箱内部对上述字符设备执行
mknod,read,write操作。
如果缺少了 Cgroup 层级的允许,即便强行用 -v /dev/nvidia0:/dev/nvidia0 把设备丢给容器,依然会报 Operation not permitted 的权限错误!
4.2 Driver Libraries 穿甲弹:不要在容器里装驱动
初学者常犯的一个错误是:不仅在宿主机上装了 NVIDIA 显卡驱动,还在自己构建的 PyTorch Docker 镜像里偷偷 apt-get 重新装了一遍驱动组件。这就造成了巨大的兼容灾难。
优秀的架构哲学是:驱动留给宿主机,容器内只需包含 CUDA Runtime(API 接口)。
那么,CUDA 在进行动态链接时,去哪里找底层的 libcuda.so 和 libnvidia-ml.so?
答案依然是 nvidia-container-toolkit 钩子!
在拉起容器时,Hook 程序不仅挂载设备设备,它还会从宿主机的 /usr/lib/x86_64-linux-gnu/ 目录中,扫描出对应当前驱动版本的那几十个 lib***.so 动态链接库文件,并将它们挂载/硬链接到了这个容器内部供 CUDA 工具链调用的特定目录池下,一并注入诸如 LD_LIBRARY_PATH 或者更新 ldconfig 缓存。这就是被称之为“穿甲弹”式的热挂载注入(Hot-Mount injection)。
4.3 GPU 细粒度隔离调度:从独占向切片演进
原生的 NVIDIA Device Plugin 长久以来面临最大的诟病是:**只能整卡分配 (Exclusive Allocation)**。
一个 Pod 只要声明了 1 gpu,这张动辄数万元、显存几十GB的显卡就会完全被该 Pod 独占霸占。即使这个 Pod 其实只跑了个小推理占用 2GB 显存,其它 Pod 也无法染指!在这个算力极度紧张的大模型时代,这属于纯纯的暴殄天物。
为了提高集群的 GPU 利用率和显存碎片利用,NVIDIA 引入了多个维度的共享策略:
方案 A:Time-Slicing (时间分片共用)
NVIDIA Device Plugin 社区现在官方支持了 Time-Slicing 策略。
原理:在 Plugin 的 ConfigMap 中配置 replicas: 10。
此时一块物理卡对外声称自己分身出了 10 个虚拟 GPU (vGPU),上报给 Kubelet 时总量变成 nvidia.com/gpu: 10。
当多个 Pod 同时被分配到同一张物理卡后,底层采用 CUDA MPS(Multi-Process Service)机制或操作系统的进程时间片调度。大家虽然在用同一张卡,但任务可以并发抢占执行。
缺点:这是假隔离。它既没有显存的绝对物理隔离边界(可能一个人写 OOM 把全机的任务都打爆),也没有严格的算力 QoS。
方案 B:MIG (Multi-Instance GPU) 物理隔离
自 NVIDIA Ampere 架构(A100)开始拥有的真正硬件级虚拟化黑科技。MIG 可以在物理层面上(硬件电路)将一块完整的大卡切分成最多 7 块相互绝缘的小卡(Instances)。
NVIDIA Device Plugin 全面支持了 MIG 抽象模式:
开启混合 MIG 后,Plugin 发现宿主机的 A100 被切成了两张计算能力和显存完全定死的逻辑卡:一个 1g.5gb,一个 2g.10gb。Plugin 会向 Kubelet 直接上报这两类全新资源名称:nvidia.com/mig-1g.5gb: 1 和 nvidia.com/mig-2g.10gb: 1。
用户便可以在 YAML 中做到硬件级独占的安全按需申报。这才是企业级多租户计算池中的标配。
5. 总结与展望:走向 DRA 星辰大海
对于致力于打造大规模企业级 AI 中台架构师来说,透彻理解 NVIDIA Device Plugin 的上下游技术栈就如同修炼了云原生领域的内功心法。
我们可以清晰地看到:
- **Kubernetes (Kube-Scheduler / Kubelet)**:只是“账房先生”,拿着账本盘点卡名和记账。
- NVIDIA Device Plugin:是“情报局长”和“政委”,连接 K8s 体系,翻译出谁能用、用哪张,并发放通行证 (
NVIDIA_VISIBLE_DEVICES)。 - **NVIDIA Container Toolkit (Hook)**:是真正的“潜入爆破手”,拿到了通行证后潜入沙箱,给原本封闭的 OCI 容器凿开
/dev/nvidia*缝隙并将依赖库送入手中。
未来演进:
尽管基于扩展资源(Extended Resource)的 Device Plugin 体系非常成熟,但它依然有致命缺陷——资源申请非常刻板,无法携带更多描述信息(例如“我需要能够 P2P 走 NVLink 连在一起的两张卡”,或是“我想同时动态获取一点点网络特性”)。
这就是目前 Kubernetes 社区正在重磅推行的 **DRA (Dynamic Resource Allocation, 动态资源分配)**。在 K8s v1.26+ 中引入的 DRA,将异构资源的请求变成基于 CRD 声明的独立 ResourceClaim 对象。这意味着未来的 NVIDIA 插件将演变成更加强大的 Driver / Resource 控制器,它可以根据复杂的 AI 拓扑结构按需现切 MIG,直接与调度系统谈判,从而为万卡集群大模型的训推调度,打开一个完全弹性的、动态流转的全新纪元。




