主题
自回归生成与 KV 缓存
一句话:生成是一个循环——用已有 token 预测下一个,采样后追加,再预测。KV 缓存把历史 token 的 Key/Value 存下来,避免每步重算,把总计算从
O(n³)降到O(n²);代价是显存随上下文和并发线性增长。
为什么需要
训练时整段序列一次送入,所有位置并行预测;推理时没有“后面的真实 token”,只能生成一个、追加一个、再生成下一个。这个循环叫自回归生成。
如果每一步都把前面所有 token 重新过一遍模型,第 t 步要算 t 个位置的注意力,总共 O(n³) 量级。序列一长,延迟会爆炸。KV 缓存解决的就是这个重复计算:已经算过的 Key 和 Value 不再重算,只算新 token 的 Q/K/V,然后把新的 K/V 追加进缓存。
核心机制
一、生成循环
text
输入 prompt → 前向 → 最后一个位置的 logits
→ 采样一个 token → 追加到序列
→ 再前向(只算新 token)→ 采样 → … → EOS 或 max_tokens每一步只产生一个 token,因为下一个位置的输入依赖当前采样结果。即使模型内部高度并行,输出侧本质上是串行的;这就是“首字延迟”和“出字速度”要分开看的原因。
二、KV 缓存存什么
每一层、每个注意力头,对每个历史位置都要保存两份向量:Key 和 Value。新 token 到来时:
- 只计算这个 token 的
q、k、v; - 把
k、v追加到该层该头的缓存; - 用
q和缓存里的所有K算分数,softmax 后对缓存里的所有V加权。
缓存大小可以精确估算:
text
每 token KV = 2 × layers × kv_heads × head_dim × dtype_bytes
总 KV = 每 token KV × 序列长度 × batch其中 2 是 K 和 V 两份,kv_heads 是 Key/Value 的头数(GQA 下小于 Query 头数),dtype_bytes 是精度字节数(FP16/BF16 为 2)。
以 Llama 3 8B 为例:32 层、8 个 KV 头、head_dim 128、FP16,每 token 是 2×32×8×128×2 = 131,072 字节,正好 128 KiB。8K 上下文约 1 GiB;batch 32 就是 32 GiB,还没算权重和激活。并发上限不是 GPU 算力决定的,而是 KV 显存决定的。
三、Prefill 与 Decode
推理分两段,资源画像完全不同:
| 阶段 | 做什么 | 瓶颈 | 指标 |
|---|---|---|---|
| Prefill | 并行处理整段 prompt,写入 KV | 算力 | TTFT(首 token 延迟) |
| Decode | 每步生成一个 token,读权重和 KV | 显存带宽 | TPOT(每 token 时间) |
Prefill 的矩阵乘很大,GPU 利用率高;Decode 每步只算一个 token,计算量小,但要把模型权重和全部 KV 从显存读一遍,受带宽限制。这就是为什么“加 batch”能提升 decode 吞吐——多个请求共享一次权重读取;也是为什么“batch 太大”会让尾延迟变差——每个请求要等更久。
四、采样:从 logits 到 token
模型输出的是词表上的 logits,需要变成概率再选一个 token:
- 贪婪:直接取最大 logit,确定性强,但容易重复、缺少多样性;
- 温度:
p_i ∝ exp(z_i / T)。T < 1更尖锐、更保守,T > 1更平坦、更随机;T = 0通常实现为直接取 argmax。 - top-k:只在概率最高的 k 个 token 里采样,砍掉长尾;
- top-p(nucleus):按概率从高到低累加,保留累计到 p 的最小集合,再在里面采样。它比固定 top-k 更适应不同分布的步。
- 重复惩罚:降低已出现 token 的 logit,缓解循环。
Agent 场景还常用约束解码:用 JSON Schema、正则或语法,把不合法的 token 在采样前屏蔽掉,保证输出能解析。它提高结构正确率,但不保证内容正确,也不保证语义上完成了任务。
五、缓存管理与并发
KV 缓存不是“存下来就完事”,还要解决分配、共享和回收:
- 分页:按固定大小的块分配 KV,避免按最大长度预留造成浪费;
- 前缀缓存:相同 system prompt 或相同文档前缀复用同一组 KV 块;
- 抢占:显存不够时换出或重算某些请求;
- GQA/MQA:减少 KV 头数,直接降低每 token KV 大小。
这些机制的细节在 显存与 KV 缓存 里展开;这里只需要记住一条:KV 缓存把“上下文长度”变成了“显存成本”,长上下文不是免费的。
图中 prefill 只发生一次,decode 循环发生 输出长度 次;每次循环只新增一行 Q/K/V,但要和全部历史 KV 做注意力。缓存让“重复计算”消失,却让“显存占用”随序列长度和 batch 线性增长。
最小上手
用纯 Python 估算 KV 缓存,验证 GQA 对并发的影响:
python
def kv_bytes(layers, kv_heads, head_dim, seq, batch=1, dtype=2):
per_token = 2 * layers * kv_heads * head_dim * dtype
return per_token, per_token * seq * batch
for name, (layers, kv_heads, head_dim) in {
"Llama-3-8B (GQA 8)": (32, 8, 128),
"same shape, MHA 32": (32, 32, 128),
}.items():
per_token, total = kv_bytes(layers, kv_heads, head_dim, seq=8192)
print(f"{name}: {per_token/1024:.0f} KiB/token, 8K={total/2**30:.1f} GiB")
per_token, total = kv_bytes(32, 8, 128, seq=8192, batch=32)
print(f"batch32: {total/2**30:.0f} GiB")输出:
text
Llama-3-8B (GQA 8): 128 KiB/token, 8K=1.0 GiB
same shape, MHA 32: 512 KiB/token, 8K=4.0 GiB
batch32: 32 GiB同样 32 层、同样 head_dim,KV 头从 8 增到 32,每 token KV 变成 4 倍;batch 32 时 8K 上下文就要 32 GiB,单张 80 GB 卡还要放权重和激活,真正能跑的并发远小于直觉。
生产动作
- 先算 KV,再定并发。用上面的公式估每 token KV,乘以平均上下文和 batch,剩余显存才是安全区;不要照抄引擎默认的
max_num_seqs。 - 给输出留 token。
max_tokens要从上下文窗口里扣掉;不预留会在 JSON、工具参数或代码中间截断。 - 采样参数按任务设。抽取、分类、代码、工具调用用低温度(0–0.2)和紧的 top-p;创意写作再放开。温度 0 也不是严格确定,见“边界”。
- 用前缀缓存省 prefill。固定 system prompt、固定文档、多轮对话都适合;缓存键要包含模型、tokenizer、前缀内容,否则会串味。
- 监控两段延迟。TTFT 高查排队和 prefill,TPOT 高查 decode 带宽和 batch;只看总延迟无法定位。
- 结构化输出用约束解码。让模型在语法允许的 token 里采样,比“求它输出 JSON”可靠;但仍要校验字段和业务含义。
边界
- KV 缓存不减少单 token 的注意力计算。它省的是重复计算,序列越长,每个新 token 仍要和全部历史做注意力。
- 显存随长度和并发线性增长。128K 上下文、batch 32 的 KV 可能是几十到上百 GiB,规划时要按峰值算。
- Decode 是带宽受限的。加算力不一定变快,提升 batch 或减少权重读取更有效;但 batch 太大会推高尾延迟。
- 温度 0 不等于可复现。GPU 算子、批处理顺序、框架版本都可能带来数值差异;需要严格复现时,要固定环境并做结果校验。
- 前缀缓存只认完全相同的 token。语义相同但措辞不同不会命中;缓存键漏了租户或权限域还可能造成越权。
- 采样不能修复知识缺失。模型不知道的事实,调温度、加 top-p 也变不出来;该检索就检索,该给上下文就给上下文。
讲清楚
推理是自回归循环:prefill 并行处理 prompt 并写入 KV,之后每步只算一个新 token 的 Q/K/V,和历史 KV 做注意力,采样后追加,直到 EOS 或上限。KV 缓存把重复计算从
O(n³)降到O(n²),代价是每 token 的 KV 显存随层数、KV 头数、head_dim 和精度线性增长;GQA 通过减少 KV 头数直接放大并发。工程上要分开看 TTFT 和 TPOT:前者受排队和 prefill 影响,后者受 decode 带宽和 batch 影响。采样参数、约束解码、前缀缓存和 KV 容量,是 Agent 上线前必须一起定的四件事。
适用:Decoder-only 自回归模型;KV 大小与采样默认值以模型和引擎版本为准 | 初稿 2026-10