
把整卡 GPU 跑出 100% 的利用率大概是大多数搞训练和推理优化的朋友最想看到的画面。但现实里更多的情况是nvidia-smi 一查GPU 只吃了三成五成显存还有一半空着任务却在原地转圈报错不报死又不死你想定位它到底卡在哪却发现自己对整条数据链路一无所知。我做过的训练优化项目里有一半以上的时间不是在调模型结构而是在跟“GPU 利用率低”这个模糊症状死磕。这个坑深得很。这类问题难就难在利用率只是一个结果数字。它背后可能是 CPU 端数据处理跟不上可能是 H2D/D2H 的搬运太多也可能是一个模型里混进了大量只有几十微秒的小 kernel 导致调度开销完全压过计算。如果没有一套能“从外面”观测 GPU 内部行为的工具仅凭业务日志去猜效率非常低。所以这篇文章我想实际拆解一套零侵入的 AI Profiling 方案不改训练代码、不加探针、不打乱原有运行流程只通过采样和计数器拿到一手的 GPU 执行数据把利用率低这个黑盒问题拉回到可量化的白盒分析。这套思路对三种人最有用一是被领导追着问“为什么 A100 被你用得像个 1060”的算法工程师二是给团队搭训练底座、做资源调度的平台工程师三是在做推理服务性能调优、想搞清楚响应时间都耗在哪儿的 SRE。下面这些内容全部基于我在真实项目里跑过的定位流程和经验不强推工具只看它怎么帮我们解决问题。1. 先搞清楚利用率低到底是谁的锅很多人拿到低利用率的第一反应是换更大的机器或者调小 batch size但这些操作往往是隔靴搔痒。GPU 利用率低是一个系统的外在表现不是根因。要定位就得先把这条链路上的每一段拆开看。1.1 一个容易被忽视的事实nvidia-smi 看到的并非全部先说个最容易产生误判的点。用nvidia-smi看 GPU-Util 显示 40%并不代表 GPU 有 60% 的时间在摸鱼。nvidia-smi 的利用率采样是有时间窗口的默认采样周期是一秒左右而且这个数字更准确的说法是“在这个采样窗口内SM 上至少有一个 kernel 在跑的时间占比”。如果一个 kernel 跑 20 微秒然后空等 80 微秒采样器可能把它判定为低利用率也可能因为恰好命中的时间点不同而给出完全不同的数字。也就是说它可能高估也可能低估但它很难真实反映 GPU 内部的计算单元到底忙不忙。更典型的情况是你看到 GPU-Util 不高但 SMI 里的 Max Clocks 和温度又很正常说明 GPU 没降频但就是没活干。这时候问题几乎可以确定不在 GPU 本身而在上游的供给端也就是 CPU、内存、磁盘、网卡或者代码里的同步逻辑。我之前排查过一个数据并行训练任务四张 A100 的利用率长期在 30% 到 50% 之间波动。第一直觉是数据加载太慢。改大 DataLoader 的 num_workers 之后峰值提升了一些但还是有明显的周期性掉坑。后来用 Profiling 工具拉出时间线才发现每轮迭代开始都有一个接近 300 毫秒的空洞这个空洞恰好对应一次跨卡 AllReduce 同步。原来问题根本不在数据而在通信和计算没有重叠梯度同步期间整卡都在等。如果只盯着 nvidia-smi这个结论很难定位到。1.2 从时间维度看GPU 工作和空闲的真实比例真正想判断 GPU 忙不忙看的是“SM 上指令发出到完成的总时间”占整段的占比。这需要我们拿到 kernel 的 start time、end time、duration以及 SM 占用率。这些数据用 nvidia-smi 拿不到但几乎所有专业的 Profiling 工具都会给你。这里有一条非常实用的经验如果 kernel 的总执行时间占比超过 70%但 GPU-Util 这个数字却很低说明你只是被采样周期骗了实际上 GPU 一直在高频率地忙闲切换这种情况下要优化的不是计算本身而是减少切换开销。如果 kernel 执行时间本身就很短占比也很低那才说明真的没活喂进来这时候要看 CPU 侧在干嘛。所以我把定位低利用率问题的思路整理成一个决策列表先确认 GPU 有没有降频或供电限制nvidia-smi -q -d POWER再看 kernel 时间与空白时间的比例确认 GPU 是否真的有活儿如果 kernel 时间占比低去查 host 侧是否在同步等待、数据加载是否阻塞如果 kernel 时间占比高但 SM 利用率低去查 kernel 本身的规格是不是网格太小、线程数太少、occupancy 不够如果以上都没有问题再查显存带宽是不是瓶颈最后查多卡场景下的通信拓扑和重叠策略1.3 利用率低的不同表现特征对应不同根因GPU 空闲并不是一种表现而是很多种表现。我总结了四类最常见的情况它们的表象可能都是“利用率低”但定位方向完全不同。第一类周期性波浪形利用率。表现为 GPU-Util 在 0% 和 80% 之间来回弹跳周期稳定。这种十有八九是同步等待可能是每个 step 结束后的loss.backward()和optimizer.step()之后有个全局同步或者 DataLoader 的 prefetch 不足。处理方向是增加流水线重叠或者用异步数据加载。第二类长期低位平稳。GPU-Util 常年在 10% 左右看起来好像任务没在跑但日志又显示 loss 在下降。这种情况通常发生在模型很小但 batch size 很大的场景kernel 太短启动频率又低GPU 大部分时间在等待 CPU 发指令。需要看 CPU 侧的事件循环是不是有阻塞或者干脆把多个小算子融合成一个大 kernel。第三类接近某个低值不再变化。比如稳定在 25% 不动这往往是被某个外部资源锁住了。我在一个推理服务里遇到过 GPU 利用率稳定在 25% 的情况最后定位到是同一个 GPU 上跑了两个 CUDA 进程上下文切换太频繁导致 SM 资源被分摊。这种问题需要看进程级别的计算资源争抢。第四类高利用率但是任务吞吐低。这个更反直觉GPU-Util 显示 95%但每秒处理的样本数反而比之前更低。这说明 GPU 在忙但忙在做无用功最常见的是大量没有数据依赖的 kernel 在反复等待 CPU 触发或者显存带宽饱和导致计算单元排队。这种时候要看的不是利用率数字而是每个 kernel 的执行时间和实际计算量是否匹配。2. 为什么零侵入方案比插桩式 Profiling 更实用定位 GPU 利用率低的问题用不专业的工具会让你获得一堆噪声用侵入式工具又可能改变任务原本的行为特征。零侵入的意义在于它在不改动业务代码的前提下通过系统层面采集数据把“GPU 为什么闲”这件事具象化。2.1 插桩式方案的核心痛点测量的动作本身在改变结果很多开发者熟悉 PyTorch Profiler 这类工具它功能很强能精确到每个 tensor 操作的耗时能算 FLOPs甚至能看到内存分配事件。但它有一个天然的副作用需要在代码里加torch.profiler.profile上下文管理器或者设置环境变量这会对程序执行的节奏产生影响。这不是玄学。PyTorch Profiler 在开启时会启用 CUDA 的 activity tracing 机制每个算子执行前后都要来一次回调处理host 侧的工作量会明显增加。本来 CPU 侧可能刚好能跟上 GPU 的节奏一开 profilingCPU 处理 profiling 事件的开销直接拉低了数据供给速度GPU 利用率的实际表现反而被误导。我在一次压测里对比过同一个训练脚本开启 PyTorch Profiler 后 GPU 利用率直接从 85% 掉到 63%但这个 63% 并不代表线上真实情况。这就是“海森堡效应”在性能分析里的体现。插桩式还有一个现实问题有些业务代码根本不允许你随便改。生产环境的推理服务模型是固化在镜像里的你想加一行 profiler 上下文都可能触发审计还有一些老旧的定制框架用的是自定义的 CUDA 封装压根不暴露 torch.profiler 的接口。在这种情况下零侵入几乎是唯一能落地的方案。2.2 零侵入工具的工作原理采样、计数器、内核态观测零侵入 Profiling 工具之所以能做到“不改代码”是因为它走的路线完全不同。一般有两条技术路径。第一条路径是周期性采样。工具以极高的频率微秒级到毫秒级去读取 GPU 的硬件计数器包括 SM 活动率、指令吞吐、显存读写带宽、L2 命中率然后按时间轴聚合出热点区间。这类工具的代表是 DCGM、NVIDIA Nsight Systems 里的 GPU 指标采样器以及通过 CUPTI 实现的低开销分析工具。它不拦截代码也不记录单个 kernel 的完整生命周期当然Nsight 可以记录 kernel 时间线但那是另一套机制而是通过多次采样去逼近真实情况。第二条路径是内核态或驱动层事件追踪。因为 CUDA 内核的启停最终要过驱动工具可以在驱动层注册回调拿到每一次 kernel launch、每次 memory copy 的起止地址和时间戳。最典型的就是 CUPTI以及基于它的 Nsight Systems。这种方案只统计数据不修改代码避免了插桩带来的行为改变。我在实际使用中最推荐的组合是驱动层事件追踪 硬件计数器采样两者互相补充。事件追踪告诉你每段时间内在做什么计数器告诉你做这些事情时 GPU 内部的资源是否被喂饱。2.3 工具选型先搞清楚你的场景需要做到哪一层网上介绍 NVIDIA Nsight Systems、Nsight Compute、CUPTI、PyTorch Profiler、DCGM 的文章很多但真正到选型时很多人的困惑是不知道这些工具的分工。我按应用层到硬件层的递进关系把它们分开讲。第一个层级是云资源监控级用 DCGMData Center GPU Manager。这套工具可以在不侵入业务进程的前提下以很高的频率输出 GPU 利用率、显存带宽、温度、功耗、SM 占用率等硬件指标。如果你想解决的是“整个训练集群里哪个节点在浪费 GPU”这种问题它是最合适的。它不需要改代码部署形式是守护进程加 Prometheus 上报。第二个层级是单机性能剖析级用 Nsight Systems。它就是我前面说的驱动层事件追踪工具的代表。你可以直接用nsys profile --tracecuda,cudnn,cublas --outputreport python train.py这种命令获取 kernel launch、memory copy、CUDA API 调用、CUDA graph 等事件的完整时间线。它不侵入业务代码只加一个外部包装器。第三个层级是kernel 内部优化级用 Nsight Compute。这是到 SM 内部去看每条指令、每个线程束如何执行比如你有没有因为局部变量太多导致 register spilling有没有因为共享内存 bank conflict 导致性能下降。这个工具侵入性极低但采集开销比较大一般用于已经定位到具体 kernel、需要进一步优化时。第四个层级是框架自带探针PyTorch Profiler 这类。它的侵入性相对前两者更高但能拿到更高级的语义信息比如哪个 PyTorch 算子对应哪个 kernel。它适合在开发环境做功能验证不适合在生产环境长期跑。2.4 零侵入在不同场景下的落地方案我的实际经验是一个完整问题定位过程至少要动用到两个层级的工具。先用 DCGM 级别的监控发现“哪个时间段 GPU 利用率掉下去了”再用 Nsight Systems 级别的事件追踪定位“那个时间段里 GPU 在等什么”。举个例子我做过一个线上推理服务的排查。GPU 利用率总体不低但偶发抖动表现是 P99 响应时间突然从 20 毫秒变成 200 毫秒。如果用插桩式 profiler你需要改动线上代码而且不一定能复现偶发问题。最后我是在容器外部用 DCGM exporter 采集了 30 分钟 GPU 指标发现抖动的大多数时间是显存带宽跑到了 90% 以上但 SM 利用率只有 20%——也就是说推理的 bottleneck 不在计算而在数据搬运。于是把优化方向从调模型结构改成了优化输入预处理和 CPU 到 GPU 的拷贝路径P99 直接降了 10 倍。这个案例让我确定了一件事零侵入方案不光是一种工具选择更是一种定位思路。它强调的是先建立系统性的外部观察再决定要不要进入代码层面做精细调整。3. 核心思路拆解拿到 Profiling 报告后先看这四个指标工具最终输出的报告往往信息量巨大。一旦盯着一堆 kernel 的名字去逐条分析很容易被带偏。我建议不管用什么工具先按固定顺序看四个指标把定位范围快速缩小。3.1 指标一SM 利用率的分布而不是平均值很多人看“SM 利用率”只看一个平均数字比如 Nsight Compute 给出的 SOLSpeed of Light平均 45%。这个数字掩盖了很多细节。真正的关键在于分布SM 利用率是整体平滑的 45%还是长时间 90% 与长时间 0% 交替如果是前者代表你的 kernel 本身写得不饱满需要看 occupancy、内存延迟隐藏、指令级并行这些底层因素。如果是后者代表 GPU 的工作是不连续的经常被外部事件打断。这个分布信息在不同工具里的呈现方式不同Nsight Systems 的 GPU Trace 视图里你可以看到 SM 的活动密度随时间的变化DCGM 的 dcgm-stats 可以看到按时间窗口的利用率波形PyTorch Profiler 的 trace 则可以通过 kernel 时间轴的疏密程度来判断。我个人的判断经验是如果每个 kernel 自己运行时的 SM 占用率都超过 60%但整段时间线上 GPU 利用率只有 30%瓶颈就一定是“喂食速度”而不是“消化能力”。3.2 指标二CPU 和 GPU 之间的时间空洞Profiling 报告里最值得花时间研究的地方是 CPU 发起一个 kernel 之后到 GPU 真正开始执行这个 kernel 之间的时间差。在 Nsight Systems 里这就是 CPU gap前一个 kernel 结束到下一个 kernel launch 之间的空隙和 GPU gapkernel 提交到开始执行之间的等待一个小任务如果 CPU gap 占了总时间的 80%就根本不关 GPU 的事。这个时间空洞的形成原因通常有三个CPU 侧占用了太多时间在序列化操作上比如频繁调用.item()、.cpu()导致 GPU 同步等待PyTorch 的 autograd 引擎在 backward 阶段产生的 CPU 计算密集事件以及原生代码和 Python 之间的桥接开销。我的实际改进案例里有一个目标检测训练任务GPU 利用率 40%用 Profiling 工具一拉时间线发现每个 iteration 里有一个接近 120 毫秒的 CPU gap而这期间 GPU 完全是 idle 的。点进去看对应的线程栈发现是在loss.item()之后接了一个 Python 侧的 eval metric 计算这个计算完全使用了全套的 CPU 资源把下一个 batch 的预处理挤压到停顿。后来把 eval metric 的统计改成每 100 步做一次GPU 利用率直接提升到 75% 以上。3.3 指标三kernel 执行明细重点看前 20 个最耗时 kernel 的名字如果整体时间线上 GPU 确实在执行不少 kernel但利用率还是低那就需要看 kernel 级别的分析。我建议做一个排序后的 TOP 20 列表执行总时间最长的 20 个 kernel记录它们的名称、总次数、平均耗时、最大耗时、最小耗时。这个列表能很快暴露几种典型问题一是存在一个超大 kernel 反复执行它内部可能就有低效逻辑需要进 Nsight Compute 进一步看二是存在大量小 kernel比如超过 10000 次调用、平均耗时不足 10 微秒这提示你 kernel launch 的开销已经超过了计算本身应该考虑 CUDA Graph 或算子融合三是某一类 kernel 名称里带有memcpy并且占据了大量时间说明数据的搬运是主 bottleneck。我之前见过一个 Transformer 推理任务GPU 利用率只有 25%TOP 20 kernel 里清一色是memcpy_DtoH复盘后发现是 Python 端每隔几步就把 GPU tensor 转成 numpy用于打印和保存中间结果。这种问题如果你只看 SMI 的数字永远不会知道数据的搬运有多夸张。3.4 指标四数据搬运和 GPU 上下文切换开销数据传输在 Profiling 报告中通常以H2DHost to Device、D2HDevice to Host、Memcpy等标签出现。很多时候 GPU 利用率低不是因为它不忙而是因为它在搬运中忙。尤其是当模型一边在处理数据另一边DataLoader又在做 pin memory 和copy_同步操作时GPU 的 DMA 引擎与 SM 计算会抢占显存带宽。这类问题在推理场景比在训练场景更明显。训练时你通常在数据加载阶段把整个 batch 搬进去然后连续做前向和反向搬运的比例不高。推理时如果每个 request 都独立搬运一份数据尤其是 input 是小图但被 resized 成大图时H2D 的时间会远远超过 kernel 计算本身。我建议当 Top 20 kernel 里出现 Memcpy 或者 Memset 类 kernel 时先检查是不是可以批量拼接把多个样本拼成一个更大张量再搬运其次检查是否已经开启了 CUDA 的可锁内存映射如果没有用 page-locked memoryH2D 的吞吐可能只有实际带宽的三成。4. 零侵入 Profiling 的完整实操流程从命令行到报告解读讲了这么多理论接下来我必须把一套能直接抄的实操流程摆出来。环境是 Linux 服务器配 NVIDIA 驱动和 CUDA 11.x 以上跑的是 PyTorch 训练或推理脚本。因为要零侵入我不建议在代码里加任何 profiler 包裹全流程通过工具的命令行包装器完成。4.1 环境准备与可观测性预检查先确认工具是否就位。以最常用的 NVIDIA Nsight Systems命令是nsys为例安装方式一般是pip install nvidia-nsight-systems或者从 NVIDIA 官网下载 deb 包。安装好之后在 shell 里确认which nsys有输出。如果没有并且你不想装企业版工具也可以退而求其次使用nvidia-cuda-toolkit自带的nvprof但 nvprof 在较新的 CUDA 版本里已经不推荐使用遇到部分底层 API 无法 trace所以我个人建议直接上 Nsight Systems。然后是确认 DCGM 是否可以用来输出高频指标。DCGM 的安装一般要匹配 NVIDIA 的官方仓库装完启动dcgmi服务后在命令行执行dcgmi stats -v看有没有输出。这一步的可观测性预检查非常重要因为它决定了你待会儿是用哪一种数据源来定位问题。另外预检查一定要记录基线在不动任何 Profiling 工具的情况下nvidia-smi --query-gpuutilization.gpu,memory.used,power.draw,clocks.sm --formatcsv连续采集 30 秒把平均值记下来。这个基线可以帮助你在开启 Profiling 之后判断工具本身对性能的影响有多大。4.2 用 nsys 直接追踪一次任务零侵入的典型用法是这样的命令nsys profile --tracecuda,cudnn,cublas,osrt --outputtrain_report --force-overwrite true python train.py解释一下关键参数--tracecuda,cudnn,cublas,osrt分别追踪 CUDA API、深度神经网络库调用和系统运行时事件比如线程、IOosrt非常重要它能采集到 CPU 侧文件读取、锁等待、线程调度的事件帮助你判断 CPU 侧是否卡在 I/O 上。--output指定报告输出名--force-overwrite true是防止旧文件干扰。如果是推理脚本或者服务是常驻进程可以加--stop-on-exittrue --kill让它随进程结束。如果服务本身是无限循环需要先记录它的 PID然后用nsys profile --attach进程号做动态 attach。我实测下来这个命令几乎不会影响推理服务的正确性但它会采集大量的系统级 trace如果原始业务本身对 CPU 负载敏感启动 Profiling 前几分钟利用率会略有下降。所以如果你要复现偶发问题最好在无人使用的窗口期抓取并连续抓多轮再做对比。4.3 生成报告并快速定位问题区域nsys命令跑完后会生成一个.nsys-rep文件可以用 Nsight Systems GUI 打开也可以用命令行导出关键指标。我在服务器上的习惯是直接用命令行导出汇总信息nsys stats --force-overwrite true --report cuda_gpu_trace --report cuda_gpu_kern_sum train_report.nsys-rep这条命令会输出两份报告一份是 CUDA GPU 的逐条事件 trace包括每个 kernel 的时间戳、时长、进程名、线程名另一份是 kernel 的汇总统计会按总耗时给 kernel 排序。拿到这两份报告后我建议先不要进 GUI 的精细视图直接按上面说的四个指标快速过滤。第一步搜memcpy关键词统计所有 D2H/H2D 事件的总时间占比。没有实例代码的情况下你可以 grep 命令的输出文件grep -i memcpy report.txt | head -50如果 Memcpy 相关事件时间占比超过 20%先处理搬运问题。第二步按照 kernel 总耗时排序找出 TOP20 kernel。如果某一个 kernel 占据总 kernel 时间的 40% 以上把它记下来待会儿用 Nsight Compute 单独剖析。第三步看 CPU gap 的分布。在.nsys-rep文件里GPU Trace 视图会有 CPU 活动条和 GPU 活动条如果你没法开 GUI可以用 Python 解析 JSON 导出的事件文件计算每个 GPU 空闲窗口的长度和频率。4.4 如果问题在于具体 kernel 的低效运行如果报告显示 GPU 的 kernel 时间占比很高但 SM 利用率仍然低这个时候需要用 Nsight Compute 对单个 kernel 做深入剖析。同样是不改代码但通过在命令行指定内核名称来捕获ncu --kernel-name regex:matmul --launch-count 3 -o kernel_analysis python train.py这里的关键参数是--kernel-name regex:xxx它只对名字匹配的 kernel 开启精细计数器其他 kernel 的采集开销基本为零。--launch-count 3是因为同一个 kernel 的多次调用行为可能不一样取前三次样本会更稳定。ncu跑完后主要看几项Achieved Occupancy是否远低于理论值Warp Stall的分布情况是 memory 等待为主还是 long scoreboard 为主以及有没有Register Spill导致的 local memory 访问。我在大量项目里看到的情况是因为不熟悉 CUDA 的线程块调度把共享内存设置过大导致 occupancy 被压到 25% 以下整卡空有一堆硬件资源用不上。4.5 在一个真实场景里如何组合跑通这套流程拿一个我印象很深的 Bert 微调项目举例。任务是四卡 A100 做 Sequence Classification 微调GPU 利用率稳定在 35%。团队一开始以为是数据量不够又加了一倍数据结果利用率还是 35%且训练耗时几乎不变。我们先用 nvidia-smi 采了 5 分钟基线确认没有供电限制和降频。然后执行了nsys profile --tracecuda,cudnn,cublas,osrt --outputbert_fintune ...一分钟就生成了报告。快速翻看后发现 CPU gap 平均超过 250 毫秒而对应 GPU 上没有繁重的 kernel说明是 CPU 在持续拖后腿。进一步在osrt事件里查线程阻塞消息发现有不少文件读取等待。跑到strace去看才发现存在一个后台进程持续往同一块磁盘写日志和 checkpoint频繁的磁盘刷写占掉了 CPU 的 wait 事件导致 DataLoader 的 preload 不能及时跟上。后来直接换到 SSD 目录存储日志并且把 checkpoint 保存频率从每 500 步改成每 2000 步GPU 利用率立刻上到了 75%。整个过程没有改一行模型代码。这个案例想说明的是零侵入 Profiling 的意义不只是工具本身而是它给了你一个完整的证据链。你可以从 SMI 的外部症状逐步下钻到内核事件最后再落到 IO、线程、内存这些系统级细节。这种排查路径用传统插桩方式是很难走通的。5. 常见问题与排查技巧实录这些坑我替你踩过了工具会用不代表能解决实际问题。在真实调试过程中有一堆零零碎碎的小坑稍不注意就会让定位方向跑偏。我把踩过的和见过的几个高频问题整理成速查表。5.1 为什么 Profiling 启动后 GPU 利用率反而更低了这是最常被吐槽的一类现象。你先跑了一遍nsys发现报告中显示的 GPU 利用率比不带 profiler 时低了 10% 到 20%然后你就得出“Profiling 本身导致性能下降”的结论。大多数情况下这个结论是错的。真实原因是当开启--traceosrt时Nsight 会接管线程的创建和销毁、同步锁等系统事件它需要把这些事件记录到 trace buffer这个写入过程占用了一部分 CPU 时间。如果你的原始任务本来就处于 CPU 供给紧张的状态额外的一点点 CPU 开销会被无限放大GPU 自然就更没活干了。所以如果你用 Nsight 定位“CPU 型瓶颈”建议先只追踪 CUDA 事件不追踪 osrt减少干扰。命令如下nsys profile --tracecuda --outputcpu_side_clean python train.py有个更稳的验证方式连续跑三组。第一组正常执行第二组开 Nsight第三组用 DCGM 在外部采集指标。如果第二组的利用率显著低于第一组就说明瓶颈在 CPU 侧如果三组结果差不多说明瓶颈在 GPU 侧。5.2 基于采样的工具与事件级工具的结果对不上经常有人发现 DCGM 显示 GPU 利用率 60%但 Nsight Systems 显示 GPU 只有 30% 的活跃时间开始怀疑工具是不是坏了。解释这个差异关键在于定义口径。DCGM 的利用率是周期性采样的近似值而且是按 SM 上的某个活动信号来判断Nsight 的事件追踪则统计的是具体 kernel 的起止时间点。如果一个 kernel 本身很宽但 SM 没有始终在处理数据两个工具的数字自然会有偏差。所以建议不要把 DCGM 的数字当成绝对真相而是当成趋势信号。如果要判断瓶颈永远以事件级的时间线为准如果要监控整体水位用 DCGM 采样是比较够用的。5.3 多机多卡场景下Profiling 报告乱了套当你在多机多卡环境里跑 Profiling单机nsys打开的报告会把同一台机器上的所有 GPU 事件混在一张时间线里如果你只关心其中一张卡就需要在 GUI 里筛选 device id。如果你需要在多台机器上同时采集推荐在每台机器上各自跑nsys然后生成独立的报告避免网络时钟偏差带来的混淆。跨机器的时钟同步本身就是一个误差源不要试图用时间戳对比两台机器的先后关系。补充一句多卡场景里最容易漏的问题并不是哪张卡利用率低而是某张卡的进度被其他卡拖住整体都慢。这种情况下还是需要看通信事件。Nsight 里可以追踪 NCCL 的 trace需要额外把--tracenccl加进去。我在做大模型训练调优时经常从通信事件里发现某台机器网卡中断分布不均匀导致 AllReduce 等待时间被拉长这比单纯看 SM 利用率有价值得多。5.4 遇到 CUDA graph 或自定义内核的时候怎么处理如果你的任务用了torch.cuda.graph或 Triton 这种自定义 kernelNsight 的默认 trace 有时候看不到里面的 kernel 名字只能看到graph或kern_launch类的事件。这时候不要慌改用 Nsight Compute 直接对 kernel name 做正则匹配或者用CUDA_LAUNCH_BLOCKING1来强制同步。但注意CUDA_LAUNCH_BLOCKING1会彻底改变你的执行行为它会强制每个 kernel 执行完后才返回 CPU利用率会瞬间掉到接近 0%。它只适合用来验证某个 kernel 是不是在报错不适合做性能分析。想对 CUDA graph 做性能分析更稳的思路是关掉 graph 启用前一次的普通 eager 执行模式用它能拿到每个 kernel 的完整明细分析完再开回 graph 去部署。6. 一个还没被足够重视的小技巧把 Profiling 沉淀成例行巡检最后分享一个我用了很久的日常习惯。我不会等到 GPU 利用率低了才去开工具而是在每次训练任务启动之后自动在后台跑一轮轻量 Profiling把生成报告的时间戳、训练步数、GPU 利用率基线保存成一个带日期的文件。当团队里有人说“最近训练好像变慢了”我不是从头去分析而是拿出昨天和上周的报告从时间线上对比很多问题一眼就能看出规律。具体实现很简单写一个 wrapper 脚本来调度# run_train_with_monitor.sh nsys profile --tracecuda,cudnn,cublas --outputprofile_$(date %Y%m%d_%H%M) python main.py把脚本放进 cron 或训练调度器里跑完自动归档。时间长了你就会积累一批性能基线比纯靠记忆更可靠。另一个更省事的方向是参考 DeepSpeed 和 Megatron 等框架自带的 profiling 日志功能它们通常有--profile-step之类的参数但那些工具偏训练专用对推理场景覆盖有限。零侵入工具的好处是通用能和任何业务共存。并不是每个项目都值得动大手术去重构代码很多利用率低的问题找到瓶颈后只需要改几个配置就能把资源利用率拉上很大一截。我个人的体会是遇到 GPU 利用率低不要急着怀疑模型先跑一轮零侵入 Profiling让数据告诉你答案。这套方法救了我很多次要交付的节点也希望它能帮你从这种最恼人的问题里脱身。