news 2026/11/2 12:32:28

显存省了GPU却吃不满?LLM训练中显存优化与吞吐提升的冲突与解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
显存省了GPU却吃不满?LLM训练中显存优化与吞吐提升的冲突与解法

1. 显存省下来了,GPU 为什么还是吃不满

显存占用从 22GB 压到 9GB,nvidia-smi里显存那一栏确实绿油油一片很健康,结果 GPU 利用率死活卡在 30% 上下,风扇都不怎么转,训练一个 epoch 的时间跟没优化前一模一样。这个场景我猜不少人都遇到过,尤其是刚上手 LLM 训练、兴冲冲搞完梯度检查点、LoRA、8-bit 优化器之后,满心以为能起飞,结果发现显存是省了,速度一点没变。

这篇就专门聊这个坑。核心关键词是LLM、GPU、显存、PyTorch、CUDA,但真正要解决的不是"怎么省显存",而是"显存省下来之后,为什么算力没被用起来"。适合已经能跑通训练脚本、做过一轮显存优化、但发现吞吐没提升的中阶玩家。如果你还在纠结torch.cuda.is_available()返回 False,那这篇可能稍微超前了一点,但看看也无妨,迟早会遇到。

先把结论摆前面:显存和算力是两条独立的流水线,省显存的操作很多时候是在给算力流水线添堵。梯度检查点用时间换空间,8-bit 优化器增加了量化/反量化开销,LoRA 让每个 step 的计算量变小但 kernel 启动次数没变,这些都会让 GPU 的 SM(流多处理器)闲着。GPU 吃不满,本质上是"喂给它的活不够连续、不够大块",而不是"显存不够它施展"。

下面我按自己踩坑的顺序,把这件事拆成四块讲:先讲清楚显存和利用率为什么是两回事,再讲几个最常见的"省显存反而拖慢"的操作,然后是真正能把 GPU 喂饱的实操手段,最后是我自己整理的一份排查清单。

2. 显存和 GPU 利用率,压根不是一回事

2.1 显存是"仓库",利用率是"流水线开工率"

打个比方。显存相当于工厂的仓库,GPU 的计算核心(SM)相当于流水线工人。你把仓库从 22 平米缩到 9 平米,确实能放下更多货了,但工人干活的速度取决于"传送带上有没有连续的活送过来",跟仓库多大没直接关系。仓库小一点,工人照样能满负荷;仓库再大,传送带断断续续,工人照样摸鱼。

nvidia-smi里的 GPU-Util 这一栏,官方定义是"过去采样周期内,至少有一个 kernel 在执行的 SM 占比"。注意关键词:至少有一个 kernel 在执行。这意味着哪怕一个 SM 上只跑了一个线程,这一瞬间它也算"忙"。所以 GPU-Util 高不代表算力用满了,它只代表"没完全闲着"。真正反映算力利用的是 SM occupancy、Tensor Core 活跃度、显存带宽利用率这些更细的指标。

我见过太多人盯着 GPU-Util 90% 就以为万事大吉,结果torch.profiler一开,发现大部分时间花在 kernel launch 的间隙和等待数据上,真正的矩阵乘只占了一小部分。所以第一步认知要摆正:GPU-Util 是个粗指标,别拿它当唯一真理。

2.2 一个 step 里,GPU 到底在等什么

一个训练 step 拆开看,GPU 的时间大致花在这几块:

  • 数据搬运:从 CPU 内存把 batch 拷到显存(H2D),如果num_workers不够或者数据预处理太重,GPU 就在这干等。
  • 前向计算:一堆 kernel 排队执行,矩阵乘、激活、归一化。
  • 反向计算:通常是前向的两倍计算量。
  • 优化器更新:Adam 这类优化器要读写大量状态,显存带宽压力大。
  • kernel launch 间隙:每个 kernel 启动都有几微秒的固定开销,kernel 越小越多,间隙占比越高。

省显存的操作,往往是在放大 kernel launch 间隙或者增加额外的搬运/量化开销。比如梯度检查点,前向的时候不存中间激活,反向的时候重新算一遍,等于把前向算了两遍,计算量上去了,但显存下来了。如果你的瓶颈本来就在计算,这一下就更慢了。

2.3 显存优化和吞吐优化,目标函数不一样

这里有个很关键的认知:显存优化和吞吐优化是两个不同的目标函数,甚至经常互相冲突。

优化手段显存效果吞吐效果原因
梯度检查点大幅降低通常下降前向重算,计算量增加
8-bit 优化器降低可能下降量化/反量化开销
LoRA大幅降低小 batch 下下降计算量小,kernel 启动占比高
增大 batch上升上升计算密度提高
混合精度降低上升Tensor Core 加速
FlashAttention降低上升减少显存读写

看这张表就明白了:真正"既省显存又提吞吐"的只有混合精度和 FlashAttention 这类算法级优化,其余大部分是拿吞吐换显存。所以当你做完一轮显存优化发现 GPU 吃不满,先别急着怀疑代码,先想想你用的手段是不是本来就牺牲了吞吐。

3. 几个"省了显存却拖慢 GPU"的典型操作

3.1 梯度检查点:用两倍前向换一半激活

梯度检查点(gradient checkpointing)的原理是:前向的时候只存少数几个"检查点"的激活,反向传播需要中间激活时,从最近的检查点重新前向算一遍。显存能降 50% 到 70%,代价是计算量增加约 30% 到 50%。

问题在于,很多人开了检查点之后,batch size 没跟着调大。比如原来 batch=8 显存爆了,开检查点后 batch=8 能跑了,但计算量多了 40%,GPU 利用率反而从 70% 掉到 50%。正确的做法是:开了检查点,就把省下来的显存换成更大的 batch,让每个 step 的计算密度提上去,把重算的开销摊薄。

我自己的经验是,开检查点后 batch 至少要翻倍,否则纯亏。如果显存省下来但 batch 加不上去(比如被序列长度卡住),那检查点就是负优化,不如换个思路。

3.2 8-bit 优化器:省的是状态,加的是量化开销

Adam 优化器要为每个参数存两份状态(一阶矩、二阶矩),7B 模型 fp32 状态下光优化器状态就 56GB。bitsandbytes 的 8-bit Adam 把这部分压到 14GB,确实香。但每次更新都要做量化、反量化,这些操作本身要读写显存、启动 kernel。

在小模型或者小 batch 下,优化器更新占总时间的比例不低,8-bit 带来的额外开销可能吃掉收益。实测下来,7B 以上模型、batch 足够大时,8-bit 优化器的吞吐损失通常在 5% 以内,可以接受;但 1B 以下的小模型,损失可能到 15%,这时候就得掂量了。

提示:8-bit 优化器省的是优化器状态显存,不省激活显存。如果你的瓶颈是激活(长序列、大 batch),它帮不上忙。

3.3 LoRA:计算量太小,kernel 启动成了瓶颈

LoRA 只训练低秩矩阵,参数量可能只有全量的 1%,显存省得飞起。但正因为计算量小,每个 step 的 kernel 执行时间很短,kernel launch 的固定开销占比就上去了。GPU 大部分时间在等 CPU 发指令,而不是在算。

这种情况在单卡小模型上特别明显。解决办法是增大 batch、增大 rank、或者用torch.compile把多个小 kernel 融合成大的。我试过在 LoRA 训练里把 batch 从 4 提到 32,GPU 利用率直接从 25% 拉到 75%,效果立竿见影。

3.4 频繁的 CPU-GPU 同步:隐形的性能杀手

这个坑很隐蔽。代码里但凡出现.item()、.cpu()、print(loss)、if loss > threshold这类操作,都会强制同步,让 GPU 停下来等 CPU。一个 step 里同步几次,GPU 利用率就废了。

我见过最离谱的一段代码,在训练循环里每个 step 都loss.item()打印,还顺手torch.cuda.memory_allocated()查显存。这两个操作加起来,GPU 利用率从 85% 掉到 40%。把打印改成每 100 步一次,利用率立刻回来。

4. 把 GPU 真正喂饱的实操手段

4.1 先定位瓶颈:用 profiler 而不是猜

在动手优化之前,先搞清楚瓶颈在哪。PyTorch 自带的 profiler 是最靠谱的工具:

import torch from torch.profiler import profile, ProfilerActivity with profile( activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1), on_trace_ready=torch.profiler.tensorboard_trace_handler('./log'), record_shapes=True, profile_memory=True, ) as prof: for step, batch in enumerate(loader): if step >= 6: break train_step(batch) prof.step()

跑完之后看 TensorBoard 的 trace,重点看三件事:GPU 上有没有大段空白(说明在等数据或等 CPU)、kernel 是不是又小又碎(说明 launch 开销大)、显存带宽利用率高不高(说明是不是 memory-bound)。

如果 GPU 时间线上大片空白,问题在数据加载或 CPU 侧;如果 kernel 密集但每个都很短,问题在 kernel launch;如果 kernel 很长但 SM occupancy 低,问题在并行度不够。

4.2 数据加载:让 GPU 永远有活干

数据加载是最常见的瓶颈,尤其在磁盘慢、预处理重的时候。几个关键参数:

  • DataLoader(num_workers=N):N 一般设成 CPU 核心数的 1/4 到 1/2,太多反而抢资源。我一般从 4 开始试。
  • pin_memory=True:让数据放在锁页内存里,H2D 拷贝能走 DMA,快很多。
  • persistent_workers=True:避免每个 epoch 重建 worker 进程。
  • prefetch_factor=2或更高:提前预取几个 batch。

如果预处理特别重(比如图像增强、tokenize),考虑离线预处理,把结果存成二进制文件,训练时直接读。我做过一个项目,把在线 tokenize 改成离线,数据加载时间从每 step 200ms 降到 20ms,GPU 利用率从 45% 拉到 90%。

4.3 增大 batch:最直接的提利用率手段

batch 越大,每个 step 的计算量越大,kernel launch 开销被摊薄,GPU 越容易吃满。显存省下来之后,第一件事就是把 batch 加到显存快满为止。

具体怎么加:先设一个保守值,跑几步看显存峰值,然后按 1.5 倍递增,直到接近显存上限(留 10% 余量给碎片)。如果 batch 加到某个值后 OOM,就回退一档。

配合梯度累积,可以在显存受限时模拟大 batch:

accum_steps = 4 for i, batch in enumerate(loader): loss = model(batch) / accum_steps loss.backward() if (i + 1) % accum_steps == 0: optimizer.step() optimizer.zero_grad()

注意梯度累积不减少 kernel launch 次数,它只是让优化器更新频率降低。所以它对显存友好,但对 kernel launch 瓶颈帮助有限。真正要提利用率,还是得靠单次 batch 变大。

4.4 torch.compile:把碎 kernel 融合起来

PyTorch 2.0 引入的torch.compile能把多个小算子融合成一个大 kernel,直接减少 launch 次数。对 LoRA 这种计算量小的场景效果特别明显:

model = torch.compile(model, mode="max-autotune")

max-autotune模式会花几分钟编译,但跑起来之后吞吐通常能提 20% 到 50%。我实测过一个 LoRA 微调任务,编译前 GPU 利用率 30%,编译后 65%,训练时间砍掉近一半。

注意:torch.compile首次编译有开销,短任务可能不划算。另外它跟某些自定义 kernel 可能不兼容,遇到报错先退回mode="default"试试。

4.5 混合精度和 FlashAttention:算法级的双赢

这两个是少数"既省显存又提吞吐"的手段。混合精度(bf16/fp16)让 Tensor Core 跑起来,矩阵乘速度翻倍,显存减半。FlashAttention 重写了注意力计算,减少显存读写,长序列下提速明显。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() with autocast(dtype=torch.bfloat16): output = model(input) loss = criterion(output, target) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

bf16 比 fp16 更稳,不容易溢出,A100/H100 上优先用 bf16。消费级卡如果只支持 fp16,记得配 GradScaler。

5. 排查清单和常见问题速查

5.1 GPU 吃不满的排查顺序

遇到 GPU 利用率低,我一般按这个顺序查:

  1. 看数据加载:profiler 里 GPU 时间线有没有大段空白?有就是数据瓶颈,调num_workers、pin_memory、prefetch_factor。
  2. 看同步操作:代码里有没有.item()、.cpu()、频繁 print?有就删掉或降频。
  3. 看 batch size:显存还有余量吗?有就加大 batch。
  4. 看 kernel 粒度:kernel 是不是又小又碎?是就上torch.compile。
  5. 看并行度:SM occupancy 低不低?低就检查模型并行策略、序列长度。
  6. 看优化器:8-bit 优化器在小模型上是不是负优化?是就换回 fp32。

5.2 常见问题速查表

现象可能原因解决方向
GPU-Util 低但显存满显存瓶颈,batch 太大减 batch 或开检查点
GPU-Util 低且显存空数据或 CPU 瓶颈查 DataLoader、同步操作
GPU-Util 高但训练慢kernel 效率低上混合精度、FlashAttention
开了检查点更慢batch 没跟着加加大 batch 摊薄重算
LoRA 训练利用率低kernel 太碎torch.compile、加大 batch
多卡利用率不均负载不均衡检查数据分片、梯度同步

5.3 几个我踩过的坑

坑一:以为显存省了就能加 batch,结果序列长度卡死。长序列训练里,激活显存跟序列长度平方相关,检查点省下来的显存可能只够把 batch 从 2 加到 3,收益有限。这种情况不如考虑序列并行或者 FlashAttention。

坑二:num_workers设太大反而慢。worker 进程之间抢 CPU、抢内存带宽,设成 CPU 核心数反而拖慢。我一般设 4 到 8,配合persistent_workers。

坑三:torch.compile编译时间被忽略。小任务跑 100 步就结束,编译花 3 分钟,纯亏。短任务别开,或者用mode="reduce-overhead"减少编译时间。

坑四:多卡训练里梯度同步成了瓶颈。DDP 每个 step 都要 all-reduce 梯度,如果模型大、卡间带宽低,GPU 就在等通信。可以开gradient_as_bucket_view=True,或者用梯度累积减少同步频率。

6. 我个人的一点经验

显存优化和吞吐优化,本质上是两个方向的事。省显存是为了"能跑起来",提吞吐是为了"跑得快",前者是及格线,后者才是性能。很多人做完显存优化就以为大功告成,其实真正的调优才刚开始。

我自己的习惯是:每做一次显存优化,就重新测一遍吞吐,并且尝试把省下来的显存换成更大的 batch。如果 batch 加不上去,就说明这个优化手段在当前场景下是负收益,得换思路。这个循环做几轮,基本就能把 GPU 喂到 80% 以上。

最后分享一个小技巧:训练脚本里加一个简单的吞吐监控,每 100 步打印一次samples/sec和 GPU 利用率,跑起来一眼就能看出优化有没有效果。比事后翻 profiler 方便多了。

import time start = time.time() for step, batch in enumerate(loader): train_step(batch) if step % 100 == 0 and step > 0: elapsed = time.time() - start throughput = 100 * batch_size / elapsed print(f"step {step}, throughput {throughput:.1f} samples/s") start = time.time()

这个监控加上nvidia-smi -l 1在旁边开着,优化效果一目了然。踩过的坑多了,就会明白一个道理:GPU 吃不满,八成不是显存的锅,而是你没把活连续地喂给它。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 12:32:19

AI+信创协同办公:安全高效的企业数字化解决方案

1. AI信创协同办公的行业背景与核心价值 信创产业作为国家信息技术应用创新的重要战略方向,正在重塑企业数字化基础设施的底层架构。而协同办公作为企业高频使用的生产力工具,其技术栈的国产化替代进程直接关系到信息安全与产业自主可控。根据第三方机构…

作者头像 李华
网站建设 2026/11/2 12:32:03

GAIIS 2026会议投稿指南:生成式AI与信息安全前沿

1. 会议背景与核心价值解析 GAIIS 2026作为IEEE旗下的旗舰学术会议,已成功举办两届并形成稳定的学术影响力。从往届数据来看,该会议最突出的特点是"1个月快速检索"机制——论文被收录后平均30天内即可完成IEEE Xplore数据库的收录和DOI分配&am…

作者头像 李华
网站建设 2026/11/2 12:32:01

MATLAB R2013b 安装全攻略:分卷解压、ISO挂载与DLL替换

简介:这份PDF资料面向需要在Windows平台搭建MATLAB R2013b开发环境的学生、科研人员与工程技术人员,聚焦于解决旧版本MATLAB获取与安装配置的实际问题。资源包共1个文件,为PDF格式,体积约321KB,内容围绕安装包下载、多…

作者头像 李华
网站建设 2026/11/2 12:30:45

混沌保密通信:从Chua电路到自保持同步的工程实践

简介:这份毕业论文面向信息安全、通信工程及非线性电路方向的学习者,系统探讨混沌理论在保密通信中的应用路径,帮助读者理解混沌同步如何提升信息传输的安全性。全文围绕三条主线展开:梳理混沌保密通信的起源、基本概念与常用方法…

作者头像 李华
网站建设 2026/11/2 12:30:30

HCIA认证首次实验:基础网络配置与连通性测试

1. 项目概述 "HCIA第一次作业"这个标题看起来像是华为认证ICT工程师(HCIA)认证课程中的首个实践任务。作为网络工程领域的入门级认证,HCIA的首次作业通常会聚焦基础网络概念的实践应用。根据华为认证体系的特点,这类作业往往需要学员完成一个完…

作者头像 李华
网站建设 2026/11/2 12:30:15

大模型算子框架选型实战:三大路线对比与性能优化避坑指南

搞算子开发的朋友应该都有这种感觉:这两年“大模型算子框架选型”这个问题,从一个小众话题一下子变成了绕不开的决策点。我自己从做加速卡适配、训推一体平台搭建一路走过来,最直观的感受是——大模型把算子的数量、复杂度、性能要求全都推高…

作者头像 李华