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

GPU Operator 与时间片

GPU 指标 — 写出暴露格式、解析它、接到告警上

在 TT Lab 中继续学习

目标

亲手编写 DCGM Exporter 的暴露格式,把它解析成按卡的表格,把分配率和利用率并排放在一起,并制作告警规则和采集目标对象。

为什么重要

GPU 是组织中最昂贵的资源,而 Kubernetes 并不知道它的利用率。它只知道“给这个 Pod 分配了一块”。所以在调度器眼里是满的集群,从电费角度看几乎在闲置,这种状态会持续好几个月。要把这个差距变成数字,就必须把 Kubernetes API 的分配量和 DCGM 的利用率放在同一个画面上,而要做到这一点,首先要知道指标的实际样子。指标中重要的不是值,而是标签——标签把指标与显卡、节点和 Pod 连接起来,而在这种连接断开的地方,才会暴露出“无主占用显存的卡”这样的故障。

步骤

  1. 创建 /root/gpumet/metrics、/root/gpumet/bin、/root/gpumet/out、/root/gpumet/rules、/root/gpumet/k8s,并在 /root/gpumet/metrics/dcgm.prom 中写出插有 4 块 A100 的节点的 /metrics 输出。指标有五个——DCGM_FI_DEV_GPU_UTIL DCGM_FI_DEV_FB_USED DCGM_FI_DEV_FB_FREE DCGM_FI_DEV_GPU_TEMP DCGM_FI_DEV_XID_ERRORS。每个指标前面放一行 # HELP 和一行 # TYPE <이름> gauge(占位符为指标名称),然后接着写出四块卡的样本。样本的标签有四个——gpu(0 到 3)、UUID,取值为 GPU-a1b2c3d4-0000-0000-0000-00000000000<번호>(占位符为 GPU 编号)、device,取值为 nvidia<번호>(占位符为 GPU 编号)、Hostname(lab-node-0)。数值为:gpu 0 的利用率 97、FB_USED 38000、FB_FREE 2760、温度 71、XID 0;gpu 1 为 2、9000、31760、41、2;gpu 2 为 0、0、40760、33、0;gpu 3 为 1、21000、19760、40、0。
  2. 创建 /root/gpumet/bin/parse-metrics.sh <노출파일>(占位符为暴露文件)。每块卡输出一行 <gpu> <util> <fb_used> <fb_total> <mem_pct>,五列用空格分隔。fb_total 是 FB_USED 与 FB_FREE 之和,mem_pct 是 fb_used * 100 / fb_total 的向下取整的整数。行必须按 gpu 编号升序,并且只能读取作为参数收到的文件(不要把路径写死在里面)。创建之后,把它接到第 1 步的文件上,并把输出保存到 /root/gpumet/out/per-gpu.txt。
  3. 在 /root/gpumet/metrics/dcgm-k8s.prom 中写出开启了 Kubernetes 映射的版本——与第 1 步相同的五个指标、四块卡,只给 gpu 0 和 gpu 3 再加三个标签(namespace="gpu-metrics",gpu 0 加 pod="train-a" 和 container="trainer",gpu 3 加 pod="infer-b" 和 container="server")。gpu 1 和 gpu 2 没有 Pod 标签。然后创建 /root/gpumet/bin/find-orphan.sh <노출파일>(占位符为暴露文件)——把没有 Pod 标签、而 FB_USED 超过 1024 的卡的 gpu 编号,按升序每行输出一个。请接到这个新文件而不是第 1 步的文件上,并把输出保存到 /root/gpumet/out/orphan.txt。
  4. 在 lab-node-0 的 status.capacity 和 status.allocatable 两处,把 nvidia.com/gpu 设为 "4",创建命名空间 gpu-metrics 之后,在 /root/gpumet/k8s/workloads.yaml 中写出三个 Pod(train-a infer-b idle-c)——全部固定到 lab-node-0,各自要求 nvidia.com/gpu: 1。应用后,三个都启动了,就在 /root/gpumet/out/gap.txt 中写五行——PHYSICAL=(节点上报的块数)、ALLOCATED=(Pod 提出要求而发出去的块数)、ALLOC_PCT=(二者的比率,向下取整的整数)、MEAN_UTIL=(第 3 步暴露文件中四块卡的平均利用率,向下取整的整数)、IDLE_BUT_ALLOCATED=(带有 Pod 标签但利用率低于 10 的卡的数量)。
  5. 在 /root/gpumet/rules/gpu-alerts.yaml 中写出 Prometheus 规则文件。最顶层有 groups,一个组的 name 为 gpu,其中规则恰好三条——GpuXidError(表达式使用 DCGM_FI_DEV_XID_ERRORS,for: 0m,severity: critical)、GpuTempHigh(表达式使用 DCGM_FI_DEV_GPU_TEMP,for: 10m,severity: warning)、GpuAllocatedButIdle(表达式使用 DCGM_FI_DEV_GPU_UTIL,for: 2h,severity: info)。每条规则都要齐备 alert expr for labels.severity annotations.summary 五项。summary 要写成包含“应该做什么”的一句话。
  6. 创建 /root/gpumet/bin/check-rules.sh <규칙파일> <노출파일>(占位符依次为规则文件与暴露文件)。从规则文件的所有 expr 中提取以 DCGM_FI_ 开头的指标名称,如果暴露文件中一个该名称的样本都没有,就每个输出一行 MISSING=<이름>(占位符为名称),并以 1 结束。如果全部都有,就输出 OK=<노출에 있는 지표 수>(占位符为暴露中的指标数量)并以 0 结束。创建之后,把它接到第 5 步的规则和第 1 步的暴露文件上,并把输出保存到 /root/gpumet/out/rulecheck.txt。
  7. 在 /root/gpumet/k8s/servicemonitor-crd.yaml 中写出 servicemonitors.monitoring.coreos.com CRD 并应用——组为 monitoring.coreos.com,版本为 v1,作用域为命名空间,种类为 ServiceMonitor。schema 为 spec.selector.matchLabels(字符串映射)、spec.endpoints(数组,minItems: 1,每项 port 为必填字符串,path 为字符串,interval 为带有 ^[0-9]+(ms|s|m|h)$ 模式的字符串),并且 spec 中 selector 和 endpoints 都是必填。接着在 /root/gpumet/k8s/servicemonitor.yaml 中写出 gpu-metrics 命名空间的 nvidia-dcgm-exporter 并应用(选择器 app: nvidia-dcgm-exporter,一个端点,port: gpu-metrics,path: /metrics,interval: 15s)。最后在 /root/gpumet/k8s/servicemonitor-bad.yaml 中以名称 dcgm-bad-interval 写出同样的内容,但把 interval 写成不带引号的 30,应用它,并把包含标准错误的拒绝输出保存到 /root/gpumet/out/07-reject.txt。
  8. 创建 /root/gpumet/bin/gpu-report.sh <노출파일> <규칙파일>(占位符依次为暴露文件与规则文件)。按顺序输出六行——GPUS=(暴露中出现的卡数)、MEAN_UTIL=(平均利用率,向下取整)、MAX_TEMP=(最高温度)、ORPHAN_GPUS=(没有 Pod 标签而 FB_USED 超过 1024 的卡数)、XID_GPUS=(XID 错误大于 0 的卡数)、ALERT_RULES=(规则文件的规则总数)。所有数字都必须从作为参数收到的两个文件中计算得出。把它接到第 3 步的暴露文件和第 5 步的规则文件上,并把输出保存到 /root/gpumet/out/report.txt。

参考

亲手编写 DCGM Exporter 的暴露格式

创建 /root/gpumet/metrics /root/gpumet/bin /root/gpumet/out /root/gpumet/rules /root/gpumet/k8s,并在 /root/gpumet/metrics/dcgm.prom 中写出插有 4 块 A100 的节点的 /metrics 输出。指标有五个——DCGM_FI_DEV_GPU_UTIL DCGM_FI_DEV_FB_USED DCGM_FI_DEV_FB_FREE DCGM_FI_DEV_GPU_TEMP DCGM_FI_DEV_XID_ERRORS。每个指标前面放一行 # HELP 和一行 # TYPE <이름> gauge(占位符为指标名称),然后接着写出四块卡的样本。样本的标签有四个——gpu(0 到 3)、UUID,取值为 GPU-a1b2c3d4-0000-0000-0000-00000000000<번호>(占位符为 GPU 编号)、device,取值为 nvidia<번호>(占位符为 GPU 编号)、Hostname(lab-node-0)。数值为:gpu 0 的利用率 97、FB_USED 38000、FB_FREE 2760、温度 71、XID 0;gpu 1 为 2、9000、31760、41、2;gpu 2 为 0、0、40760、33、0;gpu 3 为 1、21000、19760、40、0。

Prometheus 暴露格式一行是 이름{라벨="값",…} 숫자(占位符依次为指标名称、标签名称与标签值)。两行注释(# HELP、# TYPE)每个指标只出现一次,其后接着多行只有标签不同的样本。请注意没有单独的总显存指标——必须把 FB_USED 与 FB_FREE 相加才能得到总量,在这张表里四块卡的和都相同。不必手工输入 20 行,用 shell 循环生成也可以。

把暴露文本转换成按卡的表格

创建 /root/gpumet/bin/parse-metrics.sh <노출파일>(占位符为暴露文件)。每块卡输出一行 <gpu> <util> <fb_used> <fb_total> <mem_pct>,五列用空格分隔。fb_total 是 FB_USED 与 FB_FREE 之和,mem_pct 是 fb_used * 100 / fb_total 的向下取整的整数。行必须按 gpu 编号升序,并且只能读取作为参数收到的文件(不要把路径写死在里面)。创建之后,把它接到第 1 步的文件上,并把输出保存到 /root/gpumet/out/per-gpu.txt。

从标签字符串中提取 gpu="…" 来区分卡,再按指标名称收集值。用一个正则表达式,就能把 이름{라벨} 값(占位符依次为指标名称、标签与值)拆成三段。向下取整的整数用 Python 的 // 直接得到——四舍五入的话,评分器会发现。评分器也会把这个脚本接到数值不同的文件上试一试。所以不能把第 1 步文件中的数字写死当作答案。

添加 Pod 标签,找出无主占用

在 /root/gpumet/metrics/dcgm-k8s.prom 中写出开启了 Kubernetes 映射的版本——与第 1 步相同的五个指标、四块卡,只给 gpu 0 和 gpu 3 再加三个标签(namespace="gpu-metrics",gpu 0 加 pod="train-a" 和 container="trainer",gpu 3 加 pod="infer-b" 和 container="server")。gpu 1 和 gpu 2 没有 Pod 标签。然后创建 /root/gpumet/bin/find-orphan.sh <노출파일>(占位符为暴露文件)——把没有 Pod 标签、而 FB_USED 超过 1024 的卡的 gpu 编号,按升序每行输出一个。请接到这个新文件而不是第 1 步的文件上,并把输出保存到 /root/gpumet/out/orphan.txt。

已死亡的进程如果没有释放上下文,显存会被占着,却没有 Pod 占着这张卡。Kubernetes 认为这张卡是空的,就会派来新的 Pod,而这个 Pod 会因显存不足而失败。判定所用的信号有两个——pod 标签的有无,以及 FB_USED 的值。请注意不要让带标签的行和不带标签的行混在同一张卡里。本实验中是以卡为单位保持一致的。评分器也会接到数值不同的文件上试一试。

把分配率和利用率放到同一个画面上

在 lab-node-0 的 status.capacity 和 status.allocatable 两处,把 nvidia.com/gpu 设为 "4",创建命名空间 gpu-metrics 之后,在 /root/gpumet/k8s/workloads.yaml 中写出三个 Pod(train-a infer-b idle-c)——全部固定到 lab-node-0,各自要求 nvidia.com/gpu: 1。应用后,三个都启动了,就在 /root/gpumet/out/gap.txt 中写五行——PHYSICAL=(节点上报的块数)、ALLOCATED=(Pod 提出要求而发出去的块数)、ALLOC_PCT=(二者的比率,向下取整的整数)、MEAN_UTIL=(第 3 步暴露文件中四块卡的平均利用率,向下取整的整数)、IDLE_BUT_ALLOCATED=(带有 Pod 标签但利用率低于 10 的卡的数量)。

这一步的要点是,这两个数字来自不同的系统。 前三行来自 Kubernetes API,后两行来自暴露文件。任何一边单独都无法说出“在昂贵地闲置”。分配量用 kubectl get pods -n <ns> -o json(占位符为命名空间)的 limits 相加来数。平均值不要四舍五入,请用向下取整来计算。

只为需要人行动的情形设置告警

在 /root/gpumet/rules/gpu-alerts.yaml 中写出 Prometheus 规则文件。最顶层有 groups,一个组的 name 为 gpu,其中规则恰好三条——GpuXidError(表达式使用 DCGM_FI_DEV_XID_ERRORS,for: 0m,severity: critical)、GpuTempHigh(表达式使用 DCGM_FI_DEV_GPU_TEMP,for: 10m,severity: warning)、GpuAllocatedButIdle(表达式使用 DCGM_FI_DEV_GPU_UTIL,for: 2h,severity: info)。每条规则都要齐备 alert expr for labels.severity annotations.summary 五项。summary 要写成包含“应该做什么”的一句话。

告警不是因为值大才设置,而是在人需要做什么能够确定下来时才设置。XID 是硬件事件,需要腾空节点;温度是散热问题;闲置的分配是成本问题,需要联系任务的主人。三条规则的 for 不同,也是这个原因——XID 一次就立刻触发,利用率要观察很久之后才触发。这个文件不会应用到集群中。它是下一步检查器要读取的原材料。

核对规则所调用的指标是否真的出现

创建 /root/gpumet/bin/check-rules.sh <규칙파일> <노출파일>(占位符依次为规则文件与暴露文件)。从规则文件的所有 expr 中提取以 DCGM_FI_ 开头的指标名称,如果暴露文件中一个该名称的样本都没有,就每个输出一行 MISSING=<이름>(占位符为名称),并以 1 结束。如果全部都有,就输出 OK=<노출에 있는 지표 수>(占位符为暴露中的指标数量)并以 0 结束。创建之后,把它接到第 5 步的规则和第 1 步的暴露文件上,并把输出保存到 /root/gpumet/out/rulecheck.txt。

告警规则中的指标名称哪怕错一个字符,这条告警就永远不会响。 因为 Prometheus 不会把不存在的指标当作错误,而是当作空结果。所以这个核对值得在部署之前设置。像 DCGM_FI_DEV_GPU_UTILIZATION 这样看似合理的笔误,实际上经常出现。暴露文件中的名称,从以 이름{(占位符为名称)开头的行中提取即可,不能把注释行里的名称也算进去。评分器也会用故意写错的规则文件来试一试。

把采集目标做成有 schema 的对象

在 /root/gpumet/k8s/servicemonitor-crd.yaml 中写出 servicemonitors.monitoring.coreos.com CRD 并应用——组为 monitoring.coreos.com,版本为 v1,作用域为命名空间,种类为 ServiceMonitor。schema 为 spec.selector.matchLabels(字符串映射)、spec.endpoints(数组,minItems: 1,每项 port 为必填字符串,path 为字符串,interval 为带有 ^[0-9]+(ms|s|m|h)$ 模式的字符串),并且 spec 中 selector 和 endpoints 都是必填。接着在 /root/gpumet/k8s/servicemonitor.yaml 中写出 gpu-metrics 命名空间的 nvidia-dcgm-exporter 并应用(选择器 app: nvidia-dcgm-exporter,一个端点,port: gpu-metrics,path: /metrics,interval: 15s)。最后在 /root/gpumet/k8s/servicemonitor-bad.yaml 中以名称 dcgm-bad-interval 写出同样的内容,但把 interval 写成不带引号的 30,应用它,并把包含标准错误的拒绝输出保存到 /root/gpumet/out/07-reject.txt。

这就是用 CRD 来处理的好处——如果是配置文件,会被悄悄忽略的笔误,在应用时就被发现了。在结构化 schema 中,minItems 是数组长度的下限,pattern 是对字符串的正则约束。在 YAML 中,不带引号的 30 会被读成整数,而 "30s" 会被读成字符串——这个差别就是这一步的全部。应用 CRD 之后,API 服务器接受新类型需要一两次往返的时间。

计算出六行的 GPU 现状报告

创建 /root/gpumet/bin/gpu-report.sh <노출파일> <규칙파일>(占位符依次为暴露文件与规则文件)。按顺序输出六行——GPUS=(暴露中出现的卡数)、MEAN_UTIL=(平均利用率,向下取整)、MAX_TEMP=(最高温度)、ORPHAN_GPUS=(没有 Pod 标签而 FB_USED 超过 1024 的卡数)、XID_GPUS=(XID 错误大于 0 的卡数)、ALERT_RULES=(规则文件的规则总数)。所有数字都必须从作为参数收到的两个文件中计算得出。把它接到第 3 步的暴露文件和第 5 步的规则文件上,并把输出保存到 /root/gpumet/out/report.txt。

前面步骤中创建的两个脚本已经完成了一半——这里把那些计算汇集到一个工具里。卡数是 gpu 标签的不同取值的个数,而不是样本行数。平均值不要四舍五入,请用向下取整。评分器也会把这个脚本接到另一个暴露文件和另一个规则文件上试一试——所以把数字写死就会失败。带 Pod 标签的卡和不带 Pod 标签的卡是混在一起的,所以标签的有无要以卡为单位汇总后判定。