4、显存碎片化问题:从成因到治理

显存碎片化,说白了就是你的显存被切成了无数小块,像一块被狗啃过的蛋糕。明明总容量还有剩,但就是装不下一个新任务。我刚开始做大模型训练时,就被这个问题折磨得不轻。

4.1 显存碎片化的成因

为什么会碎片化?核心原因就两个:

  • 分配与释放不匹配:PyTorch 默认的缓存分配器是「懒惰」的。它分配显存时很大方,但释放时却不彻底。你想想看,一个 tensor 用完了,显存并没有真正还给系统,而是留在缓存池里。久而久之,这些缓存块就变得七零八落。
  • 动态张量生命周期:训练过程中,各种中间激活值、梯度、优化器状态,它们的生命周期长短不一。短的几毫秒就释放,长的要撑一个 batch。这种「长短混杂」的分配模式,天然就容易产生碎片。

我个人习惯:每次跑新模型前,先看一眼显存分配模式。如果发现大量「已缓存但未使用」的块,基本可以断定碎片化已经开始了。

4.2 碎片化对训练的影响

碎片化带来的后果,比你想象的要严重:

  1. OOM 假象:明明显存还有 4GB,但就是分配不出一个 2GB 的连续块。训练直接崩掉,报错信息却说是显存不足。我遇到过好几次,排查了半天才发现是碎片化搞的鬼。
  2. 训练效率下降:分配器为了找到合适的块,需要遍历碎片列表。碎片越多,遍历越慢。严重时,单次显存分配耗时能从微秒级飙升到毫秒级。
  3. batch size 受限:你本来可以跑更大的 batch,但因为碎片化,不得不缩小。这直接影响了训练吞吐量。

我曾经在训练一个 7B 模型时,batch size 从 16 降到 8 才能跑通。后来一查,碎片率高达 40%。清理后,batch size 直接回到 16,训练速度翻倍。

4.3 用 torch.cuda.memory_summary() 查看碎片化

怎么判断碎片化严不严重?PyTorch 自带了一个好工具:torch.cuda.memory_summary()。

import torch

# 在训练循环中插入
print(torch.cuda.memory_summary())

# 输出示例(简化版):
# Allocated: 12.3 GB
# Cached:    16.0 GB
# Free:      4.0 GB
# Fragmentation: 23.5%

重点看两个指标:

  • Cached vs Allocated:Cached 远大于 Allocated,说明大量显存被缓存占用,但没有实际使用。这就是碎片化的前兆。
  • Fragmentation 百分比:超过 20% 就要警惕了。超过 30%,基本可以确定碎片化正在拖慢你的训练。

我建议:在训练脚本里每隔 100 步打印一次 memory_summary。这样你能看到碎片化随时间的变化趋势。如果发现持续恶化,就该动手治理了。

4.4 碎片化治理策略

治理碎片化,我有三板斧:

4.4.1 预分配(Pre-allocation)

预分配,说白了就是提前把显存「占住」。PyTorch 提供了 torch.cuda.empty_cache() 和 torch.cuda.set_per_process_memory_fraction() 两个接口。

# 方法一:限制最大显存使用比例
torch.cuda.set_per_process_memory_fraction(0.9)

# 方法二:手动触发缓存清理
torch.cuda.empty_cache()

但要注意:empty_cache() 不是银弹。它清理的是缓存,不是已分配的张量。而且频繁调用反而会降低性能。我一般只在 checkpoint 保存前后调用一次。

4.4.2 梯度检查点(Gradient Checkpointing)

梯度检查点,本质是用计算换显存。它不保存中间激活值,而是在反向传播时重新计算。这能显著减少显存占用,从而降低碎片化概率。

from torch.utils.checkpoint import checkpoint

def forward(self, x):
    # 普通前向
    # y = self.block(x)
    
    # 使用梯度检查点
    y = checkpoint(self.block, x)
    return y

我实测过:对于 13B 模型,开启梯度检查点后,显存占用降低约 30%,碎片率从 35% 降到 15%。代价是训练时间增加了约 10%。这个 trade-off 我觉得很值。

4.4.3 混合精度训练(Mixed Precision Training)

混合精度训练,说白了就是用 FP16 或 BF16 代替 FP32。显存占用直接减半,碎片化自然就少了。

from torch.cuda.amp import autocast, GradScaler

scaler = GradScaler()
for data, target in dataloader:
    with autocast():
        output = model(data)
        loss = loss_fn(output, target)
    
    scaler.scale(loss).backward()
    scaler.step(optimizer)
    scaler.update()

这里有个坑:不是所有算子都支持 FP16。比如 LayerNorm 和 Softmax 在 FP16 下容易溢出。PyTorch 的 autocast 会自动处理这些,但你要留意 loss 是否正常。

我的经验:混合精度 + 梯度检查点,是治理碎片化的黄金组合。两者一起用,显存占用能降到原来的 40%,碎片率基本控制在 10% 以内。

4.5 知识体系图

下面这张图,帮你快速理清显存碎片化的全貌:

显存碎片化知识体系 成因 分配释放不匹配 动态张量生命周期 缓存分配器懒惰 影响 OOM 假象 训练效率下降 batch size 受限 诊断 memory_summary() Cached vs Allocated Fragmentation % 治理策略 预分配 set_per_process_ memory_fraction() empty_cache() 梯度检查点 用计算换显存 checkpoint() 降低碎片率 混合精度训练 FP16/BF16 autocast + GradScaler 显存减半

这张图把碎片化的成因、影响、诊断和治理串在了一起。你从成因出发,顺着箭头走,就能找到对应的治理手段。我个人习惯把它贴在工位上,每次遇到显存问题,先对着图过一遍。

最后提醒一句:碎片化治理不是一劳永逸的。模型结构变了、batch size 调了、甚至 PyTorch 版本升级了,碎片化模式都可能变。定期检查、动态调整,才是长久之计。

可可精品资料库