4. 显存池的常见架构:全局池 vs 线程局部池、固定大小池 vs 可变大小池

聊到显存池的具体实现,我见过不少团队一上来就撸袖子写代码。结果呢?跑起来倒是能跑,一上高并发就崩。说白了,架构选型没想清楚。

今天咱们就掰开揉碎,聊聊显存池的两种核心维度:作用域和大小策略。这两个维度交叉组合,基本覆盖了90%以上的部署场景。

4.1 全局池 vs 线程局部池

先看作用域。这个选择直接影响你的显存碎片率和并发性能。

4.1.1 全局池

全局池,就是整个进程共享一个显存分配器。所有线程都从同一个池子里拿内存、还内存。

优点很明显:

  • 内存利用率高。一个线程用完了,另一个线程马上能用。
  • 实现简单。加个锁,维护一个空闲链表就行。

缺点也致命:

  • 锁竞争严重。我曾在某推荐模型部署中遇到过,8个线程同时推理,光抢锁就占了30%的耗时。
  • 碎片容易累积。不同线程分配大小不一,时间一长,池子里全是小洞。
适用场景: 单线程推理、低并发服务、对内存敏感但对延迟不敏感的场景。

4.1.2 线程局部池

每个线程维护自己的小池子。各管各的,互不干扰。

优势:

  • 零锁竞争。每个线程只操作自己的池,性能直接拉满。
  • 缓存友好。频繁使用的内存大概率还在L1/L2 cache里。

代价:

  • 内存浪费。线程A空闲时池子空着,线程B却因为池子不够用去问系统要新内存。
  • 实现复杂。每个线程需要独立的回收机制。
我的经验: 我曾经在部署一个多路视频分析服务时,用了线程局部池。每个线程处理一路视频,显存需求稳定。效果出奇的好,延迟降低了40%。但如果你线程数超过GPU核心数太多,就别这么玩了。

4.1.3 混合方案

其实,很多工业级实现是两者结合。比如:

  • 线程局部池作为一级缓存,快速分配小内存。
  • 全局池作为二级缓存,处理大块内存和跨线程的借用。

嗯,这里要注意:两级之间要有水位线控制。别让线程局部池囤积太多内存,否则全局池就形同虚设了。

4.2 固定大小池 vs 可变大小池

再看大小策略。这个选择决定了你的池子能不能应对多样化的请求。

4.2.1 固定大小池

预先分配一批固定大小的内存块。比如每个块都是4MB,来了请求就从里面拿。

优点:

  • 分配速度极快。O(1)时间复杂度,拿头指针就行。
  • 零碎片。所有块一样大,不存在内部碎片和外部碎片。

缺点:

  • 灵活性差。如果模型需要4.5MB,要么浪费0.5MB,要么分配失败。
  • 内存浪费严重。我见过一个项目,固定分配16MB块,实际平均只用5MB,浪费率高达68%。
避坑指南: 我曾经在部署一个OCR服务时,天真地用了固定大小池。结果不同图片分辨率差异巨大,小图浪费内存,大图直接OOM。后来改成可变大小池才解决问题。

4.2.2 可变大小池

按需分配,想多大就多大。但回收时要做合并,防止碎片。

优势:

  • 内存利用率高。要多少给多少,不浪费。
  • 适应性强。能应对各种尺寸的请求。

挑战:

  • 分配速度慢。需要查找合适大小的空闲块,可能触发合并操作。
  • 碎片管理复杂。需要伙伴算法或slab分配器来维护。

4.2.3 实际中的选择

我个人习惯这样选:

场景 推荐方案 原因
模型推理(固定batch) 固定大小池 输入输出尺寸固定,浪费可控
模型推理(动态batch) 可变大小池 batch大小变化,需要灵活分配
视频处理 固定大小池 分辨率固定,帧大小一致
多模型混合部署 可变大小池 不同模型显存需求差异大

4.3 架构组合与选择

你想想看,这两个维度交叉,其实有四种组合:

  1. 全局 + 固定大小:最简单,适合单模型、单线程场景。
  2. 全局 + 可变大小:通用方案,适合多模型、低并发场景。
  3. 线程局部 + 固定大小:高性能方案,适合多线程、固定负载场景。
  4. 线程局部 + 可变大小:最灵活,但实现最复杂。适合高并发、负载多变场景。

下面这张图,是我自己总结的架构选择决策流程:

显存池架构选择决策树 显存池架构 全局池 线程局部池 固定大小 可变大小 固定大小 可变大小 单模型 单线程 多模型 低并发 多线程 固定负载 高并发 负载多变 选择建议:先确定并发模型,再确定内存需求特征
我的建议: 别一上来就追求最复杂的方案。先跑通最简单的「全局+固定大小」,用profiling工具看看瓶颈在哪。如果锁竞争严重,再切到线程局部池。如果内存浪费太多,再引入可变大小策略。迭代着来,比一次到位靠谱得多。

最后说一句,架构没有银弹。你想想看,你的业务场景是CPU密集还是IO密集?线程数是多还是少?显存需求是稳定还是波动?把这些想清楚了,选型自然就出来了。