ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GPU利用率低?从监控到优化的PyTorch实战指南

GPU利用率低?从监控到优化的PyTorch实战指南 不知道你有没有遇到过这种场景训练任务明明在跑打开nvidia-smi一看GPU-Util 只有 20%显存占用一半不到风扇转速却已经上去了。任务日志里没有报错Loss 也在降但你隐约觉得不对劲——这块几千上万的加速卡真的在干活吗这种“GPU 空转”是深度学习训练、推理任务中最常见也最隐蔽的性能问题。显存看着够用程序没有崩溃但计算资源被白白地空闲掉了。本文围绕“怎么让 GPU 不闲着”这个主题从监控、定位、优化、多卡调度几个层面展开既讲清楚概念也会给出 PyTorch 场景下可以直接用的优化方案。适合下面几类读者训练脚本跑得慢但不知道瓶颈在哪的算法工程师想提高 GPU 资源利用率、降低算力成本的基础设施或平台开发刚开始接触深度学习想知道nvidia-smi这些数字到底是什么意思的新手。读完这篇文章你能掌握 GPU 利用率相关的核心概念、常用监控手段以及一套从数据加载、批处理、混合精度到多卡并行的方法论。1. GPU 为什么会“闲着”1.1 从 nvidia-smi 的一串数字说起先来看最常见的一个命令nvidia-smi输出大概长这样----------------------------------------------------------------------------- | NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 | |--------------------------------------------------------------------------- | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | || | 0 NVIDIA A100-SXM4-40GB Off | 00000000:00:04.0 Off | 0 | | N/A 42C P0 78W / 300W | 5120MiB / 40960MiB | 68% Default | -----------------------------------------------------------------------------这一屏信息里大家最关注的往往是两列Memory-Usage 和 GPU-Util。GPU-Util 表示采样周期内 GPU 的计算单元处于活跃状态的时间百分比显存占用则反映当前数据在显存里的驻留情况。这里需要澄清一个误解GPU-Util 高不代表性能一定最优GPU-Util 低也不代表程序一定有问题。例如某些推理服务单次请求很小、但延迟要求高GPU 可能长期只有 10% 的利用率这反而是合理的。1.2 GPU 利用率不代表一切GPU 内部结构远比一个“利用率”数字复杂。现代 GPU 由多个流式多处理器Streaming MultiprocessorSM构成每个 SM 内部还有 CUDA Core、Tensor Core、共享内存、寄存器堆等资源。程序运行时SM 以 warp通常 32 个线程为单位调度任务。所以我们会看到很多相似的概念概念含义与利用率的关系SM 占用率活跃 warp 占 SM 最大 warp 数的比例决定 GPU 能否用“并行”掩盖延迟显存占用当前驻留在显存中的数据量与计算利用率无直接关系带宽占用显存读写带宽的使用比例可能是数据密集型瓶颈功耗占用当前功耗与上限的比值低功耗往往代表“没在用力”GPU-Util 只告诉你“活跃时间比例”但没有告诉你 SM 是在跑有用的浮点运算还是在等待显存数据返回。很多时候程序显示 GPU-Util 很高但实际吞吐只有同型号 GPU 的一半原因就是指令在等其他资源。1.3 常见的“假忙”状态我在实际工作中总结了三种典型的“假忙”状态等待型高占用GPU 的 SM 在空转等待数据采样器看到 SM 处于 active 状态于是记成高利用率。这种情况常见于数据加载慢、CPU 预处理慢的任务。小算子反复启动算子太小kernel 启动开销远大于计算本身GPU 在每个 kernel 之间频繁切换上下文利用率看似不错实际效率很低。单卡当作大显存扩展卡用模型被切到多张卡上但并没有并行计算每张卡都在等上游的通信结果通信成了瓶颈。这三种状态有一个共同点单看nvidia-smi很难发现问题。所以要让 GPU 不闲着第一步不是改代码而是先把“它到底在忙什么”搞清楚。2. 先把现状看清楚GPU 监控与画像2.1 nvidia-smi 的基本用法nvidia-smi是 NVIDIA 驱动自带的监控工具可以实时查看 GPU 状态、温度、功耗、显存和利用率同时也支持查询模式输出方便脚本解析。# 每 2 秒刷新一次 watch -n 2 nvidia-smi # 只输出关键字段便于脚本处理 nvidia-smi --query-gpuindex,utilization.gpu,utilization.memory,memory.used,memory.total,temperature.gpu --formatcsv,noheader,nounits输出示例0, 68, 55, 5120, 40960, 42第一条是 GPU 编号第二条是当前计算利用率第三条是显存控制器利用率后三条分别是显存使用量、显存总量和温度。把这条命令写进定时任务就可以形成一份 GPU 使用记录。查询不到数据的场景也很常见。比如 WSL 环境下如果驱动版本和系统内核不匹配nvidia-smi可能报错或识别不到 GPU。这时要优先检查 Windows 侧驱动是否更新到支持 WSL 的版本再确认 CUDA 工具链与驱动版本的对应关系。2.2 一键监控工具 gpustat / nvtopnvidia-smi虽然是万能的但输出不够直观。更推荐安装 gpustat 或 nvtoppip install gpustat gpustat --watchgpustat 会把多卡信息汇总成一张表显示每张卡的利用率、显存、显存占用率、功耗还能按进程排序。nvtop 则像是 GPU 版的 htop交互式查看进程、温度、频率变化排查间歇性卡顿时更直观。这类工具适合日常巡检不适合精确定位。当你想知道“某个 kernel 到底跑了多久”“GPU 等待了多久”就要用更底层的 profiler。2.3 用 Profiler 定位等待时间NVIDIA 官方提供了 nsys 和 ncu 两个常用工具。nsys 用来分析时间线看看 CPU 侧和 GPU 侧分别花了多少时间ncu 用来分析 kernel 内部指标比如寄存器数、内存吞吐、计算吞吐。# 采集性能时间线 nsys profile -o my_profile --force-overwrite true python train.py # 针对所有 kernel 收集硬件指标 ncu --set full python train.py关键指标包括elapsed timekernel 总耗时包含队列等待时间issue/dram throughputSM 指令发射与显存吞吐occupancySM 中活跃 warp 的比例memory workload analysis带宽瓶颈是否接近上限。PyTorch 用户还可以直接用自带的 Profiler在脚本里接入from torch.profiler import profile, ProfilerActivity with profile( activities[ProfilerActivity.CPU, ProfilerActivity.CUDA], record_shapesTrue, profile_memoryTrue, ) as prof: train_one_epoch() print(prof.key_averages().table(sort_bycuda_time_total, row_limit20))这条输出会告诉你哪些算子占用了最多的 CUDA 时间哪些算子的 host 端和 device 端时间差值巨大。如果 CPU 侧时间远大于 GPU 侧时间说明 CPU 侧正在阻塞 GPU 执行数据加载或 Python 预处理嫌疑最大。3. 训练场景三大瓶颈数据、同步、显存3.1 数据加载是第一个隐形瓶颈深度学习训练的传统循环是CPU 读取图片 - 解码 - 做数据增强 - 转成张量 - 通过 DataLoader 送到 GPU。如果 CPU 处理数据的速度跟不上 GPU 消费数据的速度GPU 就会在每次迭代开始前等待。这个问题在 CV 类任务中最明显。图像解码、随机裁剪、翻转、归一化都发生在 CPU 上一旦 num_workers 设置不合理GPU 往往会周期性“喘气”利用率冲高又掉零再冲高再掉零。判断方法很简单在训练循环里记录每个 epoch 的开始时间戳看是否存在固定的等待间隔或者用 profiler 观察 DataLoader 的 load 时间是否占比过高。3.2 同步点是第二个瓶颈程序里的loss.backward()、optimizer.step()都是同步点。在单卡场景下同步点会强制 GPU 等待一个 batch 完整执行完才能进入下一个 batch在多卡场景下梯度同步的集合通信还会让所有卡互相等待。同步不可避免但可以优化。减小同步的频率、用异步拷贝隐藏数据搬运时间、让多个数据流并发执行都是常见手段。后面实战部分会给出具体代码。3.3 显存是第三个瓶颈显存不足会直接抛异常但显存充足不代表“够用”。当显存碎片化严重时即使总剩余量足够也可能申请不到连续的大块显存。PyTorch 的缓存分配器会尽量复用内存块但频繁改变 tensor 形状会加剧碎片化。还有一个误区为了“充分利用显存”把 batch size 调到极大值结果 GPU 利用率上升了Loss 收敛反而变慢甚至多次 OOM。正确的做法是在显存允许的范围内选择能稳定收敛的 batch size然后通过梯度累积扩大等效批大小。4. 实战PyTorch 训练任务的 GPU 占用优化4.1 数据集与 DataLoader 优化这一节给出一份可以直接复用的 DataLoader 配置。假设我们有一个自定义数据集 MyDataset# train_loader 配置 train_loader DataLoader( datasetMyDataset(...), batch_size64, shuffleTrue, num_workers8, # 一般取 CPU 核心数的一半或全核数 pin_memoryTrue, # 锁页内存加速 CPU-GPU 拷贝 persistent_workersTrue, # worker 常驻避免反复重建 prefetch_factor4, # 每个 worker 预取 4 个 batch drop_lastTrue, # 丢弃末尾不足一个 batch 的样本 )几个关键点说明num_workers数据预处理并行进程数。设置过小CPU 来不及喂数据设置过大进程切换和内存拷贝开销反而变大。可以从 4 开始逐步增加到 12观察迭代耗时变化。pin_memory启用后 tensor 会存进锁页内存GPU 通过 DMA 拷贝更快。几乎总是建议开启。persistent_workersPyTorch 在版本迭代中逐步完善了常驻 worker 机制开启后避免每个 epoch 重新创建进程能明显减少训练中的间歇停顿。prefetch_factor让 worker 提前准备多个 batch把“取数”和“训练”重叠起来。如果数据是图片可以把解码从 dataset 挪到 worker 之外或使用 lmdb、TFRecord、memmap 等格式避免大量小文件随机读。小文件随机读是文件系统最怕的模式几千张小图训练集尤其明显。4.2 批大小与梯度累积显存不够但想保持大 batch 的时候梯度累积是最常用的方案。原理前向传播算 loss 后不立刻更新参数而是累加多次梯度后统一更新等效 batch size batch_size * accumulation_steps。accumulation_steps 4 optimizer.zero_grad() for step, (data, target) in enumerate(train_loader): output model(data) loss criterion(output, target) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()注意两点如果 loss 是均值形式累计多个 batch 的梯度后梯度总和会放大 accumulation_steps 倍因此需要把每次的 loss 除以 accumulation_steps。使用 BatchNorm 时要谨慎。梯度累积只是“数学上的等效大 batch”BN 仍然按每个小 batch 统计均值方差和真正的大 batch 效果不完全一致。可以考虑 SyncBN但会引入通信开销。4.3 混合精度训练混合精度AMP是当前最容易见效、改动最小的优化手段之一。核心思路是用 FP16 做前向和反向用 FP32 做权重更新同时用动态损失缩放避免 FP16 下梯度下溢。PyTorch 原生 AMP 写法from torch.cuda.amp import autocast, GradScaler model model.cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4) scaler GradScaler() for data, target in train_loader: data, target data.cuda(), target.cuda() optimizer.zero_grad() with autocast(): output model(data) loss criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()AMP 能带来多少收益取决于硬件和算子类型。在 A100、A800 等支持 Tensor Core 的卡上矩阵乘和卷积类算子提速非常明显如果模型里自定义算子较多、且没有 FP16 实现性能提升会打折。在 40 系和 50 系游戏卡上跑 ComfyUI 或其他 AI 工具时用户经常遇到的不是计算速度慢而是显存不足。之所以强调显存是因为 FP16 相比 FP32 显存占用减半AMP 除了加速还能腾出更多显存空间。如果遇到类似的显存不足优先检查是否开启了 AMP再看看模型加载方式。4.4 CUDA Stream 异步执行默认情况下 PyTorch 的操作会串行执行。使用 CUDA Stream 可以把不依赖彼此的操作放到不同流里并发执行。典型场景GPU 同时在计算和拷贝数据或两个独立网络结构分别推理。s1 torch.cuda.Stream(devicecuda) s2 torch.cuda.Stream(devicecuda) with torch.cuda.stream(s1): output model(head_a, data_a) with torch.cuda.stream(s2): output2 model(head_b, data_b) # 在默认流上等待两个流完成 torch.cuda.current_stream().wait_stream(s1) torch.cuda.current_stream().wait_stream(s2) result torch.cat([output, output2], dim1)这里要非常小心两个流如果同时对同一块显存做读写会产生数据竞争。尽量让每个流管理自己独立的 tensor并在需要合并结果的点做 wait_stream 同步。实际业务里Stream 的收益不一定稳定。如果模型本身已经接近 100% 算力再开 Stream 只会增加调度开销。所以建议先 profiling确认存在“计算与拷贝可重叠”的空间再上 Stream。4.5 多卡训练 DDP当单卡已经吃满还想继续提速就轮到多卡并行。PyTorch 的 DDPDistributedDataParallel是当前最主流的方案。# 终端启动方式 # torchrun --nproc_per_node4 train_ddp.py import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group(backendnccl) local_rank int(os.environ[LOCAL_RANK]) torch.cuda.set_device(local_rank) model model.cuda(local_rank) model DDP(model, device_ids[local_rank]) # DataLoader 需要给每张卡分配独立数据分片 sampler DistributedSampler(dataset, shuffleTrue) train_loader DataLoader(dataset, samplersampler, batch_size64, num_workers8) for epoch in range(epochs): sampler.set_epoch(epoch) for data, target in train_loader: data, target data.cuda(local_rank), target.cuda(local_rank) ...DDP 的底层逻辑是每个进程维护一份模型副本前向传播独立反向传播时通过 Ring-AllReduce 完成梯度同步。网络通信在多卡训练中往往成为新的瓶颈以下几点值得关注尽量用点对点直连的机内通信避免经过慢速交换机总的 batch size 变大学习率需要相应调整通常按线性缩放法则处理开启torch.backends.cudnn.benchmark True让 cuDNN 自动选择最快卷积算法在输入尺寸固定的场景下收益明显。4.6 让 cuDNN 和 TF32 帮你“白嫖”性能下面是两个几乎零成本的开关torch.backends.cudnn.benchmark True torch.backends.cuda.matmul.allow_tf32 True torch.backends.cudnn.allow_tf32 Truecudnn.benchmark 会针对固定输入尺寸做算法搜索找到当前硬件上最快的卷积实现。代价是第一次迭代耗时变长。allow_tf32 会把 FP32 矩阵乘法的内部计算切换到 TF32 精度在 A100 等卡上能提升不少性能但会损失一点精度。训练误差允许时推荐开启对精度敏感的对齐计算建议保持关闭。5. 多卡与共享集群场景5.1 显存分区与多显卡调度很多团队会遇到显卡数量少、任务多的矛盾。与其让一个任务独占整张卡不如用 MPSMulti-Process Service或 MIGMulti-Instance GPU做显存与算力切分。MIG 在 A100、H100 等数据中心卡上支持将物理 GPU 划分为多个独立实例每个实例拥有独立的显存、SM 和带宽。MPS 则更适合消费级和专业级卡允许多个进程共享 GPU 资源同时限制资源使用量。不过 MPS 对显存和算力的隔离并不像 MIG 那么彻底多进程并发时可能出现故障进程拖垮整卡的情况。生产环境建议先评估任务的安全边界再做切分。5.2 容器场景的 GPU 直通Docker 容器跑深度学习已经成为常态。容器本身不直接认识显卡需要借助 NVIDIA Container Toolkit 把 GPU 暴露给容器。如果启动容器时遇到类似 “failed to discover gpu vendor from cdi” 的报错通常是因为宿主机没有安装 Container Toolkit或者 Docker 配置了 CDI 但驱动目录不完整。常规做法是安装 nvidia-container-toolkit然后使用docker run --gpus all --shm-size16g -it nvcr.io/nvidia/pytorch:24.01-py3参数--gpus all表示把宿主机所有 GPU 直通给容器。如果只需要某块卡可以写成--gpus device0,1。--shm-size也很关键DataLoader 多进程在容器内共享内存默认只有 64M不调大容易出现 DataLoader worker 启动失败或卡死的现象。5.3 多租户算力的利用率管理当多个人共用一台 GPU 服务器时最直接的矛盾是“谁占了多少卡”。可以用 nvidia-smi 的进程列表查看nvidia-smi --query-compute-appspid,process_name,used_memory --formatcsv如果发现某个进程长期占着显存但不计算可以结合nvidia-smi -l或 nvtop 确认。生产环境建议建立资源申请与监控一体化的调度平台而不是靠人去盯。平台侧的核心思路是显存配额 算力配额 运行时长上限 空闲回收策略。6. 常见问题与排查思路下面整理了 GPU 利用率优化过程中最常遇到的一批问题按“现象 - 原因 - 解决”给出速查表。问题现象常见原因解决思路GPU-Util 周期性掉到 0然后又恢复数据加载瓶颈提高 num_workers、开 pin_memory、优化数据格式显存占用很高但 GPU-Util 很低模型阶段切换频繁或存在大量同步点用 profiler 定位耗时算子检查同步点多卡训练时每张卡利用率都不高通信等待检查网络拓扑、减小通信频率、考虑梯度压缩频繁 OOMbatch_size 过大 / 显存碎片化减小 batch、使用梯度累积、开启 AMP容器内 nvidia-smi 看不到 GPUContainer Toolkit 未安装或版本不匹配重装 nvidia-container-toolkit检查 Docker 配置WSL 下识别不到 GPUWindows 驱动版本过旧升级 Windows 侧驱动确认 WSL 内核支持cudnn.benchmark 后首次迭代很慢算法搜索期间尝试多种实现属正常现象稳定运行后提速模型可以跑但 CPU 占用 100%DataLoader 进程过多或数据预处理重减少 num_workers优化预处理逻辑这里特别说明一下并不是所有“GPU 利用率低”都值得优化。推理服务对延迟敏感GPU 保持较低利用率是正常的训练任务追求吞吐利用率才应该尽量拉高。优化前想清楚场景否则只是自娱自乐。7. 最佳实践与工程建议7.1 先建立基准再动手优化任何优化都必须从基准开始。记录当前任务的每秒迭代数、每秒处理样本数、GPU 平均利用率、显存峰值、训练总时长。优化过程只改一个变量验证一次。不要同时改 DataLoader、AMP、多卡方案否则出了问题很难定位。7.2 性能优化的优先级我个人推荐的优化顺序是第一优先确认数据加载和预处理没有成为瓶颈。这一步收益最大改动最小。第二优先开启 AMP 和 cudnn.benchmark。几乎零成本收益明显。第三优先调整 batch size 和梯度累积。第四优先引入 CUDA Stream做计算和拷贝重叠。第五优先多卡 DDP考虑通信代价。7.3 代码与工程层面的规范显存配额与进程管理生产环境要设置 GPU 显存上限防止单个任务把整卡打满导致其他服务抖动日志与监控记录每个训练 step 的耗时、GPU 利用率和功耗方便事后复盘随机性管理多卡训练要固定 seed并配合 DistributedSampler 的 set_epoch 防止数据重复模型保存保存 checkpoint 时包含 optimizer state 和 GradScaler 的状态AMP 场景下尤其要保存 GradScaler 的状态否则中途恢复训练可能精度异常。7.4 注意精度与安全边界AMP、TF32 虽然快但会改变浮点运算顺序和精度。对数值敏感的任务例如强化学习中的价值网络、精度要求高的科学计算开启前必须做对比实验观察收敛曲线和最终指标。对生产环境中的模型更新最好先在小规模数据上验证效果再全量发布。8. 总结让 GPU 不闲着的本质是让“数据搬运、kernel 计算、通信同步”三条流水线尽量重叠而不是简单地把利用率冲到 100%。GPU-Util 只是一个表面指标真正能解释问题的工具是 profiler。本文从 nvidia-smi 的监控开始讲清了 GPU 利用率、显存、SM 占用之间的区别给出了 DataLoader、梯度累积、AMP、CUDA Stream、DDP 等 PyTorch 场景下的可直接复用的优化代码也覆盖了 WSL、容器 GPU 直通、多卡调度等工程问题。下一步建议动手做三件事先在你的训练任务上跑一次 nsys 或 torch.profiler找到最耗时的算子再按优先级依次优化数据加载和精度设置最后把监控指标沉淀成可复用的脚本让每个新任务上线前都能自动生成一份性能报告。希望这份“GPU 不闲着”指南能帮你把每块卡发挥到它应有的水平。如果文中的排查表或多卡配置正好解决了你的问题可以收藏备用也欢迎在实际项目里继续验证这些参数的正确性。
返回列表