一、显存碎片问题概述
什么是显存碎片?
显存碎片,说白了就是显存空间被切割成一块块不连续的小区域。
我打个比方你就明白了。想象你有一个大仓库,里面堆满了各种大小的箱子。你不断搬进新箱子,又搬走旧箱子。时间一长,仓库里就出现了很多小空隙——每个空隙都不大,但加起来浪费了不少空间。
显存碎片也是这个道理。GPU显存被频繁分配和释放后,空闲区域变得支离破碎。这时候,即使总空闲显存足够大,也可能装不下一个连续的大张量。
核心定义:显存碎片是指显存中可用但非连续的空闲内存块,导致无法满足大块连续内存分配请求的现象。
碎片产生的根本原因
为什么会这样?我总结下来,主要有三个原因。
1. 分配与释放模式不匹配
训练过程中,张量的生命周期各不相同。有些张量存活时间长(比如模型参数),有些则很短(比如中间激活值)。
举个例子:
# 典型的训练循环中,张量生命周期差异巨大
model_params = torch.nn.Linear(1024, 1024) # 长期存活
for batch in dataloader:
hidden = model(batch) # 短期存活
loss = loss_fn(hidden, target) # 更短生命周期
loss.backward()
optimizer.step()
这种长短不一的分配模式,就像在仓库里频繁搬进搬出不同大小的箱子。小箱子搬走后留下的空隙,往往不足以容纳下一个大箱子。
2. 内存分配器的默认行为
PyTorch的CUDA分配器默认使用缓存机制。它不会把释放的内存立刻还给操作系统,而是留着复用。
我遇到过这样的情况:明明显存使用量看起来不高,但一分配大张量就报OOM。原因就是缓存池里的碎片太多了。
小技巧:可以用 torch.cuda.memory_summary() 查看当前显存碎片情况。我习惯在训练脚本里定期打印这个信息。
3. 张量大小不统一
不同batch size、不同序列长度,都会导致张量尺寸变化。每次分配的大小都不一样,碎片自然就多了。
你想想看,如果每次分配的都是固定大小的块,那释放后就能完美复用。但现实是,训练过程中张量尺寸千变万化。
| 场景 | 张量大小变化 | 碎片风险 |
|---|---|---|
| 固定batch size | 基本不变 | 低 |
| 动态batch size | 频繁变化 | 中 |
| 变长序列 | 剧烈变化 | 高 |
碎片对训练推理的影响
碎片带来的问题,我亲身经历过不少。这里挑几个典型的说说。
1. 显存利用率下降
最直接的影响就是显存利用率上不去。明明有80GB显存,实际能用的连续空间可能只有60GB。
我曾经在一个大模型训练任务中,看着显存占用显示75GB,但分配一个8GB的张量却报OOM。查了半天,发现就是碎片搞的鬼。
2. 训练吞吐量降低
碎片多了,内存分配器需要花更多时间查找合适的空闲块。这个查找过程不是O(1)的,碎片越多,查找越慢。
注意:碎片严重时,分配延迟可能从微秒级飙升到毫秒级。对于频繁分配张量的训练场景,这个开销不可忽视。
3. 推理时延不稳定
推理服务对延迟敏感。碎片导致的分配延迟波动,会让推理时延出现毛刺。
我记得有一次线上推理服务,P99延迟突然从10ms飙到50ms。排查下来,就是显存碎片导致某些请求分配内存时卡住了。
4. 模型规模受限
最头疼的是,碎片会限制你能训练的模型大小。即使总显存够大,但连续空间不够,大模型就放不进去。
说白了,碎片就是显存里的「内耗」。它不直接消耗显存,但让你用不了全部显存。
一句话总结:显存碎片是GPU训练和推理中「看不见的敌人」,它悄悄降低你的效率,限制你的模型规模,却不容易被直接发现。
好了,这一章我们聊了显存碎片是什么、怎么来的、有什么影响。下一章我会讲讲怎么检测和定位碎片问题,到时候给你看几个我实际用过的工具和脚本。