报错现象
先还原一个很典型的场景。月度容量评审会上,业务方说“卡不够用,队列里还有几十个任务在等”,可调度系统的报表显示 GPU 分配率已经接近 100%,一张空闲卡都挑不出来。运维把 DCGM 面板拉出来一看,全集群 DCGM_FI_PROF_SM_ACTIVE 的均值只有 0.73 左右——也就是说,在被占用的卡时里,大约有 27% 的时间连一个 warp 都没有驻留在 SM 上。卡是“分出去了”,但没在干活。
这类问题通常不是一条醒目的红色报错,而是由一组“看起来不疼不痒”的症状凑成的:
1. GPU 显存占满、算力空转。nvidia-smi 里 Utilization 长期在个位数,而 Memory-Usage 稳定在 80%~95%,功耗贴着空闲值。用户以为卡在跑,其实只是在“占着”。
2. nvidia-smi 说没有进程,但显存就是不释放。容器已经退出,显存要等宿主侧清理后才回来,中间这段时间调度器认为卡不可用。
3. torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate ...,但同一时刻 nvidia-smi 显示卡上还有大量空闲显存。这是典型的显存碎片,不是总量不足。
4. 多卡训练卡在启动阶段,日志里反复出现 Watchdog caught collective operation timeout: WorkNCCL(SeqNum=..., OpType=ALLREDUCE, Timeout(ms)=...) ran for ... milliseconds before timing out.,或者 NCCL WARN 开头的重试信息。任务既没失败也没进展,卡处于“等通信”的状态。
5. 同一份代码、同一个镜像,换一批节点吞吐差出一截。没有报错,只是慢,而慢的节点往往集中在某几个机柜或某几台交换机下面。
影响范围:小到一个容器、一个进程,中到一台 8 卡节点,大到整个 IB leaf 下的一个拓扑域。表现为“容量不够”和“任务莫名变慢”两种形态,且很容易被误判成“业务量涨了,该采购了”。
可能原因
按实际排查中出现的概率从高到低排:
1. 显存被占、SM 空转(占卡不干活)。Jupyter 内核残留、服务框架预分配显存、OOM 后未退出的僵尸进程,都会造成“卡被分配、算力闲置”。
2. 采集口径错误,27% 这个数字本身有问题。DCGM_FI_DEV_GPU_UTIL 的定义是“采样周期内是否有 kernel 在执行”,它是布尔式的,不反映 SM 用了多少。用它算可用率,会把“跑了一个小 kernel”算成满负荷,也会把“多进程分时共享”算得面目全非。
3. GPU 在等,不是在算。通信等待(NCCL all-reduce、参数同步)和数据加载等待(DataLoader、存储带宽、CPU 解码)都会让 SM 空转,但显存是满的。
4. 拓扑错配。多卡任务被分到了跨 NUMA、跨 PCIe switch、甚至跨 IB leaf 的卡上,all-reduce 带宽下降一个档位,任务整体变慢,卡时被白白消耗。
5. 调度粒度与碎片。整卡分配跑小任务、队列 bin-packing 策略不合理、任务之间有空窗期,都会让分配率虚高。
6. 硬件降频或错误。温度墙、功耗墙、ECC 错误累积、XID 报错,会让同一张卡的算力低于同型号其他卡。
逐条排查与解决
原因 1:占卡不干活
先看卡级快照,连续采样,别只看一眼:
```bash
nvidia-smi --query-gpu=index,uuid,utilization.gpu,memory.used,memory.total,power.draw,clocks.sm \
--format=csv -l 5
```
判断标准:utilization.gpu 长期低于 10%,同时 memory.used 超过总量的一半,power.draw 贴近该型号的空闲功耗。三者同时成立,基本可以确定是占卡不干活。
再看是谁占的:
```bash
nvidia-smi --query-compute-apps=pid,process_name,used_gpu_memory,gpu_uuid --format=csv
```
拿到 PID 后查生命周期:
```bash
ps -o pid,ppid,etime,time,rss,cmd -p <PID>
```
判断标准:etime(已运行时长)很长,但 time(累计 CPU 时间)很短,说明进程大部分时间在睡觉。再配合 cat /proc/<PID>/status | grep -i state 看是否处于 S(睡眠)状态。
常见根因与处置:
- Jupyter / Notebook 内核残留:给容器加空闲内核回收,或设置
c.ServerApp.kernel_manager.cull_idle_timeout。 - 服务框架显存预分配:vLLM 这类推理框架有
gpu_memory_utilization之类的参数,TensorFlow 有per_process_gpu_memory_fraction或set_memory_growth。把预分配比例调到贴合实际,别一上来就吃掉 90%。 - 训练进程 OOM 后没有退出,父进程还在重试:检查容器的重启策略,加上失败即退出。
可以用一段最简脚本把“占卡不干活”的卡筛出来:
```bash
nvidia-smi --query-gpu=index,uuid,utilization.gpu,memory.used,memory.total \
--format=csv,noheader,nounits | \
awk -F', ' '{ if ($3 < 10 && $4/$5 > 0.5) print "疑似占卡空转 GPU" $1 " uuid=" $2 }'
```
原因 2:采集口径错误
先确认你看的是哪个指标。
DCGM_FI_DEV_GPU_UTIL:采样窗口内有没有 kernel 在跑,取值 0~100。一个每秒唤醒一次跑 1ms 小 kernel 的进程,在这个指标上可能接近 100。
DCGM_FI_PROF_SM_ACTIVE:SM 上至少有一个 warp 驻留的周期占比,取值 0~1。这个才更接近“算力用了多少”。
做法:把两个指标并排画出来,看差值。
```promql
avg by (instance) (DCGM_FI_DEV_GPU_UTIL) / 100
avg by (instance) (DCGM_FI_PROF_SM_ACTIVE)
```
如果前者 0.95、后者 0.4,说明卡上跑的是“高频唤醒的轻任务”,不是重计算。这时候用 DEV_GPU_UTIL 算可用率,结论会完全反过来。
注意两点:
- PROF 系列指标(
DCGM_FI_PROF_*)属于 profiling 采集,开启后有一定额外开销,建议只在体检期间或抽样节点上开启,长期采集要先评估影响。 - 采集间隔对结论影响很大。用
dcgmi dmon做现场排查时把间隔压到 1 秒:
```bash
dcgmi dmon -l # 先列出本机支持的字段 ID 与含义
dcgmi dmon -e <字段ID列表> -d 1000
```
字段 ID 在不同 DCGM 版本间可能调整,请以 dcgmi dmon -l 的实际输出和官方文档当前版本为准。
判定标准:如果 DEV_GPU_UTIL 与 PROF_SM_ACTIVE 长期背离超过 30 个百分点,就先修采集口径,再谈优化。否则后面的所有结论都是错的。
原因 3:GPU 在等通信或在等数据
先区分是“等网络”还是“等数据”。看两组指标的搭配:
```promql
avg by (instance) (DCGM_FI_PROF_SM_ACTIVE)
avg by (instance) (DCGM_FI_PROF_DRAM_ACTIVE)
avg by (instance) (DCGM_FI_PROF_NVLINK_TX_BYTES)
avg by (instance) (DCGM_FI_PROF_PCIE_TX_BYTES)
```
判定表:
| SM 活跃 | DRAM 活跃 | NVLink/IB 流量 | 结论 |
|---|---|---|---|
| 低 | 低 | 高 | 通信等待,查 NCCL 与拓扑 |
| 低 | 低 | 低,但宿主机 CPU 高 | 数据加载瓶颈,查 DataLoader 与存储 |
| 高 | 高 | 正常 | 真的在算,问题不在这里 |
通信侧的证据可以直接从 NCCL 日志拿。用一个小规模的 all-reduce 测试跑一遍:
```bash
NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,NET \
<你的 all-reduce 测试程序>
```
日志里重点看两件事:GPU 之间是走 via P2P(NVLink / PCIe P2P)还是 via shared memory(退回主机内存),网卡侧是走 NET/IB 还是 NET/Socket。退回 shared memory 或 Socket,说明 P2P 或 RDMA 没启起来,带宽会差一个量级。
数据加载侧的判断更直接:在训练日志里看 step 时间的周期性。如果 GPU 利用率呈现规律的“锯齿”——几秒满载、几秒归零,基本就是 DataLoader 供给不上。处置方式是加 worker 数、换更快的存储、把解码放到 GPU、或者预先把小文件打包成大文件。
原因 4:拓扑错配
这是最容易被忽略、但对多卡任务伤害很大的一条。
```bash
nvidia-smi topo -m
```
输出是一张矩阵,交叉点上的标记含义要会读:
NV#:走 NVLink,#是链路数,数字越大越好。PIX:同一 PCIe switch 下。PXB:跨 PCIe switch,但要经过 PCIe host bridge。PHB:要经过 CPU 的 PCIe host bridge。NODE:跨 NUMA 节点,同一个 socket 内。SYS:跨 socket,走 UPI/QPI。
判断标准:一个 8 卡任务,如果关键的 all-reduce 组合落在 SYS 或 NODE 上,通信成本会明显高于落在 NV# 或 PIX 上。这时候不是卡坏了,是调度把不该放一起的卡放在一起了。
同时确认网卡位置:
```bash
lspci -tv | head -60
numactl -H
```
看 IB 网卡和 GPU 是否挂在同一个 PCIe switch / 同一个 NUMA 节点下。跨 NUMA 的 GPU 直连网卡,会让 RDMA 路径多绕一圈。
调度侧的动作:
- 多卡任务优先整机分配,不要把一个 8 卡任务拆到两台 4 卡机上,除非你确认过通信量。
- 启用拓扑感知调度(Kubernetes 侧有对应的 Topology Aware Scheduling 能力,具体可用性与配置方式以官方文档当前版本为准)。
- 容器加 NUMA 亲和,配合
CUDA_VISIBLE_DEVICES精确绑定到物理卡序号,避免容器内序号与物理序号错位。 - 单机内可以用
NCCL_TOPO_DUMP_FILE把拓扑导出,留档对比不同节点是否一致。
原因 5:调度粒度与碎片
先量化浪费。用一条 PromQL 粗略估算“已分配但 SM 不活跃”的卡时占比:
```promql
1 - avg by (instance) (DCGM_FI_PROF_SM_ACTIVE)
```
再按队列、按镜像、按用户维度拆开,就能看出浪费集中在谁身上。
显存碎片的量化:
```promql
avg by (instance) (DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL)
```
如果显存使用率长期在 90% 以上,但 SM 活跃度很低,且业务侧频繁报 OOM,就是碎片与大块预留并存。
处置手段:
- 小任务用 MIG 切分,把一张卡切成多个实例,各自独立调度:
```bash
nvidia-smi mig -lgip # 列出本机支持的 GI profile
nvidia-smi mig -cgi <profile_id> -C
nvidia-smi -L # 确认 MIG 设备已生成
```
- 短任务、交互式任务用 MPS 做时间片共享,减少上下文切换开销:
```bash
nvidia-cuda-mps-control -d
```
- 队列策略上,把散布(spread)和装箱(bin-packing)按队列分开配:延迟敏感的小任务用 spread,大训练任务用 bin-packing 减少碎片。
- 加 gang scheduling,避免“半个任务占着卡等另一半”的死锁型浪费。
原因 6:硬件降频与错误
```bash
nvidia-smi -q -d PERFORMANCE
nvidia-smi -q -d ECC
dmesg | grep -i xid | tail -50
```
PERFORMANCE 段里会列出 Clocks Throttle Reasons,看是 SW Power Cap、HW Slowdown、HW Thermal Slowdown 还是 SW Thermal Slowdown 被置位。如果长期挂着温度或功耗降频,同型号卡的算力就不是一个水平线。
XID 错误在 dmesg 里以 NVRM: Xid 开头。偶发一次可以观察,同一张卡反复出现就要下线。DCGM 侧对应 DCGM_FI_DEV_XID_ERRORS 指标,可以直接做告警。
硬件层面的完整体检用:
```bash
dcgmi diag -r 2
```
-r 的等级越高,跑得越久、覆盖越全,高等级会独占 GPU,务必先把节点从调度池里摘出来。
都不管用时的兜底方案
如果上面六条都排查过,指标还是对不上,按顺序做这几件事:
1. 先隔离,再深挖。Kubernetes 上 kubectl cordon <node> 加 kubectl taint nodes <node> dedicated=gpu-check:NoSchedule;Slurm 上 scontrol update NodeName=<node> State=DRAIN Reason="gpu-check"。别让可疑节点继续接活。
2. 做 A/B 对照。同一个镜像、同一份数据、同一个 benchmark,在“好节点”和“坏节点”上各跑一遍,记录吞吐、SM 活跃度、NVLink 带宽。差异稳定复现,才说明是节点问题而不是任务问题。
3. 跑完整诊断。节点下线后执行 dcgmi diag -r 3,同时保留 nvidia-bug-report.sh 的输出。这两份材料是找厂商支持时最有用的东西。
4. 换路径兜住业务。把任务临时迁到备用卡型或备用机房;对延迟不敏感的离线任务,可以先转 CPU 池跑着,别让队列堵死。
5. 回滚最近变更。驱动、CUDA、DCGM、容器运行时、调度器版本,任何一项在问题出现前动过,都先回退验证。这类问题的根因里,变更引入的比例不低。
如何预防再次发生
体检做完,最重要的是把一次性排查变成常态化的几个指标和几条规则。
一、定义四个“有效可用率”指标,而不是一个
- 分配率:卡被分配出去的时长占比。
- SM 活跃率:
DCGM_FI_PROF_SM_ACTIVE的均值。 - 显存有效率:
DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL的分布。 - 拓扑命中率:多卡任务的卡组合落在
NV#/PIX上的比例。
只看分配率,就会得到“卡很紧张”的错觉;四个一起看,才能看出那 27% 在哪里。
二、把采集口径固定下来
统一 exporter 的采集间隔和字段集,PROF 指标按需开启并记录开启窗口;所有看板以 GPU UUID 而不是序号作为主键,避免容器内序号错位;面板上把 DEV_GPU_UTIL 和 PROF_SM_ACTIVE 并排放,从源头防止误读。
三、把调度规则写进配置
- 多卡任务整机分配,卡数超过单机一半时禁止跨机拆分。
- 拓扑感知调度开启,绑定 NVLink 域。
- NUMA 亲和与
CUDA_VISIBLE_DEVICES绑定由平台统一注入,不交给用户手填。 - 小任务默认走 MIG 或 MPS 池,不占整卡。
- 空闲超时回收策略覆盖所有交互式容器。
四、加两条告警
- SM 活跃率低于阈值且显存占用高于阈值,持续一段时间 → 疑似占卡空转。
DCGM_FI_DEV_XID_ERRORS出现非零增量 → 立即定位到卡,纳入观察名单。
五、定期巡检与准入
每周对空闲窗口的节点跑一次低等级 dcgmi diag,每月对全集群做一次拓扑一致性比对;新节点上线前必须过一遍诊断和基准跑分,把结果和机型、固件、驱动一起入库。这样下次再出现“卡不够用”的声音时,你能在十分钟内回答:是真不够,还是那 27% 又藏起来了。
