☰
GPU利用率低?从观测到优化,让每一块显卡都高效运转
2026/9/28 4:23:12 网站建设 项目流程

实际开发中很少遇到“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 Multiprocessor,SM)、显存控制器、以及用于数据拷贝的 DMA 引擎。三者都可能成为瓶颈。

常见的情况包括:

  • SM 很忙,但显存带宽不够,L2 缓存命中率下降,核心在等数据返回。
  • 显存占用很高,但计算请求很少,模型权重和中间激活值只是“占用空间”而已。
  • 数据通过 PCIe 从 CPU 拷贝到 GPU 的过程很慢,GPU 的 kernel 执行时间很短,大部分时间都在等拷贝完成。
  • 多卡之间做梯度同步,单卡算得很快,但通信阶段所有卡都在等待最慢的那张卡。

所以第一步要做的是区分瓶颈。GPU 利用率只是给了一个宏观信号,要精确定位“闲在哪一层”,还需要功耗、温度、显存带宽、拷贝引擎、SM Active 占比等更多指标。

1.2 用 nvidia-smi 先看宏观状态

NVIDIA 驱动自带的nvidia-smi是最直接的观测工具。建议以固定间隔采样,而不是只运行一次。

nvidia-smi --query-gpu=index,utilization.gpu,memory.used,power.draw,temperature.gpu --format=csv -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 密集启动。程序每次只提交很小的计算任务,比如逐元素操作一个很小的 tensor,GPU 启动 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 nvtop

nvtop对多卡机器非常有用,可以一眼看出每张卡的负载是否一致。不过它适合短时排查,不适合长期留存数据的场景。

2.2 长期监控:DCGM 是更完整的底座

真正要做性能优化,不能只靠人工盯终端。NVIDIA 官方提供的 DCGM(Data 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_workers=2调到num_workers=8,只看这一项差异。没有基线就改多个参数,最终只能确认“整体变快了”,却说不清是哪一个优化起了作用。

注意:不要只验证任务能跑完,还要记录每一步的时间分布。GPU 利用率只在训练结束后看平均数是远远不够的。

3. 训练场景:让数据搬运和计算真正重叠

3.1 DataLoader 配置是 GPU 空等的首要来源

PyTorch 训练中最容易被忽略的性能瓶颈,是 DataLoader 的 CPU 预处理速度赶不上 GPU 计算速度。

from torch.utils.data import DataLoader train_loader = DataLoader( dataset, batch_size=32, num_workers=8, prefetch_factor=4, pin_memory=True, persistent_workers=True, )

这里每个参数都值得解释:

  • num_workers:负责在子进程中预处理数据的进程数。大于 0 时,主进程只需要从队列中拿已经准备好的 batch。
  • prefetch_factor:每个 worker 提前加载多少个 batch。设置过小会导致 GPU 经常空等。
  • pin_memory=True:把数据固定在主机端页锁内存,可以加速 CPU 到 GPU 的拷贝。
  • persistent_workers=True:训练迭代完一轮后,不销毁 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_type="cuda", dtype=torch.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_DEVICES=0,1 torchrun --nproc_per_node=2 train.py

在代码内部,通常这样设置:

import torch import torch.distributed as dist dist.init_process_group(backend="nccl") local_rank = dist.get_rank() torch.cuda.set_device(local_rank) device = torch.device("cuda", local_rank)

torchrun会自动为每个进程分配对应编号。如果机器是四卡,但只想用物理编号 2 和 3,那么环境变量应该写成:

CUDA_VISIBLE_DEVICES=2,3 torchrun --nproc_per_node=2 train.py

这样进程内看到的编号就是 0 和 1,分别映射到物理卡 2 和 3。

多卡通信对 GPU 利用率的影响很大。单卡算得再快,如果梯度同步阶段所有卡都在等最慢的卡,整体效率就会被拉低。使用nvidia-smi topo -m可以查看卡间拓扑:

nvidia-smi topo -m

如果输出显示两张卡之间有NV#表示走 NVLink,PIX或PHB表示走 PCIe。NVLink 带宽更高,AllReduce 等待时间更短。多机训练还需要额外关注网卡和跨机通信带宽。

3.4 用 Nsight Systems 精确定位空闲区间

当 GPU 利用率低但不知道原因时,建议用 NVIDIA 提供的 Nsight Systems 做一次性能采样。

nsys profile --trace=cuda,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 --onnx=model.onnx --saveEngine=model.engine --fp16 --workspace=4096

这里的--fp16开启半精度推理,--workspace指定构建引擎时可用的显存上限。trtexec还可以用来做基准测试:

trtexec --loadEngine=model.engine --shapes=input: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。多个任务同时运行时,容易把所有负载都挤到第一张卡。

通过环境变量可以控制程序看到哪些物理 GPU:

export CUDA_VISIBLE_DEVICES=1,3 python train.py

这个变量会让程序以为机器上只有两张卡,编号分别是 0 和 1,实际对应物理卡 1 和 3。在多进程任务里,需要注意“进程内编号”与“物理编号”的映射,避免明明设置了多卡,却始终使用同一张物理卡。

在容器环境中同样适用:

docker run --gpus '"device=1,3"' --rm nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

5.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 --runtime=docker sudo systemctl restart docker

然后验证当前 Docker 运行时:

docker info | grep -i runtime

正常输出应该包含nvidia。继续用 CDI 列表确认:

nvidia-ctk cdi list

如果输出里能看到类似nvidia.com/gpu=0的条目,说明 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(f"Peak 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 : 0

Receiver 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 利用率低下的问题,按以下顺序排查,可以避免在一堆信息里失去方向。

  1. 先确认进程是否真的在使用 GPU。使用nvidia-smi查看进程列表,确认程序 PID 出现在哪张卡上。
  2. 采集一段时间的利用率曲线。单次采样没有说服力,建议至少观察 60 秒。
  3. 看 CPU 占用。如果程序某几个 CPU 核已经打满,而 GPU 利用率低,优先怀疑数据预处理和加载。
  4. 检查磁盘 IO 和网络 IO。训练数据在远程存储上时,网络等待会直接拖慢 GPU。
  5. 查看功耗和温度。功率远低于 TDP,说明 GPU 没有持续执行重计算任务。
  6. 确认多卡通信拓扑。如果多卡训练出现周期性等待,检查卡间链路是 NVLink 还是 PCIe。
  7. 最后再深入代码层。用 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 并重启 Docker
WSL2 里 OpenGL 用 CPU宿主机驱动或 WSL 版本问题glxinfo更新驱动和 WSL
ComfyUI 报显存不足峰值显存超限查看显存曲线调低 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 浪费都发生在数据链路和任务调度层,按以下顺序做成本更低:

  1. 用工具抓住数据,确认 GPU 是否真的在空闲。
  2. 调整 DataLoader 和 batch size,解决大部分训练场景空等。
  3. 开启混合精度,扩大 batch,提高计算密度。
  4. 在推理场景引入动态批处理或连续批处理。
  5. 用 TensorRT 或 ONNX Runtime 做计算图优化。
  6. 如果依旧达不到目标,再考虑量化、蒸馏、裁剪或更换硬件。

8.3 利用率不是最终目标

“让 GPU 不闲着”本质上是让 GPU 在单位时间内处理更多有效任务,而不是让指标停留在 99%。有些场景下,低利用率反而是正确选择:在线 API 需要预留余量应对突发流量,小请求任务不适合强行凑成大 batch,生成类任务需要控制峰值显存。优化 GPU 的目标,应该是让任务在满足延迟、吞吐和稳定性要求的前提下,减少不必要的资源浪费。只要能稳定交付业务结果,GPU 利用率只是辅助判断的指标,不是唯一的考核标准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询