第一讲:显存碎片初探

什么是显存碎片?

显存碎片,说白了就是显存空间被切得七零八落。

想象一下你有一块完整的空地,想停一辆大卡车。结果空地上到处散落着小石子、小沙堆,大卡车就是停不进去。显存碎片也是这个道理——明明总空闲空间够大,但就是凑不出一块连续的区域来分配。

我刚开始做GPU推理优化时,就遇到过这么个怪事。显存占用显示还剩4GB,但一申请个3GB的tensor,直接报OOM。当时我一脸懵,后来才明白,这4GB是碎片化的,最大的连续块可能只有几百MB。

核心定义:显存碎片是指显存中已分配和已释放的内存块交错分布,导致无法找到足够大的连续空闲块来满足新的分配请求。

为什么大模型训练会遇到OOM?

大模型训练,尤其是LLM,对显存的需求是「贪婪」的。一个70B的模型,光参数就要140GB(FP16)。你想想看,这还没算上梯度、优化器状态、激活值。

但真正让OOM频发的,不是总量不够,而是碎片化。我见过一个案例,A100 80GB的卡,模型理论上只需要72GB,但训练到第3个step就炸了。为什么?因为频繁的tensor创建和销毁,把显存切成了「蜂窝煤」。

这里有个关键点:大模型训练中的显存分配是动态的。前向传播时创建激活值,反向传播时释放一部分,优化器更新时又申请新的buffer。一来二去,显存就变成了「碎片拼图」。

避坑指南:我曾经在一个分布式训练项目中,发现某个节点的OOM率比其他节点高30%。排查了三天,最后发现是那个节点的PyTorch版本不同,内存分配器策略有差异。嗯,版本对齐真的很重要。

显存分配的基本单位

GPU显存分配不是按字节来的。CUDA驱动有一套自己的「批发」逻辑。

基本单位是segment(段)和block(块)。

  • Segment:GPU驱动从显存中一次性「批发」的大块区域,通常是几MB到几百MB
  • Block:从segment中切出来的小块,用于满足具体的tensor分配请求

我习惯把segment比作「整块地皮」,block就是「地皮上的具体地块」。CUDA驱动先批下整块地,然后分配器再切分给各个tensor。

来看个实际例子:

// 伪代码:显存分配过程
cudaMalloc(&ptr1, 1024 * 1024 * 100);  // 申请100MB
// 驱动分配一个256MB的segment,从中切出100MB给ptr1

cudaMalloc(&ptr2, 1024 * 1024 * 50);   // 申请50MB
// 从同一个segment中切出50MB给ptr2

cudaFree(ptr1);  // 释放100MB
// segment中出现了一个100MB的空洞

cudaMalloc(&ptr3, 1024 * 1024 * 80);   // 申请80MB
// 空洞只有100MB,但可能因为对齐要求,实际可用只有70MB
// 80MB放不下,只能从新segment中分配
// 旧的100MB空洞就成了碎片

你看,这就是碎片产生的典型过程。释放的块不一定是下一个分配的最佳位置。

分配单位 大小范围 作用
Segment 2MB ~ 512MB 驱动层面的批量分配
Block 256B ~ 几十MB 满足具体tensor请求
Chunk 自定义 框架层面的缓存池

个人经验:我建议你在调试显存问题时,先查一下分配粒度。很多框架默认的分配粒度是512B,但如果你频繁申请1KB的tensor,碎片率会飙升。调大分配粒度,比如改成2KB,有时候能立竿见影。

碎片化的直观理解

我画了一张图,帮你理解碎片是怎么产生的:

显存碎片化过程示意图 初始:连续空闲 空闲显存(连续) 分配后:占用与空闲交错 Tensor A 空闲 Tensor B 空闲 释放Tensor A后:出现空洞 空洞(碎片) 空闲 Tensor B 空闲 碎片化:大块请求失败 想申请300MB? 没门!

这张图展示了从连续空闲到碎片化的完整过程。你想想看,如果Tensor C需要300MB,而最大的连续空闲块只有155MB,那OOM就是必然的。

碎片类型

显存碎片主要分两种:

  1. 外部碎片:已分配块之间的空隙。就像上面图中的空洞,无法被大块请求利用
  2. 内部碎片:分配块内部未被使用的空间。比如你申请了1000字节,但分配器给了你1024字节,那24字节就是内部碎片

我个人经验是,大模型训练中外部碎片是主要矛盾。内部碎片虽然也存在,但占比通常不到5%。

关键认知:显存碎片不是「显存不够」,而是「显存不连续」。很多OOM问题,加显存并不能解决,反而可能因为更大的分配粒度让碎片更严重。

嗯,这一讲先到这里。碎片是怎么一步步累积的,以及如何用工具去观测,我们后面慢慢聊。

可可精品资料库