你走进一家刚开业的手作奶茶店,老板拍着胸脯说:“我们保证每一杯都从煮茶叶开始。”你点了杯珍珠乌龙拿铁,等了 15 分钟,茶是香的,但你把这家店拉进了黑名单。第二天老板学乖了:他提前煮好一大锅乌龙茶底,客人点单只需倒茶底、加珍珠、倒牛奶,三秒出杯。同样一杯茶,速度差五倍,生意差十倍。大模型推理服务也面临同样的选择题:每个用户请求都从头把整段文字“理解”一遍,还是把大家都会用到的“公共部分”提前备好、直接复用?Prefix Cache(前缀缓存)选择后者——它把系统提示词、历史对话这类被上万个请求共享的“茶底”的推理中间结果缓存起来,新请求来了直接调用,省下最贵的重复计算。
了解 Prefix Cache 是切入大模型推理优化最实用的一课。看懂它,你就读懂了 vLLM、SGLang 等主流框架的核心加速逻辑,能解决八成线上“首字慢、吞吐低”的痛点。
目录
可视化演示
展示哈希表使用线性探测解决冲突的过程,逐步演示哈希计算、冲突检测与探测序列。
本节配有交互动效(进入视口后自动加载,约 3KB)。
哈希表插入操作 计算哈希值h=key%11,若槽位为空则直接插入 h(k)=k mod 11 冲突时线性探测下一个槽位
1. 背景与痛点:每杯奶茶都从煮茶开始
第一家奶茶店的老板坚持“每杯从煮茶开始”,他说这是对味道的尊重。可他没算过账:上午十点开门,第一位客人点珍珠乌龙拿铁,他烧水、称茶叶、煮茶底花了 12 分钟;第二位客人点一模一样的茶,他又花了 12 分钟。一个上午过去,门口排队的人已经走光了。大模型服务如果不做优化,就是这个翻版:每个请求都从零开始把整段输入文本“煮”一遍。
大模型生成回答,内部要经历两个阶段。第一个阶段叫 Prefill(预填充):模型拿到完整输入 prompt,并行地给每个 token 计算出注意力所需的 K/V 向量,一次性得到整段输入的中间表示。第二个阶段叫 Decode(解码):模型以自回归方式一个一个“蹦”出回答 token,每蹦一个都要做一次完整前向传播。两个阶段相加,才是完整的响应时间。
问题在于,线上请求往往共享大量重复内容——同一个 system prompt、同一段任务说明、同一条历史对话。这些内容在每个请求的 Prefill 阶段都被原样重算。假设输入有 1000 个 token,其中 900 个是公共前缀,那么 90% 的 Prefill 计算都在做无用功。上下文越长,浪费越离谱。
所以奶茶店的问题不是“茶不好喝”,而是“重复烧水”。大模型推理的聪明做法,就是第二天那样提前备好茶底。
- $T_{\text{total}}$生成完整回复的总时间。在奶茶店场景里,相当于从客人点单到递上奶茶的全程耗时。
- $T_{\text{prefill}}$预填充耗时。模型把整个输入 prompt 并行处理一遍,算出每个 token 的 K/V 向量。相当于“从头煮一锅茶底”的时间。
- $T_{\text{decode}}$解码耗时。模型逐个 token 自回归生成回复,每生成一个都需要一次前向传播。相当于“加小料、封杯”的固定动作,没法并行跳过。
- 加法含义总时间由两阶段相加。只要能从流程里砍掉 Prefill,总延迟就会大幅缩短——这就是 Prefix Cache 的切入点。
2. 核心原理:预煮一锅“万用茶底”
第二天老板盘点了订单,发现九成客人的配方都落在“乌龙茶底 + 鲜奶”上。他连夜煮了一大锅乌龙茶底,客人点单时直接倒茶底,再按需加珍珠、布丁或椰果。煮茶的时间从“每杯 12 分钟”变成“每天 12 分钟”。Prefix Cache 做的事完全一样:把请求中最常出现的那段 token 序列——系统提示词、few-shot 示例、多轮对话历史——当作“公共前缀”,第一次完整计算后把它们的 K/V 向量存进显存。之后任何请求只要前缀对得上,就直接读缓存。
运作流程分三步。第一步,前缀匹配:把新请求的 token 序列与缓存索引做最长公共前缀匹配;第二步,复制引用:命中部分用指针引用缓存块,完全不进 GPU 计算管线;第三步,增量计算:只对未命中的后缀 token 做 Prefill,然后进入 Decode。这样一来,Prefill 的成本从“整段 prompt”坍缩成“独有后缀”,这正是加速的主要来源。
预煮茶底第一次投入 12 分钟,后面所有点这杯茶的客人都跟着受益——Prefix Cache 的加速比,就看“茶底”在你的订单里占多大比例。
- $\text{Speedup}$加速比,表示命中缓存后整体耗时缩短为原来的几分之一。
- 分子 $T_{\text{prefill}} + T_{\text{decode}}$不缓存时的原始总耗时,相当于坚持每次都从煮茶叶开始。
- 分母 $T_{\text{decode}}$命中后的耗时,只剩逐 token 解码部分,茶底已备好。
- 比值含义若 Prefill 占 80%,加速比就是 5 倍。前缀越长,省得越多。
3. 命中条件:茶底对不上号就前功尽弃
茶底备好了,但它不是万能的。客人点“茉莉绿茶底 + 椰果”,接不上这锅乌龙茶;客人说“不要鲜奶要燕麦奶”,虽然还是乌龙茶底,但配方变了,整杯要按新配方重做。更隐蔽的是:如果老板偷偷把“鲜奶”换成了“奶精”,昨天煮的茶底和今天煮的茶底风味不再统一,老主顾喝一口就能察觉——对不上号的茶底,只能倒掉重煮。
Prefix Cache 的命中条件比人想的更苛刻。首先,token 序列必须从位置 0 开始逐位完全一致,任何 token 不同都会在匹配处断裂——“看起来一样”的文本经 tokenizer 切分后可能完全不同,一个空格、一个换行、一个 emoji 都会改变 token 序列。其次,位置编码必须对齐:旋转位置编码(RoPE)与 token 的绝对位置绑定,同一句“你好”出现在句子开头和句子中段,算出的向量不同,缓存不能跨位置复用。最后,模型配置必须一致,换了模型或改了位置编码方式,旧缓存全部作废。
预煮茶底能不能用,取决于配方是否对得上号。Prefix Cache 工程师每天都在做同一件事:判断两杯茶是不是“同一个茶底”。
- $L_{\text{LCP}}$最长公共前缀长度,即缓存可复用的 token 数量。相当于“这杯茶能和预煮茶底对上号”的配方段数。
- $S_{\text{req}}[1..k]$新请求 token 序列中,从第 1 个到第 k 个 token 组成的子序列。
- $S_{\text{cache}}[1..k]$缓存条目中对应的前 k 个 token 子序列。
- 等号与 $\max$要求两个子序列逐 token 相等,并取满足条件的最大 k。任何一个 token 不同,k 都会在此处截断。
4. 实现方式:备料区的智能货架
备料区要做的事比煮茶多。地方就那么大,几十种小料堆在一起:常用的珍珠、芋圆要摆在手边,冷门的桂花冻收到角落,放了三天的布丁要定时清掉。谁来记录每种料的位置?客人点单时怎么快速找到?这就是备料区的“货架设计”。
Prefix Cache 的实现也围绕四件事。第一,分块存储:KV 不按整段前缀存,而是切成固定大小的 block(如 16 或 64 个 token 一块),方便增量复用与内存对齐;第二,哈希索引:每个 block 的内容哈希成 key 放进哈希表,查找前缀时通过哈希快速定位;第三,显存管理:借鉴 PagedAttention 的分页思想,缓存块像操作系统内存页一样按需分配、按引用计数释放;第四,淘汰策略:显存不是无限的,用 LRU(最久未用先淘汰)或 LFU(最少使用先淘汰)保证热点前缀留下,冷门前缀腾地方。
谁的货架设计得好,谁就能在同样大的备料区里服务更多客人。Prefix Cache 的工程质量,直接决定了理论加速能兑现几成。
- $2$Key 和 Value 各一份,每个位置都要保留两套向量。
- $L$序列长度,这里是缓存的公共前缀 token 数。
- $n_{\text{layers}}$Transformer 层数,每一层都要存一份 KV。
- $d_{\text{model}}$模型隐藏维度,即单个 token 的向量长度。
- $b \times s$batch 大小与每个元素字节数(FP16 为 2 字节),体现并发和精度的影响。
5. 多轮对话:老客复购的快捷通道
老客人的特权是什么?他昨天点过“珍珠乌龙拿铁,去冰,半糖”,今天进店只说一句“老样子”,店员就全懂了。多轮对话就是这个“老样子”——每一轮问的都是同一段历史加一个新问题。第 N 轮对话中,模型收到的 prompt 是:system 提示词 + 前 N-1 轮的问答 + 本轮新问题。
做增量缓存后,前 N-1 轮算好的 KV 都还在缓存里,本轮只对新问题做 Prefill,历史部分几乎全部命中。这也是 Prefix Cache 命中率最高的场景:缓存随对话轮次自然增长,每一轮都在复用上一轮几乎全部的计算成果。几十轮聊下来,每一轮的“首字时延”都能稳定压到一两百毫秒。
复购越多,省下的重复劳动越多。长对话场景是 Prefix Cache 体验最直观的舞台:历史越长,省得越狠。
- $N_{\text{prompt}}$第 R 轮请求的输入 token 总数。
- $N_{\text{sys}}$系统提示词的 token 数,它是所有轮次共享的“万用茶底”。
- $\sum_{r=1}^{R-1}$把前 R-1 轮的提问与回答逐轮累加,这就是老客户的完整订单历史。
- $N_r^q + N_r^a$第 r 轮的用户问题与模型回答各自占用的 token 数。
- $N_R^q$本轮新增的问题。命中缓存后,只需为它计算 KV。
6. 批量推理:一锅茶底分装二十杯
下午三点,二十个客人同时涌进店里,一半人点“乌龙茶底系列”。如果后厨给每个人单独煮一锅乌龙茶,灶台根本不够用;聪明的做法是煮一锅大的,分装到二十个杯子里,再分别加各自的小料。批量推理正是这样:一批请求往往共享同一段指令——统一的评估提示词、相同的公司系统提示词。没有 Prefix Cache 时,每个请求都各自 Prefill 一遍,公共前缀被重复计算 B 次。
有了共享前缀缓存,公共部分只算一次,B 个请求只花“一次 Prefill + B 次 Decode”的成本。配合 Continuous Batching(连续批处理)调度,GPU 的空闲算力被压榨到极致,服务吞吐量成倍上涨。
一锅茶底养活了二十杯奶茶。共享前缀的调度策略,就是奶茶店的“流水线分工”。
- $T_{\text{batch}}$整批请求的总服务时间。
- $T_{\text{prefill}}^{(\text{shared})}$共享前缀的预填充耗时,理想情况只计算一次,相当于后厨只煮一锅茶底分给所有杯子。
- $B \times T_{\text{decode}}$B 个请求各自逐 token 解码,这段互相之间无法共享。
- 对比没有缓存时总时间是 $B \times (T_{\text{prefill}} + T_{\text{decode}})$,前缀部分被放大了 B 倍。
7. 框架与效果:各家后厨的标准化水平
同样是“预煮茶底”的思路,连锁品牌的落地水平天差地别。有的后厨全自动,系统自动记录哪些茶底最畅销、自动补料;有的半自动,需要店员手动登记;有的还在论证“预煮会不会影响风味”。主流推理框架对 Prefix Cache 的支持就是一个光谱:vLLM 的 Automatic Prefix Caching 最成熟,相同前缀 block 自动复用,基本零配置;SGLang 的 RadixAttention 把前缀组织成基数树,支持前缀共享与分支复用,适合结构复杂的 Agent 场景;HuggingFace TGI 支持简单 prefix caching;TensorRT-LLM 则依赖工程师显式管理 KV 池。
实测数据方面,在共享 system prompt 的 Agent 场景,前缀命中率常达 70%—90%,首 token 延迟降低 80% 以上,吞吐提升 2—5 倍。缓存命中不只是省时间,还直接省 GPU 采购成本和电费。
把奶茶店的故事讲到最后:备料水平决定出杯速度,出杯速度决定排队人数。选对框架、调好缓存策略,就是给自家推理服务装上一台“自动煮茶机”。
- $T_{\text{cached}}$命中前缀缓存后的平均请求耗时。
- $T_{\text{original}}$完全不命中时的平均请求耗时。
- $\dfrac{T_{\text{cached}}}{T_{\text{original}}}$耗时比,体现“还剩多少工作量”。
- $1 - $ 并乘 100%换算成“省下的比例”,比值 0.2 时节省 80%。
AI 应用场景
智能客服多轮会话
阶段:在线推理服务优化
解决的问题:客服机器人每轮请求都要携带整段对话历史,历史越长首字延迟越高,用户等 3 秒没回复就流失。Prefix Cache 缓存历史部分的 KV,本轮只剩用户一句话需要 Prefill,首字延迟从 2 秒降到 300 毫秒。
具体案例:某电商客服机器人用 vLLM 部署,system prompt(公司政策 + 促销信息)约 800 token,历史对话平均 1200 token。启用 Automatic Prefix Caching 后,前缀命中率 85%,GPU 占用下降 40%,同一块 A10 上从支撑 500 路并发提升到 1200 路。
离线批量评测与数据标注
阶段:离线批处理流水线
解决的问题:评测 1000 条测试样本时,每条样本都带着相同的指令前缀(system prompt + 任务说明)。逐个跑 Prefill 纯属浪费,用 Prefix Cache 让同一批次共享一份前缀 KV。
具体案例:某团队用 SGLang 跑大模型排行榜评测,1000 条样本共享 300 token 的指令。启用 RadixAttention 后,Prefill 总耗时下降 75%,评测从 6 小时缩短到 2 小时。
SaaS 多租户 API 服务
阶段:Serverless 平台调度
解决的问题:多租户共用同一模型但各有 system prompt,平台希望各租户内部请求共享缓存,同时租户之间互不串扰。Prefix Cache 按“前缀内容哈希”隔离,同租户相同前缀命中,跨租户天然失配。
具体案例:某大模型 API 网关为每个企业客户配置固定 system prompt,命中率达到 80%,平均响应时间降低 60%,按 token 计费的成本明显改善。
Python 演示
"""prefix_cache_demo.py - simulate public prefix KV cache for LLM serving""" from collections import OrderedDict class PrefixCache: """Simulate a prefix KV cache with LRU eviction.""" def __init__(self, capacity): self.capacity = capacity self.entries = OrderedDict() self.total_tokens = 0 self.saved_tokens = 0 def longest_common_prefix(self, prompt): """Return the longest cached prefix length shared with prompt.""" best = 0 for cached in self.entries: common = 0 for a, b in zip(cached, prompt): if a != b: break common += 1 best = max(best, common) return best def serve(self, prompt): """Serve one request: reuse cached prefix, then store this prompt.""" self.total_tokens += len(prompt) common = self.longest_common_prefix(prompt) self.saved_tokens += common key = tuple(prompt) self.entries[key] = len(prompt) self.entries.move_to_end(key) while len(self.entries) > self.capacity: self.entries.popitem(last=False) return common def reuse_ratio(self): return self.saved_tokens / self.total_tokens if self.total_tokens else 0.0 def build_prompt(system_prefix, history, question): """Build the prompt of round N: system + history + new question.""" prompt = list(system_prefix) for q, a in history: prompt.extend([f"user:{q}", f"assistant:{a}"]) prompt.append(f"user:{question}") return prompt def simulate_multi_round(cache, system_prefix, num_rounds): history = [] for i in range(num_rounds): prompt = build_prompt(system_prefix, history, f"q{i}") common = cache.serve(prompt) print(f"round {i + 1:2d}: prompt={len(prompt):3d} tokens, reused={common:3d}") history.append((f"q{i}", f"a{i}")) def main(): cache = PrefixCache(capacity=16) system_prefix = ("<system>",) * 6 num_rounds = 10 simulate_multi_round(cache, system_prefix, num_rounds) print(f"total tokens: {cache.total_tokens}") print(f"reused tokens: {cache.saved_tokens}") print(f"reuse ratio: {cache.reuse_ratio():.2%}") if __name__ == "__main__": main()
self.entries = OrderedDict()用有序字典维护缓存条目,顺序即 LRU 热度。common = self.longest_common_prefix(prompt)计算新请求与已缓存前缀的最长公共前缀长度。self.entries.move_to_end(key)命中后把条目移到末尾,表示刚被使用。self.entries.popitem(last=False)容量超限时从头部弹出最久未使用的条目。prompt.extend([f"user:{q}", f"assistant:{a}"])把前几轮问答逐条展开进本轮 prompt。print(f"round {i + 1:2d}: prompt={len(prompt):3d} tokens, reused={common:3d}")打印每一轮复用情况,直观看到复用数随轮次增长。
常见误区
误区我一直以为 Prefix Cache 和 KV Cache 是两种并列的缓存技术,一个是缓存注意力向量,一个是缓存答案文本。
正解KV Cache 是 Transformer 解码时缓存的 K/V 向量,避免重复计算历史 token;Prefix Cache 是建立在 KV Cache 之上的优化策略——只缓存所有请求共享的“公共前缀”(如 system prompt、历史对话)对应的 KV 值,让新请求跳过这段 Prefill。它不缓存每个请求独有的后缀 KV,更不是缓存输出文本。可以理解为:KV Cache 是“做好的半成品”,Prefix Cache 是“按配方复用半成品”。
为何常见:二者名称相近,入门资料常放在同一章节讲,初学者容易把“缓存”的对象混淆。
误区用户两次输入都以“你好”开头,我以为前缀缓存肯定能命中,省下一大段计算。
正解命中要求 token 序列从位置 0 开始逐位完全一致。tokenizer 切分后,“你好啊”和“你好呀”是两串完全不同的 token;甚至同一个句子,末尾多一个空格都会导致整条前缀失配。此外位置编码、模型配置也必须对齐。所以命中不是“前几个字一样”,而是“从第一个 token 到复用点完全一样”。
为何常见:人看文本靠语义判断“差不多”,而机器按 token 精确比较,差一个字符都是两杯不同的茶。
误区显存里多塞前缀 KV,命中概率自然更高,所以缓存空间越大越好。
正解显存是硬约束。Prefix Cache 占用显存,挤占 KV Cache 的可分配空间和 batch size;缓存条目越多,哈希查找与淘汰开销也越大。工程上要用 LRU/LFU 及时清理冷前缀,还要权衡缓存粒度——块太大复用率低,块太小索引开销高。命中率不是唯一指标,整体吞吐才是。
为何常见:“多存多中”符合直觉,但忽略了显存是有限资源,缓存和推理本身在抢同一块 GPU。
误区看演示动画里绿色部分一闪而过,我以为命中后整个请求马上返回结果。
正解命中的只是公共前缀部分的 KV 计算,新 token(用户问题、模型回答)仍然要走 Prefill 和 Decode。加速体现在“少算了前缀”,而不是“省掉全部”。首 token 延迟会大幅下降,但总响应时间仍与生成长度成正比。
为何常见:动画和宣传图把“命中”表现得像魔法,容易让人忽略 Decode 阶段依然逐 token 串行的事实。
面试题
Q1. Prefix Cache 缓存的核心对象是什么?
- A.缓存模型的权重参数
- B.缓存公共前缀的 KV 值← 正确答案
- C.缓存完整输出文本
- D.缓存训练数据样本
答案:B。Prefix Cache 缓存的是公共前缀 token 序列对应的 Key/Value 向量,让后续请求跳过这段 Prefill。模型权重是推理的基础,不是缓存对象;输出文本每次生成都不一定相同,缓存它没有意义;训练数据与推理无关。就像奶茶店备好的是“茶底”而不是“整杯成品”。
Q2. 下列哪个场景最容易命中 Prefix Cache?
- A.用户输入完全随机的短文本
- B.多轮对话携带相同历史上下文← 正确答案
- C.模型生成结果写入磁盘
- D.每次请求都重新设定系统提示词
答案:B。多轮对话中每轮请求都带着相同的 system prompt 和前几轮历史,天然构成可复用的公共前缀。随机输入没有公共部分;每次换 system prompt 会破坏前缀;写入磁盘和缓存命中无关。就像老客人说“老样子”,后厨知道他的完整订单。
Q3. Prefix Cache 命中的必要条件是什么?
- A.请求数量超过缓存容量
- B.请求的 Token 前缀完全一致← 正确答案
- C.GPU 型号必须是 A100 系列
- D.模型版本必须每天更新
答案:B。命中要求新请求的 token 序列从第 0 位开始与缓存条目逐位一致,且位置编码、模型配置对齐。请求数量、GPU 型号、模型更新频率都不影响前缀是否匹配。差一个 token,前面全部白算,就像茶底配方差一味就得重煮。
Q4. 以下哪些因素会导致前缀缓存无法命中?
- A.系统提示词中空格数量改变← 正确答案
- B.请求与缓存模型配置不同← 正确答案
- C.请求前缀与缓存条目一致
- D.缓存条目被 LRU 策略淘汰← 正确答案
答案:ABD。空格数量改变会改变 tokenizer 切分结果,导致 token 序列失配;模型配置不同(位置编码方式变化)使缓存向量不可复用;条目被 LRU 淘汰后缓存里根本不存在,无法命中。C 是命中而非失配场景。就像茶底对上了却被店员倒掉了,或者配方本身就换了。
Q5. LRU 淘汰策略在 Prefix Cache 中的主要作用是?
- A.提升生成阶段的解码速度
- B.在有限显存内保留高复用缓存← 正确答案
- C.降低模型推理精度
- D.减少前缀缓存的命中率
答案:B。显存有限,LRU(最久未使用先淘汰)让最可能被复用的热点前缀留在显存里,冷门前缀腾出空间。它不直接加速解码,也不影响精度,更不会降低命中率——而是把显存花在刀刃上。就像备料区把卖得好的茶底摆前排,滞销小料及时下架。
Q6. 关于 Prefix Cache 的显存开销,以下说法正确的是?
- A.缓存只存 K 不存 V 能省一半
- B.与序列长度和层数线性相关← 正确答案
- C.只与批大小有关无其他项
- D.前缀缓存不占用额外显存
答案:B。KV 显存 ≈ 2 × 序列长度 × 层数 × 隐藏维度 × batch × 字节数,与序列长度、层数线性相关。K 和 V 缺一不可,跳过任何一方都无法完成注意力计算;Prefix Cache 当然要占显存。理解这个公式才能回答“为什么缓存不是越大越好”。
Q7. 多轮对话中,Prefix Cache 能带来哪些提升?
- A.跳过历史对话的重复预填充← 正确答案
- B.降低首 Token 的响应延迟← 正确答案
- C.完全消除全部解码计算
- D.提高长上下文场景吞吐← 正确答案
答案:ABD。历史对话是天然公共前缀,命中后 Prefill 只剩新问题,首 token 延迟大幅下降,长上下文场景吞吐提升。但 Decode 阶段仍然逐 token 生成,不可能被消除。就像老客户点单快了,但奶茶还是一口一口喝完的。
Q8. 下列哪个推理框架对 Prefix Cache 有成熟支持?
- A.vLLM 推理引擎支持成熟← 正确答案
- B.GPT 模型参数规模更大
- C.PyTorch 通用训练框架
- D.Pandas 数据处理工具库
答案:A。vLLM 的 Automatic Prefix Caching 自动复用相同前缀 block,是生产环境最常用的方案。GPT 是模型不是推理框架;PyTorch 是训练/通用框架,不直接提供前缀缓存特性;Pandas 是数据分析库。选框架就像选后厨:得选那个把备料流程标准化了的品牌。
Q9. 命中 Prefix Cache 后,新请求仍需完成哪部分计算?
- A.公共前缀预填充和全部解码
- B.新 Token 的 KV 计算与解码← 正确答案
- C.重新编码全文并随机采样
- D.无需任何计算直接返回
答案:B。命中只复用公共前缀的 KV,新问题仍要 Prefill 计算自己的 KV,之后整个回答仍需逐个 token 解码生成。前缀部分不需要重新编码,但也绝不是说完全不用计算。就像茶底备好了,但加料、封杯、打包一样不能少。
Q10. 在设计 Prefix Cache 方案时,需要权衡哪些因素?
- A.缓存粒度影响显存和复用率← 正确答案
- B.淘汰策略匹配请求分布← 正确答案
- C.哈希查找与预填充的开销← 正确答案
- D.模型权重是否同步缓存
答案:ABC。缓存块粒度、淘汰策略、查找开销三者直接决定命中率与显存效率。块太大复用率低,块太小索引开销高;LRU 适合热点集中的请求,LFU 适合低频长尾;哈希匹配本身也有成本,前缀太短时匹配开销甚至超过重算。模型权重不是 Prefix Cache 的缓存对象,D 无关。