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

GPU Operator 与时间片

GPU 故障分类 — 从同一条消息里分出不同的原因

在 TT Lab 中继续学习

目标

在真实的调度器上造出让 GPU Pod 受阻的七种状态,并制作只看对象就能用一个词回答原因的分类器。

为什么重要

一份“GPU Pod 起不来”的报告,实际原因有六七种,需要调查的地方也各不相同。而且,资源未上报、allocatable 为 0、空位耗尽这三种情况,调度器给出的是完全相同的语句。所以光读消息是不够的,必须按 nodeSelector → 污点 → capacity → allocatable → 剩余空位 的顺序对照对象,才能把原因收敛到一个。顺序之所以重要,是因为前一步为真时,后一步根本没有可检查的对象——违反顺序,就会像删除好好的污点那样,只把集群弄坏。如果每次都由人来做这个判定,标准就会动摇,所以最后把它固化成工具。

步骤

  1. 创建工作目录 /root/gputri/cases、/root/gputri/out、/root/gputri/bin,并创建命名空间 gpu-triage。在 lab-node-0 的 status.capacity 和 status.allocatable 两处,都把 nvidia.com/gpu 设为 "2",并添加污点 nvidia.com/gpu=present:NoSchedule。在 /root/gputri/cases/case-ok.yaml 中写出 Pod case-ok——命名空间为 gpu-triage,nodeSelector 为 kubernetes.io/hostname: lab-node-0,带有能容忍该污点的容忍度,容器名称为 cuda,镜像为 nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04,limits 中为 nvidia.com/gpu: 1。应用后,等它变为 Running,就在 /root/gputri/out/01-ok.txt 中写两行——NODE= 和 PHASE=。
  2. lab-node-1 不上报任何东西(这是 device plugin 挂掉的节点)。在 /root/gputri/cases/case-nogpunode.yaml 中写出 Pod case-nogpunode——用 nodeSelector 固定到 lab-node-1,容忍度与第 1 步相同,并要求 nvidia.com/gpu: 1。应用后,确认它处于 Pending,并把 PodScheduled 条件的 reason 和 message,以 REASON= 和 MESSAGE= 两行的形式保存到 /root/gputri/out/02-nogpunode.txt。
  3. 在 lab-node-2 的 status.capacity 中把 nvidia.com/gpu 设为 "4",在 status.allocatable 中设为 "0"(驱动验证失败,节点无法提供 GPU 的状态)。在 /root/gputri/cases/case-alloc0.yaml 中写出 Pod case-alloc0——固定到 lab-node-2,其余与第 2 步相同。应用后,在 /root/gputri/out/03-alloc0.txt 中写三行——CAPACITY=、ALLOCATABLE=、MESSAGE=。请与前一步的消息作比较。
  4. 在 /root/gputri/cases/case-taint.yaml 中写出 Pod case-taint——固定到 lab-node-0,要求 nvidia.com/gpu: 1,但不加入容忍度。 其余与第 1 步的 Pod 相同。应用后,在 /root/gputri/out/04-taint.txt 中写两行——TAINT=nvidia.com/gpu=present:NoSchedule 和 MESSAGE=。请通过消息确认,资源明明还有,为什么却被拦住。
  5. 在 /root/gputri/cases/case-label.yaml 中写出 Pod case-label——把 nodeSelector 设为 nvidia.com/gpu.product: NVIDIA-A100-SXM4-40GB(没有节点带有这个标签),容忍度和 nvidia.com/gpu: 1 请求保持不变。应用后,在 /root/gputri/out/05-label.txt 中写两行——MATCHING_NODES= 中写实际带有该标签的节点数,MESSAGE= 中写条件消息。
  6. 在 /root/gputri/cases/case-exhaust.yaml 中写出 Pod case-exhaust——固定到 lab-node-0,加入容忍度,并要求 nvidia.com/gpu: 2。该节点上报了 2 块,但第 1 步的 case-ok 已经占住了一块。应用后,在 /root/gputri/out/06-exhaust.txt 中写三行——ALLOCATABLE=(该节点上报的数量)、ALLOCATED=(放置在该节点上的 Pod 的 GPU 请求之和)、REQUESTED=2。
  7. 先在 /root/gputri/cases/case-norequest.yaml 中写出 Pod case-norequest——固定到 lab-node-0,加入容忍度,但完全不写资源请求。 应用后它会启动。接着在 /root/gputri/cases/case-rtc.yaml 中写出带有 runtimeClassName: nvidia 的 Pod case-rtc 并应用,把包含标准错误的输出保存到 /root/gputri/out/07-runtimeclass.txt(不要创建 RuntimeClass)。最后创建命名空间 gpu-quota,在 /root/gputri/cases/quota.yaml 中写出 ResourceQuota gpu-quota,把 requests.nvidia.com/gpu 限制为 "1",然后应用 /root/gputri/cases/case-quota.yaml 中的 Pod case-quota(要求 3 块 GPU),把输出保存到 /root/gputri/out/07-quota.txt。最后在 /root/gputri/out/07-symptoms.txt 中写三行——NOREQUEST=、RUNTIMECLASS=、QUOTA=。后两行中,用 created 或 absent 来写 Pod 对象是否已经生成。
  8. 创建 /root/gputri/bin/triage.sh。用 bash triage.sh <네임스페이스> <파드>(占位符依次为命名空间与 Pod)调用时,只在标准输出中输出一个词就结束。答案有七种——no-request ok label-mismatch taint no-gpu-node allocatable-zero exhausted。如果 Pod 不存在,就在标准错误中输出提示,并以 1 结束。判定只依据 kubectl get -o json 给出的对象,顺序是 没有请求 → 已经放置 → nodeSelector → 污点 → capacity → allocatable → 剩余空位。创建之后,把它接到七个 Pod(case-ok case-nogpunode case-alloc0 case-taint case-label case-exhaust case-norequest)上全部运行一遍,并在 /root/gputri/out/triage.txt 中按 <파드이름> <원인>(占位符依次为 Pod 名称与原因)的形式写七行。

参考

搭建一个正常的 GPU 节点

创建工作目录 /root/gputri/cases、/root/gputri/out、/root/gputri/bin,并创建命名空间 gpu-triage。在 lab-node-0 的 status.capacity 和 status.allocatable 两处,都把 nvidia.com/gpu 设为 "2",并添加污点 nvidia.com/gpu=present:NoSchedule。在 /root/gputri/cases/case-ok.yaml 中写出 Pod case-ok——命名空间为 gpu-triage,nodeSelector 为 kubernetes.io/hostname: lab-node-0,带有能容忍该污点的容忍度,容器名称为 cuda,镜像为 nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04,limits 中为 nvidia.com/gpu: 1。应用后,等它变为 Running,就在 /root/gputri/out/01-ok.txt 中写两行——NODE= 和 PHASE=。

扩展资源是 kubelet 自己无法统计的。在真实集群中,由 device plugin 写入节点 status,而这里用 kubectl patch node <이름> --subresource=status --type=merge(占位符为节点名称)在同一个位置直接写入同样的值。只写 capacity,调度器就没有可用的空位——两处都要填。生产环境的 GPU 节点几乎总是带有污点,以免普通工作负载流入。所以 GPU Pod 会带上容忍度。

根本没有资源名称的节点

lab-node-1 不上报任何东西(这是 device plugin 挂掉的节点)。在 /root/gputri/cases/case-nogpunode.yaml 中写出 Pod case-nogpunode——用 nodeSelector 固定到 lab-node-1,容忍度与第 1 步相同,并要求 nvidia.com/gpu: 1。应用后,确认它处于 Pending,并把 PodScheduled 条件的 reason 和 message,以 REASON= 和 MESSAGE= 两行的形式保存到 /root/gputri/out/02-nogpunode.txt。

条件可以用 kubectl get pod <이름> -n <ns> -o jsonpath='{.status.conditions[0].reason}'(占位符依次为 Pod 名称与命名空间)取出。请留意消息中写了什么——在接下来的两步中,原因完全不同的 Pod 会给出同一条语句。没有在节点上写 nvidia.com/gpu,并不意味着“没有卡”,而是“在调度器看来没有”。

有 capacity 却无法提供的节点

在 lab-node-2 的 status.capacity 中把 nvidia.com/gpu 设为 "4",在 status.allocatable 中设为 "0"(驱动验证失败,节点无法提供 GPU 的状态)。在 /root/gputri/cases/case-alloc0.yaml 中写出 Pod case-alloc0——固定到 lab-node-2,其余与第 2 步相同。应用后,在 /root/gputri/out/03-alloc0.txt 中写三行——CAPACITY=、ALLOCATABLE=、MESSAGE=。请与前一步的消息作比较。

capacity 是“这个节点拥有的量”,allocatable 是“可以提供给调度器的量”。两者出现差别是正常行为(系统预留量就是这个差别),而在 GPU 上,这个差别确实会拉开到 0。两个值位于同一个 kubectl get node -o json 输出的不同列。把消息与第 2 步的文件并排放在一起看,就能一眼看出为什么需要这个实验。

只缺容忍度的 Pod

在 /root/gputri/cases/case-taint.yaml 中写出 Pod case-taint——固定到 lab-node-0,要求 nvidia.com/gpu: 1,但不加入容忍度。 其余与第 1 步的 Pod 相同。应用后,在 /root/gputri/out/04-taint.txt 中写两行——TAINT=nvidia.com/gpu=present:NoSchedule 和 MESSAGE=。请通过消息确认,资源明明还有,为什么却被拦住。

第 1 步中 case-ok 启动在同一个节点上,而这个 Pod 去不了。区别只有一个容忍度。这一步的消息与前两步不同——污点是调度器能准确说出原因的少数几个地方之一。节点上的污点可以用 kubectl get node lab-node-0 -o jsonpath='{.spec.taints}' 查看。

根本没有候选节点的 Pod

在 /root/gputri/cases/case-label.yaml 中写出 Pod case-label——把 nodeSelector 设为 nvidia.com/gpu.product: NVIDIA-A100-SXM4-40GB(没有节点带有这个标签),容忍度和 nvidia.com/gpu: 1 请求保持不变。应用后,在 /root/gputri/out/05-label.txt 中写两行——MATCHING_NODES= 中写实际带有该标签的节点数,MESSAGE= 中写条件消息。

把 GPU Feature Discovery 添加的标签原样挑出来用的清单,如果集群里没有这个标签,就会变成这样。这个 Pod 的污点和资源都没有可检查的对象——因为候选节点是 0 个。所以在判定顺序中,nodeSelector 排在最前面。带有该标签的节点数可以用 kubectl get nodes -l <키> --no-headers | wc -l(占位符为标签键)来数。

上报完好,却没有空位

在 /root/gputri/cases/case-exhaust.yaml 中写出 Pod case-exhaust——固定到 lab-node-0,加入容忍度,并要求 nvidia.com/gpu: 2。该节点上报了 2 块,但第 1 步的 case-ok 已经占住了一块。应用后,在 /root/gputri/out/06-exhaust.txt 中写三行——ALLOCATABLE=(该节点上报的数量)、ALLOCATED=(放置在该节点上的 Pod 的 GPU 请求之和)、REQUESTED=2。

唯独这个判定,光靠一个节点对象是完不成的。必须把放置在该节点上的 Pod 收集起来,把请求加在一起——kubectl get pods -A --field-selector spec.nodeName=lab-node-0 -o json 是出发点。已结束的 Pod(Succeeded、Failed)并不占着空位,所以必须从合计中去掉。消息与第 2、3 步又相同——这是第三次看到同一条语句。

完全不会创建 Pod 的两种情况

先在 /root/gputri/cases/case-norequest.yaml 中写出 Pod case-norequest——固定到 lab-node-0,加入容忍度,但完全不写资源请求。 应用后它会启动。接着在 /root/gputri/cases/case-rtc.yaml 中写出带有 runtimeClassName: nvidia 的 Pod case-rtc 并应用,把包含标准错误的输出保存到 /root/gputri/out/07-runtimeclass.txt(不要创建 RuntimeClass)。最后创建命名空间 gpu-quota,在 /root/gputri/cases/quota.yaml 中写出 ResourceQuota gpu-quota,把 requests.nvidia.com/gpu 限制为 "1",然后应用 /root/gputri/cases/case-quota.yaml 中的 Pod case-quota(要求 3 块 GPU),把输出保存到 /root/gputri/out/07-quota.txt。最后在 /root/gputri/out/07-symptoms.txt 中写三行——NOREQUEST=、RUNTIMECLASS=、QUOTA=。后两行中,用 created 或 absent 来写 Pod 对象是否已经生成。

这一步的三种情况,与前五种的症状分支不同。第一种是 Pod 好好地启动了,但设备挂不上;后两种则是 Pod 对象根本不会生成。在准入时被拒绝的话,集群里没有任何痕迹,错误只会留在应用者的终端上——所以用 2>&1 把输出存进文件的习惯,对调查具有决定性作用。Pod 不存在,可以用 kubectl get pod <이름> -n <ns>(占位符依次为 Pod 名称与命名空间)的退出码来确认。

只看对象就能用一个词回答原因的分类器

创建 /root/gputri/bin/triage.sh。用 bash triage.sh <네임스페이스> <파드>(占位符依次为命名空间与 Pod)调用时,只在标准输出中输出一个词就结束。答案有七种——no-request ok label-mismatch taint no-gpu-node allocatable-zero exhausted。如果 Pod 不存在,就在标准错误中输出提示,并以 1 结束。判定只依据 kubectl get -o json 给出的对象,顺序是 没有请求 → 已经放置 → nodeSelector → 污点 → capacity → allocatable → 剩余空位。创建之后,把它接到七个 Pod(case-ok case-nogpunode case-alloc0 case-taint case-label case-exhaust case-norequest)上全部运行一遍,并在 /root/gputri/out/triage.txt 中按 <파드이름> <원인>(占位符依次为 Pod 名称与原因)的形式写七行。

不能按 Pod 名称来决定答案——被接到第一次见到的 Pod 上时也能答对,才有用处。需要的输入有三种:这个 Pod、全部节点,以及所有命名空间的 Pod(用于计算空位)。判定容忍度时,必须同时处理 operator: Exists 和 Equal 两种情况,而且 effect 为空时,会容忍所有 effect。扩展资源只看 limits 就可以——这是因为有“requests 与 limits 必须相同”这条规则。不要费力用 shell 去解析 JSON,交给 python3 会短得多。