缓存命中了,账单没便宜:KV cache 计费按前缀还是按真复用

同一段 prompt 在两家服务上开出不同账单,原因通常不是单价,而是「命中」这个词被换成了两个不同的量。一个是文本层能比对出来的可复用 token 数,另一个是服务端这一次真正跳过重算的 token 数。前者可以在纸面上算出来,后者要等调度器分配完块才知道。计费取哪一个,决定同一份命中的两张账单。

同一份命中,两张账单:分歧在计费基数,不在单价

同一份命中在两张账单上不同,不是单价不同,而是「命中多少」被换成了两个不同的量。这条分歧的原始出处只是一条个人观察,不是任何厂商的条款。

这条观察来自 Horace He 在 2026-10-28 发的一条推文,账号 @cHHillee:a lot of inference providers seem to charge on *realized* kv cache hits? 以及 Tinker charges off of theoretical kv cache hits (based off of a 1 hour limit)。他明确说自己更认同 Tinker 与 Anthropic 那一侧,理由是反直觉的:按 realized 计费时,引擎命中越多厂商收入反而越少。这句话要按它本来的证据等级使用,作者隶属 Thinking Machines,而 Tinker 正是这家公司的产品,他在推文里夸的是自己雇主的口径,这不是第三方评测。推文也没有点名任何一家按 realized 计费的厂商,原文只是 seem to 的泛指cHHillee。

「based off of a 1 hour limit」这句在 Tinker 的公开定价页与文档里查不到对应措辞。官方定价页只写到 cached prefill 是 input tokens reused from the prompt cache cost 80% less,也就是缓存价等于 prefill 价的 20%,而 TTL、块粒度、最小可缓存长度都没有公布Tinker。真正把命中判定机制讲清楚的是另一处:Tinker 维护者在 2026-04-06 说明服务端有一层 prefix caching,做的是 longest common prefix matching,所以共享前缀即使尾部变化也能命中。这属于维护者说明,不是定价页或文档里的条款。这句话的分量在于,判定按文本前缀进行:维护者只说明共享前缀在尾部变化时仍能命中;至于匹配到的前缀是否一定被实际读到,官方未公布Tinker Cookbook。

把两种基数并排放在一起,分歧的位置就很清楚:

路径 A:按文本层比对路径 B:按服务端真读到
打折基数请求文本与已有前缀比对出的可复用 token 数这一次真从缓存读到、真跳过 prefill 的 token 数
谁在报这个数厂商自己算,例如服务端最长公共前缀匹配厂商自己算,只有它知道调度器跳过了什么
能否脱离运行时算出能,只依赖文本、断点与历史写入不能,依赖块对齐、驱逐、副本与路由
用户能否验证能自己复算到 token 级只能读厂商给的字段

这条推文自己的讨论串里当天就出现了反向论证:有回复认为服务商其实有动机尽量提高 realized 命中,因为省下的算力可以拿去跑 decode,而 decode 才是收入主体。这条反驳是否成立不在本文的取证范围内,但它提醒一件事,「厂商命中越多收入越少」是一条逻辑推论,不是实测结论。

理论命中能在纸面上算出来:它只认文本、断点和历史写入

理论命中之所以能「算」,是因为它只依赖请求文本、断点位置和历史写入。也正因如此,它给出的是上界,不是账单。

最完整的一份官方机制写在 Anthropic 的 prompt caching 文档里,三条规则各自都能独立复算。第一,缓存写入只发生在断点:标记一个块只会写一条条目,内容是截至该块的前缀哈希,更早的位置一条都不写,而且哈希是累积的,断点之前任何一块变化都会让下一次请求算出不同的哈希。第二,读的时候从断点开始逐块向前回溯,找的是「之前的请求写过的条目」,不是「看起来稳定的内容」。第三,每个断点最多检查 20 个位置,断点本身算第一个Anthropic。

20 个位置这个上限不是修辞,它会直接把命中打成零。文档自己给了一组三轮示例:

轮次总块数断点检查范围结果
第 1 轮10block 10无既有条目在 block 10 写入
第 2 轮15block 15从 15 回溯到 10在 block 10 命中,只处理 block 11 到 15
第 3 轮35block 35block 35 到 16,共 20 个位置未命中,第 2 轮写在 block 15 的条目差一格落在窗口外

第 3 轮的补救办法也在文档里:在 block 15 再放一个断点,就多开一个回溯窗口,第 2 轮的条目立刻能用。这条机制解释了为什么「理论命中」可以脱离运行时:给定请求文本、断点位置和此前的写入历史,任何人都能在纸面上复算出这次最多能命中多少 token。同一份文档还给出两个把理论口径接回现实的门槛,最小可缓存长度按型号分 512、1,024、2,048、4,096 四档,低于门槛的请求不缓存也不报错;TTL 默认 5 分钟,可选 1 小时,而缓存读按写入口径的有效期计价Anthropic。

理论命中的上限还先被负载形态决定。真实工作负载里会被共享的前缀只有几种固定形状,few-shot 示例、自一致性采样里的同一个问题、多轮对话历史、树搜索的历史分支。下图把这四种形状按可共享与不可共享的部分分开画了出来:

图注:四种典型负载下可共享的前缀形态,蓝色是可共享的 prompt 部分,绿色不可共享,黄色是模型输出。它说明「公共前缀」不是抽象概念,而是 few-shot 示例、自一致性问题、对话历史、搜索历史这几种结构;命中的上限先由这些结构决定,再看服务端兑现多少。图中不含任何命中率或延迟数值。SGLang

和 Anthropic 的哈希加断点路线不同,Tinker 公开的机制说明只有维护者的一句「longest common prefix matching」:命中由服务端按文本前缀匹配决定;断点、回溯窗口、显式 TTL 这些机制官方文档都没有公布。两家都把打折基数绑在「命中前缀」上,但把理论口径接回现实的方式完全不同,所以拿一家的机制去推另一家的账单是不成立的。

实际命中在调度那一步缩水:整块才算,队头一弹就被驱逐

理论命中是上界,实际命中是折扣后的实现结果,折多少主要看前缀工作集与 KV 容量之比,不看并发和输出长度。

缩水发生在引擎的块管理上,vLLM 的设计文档把三个动作写得很清楚。块的身份是一个哈希,由父块哈希、本块内的 token 元组,以及 LoRA ID、多模态输入哈希、多租户隔离用的 cache salt 这类额外值组成。缓存粒度只有一句话:We only cache full blocks. 命中判定是一次哈希查表,发生在调度器为新请求分配块之前。而查表结果要到分配时才兑现,分配会先把命中块 touch 一遍、从空闲队列里摘出来,再从队头弹块,如果队头恰好是缓存块,就当场把它驱逐vLLM APC。

「只缓存整块」的代价可以直接算。文档的 Time 3 例子里,新请求带 14 个 prompt token,前 10 个和上一个请求相同,但第 3 块只对上 2/4 个 token,于是只有前 2 块、共 8 个 token 命中。10 个 token 的文本级公共前缀,兑现成 8 个 token 的实际复用,差的那 2 个 token 让整块作废。

先看清命中要查的那张表和驱逐要弹的那条队列,时序才好读:

图注:vLLM 前缀缓存的四个组件。上方是 Block Pool 双向链表,块内带 ID、Hash、Prev、Next;左侧空闲队列只存 Head 与 Tail 两个指针;底部两张字典,Cache Blocks 记哈希键到 block ID,Request Blocks 记请求 ID 到已分配的 block ID。命中查表查的就是 Cache Blocks 这张映射,驱逐动的是空闲队列。图中没有哈希链与驱逐箭头。vLLM APC

下一次分配就把上界打了折:

图注:一次分配之后的三区状态。命中块 0、1、2 标为 cached 并被 touch 保留,未命中的块 7、8、9、4 按序弹出,队头的缓存块 3 标为 evicted,空闲块只剩 6 与 5。上一轮还成立的前缀,这一轮分配就被驱逐了。图中的块大小只是示例设定,evicted 也不等于这段前缀永久失效。vLLM APC

缩水的幅度有实测区间。PrefixBench-H100 在单张 94 GB H100 NVL 上让 vLLM 0.21.0 与 TensorRT-LLM 1.2.1 跑同一套负载,8,192 token 共享前缀处,复用开启相对关闭的 p50 TTFT 加速是 vLLM 6.51×、TensorRT-LLM 5.26×。这两个数是延迟收益,不是账单节省比例。更关键的是支配变量:并发从 1 拉到 32,命中率总降幅不到 3 个百分点。输出长度从 32 到 512,p50 TTFT 最多动 1.2 ms。真正让命中塌掉的是前缀工作集与 KV 容量之比,13.2× 时两个运行时的命中率分别是 5.6% 与 6.6%,把 7B 的 KV 池人为压到 56,320 token 后,两边同时从 68.2% 掉到 3.6%PrefixBench。

图注:并发为 1 时,复用开启后 p50 TTFT 随共享前缀长度的变化,两条曲线分别对应 vLLM 与 TensorRT-LLM。两条线几乎贴合,差值约恒定在 8 ms,说明差距不在缓存本身。图里只有复用开启的两条线,345 ms 与 306 ms 的复用关闭基线来自论文表格,不能读成图中那条曲线;图也读不出命中率。PrefixBench

换更聪明的驱逐策略救不回来。一项用两家公司生产轨迹、14 种驱逐算法做对照的研究给出的结论是,为传统缓存设计的复杂策略相对 LRU 几乎无增益,frequency 类反而明显更差,原因是结构性的:前缀复用被活跃会话的规律节奏主导,使 recency 异常地可预测Eviction。SGLang 的 RadixAttention 路线(2023 年论文提出)把 KV cache 组织成基数树、按匹配长度排序调度,但前缀一致性的要求和驱逐压力一样存在,数据结构不同,落空的机制同类。

条款把折扣绑在真读到的量上,市场层再把这条界线变成套利

条款把折扣绑在真读到的量上,于是命中一旦在副本、块边界或路由上落空,账单就自动回到标准价;市场层把同一条界线变成报少命中反而多赚的套利空间。

判断一家厂商的打折基数取的是哪一侧,不需要通读定价页,只需要看它把折扣挂在哪个动词上。Groq 写的是 Pricing discount will only apply on successful cache hits。Bedrock 写的是 tokens successfully read from cache are reported as cached tokens and billed at the model's cache-read rate,没读到的按标准输入价计费。DigitalOcean 写得更直白,多副本环境里同一段文本落到另一台副本上就是 miss,高负载时为了保 TTFT 主动把请求路由到没有缓存的副本,那次请求按标准输入价计费并重新填充缓存DigitalOcean。六家的条款并排看是这样:

厂商打折基数措辞命中字段未命中怎么算
Groq折扣只作用于成功命中cached_tokens按标准输入价
Bedrock成功从缓存读到的 token 才按缓存读价cacheReadInputTokens / cached_tokens未读到的按标准输入价
DigitalOcean缓存绑定具体副本cache_read_input_tokens路由到无缓存副本按标准价并重新填充
Cloudflare跳过已缓存部分的 prefillusage 里的缓存计数未跳过则按标准价
Gemini 4新增不足 2,050 token 时重算上一请求最后 2,050 tokentotal_cached_tokens按标准输入价
Cerebras缓存与新鲜 token 同价cached_tokens同价,不构成口径分歧

有两行值得单独说。Gemini 4 的 2,050 token 重算是全篇唯一一条由厂商自己把「理论前缀」切掉一截、把剩下部分按标准价重算的显式规则,它等于把「理论命中是上界」写进了合同。Cerebras 则根本不打折,缓存 token 与普通 token 同价,所以它不能算作分歧样本,把它算进去就会得出「所有服务商都按实际命中计费」这种错误概括。另外,块粒度与最小可缓存长度是两件事,不能混:Cerebras 的块是 128 token,DigitalOcean 最高 256 token,Groq 的最小长度是一个 128 到 1,024 的区间,Bedrock 按断点分 512、1,024、4,096,Gemini 是 2,048 或 4,096,Anthropic 四档从 512 到 4,096。门槛决定理论命中能不能成立,对齐单位决定成立之后能兑现多少。

字段名比措辞更容易骗人,看粒度比看名字可靠。Tinker 同时暴露两个读数,请求级的 prompt_cache_hit_tokens 是一个整数,而账单事件里的 cached 是一个布尔值,配一个 token 计数,粒度是 UTC 小时桶。Anthropic 用三段式,cache_read_input_tokens、cache_creation_input_tokens 与断点之后的 input_tokens 相加才是总输入。Mistral 的 cached_tokens 是 64 对齐的服务端读数。三个都叫「命中」,能回答的问题完全不同。

市场层把这条界线变成了利益结构。聚合平台暴露给用户的审计字段就是上游 provider 自报的 cached_tokens 与 cache_write_tokens,而这同一个数字同时喂给路由打分和用户结算OpenRouter。链路是单向的,验证缺口在链条最根部:

图注:单向链路层层自报:验证缺口卡在复用与上报两处

第三方研究确实在 2026-08 的市场快照里测到了这个缺口:provider 降价后,把重复文本里少 71 个百分点算作已缓存,少传 52 个百分点的暖缓存节省,约 5 次重复查询之后最激进的降价者反而比直接用模型厂商端点更贵Caching Cheaters。这份研究要带着三重限定读:单方发布且非同行评审、作者有投资披露、只覆盖约十分之一 token 流量,而且作者自己承认分不清是蓄意清缓存还是改价重启导致缓存丢失。所以它能支撑「这条路在结构上走得通」,不能支撑任何一家 provider 在作弊,本文也就不点名。

到这里需要划一条容易混淆的界线。另有一类研究在 vLLM 部署、H100 硬件上把同一笔共享 pod 账单按 token 计量和按等时间份额计量各切一遍,检索密集型租户的账单份额相差 11.7 到 13.7 个百分点,而同期 GPU 利用率读数是 97% 到 99%Who Pays。这是相邻但不同的一层:它说的是已发生的算力按什么规则分给租户,与「命中按哪个基数打折」不是同一件事,不能拿来解释缓存口径差异。

1T MoE 的 day-0:本地命中率 17–33%,账单只认 64 对齐的服务端读数

超大 MoE 的 day-0 把口径问题压成部署选择:本地命中率是引擎侧读数,账单是厂商侧 64 对齐读数,两者之间还隔着副本、路由与未公布的 TTL。

2026-10-29 发布的 Mistral Large 4 是个合适的现场。约 1T 总参数、49B 激活、1M token 上下文,原生多模态 MoE,FP8 与 NVFP4 两个版本,vLLM 与 SGLang 同日给出 day-0 支持,完整技术报告尚未发布。参数口径在官方材料内部就不一致,Mistral 文档写 52B 激活、1.05T 总参,Hugging Face 博客与 vLLM 博客写 49B 激活、1T 总参,并把 52B 解释为含 embedding 与输出头,引用时按出处并列才准确HF Blog。

图注:Mistral Large 4 的整体形态。文本走 tokenizer 与 embedding,图像走 Pixtral 编码器加投影层,两路合并成 6,144 维 embedding、约 1M token 的上下文,接 72 层 decoder,层内是 RMSNorm 加 MLA 与稀疏注意力,以及 RMSNorm 加 dense 与 MoE 的 FF 分支,末端是 logit soft cap 与输出头。这张图不含 KV 缓存位置、命中判定或任何计费字段,不能用它推断跨层 KV 共享。HF Blog

这个模型改变的是「可复用 KV」本身的含义。每个 query 只从压缩后的 KV memory 里选最多 2,048 个位置读取,KV 被压成 512 维加 64 维位置键的 latent 形式,另外 63/72 层持有 KV cache,9 个 NoPE 层复用 layer 35 的 latent。缓存放不放得下、命中能不能兑现,都跟着这套结构变:

图注:稀疏注意力的读取路径。当前 token 分别送入 Query 与 Indexer,Indexer 选出最多 2,048 个位置,再从 512 维加 64 维位置键的压缩 KV memory 里读取这些条目的 KV,Query 与被选 KV 一起进入注意力计算。它解释的是复用边界为什么和 dense 模型不同,图里没有跨层共享的层号,也没有任何容量或命中率数字。HF Blog

两边的读数在同一份部署里并存,量的却不是同一件事。厂商侧,usage.prompt_tokens_details.cached_tokens 必为 64 的倍数,少于 64 token 的 prompt 不会有命中,缓存输入按标准输入价的 10% 计,$0.07 对 $0.7 每百万 token,prompt_cache_key 只提高命中机会不保证命中,TTL 未公布。引擎侧,vLLM 官方 recipe 的 Agentic 预设跑 AgentX 的 393 条会话,前缀缓存默认开启并配会话粘性路由,约 32 会话时 decode 节点 KV 耗尽,本地命中率只有 17% 到 33%,TTFT p50 升到 14.95 s,把 decode 节点换成 DCP4 后回到 0.72 s。不做会话粘性路由,命中率再掉 14 到 22 个百分点vLLM Recipe。

17% 到 33% 不是账单折扣率,它是引擎侧本地计数器在特定负载下的读数,账单是厂商侧 64 对齐的那个字段。把前者当后者,就会得出「账单会按命中率打折」这种既不对也无法验证的结论。至于算账口径本身值多少钱,TurnCost 给了一个可引用的量级:600 条 coding-agent 轨迹里 98.6% 的输入 token 来自前缀缓存,在近理想缓存假设下,扁平输入定价把循环成本高估 6.5 到 7.8 倍TurnCost。

图注:同一批轨迹在两种记账口径下的成本对比,橙色是扁平输入定价,蓝色是缓存感知定价,柱上标的是两者倍数,四组分别是 7.4×、6.8×、6.5×、7.8×。这是论文在近理想缓存假设下的模拟结果,不是任何厂商的真实账单;缓存效率降到 90% 时倍率掉到 4.0×。图里没有命中率轴。TurnCost

落到一次采购或一次部署上,能问的问题只有三个,每一个都能对着文档原文核对:

  1. 打折基数是文本级还是服务端真读到的量。 找 successful、successfully read、skipped prefill、billed at the standard input rate 这类措辞,看折扣挂在命中上还是挂在读到上。
  2. 命中字段是每请求、请求级 token 还是聚合桶。 请求级整数能逐条对账,小时桶的布尔值只能告诉你这一桶整体按哪个价计,三段式则要求你把三个字段加起来才是总输入。
  3. 命中是否跨副本、跨路由生效。 副本绑定与路由策略会把「文本完全一样」变成 miss,而这一点通常写在缓存文档而不是定价页里。

参考文献

  1. @cHHillee(Horace He)推文:theoretical vs realized KV cache hits — https://x.com/cHHillee/status/2115479523697500543
  2. Tinker Pricing — Models & Pricing — https://thinkingmachines.ai/tinker/pricing/
  3. Anthropic Prompt Caching 文档与定价 — https://platform.claude.com/docs/en/build-with-claude/prompt-caching
  4. 实际口径厂商缓存计费条款合集(DigitalOcean / Groq / AWS Bedrock / Cloudflare / Cerebras / Google Gemini) — DigitalOcean:https://docs.digitalocean.com/products/inference/how-to/use-prompt-caching/;Groq:https://console.groq.com/docs/prompt-caching/;AWS Bedrock:https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html;Cloudflare:https://developers.cloudflare.com/workers-ai/features/prompt-caching/;Cerebras:https://inference-docs.cerebras.ai/capabilities/prompt-caching/;Google Gemini:https://ai.google.dev/gemini-api/docs/caching
  5. OpenRouter Prompt Caching 文档 +《Caching Cheaters on OpenRouter》 — OpenRouter 文档:https://openrouter.ai/docs/features/prompt-caching;《Caching Cheaters on OpenRouter》(2026-08):https://robvc.com/research/caching-cheaters
  6. vLLM Automatic Prefix Caching 设计文档 — https://docs.vllm.ai/en/latest/design/prefix_caching/
  7. SGLang: Efficient Execution of Structured Language Model Programs(RadixAttention) — arXiv:2312.07104
  8. PrefixBench-H100: Characterizing Prefix Reuse and Time-to-First-Token in H100 LLM Serving — arXiv:2609.19657
  9. When Fancy Eviction Fails: Rethinking Cache Replacement For LLM Prefix Reuse — arXiv:2609.28870
  10. Who Pays for the KV Cache? Attributing Shared AI Inference Spend Across Kubernetes and LLM Provider Bills — arXiv:2609.24991
  11. TurnCost: Turns, Not Observations: Cache-Aware Loop Engineering for Coding Agents — arXiv:2610.15093
  12. Mistral Large 4(Le Chonk)发布与 vLLM / SGLang day-0 支持官方资料 — https://mistral.ai/news/mistral-large-4-oss/
缓存命中了,账单没便宜:KV cache 计费按前缀还是按真复用