同一段长 prompt,为什么第二次调用「理论上」可以快约二十倍:前缀缓存到底存了什么、又省掉了哪一段算力
把「前缀缓存(prefix caching)」讲成一本可以手算的账。一次 4,200 token 的输入里,前 4,000 token 是每次都一样的系统提示与文档,后 200 token 才是这次真正的问题。按 Llama-3 系列公开 config 现算,BF16 口径下每 token 的 KV 体积约 128 KiB(8B)与 320 KiB(70B),4,000 token 的前缀分别驻留约 500 MiB 与 1.22 GiB。在「prefill 是算力瓶颈、且只计线性层」的简化模型里,命中后 prefill 的线性层浮点运算只剩 4.76%,对应约 21 倍;若再计入约 30 毫秒的非 prefill 开销(示意值),这场收益会摊薄成 5.2 倍(8B)与 15 倍(70B)。全文毫秒数都是估算、不是实测,口径与假设都在文内写明。
除另行标注外,本文引用的外部事实与页面核验均以 2026-10-24(北京时间)为时点。文中所有标「本文自行推导」的算式都写清了假设与中间值,可以自己按一遍;凡厂商口径与第三方口径不一致处,本文都标了出处与量纲,不做合并。本文没有做端到端实测:所有毫秒数都是按公式与明示假设算出的估算值。
一、四个你肯定见过的现场
先不谈机制,只说四个几乎每个用过 API 的人都见过的现象。
现场一:同一段长文档,问第二个问题明显更快。 你把一份 3,000 token 的产品文档贴进对话,问第一个问题要等两三秒;在同一段对话里接着问第二个问题,往往不到一秒就开始出字了。很多人以为是”模型热身”或者”服务端限速放开了”,其实更常见的原因是:前缀的那 3,000 token 已经被算过一遍,第二次不需要再算。
现场二:换了日期那一行,速度掉回第一次。 你在系统提示的最前面写 今天是 2026-10-24,当天反复调用还能吃到缓存;到了第二天(日期变了)的第一次调用,速度与账单都回到”首次”的水平。原因不是日期本身有毒,而是前缀缓存要求逐 token 相同:开头一变,从变化处往后全部作废。
现场三:账单上出现了”缓存命中”这一行。 主流厂商的接口会把输入 token 拆成”缓存读”与”未命中”两栏计价。以本文核验的 OpenAI 文档为例,缓存读的价格是普通输入价的一个比例(多数模型 0.1 倍,GPT-6.1 Sol 为 0.05 倍,见来源段),官方文案写作”discounted up to 95%“。注意:折扣比例不等于计算节省比例,两者是不同的尺子,第九节会分别算。
现场四:并发一上来,缓存就像没开过。 同一段前缀,白天单人测试时第 2 次调用明显变快;流量上来之后却感觉不到收益。原因通常不是”缓存坏了”,而是缓存的 KV 要和并发请求抢同一块显存,以及空闲缓存块会被淘汰——“上一个请求刚算完的前缀”很快就不是热的了。所以**“缓存有没有效”必须绑着”在什么并发下”一起问**,否则两组人拿各自的体验争论,谁都没说错。
如果你只记住一句话:前缀缓存省的是”重新算一遍前缀”的浮点运算,不是显存,也不是后续的生成步骤。 下面把这句拆开。
二、先把词说清:术语与符号
| 词 | 一句话定义 | 本文用的符号 |
|---|---|---|
| prefill(预填充) | 把输入整段一次性算进模型、产出每个位置的 KV 的阶段,可以并行 | 输入长度 |
| decode(解码) | 从第一个输出 token 起,一个 token 一个 token 往后算的阶段,只能串行 | 每步一个 token |
| KV cache | 每层注意力里已经算好的 Key、Value 张量,decode 每一步都要按位置读它们 | 每 token 字节数 |
| 前缀(prefix) | 一次请求里位于最前面、且与其他请求相同的那一段 token 序列 | 可复用长度 |
| 新问题部分 | 前缀之后这次真正新增的内容 | 长度 |
| 前缀缓存 | 把已算过的前缀的 KV 留在显存里,下次遇到相同前缀直接复用 | token 命中率 |
| 块(block) | KV cache 的分配单位,也是缓存的命中单位,一次对齐若干个 token | 块内 token 数 |
| TTFT | time to first token,从请求发出到第一个 token 出来的时间 |
几个基础量的白话解释,免得后文卡住:
- TFLOPS:每秒一万亿次浮点运算。
- BF16:一种 16 位浮点格式,每个数占 2 字节;模型权重与 KV 常默认用它存。
- KiB / MiB / GiB:按 1024 进位的存储单位,与厂商宣传里按 1000 进位的 KB/MB/GB 不同。本文显存容量用十进制 GB,KV 体积用 1024 进位,两处都标了单位。
- :每层里所有 KV 头的维度之和,等于”KV 头数 × 每个头的维度”。
两个符号约定,后面所有推导都依赖它们:参数量记作 (8B 模型即 ),层数记作 ,每个数占 字节(BF16 为 2)。有效算力记作 ,本文统一取 TFLOPS(约等于 H100 SXM 的 BF16 稠密峰值 989 TFLOPS 的 40%,即”峰值打个四折”的乐观假设)。
本文还有一个贯穿全文的量:算术强度,即每搬运 1 字节能做多少次浮点运算。它在这里的作用是提醒——重算前缀是算力开销,复用缓存是显存与带宽开销,两者要用同一把尺子比。
三、前缀到底花了哪一笔钱:prefill 与 KV 两本账
3.1 第一本账:prefill 的浮点运算量
粗略地说,一个稠密 Transformer 每处理 1 个 token,前向大约要做”参数量乘以 2”次浮点运算(一次乘、一次加):
这个式子漏了注意力那一项。注意力里的 与加权求和,每层代价约 ,其中 是全部 query 头的维度之和(不是 KV 维度,两者在 GQA 下不同):
以 8B 模型(,32 个 query 头、每头 128 维,故 )、 代入,注意力约 次运算,线性部分约 次运算。若不考虑因果掩码带来的减半,注意力约占 13.7%;把因果掩码平均折扣一半后约 6.9%。 无论取哪个,注意力在几千 token 量级上都不是主体——所以下面的推导只算线性层,并把注意力当作被忽略的小项。
3.2 第二本账:KV cache 的体积
prefill 的产物之一是每个位置、每一层、每个 KV 头的 Key 与 Value 张量。每个 token 占用的字节数是固定的:
按 Llama-3 系列公开 config(8B:32 层、8 个 KV 头、每头 128 维;70B:80 层、8 个 KV 头、每头 128 维;两个 config 的 torch_dtype 都是 bfloat16,来源是 NousResearch 托管的 Meta-Llama-3 镜像仓库,见来源段):
这里要把两种体积分开叫名字,全文不再混用:
- 前缀驻留体积():8B 约 500 MiB;70B 约 1.22 GiB。
- 整次输入体积():8B 约 525 MiB;70B 约 1.28 GiB。
这两本账是本文的骨架:第一本账(浮点运算)决定”重算有多贵”,第二本账(字节数)决定”缓存要占多少显存”。
四、命中之后省的是什么:把「重算」换成「复用」
先做一个很干净的对比,并明确本节模型的边界:只估被省掉的线性层浮点运算,不建模注意力访存、kernel 启动、调度与排队成本,因此它不是完整时延预测。
没有缓存时,每次请求都要对 个 token 做一遍 prefill:
前缀全部命中时,前缀那 个位置的 KV 已经在显存里,服务端只需对新的 个 token 做 prefill:
两者相除,得到 prefill 段的加速比:
其中 是可复用 token 占整次输入的比例(本文算例里 )。这个式子有一个反直觉的地方:参数量 被约掉了。也就是说,在”prefill 是算力瓶颈、且只计线性层”这个条件下,前缀缓存的比例收益只取决于可复用占比,与模型是 8B 还是 70B 无关——这不代表实测加速不受模型、批大小与硬件影响,只是这个简化模型里 抵消了。
代入本文算例(、):
| 模型 | 未命中线性计算量 | 未命中耗时(@400 TFLOPS,估算) | 命中后计算量 | 命中后耗时 | prefill 段比例 |
|---|---|---|---|---|---|
| 8B | 168 ms | 8 ms | 21 倍 | ||
| 70B | 1,470 ms | 70 ms | 21 倍 |
两行的”21 倍”是同一个数,因为 。两行的绝对量差近 9 倍,则来自参数量。
这里必须澄清一个容易误解的点:命中之后,前缀的 KV 并不是”不存在”了——它还在显存里,注意力的计算仍要按位置读到它。所以”命中”省掉的不是显存读写,而是把前缀重新过一遍权重矩阵的浮点运算。至于”重算”与”命中”两条路的注意力访存是否等价,本文不做建模(那取决于 kernel 与分块方式),因此不能把本文的 21 倍当时延预测。
五、为什么「省了 95% 的算力」不等于「快 95%」
这是本文最想让读者带走的一节。
上面算出 prefill 段快 21 倍,对应”线性层计算量只需原来的 4.76%“。但用户感受到的是 TTFT,而 TTFT 不等于 prefill 时间。本文采用下面这个计时边界(不把首 token 的采样重复计入):
把排队、调度与网络这些”非 prefill 项”粗算成合计 30 毫秒(量级示意,不是任何厂商承诺值;不含首 token 采样本身):
| 模型 | 未命中 TTFT(估算) | 命中 TTFT(估算) | 端到端比例 |
|---|---|---|---|
| 8B | 168 + 30 = 198 ms | 8 + 30 = 38 ms | 5.2 倍 |
| 70B | 1,470 + 30 = 1,500 ms | 70 + 30 = 100 ms | 15 倍 |
这解释了现场一里那种”感觉快了不少、但也没有二十倍”的体感:模型越小,固定的那 30 毫秒占比越大,缓存的相对收益越小。 反过来,70B 这种 prefill 本身就在秒级的模型,缓存收益几乎直接体现在体感上。
5.1 命中率的定义,与一张不再自相矛盾的表
下面统一采用 token 命中率:账单里 cached_tokens 除以 input_tokens,即 。它与”可复用比例” 的区别只有一点: 描述这一次请求里有多少 token 真的被命中,因此它的上限就是 :
在这个定义下,命中后 prefill 段耗时的比例就是:
| token 命中率 | prefill 段耗时相当于未命中的 | 说明 |
|---|---|---|
| 50% | 50% | 一半输入被复用 |
| 80% | 20% | 常见的”系统提示稳定、问题每次都变”场景 |
| 90% | 10% | 长文档问答、RAG 的较好水平 |
| 95.24% | 4.76% | 本算例的理论上限( 全部命中,只剩 要算) |
这张表的关键是最后一行:95.24% 已经是本算例的天花板。 只有把问题缩短( 变小)或把前缀加长( 变大),天花板才会抬高;把 写成 99% 是自相矛盾的——那需要 只占输入的 1%,与本算例的 200/4,200 不符。
5.2 命中率的现实分布:为什么常见值是 80%,而不是 99%
命中率不是靠”开开关”得到的,它由你的输入里有多少比例是每次都不变的决定。三个场景,都按 4,200 token 的输入给出组成:
| 场景 | 每次不变的部分 | 每次都变的部分 | 可复用比例 | prefill 剩余 |
|---|---|---|---|---|
| 多轮对话(第 3 轮) | 系统提示 3,500 + 前两轮 500 | 本轮问题 200 | 95.2% | 4.8% |
| RAG 问答 | 系统提示与模板 3,500 | 检索片段 500 + 问题 200 | 83.3% | 16.7% |
| 短系统提示 + 每次换文档 | 系统提示 800 | 文档 3,200 + 问题 200 | 19.0% | 81.0% |
这张表解释了两个常见的落差:
- 为什么”我开了缓存但没感觉”:如果场景落在第三行(每次换一份文档),可复用比例只有 19%,prefill 段只降到 81%——体感上几乎看不出来。要改善,得让稳定且确有任务价值的那部分变长(格式要求、字段定义、示例),而不是无意义地灌字数:命中率上升不等于总计算量下降,要比较的是绝对 token 数与总费用。
- 为什么多轮对话最划算:每一轮的前缀都包含此前所有轮次,前缀只增不减,可复用比例随对话推进单调上升。这是对话式产品最先吃到这项优化红利的原因。
顺带说清一个常见误解:命中率是按 token 算的,不是按请求算的。 十个请求全部命中了系统提示、但每个请求的文档都不同,命中率仍可能只有 80% 上下。
六、缓存怎么被「找到」:块哈希链与基数树
上面全是”假设能命中”。真实系统要在毫秒级里回答一个问题:这段前缀我以前算过吗?
6.1 vLLM 的答案:按块做哈希,哈希里带父哈希
vLLM 官方设计文档(docs/design/prefix_caching.md,本文以仓库 main 分支文件为核验对象)写得直白:它按”块”对 KV cache 做哈希,每个块的哈希由三部分组成——父块的哈希值、本块的 token、以及额外哈希(例如 LoRA ID、多模态输入哈希、多租户隔离用的 salt):
Block 1 Block 2 Block 3
[A gentle breeze stirred] [the leaves as children] [laughed in the distance]
Block 1: |<--- block tokens ---->|
Block 2: |<------- prefix ------>| |<--- block tokens --->|
Block 3: |<------------------ prefix -------------------->| |<--- block tokens ---->|
这段示意来自 vLLM 文档原文:第 3 块的哈希里同时包含”本块 token”与”前面所有 token”,因此不同前缀但同一段文字不会互相命中。这挡住了一个很容易犯的错:两段文字相同、位置不同,注意力算出来的 KV 是不一样的。
flowchart TD
A[请求到达 共 4200 token] --> B[按块切分 每块 B 个 token]
B --> C[逐块算哈希 父哈希 加 本块 token 加 额外哈希]
C --> D{哈希在缓存表里?}
D -- 命中 --> E[复用已有 KV 块 跳过重算]
D -- 未命中 --> F[正常 prefill 并写入缓存]
E --> G[只算未命中的那部分输入]
F --> G
G --> H[进入 decode 逐 token 生成]这张图说明了什么:命中的判定发生在”块”这一层,而且是链式的——某一块的哈希依赖它前面所有块,所以缓存命中必然是从第一个块开始连续命中的一段,不存在”中间某块命中、前面没命中”的情况。图中只画了”全命中 / 全不命中”两条极端路径,真实请求通常是部分命中:前面若干块命中、后面的块要算。
6.2 SGLang 的答案:把 KV 放进一棵基数树
SGLang 在 2024-01-17 的官方博客里给出另一条路线:不再丢弃请求结束后的 KV cache,而是把提示与生成结果的 KV都留在一棵基数树(radix tree)里,键是 token 序列、值是 KV 张量,配 LRU 淘汰与”缓存感知的调度”来提高命中率。
吞吐数字注意口径:arXiv 上的 SGLang 论文(v2,2024-06-06 修订)摘要写的是”最高 6.4 倍吞吐”,LMSYS 官方博客(2024-01-17)写的是”相对 Guidance 与 vLLM 最高 5 倍吞吐”,实测条件是 Llama-7B 与 Mixtral-8x7B、NVIDIA A10G。这两者是同一项研究工作的论文与早期博客,不是两个独立团队的交叉验证;而且它们都是整系统吞吐,不是”前缀缓存这一项”的收益。本文只当量级参考,不合并、不取平均。
6.3 三种实现路径的差别
| 路径 | 命中判定 | 复用粒度 | 公开的代表 |
|---|---|---|---|
| 块哈希 + 父哈希链 | 逐块哈希查表 | KV 块 | vLLM(官方设计文档) |
| 基数树 + LRU | 树上最长前缀匹配 | 树节点(可变长) | SGLang(arXiv:2312.07104 + LMSYS 博客) |
| 显式声明的”prompt 模块” | 用户声明可复用片段 | 片段 | Prompt Cache(arXiv:2311.04934,v2 2024-04-25) |
第三行值得单独说一句:Prompt Cache 的思路是由用户显式声明哪些片段可复用(论文里叫 prompt module),服务端提前算好这些片段的注意力状态并解决位置编码对齐问题;论文报告 TTFT 改善 8 倍(GPU 推理)到 60 倍(CPU 推理),且不需要改模型参数。它和 SGLang 的”自动发现”是两种哲学:一个靠人声明,一个靠系统猜。
七、什么会命中、什么不会:三条边界
7.1 边界一:vLLM 只缓存完整的块
vLLM 文档里有一句很短但很关键的话:“We only cache full blocks.”(只缓存完整的块。)文档里给了两个不同的例子,别把它们串成一个因果链:其一是”某块只填了 3 个 token 时,这一块还不具备被缓存的资格”;其二是”下一次请求即使前 10 个 token 一致,也只能命中前 2 块(8 个 token),因为第 3 块只匹配了 4 个里的 2 个”。前者讲资格,后者讲匹配长度。
把匹配长度这条写成式子,可复用的 token 数是:
也就是说,块内对齐会吃掉零头。块大小 是配置项(文档示例用 4,实际部署通常更大), 越大管理开销越小,但零头浪费越多。这是 vLLM 这一实现的边界,不要升格为所有实现的通用规律——基数树那条路线的复用粒度就不是整块。
7.2 边界二:必须逐 token 相同
不是”语义相同”,是”token 序列相同”。两个后果:
- 同一个意思换个说法、多一个空格、换一种中英文标点,匹配就会在那里断掉;
- 时间戳、随机 ID、请求序号写在最前面,等于给每个请求生成一条独一无二的前缀,缓存永远打不中。
7.3 边界三:变化点之后失效,之前仍可命中
因为哈希是链式的,变化所在的那个块及其之后都拿不到旧匹配;但变化点之前已经完整相同的块,仍然可以命中。这就是现场二的机制:把日期写在第一行,日期一变,从第一块开始就失效;把日期挪到最末尾,前面几千 token 照旧命中。
八、淘汰与隔离:LRU、TTL 与 salt
缓存不是白给的显存,它必须在”存得多”和”留给正在跑的请求”之间做取舍。
淘汰策略:vLLM 文档写明用 LRU——空闲队列的队头就是”最久没用过”的块,需要新块时把它淘汰掉;请求结束释放块时是逆序放进队列的,因为”最后一个块需要匹配更多 token、更不容易被别的请求复用,应该先被淘汰”。SGLang 的博客同样写的是 LRU 淘汰加缓存感知调度。
生命周期:托管 API 侧给的是一组显式参数。OpenAI 的 Prompt Caching 文档(2026-10-24 核验,入口 developers.openai.com)写明:缓存条目不会无限期保存;GPT-5.6 及之后可在 Responses API 用 prompt_cache_options.ttl 选择最短存活时间——"30m"(默认,30 分钟,写入价为未命中输入价的 1.25 倍)或 "12h"(12 小时,写入价 2.5 倍);读价与 TTL 无关,多数模型是普通输入价的 0.1 倍,GPT-6.1 Sol 是 0.05 倍。文档还给了两条实用机制:可以用显式断点(prompt_cache_breakpoint)给不同片段设不同 TTL;“显式模式”下最后一个断点之后的内容不产生写入费用——这正是”把易变内容放最后”的计费依据。
多租户隔离:vLLM 文档专门写了一节安全隔离——共享环境里可以用请求级的 cache_salt,它被注入第一个块的哈希,于是只有约定同一个 salt 的请求才能互相复用,从而避免”通过观察延迟差异推断别人缓存了什么”这类计时攻击。文档同时提示:v0.11.0(2025-10-02 发布)起默认哈希算法是 sha256;若换成非加密哈希(如 xxhash)提速,理论上会增加碰撞风险,在多租户环境下”可能导致未定义行为甚至泄漏隐私”。这条不是性能选项,是安全选项。
九、把账单算明白:命中率、写入溢价与回本点
延迟之外,还有钱。以 OpenAI 文档给出的口径做一个最小模型:未命中输入价记 1,缓存读价记 0.1(多数 GPT-5.6+ 模型;GPT-6.1 Sol 为 0.05),写入价记 1.25(30 分钟 TTL)。这只算”同一段前缀的输入费用”,不含每次新增问题与输出 token。
同一段前缀被总共用 次(1 次写入 + 次复用)时:
| 总使用次数(其中复用次数) | 走缓存的前缀价 | 不走缓存的前缀价 | 省下的比例 |
|---|---|---|---|
| 1 次(0) | 1.25 | 1.00 | −25%(更贵) |
| 2 次(1) | 1.35 | 2.00 | 32.5% |
| 3 次(2) | 1.45 | 3.00 | 51.7% |
| 4 次(3) | 1.55 | 4.00 | 61.3% |
| 20 次(19) | 3.15 | 20.00 | 84.3% |
回本条件是 ,即:
按这组价格,成功复用一次就已经回本。 工程含义很直接:一个前缀只被用一次,写入溢价让你多付 25%;会被第二次用到,就开始划算。所以真正要问的不是”要不要开缓存”,而是”哪些内容会被复用第二次”。注意这只对”该前缀的输入费用”成立:整张账单还受新增问题、输出 token、分层计价与批处理折扣影响。
十、缓存省不掉的三件事
这一节是为了防止过度期待。
| 省不掉的东西 | 为什么省不掉 | 量级参考(8B、) |
|---|---|---|
| 后续生成步骤 | 缓存只作用于输入侧;decode 每一步仍要按位置读历史 KV,历史越长读得越多 | 与本文算例无关,需按上下文长度另算 |
| 显存占用 | 缓存就是把 KV 留在显存里,命中率越高、留得越多 | 前缀驻留约 500 MiB(70B 约 1.22 GiB) |
| 输出 token 的计价 | 缓存只作用在输入侧,输出照常按 token 计费 | 由生成长度决定 |
其中第二条最容易被忽略:前缀缓存的本质是”用显存换算力”。它把”每次重算前缀”的浮点运算,换成”把前缀 KV 留在显存里”。因此它与”提高并发批大小”存在竞争关系——显存就那么多,缓存占多了,能同时驻留的请求就少了。这也是托管服务用 TTL 与命中率仪表盘管理这件事的原因:缓存不是越多越好,而是”留到刚好被下一次命中”最好。
10.1 一条能立刻用的经验式
设显存总量 、权重占 、一份前缀驻留 ,那么”整个 KV 池只放缓存、不留任何并发”时的驻留份数是:
代入 8B 算例( GB 十进制、 GB、 MiB):
这个数字要说清边界:它只是”能同时驻留多少份前缀”的上界,不等于”并发上限”。原因见 vLLM 文档的淘汰逻辑——空闲缓存块在需要时可以被淘汰、让位给新请求,所以缓存占用不是不可回收的并发配额;它影响的是”命中率与并发之间的平衡”,而不是”并发为 0 还是不为 0”。它的实际用处是让你在压测时一眼看出某个配置是不是荒唐:如果一份前缀就要 500 MiB,而缓存保留预算设成了几十 GB,那就等于自愿把并发压得很低。
十一、工程上怎么排 prompt
把前面所有机制折成一条可执行的规则:稳定在前、易变在后、尽量对齐块边界。
flowchart LR
subgraph 差的做法
A1[第一行写当前时间戳] --> A2[系统提示 3500 token]
A2 --> A3[用户问题 200 token]
end
subgraph 好的做法
B1[系统提示 3500 token 稳定不变] --> B2[加显式缓存断点]
B2 --> B3[当前时间与用户问题 200 token]
end这张图说明了什么:两张图的 token 总量、模型、问题都一样,差别只在”易变字段的位置”。放在最前面,链式哈希让后面 3,500 token 全部失效;放在最后,最多能让前面那部分命中——实际能命中多少还要看块对齐、冷启动、缓存淘汰与请求路由(块大小为 16 时,3,500 token 里可对齐的是 3,488)。
几条具体做法(都在前述机制上有依据):
- 把长期稳定、确有任务价值的内容放最前:系统提示、工具定义、few-shot 示例、长文档。OpenAI 文档的建议也是把一天内反复复用的材料放在 prompt 开头,并给它的断点设更长 TTL。
- 把易变内容放最后:当前时间、请求 ID、用户本轮问题、本轮检索片段。
- 别在开头拼随机串或当天日期:这等于主动击穿缓存。
- 注意块对齐:若系统提示是 3,500 token、块大小 16,则对齐后可复用的是 3,488 token()。差 12 个 token 听起来无所谓,但它决定最后一个块要不要重算。
- 多租户场景考虑 salt 与隔离:复用别人的缓存不只是隐私问题,也可能是可被测出来的信息侧信道(vLLM 文档的计时攻击说明)。
- 不要把规则绝对化:顺序调整要在不改变指令优先级与业务语义的前提下做;系统级的安全约束不能为了缓存而挪到用户问题后面。
11.1 一个可以直接抄的骨架
[第 1 段 · 永不变] 角色定义与输出格式约束 ← 最前面,越长越划算(须有任务价值)
[第 2 段 · 按天/按租户] 领域规则、字段释义、示例 ← 加显式缓存断点
[第 3 段 · 按会话] 历史轮次 ← 天然被下一轮复用
[第 4 段 · 每次都变] 本轮检索片段 / 当前时间戳 / 用户问题 ← 永远放最后
四段的顺序就是”复用概率从高到低”。判据很简单:任何一段如果这次和上次不同,它就不该出现在前面,否则它后面的全部内容都要陪葬。反过来说,只在一个会话里用一次的内容放最后,还能在部分托管实现里省掉写入费用(OpenAI 文档的”显式模式”即如此描述)。
十二、同一件事的四个近亲:别把它们的收益混在一起
“推理慢”是一个症状,四种常见做法的收益落在完全不同的地方。混着说,就会出现”明明开了缓存还是慢”的困惑。
| 做法 | 省的是 | 费的是 | 前提 |
|---|---|---|---|
| 前缀缓存 | prefill 的浮点运算 | 显存(驻留前缀 KV) | 前缀逐 token 相同、会被复用 |
| 分块 prefill(chunked prefill) | 不省总量,省的是抖动:长 prefill 不再把 decode 卡住 | 一点调度复杂度 | 有并发的 decode 在跑 |
| PD 分离 | 不省算力,省的是互相干扰:prefill 与 decode 各用各的卡 | 一次 KV 传输与两套资源 | 负载形态稳定、传输带宽够 |
| 投机解码 | decode 的串行步数(时间) | 更多浮点运算(小模型起草 + 大模型验证) | 有足够高的接受率 |
这张表最重要的一行是第四行:投机解码是拿算力换时间,前缀缓存是拿显存换算力。 两者的收益作用在 TTFT 的不同分量上——前者压缩 decode 的每步,后者压缩 prefill 那一段。本文只算后者。
第二件要提醒的事:四种收益不能简单相乘。 如果前缀缓存已经把 prefill 从 168 毫秒压到 8 毫秒,那么 TTFT 里剩下的主要是固定开销与首步 decode,此时再优化 prefill,边际收益接近零。反过来,如果前缀每次都变、命中率为 0,那么无论把缓存开得多激进,收益也还是零。判断顺序应该是:先看命中率,再看 prefill 占比,最后才谈优化手段。
十三、一次完整的对账:4,200 token 的请求,四本账
把散落的数字收进一个具体场景,方便按着验算。场景:8B 稠密模型、BF16、H100 SXM,一次请求 4,200 token(4,000 前缀 + 200 问题),前缀每次相同,假设有效算力 400 TFLOPS。
第一本账:算力。 未命中 次浮点运算,命中后 次;省掉的量是:
也就是那 4,000 个前缀 token 的线性层运算。
第二本账:显存。 前缀驻留 500 MiB,占一张 80 GB H100 的约 0.66%(十进制 GB 口径)。若整张卡只放缓存、不放权重、无碎片,可驻留:
份(向上取整);扣掉 16 GB 权重后约 122 份(与 10.1 节一致)。两者都是上界而非配额:空闲缓存块可被淘汰让位,所以它描述的是”缓存能占多少显存”,不是”并发会被压到多少”。
第三本账:钱。 同一前缀总共用 20 次(1 次写入 + 19 次复用):走缓存的前缀价约 ,不走缓存是 20,前缀输入费用省约 84%。
第四本账:时间。 prefill 168 毫秒 → 8 毫秒;若端到端还有 30 毫秒非 prefill 开销,则是 198 毫秒 → 38 毫秒。
四本账放在一起,结论很朴素:前缀缓存是一笔”用约 0.66% 的显存,换掉 95.24% 的 prefill 线性计算量”的交易,前提只有一个——那段前缀真的会被第二次用到。
十四、为什么前缀缓存不改变输出(以及三处例外)
很多人担心”复用缓存会不会算错”。把机制拆开看,这件事可以在数学层面说明,而不是靠”实测没发现问题”。
对位置 的那个 token,注意力输出是:
其中 由位置 的表示算出,而 由第 个位置的该层输入表示算出——该表示本身依赖它之前的全部上下文。所以准确的表述是:在”完整前缀、权重与相关配置(位置编码、LoRA、多模态输入)都相同”且理想精确计算的前提下,复用与重算得到同一组 KV。 这也是 vLLM 文档敢写”prefix caching is almost a free lunch and won’t change model outputs”的依据。
三处例外,都得说清:
- 浮点运算不满足结合律。 同一批数用不同累加顺序(不同 kernel、不同分块、不同批大小)求和,末几位可能不同。所以”理论上一致”在实践中通常是”分布一致、逐位可能不同”。业务上通不通过要按你的场景验,本文不给”无影响”的承诺。
- 随机采样本身就会让相同请求返回不同文本。 两次调用结果不同,未必与缓存有关,也可能只是采样随机性——先核对输出设置,再谈缓存。
- LoRA 与多模态输入必须进哈希。 vLLM 文档的”额外哈希”里明确列了 LoRA ID 与多模态输入哈希:同一基座挂不同 LoRA,KV 不同;图片内容不同但占位 token 相同,KV 也不同。漏算就会出现”看起来同一段文本、实际复用了错误 KV”的静默错误。这说明”额外哈希”看着像实现细节,其实是正确性的一部分。
十五、边界与未解
- “prefill 是算力瓶颈、只计线性层”这个前提有边界。 本文所有”约 21 倍”都建立在计算量除以有效算力这个模型上。当块很小、或显存带宽被打满时,prefill 也会接近带宽瓶颈,此时实际收益低于本文估算。
- 有效算力 400 TFLOPS 是假设,不是实测。 真实值取决于实现、批大小、序列长度与并行切分;本文用它只是为了把两本账放在同一把尺子上。换掉假设,结论的结构不变(比例项仍是 ),绝对值会变。H100 的峰值与带宽属外部参数,本文未在来源段附硬件规格页,取用时请自行核对。
- 30 毫秒固定开销同样是量级示意。 真实系统里它包含排队、调度、KV 块分配、网络往返与前端渲染。
- 跨副本共享缓存(多个推理副本共用一份前缀 KV)本文没有展开,它涉及一致性、网络传输与失效广播。
- 厂商口径与第三方实测的差别:SGLang 论文 6.4 倍与 LMSYS 博客 5 倍都是项目作者报告的整系统吞吐,不是独立第三方复测,也不是”前缀缓存单项”的收益;本文只做口径标注,没有交叉验证。
- “省掉的算力”不等于”省掉的账单”:第九节的成本模型只用了一家厂商文档里的写读价格做示意,真实账单还受分层计价、批处理折扣与输出计价影响。
- 本文没有做端到端实测:所有毫秒数都是估算;要拿某个数做容量规划,请用自己的模型、序列长度与硬件重算一遍。
十六、小结:五句可带走的话
- 前缀缓存省的是”重算前缀”的浮点运算,不是显存,也不是后续生成步骤。 命中后 KV 仍在显存里,注意力照样要读它。
- 在”prefill 是算力瓶颈、只计线性层”的简化模型里,加速比约等于 , 是可复用 token 占比;本例 ,所以是约 21 倍。参数量在这个式子里被约掉,不代表实测加速与模型无关。
- 用户感受到的 TTFT 会被非 prefill 项摊薄:同一个 21 倍,加上 30 毫秒固定开销后,8B 只剩 5.2 倍、70B 是 15 倍——模型越小,相对收益越小。
- 命中有三条边界:vLLM 只缓存完整块、必须逐 token 相同、变化点之后失效(之前仍可命中)。所以要”稳定在前、易变在后”。
- 成本上按 1.25 倍写入、0.1 倍读取,回本点在复用 1 次之后(),但缓存占的是显存,命中率不是越高越好,而是”留到刚好被下一次命中”最好。
16.1 三步自测:你的场景到底吃到了多少
- 看命中率:读响应里的
cached_tokens除以input_tokens。这是唯一能直接判定”有没有命中”的指标;若为 0,先检查前缀里是否混进了时间戳、请求 ID 或每次都变的检索片段。 - 做一次 A/B(只用于看收益,不用于判命中):同一段 3,000 token 的前缀连续调三次,保持问题与输出设置一致、多次采样后比较第 3 次与第 1 次的 TTFT。TTFT 变小只能说明”收益存在”,不能单独证明是缓存造成的——并发、带宽、实现差异都会影响它。
- 改一处再看:只把”当前时间”从第 1 段挪到最后 1 段,重跑第 2 步。若 TTFT 明显变好,结合第 1 步的命中率变化,就能确认你之前的写法击穿了链式哈希。
三步做完,你手上有的是”命中率 + 收益量级”两个可复核的数字。它们不足以把固定开销从 TTFT 里分离出来——那需要关缓存做对照或读服务端指标。
📋 来源与核验记录
以下来源均在 2026-10-24(北京时间)本轮实际打开核对,逐条标出本文用到的具体内容:
- SGLang 论文:arXiv:2312.07104《SGLang: Efficient Execution of Structured Language Model Programs》,v1 提交 2023-12-12、v2 修订 2024-06-06。核对内容:摘要中的 RadixAttention 与”最高 6.4 倍吞吐”。该数字是作者的整系统实验结果,不是 2026 年的现行性能实测。https://arxiv.org/abs/2312.07104
- LMSYS 官方博客:Lianmin Zheng 等《Fast and Expressive LLM Inference with RadixAttention and SGLang》,2024-01-17。核对内容:基数树保留提示与生成结果的 KV、LRU 淘汰、缓存感知调度、相对 Guidance 与 vLLM”最高 5 倍吞吐”、实测模型 Llama-7B 与 Mixtral-8x7B、硬件 A10G。https://lmsys.org/blog/2024-01-17-sglang/
- Prompt Cache 论文:arXiv:2311.04934《Prompt Cache: Modular Attention Reuse for Low-Latency Inference》,v1 2023-11-07、v2 2024-04-25。核对内容:显式声明可复用片段(prompt module)、位置编码精度处理、TTFT 改善 8 倍(GPU)至 60 倍(CPU)、不改模型参数。https://arxiv.org/abs/2311.04934
- vLLM 官方设计文档:
docs/design/prefix_caching.md(vllm-project/vllm 仓库 main 分支,2026-10-24 读取)。核对内容:块哈希三要素(父哈希 / 块内 token / 额外哈希,含 LoRA ID 与多模态哈希)、“We only cache full blocks”、块大小 4 的两个命中示例、LRU 淘汰与逆序释放、空闲块可被淘汰、“prefix caching is almost a free lunch and won’t change model outputs”、cache_salt与计时攻击说明、“As of v0.11, the default hashing algorithm is sha256”。配套版本信息:v0.11.0 发布于 2025-10-02(GitHub Releases 页)。https://raw.githubusercontent.com/vllm-project/vllm/main/docs/design/prefix_caching.md 、https://github.com/vllm-project/vllm/releases/tag/v0.11.0 - OpenAI 官方文档:Prompt caching 指南(2026-10-24 读取,入口 developers.openai.com)。核对内容:“discounted up to 95%”、“Prompt caching is enabled by default for supported OpenAI models”;GPT-5.6 及之后:写入价 1.25 倍(
"30m"默认)或 2.5 倍("12h");读价与 TTL 无关,多数模型 0.1 倍、GPT-6.1 Sol 0.05 倍;显式断点prompt_cache_breakpoint与”最后一个断点之后不产生写入费用”;用量返回cached_tokens与cache_write_tokens。本次核验中 platform.openai.com 与 developers.openai.com 两个入口的折扣文案不完全一致,本文以 developers 入口的模型级价格为准。https://developers.openai.com/api/docs/guides/prompt-caching - Llama-3 公开 config(本文算例的层数、KV 头数、数据类型来源):
NousResearch/Meta-Llama-3-8B与NousResearch/Meta-Llama-3-70B的config.json(NousResearch 托管的 Meta-Llama-3 镜像,非另训模型)。核对到 8B = 32 层 / 8 个 KV 头 / 32 个 query 头 /torch_dtype: bfloat16,70B = 80 层 / 8 个 KV 头 / 64 个 query 头 /torch_dtype: bfloat16。KV 精度取 config 的 bfloat16;实际部署若用 FP8 等更低精度, 会相应变小。https://huggingface.co/NousResearch/Meta-Llama-3-8B/blob/main/config.json 、https://huggingface.co/NousResearch/Meta-Llama-3-70B/blob/main/config.json
本文自行推导(非上述来源):每 token KV 体积与 128 KiB / 320 KiB、前缀驻留 500 MiB 与 1.22 GiB、整次输入 525 MiB 与 1.28 GiB、prefill 浮点运算量、21 倍()、端到端 30 毫秒示意下的 5.2 倍 / 15 倍、命中率表(50% → 50%、80% → 20%、90% → 10%、95.24% → 4.76%)、天花板 95.24%、三场景可复用比例(95.2% / 83.3% / 19.0%)与对应 prefill 剩余(4.8% / 16.7% / 81.0%)、注意力占比(不计因果减半 13.7%、计减半 6.9%)、显存占比 0.66%、驻留上界 153 份与 122 份、前缀价表(32.5% / 51.7% / 61.3% / 84.3%)、回本点 、以及”有效算力取峰值 40%“这一假设——全部由本文按文内明示假设( TFLOPS、固定开销 30 毫秒、写价 1.25、读价 0.1、H100 80 GB 十进制口径)算出,不是任何厂商公布的实测值。文中出现的其它比例数字(0.05 倍读价对应的 95% 折扣、写入溢价 25%、84% 的前缀费用节省、19% 与 81% 的场景对照、99% 这一自相矛盾的反例、以及 1% 的问题占比假设)同样是本文按上述假设算出的对照值,不构成对外部来源的引用。
未核验项:docs.vllm.ai 与 www.anthropic.com 两个域名在本轮取证时无法访问(curl 返回 000),因此本文未引用 Anthropic 侧的任何价格比例,vLLM 侧内容以仓库中的官方设计文档文件为准(其内容即站点该页的来源);H100 的峰值算力与显存带宽未附官方规格页链接,属本文取用的外部参数。