状态:草稿(2026-10-17),部分实测。 基准版本:NVIDIA/gpu-operator v26.3.3(提交 b0a49c0e7b2e)、NVIDIA/k8s-device-plugin v0.19.3(570e776d6813)、NVIDIA/dcgm-exporter 4.5.3-4.8.2(691c92762eb5)。由于小型集群正在运行这些版本,实测数据基于 2026-10-17 在该集群上的 只读 操作获取(读取 ClusterPolicy、守护进程集、节点标签、Pod 配置,通过 API 服务器代理
/metrics读取,以及读取运行中 Pod 内的文件)。同时引用了 2026-10-16 更长时间测量同一现象的记录。编写规则:以实测数据为基础,未测量的内容标记为“未实测”,未能确认的内容标记为“未确认”。
目录
- 1. 一句话总结:GPU 节点的“安装指南 + 检查表”
- 2. 架构图:状态列表与节点内部的准备链
- 3. 代码导读
- 4. 核心路径追踪
- 5. 作者的贡献与后续延伸
- 6. 实测:从小集群中读取的内容
- 7. 从哪里开始阅读
- 8. 适合贡献的第一步
- 9. 局限性
- 参考
1. 一句话总结:GPU 节点的“安装指南 + 检查表”
要在 Kubernetes 中使用 GPU,节点上的多个层必须协同工作:内核驱动、使容器能够看到 GPU 设备的容器工具包、向 kubelet 宣告 nvidia.com/gpu 资源的 device plugin、将 GPU 规格附加为节点标签的 GPU Feature Discovery (GFD),以及导出指标的 dcgm-exporter。只要有一个环节的顺序出错,Pod 就会卡在 Init 中或找不到 GPU。
GPU Operator 就像 安装指南一样依次安装这些层,并像 检查表一样在确认前一步完成后进入下一步。用户只需要使用名为 ClusterPolicy 的一个 CR。
| 组件 | 作用 | 运行位置 |
|---|---|---|
| gpu-operator (Operator) | 根据 ClusterPolicy 依次应用状态列表 | 1 个 Deployment |
| NFD (node-feature-discovery) | 根据 PCI 设备附加 feature.node.kubernetes.io/pci-10de.present 等标签 |
主节点 Deployment + 工作节点 DaemonSet(所有节点) |
| driver | 安装内核模块(容器驱动)。如果主机上已有驱动则禁用 | GPU 节点 DaemonSet |
| container-toolkit | 运行时配置,生成 CDI spec (management.nvidia.com) 等 |
GPU 节点 DaemonSet |
| operator-validator | 验证驱动、工具包、CUDA、插件是否实际可用并留下“准备就绪文件” | GPU 节点 DaemonSet |
| device plugin | nvidia.com/gpu 宣告与分配、分时复用、生成 CDI spec (k8s.device-plugin.nvidia.com) |
GPU 节点 DaemonSet |
| GFD | 将 GPU 数量、复用数、共享方式等设为节点标签 | GPU 节点 DaemonSet |
| dcgm-exporter | 将 DCGM 值转换为 Prometheus 格式,并附加 Pod 标签 | GPU 节点 DaemonSet |
2. 架构图:状态列表与节点内部的准备链
读图指南:
- 上方是 Operator 侧。一个“状态 (state)”对应仓库
assets/下的一个文件夹,该文件夹中的 YAML(ServiceAccount、RBAC、ConfigMap、DaemonSet…)作为一个整体被应用。编号代表实际应用顺序,虚线框表示在小集群配置中处于关闭状态。 - 下方是 GPU 节点内部。即使 Operator 规定了顺序,各个 DaemonSet 也会同时启动,因此 节点内部的顺序通过文件来协调 。验证器留下
driver-ready,toolkit-ready… 等文件,其他 DaemonSet 的 initContainer 会一直等待这些文件生成。 - Operator 只关心“DaemonSet 是否就绪”,而“GPU 是否实际可用”则由节点内的验证器来检查。职责分离是该设计的核心。
3. 代码导读
同时查看三个仓库。这里仅挑选了顶层中最重要的部分。
| 路径 | 内容 | 首先阅读的文件 |
|---|---|---|
gpu-operator api/ |
ClusterPolicy·NVIDIADriver CRD 的 Go 类型定义 | api/nvidia/v1/clusterpolicy_types.go |
gpu-operator controllers/ |
reconcile 循环、状态管理、按对象的应用与转换 | clusterpolicy_controller.go, state_manager.go |
gpu-operator assets/ |
按状态划分的清单集合(state-device-plugin/0500_daemonset.yaml 等) |
state-operator-validation/0500_daemonset.yaml |
gpu-operator cmd/nvidia-validator/ |
节点内验证器(驱动·工具包·CUDA·插件) | main.go |
k8s-device-plugin internal/plugin/ |
kubelet device plugin gRPC 服务器 | server.go |
k8s-device-plugin internal/rm/ |
构建设备列表、分时复用(::N) |
device_map.go, devices.go |
k8s-device-plugin internal/cdi/ |
生成并保存 CDI spec | cdi.go |
dcgm-exporter internal/pkg/transformation/ |
为指标附加 pod 与 namespace 标签 | kubernetes.go |
dcgm-exporter internal/pkg/server/ |
/metrics 处理,应用转换 |
server.go |
controllers/object_controls.go 超过 5000 行。建议不要从头到尾盲目阅读,而是在以下路径中遇到名称时再去查找并阅读相应的函数。
4. 核心路径追踪
4.1 reconcile 循环:将状态逐一推进“一步”
- 当 Operator 接收到 ClusterPolicy 时,
controllers/clusterpolicy_controller.go:93Reconcile开始运行。集群中仅允许存在一个 ClusterPolicy(第二个会Ignored),如果是首次运行则调用init。 controllers/state_manager.go:854init负责寻找 GPU 节点并附加标签(state_manager.go:521labelGPUNodes),并注册状态列表。通过 NFD 标签feature.node.kubernetes.io/pci-10de.present=true(10de 是 NVIDIA 的 PCI 厂商编号)来判断是否为 GPU 节点,并为这些节点附加nvidia.com/gpu.present=true和nvidia.com/gpu.deploy.<구성요소>=true标签。每个 DaemonSet 的 nodeSelector 正是这些标签。- 注册由
state_manager.go:175addState针对每个文件夹路径调用controllers/resource_manager.go:102addResourcesControls,根据 YAML 中的kind构建“应用该对象的函数”列表。一个状态 = 一个函数列表。 Reconcile将state_manager.go:1051step重复执行直至最后一个状态。step会运行该状态下的所有函数,如果有一个未就绪,则将该状态标记为 NotReady 并 进入下一个状态。全部跑完后,如果存在未就绪的状态,则会在 5 秒后再次运行。
for _, fs := range n.controls[n.idx] {
stat, err := fs(*n)
if err != nil {
return stat, err
}
// successfully deployed resource, now check if its ready
if stat != gpuv1.Ready {
result = stat
}
}
// move to next state
n.idx++
来源:NVIDIA/gpu-operator v26.3.3 controllers/state_manager.go(step 的一部分,Apache-2.0。
这里可以看出架构上的抉择: 它不会因为等待前置状态就绪而阻塞。 DaemonSet 会率先全部创建,实际顺序由节点内的准备文件来协调(4.2)。如果让 Operator 在每个节点处都进行等待,那么当某个节点变慢时,整个集群的部署就会停滞。
- 应用 DaemonSet 的函数是
controllers/object_controls.go:4621DaemonSet。如果状态被关闭,则删除对象并返回Disabled。如果开启,则object_controls.go:728preProcessDaemonSet会根据 DaemonSet 名称选择对应的 转换函数(nvidia-device-plugin-daemonset→TransformDevicePlugin,nvidia-dcgm-exporter→TransformDCGMExporter…)。ClusterPolicy 中的镜像、环境变量和配置正是通过这些转换函数注入到 YAML 中的。就绪状态由object_controls.go:3989isDaemonSetReady进行检查。 - 当新 GPU 节点加入时,
clusterpolicy_controller.go:246addWatchNewGPUNode的节点监视器会重新唤醒 reconcile。
4.2 节点内部准备链:验证器与 “-ready” 文件
验证器位于 cmd/nvidia-validator/main.go 单个文件中。存放状态文件的位置是 /run/nvidia/validations(第 146 行 defaultStatusPath),文件名在 161–189 行集中定义为常量(driver-ready, toolkit-ready, cuda-ready, plugin-ready …).
operator-validator的 initContainer 按照 driver → toolkit → cuda → plugin 的顺序进行验证并留下文件(assets/state-operator-validation/0500_daemonset.yaml).- 其他 DaemonSet 只是 等待 文件。device plugin 的 initContainer
toolkit-validation是一行每 5 秒检查一次toolkit-ready是否生成的 shell 命令(assets/state-device-plugin/0500_daemonset.yaml:29). - 驱动验证从
main.go:810runValidation开始。首先检查主机上安装的驱动(main.go:745validateHostDriver),如果失败则等待驱动容器侧(main.go:765validateDriverContainer).
通过文件进行衔接的方式虽然简单,但优点巨大。节点重启后, /run 会被清空,因此验证会自然地从头重新运行,且管理员只需在节点上执行 ls /run/nvidia/validations 一次即可查看卡在哪个环节。
4.3 device plugin:分时复用与 CDI spec
device plugin 是实现了 kubelet device plugin API 的 gRPC 服务器。其流程可归纳为三个函数:
internal/plugin/server.go:229Register— 向 kubelet 套接字注册“由我负责nvidia.com/gpu”。server.go:267ListAndWatch— 将设备列表(包含健康状态)流式传输给 kubelet。server.go:306Allocate— 在将设备分配给 Pod 时,返回需要放入容器的环境变量、设备和 CDI 名称。
分时复用(time-slicing) 发生在第 2 步。如果配置中包含 replicas: 5 , internal/rm/device_map.go:286
updateDeviceMapWithReplicas 会将 1 张 GPU 扩展并宣告为 GPU-<uuid>::0 … ::4 5 个“设备”。附加和移除 ::N 的工作由
internal/rm/devices.go:243 NewAnnotatedID 附近的小型类型负责。kubelet 并不知道这是同一块 GPU,会将其分别分发给 5 个 Pod。这里没有内存隔离。这种 ::N 与第 5 节的问题息息相关。
CDI spec 是一个文件,用于告知容器运行时:“若要分配此设备,请挂载这些设备文件并注入这些库”。
internal/cdi/cdi.go:187 CreateSpecFile 在 /var/run/cdi(第 41 行 cdiRoot)保存 spec,该函数在插件启动时调用一次(cmd/nvidia-device-plugin/plugin-manager.go:84)。spec 中包含的库文件名 带有驱动版本号 。因此,如果只更新了主机驱动而没有重启插件,spec 指向的库与实际加载的内核模块版本可能会不一致。由于插件在收到 kubelet 重启、配置更改或信号时会重新启动(main.go start 的重启循环),在更新驱动后,实际上有必要重启节点或重启插件 Pod。
4.4 dcgm-exporter:“这个 GPU 是谁的”
dcgm-exporter 会从 DCGM 读取 GPU 各项指标,并 /metrics 将其导出。 DCGM_EXPORTER_KUBERNETES=true 如果是这样,会在每行 pod·namespace·container
附加标签,而该信息不是从 Kubernetes API 获取的, 而是从 kubelet 的 pod-resources gRPC(默认 /var/lib/kubelet/pod-resources/kubelet.sock)中获取。
因为只有 kubelet 才知道“这个容器收到了哪个设备 ID”。
/metrics当收到请求时,internal/pkg/server/server.go:249Metrics→render会 遍历转换列表(server.go:309transformation.Process). 每次抓取 遍历一次)。internal/pkg/transformation/kubernetes.go:296getMappings连接到套接字, 并kubernetes.go:533listPods通过List()调用。- 在默认值(
--kubernetes-virtual-gpus关闭)下,kubernetes.go:712toDeviceToPod会构建 “设备 ID → 单个 Pod”的映射。对于分时设备 ID,GPU-<uuid>::N会剥离::前面的部分,并以 GPU UUID 作为键。
} else if strings.Contains(deviceID, "::") {
gpuInstanceID := strings.Split(deviceID, "::")[0]
// (디버그 로그 생략)
deviceToPodMap[gpuInstanceID] = podInfo
}
出处:NVIDIA/dcgm-exporter 4.5.3-4.8.2 internal/pkg/transformation/kubernetes.go 第 887–899 行摘要,Apache-2.0。
如果三个 Pod 共享同一个 GPU,这三个 Pod 会依次 覆盖同一个键(kubernetes.go:898。
最终,响应中最后出现的 Pod 会胜出。而且,由于 kubelet 是从 Go 映射中取出 Pod 列表来构建响应的,因此每次调用时响应的顺序都会发生变化
(基于 kubelet v1.34 pkg/kubelet/pod/pod_manager.go 的映射遍历源码)。
代码中“后者胜出”的逻辑与响应顺序的随机性相遇,导致 每次抓取时的标签都会发生变化。
--kubernetes-virtual-gpus(如果开启环境变量 KUBERNETES_VIRTUAL_GPUS), kubernetes.go:662
toDeviceToSharingPods 会将一个 GPU 与一个 Pod 列表绑定,并为每个 Pod 复制一行设备指标(某些字段按进程值进行拆分)。
GPU Operator 的默认部署不会开启此选项。
5. 作者的贡献与相关链接
- NVIDIA/gpu-operator#3086(Issue #3085,待评审):当宿主机驱动与库版本不匹配时,验证器日志会
显示“宿主机上没有驱动”,并且重试警告没有原因说明的问题。修复了 4.2 的
runValidation(第 810 行)将validateHostDriver的失败记录为 “无”的行(第 817 行),以及validateDriverContainer的重试警告(第 802 行,末尾会打印出字面量\n)。 故障故事请参考 自动更新让 GPU 停机的那一天 中。 - NVIDIA/cloud-native-docs#524(待审查):这是一个将相同的“驱动/库版本不匹配”问题写入故障排查文档的 PR。
- dcgm-exporter 分时标签抖动(筹备中,发布前):关于将 4.4 的
toDeviceToPod改为按 Pod 名称顺序轮询,使同一个 Pod 保持均匀的提案。因为这会改变默认行为,所以决定先通过 Issue 询问,目前只有草案。
6. 实测:小型集群中的读取结果
2026-10-17,全部为读取命令(kubectl get,运行中的 Pod 内部 ls·grep,API Server 代理 kubectl get --raw …/proxy/metrics)。
节点已按角色遮盖,Pod 已按 P、Q、R 遮盖。
ClusterPolicy 与 DaemonSet。
| 项目 | 读取值 |
|---|---|
| ClusterPolicy 状态 | state: ready,条件 Reconciled(自 2026-10-14 起) |
driver.enabled |
false — 使用宿主机安装的驱动(第 3 种状态 driver 关闭) |
dcgm.enabled / dcgmExporter.enabled |
false / true — 不使用单独的 DCGM,使用 exporter 内部的引擎 |
cdi.enabled |
true |
migManager.enabled, sandboxWorkloads.enabled |
两者均 false |
devicePlugin.config |
默认 no-sharing,配置包名称 time-slicing-config |
| DaemonSet | 期望数 / 就绪数 | nodeSelector |
|---|---|---|
| NFD worker | 8 / 8 | 无(所有节点) |
| container-toolkit | 5 / 5 | nvidia.com/gpu.deploy.container-toolkit=true |
| operator-validator | 5 / 5 | …deploy.operator-validator=true |
| device-plugin | 5 / 5 | …deploy.device-plugin=true |
| mps-control-daemon | 0 / 0 | 以上 + nvidia.com/mps.capable=true(无对应节点) |
| dcgm-exporter | 5 / 5 | …deploy.dcgm-exporter=true |
| gpu-feature-discovery | 5 / 5 | …deploy.gpu-feature-discovery=true |
在八台节点中有五台是 GPU 节点,这五台都带有 nvidia.com/gpu.present=true 和 gpu.deploy.* 八个标签(4.1 的第 2 阶段)。
验证链。 operator-validator Pod 的 initContainer 是 driver-validation → toolkit-validation → cuda-validation → plugin-validation
顺序,device plugin Pod 是 toolkit-validation → config-manager-init, dcgm-exporter Pod 是 toolkit-validation 唯一的。正如图片下方的链条所示。
分时。 只有一台 GPU 节点(GPU 节点 B) nvidia.com/device-plugin.config=shared-5 通过标签选择了不同的配置。该配置是
replicas: 5, failRequestsGreaterThanOne: true(防止一个 Pod 占用多个槽位), renameByDefault: false 结果如下。
| GPU 节点 | gpu.sharing-strategy |
gpu.replicas |
allocatable nvidia.com/gpu |
|---|---|---|---|
| B | time-slicing | 5 | 5 |
| 其余四个 | none | 1 | 1 |
一张 GPU(24GB 级别)在 kubelet 中显示为五张。标签 gpu.replicas·gpu.sharing-strategy 是由 GFD 附加的。
CDI spec. 在设备插件 Pod 内的 /var/run/cdi 中看到了两个文件。一个是插件创建的 k8s.device-plugin.nvidia.com-gpu.json ,另一个是
工具包创建的 management.nvidia.com-gpu.yaml 。这五个节点的插件 spec 修改时间都是 该插件 Pod 启动的时间(详见 4.3 的
“启动时一次性”),且今天所有五个节点的 spec 中的 libnvidia-ml.so.<판> 版本与 /proc/driver/nvidia/version 的内核模块版本均相同。
各个节点的 spec 日期从 9 月初到 10 月中旬不等,由此可知更新了驱动程序的节点在此之后也重新启动了插件。
dcgm-exporter 的 Pod 标签。 以 10 秒为间隔,读取了 GPU 节点 B 的 exporter /metrics 六次。每次每个 GPU 会输出一行 15 个指标(配置文件
dcp-metrics-included.csv),当时在该节点上请求了 GPU 且处于运行中的 Pod 有三个。
| 抓取 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
DCGM_FI_DEV_GPU_UTIL 的 pod |
P | Q | P | R | Q | R |
在五次抓取过程中全部发生了变化。2026-10-16 以 2 秒间隔读取同一节点 30 次时,三个 Pod 的次数分别为 19、8、3(第二次运行为 19、10、1,连续两次
之间变化 12 次);在 Prometheus(15 秒间隔)中累积的 1 小时数据中,单个 GPU 被拆分为三个时间序列,分别为 129、76、35 个样本,在 24 小时内具有不同的
exported_pod 值的有 7 个。这就是仪表板中按 Pod 聚合时线条断断续续的原因。
7. 从哪里开始阅读
controllers/clusterpolicy_controller.go:93Reconcile— 在 150 行内即可看清整体流程。controllers/state_manager.go:854init和第 1051 行的step— 状态列表与迈出第一步。assets/state-device-plugin/文件夹 — 直观确认“单个状态”是由哪些 YAML 组成的。object_controls.go:1520TransformDevicePlugin— ClusterPolicy 值写入 YAML 的实际示例。internal/plugin/server.go中的Register·ListAndWatch·Allocate— 设备插件 API 的三个函数。internal/pkg/transformation/kubernetes.go:296到Process— 指标关联到 Pod 的路径。
8. 适合首次贡献的地方
- dcgm-exporter README 中没有 Kubernetes 标志(flag)的说明。
pkg/cmd/app.go中有 38 个带有环境变量名的标志,但 README 并没有涉及--kubernetes-virtual-gpus或 pod-resources 套接字路径。此外,环境变量名有些混杂。其中大多数以DCGM_EXPORTER_开头, 但有 7 个不遵循此外缀,其中有 4 个(如KUBERNETES_VIRTUAL_GPUS,KUBERNETES_ENABLE_DRA,NVIDIA_RESOURCE_NAMES等)甚至连DCGM_都没有 (pkg/cmd/app.go:303)。编写一个将其整理成表格的文档 PR 是一个很好的开始。 - device plugin 的 CDI spec 不知道驱动程序已更新。 与 4.3 类似,spec 仅在插件启动时创建。“更新驱动程序后必须重新启动插件”这一点应当写入 文档中,或者将检测驱动程序版本更改并重新创建 spec 的功能作为 Issue 提出,是值得先去探讨的方向。
- gpu-operator 的 TODO。
step顶部的 TODO(在触及其他控制器拥有的对象之前检查 OwnerRef)、transformValidationInitContainer中的 “TransformValidatorComponent与……重复”的 TODO(object_controls.go:3901) 范围较小且测试(transforms_test.go)已经很严密,非常适合动手修改。 - 规则:gpu-operator 不需要 CLA,仅要求提交签名(
Signed-off-by)(CONTRIBUTING.md)。外部 PR 等待 NVIDIA 方面的 CI 验证需要较长时间。
9. 局限性
- 由于 driver、MIG、vGPU、sandbox 系列状态在小型集群中处于关闭状态,因此仅阅读了代码(未实际测量)。NVIDIAIDriver CRD 路径也未涉及。
--kubernetes-virtual-gpus开启 之后的标签未进行测量(未测量)。开启后指标行数随 Pod 数量增加所带来的开销也未进行测量。- 本次未复现 CDI spec 实际不匹配时 Pod 如何失败的情况(仅记录在第 2 篇故障笔记中)。
- 未涉及验证器的 CUDA 验证所启动的工作 Pod 以及 MPS 路径。
小 T 的一言。 五个人轮流用一张乒乓球桌,记分牌的主人栏里写的是“最后排队的人”的名字。排队顺序每次都在变,所以主人也在每次变。哪怕只按名字首字母排序也会安静下来呢!
参考
所有访问日期均为 2026-10-17。
- NVIDIA/gpu-operator v26.3.3:
controllers/,assets/,cmd/nvidia-validator/main.go,CONTRIBUTING.md - NVIDIA/k8s-device-plugin v0.19.3:
internal/plugin/server.go,internal/rm/device_map.go,internal/cdi/cdi.go,api/config/v1/replicas.go - NVIDIA/dcgm-exporter 4.5.3-4.8.2:
internal/pkg/transformation/kubernetes.go,internal/pkg/server/server.go,pkg/cmd/app.go - kubernetes/kubernetes v1.34.1:
pkg/kubelet/apis/podresources/server_v1.go,pkg/kubelet/pod/pod_manager.go - 公开 PR 与 Issue:NVIDIA/gpu-operator#3085·#3086、NVIDIA/cloud-native-docs#524
- 引用的代码片段来自上述仓库,均采用 Apache License 2.0 协议。在文章中经过了简写处理。