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

GPU Operator 与时间片

DCGM Exporter 的指标,以及这些指标没告诉你的事

在 TT Lab 中继续学习

一句话总结

GPU 可观测性的原材料,是 DCGM Exporter 通过 /metrics 输出的纯文本,而这段文本里最重要的不是值,而是标签。标签把指标与显卡、主机和 Pod 连接起来,有了这种连接,才能用数字证明“分配全部发出去了,却没有人在用”这句话。

为什么需要它

GPU 是组织中最昂贵的资源,同时也是最看不见的资源。cpu 和 memory 由 kubelet 自己统计,用 kubectl top 就能直接看到,但对于 GPU,Kubernetes 并不知道利用率。 Kubernetes 只知道“给这个 Pod 分配了一块”,至于这一块是在 100% 地运转还是闲着,它并不关心。

于是出现了一种非常常见的状态。节点的 nvidia.com/gpu allocatable 已被全部耗尽,新任务一直是 Pending,而显卡的实际利用率却只有个位数。从调度器的角度看,这个集群是满的;从电力和折旧的角度看,它几乎在闲置。这两个数字存在于不同的系统中,如果没有人把它们并排放在一起看,这种状态会持续好几个月。

分配率来自 Kubernetes API,利用率来自 DCGM。 本模块要做的,就是把这两者放到同一个画面上。

工作原理

DCGM Exporter 在每个节点上以 DaemonSet 运行,通过 NVIDIA 的 DCGM 库从显卡读取数值,并以 Prometheus 暴露格式输出。官方文档中给出的真实输出是这样的。

# HELP DCGM_FI_DEV_GPU_TEMP GPU temperature (in C).
# TYPE DCGM_FI_DEV_GPU_TEMP gauge
DCGM_FI_DEV_GPU_TEMP{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 61
DCGM_FI_DEV_FB_FREE{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 35690
DCGM_FI_DEV_FB_USED{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 4845
DCGM_FI_DEV_XID_ERRORS{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 0
DCGM_FI_PROF_GR_ENGINE_ACTIVE{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 0.995630

这里要抓住三点。

第一,名称就是 DCGM 的字段名称。 以 DCGM_FI_DEV_ 开头的是设备级别的值(温度、显存、XID 错误、时钟),以 DCGM_FI_PROF_ 开头的是性能剖析值(图形引擎活跃比例、张量管线活跃比例、DRAM 活跃比例)。查看利用率时常用的 DCGM_FI_DEV_GPU_UTIL,更接近于“采样的瞬间内核是否在运行”,是一个比较粗糙的数字,而 DCGM_FI_PROF_GR_ENGINE_ACTIVE 则以 0 到 1 之间的比例给出该时间段内引擎实际有多活跃。让一块卡长时间闲着的任务,用前一个数字看起来很忙,用后一个数字看起来很闲。 做容量规划时,用哪个数字会改变结论。

第二,显存以两个指标给出。 它们是 DCGM_FI_DEV_FB_USED 和 DCGM_FI_DEV_FB_FREE(单位为 MiB)。不要另外去找总显存指标,通常是把两者相加得到总量。这两个指标之所以重要,是因为 GPU 故障中很大一部分表现为“利用率为 0,显存却被占着”。这是已死亡的进程没有释放上下文,这张卡虽然活着,却不会还给任何人。

第三,标签占了这段文本的一半。 gpu 是节点内的索引,UUID 是显卡的全局标识,device 是设备节点名称,Hostname 是节点。在此基础上,如果开启 exporter 的 Kubernetes 映射(环境变量 DCGM_EXPORTER_KUBERNETES,参数 -k),占用这张卡的 Pod 信息还会作为标签追加进来。有了这个映射,才能回答“谁占着这张卡”这个问题;没有它,指标只能讲卡的事,讲不了人的事。

开启 MIG 后,同一个指标会在显卡级别和 GPU 实例级别两边都出现,实例行上还会追加 GPU_I_PROFILE 和 GPU_I_ID 标签。所以在 MIG 集群中不假思索地对指标 sum(),就会把同一个值加两遍。 仪表板上的合计比物理块数还大的集群,几乎都是这个原因。

采集路径与告警

要让 Prometheus 来抓取指标,需要有目标定义。使用 Prometheus Operator 的地方,这个定义就是 ServiceMonitor 自定义资源。只要写明从哪个 Service 的哪个端口、以多长间隔、从哪个路径抓取,Operator 就会替你生成 Prometheus 的配置。重要的是,它是带有 schema 的 API 对象。 如果把 interval 写成 30 而不是 30s,API 服务器会因类型不匹配而拒绝。如果是配置文件,会被悄悄忽略的笔误,在应用时就被发现了。

适合设置告警的,不要因为值大就设,要选人需要做什么能够确定下来的情形。

告警 依据指标 为什么需要人来行动
发生 XID 错误 DCGM_FI_DEV_XID_ERRORS 这是硬件或驱动事件。需要拔下显卡或腾空节点
温度超过阈值 DCGM_FI_DEV_GPU_TEMP 是散热问题,放着不管,时钟会降低,性能会悄悄下降
无主的显存占用 DCGM_FI_DEV_FB_USED 与缺少 Pod 标签 已死亡的进程占着显卡。需要处理这个节点
已分配却在闲置 分配量(API)与 DCGM_FI_DEV_GPU_UTIL 之间的差距 这是成本问题。需要请任务的主人把卡还回来

最后一行是本模块的核心,是单一指标无法构成的告警。 一边必须来自 Kubernetes API,另一边必须来自 exporter。

在现场相遇的样子

第一,利用率仪表板第一次亮起的那天,组织会吃一惊。 平均利用率 15%,而队列却很长。原因通常不是技术,而是习惯。人们开着笔记本就下班了,一个任务整整占着一块卡,却用 CPU 做数据预处理。在把数字展示出来之前,没有人知道自己正在这样做。

第二,开启时间分片后,指标会让人糊涂。 time-slicing 只增加资源名称的数量,并不拆分显卡。所以使用五个槽位的节点的指标,仍然是只有一块卡的那一行。 想按 Pod 拆分出利用率,也没有这样的值。不知道这一点,想去制作“每槽位利用率”而浪费时间的事很常见。在时间分片节点上,要分别看卡级别的指标和 Pod 级别的分配,不能把两者相乘来解读。

第三,标签基数会悄悄成为问题。 每个指标都带有 UUID 和 Pod 名称,所以在有大量短命 Pod 的集群里,时间序列会不断新增。如果把带有 Pod 名称的时间序列原样放进长期保存的对象,存储会先垮掉。需要做分离:长期保存时压缩到卡级别,Pod 级别只短期保留。

第四,本环境诚实的局限。 实验用的 Pod 中既没有 GPU,也没有 DCGM 和 Prometheus。所以下一个实验要亲手制作暴露格式文本,把它作为原材料。这看起来像是模拟,但实际运维中很大一部分工作,恰恰就是这个——读取别人给的 /metrics 转储,解析它,核对规则所引用的名称是否真的出现。PromQL 本身没有 Prometheus 就无法运行,所以不运行,只判定规则文件的结构以及所引用指标名称的存在。

参考文档

下一项实验要做什么

亲手编写 DCGM Exporter 的暴露格式文本。针对四块卡写出五个指标并带齐标签,再制作把它解析成按卡的表格的工具。另外制作一个开启 Kubernetes 映射的版本,找出没有 Pod 标签却占着显存的卡,并从 API 数出同一个集群的分配量,与利用率并排放在一起。编写告警规则文件,并制作核对规则所引用的指标名称是否真的出现在暴露文本中的检查器。最后亲自定义 ServiceMonitor CRD 并放入 API 服务器,看看写错的 interval 是如何被拒绝的。