GPU利用率低难定位?零侵入AI Profiling工具链实战解析

发布时间:2026/10/4 12:40:42
GPU利用率低难定位?零侵入AI Profiling工具链实战解析
搞深度学习训练的人应该都经历过这种诡异时刻nvidia-smi里好几张卡显存被占得满满的可用率却只有百分之二三十训练任务也不报错loss 还在一点点往下掉。你想定位这 GPU 利用率为什么这么低盯着那个百分比看半天什么都看不出来。GPU 利用率低的问题最难的从来不是优化而是定位。训练流水线上任何一环拖后腿最后都会表现为 GPU 在等可nvidia-smi只告诉你它在等不告诉你在等谁。我也不是第一次踩这个坑所以这阵子专门在团队里搭了一套零侵入的 AI Profiling 工具链不改一行train.py不重新编译不重启训练用 DCGM-Exporter、Nsight Systems、Prometheus 这几个组件把训练过程一层层拆开看。按这套办法跑下来GPU 利用率低就不再是个模糊的症状而是可以被拆到具体环节的可定位问题。下面把我的选型逻辑、部署过程、真实定位案例和避坑记录展开讲一遍。1. 为什么 GPU 利用率一低就让人摸不着头脑1.1 nvidia-smi 的利用率只是一个“转速表”nvidia-smi里的GPU-Util是驱动在当前采样周期内GPU 上有 kernel 在 SM 上执行的时间比例。这句话很多人没细想。它不代表 GPU 算力被用到了多高也不代表正在执行的 kernel 本身有多高效。它只回答一个问题这段时间里 SM 有没有在干活。举个例子一个训练 step 由 300ms 数据加载、100ms kernel 执行组成那么这 400ms 内 GPU 利用率就是 25%。nvidia-smi会告诉你 25%但它不会告诉你那 300ms 是在等文件读取还是在等另一个 rank 的 all-reduce更不会告诉你那 100ms 的 kernel 内部有没有因为访存冲突而效率减半。把 GPU 利用率想成汽车转速表会更直观。转速低不一定代表发动机坏了可能是离合没合上、供油不足、变速箱换挡逻辑不对。只看转速表永远定位不了真正的故障必须顺着动力链路一层层查。GPU 训练也是这个道理GPU-Util只是全局健康度指标不是病灶地图。还有一个常见误区是拿显存利用率推计算利用率。显存被占满不一定代表 SM 在满负荷计算参数、激活值、CUDA context、临时缓存都会吃显存。所以GPU 利用率低 显存占用高很常见但它不代表显存就是瓶颈。1.2 低利用率的根因有七个常见方向这七个方向我基本都遇到过没遇到过的也听同事提过数据加载与 CPU 预处理慢DataLoader 读取文件、图片解码、数据增强、tokenizer、JSON 解析这些全在 CPU 侧。CPU 一旦跟不上去GPU 只能干等。这是最常见的原因。kernel 过碎、启动开销占比高一次算力很小的计算也走一次 kernel launch几千个小 kernel 排队GPU 大部分时间在切换和等待而不是计算。同步与通信等待PyTorch 里tensor.item()、cudaDeviceSynchronize、分布式训练里的 all-reduce任何一端慢下来GPU 都会出现明显空档。GPU 被多个任务共享抢时间片生产环境经常一张卡同时跑着不同任务或者同一物理 GPU 上多进程并发利用率就会被摊薄。数据搬运瓶颈CPU 到 GPU 的 H2D、GPU 到 CPU 的 D2H、PCIe 带宽和 NVLink 带宽跟不上kernel 之间会插进大量 copy 时间。功耗与温度降频GPU 功耗被限制或者温度过高触发降频SM 明明在跑但时钟变低利用率数字可能不低实际吞吐却上不去。资源配额与调度限制MIG 切分、容器显存限制、cgroup 限制、调度器把单卡分给了多个任务都会导致 GPU 看起来没吃饱。真实生产环境里 GPU 利用率低很少是单一原因多数是两三个因素叠在一起所以靠一条命令、看一个指标根本不够必须有分层证据链。1.3 常规监控手段的三大盲区用nvidia-smi或云厂商监控平台看 GPU最容易撞上三个盲区。第一个盲区是秒级聚合掩盖了 step 内的波动。训练典型节奏是“CPU 准备数据 - GPU 执行 kernel - 通信等待 - 下一轮”。如果每个 step 是 1 秒GPU 只在其中 300ms 忙聚合出来就是 30%。可这 30% 背后的真实形态可能是0.7 秒完全空闲、0.3 秒打满。只看平均值你永远不知道 GPU 是“匀速低效”还是“剧烈波动”。要看到真实形态必须有连续曲线和事件时间线。第二个盲区是指标没有上下文。监控里显示第 30 秒利用率低但这一刻训练跑到了第几个 step是 loss 下降正常的阶段还是梯度累积等待阶段是预热阶段还是正常迭代阶段普通监控完全给不了答案。没有上下文根因就只能靠猜。第三个盲区也是最致命的是普通监控没有事件级信息。它只告诉你 GPU 忙不忙不告诉你在当前时间窗口里跑的是什么 kernelkernel 名叫什么、耗时多少、是访存密集还是计算密集、是 CPU 启动慢还是 GPU 执行慢。没有这些事件所谓 profiling 就只是“看脸色”而不是“做诊断”。2. 零侵入工具链怎么选为什么是 DCGM Nsight 这套组合2.1 侵入式 profiling 和零侵入 profiling 的区别很多人一说 profiling 就想到torch.profiler没错它很好用能拿到算子耗时、显存分配、甚至 tensor shape但它有两个硬伤第一要改代码第二对正在运行的训练任务几乎没法直接上。你不大可能为了定位问题就重启一个已经训练到一半的大模型任务然后再塞一段 profiler 进去。零侵入 profiling 的含义我理解是不改训练代码、不重新编译、不改训练启动脚本里的 Python 逻辑。Nsight Systems 和 DCGM-Exporter 都是外部工具前者用命令行把训练命令包一层后者直接在后台旁路采集 GPU 指标。对训练进程而言代码是透明的。两者对比如下方法是否改代码能拿到什么生产友好度torch.profiler / CUDA events需要算子耗时、shape、内存分配低需要改代码重启任务Nsight Systems不需要改代码但会注入 CUPTI系统时间线、CUDA API、kernel、通信、CPU 调用中适合短时间抓取Nsight Compute不需要改代码但开销很大kernel 内部 stall、吞吐、占用率低适合定点下钻DCGM-Exporter Prometheus零接触秒级 GPU 利用率、温度、功耗、时钟、显存高适合长期持续观察这里要澄清一点“零侵入”不等于“零开销”。Nsight Systems 和 Nsight Compute 为了拿到事件会注入 CUPTI 并显著影响训练速度尤其是 Nsight Compute整个训练会变慢几倍到几十倍。真正能做到低开销长时间挂在训练旁边的是 DCGM-Exporter 这条旁路。所以工具链要搭配着用不能只迷信某一种。2.2 这套工具链的组件分工我在团队里最终落地的组合不是某一个“神奇软件”而是一条从系统到 kernel 的取证链路。DCGM-Exporter 负责持续监控 GPU 整体状态包括利用率、显存、温度、功耗、SM 时钟、显存时钟、内存 copy 引擎利用率。数据和 Prometheus 对接再丢进 Grafana可以画连续曲线用来发现“什么时候低”“低到什么程度”“有没有周期性”。Nsight Systems 负责事件级时间线。它告诉你在某一分钟里CPU 在跑哪些系统调用CUDA API 是什么时候发起的kernel 在 GPU 上是从什么时间跑到什么时间D2H/H2D 传输占了多久。这是把“GPU 利用率低”和“到底哪一段在等”之间搭桥的关键工具。Nsight Compute 负责 kernel 内部微架构分析。如果时间线显示某个 kernel 本身耗时就异常你就值得用 Nsight Compute 下钻看它到底是访存瓶颈、计算瓶颈、占用率不足还是 warp stall 在等什么数据。最后再加两个辅助工具dstat和py-spy。前者看 CPU、磁盘、网络、内存变化后者直接采样 Python 进程当前执行栈。它们都和训练代码无关不修改代码却是定位数据管线问题时的决定性证据。打个比方DCGM 是医院的住院监测仪告诉你体温血压一直在波动Nsight Systems 是全身 CT告诉你那段异常波动的时间线上器官是怎么运作的Nsight Compute 是高倍显微镜专门盯着可疑的那块细胞看。辅助工具则是化验单。四者凑齐诊断才敢下。2.3 为什么零侵入在生产环境特别吃香理由很简单不要求用户配合不停任务不碰正在跑的代码。做 MLOps 或平台工程师的人最能体会。算法团队那边训练跑得好好的突然 GPU 利用率很低你不会希望因为 profiling 就去改代码更不愿意把训练停了重启。你希望像给服务器做体检一样把一个“采集器”挂上去等几分钟数据然后拿着证据去和算法同学对话。零侵入工具链恰好服务这种协作模式。另一个好处是训练框架无关。不管对方用的是 PyTorch、TensorFlow 还是 PaddlePaddle只要 GPU 是 NVIDIA 的DCGM 这一层都能采。Nsight Systems 对 CUDA 事件做记录也不关心上层是哪种框架。这意味着工具链可以在整个集群复用不用每个项目单独适配。这套组合尤其适合刚配置好 GPU 版 PyTorch、第一次在租来的多卡机器上微调大模型、或者正在排查 GPU 调度问题的场景。很多初期问题并不是 kernel 本身写得差而是环境、数据管线、通信链路没通零侵入工具能在不打扰训练的情况下先把这些问题筛掉。3. 实操5 步把零侵入 Profiling 工具链跑起来3.1 第 1 步部署 DCGM-Exporter 并接入 PrometheusDCGM 是 NVIDIA 数据中心 GPU 管理器的缩写DCGM-Exporter 则是把 DCGM 采集到的指标转成 Prometheus 格式。它默认监听 9400 端口返回/metrics文本。最省事的部署方式是用容器docker run -d --gpus all --rm \ --cap-add SYS_ADMIN --cap-add SYS_RESOURCE \ -p 9400:9400 \ nvcr.io/nvidia/k8s/dcgm-exporter:version镜像 tag 按环境实际情况拉取。启动后先在宿主机验证curl -s http://localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL如果能看到返回比如DCGM_FI_DEV_GPU_UTIL 75说明采集链路已经通了。接下来把它告诉 Prometheus在prometheus.yml里加一段 scrape 配置scrape_configs: - job_name: dcgm static_configs: - targets: [gpu-host-01:9400]Prometheus 默认 15 秒拉一次对 GPU 利用率这种变化频繁的指标够用。如果训练 step 很短想要更细的曲线可以把scrape_interval调到 5 秒。最后在 Grafana 里导入一个 NVIDIA DCGM 官方 Dashboard或者自己建几个关键面板。Grafana 里我最常看的面板是GPU Utilization、SM Clock、Memory Clock、Temperature、Power Usage、Memory Copy Utilization。这几条曲线别分开看要叠在一起。利用率低但温度也低、功耗也低说明 GPU 确实在闲等利用率低但温度高、功耗高则是另一个故事。注意容器方式跑 DCGM-Exporter 经常遇到权限问题表现是没有指标返回或者NVML_ERROR_INSUFFICIENT_PERMISSIONS。如果加了--cap-add SYS_ADMIN --cap-add SYS_RESOURCE还不行先换--privileged验证一轮确认是权限问题再按安全规范收紧。3.2 第 2 步用 Nsight Systems 抓一段训练时间线Nsight Systems 的安装和 DCGM 不同它更像个“启动器”把原来的训练命令前缀加上nsys profile就行。不需要改代码但启动过程会慢。我常用的命令是nsys profile -t 60 --tracecuda,nvtx,osrt -o gpu_train \ python train.py --epochs 1-t 60表示最多采集 60 秒--tracecuda,nvtx,osrt分别跟踪 CUDA 事件、NVTX 标记和操作系统运行时调用-o gpu_train指定输出报告名。跑完后会生成一个gpu_train.nsys-rep文件用 Nsight Systems GUI 打开。打开后重点看两类轨道一类是 GPU 上的 kernel 条带另一类是 CPU 上的 CUDA API 调用。GPU 轨道上出现大片空白说明 GPU 在等CPU 轨道上也在等那多半是数据没来GPU 轨道上 kernel 排得密密麻麻但单个都很短说明问题可能在小算子启动开销上CPU 轨道上 CUDA API 调用和 GPU 实际执行之间有明显时间差则可能和调度延迟有关。Nsight Systems 本身开销不低训练速度会变慢这是“事件级证据”的代价。所以我一般只让它跑 30 到 60 秒或者干脆让它只跑一两个 step。记录采集开始时刻可以这样date -u %Y%m%d_%H%M%S后台训练再配合 Prometheus 连续监控取证窗口就能对齐。3.3 第 3 步CPU 侧和 GPU 侧一起采样别只盯 GPU当年我第一次用 Nsight Systems 抓完时间线发现 GPU 确实有大段空闲但不知道空闲时 CPU 在干嘛。后来加了三样东西才把证据补齐。第一样是临时性的 GPU 实时采样nvidia-smi --query-gpuutilization.gpu,memory.used,memory.total,temperature.gpu --formatcsv -l 2 gpu_smi.log 第二样是系统资源采样dstat -c -d -n -m 2 sys.log 第三样是 Python 进程栈采样py-spy record -o cpu_spy.svg --pid $(pgrep -f train.py) --duration 60py-spy不修改代码只对 Python 进程做线程栈采样最后生成一张火焰图。它能直接告诉我主进程这段时间卡在哪个函数里是 DataLoader 的next()是 tokenizer还是某个 CPU 计算。很多“GPU 利用率低”问题靠这张火焰图五分钟就能锁死真凶。注意py-spy想 attach 到训练进程最好在训练进程所在的容器环境里运行。宿主机直接附件容器内进程会因为 pid namespace 和 ptrace 权限问题失败。遇到失败就进容器找主管道进程的 PID再跑一遍py-spy record。3.4 第 4 步把时间线、利用率和 CPU 栈对齐起来工具都跑完后证据是散在各处的怎么对齐是关键。我的做法是所有采集都按 UTC 时间记录窗口比如某次排查取的是2025-06-10 03:00:00到03:01:00这一段。DCGM 数据在 Grafana 里选这段区间Nsight Systems 报告里定位同一段py-spy 火焰图按 60 秒持续时间切分。对齐后可以采用“四看”快速定位看到了什么更可能是什么下一步动作GPU 利用率低且有长 GPU 空闲数据供给或 CPU 侧慢看 py-spy 火焰图和 dstatGPU 利用率低且时间线上 kernel 密集但极短小 kernel、启动开销占比高看 kernel 时长分布考虑合并算子或 CUDA GraphGPU 利用率呈周期性锯齿多卡同步节奏一致集合通信等待查 NCCL 调用、NVLink、网络延迟GPU 温度高、功耗高但利用率低降频或功耗限制查温度墙、风扇、power cap 配置这套“先看时间线、再追 CPU 栈”的流程比直接猜答案效率高很多。我也遇到过时间线一打开就发现 GPU 空闲间隙非常有规律的情况基本可以断定数据和 step 之间是串行依赖接下来只需要顺着 CPU 侧继续深挖。3.5 第 5 步下钻到 kernel什么时候用 Nsight Compute如果时间线显示某个 kernel 本身就特别慢比如一个正常的 LayerNorm kernel 跑了同类任务的几十倍时间这时候要用 Nsight Compute 看 kernel 内部发生了啥。命令示例ncu --section ComputeWorkloadAnalysis --section MemoryWorkloadAnalysis \ --launch-count 5 -o ncu_report python train.py--launch-count 5限定只采集前 5 次 kernel launch避免整个任务被拖垮。Nsight Compute 会给出 kernel 的占用率、计算吞吐、访存吞吐、warp stall reason 等信息。如果 Memory Throughput 接近 100%基本是访存瓶颈如果 Compute Throughput 已经很高说明这个 kernel 已经优化到一定程度再看问题没意义。Nsight Compute 的输出非常专业但也非常“贵”所以一定在一个小脚本或单卡小 batch 上跑不要直接挂到整个训练任务上。它和 Nsight Systems 的定位完全不同Nsight Systems 回答“时间去哪了”Nsight Compute 回答“某个时间片里为什么那么慢”。4. 两个真实案例复盘从 33% 到 85% 的定位过程4.1 案例一8 卡微调任务 GPU 利用率常年在 33%背景是一个开源基座模型用 LoRA 做指令微调8 张 A100训练脚本不报错loss 正常下降可 GPU 利用率平均只有 33%。算法同事先看了nvidia-smi说 GPU 利用率低但显存占用 65%怀疑是显存不够导致部分算子被换出我听着就觉得方向错了。我先挂上 DCGM-Exporter 看了 10 分钟曲线。结果很有规律GPU 利用率像锯齿一样上下抖动波峰能到 60%波谷掉到 10% 以下周期大约 2 秒。接着用 Nsight Systems 抓了 60 秒时间线发现每个训练 step 都有大约 300~500ms 的 GPU 完全空档空档后面紧接着一阵密集 kernel 执行。这说明 GPU 不是在慢慢算而是在“等”等的那段时间里 GPU 上没有任何 kernel。然后看 py-spy 火焰图主进程大量时间集中在 DataLoader 的_MultiProcessingDataLoaderIter和 tokenizer 相关函数再看 dstat磁盘读和 CPU 系统态都偏高。结论已经很清楚瓶颈在数据管线不在模型 kernel。优化动作分两步走。第一步把训练数据集先做一次离线 tokenize存成内存映射格式避免每次迭代重复做文本编码第二步把 DataLoader 的num_workers调整到 GPU 数量的合理倍数打开prefetch_factor4和pin_memoryTrue并把缓存目录放到本机 NVMe。这一步做完GPU 利用率从 33% 提到了 68%。紧接着我又拉了一次 Nsight Systems发现空闲问题基本消失但 GPU 轨道上出现大量 1 到 3 微秒的小 kernel很多是 elementwise 和 attention 计算里被拆得很碎的算子。这个阶段用torch.compile和 CUDA Graph 的抓手做算子融合最终把利用率拉到了 85% 左右。后面没有再继续压因为已经开始受功耗和通信影响收益不如前面大。4.2 案例二多机多卡利用率低不是单卡问题另一个场景是 32 卡分布式训练所有 rank 的 GPU 利用率都忽高忽低从 Grafana 看每张卡的利用率曲线几乎同步锯齿。同步锯齿本身就是一个信号如果是数据加载问题不同机器、不同 worker 的节奏应该有一些错位现在大家同时掉到谷底更像是在等一个全局同步点。用 Nsight Systems 带nccltrace 再看发现每个 step 后面都有耗时的集合通信调用而通信阶段 GPU 基本空闲。进一步检查网络链路发现部分节点跨交换机走时延明显更高而且网卡中断没有绑到合适的 CPU 核上。调整了通信 backend 的环境变量、把网卡中断绑定到 NUMA 就近的 CPU 核以后同步等待时间明显缩短。这个案例给我的教训是多卡场景里“GPU 利用率低”可能不是任何一张单卡的问题而是整条通信链路的短板效应。只看单卡利用率反而容易被局部指标误导。4.3 案例复盘中的通用经验复盘下来定位 GPU 利用率低不能只信一个指标。利用率低是结果原因可能在 CPU、在磁盘、在网络、在功耗、在算子设计甚至在调度器。零侵入工具链最核心的价值就是先把所有证据摆到同一个时间轴上再得出结论。另一个经验是每次优化只改一个变量。改完 DataLoader 就重新抓一次 Nsight Systems 和 DCGM 数据确认 GPU 空闲是不是真的缩短了然后再去动 kernel别一口气把数据、通信、算子全改了最后无法定位是哪步真正起效。5. 常见问题与避坑清单5.1 工具采集不到数据先别怀疑训练代码现象常见原因解决思路DCGM-Exporter 容器无指标返回容器缺 NVIDIA runtime 或权限加--gpus all和--cap-add SYS_ADMINDCGM 在 MIG 模式下指标异常MIG 需要单独配置实例映射检查 MIG 模式和 exporter 版本兼容性nsys 报 CUPTI 权限错误容器缺少 CAP_SYS_ADMIN加权限或用宿主机直跑 nsysPrometheus 拉取不到 9400 端口容器端口映射或网络策略问题先在宿主机 curl 验证再查服务发现py-spy 无法 attachPID namespace 不同或权限不够进入容器内跑 py-spy用主管道进程 PID有一次我在 Kubernetes 环境里部署 dcgm-exporterPod 起来了但 Grafana 一直没数据。查到最后是 exporter 容器里没有访问 NVML 的权限加回cap-add才恢复。这种问题很像“GPU 监控坏了”但实际只是采集器权限配置不对。5.2 指标解读的常见误判第一个误判是“显存利用率高 显存瓶颈”。前面说过显存高可能只是 CUDA context、激活缓存、参数占用不代表显存带宽不够。要确认显存瓶颈最好看 Memory Copy Utilization 和 kernel 内的 Memory Throughput。第二个误判是“单次采样低 一直低”。训练 step 内部波动可能非常剧烈一次nvidia-smi采样正好撞到波谷不代表整体利用率低。所以我坚持先看连续曲线再看时间线不要拿一张静态截图下结论。第三个误判是“GPU 利用率低 优化不到位”。很多情况下模型没问题是数据管线、通信链路、功耗策略有问题。拿零侵入工具链筛一遍后再谈 kernel 优化才不容易白忙。第四个误判是“profiling 工具跑出来的慢和线上慢一样”。Nsight Systems 和 Nsight Compute 都会改变任务速度尤其 Nsight Compute 会让 kernel 重放或大幅变慢。所以在确认优化效果时一定要在“关掉 profiler”的正常模式下跑否则会把工具开销当成真实性能。5.3 采集节奏与开销控制长期监控用 DCGM-Exporter 加 Prometheus秒级或 5 秒级抓取没问题。Nsight Systems 建议一次只跑 30 到 60 秒抓关键窗口。Nsight Compute 建议只跑一个小复现脚本或者用--launch-count限制 kernel 数量。采集工具本身也会占用 CPU尤其 Nsight Systems 在启动阶段会加载 CUPTI 库明显拖慢训练启动不要开着跑全量训练。还有个小技巧在训练代码里加 NVTX 标记虽然已经不是严格意义的零侵入但对 Nsight Systems 时间线的可读性提升很大。建议在正式优化阶段在 DataLoader 迭代、前向、反向、优化器 step 这几个边界处加简单的torch.cuda.nvtx.range标记。只要你接受“注入少量 profiling 标记”时间线会从几百个抽象调用变成清晰的阶段条带定位效率直接翻倍。6. 什么时候我仍然会回到侵入式 Profiling零侵入工具链再顺手也有边界。第一个边界是算子级 shape 和内存分配细节。比如你想知道某个算子的输入张量 shape 是不是因为 padding 导致显存翻倍或者某个算子在 forward 过程中临时分配了多少 CUDA 内存这些torch.profiler能直接给Nsight Systems 给不了。第二个边界是单个 kernel 内更深层的性能归因。Nsight Compute 其实已经能通过外部注入拿到多数指标但它对运行环境要求高重启和重放成本也高。如果你已经在做向内存布局、算子切分、CUDA Graph 之类细粒度优化浸入式工具往往更适合做回归对比。第三个边界是与算法团队的协作流程。算法同学在本地小数据上开发时用torch.profiler打印一段算子耗时比拉起完整工具链更轻。而一旦上了生产集群、跑大规模训练零侵入工具链反而是更安全的起点。我建议的工作流是先零侵入后侵入式。先用 DCGM 连续监控发现异常窗口再用 Nsight Systems 找到上下文用 py-spy 补 CPU 侧真相用 Nsight Compute 下钻可疑 kernel。如果证据已经指向某个具体算子和具体场景再写一个最小复现脚本用torch.profiler做精细对比。这样既能照顾生产环境的安全性也能满足最终优化时的精度需求。最后说一个我自己的习惯拿到任何一个“GPU 利用率低”问题我先不打开训练代码而是先把这套零侵入的采集流程跑一遍。绝大多数时候证据会在十分钟内指向一个具体方向比如数据管线、通信等待或者 kernel 过碎。真正动手改代码反而是最后一步。工具链听起来有点多实际串起来就是“监控指标找窗口、时间线找上下文、CPU 栈找真凶、kernel 分析找细节”熟练以后半小时能跑完一轮。希望这套思路也能帮你少熬几个夜。