01 · 没有缓存,每生成一个 token 都会发生什么
先假设模型已经加载,输入是四个 token:[A,B,C,D]。模型处理这四个位置,利用 D 位置的最终输出预测下一个 token E。为了再预测 F,一个最直接的实现会把 [A,B,C,D,E] 全部重新输入模型;再预测 G 时,又处理 [A,B,C,D,E,F]。
这里重复的不只是读了一遍 token id。每个位置的向量都要经过多层 Attention、线性变换和逐位置的其他运算。历史位置的中间结果一次次重新生成,最后却只用最后一个位置的输出预测下一步。
| 要预测谁 | 不使用缓存:本轮处理位置 | 使用缓存:本轮新增位置 |
|---|---|---|
| E | A B C D,共4个 | A B C D,共4个,建立缓存 |
| F | A B C D E,共5个 | E,共1个,读取历史缓存 |
| G | A B C D E F,共6个 | F,共1个,读取历史缓存 |
02 · 为什么历史结果可以复用
标准因果 Attention 有一条约束:某个位置只能看自己和之前的位置,不能看未来。因此,当 E 被追加到末尾,A 到 D 在同一模型、同一位置和同一执行配置下的历史表示不需要随未来输入改变。每一层都可以保留这些位置已经算出的 K 和 V。
Q 是当前查询,K 用来与查询算相似度,V 是按注意力权重汇总的内容。下一轮的新位置需要用自己的 Q 去读取历史 K/V,通常不需要历史位置的 Q。因此常规生成缓存保存 K/V,而不保存整段历史 Q。
“每一层”很关键:第1层的 K/V 来自第1层输入,第2层的 K/V 来自经过第1层后的表示,不能拿一份缓存用于所有层。
03 · Prefill 建缓存,Decode 读缓存并追加
- Prefill:整段提示词经过所有模型层;每层把提示词各位置的 K/V 写入本层缓存。最后一个位置的输出产生 logits,采样出首个新 token。
- Decode:把刚采样出的 token 输入模型,使用正确的绝对位置。每层为这个新位置计算 Q/K/V,Attention 读取历史 K/V,也包括当前位置的 K/V。
- 提交新状态:各层缓存长度增加1。最终输出再产生下一个 token;新采样 token 尚未经过前向。
cache = empty_cache_for_each_layer()
logits, cache = forward(prompt_ids, positions=0..N-1, cache=cache)
next_id = sample(logits[-1])
while need_more_tokens:
# next_id 是已经采样、但还没有算过自身 K/V 的输入
logits, cache = forward([next_id], positions=[N], cache=cache)
N += 1
next_id = sample(logits[-1])真实引擎可能按页写入、预分配槽位或采用不同函数签名。上面的循环只表达时间顺序;位置、注意力掩码、有效长度都必须与缓存中已有内容对应。
04 · 省了哪些计算,还有哪些计算
对第1节的例子,不缓存时三轮共处理 4+5+6=15 个位置;缓存时处理 4+1+1=6 个新增位置。这可以说明逐位置投影和其他历史位置计算的重复被消除,不能据此宣称整个推理提速 15/6=2.5 倍。
| 操作 | 有缓存的 Decode 是否还做 | 原因 |
|---|---|---|
| 当前 token 的投影、归一化、MLP | 仍然做 | 当前位置要经过全部模型层。 |
| 历史位置的 K/V 投影 | 通常不重做 | 读取本层已经计算好的缓存。 |
| 当前 Q 与历史 K 的打分、Softmax、加权 V | 仍然做 | 生成仍依赖历史内容。 |
| 历史位置查询的 Attention 输出 | 通常不重做 | 只需当前查询的输出。 |
| 采样与服务调度 | 仍然做 | KV 缓存没有替代输出选择和请求执行。 |
普通稠密 Attention 的单个 Decode 查询仍要访问长度为 N 的历史 K/V。查询分数形状通常是 1×N;工作量和读缓存的流量仍随上下文长度增加。所以“用了 KV Cache”不代表每个新 token 的成本恒定,也不代表模型一次前向只做 Attention。
05 · 省下重算,付出多少缓存空间
可把缓存想成每层各有两本随上下文增长的笔记。只估计普通 K/V 元素,单请求容量为:2 × 层数 L × KV 头数 Hkv × 头维度 d × 已缓存位置 N × 每元素字节数 b。批次内长度不同的请求要分别求和;分页、对齐、元数据与空闲池另计。
L=32, Hkv=8, d=128, N=4096, b=2 字节
KV = 2 × 32 × 8 × 128 × 4096 × 2
= 536,870,912 字节 = 512 MiB
8个同样长度的请求:仅有效 K/V 元素约 4 GiB假如查询有32个头,KV 只有8个头,这是 GQA 的一种配置;容量公式使用8,不能把查询头数直接代入。若同层数与维度改为32个 KV 头,单请求容量变为2 GiB。MLA、混合层、滑动窗口等缓存结构需要自己的容量公式。
这也解释了三个关联方法:分页 KV 改善存放与分配;KV 量化减少每元素字节数;前缀缓存让兼容请求共享已算内容。它们可以与历史 KV 复用叠加,但分别有新的条件与代价。
06 · 从教学模型验证等价性
下方 CPU 例子实现一个两层、单头的因果 Attention 教学网络,用固定位置向量、线性投影和逐位置变换模拟缓存依赖。它分别运行完整前向、逐 token 缓存前向以及分块前向,检查各位置输出是否一致。它没有完整 LLM 的归一化、RoPE、训练或高性能内核。
实际接入应先检查框架或引擎已有缓存接口,确认 use_cache、各层缓存布局、attention mask 和位置参数。支持某个模型的缓存结构,比设置一个布尔开关更重要。
07 · 什么时候不能直接复用
- 模型权重或适配器改变:原 K/V 属于原计算配置。
- 同一 token 的先前上下文或位置改变:它的隐藏表示可能不同。
- 非因果全序列 Attention:未来输入可能改变历史表示。
- 训练更新参数:跨优化器步骤沿用生成缓存不能替代正确的训练前向与梯度图。
- 滑动窗口截断、KV 量化或压缩:是否改变数学语义或数值误差,需要单独评估。
检查输出等价时,先固定 token 输入,在相同位置比较 logits 或隐藏向量并设置合理误差。随后才测试完整生成。采样和浮点舍入可能放大小误差,不能只凭两段文字是否一样判断缓存正确。
08 · NVIDIA 与昇腾:同一逻辑,不同实现
两类设备都能执行“缓存历史 K/V,计算当前位置”的生成逻辑。差异主要要在引擎里核对:缓存布局与精度、分页支持、实际 Attention 后端、搬运路径、并行分片以及支持的模型。不能仅凭 GPU 或 NPU 名称判断缓存收益。
可从 Transformers 的语义说明和 TensorRT-LLM 的缓存实现文档建立检查清单,再对照选定昇腾推理引擎的具体版本。本文没有把 NVIDIA 的接口当作昇腾现成可用的接口。
09 · 检查自己是否真的理解
- Prefill 后刚采样出 E,缓存里有 E 的 K/V 吗?
- 为什么新 Q 仍要读取历史 K/V?
- 生成三个新 token,为什么不是三次 Decode 前向再加一次 Prefill?
- 32个查询头、8个 KV 头时,容量公式应该用哪个数?
流程位置与关联方法
这个方法可能出现在多个位置;计算线包含模型计算与算子实现,执行线包含运行时、内存和通信。
资料与核查范围
原理依据以原始论文和官方文档为主;算例与时间线是教学推导。文档核查日期:2026-10-08。引用 main / latest 文档时,实际接口以安装版本为准。
代码为接入片段或教学伪代码;本页没有声称完成 NVIDIA / 昇腾硬件实测。