作者:互联网 时间: 2026-07-21 18:25:55
大模型生成文本时,不是一次性把完整答案写出来。

它是一个 token 一个 token 往前走:
根据已有上下文,预测下一个 token把新 token 接到上下文后面再预测下一个 token
这件事听起来简单,但一旦上下文变长,问题就来了。
比如一个请求里有:
system prompt工具说明用户问题几十页文档历史对话中间推理结果
模型每生成一个新 token,都要参考前面这些内容。
如果每一步都把前文完整重算一遍,推理会非常慢。
所以推理框架都会使用 KV Cache:
把前文已经算过的 Key / Value 缓存下来后面生成新 token 时直接复用
这就是 KV Cache 的基本作用。
但工程上真正麻烦的地方不在这里。
真正麻烦的是:
KV Cache 省了计算,却把压力转移到了显存。
上下文越长、并发越高、输出越久,KV Cache 占用就越大。
部署模型时,KV Cache 最终会变成一道资源预算题:
显存有多少?上下文多长?并发多高?延迟和吞吐怎么取舍?
这篇文章按这个问题链展开:
为什么需要 KV Cache它到底缓存了什么显存占用怎么估算部署时怎么配置和排查
先从最朴素的生成过程看起。
假设模型要生成这句话:
KV Cache 可以减少重复计算
它不是一次生成整句话,而是逐步生成:
KVKV CacheKV Cache 可以KV Cache 可以减少KV Cache 可以减少重复...
每走一步,前文就变长一点。
而 Transformer 的 attention 机制要求当前 token 去看前面的 token。
也就是说,当模型生成“重复”时,它要参考:
KV / Cache / 可以 / 减少
当它继续生成“计算”时,它又要参考:
KV / Cache / 可以 / 减少 / 重复
上下文越长,历史越多。
如果没有缓存,模型每一步都要重新处理完整前文。
这就像你写文章时,每写一个字都从标题开始重新读一遍全文。
能做,但很浪费。
KV Cache 的第一个动机就在这里:
前文已经算过了,就不要每一步都重新算。
要理解 KV Cache,绕不开 attention 里的三个量:
Q:QueryK:KeyV:Value
可以先用一个不严格但好理解的说法:
Q:当前 token 想找什么信息K:历史 token 可以被怎样匹配V:历史 token 真正提供什么内容
当前 token 会拿自己的 Q,去和前文每个 token 的 K 做匹配。
匹配分数越高,说明当前 token 越应该关注那个历史 token。
然后模型再根据这些分数,把对应的 V 加权汇总。
简化成一条链:
当前 token 的 Q↓匹配历史 token 的 K↓得到 attention 权重↓加权汇总历史 token 的 V
所以 attention 本质上是在每一层里做一次“查找和汇总”。
那为什么缓存的是 K/V,不是 Q/K/V?
原因在自回归生成的方向。
大语言模型生成时,是从左到右的。
准备生成下一个 token 时,真正发起查询的是“当前位置”。
所以当前 token 需要新的 Q。
但历史 token 的 K/V 已经算好,可以直接复用。
比如当前上下文是:
KV Cache 可以
模型已经为前面三个 token 算过:
KV -> K1, V1Cache-> K2, V2可以 -> K3, V3
下一步生成新 token 时,只需要:
算新 token 的 Q/K/V用新 token 的 Q 去看旧 K/V把新 token 的 K/V 追加进缓存
旧 token 的 Q 对后续生成没有太大价值。
因为旧 token 不再作为“当前位置”发起查询。
所以缓存的是:
Key / Value
这就是 KV Cache 这个名字的来源。
一次完整的大模型请求,可以拆成两个阶段:
先读入输入:prefill再生成输出:decode
这两个词听起来有点工程化,但其实很好理解。
prefill 就是模型正式回答前,先把你的输入全部读一遍。
这个输入包括:
system prompt用户问题历史对话工具说明RAG 检索出来的文档你塞进去的长文本
比如你把一段 128K tokens 的长文档发给模型。
模型不能没读文档就开始答。
它要先把这 128K tokens 过一遍 Transformer,并在每一层里为这些 token 生成对应的 K/V。
这个过程就是 prefill。
所以 prefill 可以理解成:
处理 prompt理解输入建立初始 KV Cache准备生成第一个 token
这个阶段通常决定首 token 延迟,也就是:
Time To First Token,TTFT
如果 prompt 很长,prefill 就会很重。
用户感受到的就是:
我发出请求后,模型迟迟没有开始吐第一个字。
decode 则发生在模型开始输出之后。
从第一个输出 token 开始,模型进入这种循环:
生成一个新 token把新 token 追加到上下文为新 token 计算新的 K/V把新的 K/V 追加到 KV Cache继续生成下一个 token
每生成一个新 token,模型会复用前面已经缓存的 K/V,只为新 token 追加新的 K/V。
这个阶段决定生成过程中的速度,也就是:
Inter-Token Latency,ITL
如果输出很长,decode 就会跑很久。
用户感受到的就是:
模型已经开始输出了,但后面每个 token 出得慢。
所以 KV Cache 对推理速度的贡献可以这样理解:
prefill 阶段:为整段输入建立初始 KV Cache,影响 TTFTdecode 阶段:持续复用并追加 KV Cache,影响 ITL 和总生成时长
没有 KV Cache,decode 会反复重算历史。
有了 KV Cache,decode 仍然要看历史,但不用把历史 token 的 K/V 重新投影一遍。
这就是流式生成能跑起来的基础。
也因此,后面调 vLLM 时要先判断慢在哪里:
第一个 token 慢:优先看 prefill、长 prompt、prefix caching后续 token 慢:优先看 decode、KV Cache 压力、batching 和显存
KV Cache 省的是计算,花的是显存。
它不是一个小缓存。
它要为每个请求保存:
batch sizesequence length会产生 KV Cache 的 attention 层数KV heads 数量每个 head 的维度K 和 V 两份数据精度
粗略公式是:
KV Cache 显存= batch_size× sequence_length× attention_layers× 2× num_kv_heads× head_dim× bytes_per_element
这几个变量不要一口气硬背,可以分成三组。
第一组是请求侧变量:
batch_sizesequence_length
batch_size 表示同一时刻有多少条序列在占用 KV Cache。
在离线推理里,它可能就是一次 batch 里有多少条样本。
在在线服务里,它更接近:
当前正在被 vLLM 调度、还没生成完的请求数量
所以这里的 batch_size 不只是代码里手写的 batch size。
只要线上同时有很多用户请求,每条请求都在生成,每条请求就都需要自己的 KV Cache。
并发越高,这一项越大。
sequence_length 表示每条序列当前占了多少 token。
它不只是 prompt 长度,而是:
sequence_length = prompt tokens + 已经生成的 output tokens
所以长 prompt 会吃 KV Cache,长输出也会吃 KV Cache。
比如用户发了 128K tokens 的文档,又让模型生成 8K tokens 的报告,那么这条序列最后接近:
128K + 8K = 136K tokens
这两个变量是部署时最容易被低估的。
因为很多人只看“模型能支持 262K 上下文”,却忘了:
262K 是单条序列的上限线上还要乘以同时存在的序列数
第二组是模型侧变量:
attention_layersnum_kv_headshead_dim
attention_layers 表示有多少层会产生标准 attention 的 K/V cache。
对传统 decoder-only Transformer 来说,它通常接近模型层数。
比如 32 层模型,可以先按 32 层估。
但如果模型是混合架构,就不能只看总层数。
有些层可能不是标准 attention,不会按同样方式产生 K/V cache。
后面讲 Qwen3.5-9B 时,这个点会直接影响估算结果。
num_kv_heads 表示 KV head 的数量。
注意它不一定等于 Q head 的数量。
很多模型会用 GQA,也就是 Grouped-Query Attention:
Q heads 可能很多KV heads 可能更少多个 Q heads 共享一组 K/V
比如:
Q heads = 16KV heads = 4
估 KV Cache 时,用的是 KV heads = 4,不是 Q heads = 16。
这也是 GQA 能降低 KV Cache 显存占用的原因之一。
head_dim 表示每个 attention head 的维度。
如果 head_dim = 256,就表示每个 KV head 里,一个 token 的 K 向量有 256 个数,V 向量也有 256 个数。
这三个变量基本由模型配置决定。
部署时你一般不会改它们。
你要做的是从模型 config 或 model card 里把它们找出来,别填错。
第三组是存储侧变量:
2bytes_per_element
公式里的 2 表示同时存两份东西:
一份 K一份 V
所以 KV Cache 是 Key + Value 的缓存,不是只存 Key。
bytes_per_element 表示每个数占多少字节。
常见情况是:
FP16 / BF16:2 bytesFP8:1 byte
所以 FP8 KV Cache 的直觉很简单:
同样的上下文和并发,KV Cache 显存理论上接近减半
但这是用精度换容量,后面要配合评估验证。
把这三组放在一起看,KV Cache 优化就清楚多了:
请求侧:并发多少、prompt 多长、输出多长模型侧:attention 层数、KV heads、head_dim存储侧:BF16 / FP16 / FP8
其中你部署时最能直接控制的是请求侧和存储侧。
模型侧变量通常是选模型时就决定了。
但先别急着调参数。
公式真正想告诉你的,是 KV Cache 的增长方式:
上下文长度翻倍,KV Cache 近似翻倍并发数翻倍,KV Cache 近似翻倍attention 层数越多,KV Cache 越大KV heads 越多,KV Cache 越大
这就是长上下文推理贵的原因。
不是模型权重放进 GPU 就万事大吉。
你还要给每个正在服务的请求,留出它自己的 KV Cache 空间。
模型权重像固定成本。
KV Cache 像运行时成本。
用户越多、上下文越长、生成越久,运行时成本越高。
我们用 Qwen/Qwen3.5-9B 这个 dense / 非 MoE 专家路由模型来算一遍。[1][2]
Qwen3.5-9B 不是传统的“32 层全都是 full attention”的 Transformer。
它的文本模型一共有 32 层,attention 类型按 4 层一组循环:
连续 3 个 linear attention block再接 1 个 full attention block
这组结构重复 8 次,所以可以写成:
8 × (3 × linear attention + 1 × full attention)
这里的 3 个 linear attention block 不是三种不同功能的层,而是同一类层连续出现三次。
先用一张表把 linear attention 和 full attention 的差别说清楚:
如果只看上下文长度 n 这一维,可以粗略对比成:
| 关注点 | full attention | linear attention |
|---|---|---|
| 基本做法 | 当前 token 和所有历史 token 逐个匹配 | 把历史信息压到可递推更新的紧凑状态 |
| prefill 复杂度 | 约 O(n²) | 通常接近 O(n) |
| decode 单 token 复杂度 | 每生成 1 个 token,要看 n 个历史 token,约 O(n) | 更多依赖递推状态,通常接近 O(1) |
| 缓存形态 | 按历史 token 保存标准 K/V cache,约 O(n) | 不按每个历史 token 保存完整标准 K/V,更多保存紧凑状态 |
| 长上下文压力 | 上下文越长,计算和 KV Cache 压力越明显 | 更适合长上下文,显存和计算增长更温和 |
表里的 O(n) / O(n²) 不是精确性能公式,真实速度还会受 kernel、batching、GPU 利用率、head_dim、实现细节影响。
但它足够说明这里最重要的区别:
full attention 的成本会明显随上下文长度增长linear attention 试图把长上下文成本压得更接近线性
这里先不展开所有 attention 变体,只关注一个和 KV Cache 估算直接相关的问题:
哪些层需要按历史 token 保存标准 K/V cache?
后面可以单独写一篇,系统讲 full attention、linear attention、sliding window attention、GQA / MQA 分别解决什么问题。
放回 Qwen3.5-9B 这个例子,它的设计是在两件事之间折中:
大多数层用 linear attention 降低长上下文成本;少数层用 full attention 保留更强的精细注意力能力。
这也是它支持长上下文时 KV Cache 压力相对小的原因之一:
不是每一层都要按 full attention 保存标准 K/V cache。
接下来只取和标准 attention KV Cache 直接相关的字段:
总层数:32layer_types:8 × (3 × linear attention + 1 × full attention)num_attention_heads:16num_key_value_heads:4head_dim:256max_position_embeddings:262,144
它们分别对应:
总层数:模型一共有多少层,但不能直接等同于 attention_layerslayer_types:哪些层是 linear attention,哪些层是 full attentionnum_attention_heads:Q heads 数量,用来理解 attention 结构num_key_value_heads:KV heads 数量,真正进入 KV Cache 公式head_dim:每个 head 的维度,真正进入 KV Cache 公式max_position_embeddings:模型默认最大上下文长度
最容易看错的是 总层数 和 layer_types 的关系。只看总层数 32,很容易以为:
attention_layers = 32
但对 Qwen3.5-9B 来说,标准 full attention 层只有 8 个,所以估标准 attention KV Cache 时:
attention_layers = 8
再把变量代入公式:
attention_layers = 8num_kv_heads = 4head_dim = 256bytes_per_element = 2# BF16 / FP16
单个 token 的 KV Cache 大约是:
8 × 2 × 4 × 256 × 2 bytes= 32,768 bytes= 32 KB
这 32 KB 的含义是:
每多一个 token这条序列大约多占 32 KB KV Cache
所以单条序列在不同上下文长度下,大概是:
| 上下文长度 | 单条序列 KV Cache 粗算 |
|---|---|
32K tokens | 约 1 GB |
128K tokens | 约 4 GB |
262K tokens | 约 8 GB |
如果线上同时有 8 条 128K 上下文请求在生成,就要再乘以并发:
4 GB × 8 = 32 GB
如果把 KV Cache 从 BF16 / FP16 换成 FP8,bytes_per_element 从 2 变成 1,理论上接近减半:
| 上下文长度 | BF16 / FP16 | FP8 |
|---|---|---|
128K tokens | 约 4 GB | 约 2 GB |
262K tokens | 约 8 GB | 约 4 GB |
这就是这节最重要的结论:
Qwen3.5-9B 在 262K 上下文下,单条序列的标准 attention KV Cache 粗算约 8 GB。
但这个估算值只能当量级参考。
真实部署还会有:
模型权重临时 tensorCUDA / NCCL / runtime 开销调度器开销多模态模块开销vLLM 预留和碎片
所以不要把它当成精确显存账本。
它更适合用来判断:
当前上下文长度和并发目标,大概是不是离谱?
最后提醒一个常见误算。
如果你把 Qwen3.5-9B 当成 32 层全 attention 模型来算:
32 × 2 × 4 × 256 × 2 bytes= 128 KB / token262K tokens -> 约 32 GB
这个结果会明显偏大。
原因不是公式错了,而是 attention_layers 填错了。
估 KV Cache 时,不要只看总层数。
要看有多少层真的会产生标准 attention 的 K/V cache。
理解显存账以后,下一步是看它在部署里怎么体现出来。
KV Cache 通常不需要你手动实现,现代推理框架一般已经内置。
你真正要关心的是三件事:
怎么给 KV Cache 分配显存怎么观察 KV Cache 是否不够用怎么根据 workload 调整上下文、并发和缓存策略
下面用 Qwen3.5-9B + vLLM 作为例子,把实践拆成三步:
启动时:给模型和 KV Cache 一个初始预算运行时:观察是不是出现 KV Cache 压力调参时:根据现象调整显存、并发、前缀复用和精度
vLLM 只是一个落地框架,不是唯一答案。其他推理框架也会面对类似问题:
KV Cache 放不放得下?一次调度多少请求合适?重复 prompt 能不能复用?KV Cache 能不能低精度保存?无效上下文能不能少塞一点?
vLLM 是一个大模型推理服务框架。你可以把它理解成:
模型权重已经有了GPU 也已经有了vLLM 负责把很多用户请求调度到 GPU 上,尽量让吞吐更高、延迟更稳、显存浪费更少。
KV Cache 是 vLLM 必须重点管理的资源,因为:
模型权重通常是固定占用KV Cache 会随着请求、上下文长度、输出长度不断变化
很多请求同时进来时,prompt 长度、输出长度和生命周期都不同,KV Cache 会变成一堆大小不同、释放时间不同的显存块。
vLLM 的 PagedAttention / block-based KV cache 管理,可以先粗略理解成:
不要给每条请求一次性分配一整块连续显存而是把 KV Cache 拆成很多小 block请求需要多少,就分配多少 block请求结束后,再把 block 回收给别的请求
这样可以减少显存碎片和浪费,让同一张 GPU 更稳定地服务更多请求。
这里不展开 vLLM 内部实现,只把它当成一个实践窗口:
我们前面算出来的 KV Cache 显存账,在真实部署里会变成哪些启动参数、日志信号和调整动作?
启动服务时,先不要急着追求“上下文越大越好”。
你至少要先定两个预算:
上下文预算:单条请求最长允许多少 token显存预算:这张 GPU 最多给 vLLM 用多少显存
一个纯文本服务的基础启动命令可以写成:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --gpu-memory-utilization 0.92
这里几个参数的含义是:[3]
| 参数 | 作用 |
|---|---|
--max-model-len 262144 | 允许单条请求最长到 262K tokens |
--reasoning-parser qwen3 | 按 Qwen3 系列格式解析 reasoning 内容 |
--language-model-only | 纯文本服务时不加载多模态能力 |
--gpu-memory-utilization 0.92 | vLLM 最多使用这张 GPU 92% 的显存 |
如果你要用 Qwen3.5 的视觉能力,就不要加 --language-model-only。
这里和 KV Cache 最直接相关的是 --max-model-len 和 --gpu-memory-utilization。
前者决定单条请求最多能长到多少 token;后者决定 vLLM 最多使用多少比例的 GPU 显存。
0.92 不是“需要 0.92GB 显存”,而是:
vLLM 最多可以使用这张卡 92% 的显存
比如一张 40GB 显卡,可用预算大约是:
40GB × 0.92 = 36.8GB
那剩下的 8% 显存去哪了?
它不是浪费,而是安全垫,用来留给 CUDA、临时 tensor、通信 buffer、显存碎片和运行时峰值波动。
如果把 gpu_memory_utilization 拉得太满,短时间看起来能多放一点 KV Cache,但高峰请求、长输出或并发波动时更容易 OOM。
这 36.8GB 里面要同时放:
模型权重KV Cache运行时临时开销CUDA / kernel / 调度开销
那这条命令大概要多大的显卡?
以 Qwen3.5-9B 的纯文本 BF16 / FP16 推理粗算:
模型权重:约 18GB262K 单条序列 KV Cache:约 8GB运行时和框架开销:预留几 GB 到十几 GB
如果你真的想跑 --max-model-len 262144,并且希望单条请求能接近 262K 上下文,比较稳的判断是:
| 显卡显存 | 粗略判断 |
|---|---|
24GB | 通常不适合 BF16 / FP16 跑满 262K 上下文 |
40GB | 单条长上下文有机会,但要控制并发并预留运行时开销 |
80GB | 更适合长上下文和一定并发 |
如果显存不够,可以先从两个方向缩小预算。
第一,降低上下文长度:
max_model_len:262K -> 128K单条序列 KV Cache:约 8GB -> 约 4GB
第二,使用 FP8 KV Cache:
BF16 / FP16 KV Cache:262K 约 8GBFP8 KV Cache:262K 约 4GB
所以显卡大小不是只由模型参数量决定,而是由这几项一起决定:
模型权重最大上下文长度并发序列数KV Cache 精度运行时预留空间
如果没有历史压测数据,可以先按下面这些思路起步,再用真实 workload 压测修正。
这里的“独占 GPU”指这张 GPU 上基本只跑这个 vLLM 实例,没有其他训练任务、推理服务、桌面进程或重型监控进程长期占用显存。
| 参数 | 先怎么设 | 什么时候调 | 主要代价 |
|---|---|---|---|
--gpu-memory-utilization | 0.85~0.90 稳妥起步;独占 GPU 时可试 0.90~0.95 | KV Cache 空间不够、频繁 preemption | 太高更容易 OOM,也会挤压运行时余量 |
--max-model-len | 按真实需求设,不要默认拉满;可先从 64K/128K 起步 | 需要支持更长 prompt 或更长输出 | 越大,最坏情况下 KV Cache 预算越高 |
--kv-cache-memory-bytes | 默认不设;需要精确控预算时再设,如 20G | 想直接指定每张 GPU 上 KV Cache 占多少显存 | 设置后会覆盖 gpu_memory_utilization 推断出的 KV Cache 大小 |
--max-num-seqs | 根据并发目标起步,显存紧就降低 | 同时运行请求太多、延迟抖动明显 | 调低会限制并发能力 |
--max-num-batched-tokens | 根据 prompt / output 长度和吞吐目标调 | 单轮调度 token 太多、KV Cache 压力大 | 调低可能降低吞吐 |
--enable-prefix-caching | 前缀重复多时建议开启 | 固定 system prompt、工具 schema、长文档多轮问答 | 需要前缀真的相同,prompt 组织要稳定 |
--kv-cache-dtype | 默认先不改;显存瓶颈明显时评估 fp8 | 想用更低精度 KV Cache 换容量 | 可能有质量影响,需要 eval 验证 |
--tensor-parallel-size / --pipeline-parallel-size | 单卡不够时再考虑 | 模型权重太大或希望释放单卡 KV Cache 空间 | 多卡通信和部署复杂度上升 |
比较常见的起步组合是:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 131072 --reasoning-parser qwen3 --language-model-only --gpu-memory-utilization 0.90 --enable-prefix-caching
如果确认显存是主要瓶颈,再逐步尝试:
--gpu-memory-utilization 0.92~0.95--kv-cache-dtype fp8--max-num-seqs 适当降低--max-num-batched-tokens 适当降低
没有一个“业界万能参数”。更接近生产实践的做法是:
先保守启动看 preemption / TTFT / ITL / 吞吐再按瓶颈逐项调整每次只改一两个参数用真实请求压测
运行起来以后,不要只看“显存占用高不高”。
判断 KV Cache 有没有压力,可以按三步走。
最直接的证据是 vLLM 日志里出现:
preempted ... not enough KV cache space
这基本可以说明:
当前这批请求需要的 KV Cache已经超过了当前可用空间
如果这个日志频繁出现,就可以优先按 KV Cache 空间不足处理。
如果没有明显日志,继续看 vLLM metrics。
重点看:
vllm:kv_cache_usage_percrunning requestswaiting requestsTTFTITLpreemption counter
判断逻辑是:
kv_cache_usage_perc 长时间很高waiting requests 开始增加TTFT / ITL 同时变差preemption counter 也在涨
这几个信号一起出现,基本就能判断 KV Cache 有压力。[7]
GPU 看板可以辅助判断,但不能单独下结论。
原因是 vLLM 可能会按 gpu_memory_utilization 预分配 GPU cache。
所以你看到显存占用很高,不一定说明 KV Cache 已经打爆。
可以按下面几种组合判断:
| 观察到的现象 | 更可能是什么问题 |
|---|---|
| KV cache usage 高 + waiting 增加 + TTFT/ITL 变差 + preemption 增加 | KV Cache 压力大 |
| GPU utilization 高,但 KV cache usage 不高,waiting 没明显增加 | 更像计算瓶颈 |
| waiting 很多,但 GPU utilization 不高 | 可能是调度、限流、上游请求或客户端消费问题 |
| 显存占用高,但 KV cache usage 不高 | 可能只是预分配或其他显存占用 |
preemption 可以理解成“抢占”:KV Cache 空间不够时,vLLM 临时暂停一部分请求,释放它们占用的 KV Cache,把显存让给其他请求。[4]
preemption 能让服务继续跑,但被暂停的请求后续可能需要重算一部分上下文。
表现出来通常是:
| 现象 | 可能感受 |
|---|---|
TTFT 变高 | 第一个 token 更久才出来 |
ITL 变差 | 已经开始输出,但 token 流得更慢 |
| 吞吐下降 | 同一时间处理的请求或 token 变少 |
| 延迟抖动 | 有些请求突然很慢 |
所以,判断 KV Cache 是否真的有压力,不是看单个指标,而是看组合信号。
确认 KV Cache 有压力以后,再进入调整。
不要把下面这些参数当成孤立开关。
先按现象选方向:
| 现象 | 优先动作 | 对应小节 |
|---|---|---|
| 频繁 preemption,KV cache usage 接近打满 | 增加 KV Cache 可用空间 | 3.1 |
| 高峰期抖动,请求同时涌入 | 控制并发和 batch token | 3.2 |
| TTFT 高,且 prompt 前缀大量重复 | 开启 Prefix Caching | 3.3 |
| 显存仍然紧,但想保留长上下文或并发 | 评估 FP8 KV Cache | 3.4 |
| prompt 里有大量旧日志、重复文档、无关历史 | 做上下文治理 | 3.5 |
如果日志里频繁出现 preemption,最直接的方向是给 KV Cache 更多空间。
vLLM 里有两个常见旋钮:[3][4]
| 参数 | 适合什么时候用 | 注意点 |
|---|---|---|
--gpu-memory-utilization | 想提高 vLLM 整体可用显存比例 | 太高可能挤压运行时余量 |
--kv-cache-memory-bytes | 想直接指定 KV Cache 显存预算 | 设置后会覆盖 gpu_memory_utilization 推断出的 KV Cache 大小 |
最常见的方式是提高 gpu_memory_utilization,比如从 0.92 提到 0.95:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --gpu-memory-utilization 0.95
如果你想更直接地控制 KV Cache 预算,可以用:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --kv-cache-memory-bytes 20G
这两个参数不要当成两个同时生效的旋钮。
一个是整体显存比例,一个是更直接的 KV Cache 显存预算。
KV Cache 不够,不一定只能加显存。如果问题来自“同一瞬间进来的请求太多”,另一条路是减少同一轮调度的压力。
常见参数是:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --max-num-seqs 64 --max-num-batched-tokens 8192
可以粗略理解为:
| 参数 | 控制什么 | 调低后的效果 | 代价 |
|---|---|---|---|
max_num_seqs | 一次最多同时调度多少条序列 | 减少同时占用 KV Cache 的请求数 | 并发能力可能下降 |
max_num_batched_tokens | 一次调度最多处理多少 token | 降低单轮调度的 token 压力 | 吞吐可能下降 |
这是一组典型取舍:
想要更高吞吐:通常希望 batch 更充分想要更低延迟:不能让请求排队太久显存紧张:要限制并发序列和批内 token
所以不要只问“这个参数应该设多少”,应该先问:
我的目标是吞吐优先,还是延迟优先?我的典型 prompt 多长?我的典型输出多长?峰值并发是多少?
参数是被 workload 推出来的。
如果慢主要发生在 prefill,且很多请求前缀高度相同,可以考虑 prefix caching。
常见场景是:
相同 system prompt相同工具 schema相同安全策略相同长文档不同用户问题
这时可以启用 Automatic Prefix Caching:[5]
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --enable-prefix-caching
它的作用是复用相同前缀对应的 KV Cache block,主要优化 prefill。
那它会不会增加显存?
更准确的说法是:
它会占用 KV Cache 空间来保留可复用的前缀 block,但这些 block 可以被后续请求共享,避免重复 prefill。
所以它不是“免费不占显存”,也不是简单“显存翻倍”。
如果前缀命中率高,它通常很划算:多保留一些可复用 block,换来更低的 TTFT 和更少的重复计算。
如果前缀几乎不重复,它的收益就有限,还可能占用一部分 cache 空间。
适合这些场景:
长文档多轮问答固定 system prompt 的聊天服务相同工具 schema 的 Agent 请求批量处理同一背景材料、不同问题的任务
注意,前缀必须真的相同才容易命中。如果你每次都把时间戳、随机 request id、动态用户状态放在最前面,prefix cache 命中率会变差。
更好的组织方式是:
稳定内容放前面:system prompt、工具说明、固定文档动态内容放后面:用户问题、临时变量、当前轮状态
如果显存还是紧,但你又想支持更长上下文或更高并发,可以考虑 KV Cache 量化。[6]
比如:
vllm serve Qwen/Qwen3.5-9B --port 8000 --max-model-len 262144 --reasoning-parser qwen3 --language-model-only --kv-cache-dtype fp8
它的收益和代价可以放在一起看:
| 方面 | 影响 |
|---|---|
| 收益 | KV Cache 占用变小,同样显存能容纳更多 token |
| 收益 | 频繁 preemption 的概率可能下降 |
| 代价 | 可能有精度损失 |
| 代价 | 需要硬件和 vLLM 版本支持 |
| 代价 | 不同模型受影响程度不同,必须用自己的 eval 验证 |
FP8 KV Cache 不是“免费提速按钮”,它更像是:
用一点潜在质量风险,换更大的上下文容量和并发空间。
所以更稳的流程是:
先跑业务 eval比较 auto / fp8 的回答质量再压测 TTFT、ITL、吞吐和 preemption最后灰度上线
还有一种 KV Cache 优化,不发生在推理框架里,而发生在应用层。
因为 KV Cache 只会缓存已经进入上下文的 token。
如果 prompt 里塞了很多过时日志、重复工具结果、无关历史对话,推理框架只能老老实实为它们分配 KV Cache。
所以应用层也要做:
| 方法 | 作用 |
|---|---|
Context Compact | 把旧工具输出压缩成摘要 |
RAG | 只召回当前问题需要的文档片段 |
Memory | 把长期信息放到外部状态里,需要时再取 |
| Prompt 组织 | 稳定前缀放前面,动态内容放后面 |
这类优化不会改变 KV Cache 的单 token 成本,但会减少进入模型的 token 数。从公式上看,就是直接降低:
sequence_length
很多线上系统里,这比单纯调 gpu_memory_utilization 更有效。
把上面这些放到一起,如果你用 vLLM 部署模型,遇到长上下文慢、显存紧、吞吐上不去,可以按这个顺序排查:
1. 看日志:有没有频繁 preemption?2. 看 workload:慢在长 prompt,还是长输出?3. 长 prompt 重复多:打开 enable_prefix_caching4. KV Cache 不够:提高 gpu_memory_utilization5. 想精确控预算:设置 kv_cache_memory_bytes6. 显存仍然紧:降低 max_num_seqs / max_num_batched_tokens7. 还要更长上下文或更高并发:评估 kv_cache_dtype=fp88. 单卡放不下:考虑 tensor parallel / pipeline parallel9. 每次改完:压测 TTFT、ITL、吞吐、preemption 和质量
这里几个指标很重要:
TTFT:Time To First Token,首 token 延迟ITL:Inter-Token Latency,生成过程中 token 间延迟吞吐:单位时间处理多少 token 或 requestpreemption:KV Cache 不够导致的抢占和重算
不要只盯平均吞吐。
线上用户往往更敏感的是:
第一下多久出来?出来以后是不是稳定地往外流?长请求会不会把短请求拖死?高峰期会不会频繁抖动?
这些问题背后,经常都有 KV Cache 预算的影子。
最后再清一下几个容易混淆的概念。
| 概念 | 解决什么问题 | 存在哪里 | 和 KV Cache 的关系 |
|---|---|---|---|
| 上下文窗口 | 模型最多能看多少 token | 模型输入序列 | 窗口越大,可能进入 KV Cache 的 token 越多 |
| KV Cache | 已经进入上下文的 token 如何少重复计算 | 推理引擎的 GPU / CPU cache | 只缓存推理中间状态,不决定内容该不该进上下文 |
| Memory / RAG | 哪些长期信息应该放在模型外部,需要时再取 | 数据库、向量库、文件、外部状态 | 减少无关内容进入上下文,从源头降低 KV Cache 压力 |
| Context Compact | 当前上下文太长时如何压缩 | 当前 prompt / message 历史 | 减少低价值 token,降低后续 KV Cache 占用 |
这也是为什么真实系统不能只靠 KV Cache。
如果你把所有历史对话、工具结果、日志和文档都塞进 prompt,KV Cache 会帮你少算,但不会帮你判断:
这些内容该不该进上下文?这些内容是不是已经过时?这些内容是不是可以压缩?这些内容是不是应该放到外部存储?
所以长上下文系统通常要一起做:
Context Compact:压缩当前上下文Memory / RAG:管理长期信息KV Cache:优化推理复用推理调度:平衡显存、吞吐和延迟
最后把全文收成一条清晰的讲法。
第一,先定义它。
KV Cache 是大模型自回归生成时,用来缓存历史 token 的 Key / Value 的机制。
第二,讲清楚它为什么快。
模型生成文本是一个 token 一个 token 往外生成。如果每生成一个新 token 都重新计算整段上下文,decode 会非常慢。有了 KV Cache,prefill 阶段先处理 prompt 并建立缓存;decode 阶段复用历史 K/V,只追加新 token 的 K/V。
第三,讲清楚它为什么贵。
KV Cache 不是免费的。它本质上是用显存换推理速度。上下文越长、并发越高、输出越长,需要保存的 KV Cache 就越大。
第四,讲清楚怎么估算。
KV Cache ≈ 并发序列数 × 序列长度 × attention 结构 × 精度
估算时不能只看模型总层数。比如 Qwen3.5-9B 是混合 attention 架构,不是每一层都会产生标准 full attention 的 KV Cache,所以要看真正产生标准 KV Cache 的 attention 层数、KV heads 和 head_dim。
第五,讲清楚部署时怎么看。
落到 vLLM 这类推理框架里,KV Cache 实践主要看几个信号和参数:
preemption 多,往往说明 KV Cache 空间不够;TTFT 高,要看 prefill、长 prompt 和 prefix caching;ITL 差,要看 decode、batching、并发和显存压力;显存紧,可以考虑降低 max_model_len、控制 max_num_seqs / max_num_batched_tokens,或者评估 FP8 KV Cache。
再压缩一下,就是三句话:
KV Cache 解决重复计算。KV Cache 的代价是显存。KV Cache 优化,本质是在上下文长度、并发、延迟、吞吐和质量之间做预算。
[1] Qwen/Qwen3.5-9B - Hugging Face
[2] Qwen3.5 model documentation - Hugging Face Transformers
[3] Engine Arguments - vLLM Documentation
[4] Optimization and Tuning - vLLM Documentation
[5] Automatic Prefix Caching - vLLM Documentation
[6] Quantized KV Cache - vLLM Documentation
[7] Metrics - vLLM Documentation