TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

阅读 NVIDIA GPU Operator:一个 ClusterPolicy 如何准备 GPU 节点,以及 dcgm-exporter 是如何寻找 GPU 主人的

状态:草稿(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 节点的“安装指南 + 检查表”

要在 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. 架构图:状态列表与节点内部的准备链

ClusterPolicy 하나가 상태 목록을 차례로 적용한다 ClusterPolicy 클러스터에 하나 Reconcile → step() 반복 안 된 상태가 있으면 5초 뒤 다시 상태 = assets/ 폴더 하나(매니페스트 묶음) → 변환 → 적용 1 pre-requisites 2 operator-metrics 3 driver 4 container-toolkit 5 operator-validation 6 device-plugin 7 mps-control 8 dcgm 9 dcgm-exporter 10 gpu-feature-disc. 11 mig-manager 12 node-status-exp. 13–20 vGPU·샌드박스·kata·cc 계열(샌드박스 워크로드용) 켜짐 꺼짐(작은 클러스터 설정 — 호스트 드라이버 사용 등) GPU 노드 안: 파일 하나씩으로 잇는 준비 사슬 operator-validator 파드의 initContainer 순서 driver 검증 driver-ready toolkit 검증 toolkit-ready cuda 검증 cuda-ready plugin 검증 plugin-ready 파일은 노드의 /run/nvidia/validations 에 남는다 device-plugin·dcgm-exporter 는 toolkit-ready 를 기다린 뒤 뜬다
图:ClusterPolicy 依次应用状态列表,在节点内部通过验证文件衔接准备顺序

读图指南:

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 循环:将状态逐一推进“一步”

  1. 当 Operator 接收到 ClusterPolicy 时, controllers/clusterpolicy_controller.go:93 Reconcile 开始运行。集群中仅允许存在一个 ClusterPolicy(第二个会 Ignored),如果是首次运行则调用 init 。
  2. controllers/state_manager.go:854 init 负责寻找 GPU 节点并附加标签(state_manager.go:521 labelGPUNodes),并注册状态列表。通过 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 正是这些标签。
  3. 注册由 state_manager.go:175 addState 针对每个文件夹路径调用 controllers/resource_manager.go:102 addResourcesControls ,根据 YAML 中的 kind 构建“应用该对象的函数”列表。一个状态 = 一个函数列表。
  4. Reconcile 将 state_manager.go:1051 step 重复执行直至最后一个状态。 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 在每个节点处都进行等待,那么当某个节点变慢时,整个集群的部署就会停滞。

  1. 应用 DaemonSet 的函数是 controllers/object_controls.go:4621 DaemonSet 。如果状态被关闭,则删除对象并返回 Disabled 。如果开启,则 object_controls.go:728 preProcessDaemonSet 会根据 DaemonSet 名称选择对应的 转换函数(nvidia-device-plugin-daemonset → TransformDevicePlugin, nvidia-dcgm-exporter → TransformDCGMExporter …)。ClusterPolicy 中的镜像、环境变量和配置正是通过这些转换函数注入到 YAML 中的。就绪状态由 object_controls.go:3989 isDaemonSetReady 进行检查。
  2. 当新 GPU 节点加入时, clusterpolicy_controller.go:246 addWatchNewGPUNode 的节点监视器会重新唤醒 reconcile。

4.2 节点内部准备链:验证器与 “-ready” 文件

验证器位于 cmd/nvidia-validator/main.go 单个文件中。存放状态文件的位置是 /run/nvidia/validations(第 146 行 defaultStatusPath),文件名在 161–189 行集中定义为常量(driver-ready, toolkit-ready, cuda-ready, plugin-ready …).

通过文件进行衔接的方式虽然简单,但优点巨大。节点重启后, /run 会被清空,因此验证会自然地从头重新运行,且管理员只需在节点上执行 ls /run/nvidia/validations 一次即可查看卡在哪个环节。

4.3 device plugin:分时复用与 CDI spec

device plugin 是实现了 kubelet device plugin API 的 gRPC 服务器。其流程可归纳为三个函数:

  1. internal/plugin/server.go:229 Register — 向 kubelet 套接字注册“由我负责nvidia.com/gpu ”。
  2. server.go:267 ListAndWatch — 将设备列表(包含健康状态)流式传输给 kubelet。
  3. server.go:306 Allocate — 在将设备分配给 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 는 스크랩마다 「이 GPU 는 누구 것」 을 묻는다 DCGM 값 GPU UUID 별 한 줄 PodMapper.Process() kubelet pod-resources List() List() 응답 — 시분할로 한 GPU 를 세 파드가 나눔 파드 P GPU-uuid::0 파드 Q GPU-uuid::1 파드 R GPU-uuid::2 순서는 kubelet 의 Go 맵 순회 → 부를 때마다 다르다 toDeviceToPod ::N 을 떼고 map[UUID] = 파드 → 마지막 파드가 이긴다 실측: 10초 간격 스크랩 6번의 pod 라벨 1회 P 2회 Q 3회 P 4회 R 5회 Q 6회 R 같은 GPU 의 시계열 하나가 세 파드 사이를 오간다 --kubernetes-virtual-gpus 를 켜면 toDeviceToSharingPods → 파드마다 한 줄씩 복사(일부 필드는 프로세스별 값)
图:dcgm-exporter 每次抓取时向 kubelet 询问 Pod,在分时 GPU 中由最后一个 Pod 获胜

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”。

  1. /metrics 当收到请求时, internal/pkg/server/server.go:249 Metrics → render 会 遍历转换列表(server.go:309 transformation.Process). 每次抓取 遍历一次)。
  2. internal/pkg/transformation/kubernetes.go:296 getMappings 连接到套接字, 并 kubernetes.go:533 listPods 通过 List() 调用。
  3. 在默认值(--kubernetes-virtual-gpus 关闭)下, kubernetes.go:712 toDeviceToPod 会构建 “设备 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. 作者的贡献与相关链接

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. 从哪里开始阅读

  1. controllers/clusterpolicy_controller.go:93 Reconcile — 在 150 行内即可看清整体流程。
  2. controllers/state_manager.go:854 init 和第 1051 行的 step — 状态列表与迈出第一步。
  3. assets/state-device-plugin/ 文件夹 — 直观确认“单个状态”是由哪些 YAML 组成的。
  4. object_controls.go:1520 TransformDevicePlugin — ClusterPolicy 值写入 YAML 的实际示例。
  5. internal/plugin/server.go 中的 Register·ListAndWatch·Allocate — 设备插件 API 的三个函数。
  6. internal/pkg/transformation/kubernetes.go:296 到 Process — 指标关联到 Pod 的路径。

8. 适合首次贡献的地方

9. 局限性

小 T 的一言。 五个人轮流用一张乒乓球桌,记分牌的主人栏里写的是“最后排队的人”的名字。排队顺序每次都在变,所以主人也在每次变。哪怕只按名字首字母排序也会安静下来呢!

参考

所有访问日期均为 2026-10-17。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论