ARTICLE DETAIL

资讯详情

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

显存省了GPU利用率却掉?从瓶颈排查到Batch Size与MFU调优全指南

显存省了GPU利用率却掉?从瓶颈排查到Batch Size与MFU调优全指南 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 模式直观很多最终裁决交给 profilernsys profile --tracecuda,nvtx --outputreport 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 提到 8M 维度变大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 做定量评估你的训练到底离理论峰值差多远各种手段调完需要一个统一指标来评估MFUModel 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 --tracecuda,nvtx,osrt --outputllm_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上的那排数字牵着走它只是个起点不是结论。
返回列表