概念科普 📅 2026-10-24 ✍️ MagicNetWorld 编辑部

同一段长 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)。全文毫秒数都是估算、不是实测,口径与假设都在文内写明。

前缀缓存KV Cache推理性能首 token 延迟vLLMSGLang

除另行标注外,本文引用的外部事实与页面核验均以 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 的阶段,可以并行输入长度 P+QP+Q
decode(解码)从第一个输出 token 起,一个 token 一个 token 往后算的阶段,只能串行每步一个 token
KV cache每层注意力里已经算好的 Key、Value 张量,decode 每一步都要按位置读它们每 token 字节数 kvk_v
前缀(prefix)一次请求里位于最前面、且与其他请求相同的那一段 token 序列可复用长度 PP
新问题部分前缀之后这次真正新增的内容长度 QQ
前缀缓存把已算过的前缀的 KV 留在显存里,下次遇到相同前缀直接复用token 命中率 hh
块(block)KV cache 的分配单位,也是缓存的命中单位,一次对齐若干个 token块内 token 数 BB
TTFTtime to first token,从请求发出到第一个 token 出来的时间TttftT_{\text{ttft}}

几个基础量的白话解释,免得后文卡住:

  • TFLOPS:每秒一万亿次浮点运算。
  • BF16:一种 16 位浮点格式,每个数占 2 字节;模型权重与 KV 常默认用它存。
  • KiB / MiB / GiB:按 1024 进位的存储单位,与厂商宣传里按 1000 进位的 KB/MB/GB 不同。本文显存容量用十进制 GB,KV 体积用 1024 进位,两处都标了单位。
  • dkvd_{kv}:每层里所有 KV 头的维度之和,等于”KV 头数 × 每个头的维度”。

两个符号约定,后面所有推导都依赖它们:参数量记作 NN(8B 模型即 8×1098\times10^{9}),层数记作 LL,每个数占 bb 字节(BF16 为 2)。有效算力记作 FeffF_{\text{eff}},本文统一取 Feff=400F_{\text{eff}}=400 TFLOPS(约等于 H100 SXM 的 BF16 稠密峰值 989 TFLOPS 的 40%,即”峰值打个四折”的乐观假设)。

本文还有一个贯穿全文的量:算术强度,即每搬运 1 字节能做多少次浮点运算。它在这里的作用是提醒——重算前缀是算力开销,复用缓存是显存与带宽开销,两者要用同一把尺子比。

三、前缀到底花了哪一笔钱:prefill 与 KV 两本账

3.1 第一本账:prefill 的浮点运算量

粗略地说,一个稠密 Transformer 每处理 1 个 token,前向大约要做”参数量乘以 2”次浮点运算(一次乘、一次加):

FLOPslinear≈2⋅N⋅(P+Q)\mathrm{FLOPs}_{\text{linear}} \approx 2 \cdot N \cdot (P+Q)

这个式子漏了注意力那一项。注意力里的 QK⊤QK^{\top} 与加权求和,每层代价约 4⋅(P+Q)2⋅dq4 \cdot (P+Q)^{2} \cdot d_{q},其中 dqd_q 是全部 query 头的维度之和(不是 KV 维度,两者在 GQA 下不同):

FLOPsattn≈4⋅L⋅(P+Q)2⋅dq\mathrm{FLOPs}_{\text{attn}} \approx 4 \cdot L \cdot (P+Q)^{2} \cdot d_{q}

以 8B 模型(L=32L=32,32 个 query 头、每头 128 维,故 dq=4096d_q=4096)、P+Q=4,200P+Q=4{,}200 代入,注意力约 9.25×10129.25\times10^{12} 次运算,线性部分约 6.72×10136.72\times10^{13} 次运算。若不考虑因果掩码带来的减半,注意力约占 13.7%;把因果掩码平均折扣一半后约 6.9%。 无论取哪个,注意力在几千 token 量级上都不是主体——所以下面的推导只算线性层,并把注意力当作被忽略的小项。

3.2 第二本账:KV cache 的体积

prefill 的产物之一是每个位置、每一层、每个 KV 头的 Key 与 Value 张量。每个 token 占用的字节数是固定的:

kv=2⋅L⋅dkv⋅bk_v = 2 \cdot L \cdot d_{kv} \cdot b

按 Llama-3 系列公开 config(8B:32 层、8 个 KV 头、每头 128 维;70B:80 层、8 个 KV 头、每头 128 维;两个 config 的 torch_dtype 都是 bfloat16,来源是 NousResearch 托管的 Meta-Llama-3 镜像仓库,见来源段):

kv(8B)=2×32×8×128×2=131,072 字节=128 KiBk_v^{(8B)} = 2 \times 32 \times 8 \times 128 \times 2 = 131{,}072\ \text{字节} = 128\ \text{KiB} kv(70B)=2×80×8×128×2=327,680 字节=320 KiBk_v^{(70B)} = 2 \times 80 \times 8 \times 128 \times 2 = 327{,}680\ \text{字节} = 320\ \text{KiB}

这里要把两种体积分开叫名字,全文不再混用:

  • 前缀驻留体积(P=4,000P=4{,}000):8B 约 500 MiB;70B 约 1.22 GiB。
  • 整次输入体积(P+Q=4,200P+Q=4{,}200):8B 约 525 MiB;70B 约 1.28 GiB。
4,000×128 KiB=500 MiB(8B 前缀驻留)4{,}000 \times 128\ \text{KiB} = 500\ \text{MiB}\quad(8B\ \text{前缀驻留}) 4,200×320 KiB≈1.28 GiB(70B 整次输入)4{,}200 \times 320\ \text{KiB} \approx 1.28\ \text{GiB}\quad(70B\ \text{整次输入})

这两本账是本文的骨架:第一本账(浮点运算)决定”重算有多贵”,第二本账(字节数)决定”缓存要占多少显存”。

四、命中之后省的是什么:把「重算」换成「复用」

先做一个很干净的对比,并明确本节模型的边界:只估被省掉的线性层浮点运算,不建模注意力访存、kernel 启动、调度与排队成本,因此它不是完整时延预测。

没有缓存时,每次请求都要对 P+QP+Q 个 token 做一遍 prefill:

Tprefillmiss≈2⋅N⋅(P+Q)FeffT_{\text{prefill}}^{\text{miss}} \approx \frac{2 \cdot N \cdot (P+Q)}{F_{\text{eff}}}

前缀全部命中时,前缀那 PP 个位置的 KV 已经在显存里,服务端只需对新的 QQ 个 token 做 prefill:

Tprefillhit≈2⋅N⋅QFeffT_{\text{prefill}}^{\text{hit}} \approx \frac{2 \cdot N \cdot Q}{F_{\text{eff}}}

两者相除,得到 prefill 段的加速比:

TprefillmissTprefillhit≈P+QQ=11−r,r=PP+Q\frac{T_{\text{prefill}}^{\text{miss}}}{T_{\text{prefill}}^{\text{hit}}} \approx \frac{P+Q}{Q} = \frac{1}{1-r},\qquad r=\frac{P}{P+Q}

其中 rr 是可复用 token 占整次输入的比例(本文算例里 r=4,000/4,200≈95.24%r=4{,}000/4{,}200\approx95.24\%)。这个式子有一个反直觉的地方:参数量 NN 被约掉了。也就是说,在”prefill 是算力瓶颈、且只计线性层”这个条件下,前缀缓存的比例收益只取决于可复用占比,与模型是 8B 还是 70B 无关——这不代表实测加速不受模型、批大小与硬件影响,只是这个简化模型里 NN 抵消了。

代入本文算例(P=4,000P=4{,}000、Q=200Q=200):

模型未命中线性计算量未命中耗时(@400 TFLOPS,估算)命中后计算量命中后耗时prefill 段比例
8B6.72×10136.72\times10^{13}168 ms3.2×10123.2\times10^{12}8 ms21 倍
70B5.88×10145.88\times10^{14}1,470 ms2.8×10132.8\times10^{13}70 ms21 倍

两行的”21 倍”是同一个数,因为 4,200/200=214{,}200/200 = 21。两行的绝对量差近 9 倍,则来自参数量。

这里必须澄清一个容易误解的点:命中之后,前缀的 KV 并不是”不存在”了——它还在显存里,注意力的计算仍要按位置读到它。所以”命中”省掉的不是显存读写,而是把前缀重新过一遍权重矩阵的浮点运算。至于”重算”与”命中”两条路的注意力访存是否等价,本文不做建模(那取决于 kernel 与分块方式),因此不能把本文的 21 倍当时延预测。

五、为什么「省了 95% 的算力」不等于「快 95%」

这是本文最想让读者带走的一节。

上面算出 prefill 段快 21 倍,对应”线性层计算量只需原来的 4.76%“。但用户感受到的是 TTFT,而 TTFT 不等于 prefill 时间。本文采用下面这个计时边界(不把首 token 的采样重复计入):

Tttft=Tqueue+Tprefill+TnetworkT_{\text{ttft}} = T_{\text{queue}} + T_{\text{prefill}} + T_{\text{network}}

把排队、调度与网络这些”非 prefill 项”粗算成合计 30 毫秒(量级示意,不是任何厂商承诺值;不含首 token 采样本身):

模型未命中 TTFT(估算)命中 TTFT(估算)端到端比例
8B168 + 30 = 198 ms8 + 30 = 38 ms5.2 倍
70B1,470 + 30 = 1,500 ms70 + 30 = 100 ms15 倍

这解释了现场一里那种”感觉快了不少、但也没有二十倍”的体感:模型越小,固定的那 30 毫秒占比越大,缓存的相对收益越小。 反过来,70B 这种 prefill 本身就在秒级的模型,缓存收益几乎直接体现在体感上。

5.1 命中率的定义,与一张不再自相矛盾的表

下面统一采用 token 命中率:账单里 cached_tokens 除以 input_tokens,即 hh。它与”可复用比例” rr 的区别只有一点:hh 描述这一次请求里有多少 token 真的被命中,因此它的上限就是 rr:

h≤r=PP+Q=95.24%h \le r = \frac{P}{P+Q} = 95.24\%

在这个定义下,命中后 prefill 段耗时的比例就是:

Tprefill(h)Tprefillmiss≈1−h\frac{T_{\text{prefill}}(h)}{T_{\text{prefill}}^{\text{miss}}} \approx 1-h
token 命中率 hhprefill 段耗时相当于未命中的说明
50%50%一半输入被复用
80%20%常见的”系统提示稳定、问题每次都变”场景
90%10%长文档问答、RAG 的较好水平
95.24%4.76%本算例的理论上限(PP 全部命中,只剩 QQ 要算)

这张表的关键是最后一行:95.24% 已经是本算例的天花板。 只有把问题缩短(QQ 变小)或把前缀加长(PP 变大),天花板才会抬高;把 hh 写成 99% 是自相矛盾的——那需要 QQ 只占输入的 1%,与本算例的 200/4,200 不符。

5.2 命中率的现实分布:为什么常见值是 80%,而不是 99%

命中率不是靠”开开关”得到的,它由你的输入里有多少比例是每次都不变的决定。三个场景,都按 4,200 token 的输入给出组成:

场景每次不变的部分每次都变的部分可复用比例 rrprefill 剩余
多轮对话(第 3 轮)系统提示 3,500 + 前两轮 500本轮问题 20095.2%4.8%
RAG 问答系统提示与模板 3,500检索片段 500 + 问题 20083.3%16.7%
短系统提示 + 每次换文档系统提示 800文档 3,200 + 问题 20019.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 数是:

Phit=B⋅⌊PmatchB⌋P_{\text{hit}} = B \cdot \left\lfloor \frac{P_{\text{match}}}{B} \right\rfloor

也就是说,块内对齐会吃掉零头。块大小 BB 是配置项(文档示例用 4,实际部署通常更大),BB 越大管理开销越小,但零头浪费越多。这是 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。

同一段前缀被总共用 k+1k+1 次(1 次写入 + kk 次复用)时:

Ccached=1.25+0.1k,Cplain=1+kC_{\text{cached}} = 1.25 + 0.1k, \qquad C_{\text{plain}} = 1 + k
总使用次数(其中复用次数)走缓存的前缀价不走缓存的前缀价省下的比例
1 次(0)1.251.00−25%(更贵)
2 次(1)1.352.0032.5%
3 次(2)1.453.0051.7%
4 次(3)1.554.0061.3%
20 次(19)3.1520.0084.3%

回本条件是 1.25+0.1k<1+k1.25 + 0.1k < 1 + k,即:

k>0.250.9≈0.28k > \frac{0.25}{0.9} \approx 0.28

按这组价格,成功复用一次就已经回本。 工程含义很直接:一个前缀只被用一次,写入溢价让你多付 25%;会被第二次用到,就开始划算。所以真正要问的不是”要不要开缓存”,而是”哪些内容会被复用第二次”。注意这只对”该前缀的输入费用”成立:整张账单还受新增问题、输出 token、分层计价与批处理折扣影响。

十、缓存省不掉的三件事

这一节是为了防止过度期待。

省不掉的东西为什么省不掉量级参考(8B、P=4,000P=4{,}000)
后续生成步骤缓存只作用于输入侧;decode 每一步仍要按位置读历史 KV,历史越长读得越多与本文算例无关,需按上下文长度另算
显存占用缓存就是把 KV 留在显存里,命中率越高、留得越多前缀驻留约 500 MiB(70B 约 1.22 GiB)
输出 token 的计价缓存只作用在输入侧,输出照常按 token 计费由生成长度决定

其中第二条最容易被忽略:前缀缓存的本质是”用显存换算力”。它把”每次重算前缀”的浮点运算,换成”把前缀 KV 留在显存里”。因此它与”提高并发批大小”存在竞争关系——显存就那么多,缓存占多了,能同时驻留的请求就少了。这也是托管服务用 TTL 与命中率仪表盘管理这件事的原因:缓存不是越多越好,而是”留到刚好被下一次命中”最好。

10.1 一条能立刻用的经验式

设显存总量 MM、权重占 MwM_w、一份前缀驻留 kv⋅Pk_v \cdot P,那么”整个 KV 池只放缓存、不留任何并发”时的驻留份数是:

nmax⁡=M−Mwkv⋅Pn_{\max} = \frac{M - M_w}{k_v \cdot P}

代入 8B 算例(M=80M=80 GB 十进制、Mw=16M_w=16 GB、kv⋅P≈500k_v\cdot P \approx 500 MiB):

nmax⁡=80−160.524≈122n_{\max} = \frac{80-16}{0.524} \approx 122

这个数字要说清边界:它只是”能同时驻留多少份前缀”的上界,不等于”并发上限”。原因见 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(16×⌊3500/16⌋16 \times \lfloor 3500/16 \rfloor)。差 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。

第一本账:算力。 未命中 6.72×10136.72\times10^{13} 次浮点运算,命中后 3.2×10123.2\times10^{12} 次;省掉的量是:

2⋅8×109⋅4,000=6.4×10132 \cdot 8\times10^{9} \cdot 4{,}000 = 6.4\times10^{13}

也就是那 4,000 个前缀 token 的线性层运算。

第二本账:显存。 前缀驻留 500 MiB,占一张 80 GB H100 的约 0.66%(十进制 GB 口径)。若整张卡只放缓存、不放权重、无碎片,可驻留:

80×109500×220≈153\frac{80\times10^{9}}{500 \times 2^{20}} \approx 153

份(向上取整);扣掉 16 GB 权重后约 122 份(与 10.1 节一致)。两者都是上界而非配额:空闲缓存块可被淘汰让位,所以它描述的是”缓存能占多少显存”,不是”并发会被压到多少”。

第三本账:钱。 同一前缀总共用 20 次(1 次写入 + 19 次复用):走缓存的前缀价约 1.25+0.1×19=3.151.25 + 0.1\times19 = 3.15,不走缓存是 20,前缀输入费用省约 84%。

第四本账:时间。 prefill 168 毫秒 → 8 毫秒;若端到端还有 30 毫秒非 prefill 开销,则是 198 毫秒 → 38 毫秒。

四本账放在一起,结论很朴素:前缀缓存是一笔”用约 0.66% 的显存,换掉 95.24% 的 prefill 线性计算量”的交易,前提只有一个——那段前缀真的会被第二次用到。

十四、为什么前缀缓存不改变输出(以及三处例外)

很多人担心”复用缓存会不会算错”。把机制拆开看,这件事可以在数学层面说明,而不是靠”实测没发现问题”。

对位置 ii 的那个 token,注意力输出是:

Attn(xi)=∑j≤isoftmax ⁣(qi⋅kjd)vj\mathrm{Attn}(x_i) = \sum_{j \le i} \mathrm{softmax}\!\left(\frac{q_i \cdot k_j}{\sqrt{d}}\right) v_j

其中 qiq_i 由位置 ii 的表示算出,而 kj,vjk_j, v_j 由第 jj 个位置的该层输入表示算出——该表示本身依赖它之前的全部上下文。所以准确的表述是:在”完整前缀、权重与相关配置(位置编码、LoRA、多模态输入)都相同”且理想精确计算的前提下,复用与重算得到同一组 KV。 这也是 vLLM 文档敢写”prefix caching is almost a free lunch and won’t change model outputs”的依据。

三处例外,都得说清:

  1. 浮点运算不满足结合律。 同一批数用不同累加顺序(不同 kernel、不同分块、不同批大小)求和,末几位可能不同。所以”理论上一致”在实践中通常是”分布一致、逐位可能不同”。业务上通不通过要按你的场景验,本文不给”无影响”的承诺。
  2. 随机采样本身就会让相同请求返回不同文本。 两次调用结果不同,未必与缓存有关,也可能只是采样随机性——先核对输出设置,再谈缓存。
  3. LoRA 与多模态输入必须进哈希。 vLLM 文档的”额外哈希”里明确列了 LoRA ID 与多模态输入哈希:同一基座挂不同 LoRA,KV 不同;图片内容不同但占位 token 相同,KV 也不同。漏算就会出现”看起来同一段文本、实际复用了错误 KV”的静默错误。这说明”额外哈希”看着像实现细节,其实是正确性的一部分。

十五、边界与未解

  1. “prefill 是算力瓶颈、只计线性层”这个前提有边界。 本文所有”约 21 倍”都建立在计算量除以有效算力这个模型上。当块很小、或显存带宽被打满时,prefill 也会接近带宽瓶颈,此时实际收益低于本文估算。
  2. 有效算力 400 TFLOPS 是假设,不是实测。 真实值取决于实现、批大小、序列长度与并行切分;本文用它只是为了把两本账放在同一把尺子上。换掉假设,结论的结构不变(比例项仍是 1/(1−r)1/(1-r)),绝对值会变。H100 的峰值与带宽属外部参数,本文未在来源段附硬件规格页,取用时请自行核对。
  3. 30 毫秒固定开销同样是量级示意。 真实系统里它包含排队、调度、KV 块分配、网络往返与前端渲染。
  4. 跨副本共享缓存(多个推理副本共用一份前缀 KV)本文没有展开,它涉及一致性、网络传输与失效广播。
  5. 厂商口径与第三方实测的差别:SGLang 论文 6.4 倍与 LMSYS 博客 5 倍都是项目作者报告的整系统吞吐,不是独立第三方复测,也不是”前缀缓存单项”的收益;本文只做口径标注,没有交叉验证。
  6. “省掉的算力”不等于”省掉的账单”:第九节的成本模型只用了一家厂商文档里的写读价格做示意,真实账单还受分层计价、批处理折扣与输出计价影响。
  7. 本文没有做端到端实测:所有毫秒数都是估算;要拿某个数做容量规划,请用自己的模型、序列长度与硬件重算一遍。

十六、小结:五句可带走的话

  1. 前缀缓存省的是”重算前缀”的浮点运算,不是显存,也不是后续生成步骤。 命中后 KV 仍在显存里,注意力照样要读它。
  2. 在”prefill 是算力瓶颈、只计线性层”的简化模型里,加速比约等于 1/(1−r)1/(1-r),rr 是可复用 token 占比;本例 r=95.24%r=95.24\%,所以是约 21 倍。参数量在这个式子里被约掉,不代表实测加速与模型无关。
  3. 用户感受到的 TTFT 会被非 prefill 项摊薄:同一个 21 倍,加上 30 毫秒固定开销后,8B 只剩 5.2 倍、70B 是 15 倍——模型越小,相对收益越小。
  4. 命中有三条边界:vLLM 只缓存完整块、必须逐 token 相同、变化点之后失效(之前仍可命中)。所以要”稳定在前、易变在后”。
  5. 成本上按 1.25 倍写入、0.1 倍读取,回本点在复用 1 次之后(k>0.28k>0.28),但缓存占的是显存,命中率不是越高越好,而是”留到刚好被下一次命中”最好。

16.1 三步自测:你的场景到底吃到了多少

  1. 看命中率:读响应里的 cached_tokens 除以 input_tokens。这是唯一能直接判定”有没有命中”的指标;若为 0,先检查前缀里是否混进了时间戳、请求 ID 或每次都变的检索片段。
  2. 做一次 A/B(只用于看收益,不用于判命中):同一段 3,000 token 的前缀连续调三次,保持问题与输出设置一致、多次采样后比较第 3 次与第 1 次的 TTFT。TTFT 变小只能说明”收益存在”,不能单独证明是缓存造成的——并发、带宽、实现差异都会影响它。
  3. 改一处再看:只把”当前时间”从第 1 段挪到最后 1 段,重跑第 2 步。若 TTFT 明显变好,结合第 1 步的命中率变化,就能确认你之前的写法击穿了链式哈希。

三步做完,你手上有的是”命中率 + 收益量级”两个可复核的数字。它们不足以把固定开销从 TTFT 里分离出来——那需要关缓存做对照或读服务端指标。

📋 来源与核验记录

以下来源均在 2026-10-24(北京时间)本轮实际打开核对,逐条标出本文用到的具体内容:

  1. 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
  2. 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/
  3. 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
  4. 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
  5. 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
  6. 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 等更低精度,kvk_v 会相应变小。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 倍(1/(1−r)1/(1-r))、端到端 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%)、回本点 k>0.28k>0.28、以及”有效算力取峰值 40%“这一假设——全部由本文按文内明示假设(Feff=400F_{\text{eff}}=400 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 的峰值算力与显存带宽未附官方规格页链接,属本文取用的外部参数。