3、显存管理与批大小:显存瓶颈分析、最大批大小估算公式、显存碎片化与连续批处理
聊到批处理策略,显存管理是绕不开的硬骨头。我见过太多团队,模型推理速度上不去,一查原因,不是算力不够,而是显存爆了。说白了,你批大小设得再大,显存放不下,一切都是空谈。
3.1 显存瓶颈分析:钱都花哪儿了?
推理时显存主要被三样东西吃掉:模型参数、KV Cache 和 中间激活值。我个人习惯把显存想象成一个仓库,参数是固定货架,KV Cache 是临时周转区,激活值则是流水线上的半成品。
- 模型参数:权重和偏置,加载后基本不变。比如一个 7B 模型,FP16 精度下大约占 14GB。
- KV Cache:这是大头。每生成一个 token,都要把 Key 和 Value 缓存下来。批大小越大、序列越长,它膨胀得越快。
- 中间激活值:前向传播时各层的输出。虽然推理时比训练小,但批大小上去后,依然不可忽视。
我在项目中遇到过一个问题:同样的模型,别人能跑 64 的批大小,我们只能跑 32。排查下来,是 KV Cache 的分配策略太粗糙,导致显存利用率极低。你想想看,明明仓库还有空间,但货架摆得太散,新货就放不进去了。
显存占用公式(简化版):
总显存 ≈ 参数显存 + KV Cache 显存 + 激活值显存 + 预留空间
3.2 最大批大小估算公式:别靠猜,算清楚
最大批大小怎么定?我建议你直接算,别靠感觉。公式其实不复杂:
最大批大小 = (总显存 - 参数显存 - 预留显存) / (每样本所需显存)
其中每样本所需显存,主要就是 KV Cache 和激活值。以 LLaMA-7B 为例,FP16 精度,序列长度 2048:
| 项目 | 显存占用 |
|---|---|
| 模型参数 | 约 14 GB |
| 每样本 KV Cache | 约 2 MB × 层数 × 注意力头数 |
| 每样本激活值 | 约 1 MB(估算) |
| 预留显存(10%) | 约 2 GB |
假设你有 40GB 的 A100:
可用显存 = 40 - 14 - 2 = 24 GB
每样本显存 ≈ 2 MB × 32 层 × 32 头 + 1 MB ≈ 2.05 GB
最大批大小 ≈ 24 / 2.05 ≈ 11.7 → 取整为 11
嗯,这里要注意:这只是理论值。实际运行时,显存碎片化会让可用空间打折扣。我曾经按公式算出能跑 16,结果跑到 12 就 OOM 了。为什么?碎片化在作祟。
避坑指南:我曾经因为没算预留显存,直接按公式设批大小,结果服务跑了一天后,显存碎片越积越多,最终 OOM。建议预留 10%-15% 的显存作为缓冲。
3.3 显存碎片化与连续批处理:怎么把碎片拼起来?
显存碎片化,说白了就是内存分配和释放不连续,导致很多小块空闲空间无法被大请求利用。推理服务跑久了,这种现象尤其明显。
为什么会这样?因为每次请求的序列长度不一样,KV Cache 的大小也就不同。短的释放后留下小洞,长的又需要大块连续空间。久而久之,显存就像被啃过的奶酪,到处是孔。
解决思路有两个方向:
- 预分配大块内存:一次性申请一大块连续显存,自己管理分配。比如 vLLM 的 PagedAttention 就是这么干的,把 KV Cache 切成固定大小的块,按需分配。
- 连续批处理(Continuous Batching):不等待整个批次完成,而是动态插入新请求。当一个请求的序列结束,立即释放它的显存,并分配给新请求。
我个人更推荐连续批处理。它不仅能缓解碎片化,还能提高吞吐。你想想看,传统批处理就像等一桌人全吃完才翻台,连续批处理则是边吃边翻台,效率自然高。
核心逻辑:连续批处理的核心是「动态调度」。每个请求的生成进度不同,有的刚进来,有的快结束了。系统实时监控显存使用,把空闲的 KV Cache 块分配给新请求。
下面这张图展示了显存碎片化与连续批处理的对比:
实现连续批处理,我建议关注几个关键点:
- 请求队列管理:维护一个等待队列,当有显存释放时,立即从队列中取出新请求。
- KV Cache 块分配:使用固定大小的块(如 16 或 32 个 token 的 KV Cache),减少碎片。
- 调度策略:优先处理即将结束的请求,尽快释放显存。
注意:连续批处理虽然好,但实现复杂度高。如果团队资源有限,可以先从预分配大块内存入手,逐步过渡。我曾经在一个小团队里强行上连续批处理,结果调度逻辑写了一个月还没调通,得不偿失。
最后总结一下:显存管理是批处理策略的基石。算清楚最大批大小,理解碎片化问题,再选择合适的批处理方式,你的推理服务才能跑得稳、跑得快。