跳到主要内容

L1.9 GPU Operator 与 Device Plugin 部署

三维坐标 layer: L1(算力与编排)level: Seniorpillar: 硬件架构

L1.4 讲了 device plugin → kubelet → 调度器链路;本章给出落地清单:如何在 K8s 集群上用 GPU Operator 自动装驱动、container toolkit、device plugin、DCGM,并验收 nvidia.com/gpu 资源可见。读完你应当能回答:「一台裸金属 GPU 节点,到『Pod 里能跑 nvidia-smi』之间隔着哪几层,每层坏了长什么样」。

学习目标

  • 前置知识:读过 L1.7 K8s 核心对象 与 L1.4 调度原理;集群已有 K8s 1.28+(截至 2026 年中稳定版为 v1.36)。无需写过 Operator。
  • 学完产出:① 能列出 GPU Operator 管理的 6 大组件及其依赖顺序,并说清「为什么 driver 必须先于 toolkit、toolkit 必须先于 device plugin」;② 能解释 device plugin 的 ListAndWatch 如何把 GPU 上报为 nvidia.com/gpu,以及掉卡时 healthy → unhealthy 的传播路径;③ 能完成 Helm 安装 + gpu-feature-discovery 节点标签验收;④ 能提交一个 resources.limits.nvidia.com/gpu: 1 的测试 Pod 并进入容器跑 nvidia-smi;⑤ 知道 MIG 模式下资源名变为 nvidia.com/mig-1g.5gb 等切片名,并能对一个推理业务做「整卡 / MIG / time-slicing」三选一的容量决策。
  • 阅读姿势:几百张 GPU 节点手工装驱动是运维事故温床——GPU Operator 把「节点 GPU 就绪」变成 GitOps 可声明的状态。盯住一条主线:GPU 在 K8s 里不是「插上就能用」的一等公民,而是靠一条脆弱的组件链条「翻译」成可调度资源的;本章所有内容都在回答「这条链条怎么搭、怎么验、断在哪」。

背景与现状

裸金属 GPU 节点要上 K8s,传统手工步骤:

  1. 装 NVIDIA 驱动(版本与 CUDA 用户态匹配)
  2. nvidia-container-toolkit(让 runtime 挂 /dev/nvidia*
  3. 部署 nvidia-device-plugin DaemonSet(向 kubelet 注册扩展资源)
  4. (可选)DCGM Exporter、MIG Manager、GPU Feature Discovery

GPU Operator 用一组 CRD + Operator 控制器自动编排上述步骤,并支持驱动容器化(driver container)以便与 host kernel 解耦升级。

一个真实感的业务症状:某团队周五扩容了 8 台新 GPU 节点,周一发现训练任务全部 Pending。kubectl describe pod 显示 0/8 nodes are available: 8 Insufficient nvidia.com/gpu——节点明明插着卡,调度器却「看不见」GPU。逐层排查:nvidia-smi 在节点上报 command not found(驱动没装)→ 追到 GPU Operator 的 driver DaemonSet 在新节点上 CrashLoopBackOff → 日志显示新节点镜像用了更新的内核版本,driver 容器仓库里没有匹配该 kernel 的预编译驱动镜像。这个案例浓缩了本章的核心认知:nvidia.com/gpu 这个数字是一条组件链条层层翻译出来的,链条上任何一环断掉,表象都是同一个「Insufficient nvidia.com/gpu」——不懂链条就只能瞎猜,懂链条则五分钟定位。

📅 时效口径(截至 2026 年中):K8s 稳定版为 v1.36DRA(Dynamic Resource Allocation)已于 v1.34(2025-08)GA,正逐步接替「整数 GPU 计数」的 device plugin 模型,但生产环境仍以 device plugin + GPU Operator 为绝对主流,DRA 适合表达 MIG/拓扑/MPS 等细粒度需求;NVIDIA GPU Operator 持续活跃迭代(驱动预编译镜像、DRA driver 整合等 roadmap 项以官方 release notes 为准)。本章命令以 GPU Operator 24.x~25.x 时代的稳定接口为基线,长期有效。

原理与架构

2.1 GPU Operator 组件栈

依赖顺序不是画出来好看的,而是硬性的——GPU Operator 内部用 validator(initContainer 探针)串行推进:

顺序组件为什么必须在前一环之后
1Driver DaemonSet内核模块 nvidia.ko 不加载,/dev/nvidia* 设备节点根本不存在
2Container Toolkit它负责把设备节点与驱动库注入容器;没有驱动它无物可挂
3Device Plugin它枚举 GPU 需要调用 NVML(驱动用户态库);驱动/toolkit 未就绪则枚举为 0
4DCGM / GFD / MIG Manager监控、打标签、切分都建立在「GPU 可枚举」之上

这解释了上面案例的现象:driver 一环 CrashLoop,后面所有组件的 validator 全部卡住等待,device plugin 永远不会注册,节点 capacity 里就永远不出现 nvidia.com/gpu。排查 GPU 节点问题的标准姿势因此是「顺着链条从下往上验」——先节点 nvidia-smi,再容器 runtime,再 device plugin 日志,最后看调度:

# 链条自检四连(在问题节点上从下往上跑)
nvidia-smi # ① 驱动层:内核模块 + 用户态库
kubectl -n gpu-operator get pods -o wide | grep <node> # ② Operator 各组件在该节点的状态
kubectl -n gpu-operator logs ds/nvidia-device-plugin-daemonset --tail=50 # ③ 插件注册日志
kubectl describe node <node> | grep -A3 Capacity # ④ 账本:nvidia.com/gpu 是否出现

另一个高频变体症状:GPU 数量「对不上」——节点物理 8 卡,capacity 却显示 16 或 56。前者多半是有人留了一份 time-slicing ConfigMap(1 卡虚拟 2 份),后者是节点开了 MIG 且 profile 为 7 切片(8 × 7 = 56 个 mig-* 资源)。数字异常不一定是故障,先查 device plugin 的配置来源再下结论。

2.2 Device Plugin 工作机制

Device plugin 是一个跑在每个 GPU 节点上的 DaemonSet,通过 Unix socket 上的 gRPC 与 kubelet 对话,核心就三个动作:

  1. ListAndWatch:向 kubelet 注册 nvidia.com/gpu,上报 healthy GPU 数量。这是一条长连接流——设备列表一有变化(掉卡/恢复)就推送增量,kubelet 据此更新节点 status.capacitystatus.allocatable
  2. Allocate:Pod 被调度到节点、kubelet 准备启动容器时调用。device plugin 把具体分到哪几张卡的设备节点(/dev/nvidia0 等)、驱动库、CUDA 挂载路径写入容器 OCI spec——「哪张卡给哪个 Pod」是节点上此刻才决定的,调度器只算数量不认卡号
  3. Healthcheck:device plugin 内部用 NVML 监听 GPU XID 错误(GPU 硬件/驱动异常码,如 XID 79 = GPU 掉出总线);出现致命 XID 或掉卡时把该设备标记 unhealthy,通过 ListAndWatch 推送给 kubelet,allocatable 数量减一,调度器不再往这张坏卡上分配新 Pod

又一个真实感症状:某推理平台凌晨收到告警「节点 gpu-17 的 nvidia.com/gpu capacity 从 8 变成 7」。值班同学登录一看:dmesg 里 XID 79,一张卡掉了;device plugin 已自动把它摘出资源池,新 Pod 不再调度上来——但原本跑在这张坏卡上的推理 Pod 还「活着」,进程 hang 在 CUDA 调用上,探针超时后才被重启,重启后又因为 Allocate 只剩健康卡而恢复正常。这个案例揭示 device plugin 健康检查的边界:它只管「不再分配」,不管「驱逐已分配」——后者要靠 liveness 探针、DCGM 告警联动 cordon/drain 来补齐(思考题 2 展开算这笔账)。

2.3 MIG 与资源名

启用 MIG(Multi-Instance GPU,A100/H100 起支持)后,一张物理卡被硬件级切分成多个算力与显存都隔离的实例,device plugin 上报的是切片而非整卡:

资源名含义
nvidia.com/gpu整卡(MIG 关闭时)
nvidia.com/mig-1g.5gbA100 40GB 的 1/7 算力 + 5GB 显存切片
nvidia.com/mig-2g.10gb2/7 算力 + 10GB 显存切片
nvidia.com/mig-3g.20gb3/7 算力 + 20GB 显存切片

Pod 必须请求与切片规格匹配的 resource name;MIG Manager 负责按 ConfigMap 声明的 profile 对节点执行切分/重切(重切会短暂驱逐该卡上的负载,需窗口期操作)。MIG 与 time-slicing 的本质区别:MIG 是硬件隔离(显存越界直接失败,邻居打满不影响你),time-slicing 只是时间片轮转共享(显存不隔离、互相可饿死)——多租户生产共享选 MIG,内部低优先级小推理才考虑 time-slicing。

2.4 演进视角:Device Plugin → DRA

Device plugin 模型的根本局限是**「资源 = 整数计数」**:调度器只知道节点有 N 个 nvidia.com/gpu,不知道卡间 NVLink 拓扑、不能表达「给我同一 NUMA 下的 2 张卡」。DRA(Dynamic Resource Allocation)ResourceClaim/DeviceClass 把设备变成有结构化属性、可协商分配的对象,v1.34 起 GA。

维度Device Plugin(现状主流)DRA(v1.34 GA,逐步渗透)
资源表达整数计数:nvidia.com/gpu: 2结构化 claim:型号/显存/拓扑/MIG profile 可协商
卡的选择节点上 Allocate 时才定,调度器不感知调度期即可参与设备属性匹配
MIG / 共享靠资源名枚举(mig-1g.5gb 等)+ ConfigMap一等公民语义
生态成熟度多年生产验证,工具链完备驱动/监控/配额生态仍在补齐

截至 2026 年中的务实姿势:存量集群继续 device plugin,新建多租户/MIG 重度集群开始评估 DRA;GPU Operator 已整合 k8s-dra-driver 组件(成熟度与默认开关以官方 release notes 为准),两种模型会长期共存。对读者的建议:本章的组件链心智模型在 DRA 时代依然成立——驱动、toolkit、健康检查一个都不会少,变的只是「资源如何向调度器表达」这最后一环。

动手实践:Helm 安装与验收

以下在已有 GPU 的 K8s 集群kind + fake plugin 上演练流程;无 GPU 时用 fake plugin 验证 YAML 语法。

步骤 1:添加 Helm repo 并安装(生产集群)

helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

# 最小安装(驱动已预装在节点上时可用 driver.enabled=false)
helm install gpu-operator nvidia/gpu-operator \
-n gpu-operator --create-namespace \
--set driver.enabled=true \
--set toolkit.enabled=true \
--set devicePlugin.enabled=true \
--set dcgmExporter.enabled=true

云厂商托管节点(如 EKS GPU AMI、GKE 自带驱动)通常已预装驱动,此时务必 driver.enabled=false,否则 Operator 的 driver 容器会与 host 驱动打架。

步骤 2:验收 Operator Pod

kubectl get pods -n gpu-operator
# 预期:gpu-operator-*、nvidia-device-plugin-daemonset-*、nvidia-dcgm-exporter-* Running

kubectl get nodes -o json | jq '.items[].status.capacity | select(has("nvidia.com/gpu"))'
# 预期:{"nvidia.com/gpu": "8", ...}

若 capacity 里没有 nvidia.com/gpu,按 2.1 节的链条从下往上验:节点 nvidia-smi → driver DaemonSet 日志 → device plugin 日志。

步骤 3:GPU 测试 Pod

# gpu-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: gpu-test
spec:
restartPolicy: Never
containers:
- name: cuda
image: nvcr.io/nvidia/cuda:12.6.0-base-ubuntu22.04
command: ["nvidia-smi"]
resources:
limits:
nvidia.com/gpu: 1
kubectl apply -f gpu-test.yaml
kubectl logs gpu-test
# 预期:正常输出 nvidia-smi 表格
kubectl delete pod gpu-test

步骤 4:DCGM 指标(可选)

kubectl port-forward -n gpu-operator svc/nvidia-dcgm-exporter 9400:9400
curl -s localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL | head

部署 Checklist(生产)

步骤验收命令通过标准
驱动版本节点 nvidia-smi与 CUDA 镜像兼容
Toolkitdocker run --gpus all nvidia/cuda:12.6.0-base nvidia-smi容器内可见 GPU
Device Pluginkubectl describe node | grep nvidia.com/gpu数量 = 物理卡数
测试 Podkubectl logs gpu-testnvidia-smi 正常
DCGMPrometheus scrapeDCGM_FI_* 指标 UP
MIG(若启用)kubectl describe node出现 mig-* 资源名

踩坑预警

  1. 驱动与内核模块:Operator 驱动容器需匹配 host kernel;升级 K8s / OS 内核前先查 NVIDIA 支持矩阵——本章开头案例的根因即在此。
  2. Containerd vs Docker:需配置 nvidia runtime class;K8s 1.24+ 默认 containerd。
  3. Time-Slicing:device plugin ConfigMap 可把 1 物理 GPU 虚拟成 N 份 nvidia.com/gpu——适合推理小模型,训练禁用(显存不隔离,见 2.3 节)。
  4. 整集群统一驱动版本:混装 535/550 驱动会导致 CUDA 符号不一致。
  5. MIG 重切有代价:改 MIG profile 会驱逐该卡负载并短暂不可用,务必放在维护窗口,且确保 Pod 有 PDB 保护。

配套代码:生产部署 checklist 见本章;K8s 前置见 L1.7 + ai-infra-labs kind gang

深入思考

下面三题每题先给题干,再用 <details> 折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。

思考题 1:何时用手工 device plugin,何时必须 GPU Operator?

10 台节点的小集群和 200 台节点的自管裸金属集群,对「GPU 就绪」这件事的工程化程度要求完全不同。结合 2.1 节的组件栈与依赖顺序,给出一个可操作的决策框架:什么规模/场景下只部署 device plugin DaemonSet 就够,什么情况下 GPU Operator 是必需品?并估算两种路线的运维人力差距。

展开参考答案(含决策流程图 + 人力成本算一遍)

结论:决策变量不是「节点数」本身,而是「驱动生命周期归谁管」——驱动由云厂商镜像托管、节点少且异构低时,裸 device plugin + DCGM 足够;一旦驱动要自己管(自管裸金属)、或需要 MIG/驱动滚动升级/GitOps 声明式对账,GPU Operator 就从「可选」变成「必需」,因为它把 2.1 节那条脆弱的依赖链变成了控制器自动收敛的状态。

人力成本算一遍(100 台自管裸金属节点,驱动每季度升级一次):

  1. 手工路线:每节点装驱动 + toolkit + 验证约 1 小时 → 首次上线 100 × 1 = 100 小时;每季度驱动升级重走一遍 → 年成本 100 + 3 × 100 = 400 小时 ≈ 50 人日(还没算手工操作的出错返工)。
  2. Operator 路线:Helm 首装 + 调参约 2 小时;此后每季度升级 = 改 ClusterPolicy 里的驱动版本号 + 观察滚动 ≈ 2 小时 → 年成本 2 + 3 × 2 = 8 小时 = 1 人日
  3. 差距 400 / 8 = 50 倍,且 Operator 路线的升级是声明式滚动、可灰度可回滚,手工路线每次都是一场「祈祷式运维」。

反过来,若你只有 8 台 EKS GPU 节点、驱动由官方 AMI 托管:手工路线只剩「apply 一个 device plugin DaemonSet」这一次性动作,年成本近似 0,此时引入 Operator 反而多了一层需要理解和升级的控制面。这正是 2.1 节依赖链的直接推论:Operator 的价值 = 它替你自动收敛的链条环节数 × 节点数 × 变更频率——三个因子都小时,它就是过度工程。

多租户 + 细粒度共享场景额外加一票给 Operator:MIG Manager 与 DRA driver(2.4 节)都只在 Operator 体系内才有开箱整合。

思考题 2:掉卡之后——device plugin 健康检查的边界在哪里?

2.2 节提到 device plugin 检测到 XID 错误会把设备标记 unhealthy。请沿着「XID 发生 → ListAndWatch 推送 → kubelet 更新 allocatable → 调度器感知」这条链路,分析:为什么掉卡后新 Pod 不会再被调度上来,但正在这张卡上跑的 Pod 却不会被自动驱逐?一个 16 节点 × 8 卡的集群应该怎样补齐这个缺口?

展开参考答案(含掉卡传播时序图 + 故障频率算一遍)

结论:device plugin 的职责边界是「资源账本」而非「工作负载生命周期」——它能把坏卡从 allocatable 里划掉(管住增量),但 K8s 的扩展资源模型里没有「这张卡上的存量 Pod 应该死」的语义,驱逐必须由探针、DCGM 告警联动 cordon/drain 或自愈控制器来补齐。

注意时序图里的分工:绿色路径(不再分配)是 device plugin 自动完成的;而存量 Pod 只能靠自己的 liveness 探针发现「CUDA 调用 hang 死」,被 kubelet 重启后走一次新的 Allocate 才落回健康卡。若 Pod 没配探针,它会拿着一张坏卡永远僵着——这正是 2.2 节案例里「凌晨告警 capacity 掉到 7,但推理 Pod 还活着」的机制根源。

故障频率算一遍(16 节点 × 8 卡 = 128 卡,按业界常见的年化 5% 单卡故障率估算):

  1. 期望年故障数 = 128 × 5% = 6.4 次/年,即平均每 365 / 6.4 ≈ 57 天一次掉卡
  2. 这意味着「掉卡处置」不是黑天鹅而是双月度例行事件——纯靠值班同学半夜手工 cordon 是不可持续的。
  3. 若每次人工处置 2 小时、自动化后降到 0(自愈控制器 drain + 工单),年省 6.4 × 2 ≈ 13 小时;更重要的是把「坏卡上僵尸 Pod 拖垮 SLA」的长尾风险从小时级压到分钟级。

补齐缺口的标准三件套:① 所有 GPU Pod 配 liveness 探针(或框架自带的心跳,如训练任务的 elastic agent);② DCGM Exporter 的 DCGM_FI_DEV_XID_ERRORS 指标接告警,触发自动 cordon + drain 该节点/卡;③ 掉卡即开硬件工单,修复后 device plugin 会通过 ListAndWatch 自动把卡加回账本,无需重启组件。

思考题 3:一张 A100,七个租户——MIG、time-slicing 还是整卡?

你的平台要承接一批 int8 后量化的小推理模型(单模型显存占用约 3GB,单实例约 60 QPS),底座是 A100 40GB 节点。结合 2.3 节 MIG 切片与 time-slicing 的隔离性差异,为「多租户对外服务」与「内部低优先级批量任务」分别选型,并算出三种方案的单卡服务密度差距。

展开参考答案(含三方案对比图 + 服务密度算一遍)

结论:对外多租户必须选 MIG——1g.5gb 切片给 3GB 模型提供硬件级显存与算力隔离,单卡密度提升到 7 倍且邻居互不干扰;time-slicing 虽然名义上能虚拟出更多份,但显存不隔离、一个租户 OOM 全卡遭殃,只配用于内部可容忍互相干扰的低优先级任务;整卡独占留给延迟极端敏感或显存吃满的大模型。

服务密度算一遍(A100 40GB,模型 3GB / 60 QPS):

方案实例数/卡单卡总 QPS显存安全边界隔离性
A 整卡独占1603GB / 40GB,浪费 92.5%天然隔离
B MIG 1g.5gb77 × 60 = 420每片 3GB / 5GB = 60%,硬隔离硬件级 ✅
C time-slicing 10 份10(名义)理论 600,实际算力分时共享打折10 × 3 = 30GB 共享池,无越界保护无 ❌

三个关键数字自洽验证:① 方案 B 密度是方案 A 的 420 / 60 = 7 倍,正好等于切片数——因为瓶颈是「实例数」而非算力;② 方案 C 名义实例 10 / 7 ≈ 比 MIG 多 43%,但代价是任何一个租户内存泄漏、显存涨过 4GB,挤爆的是整卡 40GB 共享池,7 个无辜邻居一起 CUDA OOM——对外服务的 SLA 无法接受这种连坐;③ 方案 B 每片仍有 5 - 3 = 2GB 余量吃请求突发的 KV/activation 波动,是三者中「密度 × 安全」乘积最优的点。

落地映射:对外多租户 → MIG Manager 声明 all-1g.5gb profile,Pod 请求 nvidia.com/mig-1g.5gb: 1;内部低优批量 → time-slicing ConfigMap(并给这些 Pod 打上「可被抢占」的低 PriorityClass);70B 级大模型 → 保持整卡/多卡。这正是 2.3 节「MIG 是硬件隔离、time-slicing 只是时间片轮转」的直接推论:资源名(nvidia.com/gpu vs nvidia.com/mig-*)背后是两种完全不同的多租承诺。

延伸阅读

1. 官方文档 / 一手资料

2. 相关仓库

  • NVIDIA/gpu-operator — 看 controllers/ 与 ClusterPolicy CRD,理解组件链的状态机收敛。
  • NVIDIA/k8s-device-plugin — 看 internal/plugin/ 的 ListAndWatch/Allocate 实现,对应 2.2 节。

3. 站内前置与关联