多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

GPU利用率低?用Nsight Systems零侵入定位瓶颈

GPU利用率低?用Nsight Systems零侵入定位瓶颈 上周隔壁团队找我排查一个训练变慢的问题A100的GPU利用率只有22%显存占用却居高不下loss曲线也正常可每个epoch就是跑不停。代码里的DataLoader配置翻来覆去调了好几次始终没有实质改善。我试着在训练脚本里加一行with torch.profiler.profile(...)结果对方说这是线上定时任务不想改代码。于是我把方案换成了零侵入的AI Profiling工具——不需要动任何业务脚本只在外部启动一次profiling把CPU、CUDA API、GPU kernel串成统一时间线不到十分钟就定位到真正卡点。这篇文章就围绕这套思路展开介绍如何用Nsight Systems这类零侵入AI Profiling工具从监控数字到时间线一步步定位GPU利用率低的根因并给出我实测中常见瓶颈与对应优化动作。适合用PyTorch/TensorFlow写训练、维护GPU服务器、以及总是被“GPU利用率上不去”困扰的读者。很多“疑难杂症”其实不是GPU本身的问题而是GPU旁边那些配套设施在拖后腿。1. GPU利用率低到底是被什么卡住了从监控数据说到时间线1.1 nvidia-smi里的利用率只是个“平均结果”GPU利用率估计是很多人最先看的数据。命令行输入nvidia-smi看到GPU-Util 98%就安心看到20%就焦虑。但这个数字本质上是GPU硬件在采样窗口内执行kernel的忙闲比例更像一个“平均结果”。它只是告诉你GPU有多忙不告诉你GPU在忙什么。比如某个100ms窗口里GPU实际执行了30ms的kernel剩下70ms完全闲着nvidia-smi会显示接近20%~35%的利用率但你完全看不到那空白的70ms到底在等什么是等数据从CPU搬过来是等CPU把下一个batch算好还是kernel排队没轮到它这就是为什么很多人对着nvidia-smi看半天也定位不了根因。监控数值只能证明“瓶颈不在GPU计算能力上”无法指出瓶颈具体在哪。要搞清楚那70ms去哪了就必须把程序执行过程中发生的事件全部记录下来CPU在某个时刻调用了什么函数CUDA API在什么时候被调用kernel在GPU上什么时间开始什么时间结束数据传输占了多少时间。把这些事件按时间对齐就是AI profiling的核心工作。1.2 三种典型的“闲置模式”结合我排过的案例GPU利用率低的情况大致可以归成三类CPU喂不动GPUGPU在等CPU准备数据时间线上表现为GPU kernel之间有长空白而CPU侧的数据预处理函数占用很高。常见于DataLoader配置不合理、图像解码太慢、样本读取走网络等。数据搬运占大头每个batch在CPU与GPU之间传输的时间比GPU计算时间还长。典型特征是kernel本身不长但前后总有很长的cudaMemcpy HtoD或DtoH。kernel碎片化GPU一直在执行但执行的是大量细碎的小kernel中间夹杂着启动开销和同步等待看起来利用率不低但真正的“有效计算”只有一小部分。用流水线打比方GPU利用宰低可能是原料车间没及时送料可能是叉车运力不足也可能是机床把大量时间花在切刀和换工件上。三个问题的优化方式完全不同——分别是加大缓存、优化搬运路径、减少换刀次数。AI profiling要解决的就是把这三种情况从“都是GPU利用率低”中区分出来。1.3 为什么我要强调“零侵入”传统上我们用PyTorch Profiler或自定义埋点需要在训练循环里包一层profile上下文、在关键位置加NVTX标记这其实已经修改了代码路径。对调试环境来说没问题但线上任务有排期、有容错改一行代码也要重新走发布流程。而且加插桩本身会影响程序行为一旦开启了CUPTI回调内存分配和kernel launch的时间分布会轻微变化尤其对短步长任务是种扰动。零侵入profiling工具走的是另一条路它像一个外部观察者通过操作系统和CUDA层的接口去捕获事件目标程序感知不到自己被分析除了运行开销。这样第一不影响线上脚本逻辑第二不需要为调试临时改代码第三容易复现问题现场。以NVIDIA Nsight Systems为例实际命令就是一行nsys profile python train.py训练脚本该怎么写还是怎么写。2. 零侵入不等于不干活工具选型与原理拆解2.1 我用过的三类工具我不太建议一上来就迷信某个工具。按场景可以分成三层第一层是看一眼平均状态的nvidia-smi和nvtop第二层是长期聚合监控的DCGM第三层是深度定位的Nsight Systems。三层不是替代关系是配合关系。工具侵入程度核心能力最佳使用场景nvidia-smi无查看GPU利用率、显存、温度、功耗第一眼排查、快速确认有没有进程nvtop无类top的交互式GPU状态多卡机上肉眼观察波动DCGM (dcgmi/dcgm-exporter)无需要部署采集GPU利用率、功率、温度、PCIe吞吐等指标可对接Prometheus长期监控、离线告警Nsight Systems无代码修改但需外部启动统一时间线CPU函数、CUDA API、GPU kernel、memcpy单次深度定位瓶颈根因这里关键是Nsight Systems能为瞬间的问题提供完整时间线DCGM能告诉你“过去一周利用率一直低还是偶尔低”。两者结合一个管“这一次为什么慢”一个管“这是我机器的常态还是异常”。如果你用的是其他GPU平台找对应厂商提供的Trace/Activity工具思路完全一样。2.2 Nsight Systems为什么能做到零侵入简单说Nsight Systems针对NVIDIA GPU通过CUDA的Activity API和操作系统的性能事件接口在目标进程外部收集事件。nsys profile可以理解为一个wrapper先用profiler进程启动你的应用再通过内核事件采集CPU函数调用、通过CUPTI采集CUDA API和GPU kernel执行、通过NVML采集GPU硬件计数。因为大多数数据来自驱动和硬件不依赖你在代码里写埋点所以可以做到零侵入。我常在容器里这样用docker run --gpus all -it --rm \ -v $(pwd):/workspace \ -w /workspace \ nvcr.io/nvidia/pytorch:23.08-py3 \ nsys profile -o /workspace/trace --tracecuda,nvtx python train.py注意容器里跑nsys如果报权限或找不到libcupti的错多半是挂载不完整。最典型的锅是容器里缺/usr/local/cuda/lib64或者驱动版本对应的libcupti没装报错信息往往含糊不清不是“failed to create session”就是“no CUDA devices found”。解决方法是装带完整CUDA toolkit的基础镜像别用只装了runtime的精简镜像。这也是为什么我前面会强调先确认驱动和PyTorch的CUDA版本匹配——基础环境不对工具链全都没法工作。2.3 为什么不用py-spy这类CPU采样器很多同事喜欢用py-spy降采样能快速看到python函数热点这对纯CPU任务很好用。但AI训练是CPUGPU协同瓶颈往往藏在两者之间的协作等待里。py-spy看到CPU热点函数最多告诉你“预处理很慢”却不知道每次等待GPU空闲多少、kernel之间的gap有多大。事件时间线把CPU调用和GPU kernel放在同一根时钟轴上才能看出因果关系。比如时间线上CPU调用了cudaMemcpy之后要等500us才能接到事件返回GPU那边已经空了这立刻说明数据传输是瓶颈。这类跨设备等待关系只在事件trace中表现清晰。这也是AI profiling区别于通用profiler的价值所在。3. 一条命令跑通profilingNsight Systems 实战详解3.1 第一次跑先想清楚“只抓关键事件”我通常不会一上来抓全部事件。事件全开的报告可能上百GB机器直接卡死。先用最小集合看整体nsys profile -o resnet_trace --tracecuda,nvtx,osrt -f true \ python train.py --max-steps 30-o指定输出文件名nsys会生成.nsys-rep文件。--tracecuda,nvtx,osrt抓取CUDA事件、NVTX标记和操作系统运行时调用。--max-steps 30虽然是我脚本里的自定义参数但真实场景建议用脚本自带的步数/时限制比如--num-batches 30、--epochs 0目的是让profiling时间可控。这一步的目标不是分析细节而是确保nsys能启动、能采集、不会因为trace规模过大影响程序本身。如果能顺利跑完再看是否要补充内存拷贝、文件IO等事件。如果你是租用GPU机器做测试也建议先在交互式环境跑短trace确认整个环境OK后再挂到长任务上。3.2 怎么从命令行拿“摘要报告”跑完后要快速看报告。我习惯用命令行摘要而不是打开图形界面nsys stats resnet_trace.nsys-rep它会输出多个统计表重点看CUDA GPU Kernel Summary、CUDA API Summary和OS Runtime Summary。摘要里会直接列出每个kernel的调用次数、总耗时、平均耗时以及CUDA API的等待时间分布。以我一次实际遇到的情况为例输出里光cudaMemcpy这个API的累计等待时间就占了整个trace的40%而GPU kernel实际执行时间只占25%这几乎一锤定音瓶颈在数据搬运。如果觉得默认摘要太长还可以只导出kernel统计nsys stats --report cuda_gpu_kern_sum resnet_trace.nsys-rep打开报告后我把最重要三个指标圈出来Time (%)单个kernel耗时在GPU总时间中占比。若Top kernel占比很低说明碎片化严重。Callskernel调用次数。Avg (ns)平均执行时长。平均低于10us的kernel往往需要关注launch overhead。3.3 三个先看的关键区间拿到摘要后我按顺序排查三个区间第一是GPU kernel总执行时间 vs 总墙钟时间。比如训练脚本跑了120sGPU上所有kernel累计只有40s那就意味着80s不是被CPU占着就是被同步等待占着GPU被浪费了。第二是CUDA API等待时间。cudaMemcpy和cudaStreamSynchronize这类API的耗时实际上大多是等待异步操作完成。如果这个等待占总墙钟时间的比例很高通常说明流水线没有重叠CPU在等GPU算完或者GPU在等CPU拷贝数据。第三是CPU侧的OS Runtime。文件读取、内存映射、CPU算子计算都可能在这里暴露。如果某个CPU调用占用时间远高于预期那就是DataLoader或预处理逻辑的问题。我个人经验顺序是先看GPU kernel总时间再看cudaMemcpy占比再看CPU侧耗时。这三步基本能圈定瓶颈大类剩下才是针对性地改代码。4. 从报告到加速四类瓶颈模式与优化动作4.1 CPU喂不动GPUDataLoader与预处理优化时间线长相GPU层kernel执行区间之间有大量空白CPU层DataLoader相关函数如pickle反序列化、图像解码、collate占用很高CUDA API里很少看到memcpy因为数据还没到“送”的阶段。做法调整DataLoader的num_workers和prefetch_factor。我通常先看机器核数从num_workers4开始逐步加到8或12再看GPU utilization曲线。prefetch_factor2或4让每个worker提前准备多个batch。注意worker数不是越多越好太多会挤占GPU进程的CPU时间。示例train_loader DataLoader( train_dataset, batch_size64, num_workers8, prefetch_factor4, pin_memoryTrue, persistent_workersTrue, )persistent_workersTrue会复用worker进程避免每个epoch重建能明显减少数据准备侧峰值。我还习惯把耗时预处理搬到GPU上例如用torchvision的Tensors变换或在CPU端只做轻量操作。这类改动不一定算零侵入但既然profiling定位到CPU-Bound就值得改。4.2 数据搬运比计算还慢pin_memory与non_blocking时间线长相GPU kernel本身执行不差但每个batch前后都有很长一段cudaMemcpy HtoD主机到设备或DtoH传输占比超过30%。优化几个方向DataLoader开启pin_memoryTruePyTorch会使用锁页内存减少分页交换的开销也让异步拷贝成为可能。执行.to(device, non_blockingTrue)前提是数据已经是锁页内存。将频繁的CPU-GPU双向拷贝改成一次大块传输而非多次小块。PCIe传输有固定开销小数据传输反而浪费带宽。如果是推理场景可以考虑固定延迟的TensorRT但训练侧主要是批量输入避免单条传输。以我做过的语音模型训练为例一个batch特征从CPU拷到GPU需要约80ms模型前向才40ms方向整个反了。改pin_memorynon_blocking后拷贝基本被计算隐藏训练耗时直接下降40%。这类问题的特征是GPU util不低但墙钟时间降不下去因为GPU在等待和计算之间来回切换平均利用率被“计算拷贝交错”打得很低。4.3 kernel太碎算子融合与CUDA Graph时间线长相GPU一直有kernel在跑但kernel个数巨多单个平均执行时间只有几微秒GPU kernel summary里Top kernel占比小整体利用率可能还不低比如80%但训练仍慢。真相是大量时间消耗在kernel launch、调度和同步上。处理把逐元素操作合并。比如x x * scale bias这一个表达式可能拆成好几个kernel改成torch.addcmul或使用融合算子。使用torch.compile(model, modereduce-overhead)PyTorch的编译后端会把小算子融合成大的GPU kernel并减少python解释器介入。对固定shape和固定执行流使用CUDA Graph技术捕获GPU工作流并重放。PyTorch 2.x里可以直接用torch.cuda.graphs。代码示例model torch.compile(model, modereduce-overhead)reduce-overhead模式主要目标是降低launch overhead对小算子多的模型收益最明显。我实测过一个DNN排序模型torch.compile后kernel个数减少约70%单kernel平均执行时长上升整体训练吞吐提升35%。但注意动态shape任务第一次trace会花时间先评估你的输入shape是否稳定。4.4 同步等待和频繁malloc被低估的隐形杀手时间线长相CUDA API summary里有大量cudaStreamSynchronize、cudaDeviceSynchronize或cudaMallocCPU pipe被同步点占住GPU反而空转。这种问题常见于代码里混用了同步操作比如在loss.item()、写日志、用.cpu().numpy()转数据。每做一次item()或numpy()都会强制CPU等待GPU执行到某个点相当于把流水线截断。训练循环里如果每个step都写loss到文本就会形成持续同步。我之前查过一个例子一个看似正常的训练代码每个step调用了logger.info(f{loss.item():.4f})就这一个小小的item()让GPU在每个迭代末都要idle等CPU同步利用率从90%被拉到60%。优化日志输出改成每N步一次或异步提交。尽量降低使用loss.item()的频率尤其不要在循环内每个step都取标量。少在循环中动态分配显存避免反复触发cudaMalloc。PyTorch缓存分配器已经做了内存池但如果你手动cudaMalloc或者大量创建临时显存张量仍可能出现分配开销。适当使用torch.cuda.set_per_process_memory_fraction或固定显存池做一次热身。5. 容易被误读的GPU监控信号我的排障经验5.1 利用率高不等于效率高有次帮人看问题对方说“GPU利用率都到99%了还慢”。我拉kernel summary一看99%利用率里一半时间是5us以内的小kernel真正重计算kernel只占20%。这就像一台机床一直转但大部分时间在空切看起来忙实际加工时间很少。对这类情况堆更多CPU线程、扩容GPU卡都没用反而要减算力投入、做kernel融合。所以以后看到“GPU利用率低”先别急着加batch size或换卡先回答一个问题GPU到底是被什么拖住了是没活干还是活太碎5.2 nvidia-smi的采样窗口会骗人nvidia-smi显示“95% utilization”时不代表GPU真的95%时间都满负荷。GPU硬件利用率是NVML在采样周期内统计的很短的一段窗口内可能集中执行大量kernel其他时间完全空闲。为了看更细的波动我常用nvidia-smi dmon -i 0 -c 10对单卡按100ms粒度采样能看到比默认输出更细的锯齿波动。另外显存占用率不等于计算利用率。模型刚加载时显存可能占满但还没开始训练GPU利用率自然低这时应该看训练进程运行状态而不是被“显存满”吓到。GPU查询命令有很多但关键不是会敲多少条而是明白每个数字背后的采样口径。5.3 零侵入profiling的几条实用心得做零侵入profiling也有几个容易踩的坑分开说一说第一控制trace范围。默认全trace在长时间训练上会产生巨大文件。我习惯先给训练脚本加一个--profiling-steps之类的参数或用环境变量控制跑少量步数确保profiling过程本身不会把机器拖垮。第二分布式训练时不要每个rank都trace。通常profiling一个rank最好是rank 0就够了否则日志和性能开销会叠加。如果需要看到集合通信的等待再考虑用NCCL的trace功能但也只在单节点做一次。第三容器环境要提前验证nsys可用。我踩过比较常见的问题是libcupti版本不匹配导致能启动但采不到CUDA事件。验证方式很简单跑一个10步的极短training用nsys stats看有没有CUDA GPU Kernel Summary表没有就换完整镜像或补CUDA toolkit。第四profiling结果和优化效果要有闭环。拿到一份报告后做一次优化动作再跑一次同样命令对比两个报告的GPU Kernel Time和Total Time变化。没有前后对比的profiling报告价值会大打折扣。我个人现在的习惯是遇到GPU利用率低的反馈不再急着翻代码而是先跑一次零侵入的trace让时间线替我说话。很多时候根因根本不在GPU的计算能力上而是在CPU喂数据、PCIe搬运、同步点或者kernel调度这些周边环节。相比往训练脚本里插一堆profiler代码用Nsight Systems这类工具从外部观察既能保留现场又不会引入代码改动风险。如果你还没试过建议下次遇到“GPU利用率上不去”时先花十分钟跑一条profile命令看看时间线里那些空白到底等在哪里多半比看nvidia-smi的百分比有用得多。
返回列表