☰
显存省了GPU利用率却掉?从瓶颈排查到Batch Size与MFU调优全指南
2026/10/2 15:21:14 网站建设 项目流程

1. 显存降了,训练反而慢了:先搞清楚 GPU 到底在等什么

做 LLM Training 的人应该都经历过类似的心路历程:模型 7B 参数,单卡 24G 显存装不下,于是梯度检查点、混合精度、优化器 offload 轮番上阵,总算把显存占用从“OOM 边缘”压到 85%、80%、75%。心里正美滋滋,结果打开nvidia-smi一看,GPU 利用率不但没上去,反而从 95% 掉到了 60% 甚至更低。更扎心的是,每个 step 的耗时反而变长了,训练速度直接倒退。这个 Lab 系列写到第 10 期,我觉得值得专门花一篇来讲讲这个反直觉问题:显存明明省下来了,为什么 GPU 还是吃不满?

这篇文章就是来拆解这个现象的。我把它背后的原理、排查链路和最终解决方案完整捋了一遍,适合正在做 LLM 微调或预训练、被显存逼着上了各种省显存手段、又发现训练速度不升反降的读者。不管你是刚入门的小白还是已经踩过几个坑的老手,都能在这里找到可以直接抄作业的排查步骤和调优思路。

1.1 显存占用和 GPU 利用率是两回事

很多人把nvidia-smi里的 Memory-Usage 和 Volatile GPU-Util 当成两个能互相解释的指标,这是最大的误区。显存占用是“货仓里堆了多少货”,GPU 利用率是“装卸工人忙不忙”。货仓堆得满,不代表工人在干活;货仓空了,工人更不一定闲着——他可能在等货车把下一批货送来。

GPU 利用率这个数字,本质上反映的是流式多处理器(SM)在一个统计周期内是否有活动 kernel 在执行,而且统计窗口通常长达一秒;显存占用则只是显存分配器登记的字节数。两者没有必然的线性关系。省显存的手段,比如梯度检查点、优化器 offload,本质都是“用别的东西换存储空间”——别的东西可能是计算、可能是通信、也可能是等待。当你把显存压力卸下来之后,代价会原封不动地体现在 GPU 的“吃不饱”上。所以别一看到显存降了就开心,先看看吞吐有没有跟着降。

1.2 一组实测数据:梯度检查点 + Offload 后发生了什么

我在一个 7B 参数模型、单张 24G 显存、batch size 12 的微调任务上做过对比实验。基线是 fp16 精度 + Adam 优化器,不开任何省显存手段;优化组则开了梯度检查点,并把优化器状态 offload 到 CPU 内存。实际跑出来的数据是这样的:

配置显存峰值step 耗时GPU 利用率吞吐 tokens/s
基线 fp1623.4G1.85s94%3.9k
开启检查点 + offload14.2G2.71s62%2.7k

显存实打实省下了 9G,但每个 step 反而慢了约 0.9 秒,吞吐掉了三成。这就是典型的“省显存省出了新瓶颈”。出现这种结果并不奇怪:你省显存的手法本身就在烧时间。但到底烧在哪个环节,是数据加载、重计算、还是 PCIe 传输?接下来的章节就是一步步把这笔时间账算清楚。

2. 三分钟定位:是数据没喂进来,还是 Kernel 根本没跑满

当 GPU 利用率下降,第一步必须区分两种完全不同的病因。一种是数据管线喂得太慢,GPU 在饿肚子;另一种是 GPU 一直有活干,但每个 kernel 都没把算力跑满。这两者的调试手段完全不一样,先定位再动手,省得瞎调半天浪费感情。

2.1 特征一:利用率呈锯齿状,周期归零 → 数据管线瓶颈

如果 GPU 利用率曲线是“冲高—骤降—归零—再冲高”的形状,像锯齿一样周期性波动,基本可以断定是数据供给问题。训练循环的典型节奏是:GPU 花 200ms 跑完当前 batch 的 forward + backward,然后等 DataLoader 花 500ms 准备下一个 batch,这段时间 GPU 完全闲置。

为什么这个问题之前没暴露?因为以前算得慢,GPU 跑一个 batch 要 900ms,数据加载的 500ms 被完整掩盖住了。你把计算提速之后,数据加载的短板立刻浮出水面。这就好比一条产线,以前组装环节要 1 小时,配送环节 30 分钟根本不影响;现在组装提速到 20 分钟,配送却还是 30 分钟,产线就只能干等着。

验证方法很简单:训练过程中跑watch -n 1 nvidia-smi,如果每次刷新都能看到 util 在 0% 和 90% 之间反复横跳,十有八九是数据喂不上了。更精确的验证是在训练循环里直接统计 DataLoader 的__next__耗时,如果单次加载时间超过 GPU 计算时间的一半,那就实锤了。

2.2 特征二:GPU 全程有事干但吞吐低 → 单 Kernel 效率问题

另一种情况更隐蔽:nvidia-smi显示利用率一直维持在 70%、80%,不归零,但吞吐就是上不去。这时候问题不在数据管线,而在单个 kernel 的计算密度或访存模式。

典型原因有三个:一是 batch size 太小,导致 GEMM(通用矩阵乘法)的 M 维度不足,GPU 上一大半 SM 处于半占用状态;二是显存读写过于频繁,计算单元在等数据,形成 memory-bound;三是 kernel 太碎,频繁 launch 的开销占比过高。这三种原因在 profiler 里的表现还不一样,需要放到 2.3 来验证。

判断方法是把利用率曲线放大到毫秒级。nvidia-smi的 1 秒采样太粗,它显示 80% 完全可能是“50% 时间算满、50% 时间全空”平均出来的结果。要看到真相,必须上 profiler。

2.3 快速诊断命令与工具组合

分享一套能直接上手的排查命令,按粒度从粗到细排列:

  • 先看粗粒度:nvidia-smi dmon -s uct -c 100,这是每 200ms 采样一次 SM 利用率、温度、显存读写;
  • 再看实时曲线方向:nvtop,比nvidia-smi的 watch 模式直观很多;
  • 最终裁决交给 profiler:nsys profile --trace=cuda,nvtx --output=report python train.py,跑完用nsys stats report.nsys-rep看 CPU 侧和 CUDA 侧的时间分布。

我在实际操作中习惯给训练脚本加一个 PyTorch Profiler 的开关,跑两三个 step 就够。核心看一条:CPU 侧的时间远大于 CUDA 侧的时间,说明数据或 host 侧逻辑在拖后腿;如果 CUDA 侧时间占比高但吞吐依然低,说明 kernel 本身效率有问题。前者去调 DataLoader 和预处理,后者去调 batch size 和模型实现。别一上来就怀疑模型架构,大部分“GPU 吃不满”都是数据或者配置层面的问题。

3. 你用的省显存技巧,每一条都带着“价格标签”

很多人只知道省显存手段能降占用,却不知道它们各自的“账单”长什么样。这一节把最常用的三种手段逐个拆开,看看它们到底在用什么换显存,以及为什么会让 GPU 吃不饱。

3.1 梯度检查点:用前向重计算换显存,算力开销你算过吗

梯度检查点(activation checkpointing)的原理是:前向传播时只保存部分中间激活值,反向传播需要哪些激活值,就临时重算一遍。以 7B 模型为例,如果每层 Transformer block 设一个检查点,反向传播时每个 block 都要额外跑一遍完整前向,总计算量大约增加 30% 到 40%。

代价的本质是算力:你本来一个 step 跑 10 万亿次浮点运算,现在要跑 14 万亿次才能完成同一个参数更新。如果 GPU 算力本来就满载,step 时间就必然变长。更有迷惑性的是,重计算会让 GPU 看起来更忙——nvidia-smi利用率甚至可能是满的,但吞吐反而掉下来了。很多人看到“利用率高、吞吐低”更懵,其实根源就在这。

所以梯度检查点别无脑开。只有当它省出的显存能支撑你把 batch size 翻倍、并带来实打实的吞吐收益时才值得。简单估算:step 慢 40%,但 batch 翻倍后每 step 处理的样本量多了 100%,净吞吐仍然能提升 40% 左右。这笔账要算。

3.2 Offload 优化器状态:等待 PCIe 传输时 SM 只能闲着

Offload 省显存的逻辑是把 Adam 优化器状态搬到 CPU 内存。Adam 每个参数要存三份 fp32 数据:主权重、一阶矩、二阶矩,也就是 12 字节。7B 模型光优化器状态就有大约 84GB,这对显卡来说是天文数字,放到系统内存却很容易。但问题在于,训练过程中 GPU 和 CPU 之间要来回搬运数据,全程走 PCIe 总线。

PCIe 4.0 x16 的真实单向带宽大约 25GB/s,双向合计也没你想象的高。84GB 的状态哪怕只按块来回传,也是不小的开销,而且这段时间 SM 是完全空闲的。更麻烦的是,如果同时开了数据并行,梯度 allreduce 的通信会和 offload 的数据传输抢 PCIe 带宽,两边一起变慢。这种配置下 GPU 吃不满不是玄学,是总线带宽的天花板。所以我一般建议:能不开 offload 就不开,或者只在显存极度紧缺时作为临时兜底方案。

3.3 混合精度的一体两面:小 Batch 下的 Kernel Launch 开销

混合精度(fp16/bf16)能省一半显存,这个大家都知道。但很多人忽略了它对 kernel 执行形态的影响。小 batch 下,fp16 kernel 的 SM 硬件占用率可能达不到饱和阈值,计算单元大量空转;同时小 GEMM 的启动开销占比明显上升,kernel launch 的延迟被放大,GPU 就会出现“忙但产出不高”的怪象。

这个问题的本质还是 batch size 太小。省显存后如果不调整 batch size,等于把货仓清空了,但每趟只送半车货,工人当然干不满。混合精度本身没问题,问题出在省出来的空间没有重新投入到算力密度上去。

3.4 MoE 场景:负载不均比显存更影响利用率

如果你的模型是 MoE 架构,比如最近很多人在 8G 显存上跑 Minimax H3、或者自己在做 MoE 微调,GPU 吃不满的另一个重要来源是专家负载不均衡。MoE 的优势是每次只激活部分专家,省显存效果明显;但 token 在不同专家上的分配天然不平均,有的专家被疯狂点击,有的专家闲得发慌。在 expert parallel 场景下,所有卡要等最慢的专家算完才能进入下一步,整体利用率就被拉下来了。

这里顺便说一个很多人问过的点:MoE 并不意味着所有参数必须同时驻留在显存里。它支持按需把专家参数加载到显存,这也是低显存跑模型常用的手段。但如果你采用动态加载专家的策略,每步切换专家的 H2D 传输同样会制造大量 GPU 空窗。显存是省了,GPU 吃不满的问题反而更突出。所以 MoE 调优时,除了盯显存,还要多看各专家和各卡的耗时分布,负载均衡往往比显存占用更能决定整体吞吐。

4. 把省下来的显存花出去:Batch Size、梯度累积和 MFU 的取舍

排查完省显存手法的隐藏代价,接下来的问题就是:显存已经省出来了,怎么把它变成真实的训练速度?答案不是“维持原样”,而是把省下来的空间重新投入到更大的 batch size 上。

4.1 省显存的正确姿势是换 Batch Size,而不是停在原地

很多人省显存是为了“跑起来”,省完发现能跑了,就不再动任何配置,这是最大的浪费。显存省下来的唯一目的,应该是让每个 GPU kernel 干更饱满的活。最直接的抓手就是增大 batch size。

为什么 batch size 直接决定 GPU 利用率?以 Transformer 里的 GEMM 为例,矩阵乘法的计算形状是三维的(M、N、K),其中 M 就是 batch 维度。batch size 从 1 提到 8,M 维度变大,SM 上的 tile 划分更满,利用率能轻松从 40% 拉到 90% 以上。这就好比工人搬箱子,一次搬 1 箱和一次搬 8 箱,效率完全不是一个量级。

具体操作上,我在前面那个 7B 实验里,省出 9G 显存后把 batch size 从 12 调到 22,显存峰值仍然安全。结果 step 时间从 2.71s 涨到 3.1s,但吞吐从 2.7k 变成了 4.5k tokens/s——不仅超过了省显存前的基线,还比“不省显存”的原始配置高出一截。这才是省显存该有的结果。

4.2 梯度累积解决不了 Kernel 密度问题,只能降低优化器频率

但如果你把 batch 加到 22 就爆显存了怎么办?答案是梯度累积(gradient accumulation),把大 batch 拆成几个小 batch 分别算梯度,攒够再更新参数。注意:梯度累积能解决显存峰值问题,却不能提升 GPU 利用率,因为每个 micro-batch 的 kernel 依然是小的,SM 该吃不饱还是吃不饱。

再说得直白一点,梯度累积是给优化器看的,不是给 GPU 看的。它能降低梯度噪声、稳定训练,但不会让单次 kernel 的干活密度变大。正确组合是:先用省显存技术降低单 batch 显存开销,尽量增大真实 batch size;如果真实 batch 仍然受显存限制,再用梯度累积把等效 batch 做上去。

还有一个容易被忽视的坑:开了梯度累积后,如果用 DDP 做分布式训练,每个 micro-batch 都会触发梯度 allreduce,通信次数会翻好几倍,GPU 利用率又被通信拉低。正确做法是前几个 micro-batch 只累加梯度、不同步,最后一个 micro-batch 才做梯度同步。PyTorch 的no_sync()上下文就是干这个的,别漏掉。

4.3 用 MFU 做定量评估:你的训练到底离理论峰值差多远

各种手段调完,需要一个统一指标来评估:MFU(Model FLOPs Utilization,模型浮点利用率)。它等于实际每秒完成的 FLOPs 除以 GPU 理论峰值 FLOPs。LLM 训练里 MFU 能到 45% 到 55% 已经算很好,很多看起来没毛病的训练跑到 30% 也正常。

计算公式不复杂:一个 step 的标准 FLOPs 约等于 6 乘以参数量再乘以处理的 token 数(粗略覆盖前向和反向)。把 step 耗时代入,得到实际 FLOPs/s,再除以 GPU 理论峰值就是 MFU。我把我那个实验的三种配置算了下:

配置batch sizestep 耗时MFU 估算
基线 fp16121.85s约 34%
省显存 + 原 batch122.71s约 23%
省显存 + 加大 batch223.10s约 38%

结论很明显:同样一堆省显存手段,配合 batch size 调整后,MFU 不降反升。所以优化的重点不是盯着显存数字,而是盯着 MFU 和吞吐这两个真正反映训练效率的指标。

5. 从 Nsight 到 PyTorch Profiler:一套能抄的 GPU 利用率体检流程

前面反复提到 nvidia-smi 的局限性,这里展开讲清楚,顺便给出一套完整的体检流程。观测粒度决定判断质量,用错工具会把排查方向带偏。

5.1 观测粒度决定判断质量:为什么 nvidia-smi 会骗你

nvidia-smi的 GPU-Util 是基于一秒采样周期算出的“这期间有没有 kernel 在跑”的粗略比例,它不是真正的算力负载。一个 kernel 只占满 10% 的 SM,但只要它在这一秒内执行了,利用率就可能显示 100%;反过来,kernel 密集但每个都很短,采样点恰好落在空隙上,显示 0%。也就是说,肉眼看到的数值不仅滞后,还是失真的。

要做靠谱的体检,必须把观测粒度压缩到 kernel 级别。我一般分三级:第一级用nvidia-smi dmon -s uct拿 200ms 粒度的 SM 活动和显存读写,适合快速判断“是不是数据管线在卡”;第二级用 Nsight Systems 看整条训练时间轴上 CPU、GPU、内存传输的重叠关系;第三级用 PyTorch Profiler 落到每个 kernel 的名字和耗时。

5.2 实操:用 nsys 和 torch.profiler 抓出吃不满的元凶

Nsight Systems 的固定用法我贴一下,跑 5 个 step 就够:

nsys profile --trace=cuda,nvtx,osrt --output=llm_training python train.py --max-steps 5

打开生成的llm_training.nsys-rep,重点看三样东西:CUDA 内核条带是否连续铺满时间轴;CPU 侧的预处理和 DataLoader 加载是否零散分布;以及有没有大段空白,空白前后的时间戳对应的什么操作。

如果时间轴里 GPU 条带断断续续、CPU 侧却常有数据准备操作,说明数据管线是瓶颈;如果 GPU 条带连续但吞吐低,就去 PyTorch Profiler 看单个 kernel。PyTorch Profiler 的接入很轻量:

from torch.profiler import profile, ProfilerActivity with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof: for _ in range(3): loss = train_step() prof.export_chrome_trace("trace.json")

用 Chrome 打开 trace.json 看火焰图,注意 gap 出现的位置:如果 gap 之前是 DataLoader 相关操作,通常是数据在排队;如果 gap 前后都是模型 kernel,则可能是 kernel 间依赖等待(比如 allreduce 同步)或者小 kernel 的 launch 开销太大。

5.3 体检结果解读与对应的调优出手清单

最后放一张我常用的对照清单,算是排查“GPU 吃不满”的完整套路:

体检结果根因优先出手方向
GPU 条带大面积空白 + CPU 侧忙数据管线瓶颈调大 num_workers、prefetch_factor、pin_memory;预处理提前做并持久化
GPU 条带连续但 kernel 很细碎小 batch / kernel launch 开销增大 batch size;合并小操作;用 torch.compile 减小 launch 开销
大量 memcpy H2D/D2Hoffload 或数据拷贝过频减少 offload 频率;检查 DataLoader 是否有不必要的拷贝
通信时间占比高梯度同步 / 专家通信调整并行策略;梯度累积后延迟 allreduce
单个大 kernel 耗时异常访存或算子实现问题换 kernel 实现、调整 attention 后端、关注显存带宽占用

这套流程走完,你基本能回答“显存省下来了,为什么 GPU 还是吃不满”的绝大多数变体。核心思路其实就一句话:先分清楚 GPU 是在等数据还是在磨洋工,然后对每一个磨洋工的原因下手。省显存只是手段,把省出来的空间换成更大的 batch 和更高的 MFU,才是目标。我在实际项目中吃过不少亏,最深刻的体会是别被nvidia-smi上的那排数字牵着走,它只是个起点,不是结论。

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

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

立即咨询