TT Lab
开始
学习 学习路径 课程

GPU Operator 与时间片

把一行标签改掉,Pod 就没了

在 TT Lab 中继续学习

目标

用节点标签放置 GPU Operator 展开的五个 DaemonSet,在一个节点上只排除一个 operand,并制作判断“该有的 operand 是否都有了”和“是否存在顺序错乱的节点”的工具。

为什么重要

安装 GPU Operator 后,会启动十几个 Pod。这些 Pod 并不是一个程序,而是 Operator 读取 ClusterPolicy 之后展开的多个 DaemonSet。驱动、Container Toolkit、设备插件、GPU Feature Discovery、DCGM exporter、验证器和 MIG Manager 各有自己的 DaemonSet,每个 DaemonSet 都通过 nodeSelector 查看 nvidia.com/gpu.deploy.<이름>(占位符为名称)标签。了解了这个结构,在运维中能做的事就不一样了——只把一个节点从 operand 中排除,不在已装好驱动的节点上安装驱动,隔离出问题的节点,都只需要一行标签。不了解的话,结果就会相反。如果某个标签贴错了,出现没有 Container Toolkit、只有设备插件在运行的节点,资源会被上报,调度器会成功,只有工作负载拿不到设备。没有任何地方报错,几个小时就这样过去了。

步骤

  1. 在 /root/gpuops 中工作(export KUBECONFIG=/root/.kube/config)。创建命名空间 gpu-operator。给 lab-node-0(GPU 节点,没有驱动)添加 feature.node.kubernetes.io/pci-10de.present=true、nvidia.com/gpu.present=true,同时添加 nvidia.com/gpu.deploy.driver=true、nvidia.com/gpu.deploy.container-toolkit=true、nvidia.com/gpu.deploy.device-plugin=true、nvidia.com/gpu.deploy.gpu-feature-discovery=true、nvidia.com/gpu.deploy.dcgm-exporter=true。给 lab-node-1(GPU 节点,驱动已经装在主机上)添加相同的标签,但只把 nvidia.com/gpu.deploy.driver 设为 false。lab-node-2(没有 GPU)上,不要添加任何以 nvidia.com/gpu.deploy. 开头的标签。然后在 /root/gpuops/out/roster.txt 中写五行——按 <오퍼랜드이름>=<그 오퍼랜드를 켜는 라벨 키>(占位符依次为 operand 名称与启用该 operand 的标签键)的形式,对应 driver、container-toolkit、device-plugin、gpu-feature-discovery、dcgm-exporter 这五个。
  2. 在 /root/gpuops/k8s/toolkit-ds.yaml 中写出 DaemonSet nvidia-container-toolkit-daemonset——命名空间为 gpu-operator,Pod 标签和选择器为 app: nvidia-container-toolkit-daemonset,nodeSelector 为 nvidia.com/gpu.deploy.container-toolkit: "true",容器镜像为 nvcr.io/nvidia/k8s/container-toolkit:v1.16.2,内存请求为 128Mi。应用之后,请确认 desiredNumberScheduled 为 2,并且 lab-node-0 和 lab-node-1 上各启动了一个 Pod。
  3. 在 /root/gpuops/k8s/driver-ds.yaml 中写出 DaemonSet nvidia-driver-daemonset——命名空间相同,Pod 标签和选择器为 app: nvidia-driver-daemonset,nodeSelector 为 nvidia.com/gpu.deploy.driver: "true",镜像为 nvcr.io/nvidia/driver:550.90.07-ubuntu22.04,内存请求为 512Mi。应用之后,请确认明明有两个 GPU 节点,这个 DaemonSet 却只在一个节点上启动,并在 /root/gpuops/out/driver.txt 中写两行——DESIRED=<숫자>(占位符为数字)和 SKIPPED=<빠진 노드 이름>(占位符为被排除的节点名称)。
  4. 再创建两个 DaemonSet。/root/gpuops/k8s/device-plugin-ds.yaml 中的 nvidia-device-plugin-daemonset 使用 nodeSelector nvidia.com/gpu.deploy.device-plugin: "true"、镜像 nvcr.io/nvidia/k8s-device-plugin:v0.16.2、内存请求 128Mi。/root/gpuops/k8s/gfd-ds.yaml 中的 gpu-feature-discovery 使用 nodeSelector nvidia.com/gpu.deploy.gpu-feature-discovery: "true",镜像和内存请求相同。两者的 Pod 标签和选择器都是 app: <데몬셋 이름>(占位符为 DaemonSet 名称)。应用之后,请确认两个 DaemonSet 的 desiredNumberScheduled 都是 2。
  5. 在 /root/gpuops/k8s/dcgm-ds.yaml 中写出 DaemonSet nvidia-dcgm-exporter——nodeSelector 为 nvidia.com/gpu.deploy.dcgm-exporter: "true",镜像为 nvcr.io/nvidia/k8s/dcgm-exporter:3.3.7-3.5.0-ubuntu22.04,内存请求为 128Mi,Pod 标签和选择器为 app: nvidia-dcgm-exporter。应用后确认它在两个节点上都启动了,然后把 lab-node-1 的 nvidia.com/gpu.deploy.dcgm-exporter 改为 false,并确认 Pod 消失。在 /root/gpuops/out/optout.txt 中写两行——BEFORE=<바꾸기 전 desiredNumberScheduled> 和 AFTER=<바꾼 뒤 값>(占位符依次为修改前的 desiredNumberScheduled 与修改后的值)。
  6. 创建 /root/gpuops/operand-audit.sh。遍历集群中的所有节点,对该节点上 nvidia.com/gpu.deploy.<이름>(占位符为名称)标签为 true 的每个 operand,检查该 DaemonSet 的 Pod 是否在该节点上处于 Running。如果没有,就输出一行 <노드> MISSING <오퍼랜드이름>(占位符依次为节点与 operand 名称);如果该节点上一个都不缺,就输出一行 <노드> OK(占位符为节点)。只要缺了一个,退出码就是 1,否则是 0。operand 名称与 DaemonSet 名称的对应,就是第 1 步中的五个。创建之后,对当前集群运行它,并把输出保存到 /root/gpuops/out/audit.txt。不要把节点名称写在脚本里——评分器会再多创建一个节点后再调用它。
  7. 假设有人只在 lab-node-2 上手工添加了 nvidia.com/gpu.deploy.device-plugin=true。请真的添加这个标签(不添加其他 deploy 标签),并确认设备插件的 Pod 在该节点上启动了。然后创建 /root/gpuops/order-check.sh <노드이름>(占位符为节点名称)——如果该节点上设备插件的 Pod 处于 Running,但没有 Container Toolkit 的 Pod,就输出一行 device-plugin-without-toolkit 并以退出码 1 结束,其他情况输出一行 ok 并以 0 结束。依次对三个节点运行它,并在 /root/gpuops/out/order.txt 中按 <노드> <결과>(占位符依次为节点与结果)的形式写三行。
  8. 在 /root/gpuops/out/matrix.txt 中为每个节点写一行,共三行。格式是 <노드> driver=<yes|no> toolkit=<yes|no> device-plugin=<yes|no> gfd=<yes|no> dcgm=<yes|no>,其中 yes 表示该 operand 的 DaemonSet Pod 在该节点上处于 Running。行的顺序是 lab-node-0、lab-node-1、lab-node-2。然后在第四行写 TOTAL_PODS=<gpu-operator 네임스페이스에서 Running 인 데몬셋 파드 총수>(占位符为 gpu-operator 命名空间中处于 Running 的 DaemonSet Pod 总数)。数字和 yes/no 要向 API 查询后填写——凭前面步骤的记忆来写,就会对不上。

参考

为每个节点设置不同的 operand 开关标签

在 /root/gpuops 中工作(export KUBECONFIG=/root/.kube/config)。创建命名空间 gpu-operator。给 lab-node-0(GPU 节点,没有驱动)添加 feature.node.kubernetes.io/pci-10de.present=true、nvidia.com/gpu.present=true,同时添加 nvidia.com/gpu.deploy.driver=true、nvidia.com/gpu.deploy.container-toolkit=true、nvidia.com/gpu.deploy.device-plugin=true、nvidia.com/gpu.deploy.gpu-feature-discovery=true、nvidia.com/gpu.deploy.dcgm-exporter=true。给 lab-node-1(GPU 节点,驱动已经装在主机上)添加相同的标签,但只把 nvidia.com/gpu.deploy.driver 设为 false。lab-node-2(没有 GPU)上,不要添加任何以 nvidia.com/gpu.deploy. 开头的标签。然后在 /root/gpuops/out/roster.txt 中写五行——按 <오퍼랜드이름>=<그 오퍼랜드를 켜는 라벨 키>(占位符依次为 operand 名称与启用该 operand 的标签键)的形式,对应 driver、container-toolkit、device-plugin、gpu-feature-discovery、dcgm-exporter 这五个。

Operator 只有一个,operand 有多个——读取了 ClusterPolicy 的 Operator 会把它们展开成驱动、Toolkit、设备插件、GFD、DCGM exporter 之类的 DaemonSet。每个 DaemonSet 的 nodeSelector 都查看 nvidia.com/gpu.deploy.<이름>(占位符为名称)标签,所以只在一个节点上排除一个 operand,用一行标签就能做到。官方文档给出的、只在特定节点上不安装驱动的办法是 nvidia.com/gpu.deploy.driver=false。用 kubectl label node <이름> <키>=<값> --overwrite(占位符依次为节点名称、键与值)可以一次添加多个标签。

用标签放置第一个 operand

在 /root/gpuops/k8s/toolkit-ds.yaml 中写出 DaemonSet nvidia-container-toolkit-daemonset——命名空间为 gpu-operator,Pod 标签和选择器为 app: nvidia-container-toolkit-daemonset,nodeSelector 为 nvidia.com/gpu.deploy.container-toolkit: "true",容器镜像为 nvcr.io/nvidia/k8s/container-toolkit:v1.16.2,内存请求为 128Mi。应用之后,请确认 desiredNumberScheduled 为 2,并且 lab-node-0 和 lab-node-1 上各启动了一个 Pod。

DaemonSet 并不是“每个节点一个”,而是“每个符合条件的节点一个”。这个条件就是 nodeSelector,GPU Operator 在这里挂上自己的标签,按节点为单位开启和关闭 operand。desiredNumberScheduled 是 DaemonSet 控制器“判断这个 DaemonSet 应该启动的节点数”——标签匹配的节点增加或减少,这个数字就会随之变动。用 kubectl -n gpu-operator get ds -o wide 可以一目了然地查看。

只有驱动被排除的节点

在 /root/gpuops/k8s/driver-ds.yaml 中写出 DaemonSet nvidia-driver-daemonset——命名空间相同,Pod 标签和选择器为 app: nvidia-driver-daemonset,nodeSelector 为 nvidia.com/gpu.deploy.driver: "true",镜像为 nvcr.io/nvidia/driver:550.90.07-ubuntu22.04,内存请求为 512Mi。应用之后,请确认明明有两个 GPU 节点,这个 DaemonSet 却只在一个节点上启动,并在 /root/gpuops/out/driver.txt 中写两行——DESIRED=<숫자>(占位符为数字)和 SKIPPED=<빠진 노드 이름>(占位符为被排除的节点名称)。

GPU 节点有两个,驱动 DaemonSet 却只启动一个。因为标签值不是 "true" 时,nodeSelector 就不匹配——写成 false 与彻底删除标签,对人来说读起来不同,但对 nodeSelector 来说完全一样。这就是表达“这个节点上已经装好了驱动”的办法。驱动是加载内核模块的 operand,所以不能与主机上已有的驱动重复。数值不要凭命令的输出来抄,请用 -o jsonpath 获取后再写。

同时启动设备插件与 GFD

再创建两个 DaemonSet。/root/gpuops/k8s/device-plugin-ds.yaml 中的 nvidia-device-plugin-daemonset 使用 nodeSelector nvidia.com/gpu.deploy.device-plugin: "true"、镜像 nvcr.io/nvidia/k8s-device-plugin:v0.16.2、内存请求 128Mi。/root/gpuops/k8s/gfd-ds.yaml 中的 gpu-feature-discovery 使用 nodeSelector nvidia.com/gpu.deploy.gpu-feature-discovery: "true",镜像和内存请求相同。两者的 Pod 标签和选择器都是 app: <데몬셋 이름>(占位符为 DaemonSet 名称)。应用之后,请确认两个 DaemonSet 的 desiredNumberScheduled 都是 2。

设备插件是向 kubelet 上报 nvidia.com/gpu 资源的 operand,GFD 是把该节点的 GPU 信息转换为标签并添加的 operand。二者出自同一个镜像,但做的事不同,所以 DaemonSet 是分开的,既然是分开的,标签也是分开的。在排除了驱动的节点上,这两个也必须启动——因为驱动已经在主机上了。请注意,也有名称不以 nvidia- 开头的 operand。

撤下标签,Pod 就消失

在 /root/gpuops/k8s/dcgm-ds.yaml 中写出 DaemonSet nvidia-dcgm-exporter——nodeSelector 为 nvidia.com/gpu.deploy.dcgm-exporter: "true",镜像为 nvcr.io/nvidia/k8s/dcgm-exporter:3.3.7-3.5.0-ubuntu22.04,内存请求为 128Mi,Pod 标签和选择器为 app: nvidia-dcgm-exporter。应用后确认它在两个节点上都启动了,然后把 lab-node-1 的 nvidia.com/gpu.deploy.dcgm-exporter 改为 false,并确认 Pod 消失。在 /root/gpuops/out/optout.txt 中写两行——BEFORE=<바꾸기 전 desiredNumberScheduled> 和 AFTER=<바꾼 뒤 값>(占位符依次为修改前的 desiredNumberScheduled 与修改后的值)。

DaemonSet 控制器会一直比对 nodeSelector 和节点标签。标签一旦不再符合条件,就会删除该节点上的 Pod——这就是没有修改 DaemonSet,Pod 却消失的原因。在运维中想只把一个节点从 operand 中排除时,用的旋钮正是这个。官方文档中还有一次性排除节点上所有 operand 的 nvidia.com/gpu.deploy.operands=false。两个数字必须分别在修改标签之前和之后获取——不能事后一次写出来。

判断该有的 operand 是否都有了的工具

创建 /root/gpuops/operand-audit.sh。遍历集群中的所有节点,对该节点上 nvidia.com/gpu.deploy.<이름>(占位符为名称)标签为 true 的每个 operand,检查该 DaemonSet 的 Pod 是否在该节点上处于 Running。如果没有,就输出一行 <노드> MISSING <오퍼랜드이름>(占位符依次为节点与 operand 名称);如果该节点上一个都不缺,就输出一行 <노드> OK(占位符为节点)。只要缺了一个,退出码就是 1,否则是 0。operand 名称与 DaemonSet 名称的对应,就是第 1 步中的五个。创建之后,对当前集群运行它,并把输出保存到 /root/gpuops/out/audit.txt。不要把节点名称写在脚本里——评分器会再多创建一个节点后再调用它。

这个判定不能依据“DaemonSet 想要几个”,而要依据“这个节点上是否真的在运行”。二者是不同的——确实会出现标签正确,但因为污点或资源而无法启动 Pod 的节点。如果对每个节点都调用 kubectl,节点一多就会变慢。节点列表和 Pod 列表各获取一次,再用 jq 过滤,API 调用两次就够了。用 jq 读取标签时如果使用 //,值为 false 的标签和不存在的标签就无法区分。

找出不合常理的组合

假设有人只在 lab-node-2 上手工添加了 nvidia.com/gpu.deploy.device-plugin=true。请真的添加这个标签(不添加其他 deploy 标签),并确认设备插件的 Pod 在该节点上启动了。然后创建 /root/gpuops/order-check.sh <노드이름>(占位符为节点名称)——如果该节点上设备插件的 Pod 处于 Running,但没有 Container Toolkit 的 Pod,就输出一行 device-plugin-without-toolkit 并以退出码 1 结束,其他情况输出一行 ok 并以 0 结束。依次对三个节点运行它,并在 /root/gpuops/out/order.txt 中按 <노드> <결과>(占位符依次为节点与结果)的形式写三行。

Container Toolkit 注册了运行时,GPU 容器才能看到设备。如果只有设备插件在运行,资源会被上报,但拿到这个资源的 Pod 抓不到设备——调度器说成功了,工作负载却失败,这是最难发现的组合。所以要看的不是“启动了什么”,而是“哪些东西一起启动了”。脚本只看作为参数收到的节点,判断依据是两个 DaemonSet 的 Pod 数量。总是输出 1 的脚本不行——评分器也会用正常的节点来调用它。

从实际状态中提取节点与 operand 的表

在 /root/gpuops/out/matrix.txt 中为每个节点写一行,共三行。格式是 <노드> driver=<yes|no> toolkit=<yes|no> device-plugin=<yes|no> gfd=<yes|no> dcgm=<yes|no>,其中 yes 表示该 operand 的 DaemonSet Pod 在该节点上处于 Running。行的顺序是 lab-node-0、lab-node-1、lab-node-2。然后在第四行写 TOTAL_PODS=<gpu-operator 네임스페이스에서 Running 인 데몬셋 파드 총수>(占位符为 gpu-operator 命名空间中处于 Running 的 DaemonSet Pod 总数)。数字和 yes/no 要向 API 查询后填写——凭前面步骤的记忆来写,就会对不上。

这张表就是“此刻这个集群的 GPU 栈是什么样子”。出了事故时,最先制作的就是这张表,而如果手头有生成这张表的命令,30 秒就能完成。只需 kubectl -n gpu-operator get pods -o json 一次,需要的内容就全在里面了——请一起查看 .spec.nodeName、.metadata.labels.app 和 .status.phase。由于第 7 步添加的标签,lab-node-2 上应该只有一个设备插件在运行。