4. 碎片产生的根源

好,咱们今天来聊聊一个让所有GPU开发者都头疼的问题——显存碎片。

你可能有过这种经历:程序跑着跑着,突然报了个“out of memory”。你一看显存占用,明明还有好几GB空闲,怎么就OOM了呢?

嗯,这大概率就是碎片在作祟。

我在做分布式训练框架的时候,就曾被这个问题折磨过整整两周。每次跑到第37个epoch必挂,排查到最后,发现是显存被切成了无数个小块,连一个完整的张量都放不下了。

那碎片到底是怎么产生的?说白了,就三个元凶:频繁分配释放、大小不一、生命周期交错。咱们一个一个拆开看。

4.1 元凶一:频繁分配释放

你想想看,如果你的程序里有个循环,每次迭代都cudaMalloc和cudaFree一次,会发生什么?

每次分配,CUDA驱动都会去翻找一块连续的空闲内存。释放的时候,这块内存就变成了一个“空洞”。

循环跑1000次,显存里就多了1000个空洞。这些空洞大小不一,位置随机。下次再分配时,可能没有一个空洞能装下你的数据。

我见过最夸张的一个项目,一个batch里调用了上万次cudaMalloc。结果显存利用率不到40%,剩下的全成了碎片。

核心结论: 分配释放越频繁,碎片化速度越快。这是碎片产生的第一推动力。

4.2 元凶二:大小不一

如果每次分配的内存块大小都一样,那其实还好。就像切豆腐,每块都一样大,用完放回去,下次还能用。

但现实是残酷的。你的模型里有各种形状的张量:

  • 权重矩阵:1024x1024
  • 偏置向量:1024
  • 中间激活值:64x64x256
  • 梯度缓存:和权重一样大

这些大小完全不同的内存块,就像不同尺寸的积木。你把它塞进显存里,再拿出来,留下的空洞就很难被后续的分配完美填上。

举个例子:你分配了一个1MB的块,然后释放了。接着想分配一个2MB的块。如果1MB的空洞旁边没有连续的1MB空闲空间,那这个2MB的请求就只能去更远的地方找空间。

久而久之,显存里就布满了“小碎块”——每个单独看都不小,但就是凑不出一块大的。

我的经验: 我曾经在一个推理服务里发现,仅仅是因为输入图片尺寸不同(导致特征图大小不同),显存碎片率就从5%飙升到了35%。后来我强制把所有特征图padding到固定尺寸,碎片率直接降到了8%以下。

4.3 元凶三:生命周期交错

这个最隐蔽,也最致命。

假设你有三个张量A、B、C:

  • A在时间点t0分配,t2释放
  • B在t1分配,t3释放
  • C在t2分配,t4释放

你看,A和B的生命周期有重叠,B和C也有重叠。但A和C没有重叠。

当A在t2释放时,它留下的空间被B占着(因为B还没释放)。等B在t3释放时,A的空间已经被C或者其他东西占用了。

这就导致了一个结果:明明每个块都释放了,但空闲空间就是无法合并成连续的大块。

我把它叫做“内存版的俄罗斯方块”——每个块都在不同的时间点出现和消失,最后留下的空洞形状复杂到让人崩溃。

避坑指南: 我曾经在实现一个pipeline并行策略时,没有仔细规划各个stage的张量生命周期。结果显存碎片率高达60%,训练速度直接腰斩。后来我花了整整两天,把所有张量的生命周期画成时序图,才找到问题所在。

4.4 三大元凶的协同效应

这三个元凶很少单独出现。它们通常会一起上,形成“碎片化风暴”。

比如在动态图训练中:

  1. 每次前向传播都会创建新的中间张量(频繁分配)
  2. 不同层的输出大小不同(大小不一)
  3. 有些张量在反向传播后才释放,有些则要保留到优化器更新(生命周期交错)

三者叠加,显存很快就变成了一盘散沙。

下面这张图可以帮你直观理解这个过程:

显存碎片产生的三大元凶 初始显存: 连续空闲空间(600MB) ↓ 频繁分配释放 分配后: A(150MB) B(100MB) C(200MB) 空闲(150MB) ↓ 释放A,分配D(大小不一) 释放A后: 空洞(150MB) B(100MB) C(200MB) 空闲(150MB) 分配D(200MB): 空洞(150MB) B(100MB) C(200MB) D(150MB) ※ 空洞(150MB) + 空闲(0MB) = 无法分配200MB的D,只能占用尾部空间

你看,明明总空闲空间有300MB(空洞150MB + 尾部空闲150MB),但因为不连续,200MB的D只能挤在尾部。那个150MB的空洞,就这么浪费了。

4.5 如何判断你的程序是否被碎片困扰?

这里我分享一个实用的小技巧:

症状 表现 可能性
显存占用不高但OOM nvidia-smi显示占用60%,但分配新张量时报错 极高
训练速度逐渐变慢 前几个epoch很快,后面越来越慢 中等
cudaMalloc耗时波动大 有时1ms,有时100ms 极高
程序在固定迭代次数后崩溃 每次都在第N次迭代后OOM 高
我的建议: 如果你怀疑有碎片问题,可以用 cudaMemGetInfo 在关键节点打印显存使用情况。对比 free 和 total - used 的差值,差值越大,碎片越严重。

好了,三大元凶讲完了。总结一句话:频繁分配释放是“量”的问题,大小不一是“形”的问题,生命周期交错是“序”的问题。 三者叠加,就是显存碎片的完美风暴。