4、显存碎片化问题:内部碎片与外部碎片
显存碎片化,说白了就是你的显存明明还有不少空闲,但就是分配不出一个连续的大块内存给模型用。这个问题我几乎在每个大模型训练项目里都遇到过,尤其是跑那种几十B参数量的模型时,显存碎片化能把人逼疯。
咱们先搞清楚两种碎片化类型,再聊聊它们怎么影响训练和推理,最后看几个真实案例。
4.1 内部碎片与外部碎片
内部碎片,指的是你申请了一块显存,但实际用不完,剩下的那部分就浪费了。举个例子:你申请了一个1024字节的块,但实际只用了800字节,那224字节就是内部碎片。
为什么会这样?因为CUDA显存分配有对齐要求。我记得CUDA默认的对齐大小是256字节或512字节,你申请一个奇数大小的张量,它实际占用的显存会被向上取整到对齐边界。嗯,这里要注意,PyTorch的缓存分配器也会做类似的事情。
外部碎片,这才是真正的噩梦。它指的是显存中有很多空闲的小块,但它们不连续,导致你无法分配一个大块。打个比方:你有一排连续的空座位,但中间隔三差五坐了人,你就没法让一个10人团队坐在一起。
我曾在项目中遇到过这种情况:显存还剩12GB,但分配一个8GB的连续块时直接OOM。为什么?因为那12GB被分割成了几十个几百MB的小碎片,没有一个连续区域能容纳8GB。
核心区别一句话总结:
- 内部碎片:申请了但没用上的空间
- 外部碎片:空闲但不连续的空间
4.2 碎片化对训练推理的影响
碎片化对训练的影响,我总结为三个层面:
- OOM风险剧增:明明总显存够用,但就是分配不出来。我见过一个团队,训练一个13B模型,显存占用率只有75%就报OOM了,查了半天发现是碎片化严重。
- 训练速度下降:碎片化会导致内存分配器频繁进行碎片整理和合并操作,这些操作虽然不占用计算资源,但会阻塞内存分配请求。你想想看,每次前向传播都要等几十毫秒的显存分配,累积起来就是几分钟甚至几小时的浪费。
- 推理延迟不稳定:推理场景下,碎片化会导致某些请求的显存分配时间异常长,造成延迟抖动。我做过一个在线推理服务,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了再手忙脚乱地调。你想想看,一个训练任务跑了两天,突然在第48小时OOM了,那损失可不是几行代码能弥补的。
最后说一句:碎片化不是不可解决的,但需要你在设计阶段就把它考虑进去。显存池、预分配、缓存复用,这些手段都能有效缓解碎片化。我后面会专门讲显存池的构建方法,到时候你会看到这些策略的具体实现。
核心要点回顾:
- 内部碎片:对齐和申请过大导致,相对容易控制
- 外部碎片:频繁分配释放导致,是主要矛盾
- 监控碎片率,超过0.5就要警惕
- 预分配和显存池是解决碎片化的有效手段
想要更多资料,联系可可