2384 字
约 7 分钟
16
S23-A14 · GPU 利用率的三种低效:为什么卡很忙但没产出

一块 A100 的利用率长期显示 92%,团队以为"算力用满了"。而实际吞吐只有同规格机器的三分之一——因为那 92% 里有一半是"等数据":GPU 在做空转的等待循环,驱动统计成"忙碌"。

GPU 利用率是个被高估的指标。它统计的是"核有没有在跑",而不是"跑的是不是有用的计算"。这个指标的来源也值得知道:它采样的粒度通常在几百毫秒,所以"瞬时满载"与"持续满载"在看板上是同一个数——而这两者对吞吐的含义完全不同。
而后者要靠别的数才能看出来。


1. 三种"看起来很高"的低效

类型 特征
数据饥饿 利用率高但与吞吐不匹配
小批量 每步算得快但步数多
显存受限 频繁换入换出

第一项最典型:**批处理把数据从 CPU 拷到 GPU 的时间算作"忙碌"**第三项(显存受限)还有一个"隐性"形态:显存够用但带宽被打满,此时计算单元大部分时间在等数据,利用率高而吞吐低,表现与数据饥饿几乎一致。
,而这段时间算力是闲置的。判定方式是看"每秒处理的样本数",而不是利用率。

第三项表现为"利用率高但延迟抖动大":显存不够时驱动会把部分数据换出,实际是在搬运而不是计算。

2. 三个必须一起看的指标

指标 说明
吞吐(samples/s 或 tokens/s) 真实产出
显存占用与峰值 是否贴着上限
计算单元利用率(SM/Tensor Core) 真正在算的比例

第一项(吞吐)的定法要固定口径:tokens/s 与 samples/s 不能混用,输入长度不同时同一个 tokens/s 对应的算力消耗也不同。所以报告里必须写"平均输入长度"。
第二项要区分"平均"与"峰值":平均 60% 而峰值 98% 意味着已经没有余量,任何并发增加都会 OOM。

第三项比"整体利用率"细一层:Tensor Core 利用率低而整体利用率高,说明大部分时间在做非矩阵运算(如数据搬运、控制逻辑)。

// 一次看清三项(NVIDIA)
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv -l 1
// 更细:SM 占用与显存带宽(需要 profiler)
ncu --metrics sm__throughput.avg.pct_of_peak_sustained_elapsed,dram__throughput.avg.pct_of_peak_sustained_elapsed -c 1 python infer.py

3. 数据饥饿的识别与修复

现象 修复方向
GPU 忙但吞吐低 增加预取、加 worker
CPU 单核打满 数据解码是瓶颈
磁盘 IO 断续 数据本地化或缓存
每步之间有间隔 用 pinned memory + 异步拷贝

第四项是最容易见效的技术点:普通内存到显存是同步拷贝,会阻塞计算第一项(加 worker)有个上限:worker 数超过 CPU 核数后收益迅速下降,而每个 worker 都会复制一份数据预处理状态,内存可能成为新瓶颈。
;用 pinned(page-locked)内存配异步流,能把拷贝与计算重叠。

// 异步拷贝:让数据搬运与计算重叠
loader = DataLoader(ds, batch_size=64, num_workers=8,
                    pin_memory=True,            # 用 pinned 内存
                    prefetch_factor=4)          # 提前准备若干批
for batch in loader:
    x = batch.to("cuda", non_blocking=True)  # 异步 H2D
    y = model(x)
// 关键:num_workers 与 prefetch 不足时,GPU 就会等数据

4. 小批量的两笔账

维度 小批量 大批量
单步延迟 低 高
吞吐 低(核没喂满) 高
显存 低 高
数值稳定性 差(梯度噪声大) 好

推理场景常用"小批量 + 高并发"来压延迟,代价是吞吐上不去,因为每步的固定开销(kernel 启动、同步)被摊薄得不够。

第三项(数值稳定性)常被忽略:批量小到个位数时,训练中的批归一化统计会非常不稳定,表现为 loss 抖动。所以"小批量提延迟"通常只用于推理,不用于训练。
判定方式是算"每步耗时 / 批量":如果这个值随批量增大而明显下降,说明固定开销占比高,批量还有提升空间。

还有一个"服务形态"层面的低效:把多个小模型塞进同一块卡,看起来利用率高,实际每个模型都分不到足够的显存做批处理,于是三者的吞吐都不如"一块卡跑一个"。

// 三种部署形态的取舍
1 卡 1 模型:批处理空间足,延迟低,但卡数需求高
1 卡 N 模型:卡省了,但每模型 batch 受限,总吞吐可能更低
N 卡 1 模型(张量并行):单请求快,但通信开销与卡数成正比
// 判据:算总吞吐(所有模型 tokens/s 之和)而不是看利用率

5. 显存受限的表现

现象 说明
峰值贴近上限 无余量
偶发 OOM 长序列或大 batch 触发
吞吐随并发不增 已到显存墙
延迟抖动 换入换出

第二项的偶发性最难查:同样代码在 99% 请求下正常,1% 的长输入触发 OOM第四项(延迟抖动)的判据是"抖动是否与输入长度相关":相关则是显存换入换出,不相关则多半是排队或其它进程争抢。
,而错误信息只写"out of memory",不写"因为这条输入太长"。

缓解手段是"输入长度上限 + 动态批处理",而不是"把显存加满"——加显存只是把墙往后推,墙还在。

// 三个约束比"加显存"更有效
serving:
  max_input_tokens: 4096          # 硬上限,超限即拒绝
  max_batch_tokens: 16384         # 按 token 数而非请求数分批
  kv_cache_budget_ratio: 0.6      # 给 KV cache 的显存比例
// 效果:OOM 从"偶发"变成"可预测的拒绝"

监控上还有一个"看不见"的部分:CPU 侧的准备时间。分词、图像解码、请求路由都发生在 GPU 之外,而这些时间在 GPU 利用率上完全不可见——于是"GPU 利用率 92%、端到端延迟很高"这类现象,答案常常在 CPU 那一侧。

// 端到端拆解:把时间分到 GPU 内外
// 总延迟 = 排队 + 预处理(CPU)+ 计算(GPU)+ 后处理(CPU)
// 若预处理占比 > 30%,优化 GPU 无用
pidstat -u -t -p $(pgrep -f server) 1 5     # 看 worker 线程的 CPU 时间

6. 排错:五种"GPU 很忙但慢"

现象 成因 处理方向
利用率 95% 吞吐只有 1/3 数据饥饿 加 worker/prefetch
利用率随并发不升 显存墙 限制长度或分批
延迟高但吞吐正常 排队而非计算 看队列而非 GPU
单卡快、多卡慢 通信开销 检查并行策略
时快时慢 其他进程争抢 隔离或限额

第四项(多卡变慢)要看"扩展效率":8 卡跑出 5 卡的效果(62%)算正常,跑出 2 卡的效果说明通信是瓶颈。而通信瓶颈往往来自"每步数据量小、同步次数多"。
第五项在多租户环境里非常普遍:同一块卡上有别人的任务,利用率显示的是卡的总利用率,而你的任务只拿到一部分。

7. 边界:什么情况利用率低是正常的

场景 为什么 做法
小模型 + 低延迟要求 核喂不满是设计取舍 接受
稀疏/不规则计算 有分支与空转 接受
单请求在线推理 批量小 提高并发而非批量
训练的小 step 通信占比高 看扩展效率

判断口径一句话:每秒产出的 token 或样本数,是否与这块卡的规格相称。最后一条与"成本"有关的边界:利用率不是省钱的指标,吞吐/成本才是。同样一块卡,利用率 60% 但吞吐是别人的两倍,那就是更省的——所以优化目标应当写成"每美元多少 token",而不是"利用率多少"。
相称就别看利用率;不相称,利用率再高也是假象。


8. 自测

  1. GPU 利用率统计的是什么?为什么"92% 但吞吐只有 1/3"是常见现象?
  2. 三个必须一起看的指标分别是什么?为什么"平均显存 60% 而峰值 98%"意味着没余量?
  3. 数据饥饿的四种现象里,哪一种最容易被误判为"计算慢"?pinned memory 解决了什么?
  4. 小批量的两笔账怎么算?"每步耗时 / 批量"这个比值说明什么?
  5. 偶发 OOM 为什么难查?为什么"加显存"不是解法?
  6. 判断"利用率低是否正常"的那个问题是什么?
S23-A14 · GPU 利用率的三种低效:为什么卡很忙但没产出
https://www.clxhxhhr.top/posts/6492/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。