大模型为什么能复用「读过的内容」:从 KV Cache 到 vLLM 前缀缓存
一句话结论:同一模型处理完全相同的提示词前缀时,可以复用之前算出的注意力 Key/Value,而不用再次计算那段输入。vLLM 的自动前缀缓存将这些结果按块保存并识别可共享的前缀;它主要节省输入阶段的计算,不会神奇地加速不同回答的逐字生成。
假设公司的知识库有一本很长的产品手册。十位同事分别问「如何退款」「如何开发票」,每次请求都附带同一本手册。没有缓存时,模型要把手册重新读十遍;有缓存时,第一次读完形成可复用的中间结果,后续请求只需处理不同的问题。这比笼统地说「AI 记住了文档」更准确:缓存的是特定模型对相同 token 前缀计算出的中间张量,不是跨模型通用的知识,也不是永久记忆。
这篇文章基于 vLLM 的前缀缓存设计文档、功能说明与最初的 PagedAttention 论文梳理机制。资料核验日期:2026 年 9 月 27 日。具体参数和默认行为会随 vLLM 版本变化,部署时应以所用版本文档和启动日志为准。
先分清两种「缓存」
Transformer 生成下一个 token 时,需要关注此前的 token。对每个旧 token,模型在每层注意力中产生 Key(K)与 Value(V)向量。如果每生成一个新 token 都重新计算全部旧 token 的 K/V,会做大量重复工作。KV Cache 把本次请求已经算好的 K/V 留在显存中,供随后生成步骤读取。它解决的是一条请求内部的重复计算。
自动前缀缓存(Automatic Prefix Caching,APC)则进一步问:另一条请求开头的 token 与之前请求相同,能否复用那一段 KV Cache?如果能,新请求可跳过这段共享前缀的预处理计算。注意两者不是同义词:每次生成通常都要使用 KV Cache,跨请求复用则需要前缀匹配、可用的缓存块,以及相应的服务配置。
可以把它想成两层便签:第一层是你读一本书时给自己做的笔记,避免翻到下一页时从第一页重读;第二层是同一版书的下一位读者借用前面那些相同章节的笔记。第二层只能借用从开头连续相同的章节,不能因为两本书中间都出现「退款」一词就复用不相关的笔记。
为什么先要把 KV Cache 切成块
用户的输入长度、回答长度事先都不固定。如果为每条请求按最大可能长度预留一整段连续显存,短回答会留下大量空位;并发请求增加时,还会遇到连续空间不够、却存在零散空闲空间的问题。
PagedAttention 论文借鉴操作系统分页:把一条序列的 KV Cache 切成固定容量的逻辑块,用块表映射到显存中的物理块。逻辑上连续的 token,其物理块不必挨在一起。新 token 到来时按需分配块;一个请求完成后,块可以回收。论文原始实验报告了吞吐提升,但这些数字针对当时的模型、基线和工作负载,不能直接当作当前部署的性能承诺。
例如每块可容纳 4 个 token,一段 10-token 的输入占 3 块:前两块各放 4 个,最后一块放 2 个。无需提前为「未来可能输出 1000 个 token」占满显存。最后一块会有未填满的空间,这是按块管理的代价;块太大浪费更多,块太小又增加管理与访问开销。
分页解决的是如何摆放与共享缓存,前缀缓存解决的是何时复用已算出的缓存。两者配合,才能把一段共享提示词指向已有的物理 KV 块,而不是为每条请求复制一份。
一次前缀命中具体发生了什么
把两个请求写成 token 序列:
请求 A:系统规则 | 产品手册 | 问题:如何退款?
请求 B:系统规则 | 产品手册 | 问题:如何开发票?
└── 连续相同的前缀 ──┘2
3
这里的竖线只是阅读上的分隔符,真实系统按 tokenizer 的 token ID 判断。即使肉眼看起来几乎一样,模板、空格、工具定义、图片内容或文档版本改变,都可能让 token 序列提前分叉。把长期不变的系统规则和文档放在前面、变化的问题放在后面,通常更有利于命中。
vLLM 的设计不是简单地只给每块内容单独求哈希。一个块的身份包含此前前缀的哈希、该块的 token,以及额外信息。这样,内容相同但前文不同的两个块不会被误当成同一个上下文;第二块的命中依赖第一块,第三块又依赖第二块。系统从左往右查找最长的连续命中前缀,遇到不匹配便对剩余输入重新计算。当前设计文档描述的基础复用单位是填满的 KV 块;尾部不足一块的 token 不应想当然地视为完整可共享块。具体模型或新版本可能另有细粒度机制。
再看一个小例子,假定每块 4 个 token:
| 请求 | 第 1 块 | 第 2 块 | 第 3 块 |
|---|---|---|---|
| A | 规则 1—4 | 手册 1—4 | 「退款」问题开头 |
| B | 规则 1—4 | 手册 1—4 | 「发票」问题开头 |
如果前两块在缓存里且未被淘汰,B 可以复用它们,再计算第三块及之后的内容。如果第一块中一个 token 发生变化,即使第二块表面文字相同,其前缀哈希也不同。这是正确性要求:同一段文字在不同上下文中,其注意力结果可能不同。
什么工作负载真正受益
长而稳定的公共前缀、短而变化的后缀是典型场景:同一份手册的多次问答、相同系统规则下的批量任务、保留完整历史的多轮对话。vLLM 官方说明指出,APC 省的是处理输入的 prefill 阶段;输出 token 的 decode 阶段仍需逐步生成。因此,输入很长、回答较短时,首 token 等待时间更可能改善;回答特别长且解码占主要时间时,整体收益可能有限。
反过来,以下情况容易没有收益:每个请求的开头都不同;把时间戳、用户 ID 或随机数塞进系统提示词最前面;共享部分还没形成可缓存的块;缓存因显存压力被淘汰;请求落到不共享缓存的不同实例。修改提示词模板也可能让此前积累的缓存失效。
一个常见误解是「命中率 80%,整次请求就快 80%」。命中率描述复用了多少输入块,端到端时延还包括网络、排队、新输入的计算和输出生成。要用相同模型、相同硬件和相似并发负载,分别观察首 token 时延、输出速度、吞吐、缓存命中与显存占用,才知道自己的业务是否受益。
多租户系统还要考虑隔离
共享缓存可能带来侧信道:如果某个请求异常快,攻击者可能推测系统以前是否处理过某个前缀。vLLM 的安全文档说明,服务可以给请求设置 cache_salt,将盐纳入首块哈希,让不同盐值的请求无法跨组复用前缀块。在多租户部署中,可以按信任边界分配盐值;同一组可以分享,跨组隔离。盐值需要稳定且只在允许共享的请求间一致,不能把所有租户都放进同一共享组。
这仍不是完整的数据安全方案。权限校验、日志脱敏、数据保留策略和实例隔离要另行设计;缓存隔离只解决其中一条复用途径。也不要把含机密的提示词、令牌或个人数据写到公开示例和仓库里。
一个可复现的概念实验
下面的 Python 脚本只模拟前缀块匹配,不运行模型,也不测 GPU 性能。它展示为什么块哈希必须包含之前的前缀,以及为什么中间一处变化会打断后续复用。保存为 prefix_demo.py,用 Python 3 运行即可,无第三方依赖。
import hashlib
def block_hashes(tokens, block_size=4):
parent = b""
result = []
for start in range(0, len(tokens) - block_size + 1, block_size):
block = tokens[start:start + block_size]
payload = parent + b"|" + "\x1f".join(block).encode("utf-8")
parent = hashlib.sha256(payload).digest()
result.append(parent)
return result
base = ["系统", "规则", "请", "回答", "手册", "第", "一", "章", "退款", "怎么", "办理", "?"]
same_prefix = base[:8] + ["发票", "怎么", "开具", "?"]
changed_early = ["另一", "规则"] + base[2:8] + ["发票", "怎么", "开具", "?"]
reference = block_hashes(base)
for name, request in [("相同前缀", same_prefix), ("开头变化", changed_early)]:
candidates = block_hashes(request)
matched = 0
for left, right in zip(reference, candidates):
if left != right:
break
matched += 1
print(f"{name}: 连续命中 {matched} 块")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
预期输出是「相同前缀」命中 2 块,「开头变化」命中 0 块。示例把字符串当成 token,真实 tokenizer、哈希序列化、块大小、模型状态及淘汰策略更复杂;它仅用于验证从左到右连续匹配的思路,不能据此推算实际缓存命中率。
落地时怎么做
先记录请求模板,把稳定的系统指令、工具描述和公共文档集中放在前面,让用户特有内容靠后;确认这种重排不改变业务语义。再查所用 vLLM 版本的功能说明和启动参数,确认前缀缓存是否启用、模型是否支持,以及缓存的分组与显存策略。多租户服务先确定 cache_salt 的边界,再压测。
压测至少准备两组请求:一组共享长前缀、只变化末尾问题;另一组每次使用不同前缀。保持模型、输出长度分布与并发度尽量相同,分别观察冷缓存和热缓存。若热缓存组的首 token 时延下降而长回答的生成速度变化不大,这是符合机制的;若没有差异,先排查 token 前缀、路由实例、块粒度和淘汰,而不是立即认定缓存无效。
KV Cache 是生成时避免自己重读,PagedAttention 是把笔记分块、按需摆放,自动前缀缓存是让后来的相同开头请求借用已经写好的笔记。理解这三层后,就能判断一个优化究竟减少了计算、减少了显存浪费,还是只改善了特定负载的吞吐。
原始资料
- vLLM:Automatic Prefix Caching 设计
- vLLM:Automatic Prefix Caching 功能说明
- vLLM:Security / Cache Salting
- Kwon 等:Efficient Memory Management for Large Language Model Serving with PagedAttention,2023 年论文;文中的性能数据限于其原始实验。
资料核验:2026 年 9 月 27 日。本文中的书本与笔记类比、块大小为 4 的例子和 Python 脚本均为教学示意,不代表 vLLM 的完整实现。
💬 评论