3、显存池核心架构:预分配机制、回收机制、碎片整理机制

聊到显存池,很多人第一反应就是「不就是提前申请一块显存嘛」。嗯,这话对了一半。提前申请只是表象,真正的核心在于:怎么分、怎么收、怎么整理。这三个机制,说白了就是显存池的「心脏」。我在好几个大模型推理项目里都吃过亏,今天就把这些经验掰开揉碎了讲给你听。

3.1 预分配机制:别等到用的时候才去要

预分配,顾名思义,就是在模型加载或者服务启动的时候,先把显存「圈」好。为什么要这么做?你想想看,推理服务跑起来之后,如果每次请求来了才去 cudaMalloc,那延迟会高得吓人。我见过一个团队,上线第一天就因为频繁申请显存导致服务超时,最后不得不紧急回滚。

我个人习惯的做法是:

  • 按需预分配:根据模型的最大 batch size、序列长度、层数,算出理论峰值显存,然后一次性申请。比如一个 7B 模型,FP16 精度下,单次推理大约需要 14GB 显存,我会预分配 16GB 作为缓冲。
  • 分块预分配:不申请一整块大显存,而是切成固定大小的块(比如 256MB、512MB、1GB)。这样做的好处是,后续回收和分配都更快,碎片也更少。
  • 预留紧急池:我会额外留出 5%-10% 的显存作为「紧急备用池」。当正常池不够用时,可以从这里临时借调,避免 OOM。

核心原则:预分配不是越多越好。分配太多,其他进程(比如数据预处理)就没显存用了;分配太少,又容易 OOM。我建议根据实际负载动态调整,而不是写死一个值。

3.2 回收机制:用完的显存,别急着还给 CUDA

回收机制,很多人理解成「把显存释放掉」。其实不对。真正的回收,是把用完的显存块标记为「空闲」,放回池子里,而不是真的调用 cudaFree。为什么?因为 cudaFree 的开销很大,频繁调用会严重影响性能。

我在项目中遇到过这样一个坑:某个推理服务,每次请求结束后都调用 cudaFree 释放显存。结果呢?显存占用倒是降下来了,但吞吐量直接腰斩。后来我改成「延迟回收」——显存块用完后先不释放,而是放入一个「待回收队列」,等队列满了或者空闲时间超过阈值,再统一回收。

回收策略一般有三种:

策略 描述 适用场景
立即回收 用完后马上标记为空闲,可被下一个请求复用 请求量稳定、显存充足的场景
延迟回收 放入待回收队列,定时或定量触发回收 请求波动大、需要降低碎片率的场景
分级回收 根据显存块大小设置不同的回收优先级 多模型混合部署、显存紧张的场景

小技巧:我习惯在回收时记录每个显存块的「年龄」——即它被空闲了多久。年龄越大,越优先被复用。这样可以避免「冷块」长期占用池子。

3.3 碎片整理机制:显存也会「碎」

碎片,是显存池最头疼的问题。你想想看,频繁分配和回收,显存块会变得七零八落。比如你有一个 8GB 的池子,里面有很多 256MB 的小块,但你想分配一个 2GB 的大块,结果发现没有连续空间——这就是碎片导致的。

我曾经在一个推理集群上遇到过:显存池明明还有 6GB 空闲,但就是分配不出一个 1GB 的块。排查了半天,发现是碎片太严重了。后来我加了一个碎片整理机制,才彻底解决。

碎片整理的常见做法:

  • 移动合并:把分散的小块显存「挪」到一起,合并成大块。注意,这需要暂停当前所有推理请求,所以一般放在服务低峰期做。
  • 分代整理:把显存块按「年龄」分代。年轻代(刚空闲的块)频繁整理,老年代(长期空闲的块)少整理。这样可以降低整理开销。
  • 预留连续区:在预分配时,专门留出一块「连续大块区」,只用于大块分配。这样即使小块区碎片严重,大块区也不受影响。

注意:碎片整理不是万能的。如果碎片率超过 30%,说明你的预分配策略或者回收策略有问题。我建议先优化前两个机制,而不是依赖碎片整理来「擦屁股」。

3.4 核心架构图:一张图看懂显存池

下面这张图,是我自己总结的显存池核心架构。它把预分配、回收、碎片整理三个机制串在了一起。你仔细看,应该能理解它们是怎么协同工作的。

显存池核心架构图 预分配机制 按需预分配 分块预分配 预留紧急池 回收机制 立即回收 延迟回收 分级回收 碎片整理机制 移动合并 分代整理 预留连续区 分配 触发 显存池(Memory Pool) 256MB 512MB 1GB 128MB 2GB(连续大块区) 碎片区域(需整理) 三个机制协同工作:预分配提供基础,回收保证复用,碎片整理解决空间连续性

3.5 避坑指南:我曾经踩过的三个坑

最后,分享几个我亲身经历过的坑,希望能帮你少走弯路。

  • 坑一:预分配太大导致其他进程 OOM。有一次我预分配了 80% 的显存,结果数据预处理进程因为拿不到显存直接崩溃。后来我改成动态预分配,根据当前空闲显存按比例分配。
  • 坑二:回收不及时导致显存泄漏。某个版本我忘了在异常路径里回收显存,结果服务跑了一天后,显存占用从 4GB 涨到了 12GB。排查了好久才发现是异常处理没做好。
  • 坑三:碎片整理触发太频繁。我一开始把碎片整理的阈值设得很低,结果每次请求结束后都触发整理,性能反而下降了。后来我改成「碎片率超过 20% 且空闲时间超过 5 秒」才触发,效果好了很多。

总结一下:预分配、回收、碎片整理,这三个机制是显存池的「三驾马车」。预分配决定了显存池的「容量」,回收决定了「效率」,碎片整理决定了「稳定性」。三者缺一不可,而且需要根据实际负载动态调整。我个人建议,上线前先用压力测试跑一遍,看看碎片率和回收延迟,再针对性地调参。

可可精品资料库