做AI训练的朋友应该都撞见过同一个画面:nvidia-smi刷出来,GPU 那栏利用率在 20% 和 60% 之间反复横跳,显存占了大半,但吞吐就是上不去。卡是真的贵,一秒钟的空转都是钱在烧,这时候谁都没法淡定。这篇文章就是要解决"AI训练GPU利用率低怎么排查"这个老问题——我把实践中遇到的瓶颈归结为 6 个大类,每一个都会给出定位方法、排查命令和修复手段,最后附一张可以直接抄的排查清单。适合正在训练脚本的算法工程师、被老板追问"卡为什么没吃满"的平台运维,以及所有想省下一笔 GPU 租用成本的个人开发者。
1. 定位瓶颈之前的三个准备
1.1 先把"GPU利用率"这三个字搞懂
很多人一上来就盯住nvidia-smi里的utilization.gpu,这个数字其实很容易骗人。它统计的是 SM(流式多处理器)上有活动指令的时间占比,换句话说,只要 GPU 在干活就算利用率高,但干的活是不是"有用的活"它不管。曾经我碰到过一个训练任务,利用率稳定在 95%,可吞吐却低得离谱,后来发现模型里有个算子把大量时间耗在了内存拷贝上,SM 在等数据,依然显示忙碌,实际上算力资源整个是空转的。
所以排查利用率之前,必须建立三个不同视角的指标维度:SM 利用率(是不是真的有计算在跑)、显存带宽利用率(数据搬运是否堵车)、算力吞吐(每秒能完成多少样本或者 FLOPs)。这三个指标在nvidia-smi里看不到完整信息,得借助 DCGM、NVIDIA Nsight Compute(ncu)这类工具。理解这一点后,后文所有的排查思路你就知道在盯什么了——我们真正要找的是"计算资源被空转、被等待、被抢走"的根因,而不是单纯追求一个看起来很美的百分比。
1.2 建立指标打点与基线
排查问题跟看病一样,得先有"正常指标"才能判断异常。最简单的做法是先把当前环境的基线打出来:单卡跑一个小的 ResNet 或 BERT-tiny,记录稳定状态下的 step time、吞吐(samples/s)、GPU 利用率、显存占用和功率。如果这批数据全都不正常,那可能根本就不是代码问题,而是硬件或驱动层面的问题。
打点工具有几个层次。第一层是命令行的快速观察:watch -n 1 nvidia-smi看动态变化;nvidia-smi dmon -d 1 -s pucvmet能按秒输出 SM、内存、温度、功耗等多维度状态;gpustat -i 1是交互式监控里体验最好的一个。第二层是专业剖析器:PyTorch 自带torch.profiler,能展示算子级的时间线;NVIDIA 的nsys profile能给出 CPU 和 GPU 之间的完整时间线;ncu能看到具体 kernel 内部的计算单元利用率。建议把第一层做成命令行习惯,第二层在怀疑具体环节时用。
1.3 先判断三类瓶颈信号
瓶颈大概逃不出三类:IO bound(数据供给和搬运跟不上)、communication bound(多卡通信拖了后腿)、compute bound(计算本身慢)。定位之前有个小技巧,不用急着上重型工具,先观察 step time 的结构——如果每个 step 里 GPU kernel 跑得非常快,但跟前一个 step 之间有一段很长的空隙,那大概率是 IO 或者 CPU 侧调度出了问题;如果 kernel 本身跑得慢,才值得往计算和算子上深挖。
这里有个重要的建议:在排查刚开始时,不要急着改 batch size、换模型或者锁频,否则多个变量同时变化,你根本说不清问题出在哪。一次只动一个变量,是所有性能调优的铁律。
2. 瓶颈一:数据供给链路——最常见的隐性杀手
2.1 为什么数据管道会把 GPU 拖死
我排查过的项目里,有六成以上 GPU 利用率低的问题根因在数据链路上。典型的情形是:PyTorch 的DataLoader在主进程里做 collate、图像解码、tokenize,或者磁盘上躺着一堆几十 KB 的小文件,读取的时候大量时间花在 IO 寻址上——GPU 就像一个米其林大厨,锅都热好了,配菜却一直端不上来,只能干等。
还有一个容易被低估的是 Python 的 GIL 锁。你以为设了num_workers=16数据并行加载就够了,但如果你在训练循环里直接调用了 tokenizer 或者做了自定义的collate_fn,这些东西可能依然在主进程里运行,GIL 会跟模型前向反向抢 CPU 时间,GPU 的利用率就会呈现出锯齿状的起伏。
2.2 定位方法
先看现象:nvidia-smi dmon里 GPU 利用率呈现有规律的掉坑,比如每隔几秒就跌到个位数。这时候把注意力从 GPU 挪到 CPU 和磁盘上,执行top -H -p <训练进程PID>查看线程占用,配合iostat -x 1看磁盘读写是否被打满,用pidstat -d 1看进程级别的 IO 负载。
工具层面最直观的还是nsys profile --trace=cuda,osrt,nvtx python train.py,跑几十个 step 后导出报告,在时间线上你会清楚地看到:GPU kernel 之间有大段空闲,空闲期间 CPU 侧在忙图像解码、内存拷贝甚至日志打印。我处理过一个真实案例,某个中医问答模型训练集有 54 万条对话数据,单卡环境下预处理每轮要重新做最长截断和 tokenizer,GPU 利用率连 40% 都摸不到。问题不在于数据量大,而在于预处理全部堆在了训练进程中同步执行。
2.3 修复实践与效果
最直接有效的做法是调整 DataLoader 参数:合理提高num_workers,经验值建议设为物理核数减 2,比如 16 核的机器用 14;开启pin_memory=True,并且在模型.to('cuda')前将输入也加上non_blocking=True,这样可以减少主机到设备的数据拷贝等待。更大的改动是把 tokenize、图解码这类重活全部挪进 worker 进程,让主进程只做张量拼装。
如果数据规模上了几十 GB 甚至 TB 级,就必须考虑存储层的改造。把海量小文件合并成 TFRecord、WebDataset 或者内存映射格式,读取速度会快很多;也可以在训练前把整个数据集预处理成张量缓存到本地 SSD。实操下来,数据链路问题修复后 GPU 利用率从 30% 拉到 85% 以上是很常见的结果,而且改动量不大,收益却立竿见影。
3. 瓶颈二:多卡协同与通信开销
3.1 多卡训练为何是利用率洼地
单卡跑到 95% 不稀奇,四卡八卡还能跑到 90% 才是本事。多卡训练的利用率天花板往往不是算力,而是通信。最常见的 DDP 模式下,每轮梯度更新前所有卡要同步梯度,这个 AllReduce 过程传输的数据量等于模型参数的两倍,模型越大通信越狠。如果每张卡的 batch size 还很小,计算时间短、通信时间长,那么通信就成了绝对瓶颈——你可以类比成八个人协作搬砖,每搬一块砖就要开一次全体会议,会议时间比搬砖还长,产出自然上不去。
3.2 定位方法
先看加速比:记录 1 卡稳定吞吐,再记录 N 卡稳定吞吐,算一个加速比 = N卡吞吐 / 1卡吞吐。理想情况接近 N,4 卡如果加速比低于 2.5,基本可以断定通信配置有问题。然后检查单机和跨机的硬件拓扑,执行nvidia-smi topo -m,会看到 GPU 之间是 NVLink、PCIe Switch 还是跨 CPU/跨 NUMA 连接。卡和卡如果被分到了不同的 PCIe Switch,通信带宽会被大幅压缩。
更细的定位用nsys看 kernel 时间线:如果出现大量名字里带 NCCL 的 kernel 时间块,或者 GPU kernel 之间频繁出现通信等待,就确认是通信开销过大。这个环节也可以给训练脚本加上torch.distributed层面的日志,或者临时打开NCCL_DEBUG=INFO,你会看到详细的传输速率和拓扑识别过程。
3.3 修复实践
通信问题的修复思路就一条:提高"计算时长 / 通信时长"的比例。最粗暴有效的办法是增大单卡 batch size,让每轮计算时间更长,通信次数自然减少;如果显存放不下,就用梯度累积来模拟大 batch。模型太大则配合梯度检查点(gradient checkpointing)节省显存空间,把省下来的显存换成更大的 batch。
拓扑层面,单机多卡优先让张量并行和流水线并行避开跨 PCIe 的链路;跨机训练要确保使用带宽充足的高速网络,比如 InfiniBand 或者 RoCE,并检查 NCCL 是否识别了正确的网络接口。还要注意一个细节:DataParallel这种老接口是多线程模型复制,性能差且利用率低,要坚决换成DistributedDataParallel。
4. 瓶颈三:模型结构与计算策略本身
4.1 计算瓶颈的特征
如果数据链路和通信都排查干净了,GPU 利用率还是上不去,那就得把目光收回到计算本身。计算瓶颈有个容易混淆的表现:GPU 利用率可能很高,但吞吐也提不上去,比如某些 kernel 在硬件上本来就只有 30% 的峰值效率,指标自然好看不了。
典型的问题包括:LayerNorm、SiLU 这类算子非常吃内存带宽;小矩阵算子太多,kernel launch 的开销占比反而比计算本身还大;或者是代码里混入了 CPU 回退逻辑。我见过有人用 RAG 架构训练向量检索模型,把大量小规模的向量距离计算用 Python for 循环来做,GPU 根本没被喂到足够大的张量,利用率低几乎是必然。
4.2 定位方法
用 PyTorch Profiler 看算子排行:torch.profiler.profile加上activities=[torch.profiler.ProfilerActivity.CUDA, torch.profiler.ProfilerActivity.CPU],跑几十个 step 后导出表格,你会看到哪些算子占了最多 GPU 时间。更进一步用ncu --section SpeedOfLight ./your_script,能得到每个 kernel 在硬件层面的 SM busy 百分比和 issue 利用率,判断这算子到底是不是硬件层面的效率洼地。
如果某个自定义算子占了 40% 以上的时间,或者 launch overhead 一大片,那就到了该做算子融合或者批量化的阶段。CUDA Graphs 对 launch overhead 非常有效,它把你的一串 kernel 调用固化成一个图,减少 CPU 端反复发指令的开销,尤其适合小算子密集的模型结构。
4.3 通用的优化手段
现在最简单的收益路径是torch.compile,把原来的训练函数包一层,很多场景下自动算子融合和 CUDA Graphs 就能带来 20% 到 50% 的吞吐提升,代码改动却只有几行。其次要打开混合精度,用 FP16/BF16 把内存带宽压力减半,注意 PyTorch 还提供了 TF32 选项,在 Ampere 及以后架构上能兼顾精度和速度。
检查cudnn.benchmark=True也很重要,这个开关会让 cuDNN 先跑一遍选择最佳卷积算法,长期训练中能省不少时间。另外,如果你的模型里大量使用for循环遍历序列,优先考虑把计算批量矩阵化,否则小 kernel 满天飞,提升算子效率也救不了整体性能。
5. 瓶颈四:CPU 调度与框架开销
5.1 这个瓶颈最容易被忽略
如果说数据链路和多卡通信是显性问题,那 CPU 调度与框架开销就是隐性问题。GPU 利用率低的时候,很多人会忽略这是 CPU 侧的资源竞争和框架等待造成的。具体现象包括:训练进程里每个 step 之间有一段明显的 CPU 等待时间;GPU kernel 结束到下一个 kernel 启动之间隔了上百微秒;或者训练刚开始时利用率有模有样的,跑着跑着周期性掉坑。
我抓住过一个实例:训练脚本里每一步都调用wandb.log同步上传指标,网络一抖动,GPU 就跟着停车等日志。这类问题特别容易在大型框架工程里出现,因为 DDP 的 hook、checkpoint 的保存、验证集的评估都是跟训练迭代同步执行的,任何一个环节卡住,都会反噬 GPU 利用率。
5.2 定位与修复
定位 CPU 瓶颈有几个趁手工具。执行htop看哪些 CPU 核被吃满;perf top看热点函数;py-spy dump --pid <训练进程>能直接打出 Python 线程的调用栈,平时遇到 Python 进程看起来卡死又不知道卡在哪,它是最快的现场抓拍手段。
修复层面,首先是给训练进程和日志、checkpoint 进程做资源隔离,比如日志传输放到独立进程异步执行,不要在训练循环内部做同步上传;checkpoint 保存的频率适度降低,或者改到另一个进程里做序列化。其次是验证集评估不要打断训练主循环,可以等一个 epoch 训完再做,或者用单独的进程去评估。还有一个小技巧,如果你发现 DataLoader worker 的 CPU 占用把训练进程的 CPU 挤没了,可以用taskset或者 cgroup 限定 worker 只能使用部分核,让训练进程永远有充足的 CPU 资源。
6. 瓶颈五:显存与资源配置
6.1 显存分配和碎片带来的隐性代价
显存的问题表面上不会直接显示成"利用率低",但它造成的连锁反应非常致命。最典型的是 OOM 后的重试机制:框架检测到显存不足,自动缩小下一次 batch size 重新执行,GPU 利用率就会随之大幅波动;还有 PyTorch 的缓存分配器虽然自带显存池,但如果训练代码里频繁创建临时张量、在循环内部反复分配内存,池子碎片化会导致显存峰值暴涨,进而触发不必要的清理和重新分配。
另一个常见场景是同一台机器上还有其他任务在抢资源。GPU 的显存和算力是两个维度:显存可能还剩不少,但另一个任务已经把 SM 占满,你的功率和时钟频率都会被压制。早期遇到这种问题,我一度以为是驱动调度出了问题,后来才发现是同机的另一个训练任务在抢算力,导致我这边的所有 kernel 都在排队。
6.2 定位与修复
先看nvidia-smi的进程列表,确认 GPU 上到底挂了几个进程,每个进程占了多少显存和 SM 份额。再看驱动层的 Xid 错误,执行dmesg | grep -i xid,如果出现错误代码 43 或者 GPU crash dump triggered 的记录,说明硬件或驱动层面已经在报错了,不是简单调参能解决的。
修复手段从代码侧做起:保证训练循环里不要有动态分配大张量的操作,尽量复用预先分配的 buffer;打开torch.backends.cudnn.benchmark=True让卷积工作区预分配。如果同机确实需要共享 GPU,可以考虑 MPS(Multi-Process Service)来切分算力,或者用 Kubernetes 的显存调度让请求和限制对齐,避免资源争抢。对于显存碎片问题,可以通过torch.cuda.memory._dump_snapshot分析分配记录,确认哪些中间张量在持续积累,然后针对性优化。
7. 瓶颈六:硬件链路与驱动配置
7.1 从驱动到 PCIe/NVLink 的隐形降速
最后一个瓶颈可能最让人头疼,因为它不是你改代码就能解决的——硬件链路和驱动配置。实际排查中,我见过 GPU 利用率低的原因竟然是 PCIe 链路降速:显卡原本应该跑在 x16 上,结果 x8 甚至 x4 的带宽在顶着,数据传输吞吐直接腰斩。还有供电不足导致的功率墙,显存温度过高导致的降频,这些信号的共同特点是 GPU 表面看起来一切正常,但性能就是上不去。
驱动层面的问题也不少见,比如错误代码 43 通常指向驱动与硬件不匹配或设备被系统禁用;gpu crash dump triggered 则说明内核驱动捕获到了 GPU 异常,训练会卡顿甚至中断。笔记本上还经常遇到双显卡切换的问题,比如 Intel UHD Graphics 和 NVIDIA RTX 4060 Laptop GPU 同时存在,训练时 CUDA 却跑在核显上,效率自然无从谈起。
7.2 配置与对策
排查命令比较固定:先检查驱动的运行状态nvidia-smi,再查内核日志dmesg | grep -i xid;确认 PCIe 链路状态用lspci -vvv | grep -E "LnkSta|LnkCap",很明显能看到当前速度和带宽宽度;温度与功耗用nvidia-smi -q -d CLOCK,TEMPERATURE,POWER查看,如果温度长期徘徊在 85°C 以上,大概率是散热压不住了。
对策分两手:一手是设置持久模式,nvidia-smi -pm 1,让 GPU 在训练间隙不掉入低功耗状态,避免反复初始化;另一手是锁频或限制功耗,比如nvidia-smi -lgc 1500,1600把频率固定在稳定区间,或者nvidia-smi -pl 250限制功耗墙。要注意,锁频这些操作在共享机器上可能与别人的任务冲突,自己独占卡做实验时再考虑。只要风扇、供电、机箱散热不解决,锁频也是权宜之计。
7.3 常见硬件报错速查
| 报错/现象 | 可能原因 | 初步排查方向 |
|---|---|---|
| Xid 43 | 驱动与硬件不兼容、显存故障 | 重装对应版本驱动,测试另一张卡 |
| GPU crash dump triggered | GPU 硬件异常或驱动 bug | 查看 dmesg 完整日志,跑稳定性测试 |
| 温度过高 | 散热硅脂老化、机箱风道堵塞 | 清灰、检查风扇转速 |
| PCIe 链路显示 x8 而非 x16 | 插槽物理问题或供电不足 | 重新插拔、更换插槽 |
| 利用率波动且时钟低 | 供电墙或功耗墙触发 | 查看 PWR 状态,考虑更换电源 |
这类问题还有个值得养成的习惯:每次重装驱动、换硬件或者升级 CUDA 版本,第一时间跑一遍同样的基准测试,把吞吐和利用率记录在案。否则环境变化导致性能回退,你永远不知道是哪一步引起的。
8. 综合排查清单:一张表定位你卡在哪儿
8.1 六步排查顺序与速查命令
排查的顺序建议从"成本最低"开始:先数据、再通信、再算子,然后是 CPU 调度、显存和硬件。顺序背后的逻辑是,数据管道错误在代码层最快能定位,硬件链路问题排查成本最高,放到最后。下面是综合整理出来的速查表,直接照着执行就行。
| 步骤 | 快速命令/工具 | 关键指标 | 经验阈值 | 恢复手段 |
|---|---|---|---|---|
| 1. 数据链路 | nsys profile、iostat -x 1 | CPU 端 gap 占比、磁盘 IO 利用率 | gap 占比 > 30% | 调高 num_workers,预处理离线化 |
| 2. 多卡通信 | nvidia-smi topo -m、NCCL_DEBUG=INFO | 加速比、NCCL kernel 占比 | 4 卡加速比 < 2.5 | 增大 batch,梯度累积,优化拓扑 |
| 3. 算子效率 | torch.profiler、ncu | 单算子 GPU 时间占比 | 自定义算子 > 40% | 算子融合,CUDA Graphs,混合精度 |
| 4. CPU 调度 | py-spy dump、htop | 主进程 CPU 使用、日志阻塞 | step 间隙明显 | 异步日志,隔离 worker CPU |
| 5. 显存资源 | nvidia-smi、memory snapshot | 显存峰值、进程抢占 | 显存波动 > 30% | 预分配 buffer,MPS 或调度对齐 |
| 6. 硬件链路 | dmesg、lspci -vvv | Xid 错误、PCIe 带宽、温度 | 温度稳定 85°C 以上 | 检查驱动、供电、散热 |
8.2 我验证过的几个典型场景
第一个场景是我自己踩过的:单卡 RTX 4090 跑 RAG 向量检索模型,GPU 利用率只有 30%,排查到数据管道时发现num_workers=0,而且向量化逻辑里有个自定义 collate 在主进程里做 CPU 推理。把 num_workers 调到 24、数据预处理提前到离线阶段后,利用率直接稳定在 92%。
第二个场景是帮一个团队排查 4 卡 DDP 训练,加速比只有 1.6。检查nvidia-smi topo -m后发现 4 张卡挂了两个不同的 PCIe Switch,每个 step 的 batch size 又只有 16,通信占了大头。改成 64 的 batch、叠了两次梯度累积后,加速比升到 3.5。
第三个场景是共享 GPU 机器上,另一个任务在凌晨自动启动,把我这边的显存和算力挤掉大半,但没有报任何错误。后来监控到功率和时钟频率同时下降才定位到是资源争抢。解决方式是统一用 k8s 管理资源配额,避免裸机上的任务互踩。
8.3 把排查沉淀成可持续的机制
排查一次只能解决当下问题,更长效的做法是把指标采集做成基础设施。把 DCGM 接入 Prometheus 和 Grafana,记录每个训练任务的 GPU 利用率、功率、温度、PCIe 吞吐,每次改动训练策略后跑一轮性能回归,把吞吐和利用率写进实验记录。这样下次再出现利用率低,不用重新抓现场,翻监控就能定位到大概率的瓶颈方向。
我个人在实际操作中的体会是:把 GPU 利用率当作"现象"而不是"目标"。利用率高但吞吐低,可能是 kernel 在空转;利用率低但吞吐稳定,可能只是数据管道有富余。最终要盯的目标只有一个——单位时间能完成多少有效训练量。按本文清单从数据链路一路排查到硬件链路,大多数利用率问题都会在第一个和第二个瓶颈处找到答案,剩下的才是算子级和硬件级的硬仗。