2、常见显存杀手:Batch Size过大、序列长度过长、中间激活值爆炸、模型参数冗余
做深度学习训练,最怕什么?
我个人最怕的就是跑了大半天,突然弹出一行「CUDA out of memory」。嗯,那种感觉,就像你写代码写到一半电脑蓝屏一样酸爽。
今天咱们就来聊聊,到底是哪四个家伙把显存给吃光的。我这些年排查过的OOM案例,十有八九都能归到这几类里。
2.1 Batch Size过大——最直接的显存杀手
Batch Size(批大小)这东西,说白了就是一次往GPU里塞多少样本。你塞得越多,显存占得越狠。
为什么它会吃显存?
因为每个样本都要走一遍前向传播,都要在显存里留下自己的足迹。梯度、中间激活值,全都按Batch Size翻倍。
显存占用公式(简化版):
显存 ≈ Batch Size × (模型参数 + 中间激活值) × 精度字节数
我在项目中遇到过一位同事,把Batch Size从32调到128,想着「反正显存够大」。结果呢?跑了不到10步就OOM了。他忘了,显存占用是线性增长的,不是你想调就能调。
我的建议:
先从小Batch Size开始试,比如2或4。确认能跑通,再逐步往上加。别一上来就搞个128,那是给自己挖坑。
2.2 序列长度过长——Transformer的软肋
如果你用Transformer做NLP任务,序列长度绝对是显存大户。为什么?
因为自注意力机制的计算复杂度是 O(n²),n就是序列长度。长度翻倍,显存占用翻4倍。你想想看,这多恐怖。
举个例子:
- 序列长度512,显存占用约2GB
- 序列长度1024,显存占用约8GB
- 序列长度2048,显存占用约32GB
我曾经在做一个长文档摘要项目时,序列长度设成了4096。结果一张A100直接爆掉。后来我改用滑动窗口+分段处理,才把问题解决。
避坑指南:
我曾经以为「显存够大就能随便加长序列」,结果被现实狠狠教育了一顿。序列长度超过1024时,一定要算好显存预算。
2.3 中间激活值爆炸——隐藏的显存黑洞
很多人只盯着模型参数看,觉得「参数越少,显存越省」。其实不然。
中间激活值(Intermediate Activations)才是真正的显存黑洞。每个Batch的每个样本,在每一层都会产生激活值。这些值在反向传播时要用,所以不能丢。
哪些操作会产生大量激活值?
- 大卷积核(比如7×7)
- 深层网络(比如ResNet-152)
- 注意力头数过多
- FFN中间层维度太大
我记得有一次排查一个ViT模型的OOM问题,模型参数才300M,但中间激活值占了6GB。嗯,这就是典型的「激活值爆炸」。
排查方法:
用 torch.cuda.memory_summary() 查看显存分配情况。如果发现大量「storages」来自中间张量,那就是激活值的问题。
2.4 模型参数冗余——不必要的显存浪费
最后一个杀手,是模型参数本身。但这里说的不是「模型太大」,而是「参数冗余」。
常见的冗余情况:
- 全连接层维度设置过大(比如4096→4096,其实2048就够了)
- 使用了不必要的偏置项(某些场景下可以去掉)
- Embedding层词汇表太大(比如10万词,实际只用1万)
- 重复的LayerNorm或BatchNorm
我见过一个项目,模型里塞了12个Transformer层,每层都有独立的LayerNorm。其实有些层可以共享归一化参数,能省下不少显存。
优化建议:
用 torchsummary 或 ptflops 分析模型参数分布。看看哪些层是「显存大头」,然后针对性优化。
知识体系总览
下面这张图,是我自己总结的显存杀手排查框架。你可以把它当成一个检查清单:
快速排查表
为了方便你快速定位问题,我整理了一张排查表:
| 杀手类型 | 典型症状 | 排查方法 | 快速解决 |
|---|---|---|---|
| Batch Size过大 | 小Batch能跑,大Batch就崩 | 逐步减小Batch Size测试 | 梯度累积 |
| 序列长度过长 | 长文本任务OOM | 检查序列长度设置 | 滑动窗口/截断 |
| 中间激活值爆炸 | 模型参数小但显存占用大 | torch.cuda.memory_summary() | 梯度检查点 |
| 模型参数冗余 | 模型结构复杂但效果一般 | torchsummary分析 | 剪枝/共享参数 |
最后提醒一句:
别一上来就怀疑是「显存不够」。先按上面四个方向排查一遍,很多时候问题出在代码或配置上,而不是硬件。
我曾经花了两天时间申请新显卡,结果发现只是Batch Size设大了。嗯,那种感觉,你懂的。