资讯中心

GPU利用率低?从观测到优化,让每一块显卡都高效运转

📅 2026/8/26 7:14:06
GPU利用率低?从观测到优化,让每一块显卡都高效运转
实际开发中很少遇到“GPU完全没干活”的情况更多是“GPU一直在跑但不知道它到底有没有干正事”。GPU利用率显示 99%训练速度却没有提升显存已经占满计算单元却在空等数据机器上插了八张卡只有一张卡在发热其余七张闲置推理服务 GPU 占用只有 20%但请求排队依然严重。真正想回答“怎么让 GPU 不闲着”需要先承认一个前提GPU 利用率不是单一指标而是一整条数据链路共同作用的结果。这篇文章从一名普通开发者的视角出发讲清楚 GPU 在训练、推理、容器、多卡和常见故障场景下的状态观测方法以及如何通过数据加载、训练策略、显存管理、推理批处理和调度优化让 GPU 进入更高效的工作状态。内容会尽量用手册式的步骤、命令和表格呈现适合刚接触 GPU 开发的读者也适合已经能在单卡上跑通训练、但想提升资源利用率的团队参考。1. 先搞清楚 GPU 到底在哪一层“闲着”1.1 GPU 利用率不是“0% 和 100%”二选一很多刚接触 GPU 的开发者会陷入一个误区只要 GPU-Util 没有到 90% 以上就觉得 GPU 被浪费了。实际不是这样。现代 GPU 通常由三部分资源共同组成流式多处理器Streaming MultiprocessorSM、显存控制器、以及用于数据拷贝的 DMA 引擎。三者都可能成为瓶颈。常见的情况包括SM 很忙但显存带宽不够L2 缓存命中率下降核心在等数据返回。显存占用很高但计算请求很少模型权重和中间激活值只是“占用空间”而已。数据通过 PCIe 从 CPU 拷贝到 GPU 的过程很慢GPU 的 kernel 执行时间很短大部分时间都在等拷贝完成。多卡之间做梯度同步单卡算得很快但通信阶段所有卡都在等待最慢的那张卡。所以第一步要做的是区分瓶颈。GPU 利用率只是给了一个宏观信号要精确定位“闲在哪一层”还需要功耗、温度、显存带宽、拷贝引擎、SM Active 占比等更多指标。1.2 用 nvidia-smi 先看宏观状态NVIDIA 驱动自带的nvidia-smi是最直接的观测工具。建议以固定间隔采样而不是只运行一次。nvidia-smi --query-gpuindex,utilization.gpu,memory.used,power.draw,temperature.gpu --formatcsv -l 1-l 1表示每秒采样一次。输出大致是index, utilization.gpu [%], memory.used [MiB], power.draw [W], temperature.gpu [deg C] 0, 97 %, 16384 MiB, 310.25 W, 71 0, 12 %, 16512 MiB, 78.10 W, 63第一次采样看到 97%第二次变成 12%这种情况很典型GPU 正在执行一些密集计算但随后进入空闲等待。显存没有释放说明空间仍在被模型占用只是 SM 没有任务可跑。常见指标的含义需要重新梳理指标含义常见误解utilization.gpu采样周期内 SM 繁忙的百分比不等于计算效率等待数据也会算“忙”memory.used显存占用只代表分配了多少不代表计算量power.draw当前功率远低于 TDP 通常说明负载不高temperature.gpu核心温度温度过高会触发降频抑制利用率memory.clock显存时钟频率低于满频说明显存带宽未打满SM clock核心时钟频率降频说明遇到功耗墙或温度墙1.3 三种“看起来忙但实际低效”的状态第一种是等待数据。当磁盘读取慢、CPU 预处理慢GPU kernel 执行时间极短nvidia-smi会看到利用率在 0% 和 100% 之间快速跳动显卡功率不稳定。这种状态在训练场景最常见。第二种是小 kernel 密集启动。程序每次只提交很小的计算任务比如逐元素操作一个很小的 tensorGPU 启动 kernel 的开销会占大头。看起来 GPU 一直在执行实际大量时间消耗在任务调度和上下文切换上。第三种是同步等待。多卡训练中每张卡完成反向传播后需要做 AllReduce 梯度同步。如果网络连接是 PCIe 而不是 NVLink或者跨机器通信带宽不足计算时间被通信时间拉长GPU 会出现明显的“忙一会儿、停一会儿”现象。2. 先建立可观测性数据采集比优化更重要2.1 临时观察工具nvidia-smi 循环采样和 nvtop如果只需要临时看几秒钟可以这样采样nvidia-smi dmon -s pucct -d 1-d 1表示每秒输出一行。-s pucct表示监控功率、利用率、时钟、温度和内存状态。这个命令的好处是输出紧凑适合放在另一个终端里实时观察训练过程。另一个常用工具是nvtop它类似 Linux 下的top可以交互式查看 GPU 占用、显存、温度、风扇转速和每个进程的资源占用。在 Ubuntu/Debian 环境中安装sudo apt install nvtopnvtop对多卡机器非常有用可以一眼看出每张卡的负载是否一致。不过它适合短时排查不适合长期留存数据的场景。2.2 长期监控DCGM 是更完整的底座真正要做性能优化不能只靠人工盯终端。NVIDIA 官方提供的 DCGMData Center GPU Manager更适合长期采集。nvidia-smi dmon -e 1002,1003 -d 5这里1002是 SM 利用率1003是显存带宽利用率。实际项目一般会把 DCGM 接入 Prometheus 的dcgm-exporter然后在 Grafana 里生成看板。这样做的好处是可以记录完整训练周期的利用率变化。可以在训练结束后对比优化前后数据。可以按时间维度分析卡间负载是否均衡。如果暂时不想搭建整套监控可以直接用 DCGM 自带命令保存文本dcgmi dmon -e 1002,1003,1004,1005 -d 5 gpu_metrics.log这样至少留了一份可回溯的数据。2.3 建立利用率画像后再开始调整优化 GPU 之前建议先跑一份“基线画像”。固定一个任务、固定输入数据规模记录十分钟内的四类数据角色采集指标目的进程侧每 step 耗时、显存峰值判断全局损耗GPU 侧SM 利用率、显存带宽利用率判断计算是否饱和传输侧PCIe 读写速率、主机内存占用判断数据链路瓶颈系统侧CPU 占用、磁盘 IO、内存交换判断 CPU 准备是否拖后腿得到基线之后每次只改一个变量再跑同一份任务对比。比如从num_workers2调到num_workers8只看这一项差异。没有基线就改多个参数最终只能确认“整体变快了”却说不清是哪一个优化起了作用。注意不要只验证任务能跑完还要记录每一步的时间分布。GPU 利用率只在训练结束后看平均数是远远不够的。3. 训练场景让数据搬运和计算真正重叠3.1 DataLoader 配置是 GPU 空等的首要来源PyTorch 训练中最容易被忽略的性能瓶颈是 DataLoader 的 CPU 预处理速度赶不上 GPU 计算速度。from torch.utils.data import DataLoader train_loader DataLoader( dataset, batch_size32, num_workers8, prefetch_factor4, pin_memoryTrue, persistent_workersTrue, )这里每个参数都值得解释num_workers负责在子进程中预处理数据的进程数。大于 0 时主进程只需要从队列中拿已经准备好的 batch。prefetch_factor每个 worker 提前加载多少个 batch。设置过小会导致 GPU 经常空等。pin_memoryTrue把数据固定在主机端页锁内存可以加速 CPU 到 GPU 的拷贝。persistent_workersTrue训练迭代完一轮后不销毁 worker避免反复创建进程的开销。需要注意num_workers并不是越大越好。如果数据集的预处理逻辑本身不重而磁盘 IO 已经接近上限开更多进程只会让磁盘更忙。建议从2、4、8逐个尝试观察 GPU 利用率变化。3.2 batch size、梯度累积和混合精度batch size 直接决定 GPU 计算密度。同样一个 epoch如果每个 batch 只有 4 张图kernel 启动次数多GPU 的调度开销占比高如果 batch 能增加到 32 或 64单位时间内的算力利用率会明显提升。但显存是有限的。显存不足时梯度累积是一种补救手段accumulation_steps 4 scaler torch.cuda.amp.GradScaler() for step, (inputs, labels) in enumerate(train_loader): with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(inputs) loss criterion(outputs, labels) loss loss / accumulation_steps scaler.scale(loss).backward() if (step 1) % accumulation_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()梯度累积的语义是“多个小 batch 的梯度叠加后再更新权重”逻辑上接近一个大 batch但要注意三点如果模型里有 BatchNorm小 batch 下的统计量会不准确需要额外处理。如果原本的大 batch 使用了学习率调度改成梯度累积后学习率是否需要调整需要重新实验。torch.cuda.amp.GradScaler()只对 float16 有效如果使用 bfloat16可以省略GradScaler但必须保证硬件支持。混合精度能让 GPU 明显“更忙”因为 float16 的显存占用和计算开销都比 float32 低同一块 GPU 能装入更大的 batch。遇到不支持混合精度的算子在代码里也不用担心PyTorch 会自动回退到 float32。3.3 多卡训练避免“一张卡干活其余卡围观”多卡训练最频繁的问题是忘记通过环境变量控制设备编号导致所有进程都跑在同一张卡上其余卡完全没有负载。CUDA_VISIBLE_DEVICES0,1 torchrun --nproc_per_node2 train.py在代码内部通常这样设置import torch import torch.distributed as dist dist.init_process_group(backendnccl) local_rank dist.get_rank() torch.cuda.set_device(local_rank) device torch.device(cuda, local_rank)torchrun会自动为每个进程分配对应编号。如果机器是四卡但只想用物理编号 2 和 3那么环境变量应该写成CUDA_VISIBLE_DEVICES2,3 torchrun --nproc_per_node2 train.py这样进程内看到的编号就是 0 和 1分别映射到物理卡 2 和 3。多卡通信对 GPU 利用率的影响很大。单卡算得再快如果梯度同步阶段所有卡都在等最慢的卡整体效率就会被拉低。使用nvidia-smi topo -m可以查看卡间拓扑nvidia-smi topo -m如果输出显示两张卡之间有NV#表示走 NVLinkPIX或PHB表示走 PCIe。NVLink 带宽更高AllReduce 等待时间更短。多机训练还需要额外关注网卡和跨机通信带宽。3.4 用 Nsight Systems 精确定位空闲区间当 GPU 利用率低但不知道原因时建议用 NVIDIA 提供的 Nsight Systems 做一次性能采样。nsys profile --tracecuda,nvtx -o trace_output python train.py采样完成后用 Nsight Systems 打开trace_output.nsys-rep重点看时间轴上的 GPU 活动区间。通常情况下GPU 在很长一段时间没有任务说明 CPU 侧数据准备太慢。GPU 任务密集但出现规律性空隙说明存在同步等待。GPU kernel 很多但每个都很短说明需要减少小 tensor 操作或者合并 kernel。Nsight Systems 的一次全量采样往往能告诉你“GPU 空在哪”是性能分析里最值得投资的一步。4. 推理场景不要追求 100% 利用率4.1 先确定目标在线低延迟还是离线高吞吐推理场景和训练不同。训练阶段可以为了吞吐量不断增大 batch size让 GPU 始终保持高负载。但在线推理服务如果强行增加 batch单个请求需要等同一个 batch 里的其他请求处理完P99 延迟会上升。场景核心指标典型策略在线 API 服务P99 延迟、单请求耗时控制 batch 大小不做长排队离线批处理任务每秒处理样本数尽量用大 batch 跑满 GPU大语言模型推理首 token 延迟、吞吐量动态批处理、连续批处理图像生成生成耗时、显存占用峰值显存控制、模型优化所以在做推理优化时不能只看 GPU 利用率。如果延迟达标、吞吐稳定GPU 利用率只有 40% 也可能是完全合理的。4.2 动态批处理让稀疏请求填满 GPU单次推理只发一个请求GPU 很难跑出高利用率。更合理的做法是引入动态批处理让服务在等待窗口内聚合多个请求再一次性送到 GPU。以 Triton Inference Server 为例配置 dynamic batching 时通常会设置{ max_batch_size: 8, dynamic_batching: { preferred_batch_size: [4, 8], max_queue_delay_microseconds: 200 } }这样服务会等待 200 微秒尽量把请求凑满 4 或 8 个再执行。延迟会增加一点点但 GPU 利用率会明显提升。对于大语言模型推理vLLM 等框架采用continuous batching策略不再等整个 batch 做完而是不断把结束生成的请求移除、插入新请求。这种方法能在严格延迟约束下显著提高 GPU 吞吐。4.3 用 TensorRT 和 ONNX Runtime 做计算图优化深度学习框架默认的推理执行方式往往包含很多额外开销。TensorRT 会对计算图做层融合、精度校准和 kernel 自动选择减少 GPU 上的空转片段。常见流程是先把模型导出为 ONNX再用 TensorRT 转换。trtexec --onnxmodel.onnx --saveEnginemodel.engine --fp16 --workspace4096这里的--fp16开启半精度推理--workspace指定构建引擎时可用的显存上限。trtexec还可以用来做基准测试trtexec --loadEnginemodel.engine --shapesinput:1x3x224x224 --fp16但要注意TensorRT 优化后的引擎是和具体 GPU 架构、CUDA 版本、batch size 绑定的换卡后通常需要重新生成。模型结构变化频繁的时候维护多份引擎文件的成本较高。4.4 图像生成场景的显存优化思路像 ComfyUI、Stable Diffusion 这类图像生成应用经常出现“显存不足”但 GPU 利用率并不高。原因往往是峰值显存分配问题模型权重、临时 tensor、中间激活同时叠加在显存中某个瞬间超出容量。处理这类问题不能靠单纯调大 batch常用方向包括降低分辨率或 batch size。使用 fp16 或 fp8 精度。开启模型 offload把部分层临时放到 CPU 内存。使用显存优化插件把不需要的中间结果及时释放。检查是否打开了“内存不足自动优化”这类重分配功能。在显存不足的机器上第一目标应该是让程序稳定跑通其次才是把 GPU 利用率拉高。如果每一步都在 OOM 边缘反复试错效率远低于主动降低 batch。5. 多卡与容器环境让每张卡都分到明确的活5.1 通过 CUDA_VISIBLE_DEVICES 管理设备编排多卡机器上如果某个任务没有显式指定 GPU它会默认使用编号 0。多个任务同时运行时容易把所有负载都挤到第一张卡。通过环境变量可以控制程序看到哪些物理 GPUexport CUDA_VISIBLE_DEVICES1,3 python train.py这个变量会让程序以为机器上只有两张卡编号分别是 0 和 1实际对应物理卡 1 和 3。在多进程任务里需要注意“进程内编号”与“物理编号”的映射避免明明设置了多卡却始终使用同一张物理卡。在容器环境中同样适用docker run --gpus device1,3 --rm nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi5.2 Docker GPU 直通与 CDI 报错排查Docker 里要用 GPU需要安装 NVIDIA Container Toolkit。如果没有正确配置会看到类似错误docker: Error response from daemon: failed to discover gpu vendor from cdi: no known这个错误的常见原因是 Docker 没有使用 NVIDIA Container Runtime。可以这样修复sudo nvidia-ctk runtime configure --runtimedocker sudo systemctl restart docker然后验证当前 Docker 运行时docker info | grep -i runtime正常输出应该包含nvidia。继续用 CDI 列表确认nvidia-ctk cdi list如果输出里能看到类似nvidia.com/gpu0的条目说明 CDI 配置正常。之后再运行容器就不会再报告no known。Kubernetes 集群里则需要部署 NVIDIA GPU Operator由它统一处理驱动、device plugin、DCGM 和运行时配置。手动环境下的排查步骤在集群环境里不一定适用需要以实际集群的 Operator 状态为准。5.3 WSL2 和虚拟机场景中 GPU 被识别但没生效WSL2 里经常遇到一种情况nvidia-smi能看到 GPU但 OpenGL 渲染仍然走 CPU 软件模拟。原因通常是宿主机没有安装对应的 GPU 驱动或者 WSL 版本太旧导致 D3D12 与 OpenGL 的转换层没有生效。检查方式nvidia-smi glxinfo | grep -i opengl如果glxinfo显示的是llvmpipe说明 OpenGL 正在使用 CPU 渲染。这时需要检查 Windows 宿主机的显卡驱动版本以及 WSL 是否更新到支持 GPU 加速的版本。WSL2 的 GPU 能力依赖宿主机驱动和 WSL 内核共同工作不能只在 Linux 侧单独解决。Ollama 在 WSL 中无法识别 GPU 的情况也类似需要确认宿主机驱动是否支持当前 CUDA 版本。运行 Ollama 的用户是否具有访问/dev/dxg和/dev/nvidia0的权限。有无通过CUDA_VISIBLE_DEVICES强制屏蔽 GPU。6. 几种常见的“低效忙碌”问题与处理6.1 显存 OOM 不等于 GPU 在满负荷运行显存不足和计算饱和是两个维度。模型可能因为参数巨大占满显存但实际推理时 SM 几乎不工作。OOM 产生的原因很多时候是某个中间 tensor 在峰值时刻超出了显存容量。排查时可以在训练代码里加入显存峰值统计torch.cuda.reset_peak_memory_stats() outputs model(inputs) peak torch.cuda.max_memory_allocated() print(fPeak memory: {peak / 1024**3:.2f} GB)通过逐步缩小输入观察峰值显存随 batch size 的变化可以判断显存主要消耗在哪些环节。如果模型本身很大可以换用模型并行或 offload如果是激活值过大可以减小 batch size 或开启梯度检查点。6.2 降频、功耗墙和散热导致利用率偏低GPU 显示满载但实际 SM 时钟频率低于理论峰值说明可能撞到了功耗墙或温度墙。需要检查一下时钟状态nvidia-smi -q -d CLOCK在数据中心环境可以主动限制功耗让 GPU 长期稳定运行而非间歇降频nvidia-smi -pl 250-pl参数只在部分专业卡上有效普通消费卡可能不支持。它用于设定功耗上限。实际项目中与其让 GPU 在超标功耗和降频之间反复横跳不如设置一个合理功耗上限换来更稳定的吞吐。nvidia-settings在部分卡上无法调节风扇转速这是常见现象。笔记本和消费级显卡的风扇控制通常被硬件固件接管不必在软件层面强行修改。6.3 PCIe 链路错误计数解读nvidia-smi的 PCIE 信息里包含一些错误计数器例如GPU 00000000:01:00.0 PCIe Link Current : 8 Max : 16 ... Error Counters Receiver Errors : 5 Bad Dllp Count : 2 Bad Tlp Count : 0Receiver Errors、Bad Dllp Count、Bad Tlp Count都位于传输层如果持续增加通常说明 PCIe 链路不稳定而不是 GPU 计算资源不足。可能原因包括显卡电源供电不足。PCIe 插槽或转接线接触不良。PCIe 链路被强制工作在超出稳定范围的速率。主板 BIOS 设置或 PCIe spread spectrum 配置问题。排查时先查看nvidia-smi -q -d PCIE中的Current和Max是否一致再检查系统日志dmesg | grep -i pcie如果错误计数持续增长优先检查供电、插槽和线缆必要时在 BIOS 中把 PCIe 模式从 Gen4 降到 Gen3 做稳定性验证。不要第一反应就换显卡。7. 遇到 GPU 资源效率问题时的排查顺序7.1 七步排查链路遇到 GPU 利用率低下的问题按以下顺序排查可以避免在一堆信息里失去方向。先确认进程是否真的在使用 GPU。使用nvidia-smi查看进程列表确认程序 PID 出现在哪张卡上。采集一段时间的利用率曲线。单次采样没有说服力建议至少观察 60 秒。看 CPU 占用。如果程序某几个 CPU 核已经打满而 GPU 利用率低优先怀疑数据预处理和加载。检查磁盘 IO 和网络 IO。训练数据在远程存储上时网络等待会直接拖慢 GPU。查看功耗和温度。功率远低于 TDP说明 GPU 没有持续执行重计算任务。确认多卡通信拓扑。如果多卡训练出现周期性等待检查卡间链路是 NVLink 还是 PCIe。最后再深入代码层。用 Nsight Systems 或 PyTorch Profiler 抓取具体 kernel 时间线。7.2 常见问题速查表问题现象常见原因检查方式处理建议GPU 利用率在 0% 和 100% 间跳CPU 预处理跟不上观察 CPU 占用、DataLoader 参数增加 num_workers、prefetch_factor显存占满但利用率低模型权重占用计算不足查看进程显存分配减小 batch、检查模型参数量多卡只有一张在跑CUDA_VISIBLE_DEVICES 设置错误检查进程可见设备显式指定多卡编号Docker 无法识别 GPUNVIDIA Runtime 未配置docker info、nvidia-ctk cdi list执行 runtime configure 并重启 DockerWSL2 里 OpenGL 用 CPU宿主机驱动或 WSL 版本问题glxinfo更新驱动和 WSLComfyUI 报显存不足峰值显存超限查看显存曲线调低 batch、打开重分配温度过高导致频繁降频散热或功耗墙nvidia-smi -q -d CLOCK清理散热、设置功耗上限PCIe 错误计数增加链路不稳定dmesg、PCIE 信息检查供电、插槽、线缆注意排查时要记录时间点。利用率曲线的“上下文”比单个数字重要得多。训练启动、数据加载、模型编译、梯度同步阶段GPU 的利用模式完全不同。8. 把“让 GPU 不闲着”做成日常流程8.1 训练和推理发布前的检查清单每次启动大规模训练或上线推理服务之前可以用下面这份清单快速自检驱动、CUDA、PyTorch 版本是否兼容。当前任务使用的 GPU 编号是否明确。显存需求是否超过单卡容量是否需要梯度累积或 offload。DataLoader 参数是否与 CPU 核数、磁盘性能匹配。多卡任务是否确认卡间拓扑和通信方式。容器环境是否配置了 NVIDIA Container Runtime。是否有其他进程占用 GPU导致资源竞争。是否记录了本次运行的基础指标方便事后对比。8.2 低成本的优化顺序先不要急着加卡或者换更强的 GPU。多数项目的 GPU 浪费都发生在数据链路和任务调度层按以下顺序做成本更低用工具抓住数据确认 GPU 是否真的在空闲。调整 DataLoader 和 batch size解决大部分训练场景空等。开启混合精度扩大 batch提高计算密度。在推理场景引入动态批处理或连续批处理。用 TensorRT 或 ONNX Runtime 做计算图优化。如果依旧达不到目标再考虑量化、蒸馏、裁剪或更换硬件。8.3 利用率不是最终目标“让 GPU 不闲着”本质上是让 GPU 在单位时间内处理更多有效任务而不是让指标停留在 99%。有些场景下低利用率反而是正确选择在线 API 需要预留余量应对突发流量小请求任务不适合强行凑成大 batch生成类任务需要控制峰值显存。优化 GPU 的目标应该是让任务在满足延迟、吞吐和稳定性要求的前提下减少不必要的资源浪费。只要能稳定交付业务结果GPU 利用率只是辅助判断的指标不是唯一的考核标准。