3、推理场景下的显存特征:推理与训练的内存需求差异、动态形状(Dynamic Shape)带来的挑战、KV Cache的碎片化问题
3.1 推理 vs 训练:内存需求的天壤之别
做推理优化这么多年,我最大的感受就是——推理和训练对显存的需求,完全是两码事。
训练时,显存里塞的是什么?是激活值、梯度、优化器状态、参数副本……说白了,训练是“算完还要留着反向传播用”。你想想看,一个 batch 下去,每一层的激活值都得存着,等反向传播时再拿出来算梯度。这显存消耗,跟序列长度、batch size 几乎是线性增长。
但推理不一样。推理是前向传播,算完就扔。我经常跟团队说:推理的显存,核心就三块——模型参数、临时计算缓冲区、KV Cache。
| 对比维度 | 训练 | 推理 |
|---|---|---|
| 激活值存储 | 需要保留所有层激活值 | 逐层计算,用完即释放 |
| 梯度/优化器状态 | 需要存储 | 不需要 |
| 参数副本 | 可能有多份(数据并行) | 通常只有一份 |
| KV Cache | 通常不需要(或仅用于验证) | 核心内存消耗点 |
| 显存峰值 | 高(batch size 大) | 相对低,但碎片化严重 |
我在项目中遇到过好几次这样的情况:训练时显存够用,一到推理部署就 OOM。为什么?因为推理的显存分配模式跟训练完全不同。训练是“一次性分配,长期持有”,推理是“频繁分配,频繁释放”。这种模式,对显存管理的要求更高。
核心结论:推理的显存瓶颈不在总量,而在分配和释放的碎片化问题。
3.2 动态形状(Dynamic Shape)带来的挑战
说到动态形状,我得先吐槽一句:这玩意儿是推理优化的头号杀手。
什么叫动态形状?说白了就是每次请求的输入长度不一样。今天用户问“你好”,明天用户问“请帮我写一篇关于人工智能在医疗领域应用前景的详细分析报告”。这两个请求的序列长度,差了十倍不止。
为什么会带来挑战?我举个例子你就明白了。
假设你给模型分配了一个最大长度 2048 的缓冲区。短请求来了,只用了 10 个 token,剩下 2038 个 token 的空间就浪费了。长请求来了,如果超过 2048,直接截断或报错。你想想看,这显存利用率能高吗?
我见过不少团队的做法是:按最大长度预分配。简单粗暴,但显存浪费严重。另一种做法是动态扩容——请求来了,按实际长度分配,不够了再扩。但这样会带来频繁的内存 realloc,性能损失很大。
我的建议:不要一刀切。可以按请求长度分桶(bucketing),比如 128、256、512、1024、2048 几个档位。短请求进小桶,长请求进大桶。这样既避免了浪费,又减少了动态分配次数。
我曾经在一个项目中,因为动态形状导致推理延迟抖动超过 50%。排查了半天,发现是每次请求长度不同,导致显存分配器频繁触发碎片整理。后来改用分桶策略,延迟抖动降到了 5% 以内。
3.3 KV Cache 的碎片化问题
好,终于说到重点了——KV Cache 的碎片化。这是推理场景下最让人头疼的问题之一。
先简单回顾一下:在自回归生成中,每生成一个 token,都需要用到之前所有 token 的 Key 和 Value。为了不重复计算,我们把它们缓存起来,这就是 KV Cache。
但问题来了:每个请求的序列长度不一样,生成的 token 数也不一样。有的请求生成 10 个 token 就结束了,有的生成 500 个。KV Cache 的大小是动态变化的。
你想想看,显存分配器面对的是什么样的场景?
- 请求 A 来了,分配 100 个 token 的 KV Cache
- 请求 A 生成了 50 个 token,释放了
- 请求 B 来了,需要 200 个 token 的 KV Cache
- 但刚才释放的 100 个 token 空间不连续,凑不出 200 个
这就是外部碎片。更麻烦的是内部碎片——你给请求分配了 1024 的缓冲区,它只用了 300,剩下的 724 就白白浪费了。
注意:KV Cache 碎片化不仅浪费显存,还会导致分配延迟增加。当显存碎片严重时,分配器可能需要遍历空闲链表、合并相邻块,甚至触发垃圾回收。这些操作都会增加推理延迟。
我记得有一次线上事故,推理服务的 P99 延迟从 200ms 飙升到 2s。查了半天,发现是 KV Cache 碎片化导致显存分配耗时暴增。后来我们做了两件事:
- 预分配 + 分桶:按请求长度分桶,每个桶预分配固定大小的 KV Cache 块
- 内存池化:请求结束后,KV Cache 不释放回系统,而是放回内存池复用
效果立竿见影,P99 延迟降回了 250ms 以内。
3.4 一张图看懂推理显存特征
下面这张图,是我自己总结的推理显存特征全景。你看完应该能有个整体把握。
3.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
坑1:我曾经以为显存总量够大就不会有碎片问题。结果 80GB 的 A100,推理时只用了 30GB 就 OOM 了。原因就是碎片太多,分配不出连续的大块内存。
坑2:动态形状下,不要用默认的 CUDA 分配器。默认分配器对碎片化场景优化不足。我建议用 cudaMallocAsync 或者第三方库如 cub 的分配器,效果会好很多。
坑3:KV Cache 的预分配大小不是越大越好。我见过有人给每个请求预分配 2048 的 KV Cache,结果大部分请求只用了几十个 token。显存利用率不到 10%。按实际需求分桶,才是正道。
嗯,这一章的内容就到这里。显存碎片化是个系统工程问题,不是改一个参数就能解决的。你需要从分配策略、内存管理、请求调度等多个维度去优化。希望这一章能给你一个清晰的思路。
一句话总结:推理场景的显存管理,核心是应对动态性和碎片化。分桶预分配 + 内存池化,是目前最实用的组合拳。