从云原生走向 AI 原生:一套面向未来的架构方法论 → 阅读《AI 原生基础设施》

GPU 集群监控

已完成

本章将讨论以下几个问题:

  • 为什么 nvidia-smi、Kubernetes 指标和实际 GPU 使用量会对同一块硬件讲出三个不同的故事
  • 为什么 GPU 利用率百分比常常误导人,让人把“忙碌”误以为“高效”
  • 为什么僵尸进程会在 Pod 被删除后仍然占据 GPU 内存
  • 如何使用 DCGM 弥合“你以为正在发生的事情”和“实际正在发生的事情”之间的差距
  • 哪些模式可以帮助你在生产工作负载崩溃前预测 GPU 故障
  • 为什么许多团队会把 60% 到 70% 的 GPU 预算浪费在闲置或未充分利用的资源上

GPU 集群监控之所以困难,不在于指标太少,而在于指标太多却彼此割裂。表面上看,一切都可能是健康的:节点在线,Pod 正在运行,显卡温度正常,nvidia-smi 甚至还报告出一个相当可观的利用率数字。但与此同时,训练任务可能已经两个小时没有推进,推理服务在持续返回错误,调度器后面还堆着几十个 Pending 的 Pod,而这些异常并不会自然地出现在同一个仪表板上。GPU 监控的核心挑战,正是如何在这些彼此不一致的信号之间建立可解释的联系。

先看一个典型的场景:

$ kubectl get pods
NAME              READY   STATUS    RESTARTS   AGE
training-job-42   1/1     Running   0          3h
inference-svc-1   1/1     Running   0          5h
inference-svc-2   1/1     Running   0          5h

$ nvidia-smi
GPU 0: Tesla T4, 15360MiB, 42°C
 2156MiB / 15360MiB used
 GPU-Util: 78%

如果只看这两段输出,几乎所有人都会得出“系统正常,只是应用自身有问题”的结论。但真正令人头痛的地方在于,这类判断往往恰恰是错的。GPU 世界里最危险的情况之一,就是所有基础监控都显示绿色,而真正的容量冲突、调度拥塞或运行时泄漏却藏在这些“看起来正常”的数字背后。

GPU 现实的三种视图

CPU 监控之所以相对直接,是因为操作系统内核天然掌握 CPU 调度、内存分配和进程运行的全貌。GPU 并不是这样。对同一块显卡,至少存在三种彼此脱节的现实视图:硬件视图、Kubernetes 资源视图以及进程或容器级的实际使用视图。理解 GPU 集群监控,首先必须接受这三层现实并不自动一致。

第一层是 nvidia-smi 呈现的物理现实,也就是绝大多数人排障时第一眼看到的东西:

$ nvidia-smi
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.104.12              Driver Version: 535.104.12     CUDA Version: 12.4   |
+-------------------------------+----------------------+----------------------+------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |            |
| Fan  Temp  Perf  Pwr:Usage/Cap| Memory-Usage         | GPU-Util  Compute M. | MIG M.     |
|===============================+======================+======================+============|
| 0  Tesla T4              Off  | 00000000:00:04.0 Off| 0                    | N/A N/A    |
| N/A  42C    P0    28W / 70W  | 8234MiB / 15360MiB  | 78%      Default     | N/A        |
+-------------------------------+----------------------+----------------------+------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
| GPU   GI   CI    PID   Type   Process name                      GPU Memory Usage        |
|=========================================================================================|
| 0    N/A  N/A   2847    C     python train.py                  4234MiB                 |
| 0    N/A  N/A   3923    C     python inference.py              2896MiB                 |
| 0    N/A  N/A   4102    C     python notebook.py               1104MiB                 |
+-----------------------------------------------------------------------------------------+

这份输出显得信息丰富,却并不意味着它足以解释系统当前的行为。最典型的误导来自其中的 GPU 利用率。很多人看到 GPU-Util: 78%,会自然理解为“GPU 完成了 78% 负载的有效工作”;但这个数字真正表达的并不是完成了多少计算,而是“在采样窗口里,GPU 是否至少执行过某个核函数”。这两者之间差异极大。

为什么 GPU 利用率会撒谎

下面这个看似荒谬的程序,正好能说明问题:

import cupy as cp
import time

while True:
    # 创建一个只有一个元素的微小数组
    x = cp.array([1.0])
    # 执行一个微不足道的 GPU 操作:给单个元素加 1
    y = x + 1
    # 这会启动一个约 1 微秒的 GPU 核函数
    # CPU 线程休眠 999 微秒
    # GPU 在这段时间内闲置
    time.sleep(0.000999)
代码片段:terrible_gpu_code.py

这个程序每轮循环只让 GPU 做一件微不足道的事:给一个单元素数组加一。核函数大约只运行 $1\mu s$,随后 CPU 休眠约 $999\mu s$。换言之,在每个毫秒周期中,GPU 真正工作的时间只有千分之一,理论上的实际利用率只有 0.1%。然而,当你运行它时,nvidia-smi 却可能报告一个极高的利用率:

$ python terrible_gpu_code.py &
[1] 2847

$ nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader
87

# 87% 的利用率!
# 但我们几乎什么都没做

这并不是驱动“坏了”,而是 nvidia-smi 统计口径与人们的直觉完全不同。它并不衡量 GPU 做了多少有效工作,而是在固定采样窗口里回答一个二元问题:这段时间里是否存在任何核函数执行。要看懂这件事,必须进一步理解 nvidia-smi 的采样方式。

nvidia-smi 实际如何测量

GPU 驱动并不会持续、逐周期地跟踪每一个核函数到底占用了多少时间。更准确地说,它会以固定时间窗口做采样,通常大约每 $1/6$ 秒,即约 166ms 询问一次状态。每次采样时,它只判断在这个窗口内有没有检测到核函数活动:

时间:0ms            166ms           332ms
核函数:[50μs 核函数]----等待----[2μs]----等待----[100μs]----等待----
采样: <-------- 采样 1--------> <-------- 采样 2-------->
结果: "检测到活动"              "检测到活动"

由此就会出现一个非常反直觉的结果:

  • 采样 1 中只要有一个 50 微秒的核函数运行,整个 166ms 窗口就会被记作“活跃”
  • 采样 2 中哪怕只出现一个 100 微秒的核函数,整个窗口同样会被记作“活跃”
  • 最终 nvidia-smi 会给出 100% 利用率,因为两个采样窗口都检测到了活动

但如果从真实执行时间来算:

  • 核函数总运行时间:50μs + 2μs + 100μs = 152μs
  • 总经过时间:332ms
  • 实际利用率:0.152ms / 332ms = 0.046%

这就是 GPU 监控中第一个必须牢记的结论:nvidia-smi 的 GPU 利用率更像“活跃窗口比例”,而不是“有效计算占比”。因此,一个持续提交大量极短核函数的程序可以显示接近 100% 的利用率,同时实际上几乎没有完成任何有意义的工作。GPU 在统计口径上很忙,却没有产出。

三层内存

显存监控同样不能按表面数字理解。对许多开发者而言,显存似乎只有“已用”和“未用”两种状态;但实际运行时,至少存在三层不同含义的内存状态:

  1. 空闲内存:没有被任何进程占用
  2. 预留内存:被 CUDA 或框架的内存池拿走,但尚未被真实张量使用
  3. 已分配内存:真正被数据对象占据的显存

以下示例展示了这一点:

import cupy as cp

# 在任何 GPU 操作之前
# GPU 总共有 16GB,全部空闲

array1 = cp.zeros((512, 512, 512), dtype=cp.float32) # 512MB
# CUDA 预留了一个 2GB 的池(默认行为)
# 但只为实际数据分配了 512MB
代码片段:memory.py

这时如果仅用 nvidia-smi 检查:

$ nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader
512 MiB, 15848 MiB   # Shows only allocated, not reserved

你会误以为系统还几乎完整地空着。但如果随后立刻尝试再分配 15GB,大概率会失败,因为 CUDA 已经先行预留了一部分显存给内存池。也就是说,决定后续分配是否成功的,并不只是“当前真正被张量占用的显存”,还包括运行库为了性能而预抓取的那部分内存。真正可用的容量可能是:

  • 总显存:16GB
  • CUDA 预留:2GB
  • 剩余可用:14GB
  • 下一次申请:15GB
  • 结果:失败

遗憾的是,nvidia-smi 并不会直接把这种“框架已预留但尚未真正存放业务数据”的层次显式呈现出来。要看懂这一层,只能回到进程内部去读内存池指标:

pool = cp.get_default_memory_pool()

print(f"Allocated: {pool.used_bytes() / 1e9:.2f} GB")   # 0.51 GB
print(f"Reserved:  {pool.total_bytes() / 1e9:.2f} GB")  # 2.00 GB
代码片段:memory.py

从监控角度说,这意味着单看节点级显存占用永远不够。你还必须知道框架内部的内存池策略,否则就无法分辨“真的快用满了”与“只是框架为后续性能预留了一块空间”之间的差别。

僵尸进程问题

在生产环境中,比显存池更令人烦躁的是僵尸进程。最典型的场景如下:

$ kubectl delete pod training-job-42
pod "training-job-42" deleted

$ nvidia-smi |
No running processes found

# 但是...
$ nvidia-smi --query-gpu=memory.used --format=csv,noheader
4234 MiB

# 内存仍然被占用!

Pod 已经删除,进程列表里也不再显示活跃任务,但数 GB 的显存却仍然挂在设备上。进一步排查时,常常会看到这样的输出:

$ fuser -v /dev/nvidia0
USER     PID ACCESS COMMAND
/dev/nvidia0:
root     kernel mount /dev
65534    27341 F...m  python <defunct>

从 Linux 进程视角看,这个任务已经结束;但从 GPU 驱动视角看,与它绑定的上下文和显存状态却未必已经被正确清理。于是,一个看似已经消失的进程,仍可能继续占据 GPU 内存。

为什么僵尸进程能占据 GPU 内存

理解这个问题的关键,在于 CPU 进程生命周期与 GPU 驱动状态机并不是完全同步的。正常情况下,Python 脚本结束时,运行时会触发对象析构,CUDA 清理例程得以执行,驱动接收到显式释放请求,于是上下文关闭,显存归还给设备池。问题在于,容器在真实生产环境里很少总是“正常退出”。它可能因为达到 CPU 或系统内存限制而被 OOMKilled,可能因为节点压力而被驱逐,也可能被开发者用 kubectl delete pod --force --grace-period=0 粗暴终止。

在这些情况下,内核向进程发送的是 SIGKILL。这个信号不会给用户态代码留下收尾时间,进程会被立刻剥离出系统资源视图:CPU 时间被回收,系统内存被释放,文件描述符被关闭。但 GPU 驱动并不是以内核相同的生命周期模型管理显存。驱动内部维护着自己的进程与上下文表,它仍然可能在等待那个永远不会再到来的清理调用。于是,内核已经认为进程不存在,而 GPU 驱动仍然保留着该 PID 曾经持有的显存分配记录。

这也是 GPU 资源管理与普通 CPU 内存管理最不同、也最容易让 Kubernetes 运维者感到失控的地方之一。面对这种僵尸显存,你能采取的手段通常都很痛苦:

  • 重新加载 NVIDIA 驱动模块,强制清空所有上下文,但代价是该节点上的所有 GPU 工作负载都会中断
  • 直接重启整台节点,以换取一个彻底干净的设备状态,但这意味着节点级停机和 Pod 重调度
  • 尝试 nvidia-smi --gpu-reset,但只在驱动认为该 GPU 不再被任何进程占用时才可能成功

这正是为什么 GPU 显存泄漏在 Kubernetes 里如此阴险。它们不会像普通应用错误那样立即爆炸,而是缓慢吞噬可用显存,直到新的 Pod 明明被调度到了“看起来还有空间”的 GPU 上,却反复因为 OOM 失败。

Kubernetes 看到的是另一种现实

从硬件视角转向编排层时,现实又会发生一次断裂。Kubernetes 并不理解物理 GPU 内部到底发生了什么,它只知道设备插件向它暴露了多少“资源单位”:

$ kubectl describe node gpu-node-1
Capacity:
  nvidia.com/gpu: 4
Allocatable:
  nvidia.com/gpu: 4
Allocated resources:
  nvidia.com/gpu: 4

这段输出看起来像是在说“节点上有四块 GPU,四块都被用了”。但如果你实际上运行的是一块开启了 4 个副本时间分片的 T4,那么这四个资源单位并不是四块彼此独立的物理 GPU,而只是设备插件向 Kubernetes 暴露出来的四个可分配槽位。继续往下看 Pod 视角:

$ kubectl get pods -A -o custom-columns= \
NAME:.metadata.name, \
GPU_REQUEST:.spec.containers[*].resources.limits.nvidia\.com/gpu

NAME             GPU_REQUEST
training-job-1   1
training-job-2   1
inference-svc    1
notebook-3       1

四个 Pod 都会“以为”自己拿到了一块独立 GPU。Kubernetes 也会把它们记作四个完整分配。但物理世界中,它们可能全都在争同一块 T4。这说明 Kubernetes 的 GPU 视图天然是抽象化、离散化的,它只适合回答“还能不能调度”,却不适合解释“为什么现在性能很差”。

在 MIG 场景中,这种断裂甚至更严重。节点视图可能是这样:

$ kubectl describe node gpu-node-2
Capacity:
  nvidia.com/mig-1g.5gb: 2
  nvidia.com/mig-2g.10gb: 1
  nvidia.com/mig-3g.20gb: 1
Allocated resources:
  nvidia.com/mig-1g.5gb: 2
  nvidia.com/mig-2g.10gb: 1
  nvidia.com/mig-3g.20gb: 0

Kubernetes 看到的是四种资源类型,而不是四个具名的、可追踪的 MIG 实例。于是,当一个 Pod 请求 nvidia.com/mig-1g.5gb: 1 时,Kubernetes 知道它拿到了一份 1g.5gb 资源,但它并不知道那到底是 MIG-aaa-111 还是 MIG-bbb-222。这种缺失在排障时会立刻变成问题:

$ kubectl get pod inference-1 -o yaml | grep gpu
    nvidia.com/mig-1g.5gb: "1"

$ nvidia-smi -L
MIG 1g.5gb Device 0: (UUID: MIG-aaa-111)
MIG 1g.5gb Device 1: (UUID: MIG-bbb-222)
# Kubernetes 不跟踪 UUID 映射

从 Kubernetes API 中,你只知道 Pod 拿到了一份 mig-1g.5gb;但从实际硬件上看,这个节点上有两个相同规格、状态却可能截然不同的实例。假设此时用户抱怨推理延迟从 50ms 恶化到 200ms,你检查监控后看到:

# 用户抱怨:“我的推理很慢!”
# 你检查他们的 Pod:
$ kubectl get pod slow-inference -o yaml | grep gpu
    nvidia.com/mig-1g.5gb: "1"

# 你检查 GPU 指标:
$ nvidia-smi
MIG 1g.5gb Device 0: Utilization 15%, Temperature 45°C
MIG 1g.5gb Device 1: Utilization 99%, Temperature 85°C

如果 Pod 绑定在 Device 0,那么问题多半不在 GPU 饱和,而在应用代码、网络、批处理队列或数据加载链路;如果它绑定在 Device 1,那么高利用率与高温已经足以解释延迟膨胀。问题在于,Kubernetes 自身并没有把这条 Pod 到具体 MIG UUID 的映射公开出来。也就是说,Kubernetes 提供的抽象恰恰在你最需要理解硬件行为的时候失效了。

请求与实际使用之间的巨大鸿沟

第三种现实视图,是 Pod 请求值与实际运行行为之间的差距。这既是监控问题,也是组织行为问题。一个典型的机器学习 Pod 往往在 YAML 中写出这样的资源声明:

# Pod 请求 8GB GPU 内存
resources:
  limits:
    nvidia.com/gpumem: 8192
代码片段:resources.yaml

但进入容器内部查看实际使用时,得到的可能是另一番景象:

$ kubectl exec training-job -- python -c "
import cupy as cp
print(f'Allocated: {cp.get_default_memory_pool().used_bytes() / 1e9:.2f} GB')
print(f'Reserved: {cp.get_default_memory_pool().total_bytes() / 1e9:.2f} GB')
"
Allocated: 2.34 GB
Reserved: 4.00 GB

于是,同一个 Pod 会同时拥有三组数字:Kubernetes 看见它请求了 8GB,CUDA 内存池实际预留了 4GB,而业务数据真正占据的显存只有 2.34GB。三组数字都不是假的,却分别描述了三个完全不同的语义层面。开发者之所以写下 8GB,往往是因为半年前模型曾在 7.5GB 左右崩过一次,于是他们加上一层保险;KAI 或其他调度组件会忠实地把这 8GB 视为已被占据的容量,不再对其他人开放;CUDA 出于性能考虑,又会先抓 4GB 池子备用;而实际训练数据也许只用到 2GB 多一点。

这类差异直接造成可观的浪费。对调度器而言,其他需要 3GB 的 Pod 无法放进来,因为“8GB 已经没了”;对物理 GPU 而言,那块 4GB 未被真正使用的空间却谁也不能碰。开发者以为自己是在保守行事,平台以为自己是在严格保障,最终的结果却是所有人一起把昂贵的显存锁死在低效配置里。

GPU 囤积的级联效应

这种浪费并不只体现在 GPU 本身,还会向整个节点级资源扩散。一个典型 GPU 节点可能有 64 个 CPU 核、256GB 内存和 4 块 Tesla T4。如果一个非常轻量的 GPU Pod 进入系统:

resources:
  requests:
    cpu: "2"
    memory: "8Gi"
  limits:
    nvidia.com/gpu: 1

它表面上只需要少量 CPU 与系统内存,却会直接占掉节点 25% 的 GPU 配额。于是,资源不平衡开始向外扩散:

$ kubectl describe node gpu-node-1
Allocatable:
  cpu: 64
  memory: 256Gi
  nvidia.com/gpu: 4
Allocated:
  cpu: 2        # 3% 使用
  memory: 8Gi   # 3% 使用
  nvidia.com/gpu: 1  # 25% 使用
Remaining:
  cpu: 62       # 97% 浪费
  memory: 248Gi # 97% 浪费
  nvidia.com/gpu: 3  # 只能再放 3 个 GPU Pod

如果再来三个类似的轻量 Pod,节点四块 GPU 会全部“分配完毕”,而 CPU 和系统内存的绝大多数仍然闲置。很多组织又会把 GPU 节点和普通 CPU 节点分开管理,因此这些剩余 CPU 与 RAM 也不会自然被其他业务吃掉。结果就是,你的监控面板可能显示“GPU 100% 已分配”,团队开始申请更多 GPU 节点;但真实问题并不是物理 GPU 不够,而是整块 GPU 作为最小分配单位时把大量伴随资源一并锁死了。

如果按月成本来算,问题会变得更刺眼。设一个 GPU 节点月成本约 2000 美元,而最终只有 8 个 CPU 核、32GB RAM 和 4 块 GPU 被真正用到,那么 CPU 和系统内存层面大约 87% 的资源价值都被无效捆绑到了 GPU 分配上。很多团队之所以感到 GPU 集群“极贵”,并不只是因为 GPU 本身贵,更因为这种以 GPU 为瓶颈的资源囤积把整台节点拖入了低效状态。

DCGM:弥合现实差距

到这里可以看到,仅靠 nvidia-smi 和 Kubernetes API 已经无法解释真实运行状况。要把硬件视图、编排视图和容器使用视图连接起来,你需要一个中间层。这就是 NVIDIA 的 数据中心 GPU 管理器(DCGM) 的价值所在。它提供进程级、容器级乃至 Pod 级的 GPU 指标,使你终于可以把“谁在用、用了多少、在哪块设备上用”串起来。

部署 DCGM exporter 的过程通常并不复杂:

$ helm repo add nvidia https://nvidia.github.io/dcgm-exporter/helm-charts
$ helm install dcgm-exporter nvidia/dcgm-exporter \
    --namespace monitoring \
    --set serviceMonitor.enabled=true

Exporter 启动后,就可以把它暴露出来的指标作为 Prometheus 目标抓取。先做一个最简单的检查:

$ kubectl port-forward -n monitoring svc/dcgm-exporter 9400:9400 &
$ curl -s localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL
DCGM_FI_DEV_GPU_UTIL{gpu="0",UUID="GPU-abc-123-def",pod="training-job-1"} 45.2
DCGM_FI_DEV_GPU_UTIL{gpu="0",UUID="GPU-abc-123-def",pod="inference-svc"} 12.8
DCGM_FI_DEV_GPU_UTIL{gpu="0",UUID="GPU-abc-123-def",pod="notebook-3"} 0.0

nvidia-smi 那个单一的节点级数字相比,DCGM 的价值在于可归因性。你终于可以看到同一块 GPU 上,哪个 Pod 在消耗多少利用率。更重要的是,DCGM 暴露的并不只是一个简单的“GPU 忙碌百分比”,还包括显存使用、内存带宽、Tensor Core 活动等更具解释力的指标。尤其关键的是,要把 GPU 活跃度与 SM 活动区分开来:某个 Pod 也许在 nvidia-smi 上表现为 90% GPU 利用率,但如果 DCGM 显示它的 SM 活动只有 20%,那就说明 GPU 虽然一直有核函数驻留,却并没有把计算单元高效用起来。

对于 MIG,这种可归因能力甚至会成为唯一可靠的真相来源。DCGM 能提供按 UUID 归属的切片级指标:

$ curl -s localhost:9400/metrics | grep "MIG-"
DCGM_FI_DEV_GPUUTIL{UUID="MIG-aaa-111",pod="inference-1"} 67
DCGM_FI_DEV_GPUUTIL{UUID="MIG-bbb-222",pod="inference-2"} 89
DCGM_FI_DEV_GPUUTIL{UUID="MIG-ccc-333",pod="training"} 34
DCGM_FI_DEV_FBUSED{UUID="MIG-aaa-111",pod="inference-1"} 2147483648
DCGM_FI_DEV_FBUSED{UUID="MIG-bbb-222",pod="inference-2"} 3221225472
DCGM_FI_DEV_FBUSED{UUID="MIG-ccc-333",pod="training"} 8589934592

这样一来,前面 Kubernetes 无法回答的问题终于有了答案:哪个 Pod 正在用哪个 MIG 实例,以及它在那个实例上到底用了多少算力和显存。有人抱怨性能时,你不再需要猜测它是不是落在了高温高利用率的切片上,而是可以直接从指标中看到 Pod 与具体 MIG UUID 的映射关系。

分配与利用率之间的差距

一旦拥有了 DCGM,你就可以开始把“分配出去的资源”与“真正被消费的资源”做系统化对比。首先要找的,往往不是最忙的 Pod,而是最会囤积资源的 Pod。下面这个 PromQL 查询的目标,就是找出持有 GPU 配额却几乎没怎么使用的工作负载:

(
 sum by (pod, namespace) (
  DCGM_FI_DEV_GPUUTIL
 ) < 10
)
and
(
 sum by (pod, namespace) (
  kube_pod_container_resource_limits{resource="nvidia.com/gpu"}
 ) > 0
)
代码片段:Pod GPU 囤积查询

结果可能长这样:

{namespace="team-ml", pod="notebook-23"}   0
{namespace="team-ml", pod="notebook-24"}   3
{namespace="research", pod="experiment-old"}  0
{namespace="research", pod="debug-session"}   0

这些 Pod 占着 GPU,却几乎没做任何工作。进一步地,你还可以把浪费直接折算成金钱:

sum by (namespace) (
  kube_pod_container_resource_limits{resource="nvidia.com/gpu"}
  *
  (100 - clamp_min(DCGM_FI_DEV_GPUUTIL, 0.01))
  / 100
  *
  1.45
)

示例结果如下:

{namespace="team-ml"}     $67.20/小时
{namespace="research"}    $43.50/小时
{namespace="inference"}   $8.70/小时

当浪费被表达为每小时、每天或每周的金钱流失时,团队才更容易意识到“低利用率”不是抽象优化问题,而是实打实的预算泄漏。显存层面的浪费通常更严重。对比 Pod 请求值与 DCGM 中实际显存占用时,能看到更清晰的失配:

$ join -t$'\t' \
  <(kubectl get pods -A -o json | \
    jq -r '.items[] |
    select(.spec.containers[].resources.limits."nvidia.com/gpu" != null) |
    "\(.metadata.namespace)/\(.metadata.name)\t\(.spec.containers[].resources.limits."nvidia.com/gpu")"' | sort) \
  <(curl -s localhost:9400/metrics | grep DCGM_FI_DEV_FB_USED | \
    awk '{print $1}' | sed 's/.*pod="//' | sed 's/".*//' | \
    awk '{getline val; print $0 "\t" int(val/1048576)}' | sort)

team-ml/training-1    8192  2340
team-ml/training-2    8192  1890
research/model-test   16384 4530
inference/service-1   4096  1200

这些数字的模式非常熟悉:许多训练与推理任务会请求实际所需显存的三到四倍。这并不一定意味着开发者不负责任,反而通常是理性的防御行为。他们曾经因为批量大小上升、输入长度变长或实验参数变化而在后期训练中突然 OOM,于是自然会倾向于“多要一些,以防万一”。可是在 GPU 内存不能像 CPU 一样轻松超额订阅的前提下,这种行为会直接把集群的可用容量锁死。于是你看到的是 90% 的显存已经分配,却只有 25% 到 30% 真正在被使用。

真正重要的监控模式

只有在 DCGM 补齐了可归因指标之后,GPU 集群里的许多故障模式才首次变得可见。第一个应该监控的是持续增长的显存占用,也就是 GPU 内存泄漏。与普通 CPU 程序不同,GPU 侧的内存分配往往不会被自动垃圾回收;如果框架、库或用户代码没有显式释放,那些分配会持续存在并逐步累积,直到最后触发 OOM。识别这种风险的一个实用查询如下:

rate(DCGM_FI_DEV_FB_used[1h]) > 0
and
DCGM_FI_DEV_FB_USED/DCGM_FI_DEV_FB_FREE>0.8

这个表达式的思路很直接:过去一小时显存还在持续增长,同时当前显存占用已经超过可用容量的 80%,说明这个 Pod 已处于逼近 OOM 的危险区。将其转成告警规则时,可以这样写:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: gpu-memory-leak

spec:
  groups:
    - name: gpu
      rules:
        - alert: GPUMemoryLeak
          expr: |
            rate(DCGM_FI_DEV_FB_USED[1h]) > 1048576
            and
            DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE) > 0.8
          annotations:
            summary: "在 {{ $labels.pod }} 上检测到 GPU 内存泄漏"
            description: "内存使用量以 {{ $value | humanize }}B/小时增长"
代码片段:gpu-memory-leak.yaml

这种告警的价值,在于它把问题前移到真正崩溃之前。如果没有这类规则,团队往往只有在 Pod 已经 OOM、训练进度丢失之后,才第一次意识到内存一直在泄漏。

第二类值得重点监控的模式是热降频。GPU 在温度上升到一定阈值后会主动降低频率,以防止硬件受损。对用户而言,现象通常表现为“昨天两小时跑完的训练今天要三小时”,而节点看上去一切正常。通过 nvidia-smi -q -d TEMPERATURE 可以看到相关阈值:

$ nvidia-smi -q -d TEMPERATURE
GPU Current Temp            : 83 C
GPU Shutdown Temp           : 96 C
GPU Slowdown Temp           : 93 C
GPU Max Operating Temp      : 85 C

这些阈值随 GPU 型号而不同,但模式基本一致:接近 85°C 时开始降速,接近 93°C 时会更明显地降低频率,96°C 左右则进入保护性关闭。对这类问题,一个非常实用的查询是:

DCGM_FI_DEV_GPU_TEMP > 80
and
rate(DCGM_FI_DEV_GPU_TEMP[5m]) > 0

这个查询识别的是“已经很热而且还在继续升温”的 GPU。它比单纯盯住一个温度阈值更有意义,因为温度变化率能帮助你更早捕获风扇故障、散热受阻或异常负载飙升。

第三类模式与共享过载有关,也就是时间分片或多进程共享引发的上下文切换开销。当太多进程竞争同一块 GPU 时,GPU 在不同上下文之间保存和恢复状态的开销会变得可见:

# 统计每个 GPU 的进程数
$ nvidia-smi --query-compute-apps=pid --format=csv,noheader | wc -l
12

# 检查上下文切换开销
$ nvidia-smi dmon -s u -c 10
# gpu  sm  mem  enc  dec
   0   45   23    0    0
   0    0    0    0    0  # 上下文切换
   0   67   45    0    0
   0    0    0    0    0  # 上下文切换
   0   23   12    0    0

这些全为零的行并不等于 GPU 真正空闲,而是在做上下文切换。每次切换也许只要几毫秒,但如果有十几个进程争抢同一块卡,这些毫秒最终会积累成明显的吞吐损失。对应的一个简单 PromQL 规则是:

count by (gpu_id) (
 DCGM_FI_DEV_GPUUTIL{} > 0
) > 8

当同一块 GPU 上活跃进程数长期超过某个阈值时,通常意味着你需要重新审视时间分片策略、调整共享粒度,或者把部分工作负载迁走。

现实检验

一旦建立起真正可归因的 GPU 可观测性,很多团队最终都会发现一些相似的事实:

  1. 大约 30% 到 40% 的 GPU 分配长期处于完全闲置状态
  2. 另外约 30% 的分配虽然“在用”,但利用率低于 20%
  3. 显存请求值通常比真实使用高出 3 到 4 倍
  4. 交互式 Notebook 会把 GPU 持有数天,而真正使用时间也许只有几分钟
  5. 失败实验或废弃调试会话会继续运行,因为没人持续检查

监控本身并不会替你解决这些问题,但它会把原本可以长期隐藏在绿色仪表板背后的浪费和故障模式强行暴露出来。一旦问题可以被归因、量化并与成本挂钩,治理才真正开始变得可能。

关键要点

  • GPU 集群监控不能依赖单一视图,因为 nvidia-smi、Kubernetes 和实际运行中的容器使用情况描述的是三层不同现实。
  • nvidia-smi 告诉你物理设备层面的状态,Kubernetes 告诉你资源被如何声明和分配,但只有 DCGM 才能把硬件使用与具体 Pod、容器和 MIG 实例真正关联起来。
  • GPU 利用率并不等于效率;一个 GPU 可以在统计口径上非常忙碌,却几乎没有完成多少有效计算。
  • 显存请求值与真实使用值之间长期存在巨大落差,很多团队会以“防 OOM”为理由请求实际所需三到四倍的资源。
  • 真正有价值的监控体系,不是把仪表板做得更漂亮,而是要能揭示浪费、识别归因、提前预警,并把问题直接转化为故障风险与成本信号。
  • 当浪费可以被持续观测并用资金损失表达出来时,GPU 资源治理才会从可选优化变成平台层面的必需能力。
创建于 2026/05/24 更新于 2026/05/24 8118 字 阅读约 17 分钟