4、显存碎片化问题:内部碎片与外部碎片

显存碎片化,说白了就是你的显存明明还有不少空闲,但就是分配不出一个连续的大块内存给模型用。这个问题我几乎在每个大模型训练项目里都遇到过,尤其是跑那种几十B参数量的模型时,显存碎片化能把人逼疯。

咱们先搞清楚两种碎片化类型,再聊聊它们怎么影响训练和推理,最后看几个真实案例。

4.1 内部碎片与外部碎片

内部碎片,指的是你申请了一块显存,但实际用不完,剩下的那部分就浪费了。举个例子:你申请了一个1024字节的块,但实际只用了800字节,那224字节就是内部碎片。

为什么会这样?因为CUDA显存分配有对齐要求。我记得CUDA默认的对齐大小是256字节或512字节,你申请一个奇数大小的张量,它实际占用的显存会被向上取整到对齐边界。嗯,这里要注意,PyTorch的缓存分配器也会做类似的事情。

外部碎片,这才是真正的噩梦。它指的是显存中有很多空闲的小块,但它们不连续,导致你无法分配一个大块。打个比方:你有一排连续的空座位,但中间隔三差五坐了人,你就没法让一个10人团队坐在一起。

我曾在项目中遇到过这种情况:显存还剩12GB,但分配一个8GB的连续块时直接OOM。为什么?因为那12GB被分割成了几十个几百MB的小碎片,没有一个连续区域能容纳8GB。

核心区别一句话总结:

  • 内部碎片:申请了但没用上的空间
  • 外部碎片:空闲但不连续的空间

4.2 碎片化对训练推理的影响

碎片化对训练的影响,我总结为三个层面:

  1. OOM风险剧增:明明总显存够用,但就是分配不出来。我见过一个团队,训练一个13B模型,显存占用率只有75%就报OOM了,查了半天发现是碎片化严重。
  2. 训练速度下降:碎片化会导致内存分配器频繁进行碎片整理和合并操作,这些操作虽然不占用计算资源,但会阻塞内存分配请求。你想想看,每次前向传播都要等几十毫秒的显存分配,累积起来就是几分钟甚至几小时的浪费。
  3. 推理延迟不稳定:推理场景下,碎片化会导致某些请求的显存分配时间异常长,造成延迟抖动。我做过一个在线推理服务,P99延迟从5ms飙到50ms,最后发现是碎片化导致分配器触发了全量整理。

避坑指南:我曾经在一个训练任务中,发现每次迭代到第1000步左右就会卡住几秒钟。排查后发现是显存碎片化触发了CUDA驱动层的垃圾回收。解决办法是在训练循环中定期调用 torch.cuda.empty_cache(),但注意不要频繁调用,否则反而会降低性能。

4.3 实际案例分析

咱们看一个具体的例子。假设你在训练一个GPT类模型,batch size=8,序列长度=2048,隐藏层维度=4096。训练过程中,你会频繁创建和销毁各种中间张量:

  • 前向传播时创建激活值
  • 反向传播时创建梯度
  • 优化器更新时创建临时变量
  • checkpoint保存时分配大块内存

这些操作会不断分配和释放不同大小的显存块。久而久之,显存就像被狗啃过一样,到处都是小洞。

我做过一个实验:用PyTorch的 torch.cuda.memory_summary() 查看显存状态,发现训练100步后,显存中有超过2000个空闲块,平均大小只有几十MB。而模型需要的一个连续块是2GB,根本分配不出来。

# 查看显存碎片化情况的代码片段
import torch

# 模拟训练过程中的显存分配
def simulate_fragmentation():
    tensors = []
    for i in range(100):
        # 随机大小的张量分配
        size = torch.randint(100, 1000, (1,)).item()
        t = torch.randn(size, 1024, device='cuda')
        tensors.append(t)
        
        # 随机释放一些张量
        if torch.rand(1).item() > 0.7:
            idx = torch.randint(0, len(tensors), (1,)).item()
            del tensors[idx]
    
    # 查看显存状态
    print(torch.cuda.memory_summary())

# 注意:实际运行会占用大量显存,谨慎执行
# simulate_fragmentation()

另一个案例是推理场景。我优化过一个BERT推理服务,发现随着服务运行时间增长,显存碎片化越来越严重。原因是有不同长度的输入序列,每次推理分配的显存大小都不一样。短序列释放的小块无法被长序列复用。

解决方案是使用显存池预分配策略:在服务启动时,预先分配几个固定大小的显存块(比如对应128、256、512序列长度),然后推理时直接从池中取用,用完归还。这样碎片化问题基本消失了。

我的个人经验:

  • 训练场景:使用 torch.cuda.set_per_process_memory_fraction() 限制最大显存使用量,给碎片化留出缓冲空间
  • 推理场景:使用显存池或缓存分配器,避免频繁的malloc/free
  • 通用技巧:定期调用 torch.cuda.empty_cache() 但不要过于频繁(每100-500步一次即可)

4.4 碎片化度量与监控

怎么量化碎片化程度?我一般用两个指标:

指标 定义 正常范围 危险范围
碎片率 1 - (最大连续空闲块 / 总空闲显存) < 0.3 > 0.6
空闲块数量 显存中空闲块的总数 < 100 > 500

你可以用 torch.cuda.memory_stats() 获取这些数据。我习惯在训练脚本里加一个监控回调,每100步打印一次碎片率,一旦超过0.5就触发告警。

def check_fragmentation():
    stats = torch.cuda.memory_stats()
    # 获取最大空闲块大小(需要CUDA 11.4+)
    max_free = stats.get('max_free_blocks_size', 0)
    total_free = stats.get('total_free_memory', 0)
    
    if total_free > 0:
        frag_ratio = 1 - max_free / total_free
        print(f"碎片率: {frag_ratio:.2f}, 空闲块数: {stats.get('num_free_blocks', 0)}")
        return frag_ratio
    return 0
显存碎片化知识体系 显存碎片化 内部碎片 外部碎片 对齐浪费 申请过大 频繁分配释放 大小不匹配 影响:OOM / 速度下降 / 延迟抖动 解决方案:显存池 / 预分配 / 定期整理

嗯,碎片化这个问题,说白了就是显存管理不当的后果。我个人习惯是在项目初期就做好显存规划,而不是等到OOM了再手忙脚乱地调。你想想看,一个训练任务跑了两天,突然在第48小时OOM了,那损失可不是几行代码能弥补的。

最后说一句:碎片化不是不可解决的,但需要你在设计阶段就把它考虑进去。显存池、预分配、缓存复用,这些手段都能有效缓解碎片化。我后面会专门讲显存池的构建方法,到时候你会看到这些策略的具体实现。

核心要点回顾:

  • 内部碎片:对齐和申请过大导致,相对容易控制
  • 外部碎片:频繁分配释放导致,是主要矛盾
  • 监控碎片率,超过0.5就要警惕
  • 预分配和显存池是解决碎片化的有效手段

想要更多资料,联系可可