LabHub
学习 学习路径 课程

GPU Operator 与时间片

复现 GPU 事故 — 从配置合并到滚动发布停住

在 LabHub 中继续学习

目标

用八个步骤重现一次曾令整个 GPU 集群无法使用的事故。亲手构造 containerd 配置合并的位置、检查实际加载内容的习惯、资源宣告与 runtime handler 两道关口、time-slicing 的算术,以及 rollout 停滞时集群呈现的状态。

为什么重要

GPU 运维中真正折磨人的并非驱动,而是相信配置已经生效的那一刻。即使 drop-in 文件中正确写有 nvidia runtime,实际加载的 runtime 也可能只有 runc;即使 toolkit Pod 为 Ready,若没有重启 containerd,handler 也不会注册;而已经运行的 Pod 即使面对这些全部失效,也不会发出任何提示。这正是故障潜伏到节点重启时才集中爆发的原因。因此,本实验不看“写了什么”,只看“实际加载了什么、实际调度了什么”。这一习惯就能把耗时数小时的根因排查缩短到 3 秒。

**此环境中没有真实 GPU、containerd 或 GPU Operator。**第 1~4 步使用配置文件和 TOML parser;第 5~8 步则在 kwok 启动的真实 control plane 和 3 个假节点(lab-node-0/1/2)上操作。调度、资源宣告与 DaemonSet rollout 由真实 controller 处理,因此能够如实复现;但容器不会真正运行,节点上的 GPU 也只是 node object 中的数字。

步骤

  1. 创建 /root/gpuop/etc/containerd/config.toml。顶层必须包含 version = 2imports 数组,且 imports 的一个条目必须以 conf.d/*.toml 结尾。在 plugins."io.containerd.grpc.v1.cri".containerd 下设置 default_runtime_name = "runc";在 plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc table 中加入 runtime_type = "io.containerd.runc.v2"options.SystemdCgroup = true
  2. 创建 /root/gpuop/etc/containerd/conf.d/99-nvidia.toml。在 plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia table 中设置 runtime_type = "io.containerd.runc.v2";在其下的 options 中加入 BinaryName = "/usr/bin/nvidia-container-runtime"SystemdCgroup = true
  3. tomllib 读取两个文件,将判定结果以五行写入 /root/gpuop/out/merge.txtMAIN_RUNTIMES= 是主配置直接定义的 runtime 名称;DROPIN_RUNTIMES= 是 drop-in 定义的名称;若双方定义同一个 table,则 CONFLICT=yesCONFLICT_TABLE= 写发生冲突的 table 路径(plugins."io.containerd.grpc.v1.cri".containerd.runtimes);最后一行是 LOADED_RUNTIMES=。只有最后一行不是计算值,而是观测值:事故当天在该节点上通过 containerd config dump 确认,实际加载的 runtime handler 只有 runc
  4. 创建 /root/gpuop/bin/check-runtime.sh。它必须不使用绝对路径调用 containerd config dump,在输出中查找 containerd.runtimes.nvidia;找到时以 0 退出,未找到时以 1 退出。不得读取 /etc/containerd/config.toml 路径,评分器会将其判定为错误答案。
  5. /root/gpuop/k8s/runtimeclass.yaml 中编写名称与 handler 均为 nvidia 的 RuntimeClass;在 /root/gpuop/k8s/gpu-pod.yaml 中编写 Pod:namespace 为 gpu-lab,名称为 cuda-probe,包含 runtimeClassName: nvidia,且 container resource 的 limits 中包含 nvidia.com/gpu: 1。先创建 namespace,再应用两个文件。Pod 不会启动;请将 PodScheduled condition 的 reasonmessage 以两行保存到 /root/gpuop/out/unscheduled.txt
  6. 使用 kubectl patch node lab-node-0 --subresource=status,在 capacityallocatable 两处都把 nvidia.com/gpu 设为 "4"。Pod 变为 Running 后,将三个节点的名称和宣告量以两列表格保存到 /root/gpuop/out/node-gpu.txt
  7. /root/gpuop/k8s/time-slicing.yaml 中编写 namespace 为 gpu-operator、名称为 time-slicing-config 的 ConfigMap。data.any 必须是 YAML 字符串;其中 sharing.timeSlicing.failRequestsGreaterThanOne: true,且 resources 数组第一项必须为 name: nvidia.com/gpureplicas: 5。应用后,将 lab-node-1 的宣告量提高到 "20",并在 /root/gpuop/out/slots.txt 中写七行:PHYSICAL_GPUSREPLICASADVERTISEDVRAM_PER_GPU_GIBVRAM_GUARANTEED_PER_SLOT_GIBVRAM_SHARED_PER_GPU_GIBISOLATION。设备按 4 张 40GiB A100 计算。
  8. /root/gpuop/k8s/toolkit-ds.yaml 中编写 DaemonSet:namespace 为 gpu-operator,名称为 nvidia-container-toolkitupdateStrategy.rollingUpdate.maxUnavailable: 1,镜像为 nvcr.io/nvidia/k8s/container-toolkit:v1.16.2,内存 request 为 128Mi,然后应用。三个 Pod 全部 Ready 后,将镜像升级到 v1.17.0,同时把内存 request 改为 64Gi。rollout 停滞后,在 /root/gpuop/out/rollout.txt 中写六行:DESIREDUPDATEDREADYOLD_IMAGE_PODSMAX_UNAVAILABLE,以及 BLOCKER=insufficient-memory

参考

重现事故节点的 containerd 主配置

创建 /root/gpuop/etc/containerd/config.toml。顶层必须包含 version = 2imports 数组,且 imports 的一个条目必须以 conf.d/*.toml 结尾。在 plugins."io.containerd.grpc.v1.cri".containerd 下设置 default_runtime_name = "runc";在 plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc table 中加入 runtime_type = "io.containerd.runc.v2"options.SystemdCgroup = true

TOML 不是靠缩进,而是靠 table 名称构造层级。含点号的键必须用双引号括起来,才能作为一个整体读取。写完后不要只靠目视检查,请用 parser 读取一次进行确认。

重现 toolkit 投放的 drop-in

创建 /root/gpuop/etc/containerd/conf.d/99-nvidia.toml。在 plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia table 中设置 runtime_type = "io.containerd.runc.v2";在其下的 options 中加入 BinaryName = "/usr/bin/nvidia-container-runtime"SystemdCgroup = true

drop-in 会在与主配置相同的 table 路径下再声明一个自己的 runtime。关键在 options 中的 binary 名称:这个 wrapper 会在创建容器前注入设备和 library。cgroup driver 不得与主配置不一致。

判断两个文件是否在同一 table 冲突

tomllib 读取两个文件,将判定结果以五行写入 /root/gpuop/out/merge.txtMAIN_RUNTIMES= 是主配置直接定义的 runtime 名称;DROPIN_RUNTIMES= 是 drop-in 定义的名称;若双方定义同一个 table,则 CONFLICT=yesCONFLICT_TABLE= 写发生冲突的 table 路径(plugins."io.containerd.grpc.v1.cri".containerd.runtimes);最后一行是 LOADED_RUNTIMES=。只有最后一行不是计算值,而是观测值:事故当天在该节点上通过 containerd config dump 确认,实际加载的 runtime handler 只有 runc

让 parser 而不是人工完成判定。读取主配置的 import 列表并展开 glob,分别收集双方 runtime table 中定义的名称。只有最后一行不是计算值,而是事故当天的观测值,该值已写在说明中。

检查实际加载配置的脚本

创建 /root/gpuop/bin/check-runtime.sh。它必须不使用绝对路径调用 containerd config dump,在输出中查找 containerd.runtimes.nvidia;找到时以 0 退出,未找到时以 1 退出。不得读取 /etc/containerd/config.toml 路径,评分器会将其判定为错误答案。

判定依据不是文件,而是运行中 daemon 持有的配置。调用命令时不要使用绝对路径;评分器会在前面放置假 daemon,并实际运行脚本两次。未找到目标时必须以非零状态退出,检查才真正有意义。

RuntimeClass、GPU Pod 与 Pending

/root/gpuop/k8s/runtimeclass.yaml 中编写名称与 handler 均为 nvidia 的 RuntimeClass;在 /root/gpuop/k8s/gpu-pod.yaml 中编写 Pod:namespace 为 gpu-lab,名称为 cuda-probe,包含 runtimeClassName: nvidia,且 container resource 的 limits 中包含 nvidia.com/gpu: 1。先创建 namespace,再应用两个文件。Pod 不会启动;请将 PodScheduled condition 的 reasonmessage 以两行保存到 /root/gpuop/out/unscheduled.txt

RuntimeClass 是 cluster-scoped 资源,因此没有 namespace;名称和 handler 是两个不同字段。Pod 无法启动是正常结果,请把 scheduling condition 中记录的原因保存到文件。条件生成可能需要几秒,不要固定 sleep,而应持续检查条件。

用 node status 重现 device plugin 的工作

使用 kubectl patch node lab-node-0 --subresource=status,在 capacityallocatable 两处都把 nvidia.com/gpu 设为 "4"。Pod 变为 Running 后,将三个节点的名称和宣告量以两列表格保存到 /root/gpuop/out/node-gpu.txt

extended resource 位于 node object 的 status 下,而 status 是独立 subresource,普通 patch 不会生效。capacity 和 allocatable 两处必须同时填写,值使用字符串。资源出现后,scheduler 会重新处理等待中的 Pod。

读取 time-slicing 配置并计算 slot 与内存

/root/gpuop/k8s/time-slicing.yaml 中编写 namespace 为 gpu-operator、名称为 time-slicing-config 的 ConfigMap。data.any 必须是 YAML 字符串;其中 sharing.timeSlicing.failRequestsGreaterThanOne: true,且 resources 数组第一项必须为 name: nvidia.com/gpureplicas: 5。应用后,将 lab-node-1 的宣告量提高到 "20",并在 /root/gpuop/out/slots.txt 中写七行:PHYSICAL_GPUSREPLICASADVERTISEDVRAM_PER_GPU_GIBVRAM_GUARANTEED_PER_SLOT_GIBVRAM_SHARED_PER_GPU_GIBISOLATION。设备按 4 张 40GiB A100 计算。

ConfigMap 的 data 值必须是字符串;若把配置写成 mapping,应用会被拒绝。请使用 block scalar。计算中的陷阱在内存:slot 数增加五倍,并不意味着内存被平均分为五份。

让 rollout 停滞并报告分裂状态

/root/gpuop/k8s/toolkit-ds.yaml 中编写 DaemonSet:namespace 为 gpu-operator,名称为 nvidia-container-toolkitupdateStrategy.rollingUpdate.maxUnavailable: 1,镜像为 nvcr.io/nvidia/k8s/container-toolkit:v1.16.2,内存 request 为 128Mi,然后应用。三个 Pod 全部 Ready 后,将镜像升级到 v1.17.0,同时把内存 request 改为 64Gi。rollout 停滞后,在 /root/gpuop/out/rollout.txt 中写六行:DESIREDUPDATEDREADYOLD_IMAGE_PODSMAX_UNAVAILABLE,以及 BLOCKER=insufficient-memory

先等待三个节点全部就绪,再推送新 spec,否则无法区分到底是什么导致停滞。节点容量为 32Gi,因此申请超过它的内存时,没有任何节点能够接收该 Pod。报告中的数字请直接从 DaemonSet status 读取。