1. 从一次显存告警说起:为什么推理课要先讲内存
很多人第一次接触大模型推理,注意力全在模型权重上——参数量多大、精度多高、效果多好。直到某天在单卡上加载一个 7B 模型,权重明明只占十几 GB,显存却直接爆掉,服务起不来,才开始意识到:推理阶段真正吃显存的,往往不是权重本身,而是那些看不见的中间状态。
这正是 vLLM 系列课程把"推理与内存基础"单独拎出来讲的原因。模型跑起来只是第一步,能不能稳定地跑、能不能同时服务多个请求、能不能把有限的显存榨出最大吞吐,全取决于你对内存这件事的理解深度。这一课的核心,就是要把大模型推理过程中显存到底被谁吃掉了、KV Cache 是什么、为什么它决定了并发上限、以及 vLLM 用什么机制把这块内存管起来,一层层拆开讲清楚。
这篇文章适合三类人:一是刚上手本地部署、被显存问题反复折磨的开发者;二是准备把模型推上生产、需要估算硬件成本的同学;三是想搞懂 vLLM 为什么比朴素推理快那么多的技术爱好者。读完之后,你应该能自己算出一个模型在给定显存下大概能扛多少并发,也能明白 PagedAttention 到底解决了什么痛点。整篇内容围绕推理内存这条主线展开,不堆砌公式,尽量用能上手的方式讲透。
2. 推理阶段显存到底被谁吃掉了
2.1 权重只是显存账单的第一笔
先建立一个基本认知:模型权重占用的显存是可以精确计算的。以常见的 FP16 精度为例,每个参数占 2 字节,一个 7B 模型大约需要 14GB,13B 约 26GB,70B 则接近 140GB。这个数字是静态的、可预测的,加载完就固定在那里,不会随请求变化。
但实际部署时你会发现,一张 24GB 的卡加载 7B 模型后,可用显存远没有 10GB 那么宽裕。原因在于权重之外还有几块开销:框架自身的运行时开销、CUDA 上下文、激活值缓冲区、以及最关键的 KV Cache。很多人估算硬件时只算了权重,结果上线后并发一上来就 OOM,问题几乎都出在没给 KV Cache 留够空间。
提示:做容量规划时,权重占用只是下限。真正决定你能开多少并发的,是权重之外剩下的那部分显存怎么分配。
2.2 KV Cache:推理内存的真正主角
要理解 KV Cache,得先回到 Transformer 的自注意力机制。模型生成每一个新 token 时,都需要拿当前 token 的 Query 去和前面所有 token 的 Key、Value 做注意力计算。如果每生成一个 token 都把前面所有 token 重新算一遍 K 和 V,计算量会随序列长度平方级增长,完全不可接受。
于是有了缓存:把已经算过的 Key 和 Value 存下来,生成新 token 时直接复用,只计算当前 token 的 K、V 并追加到缓存里。这就是 KV Cache。它把每步的计算复杂度从随长度平方降到了线性,代价是用显存换时间。
KV Cache 的大小可以这样估算:
KV Cache 字节数 = 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × 批大小 × 精度字节其中前面的 2 代表 Key 和 Value 两份。以某个 7B 模型为例,假设 32 层、32 个头、头维度 128、FP16 精度,那么单个 token 的 KV Cache 约为 2 × 32 × 32 × 128 × 2 = 512KB。如果序列长度 2048、批大小 1,就是约 1GB;批大小开到 16,直接飙到 16GB。这就是为什么长上下文和高并发会迅速吃光显存。
2.3 激活值与临时缓冲区:容易被忽略的零头
除了权重和 KV Cache,前向计算过程中还会产生激活值。这些是层与层之间传递的中间结果,计算完就可以释放,属于临时开销。在推理(尤其是 decode 阶段)中,激活值占用相对较小,但在 prefill 阶段处理长 prompt 时会明显增大。
还有一类是框架预留的临时缓冲区,比如用于 kernel 计算的 workspace、通信缓冲区等。这些通常不大,但在显存紧张时也会成为压垮骆驼的最后一根稻草。做精细调优时,可以通过调整框架参数来压缩这部分开销,但收益有限,重点还是放在 KV Cache 的管理上。
把这三块加起来看:权重是固定的地基,KV Cache 是随负载浮动的可变部分,激活值是临时过客。理解了这张账单,你就知道优化显存的方向在哪里——不是去抠权重,而是去管好 KV Cache。
3. 从连续分配到分页:KV Cache 管理的演进逻辑
3.1 朴素方案为什么浪费严重
最直觉的 KV Cache 管理方式,是给每个请求预分配一块连续显存,大小按最大可能序列长度来算。比如模型支持 4096 上下文,那就给每个请求都预留 4096 个 token 的缓存空间。听起来简单,问题却很大。
首先,绝大多数请求的实际输出长度远小于最大值。用户问一句话,模型回几十个 token 就结束了,但显存里那块 4096 的空间从头到尾都被占着,利用率可能只有百分之几。其次,显存碎片化严重。不同请求的缓存块大小不一、生命周期不同,连续分配会导致大量无法利用的空洞。最后,并发能力被严重限制——明明显存还有富余,却因为要预留大块连续空间而无法接纳新请求。
这就是早期推理框架吞吐上不去的核心原因之一:不是算力不够,而是显存被低效地锁死了。
3.2 PagedAttention 的核心思路
vLLM 提出的 PagedAttention,灵感来自操作系统的虚拟内存分页机制。核心思想是:不再要求每个序列的 KV Cache 在物理上连续,而是把它切成固定大小的块(block),每个块存放固定数量 token 的 K 和 V。这些块在物理显存里可以分散存放,通过一张映射表把逻辑上的序列位置对应到物理块。
这样做的好处立竿见影。第一,按需分配,序列长到哪就分配到哪,不再预留最大长度,浪费大幅减少。第二,块大小固定,显存管理变成简单的块分配和回收,几乎不产生碎片。第三,块之间相互独立,多个序列如果共享相同的前缀(比如相同的系统提示词),可以让它们的逻辑映射指向同一批物理块,实现内存共享。
注意:PagedAttention 的块大小是个需要权衡的参数。块太小,映射表开销大、管理频繁;块太大,内部碎片又会增加。常见取值在 16 左右,具体要结合模型和负载实测。
3.3 内存共享带来的额外收益
分页机制还顺带解决了一个场景:并行采样和束搜索。当同一个 prompt 要生成多个候选输出时,这些输出共享同一段前缀。传统方案下,每个候选都要复制一份前缀的 KV Cache,显存成倍增长。而在分页机制下,前缀对应的物理块被多个序列共同引用,只有分叉之后的部分才各自分配新块。
这种写时复制(Copy-on-Write)的思路,和操作系统里 fork 进程共享内存如出一辙。它让采样类任务的显存开销大幅下降,也让 vLLM 在处理这类请求时优势明显。理解这一点,你就能明白为什么同样的硬件,换用 vLLM 后并发数能翻好几倍——省下来的不是算力,是被浪费的显存。
4. 显存预算怎么算:一个可复现的估算流程
4.1 先确定可用显存基线
估算的第一步,是搞清楚真正能拿来用的显存有多少。假设你有一张标称 24GB 的卡,实际可用会略低于这个数,因为驱动和 CUDA 上下文会占用一部分。稳妥起见,可以按标称值的 90% 到 95% 来估算,也就是大约 22GB 左右。
然后减去权重占用。7B FP16 约 14GB,剩下约 8GB 给 KV Cache 和其他开销。如果用的是量化版本,比如 INT8 或 INT4,权重会显著缩小,留给 KV Cache 的空间就更多。这也是量化在推理场景受欢迎的原因之一——不只是省权重,更是为并发腾出空间。
4.2 用 gpu_memory_utilization 控制分配比例
vLLM 提供了一个关键参数gpu_memory_utilization,默认 0.9,表示允许使用 90% 的显存。框架会先加载权重,然后把剩余显存中的大部分划给 KV Cache 池。这个参数设得太高,容易和框架其他开销冲突导致 OOM;设得太低,又浪费了显存。
我的经验是:初次部署先用默认值跑通,观察实际占用,再逐步微调。如果服务稳定且显存有余量,可以适当上调;如果频繁 OOM,就往下调一点,或者检查是不是并发配置过高。
from vllm import LLM llm = LLM( model="your-model-path", gpu_memory_utilization=0.9, max_model_len=4096, tensor_parallel_size=1, )4.3 反推并发上限
有了 KV Cache 池的总大小,就能反推能支持多少并发。公式很简单:
可缓存 token 总数 = KV Cache 池字节数 / 单 token KV 字节数 并发数 ≈ 可缓存 token 总数 / 平均序列长度假设 KV Cache 池有 8GB,单 token 缓存 512KB,那么可缓存约 16384 个 token。如果平均每个请求占用 1024 个 token(输入加输出),理论上能同时服务约 16 个请求。这个数字是估算,实际会受调度策略、请求长度分布影响,但足以帮你判断硬件是否够用。
| 参数 | 示例值 | 说明 |
|---|---|---|
| 显存总量 | 24GB | 标称值 |
| 可用基线 | 约 22GB | 扣除驱动等开销 |
| 权重占用 | 14GB | 7B FP16 |
| KV Cache 池 | 约 8GB | 剩余部分 |
| 单 token 缓存 | 512KB | 由模型结构决定 |
| 可缓存 token | 约 16384 | 池大小除以单 token |
| 估算并发 | 约 16 | 按平均 1024 token 算 |
这张表可以直接套用到你自己的模型上,把权重和单 token 缓存换成实际值即可。养成先算账再上线的习惯,能省下大量反复试错的时间。
5. 实测中那些文档不会告诉你的细节
5.1 长 prompt 和长输出对显存的影响不对称
很多人以为显存占用只和总 token 数有关,其实 prefill 和 decode 两个阶段的特征完全不同。prefill 阶段一次性处理整个输入 prompt,计算密集,激活值占用高,但 KV Cache 是逐步写入的。decode 阶段每步只处理一个 token,计算量小,但 KV Cache 持续增长。
这意味着一个超长输入的请求,会在 prefill 瞬间产生较大的激活值峰值,可能触发瞬时 OOM;而一个超长输出的请求,则是慢慢把 KV Cache 撑大。调优时要分别对待:前者关注 prefill 的批处理策略,后者关注 KV Cache 池的容量。
5.2 块大小和调度策略要一起调
前面提到块大小需要权衡,但很多人忽略了它和调度策略的联动。vLLM 的调度器会在显存不足时决定抢占哪些序列、如何换出。块大小会影响调度的粒度——块越小,调度越灵活,但管理开销越大。实测中,如果发现吞吐上不去,不妨试试调整块大小,配合观察调度日志,往往能找到更优的组合。
提示:不要孤立地调单个参数。显存利用率、块大小、最大序列长度、并发上限这几个是相互牵制的,改一个要重新评估其他几个。
5.3 量化不是万能药,要看场景
量化能省权重显存,但对 KV Cache 的影响要分情况。如果只量化权重,KV Cache 仍是 FP16,那么省下的显存主要来自权重部分,对并发的提升有限。如果连 KV Cache 也量化,收益会明显得多,但可能带来精度损失,需要评估对业务效果的影响。
我的建议是:先用权重量化把模型塞进单卡,跑通流程;如果并发仍不够,再考虑 KV Cache 量化,并且一定要做效果对比测试,别为了吞吐牺牲了输出质量。
5.4 监控比调参更重要
上线之后,别只盯着 QPS 看。要监控 KV Cache 的使用率、被抢占的序列数、请求排队时间这些指标。KV Cache 使用率长期接近 100%,说明显存是瓶颈,该考虑加卡或优化;如果使用率很低但吞吐上不去,问题可能在计算或调度,而不是显存。
把这些指标接进监控面板,比事后拍脑袋调参靠谱得多。我见过太多团队在 OOM 之后才去翻日志,其实提前监控就能发现苗头。
6. 把内存这件事想透,推理优化就成功了一半
回过头看,vLLM 这一课把推理与内存基础放在前面,是有道理的。大模型推理的性能瓶颈,很多时候不在算力,而在显存怎么用。权重是死的,KV Cache 是活的,PagedAttention 把这块活的内存管得井井有条,才有了高吞吐的可能。
我自己在实际部署中的体会是:与其一上来就追求极限并发,不如先把显存账算清楚,把 KV Cache 的估算流程跑一遍,心里有个底。然后再根据监控数据逐步调优,每次只动一个变量,观察效果。这样既不会盲目加硬件,也不会因为参数乱调而陷入反复 OOM 的循环。
最后分享一个小技巧:做容量规划时,把平均序列长度往大了估一点,留出安全余量。因为线上请求的长度分布往往比测试时更极端,长尾请求会悄悄吃掉你预留的显存。宁可保守一点,也别让服务在高峰期因为一个超长请求而崩掉。