小王问,大王答 · 一场关于模型记忆的追问

一个 token 的缓存,
如何通向
一份共享的记忆?

从 KV Cache、前缀命中和 Claude 的缓存点,一路追问到 YOCO、共享状态与稀疏读取。用 22 个问答,把“存得下”和“算得起”放进同一幅图。

小王 × 大王22 个问答 · 17 张机制图4 个离线实验2026 · 09 · 10
THE QUESTION BEHIND THE QUESTION
同一段历史,可以在哪些地方少算一次?ABCDE新问题已计算的上下文状态KV memory继续计算找到它prefix → state让它更小shared global KV服务系统的优化模型架构的优化

读完全文,要能分清:身份、复用、状态体积,以及每一步读取的代价。

阅读目录 · 6 个章节 · 22 个问答

小王:“计算好一个 token,就保存一个 token。以后用 hash 找回来,为什么还需要那么多缓存规则?”

大王:“这个方案可以成立。接下来要分别回答:存下了哪些状态,索引怎样组织,旧状态能否恢复,以及每次还要读多少历史。”

小
小王 · 追问者

不满足于术语,追问每一步究竟能不能实现。

大
大王 · 讲解者

用计算、代码边界和反例,把问题一层层拆开。

我们的讨论从 DeepSeek V4.1 Flash 的效率出发,经过 Claude API 的 cache_control,碰到一个关键反问:前缀身份明明可以用 hash 表示,为什么总说“必须包含整个前缀”? 再向前走,YOCO 把问题推进了一层:假如每次都要保存很多层的历史,能否让这些层共用一份?

输入 token / 表示已缓存或共享的状态新计算 / 需要重算hash、目录或恢复入口

图中的 A、B、“苹果”等均为教学用 token;实际中文如何分词,取决于模型的分词器。模拟数值与真实 API 指标会明确区分。

01 / COMPUTATION · 从计算开始

先弄清:KV 到底是什么?

缓存的价值来自少做重复计算。理解这件事,需要先区分“读入已有输入”和“生成新的内容”。

01
小王 · 问

为什么评价一个新模型,会一路聊到缓存?

大王 · 答

设想一个会读代码、调用终端并修复错误的助手。第一次给它项目资料,第二次带回测试结果,第三次继续修改。同一份工具说明、项目约定和历史对话,会反复出现在请求中。

普通问答的直觉

输入一次 → 得到一段回答

人容易只关注回答是否聪明、每秒输出多少字。

→
多轮任务的现实

反复带回历史 → 多次计算 → 验收

同一份上下文的重复处理成本,也会影响任务能否做得起。

因此,看待 Flash 类模型,一个有用的问题是:在达到同样验收标准的前提下,完整任务花了多少钱、多久,失败后还要重试几次? 更低的输入处理成本可能改变这个答案。

02
小王 · 问

KV Cache 保存的,究竟是什么?

大王 · 答

先用大白话说:模型读过一个位置后,会留下方便后续位置查询的信息。注意力机制里,K(Key)参与决定“关注哪个位置”,V(Value)提供被读出的内容;Q(Query)表示当前这次查询。 它们都是数字向量,不是原文、回答摘要或普通数据库记录。[1]

FIG 01Q、K、V:一次“带着问题读取历史”的过程
↔ 左右滑动查看整张图
Q、K、V:一次“带着问题读取历史”的过程只画一个注意力层、一个注意力头的结构。真实模型有多个头和多层;图省略了缩放、位置编码、归一化与残差等细节。已有 token:先变成向量,再产生每层的 K / V我K1 / V1喜欢K2 / V2苹果K3 / V3当前要处理的 tokenQ 查询匹配 K,再加权读取 VK:帮助决定“该看谁”V:被读取、混合的信息
只画一个注意力层、一个注意力头的结构。真实模型有多个头和多层;图省略了缩放、位置编码、归一化与残差等细节。 [1]
Attention(Q, K, V) = softmax(QKᵀ / √d) V

先计算当前查询与各个 Key 的匹配程度,再按权重汇总 Value。

一次调用通常包含两种工作:Prefill 是处理已经给定的一长段输入,通常能批量、并行地计算许多位置;Decode 是在通常的自回归生成中,根据前面的结果继续产生下一个 token。缓存让后续计算可以直接使用已生成的 K/V,免去反复编码旧输入。[1]

这里的“每个 token 有对应状态”,描述的是逻辑上的对应关系。它不意味着 Prefill 要把整段输入逐 token 串行运行,也不意味着每个 token 都进行一次独立的 GPU 内存分配。

03
小王 · 问

“KV 相同就命中”为什么还不够准确?

大王 · 答

把这句话作为复用的目标没有问题。但服务端需要在重新计算 KV 之前,就判断能否复用。常见方法是给输入前缀建立可快速查找的身份,例如前缀树或 hash,再去找对应的缓存状态。[2]

FIG 02查找发生在昂贵的模型计算之前
↔ 左右滑动查看整张图
查找发生在昂贵的模型计算之前如果先把全部 KV 重新算出来再比较,就已经支付了主要计算成本。匹配依据来自输入身份与相关配置。新的输入前缀先识别输入与运行配置计算缓存键便宜的 hash / 前缀查找查询:状态还在吗?找到可用状态加载已经计算好的 KV然后只处理未命中的部分未命中:执行模型计算按规则留下新缓存
如果先把全部 KV 重新算出来再比较,就已经支付了主要计算成本。匹配依据来自输入身份与相关配置。 [2]

对于固定权重、相同输入表示、位置与注意力规则的因果模型,相同前缀为精确复用提供了一个容易检验的充分条件。实际系统还要考虑模型或适配器身份、多模态内容,以及租户隔离等条件。[3]

充分条件不等于数学上唯一可能的条件。 某些不同输入也可能碰巧产生相同的中间值,特殊模型还可能有更强的状态等价关系。但普通前缀缓存无需证明所有等价情形;它选择一种容易保证正确的查找规则。

02 / IDENTITY · 一步步拆到 token 级

整个前缀,究竟要不要“整个存下来”?

这里的核心区别,是前缀的身份与前缀的计算状态。把它们分开,hash、单 token 缓存和块缓存就能放进同一张图。

04
小王 · 问

一次请求,不应该只有“命中”或“没命中”吗?

大王 · 答

一次请求可以只复用输入的前半段。假设这次总共输入 110,000 个 token,其中 100,000 个来自已有缓存,剩余 10,000 个需要重新处理,那么按 token 统计的命中率为:

100,000110,000 ≈ 90.9%

这是一个教学算例。实际指标要先说明分子、分母和统计范围。

“发生过任意命中的请求占比”和“全部输入 token 中命中的占比”是两个不同指标。一百次请求每次只命中一个 token,前者也能是 100%;它未必省下了多少计算。

LAB 01只改一个位置,看看精确前缀复用到哪里停止
上次输入 · 已建立可用的逐 token 缓存
ABCDEFGHIJKLMNOP
本次输入 · 绿色可复用;橙色虽可同字,但此前条件已变化
ABXDEFGHIJKLMNOP
2可复用的前缀 token
14本次需要重新处理
12.5%token 口径命中率

16 个位置里只改了第 3 个,但普通精确前缀缓存只能直接复用前 2 个。

教学模拟:假设完整历史注意力、固定配置、逐 token 查找入口均已就绪,忽略驱逐和到期。它展示精确前缀缓存的规则;并未调用真实模型。

这也解释了一个常见的工程建议:在不破坏任务语义的前提下,把稳定资料尽量放在前面,把时间戳和本轮问题等变化内容放在后面。一个很早出现的动态字段,可能让后面很长的相同文本无法直接复用。[4]

05
小王 · 问

同一个“苹果”,为什么不能直接使用同一份 KV?

大王 · 答

比较“我 喜欢 苹果”和“我 讨厌 苹果”。末尾 token 相同、位置相同,但模型处理“苹果”时读到的历史不同。前面注意力层得到的表示,会继续成为后面层计算 K/V 的输入。

FIG 03“苹果”相同,条件可以不同
↔ 左右滑动查看整张图
“苹果”相同,条件可以不同在标准的逐位置输入变换下,第一层注意力前的 K/V 可以相同;前层注意力混入不同历史后,高层 K/V 通常会出现差异。图中的“可能”很重要:前缀不同不构成“所有数值必然不同”的证明。固定 token 与位置,比较前文条件我喜欢苹果第 1 层 K/V可以相同看过前文后继续向上我讨厌苹果第 1 层 K/V可以相同看过前文后继续向上更高层“苹果”的表示可能不同同一个词 + 同一个位置,仅标识了当前位置;还没有标识它经历过的上下文。
在标准的逐位置输入变换下,第一层注意力前的 K/V 可以相同;前层注意力混入不同历史后,高层 K/V 通常会出现差异。图中的“可能”很重要:前缀不同不构成“所有数值必然不同”的证明。 [1]
KViℓ = Fℓ(x1, …, xi; 模型与运行条件)

F 表示模型的计算关系;它与用来查目录的 hash 函数是两件事。

因此,仅用 token_id 或 (token_id, position) 作为通用的高层 KV 缓存键,会把不同的上下文混为一谈。需要把此前的条件也区分开。

但这里还没有推出“缓存键必须把整段前缀明文存进去”。那正是下一个问题。

06
小王 · 问

“B 在 A 之后”可以用 hash map 表示。这个反问对吗?

大王 · 答

对。你给出的 parent hash 方案成立。 前缀身份完全可以递归表示。令 h₀ 编码初始身份条件,对每个 token 计算:

hi = Hash(hi−1, tokeni, 相关身份信息)

键只需代表“这条历史路径”,无需反复携带整段前缀明文。

FIG 04父前缀的 hash,足以参与构造下一个缓存键
↔ 左右滑动查看整张图
父前缀的 hash,足以参与构造下一个缓存键两个“苹果”属于不同历史分支。系统可以保存 parent hash 或父节点指针,无需在每个键中重复放入整段前缀文本。我共同的父状态 h₁喜欢h₂ = Hash(h₁, 喜欢)讨厌h₂′ = Hash(h₁, 讨厌)苹果键:Hash(h₂, 苹果)苹果键:Hash(h₂′, 苹果)token 一样;父身份不同每个键可以很短,但沿途走过哪条路径,仍被递归地区分开。
两个“苹果”属于不同历史分支。系统可以保存 parent hash 或父节点指针,无需在每个键中重复放入整段前缀文本。 [3]

vLLM 的 hash_block_tokens 就把父块 hash、当前块 token 与附加身份信息一起交给 hash 函数。这与“parent_hash + current_token”的思想一致,只是它通常以块为单位。[3]

KEY / 身份

这份状态属于谁?

Hash(parent, token) 是查找用的短标识。严格的生产实现还要处理碰撞风险和隔离条件。

VALUE / 状态

这段历史计算后留下什么?

目录找到的是实际的 KV 存储或其引用。继续运行模型时,仍需要读取相关数值。

你提出的 hash map 可以指向真实 KV。这个想法并未要求“用几十字节替代全部模型状态”。关键边界是:压短标识很容易;要让模型只靠一个小状态继续工作,还需要另外的计算结构。

把 KV_B 与 Hash(A,B) 直接写成等号,会混淆二者。清楚的写法是 cache[key] → KV 或恢复状态的引用。

07
小王 · 问

那为什么不每算好一个 token,就建立一个可复用入口?

大王 · 答

可以这样做。 SGLang 的公开 RadixCache 代码支持 page_size = 1 的匹配分支;在普通单 token 键模式下,前缀匹配可以精确到一个 token。这足以说明:单 token 级索引没有普遍的理论障碍。[5]

粗一点的粒度通常是在做成本权衡。把一段连续的 K/V 放进同一个块,索引、引用计数、传输和淘汰操作就可以成组处理。在只复用完整块的简化方案中,公共前缀长为 p、块大小为 b 时:

可复用长度 r = b × ⌊p / b⌋
仅由块对齐造成的额外重算 = p − r < b

这个界限只约束“粒度造成的损失”;缓存过期或状态不完整时,损失可能远大于 b−1。

LAB 02缓存粒度:少建一些入口,要多算多少 token?
999
整块复用相同却需重算的边界从首处分歧起的新后缀
992整块可复用 token
7仅因粒度而多算
62旧请求中的完整块入口

理论可复用 999 个,按 16 对齐后可复用 992 个。额外重算 7 个;最后 1 个属于新后缀。

教学假设:旧请求与新请求都长 1,000 token,整块缓存已保留且可找到。这里只计算整块对齐的损失;不含最后一个位置的 logits 补算、缓存传输或特殊混合架构的恢复开销。

vLLM 的前缀缓存文档描述了一种完整块复用方案。b = 16 时,999 个相同 token 只能按该规则复用到 992,多算 7 个,换来更少的目录项。[2]

但不能进一步把“单 token 索引”想成“每个 token 调一次 GPU 内存分配”。物理内存池、匹配粒度与树节点表示可以分别设计;压缩前缀树也不必给每个 token 分配一个独立的树节点。更小的物理页甚至能减少尾部空洞,代价需要按实际系统测量。

小王父 hash 已经很短了,多算几个 hash 的代价会很大吗?

大王:短键省下了重复保存整段前缀的空间,但链式构造仍有前后依赖。vLLM 的 hash_block_tokens 接受 parent_block_hash;新块的键需要先知道父块的键。对一条首次建立索引、长度为 N 的序列,块大小为 b 时,大约有 N/b 次依次相连的键构造。把 b 从 16 改为 1,就把这条链上的步骤数扩大到约 16 倍。[3]

块大小 bN = 1,000,000 的完整块数单个分叉边界最多多算边界余数均匀时的平均值
11,000,0000 token0 token
1662,50015 token7.5 token
6415,62563 token31.5 token
E[额外 token] = (b − 1) / 2

仅当公共前缀在块内的余数近似均匀时成立。严格上界是 b − 1;b/2 只是平均量级。

这里比较的是一个分叉边界的额外 token 数与整条新路径的索引工作量。不能据此直接宣布“常数收益一定抵不过线性代价”:一个 token 的注意力成本也可能随历史长度增加,缓存还可能被许多请求重复利用。

还有两个限定:哈希仍需读取当前块的 token,扫描输入的总工作通常仍是 O(N);已有会话可以从现成父 hash 增量续算,新增索引主要对应 ΔN。压缩树、批处理和不同哈希方案也可能有不同实现。因此,应当把串行链视作这类实现的具体成本,而非所有单 token 缓存的必然缺陷。

03 / RESTORATION · 从算法走向服务

有相同前缀,还差哪些条件才能命中?

缓存目录得找到它,数据得还在,而且留下来的状态必须足以从那个位置继续。API 的缓存点就处在这一层。

08
小王 · 问

Claude 的 breakpoint 到底在“截断”什么?

大王 · 答

更合适的中文是缓存边界或恢复点。“截断点”容易让人误会后面的内容不再输入给模型。

把 cache_control 放在某个内容块上,表达的是:从整个有效提示的开头,到这个块结束的前缀,成为候选缓存范围。Claude 文档规定相关顺序为 tools → system → messages。[4]

FIG 05缓存点,是“截至这里”的恢复入口
↔ 左右滑动查看整张图
缓存点,是“截至这里”的恢复入口缓存覆盖完整前缀,含被标记的内容块。图中的 API 内容块与 GPU 存储 KV 的物理块是两种对象。Claude 的有效前缀顺序:tools → system → messages工具定义系统指令稳定的文档 / 历史cache_control这次的新问题 / 新工具结果可复用的前缀入口从开头一直到标记处标记后面的内容仍交给模型处理缓存点控制“缓存到哪里”,不会把后面的输入截掉。
缓存覆盖完整前缀,含被标记的内容块。图中的 API 内容块与 GPU 存储 KV 的物理块是两种对象。 [4]

底层 K/V 的产生并不等待这个标记。API 标记决定服务层怎样保存、查找和计费,不能拿来推断 GPU 何时开始拥有 K/V。

多个缓存点可以对应不同稳定程度:工具和系统指令几乎不变,项目资料偶尔改变,对话不断追加。较长的入口失效时,较短的入口仍可能有效。但它们都表示“从开头到某处”,不会把后面相同的文本自动变成独立于前文的缓存片段。

09
小王 · 问

既然有自动缓存,还需要手动放缓存点吗?

大王 · 答

关键要看请求怎样变化。Claude 当前文档支持顶层 cache_control,会把缓存点放到最后一个可缓存内容块。对于不断在旧对话后追加内容的请求,这很方便。[4]

APPEND / 只在末尾追加

第 1 次:A + B

第 2 次:A + B + C
旧的完整前缀仍在。自动向前移动缓存点,容易找到上次的写入。

REPLACE / 每次换掉后缀

第 1 次:A + 问题 X

第 2 次:A + 问题 Y
如果只存过 A+X,A 本身未成为缓存入口,稳定资料仍可能无法命中。

第二种情况下,把显式边界放在稳定资料 A 的末尾通常更合适。自动缓存不会替你发现所有稳定片段并自动建立入口。 当前规则的回看范围也有限,最多检查每个边界附近 20 个内容块位置,查找的是先前真正写入的入口。[4]

示例 A · 追加式对话:顶层自动缓存
response = client.messages.create(
    model=model_id,
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=stable_system,
    messages=growing_history,
)
示例 B · 固定资料 + 每次不同的问题:显式边界
response = client.messages.create(
    model=model_id,
    max_tokens=1024,
    system=stable_system,
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": stable_document,
             "cache_control": {"type": "ephemeral"}},
            {"type": "text", "text": current_question},
        ],
    }],
)

代码展示请求结构;client、model_id 和内容变量需在应用中提供。前缀还需达到相应模型的最小缓存长度;缓存存在有效期,当前文档提供 5 分钟和 1 小时选项。调用方式、限制与支持范围以所用平台文档为准。

时间从哪里开始算?

Claude 当前文档把 TTL 的起点放在写入或读取缓存的请求开始时。例如一个响应流式输出了 4 分钟,默认 5 分钟 TTL 大约只剩 1 分钟;后续若还要等工具执行,可能来不及复用同一入口。[4]

FIG 11响应刚结束,不代表缓存还剩完整的 5 分钟
↔ 左右滑动查看整张图
响应刚结束,不代表缓存还剩完整的 5 分钟示意只有这一次写入或读取,期间没有其他请求刷新同一条缓存。TTL 从写入或读取该条目的请求开始计时;具体能否命中还取决于入口、可用性与平台规则。5 分钟 TTL:生成响应的时间,也在倒计时里0~4 分钟:响应持续生成约剩 1 分钟请求开始响应结束缓存到期下一次读到同一条缓存,才会按该次请求开始时间刷新 TTL。
示意只有这一次写入或读取,期间没有其他请求刷新同一条缓存。TTL 从写入或读取该条目的请求开始计时;具体能否命中还取决于入口、可用性与平台规则。[4]

为什么没有报错,usage 却全是 0?

缓存前缀必须达到模型的最小长度。当前官方文档给出的部分门槛如下;低于门槛仍会正常处理请求,不会因为未缓存而报错。[4]

模型最小缓存前缀
Claude Opus 5512 token
Claude Sonnet 5 / Sonnet 4.61,024 token
Claude Haiku 4.54,096 token

记录日期:2026 年 9 月 10 日。门槛看被标记的可缓存前缀长度,不只看整个请求是否够长。若 cache_creation_input_tokens 和 cache_read_input_tokens 都为 0,可先查这个条件;平台适配与模型版本仍应对照对应文档。

10
小王 · 问

DeepSeek 是怎样提高实际缓存命中率的?

大王 · 答

DeepSeek 的上下文缓存文档公开了三类持久化位置:请求输入和输出的结束位置、多个请求的共同前缀、长输入或输出中的固定 token 间隔。 后续请求要完整匹配一个已经保存的缓存前缀单元,才能复用它。[6]

FIG 06DeepSeek:稳定前缀可以在被观察到之后单独持久化
↔ 左右滑动查看整张图
DeepSeek:稳定前缀可以在被观察到之后单独持久化沿用 DeepSeek 官方文档中的 A+B、A+C、A+D 场景。假设 A 尚未被其他边界覆盖;第 2 次发现 A 后,将它写成独立缓存前缀单元,第 3 次才能匹配。真实服务还受异步写入与可用性影响。请求 1A · 同一份长资料B留下 A+B请求 2A · 同一份长资料C发现并持久化 A请求 3A · 同一份长资料D复用 A,只处理 D① 请求边界:输入结束 / 输出结束② 发现公共前缀:单独建立恢复单元③ 长序列:按固定 token 间隔建入口三种规则共同增加可用恢复位置
沿用 DeepSeek 官方文档中的 A+B、A+C、A+D 场景。假设 A 尚未被其他边界覆盖;第 2 次发现 A 后,将它写成独立缓存前缀单元,第 3 次才能匹配。真实服务还受异步写入与可用性影响。 [6]

用一个抽象式表达就很清楚。设两次请求客观共有的前缀长度为 p,已经保存且可恢复的边界集合为 E。在这一完整单元规则下,最长的直接恢复位置可以写成:

r = max({e ∈ E : e ≤ p} ∪ {0})

输入不变时,p 不由缓存策略决定;策略可以改变 E,从而改变实际可恢复到的 r。

请求边界抓住自然的继续点;公共前缀检测补上真实出现的复用位置;固定间隔为长序列提供额外覆盖。它们都在增加“可以找回并使用的状态”,不会让两个原本不同的前缀 变成相同。

上图是服务行为的解释。DeepSeek 未在这份 API 文档中公开完整缓存目录的数据结构,不能据此认定服务端一定采用某种 radix tree、hash 链或特定 checkpoint 间距。

小王既然都算过了,为什么还要等公共前缀出现,才让它落盘?

大王:多保留一个入口,会占用索引、写入带宽、存储空间,某些模型还需要额外的恢复快照。这就需要决定“哪些状态值得占着资源”,也就是缓存准入策略(cache admission policy)。

公共前缀被重复使用,是比“它曾经出现过”更强的复用信号。资源有限时,优先保留这类前缀,可以减少只用一次的内容对热缓存的挤占。DeepSeek 文档也提示缓存构建需要数秒,因此刚完成计算与持久化入口立即可用之间,还可能存在时间差。[6]

保留的预期收益 ≈ 未来复用次数 × 每次省下的代价 − 写入、存储与恢复代价

这是解释取舍的成本模型,非 DeepSeek 公布的实际准入公式。

“再次使用后更值得保存”与 cache-on-second-access 的思想相通。DeepSeek 同时还保存请求边界和固定间隔入口,不能把整个系统概括为严格的二次访问缓存,也不能声称所有前缀都必须等重复两次才落盘。

11
小王 · 问

已经缓存 A+B,为什么不能直接把 B 切掉,拿到 A?

大王 · 答

这是比“索引太多”更深入的一问。对于保存了每层完整历史 K/V 的标准实现,这个想法可以成立: 只要数据还在,取出前 A 个位置即可;是否能找到相应入口,是另一层问题。

但很多长上下文架构会主动丢掉旧的局部状态。DeepSeek 文档也明确指出,滑动窗口注意力使它的缓存存储与匹配规则发生变化。[6]

FIG 07保存了更晚的状态,不保证还能切回更早的位置
↔ 左右滑动查看整张图
保存了更晚的状态,不保证还能切回更早的位置上半图是完整历史 KV 的理想情况;下半图是只留最近窗口的简化模型。窗口、递归状态或压缩累积量,都可能让“恢复到中途”额外需要快照或重算。此图不声称还原了 DeepSeek 的私有服务实现。完整历史 KV:若数值全在,可以截取前半部分12345678前 4 个仍然在,可按位置截取滚动窗口(示意 W=4):运行到第 8 个时,只留下最近 4 个12345678想恢复第 4 个位置:旧窗口可能已不在当前存的是位置 5~8
上半图是完整历史 KV 的理想情况;下半图是只留最近窗口的简化模型。窗口、递归状态或压缩累积量,都可能让“恢复到中途”额外需要快照或重算。此图不声称还原了 DeepSeek 的私有服务实现。 [6][7]

所以,“存在 A+B 的压缩恢复状态” 并不自动推出 “A 的全部恢复状态仍可被切片拿出”。模型采用什么记忆结构,直接影响缓存系统能提供多细的恢复边界。

12
小王 · 问

SSD 和 KV 压缩,又怎样影响命中率?

大王 · 答

同一个前缀,可能因为缓存未写完、已经被淘汰、过期或隔离条件不同而无法命中。DeepSeek 当前文档描述的是默认开启的硬盘缓存,并说明构建需要时间、采用尽力而为策略,不承诺 100% 命中。[6]

01

同一状态占用更少空间

来自 KV 量化、压缩、共享等设计;这些方法作用的维度并不相同。

02

同样容量容纳更多历史

在其他条件相同的假设下,更少的缓存内容被提前淘汰。

03

再次访问时更可能还在

实际命中机会可能增加;存储带宽和恢复开销仍然需要计入。

上面是一条有条件的因果链,并非产品命中率提升的实测结论。缓存小了,可以选择多保留历史,也可以把资源拿去服务更多并发;最终怎么分配,取决于系统策略。

能复用 = 身份匹配 ∩ 入口可查 ∩ 数据可用 ∩ 状态足够恢复

这几个条件需要同时成立。它们互相影响,不能随意当作独立概率相乘。

到这里,hash 已经完成了它擅长的事:快速识别身份。下一个问题开始碰模型架构——为什么那份需要保存的状态会那么大?

04 / ARCHITECTURE · 让历史本身更便宜

YOCO:多层能否共用一份长期记忆?

服务系统在回答“过去的计算能不能找回来”。YOCO 进一步改变“模型会留下几份长期状态”,以及哪些历史位置还需要继续计算。

13
小王 · 问

传统 KV Cache 为什么会随“长度 × 层数”膨胀?

大王 · 答

普通的全历史 Transformer 中,各层使用各自的中间表示产生 K/V。每一层都要记住整个历史,于是序列越长、层数越多,缓存越大。

FIG 08相同的历史,在不同层留下不同的 K/V
↔ 左右滑动查看整张图
相同的历史,在不同层留下不同的 K/V图中每行是一层的完整序列 KV,绿色方块内数字代表 token 位置。普通模型各层的状态不同,因此不能在推理时随意合并;跨层共享需要模型设计与训练配合。普通全历史注意力:每层留下自己的整段 KV第 1 层1234567…N第 2 层1234567…N第 3 层1234567…N第 4 层1234567…N横向:序列长度 N纵向:层数 L
图中每行是一层的完整序列 KV,绿色方块内数字代表 token 位置。普通模型各层的状态不同,因此不能在推理时随意合并;跨层共享需要模型设计与训练配合。 [1][7]
MKV ≈ 2 × L × N × HKV × dhead × s

2 表示 K 和 V;L 是层数,N 是长度,H_KV 是 KV 头数,d_head 是每头维度,s 是每个数值的字节数。忽略批量、元数据和其他状态。

这个公式只描述标准全历史 K/V 的一个简化形状。它也让几种优化的位置变得直观:减少 KV 头数,减少每个状态的维度,降低数值精度,减少保留的位置,或减少重复保存长期 KV 的层数。YOCO 主要从最后一项切入。[7]

01 · HEADS

少一些 KV 头

多个查询头共享少量 KV 头,例如 GQA / MQA。

02 · WIDTH

窄一些状态

用更小的向量或潜在表示承载历史。

03 · PRECISION

少一些比特

低精度保存数值;量化尺度也要占空间。

04 · POSITIONS

少一些位置

合并、压缩或只保留部分历史位置。

05 · LAYERS

少一些重复层

让多个层共享同一个长期状态源。

06 · K / V

共享 K 与 V 的载体

模型若让同一向量承担两种用途,前面的 ×2 也需要重写。

第六项要通过架构与训练来实现。普通模型中的 K 和 V 可以不同,无法在服务端把两份向量直接并成一份而保持原计算。第 18 问会用一个带位置旋转的共享方案,把这个边界讲清楚。

14
小王 · 问

YOCO 的 Once,究竟是哪一种“只做一次”?

大王 · 答

YOCO 的全称是 You Only Cache Once。原论文把模型分成前半的 Self-decoder 和后半的 Cross-decoder:前半先得到上下文表示,由它生成共享的全局 K/V;后半各层通过各自的查询读取这份记忆。[7]

FIG 09YOCO 的“只缓存一次”:后半部分共享全局 KV
↔ 左右滑动查看整张图
YOCO 的“只缓存一次”:后半部分共享全局 KV这是一张原始 YOCO 思想图。前半部分可采用滑动窗口注意力或门控递归结构;它仍有自己的有界状态。后半各层共用一套全局 K/V,但各自继续计算不同的查询与表示。token 的计算依次向下历史记忆从这里产生,供后半层读取输入 token / 向量前半:Self-decoder逐步处理 token;保留有界局部/递归状态前半部分的最终表示一套共享的全局 K/V随 token 序列增长,持续追加Cross-decoder 第 1 层Cross-decoder 第 2 层Cross-decoder 第 3 层各层有自己的 Q 与后续计算读取的长期 K/V 来自同一份记忆输出:预测下一个 token
这是一张原始 YOCO 思想图。前半部分可采用滑动窗口注意力或门控递归结构;它仍有自己的有界状态。后半各层共用一套全局 K/V,但各自继续计算不同的查询与表示。 [7][8]

“Once”说的是全局历史 K/V 只保留一套,供多个后半层复用。它不表示一个 token 在整个模型里只计算一次,不表示模型跨请求只运行一次,也不表示前半部分完全没有状态。原论文专门在脚注中说明了后一点。[7]

普通全历史 KV:O(LN)
YOCO 的窗口变体:O(N + LselfW)

为突出长度与层数,省略头数、维度和精度。W 为局部窗口宽度;共享全局 KV 仍随 N 增长。

LAB 03只改变“全局 KV 保存几份”,内存会怎么变?
40 层都保留整段历史16.384 GB
一套全局 KV + 前 20 层局部窗口0.420 GB
0.410共享的全局 KV / GB
10.49局部状态 / MB
39.0×本例的总 KV 体积比

相同每-token状态宽度下,100,000 token 的长期记忆由 40 份变为 1 份;局部窗口另计。

模型形状假设,非 DeepSeek 或 Claude 实测:共 40 层;前 20 层各保留 128 个位置;每层每 token 的 K/V 总计 4,096 字节(8 个 KV 头 × 128 维 × K/V × 2 字节)。采用十进制 GB/MB。仅计算 KV,不含模型权重、激活、工作区、元数据等。

因此 YOCO 也没有把整个百万 token 历史压成一个固定长度 hash。它减少的是长期记忆在层与层之间的重复保存。越长的上下文,固定大小的局部窗口所占比例越小。

两部分仍按因果顺序运行:当前位置只能使用允许看到的历史,后续新 token 还会再经过前半和后半。这里的“前半像编码器”不意味着它可以双向读到未来。[7]

15
小王 · 问

为什么共享 KV,还能让 Prefill 跳过一些层?

大王 · 答

在普通 Transformer 中,历史位置必须经过前面的层,才能生成后面每层要使用的 K/V。即便你不需要那些历史位置的输出,也很难跳过这些计算。

YOCO 改变了依赖关系:后半各层需要的历史 K/V 直接由前半部分输出提供。于是,已经给定的历史位置不必再经过整个后半部分,只为建立未来要用的缓存。公开实现中,CrossDecoder.forward 会先生成并追加共享 K/V,再根据 skip_cross_decoder 决定是否继续跑后半层。[8]

FIG 10YOCO 省下的,是前缀历史位置的后半计算
↔ 左右滑动查看整张图
YOCO 省下的,是前缀历史位置的后半计算仅需接着生成下一个 token 时,历史位置主要用于建立共享记忆;最后一个输入位置仍需通过后半层产生下一 token 的概率。随后每个新 token 继续经过两部分。表格是依赖关系示意,不按 FLOPs 面积比例绘制。每一列是一个输入位置;每一行是一组计算x₁x₂x₃…xₙ₋₁xₙ新 token前半 Self-decoder计算计算计算计算计算计算计算生成 / 追加共享 KV计算计算计算计算计算计算计算后半 Cross-decoder省去省去省去省去省去计算计算这些历史位置无需产生最终输出首个预测继续生成
仅需接着生成下一个 token 时,历史位置主要用于建立共享记忆;最后一个输入位置仍需通过后半层产生下一 token 的概率。随后每个新 token 继续经过两部分。表格是依赖关系示意,不按 FLOPs 面积比例绘制。 [8]

这个设计要按位置理解。若提示词有 N 个位置,要得到第一个新 token 的概率,仍需用最后一个输入位置的表示运行后半层。能够省去的是许多此前历史位置的后半计算。

可以省的情况

只需要从已知前缀继续生成

大量历史位置只负责建立记忆。它们的后半表示既不需要输出,也不再用于生成未来的 K/V。

不能照搬的情况

训练或需要逐位置输出分数

要计算各个位置的预测和损失时,相关后半计算仍然必要。具体接口能否省略,取决于它要求返回什么。

此外,前半部分采用高效的局部或递归结构,也会降低长输入处理成本。论文中出现的倍数,依赖于对照模型、序列长度与实现;不能把它们直接贴到另一个产品上。

05 / CASE STUDY · 把结构展开成账本

从 YOCO 到 V4.1:存什么、重算什么、读什么?

把跨层共享、890 字节、有限重放与稀疏读取放到各自的统计口径里。每一步都区分设计条件、数学推导与运行证据。

V4.1 案例 · 资料口径

以下 V4.1 专属配置、代码行为和报告表述,依据随文提供的技术摘录;本文按这些参数复算内存,并推导依赖与复杂度。原始 config.json、model.py、kernel.py 及报告正文未在本次取得,因此这些内容不标为已独立核实的源码事实,也不作为运行实测。通用机制与已直接查到的官方 API 规则另列来源。[9]

16
小王 · 问

V4.1 的“YOCO-like”,具体体现在哪里?

大王 · 答

它可以具体展开为:哪些层生产历史,哪些层共享历史,哪些状态仍归各层自己保留。 按技术摘录的配置,40 层分为 0–19 与 20–39 两部分;global KV 源为 [2, 8, 14, 20],前两层只保留滑窗。[9]

FIG 1240 层的长期状态,集中到 4 个源
↔ 左右滑动查看整张图
40 层的长期状态,集中到 4 个源参数来自随文技术摘录:[2, 8, 14, 20] 为 KV 源。2–19 层分成三个六层组,六层包含源层本身;20–39 层为二十层组。图只呈现源与使用者的关系,未代替完整执行图。按技术摘录整理;层号从 0 开始。长历史与局部窗口分开画。层号0–1无 global 源压缩比 ratio = —各层局部滑窗每层另有自己的 SWA 窗口层号2–7源层 2压缩比 ratio = 26 层合计共享这一源每层另有自己的 SWA 窗口层号8–13源层 8压缩比 ratio = 26 层合计共享这一源每层另有自己的 SWA 窗口层号14–19源层 14压缩比 ratio = 26 层合计共享这一源每层另有自己的 SWA 窗口层号20–39源层 20压缩比 ratio = 120 层合计共享这一源每层另有自己的 SWA 窗口共享 global KV,不会自动消除每层的局部状态。
参数来自随文技术摘录:[2, 8, 14, 20] 为 KV 源。2–19 层分成三个六层组,六层包含源层本身;20–39 层为二十层组。图只呈现源与使用者的关系,未代替完整执行图。[9]

摘录把后半段的 global KV 联系到前半段的最终表示,同时保留了各层自身的局部滑窗状态。这与 YOCO 的跨层记忆共享有明确的结构联系;前半部分内部也可以分组共享。比较模型时,应看实际的依赖边,而不只看“Encoder / Decoder”两个名字。[7][9]

问题这组材料能够说明什么仍需分清的边界
谁生产 global KV?摘录列出 4 个源与对应层组。同源表示、同一投影与同一缓存对象,要分开辨认。
Prefill 能跳过后半吗?报告转述描述了部署侧缩短路径;参考实现转述仍逐层执行。架构可省、报告声称省、公开代码已实现省,是不同证据。
890 B/token 怎样算?按宽度、精度、尺度和四个源的压缩比可复算。它度量 global KV 斜率,未涵盖全部显存。
128 token 能恢复吗?摘录将 Bounded Replay 表述为近似状态重建。近似恢复需要质量评估;不能宣称数值等价。
17
小王 · 问

890 bytes/token,究竟省了哪些东西?

大王 · 答

先明确计算单位:每增加一个原始输入 token,需要多保留多少 global KV。 这与“整条序列共占多少显存”不同。按给定的量化和共享条件,每个压缩位置需要 356 字节。[9]

FIG 13890 字节:每一项从哪里来?
↔ 左右滑动查看整张图
890 字节:每一项从哪里来?按技术摘录给出的条件复算:main KV 使用 4 bit 数值及每 16 维 1 字节尺度;indexer K 使用 4 bit 数值及每 32 维 1 字节尺度。源层压缩比为 2、2、2、1。忽略压缩组尾部、布局对齐和额外元数据。每一个压缩位置的存储:数值本体 + 量化尺度main KV · 512 维256 B 数值 + 32 B 尺度288 Bindexer K · 128 维64 B 数值 + 4 B 尺度68 B每个压缩位置:288 + 68 = 356 B三个 ratio = 2 的源3 × 356 / 2 = 534 B/token一个 ratio = 1 的源1 × 356 = 356 B/token534 + 356 = 890 B/token
按技术摘录给出的条件复算:main KV 使用 4 bit 数值及每 16 维 1 字节尺度;indexer K 使用 4 bit 数值及每 32 维 1 字节尺度。源层压缩比为 2、2、2、1。忽略压缩组尾部、布局对齐和额外元数据。[9]
Mglobal(N) ≈ 356 × (N/2 + N/2 + N/2 + N)
= 890 N bytes

这是长序列下的线性项;短序列需按实际压缩组、取整和内存布局计算。

这里的尺度,是把低精度数值还原到正确大小所需的系数。FP4 的每个数值占 0.5 字节,但不能只乘 0.5 就结束:main KV 的尺度贡献 32 字节,indexer K 的尺度再贡献 4 字节。报告摘录将它们分别称为 E4M3 与 UE8M0 尺度。[9]

再看局部窗口:按 40 层、每层 128 个位置、512 维且每数值 1 字节的 FP8 载体估算,SWA 的数值本体为:

MSWA,payload = 40 × 128 × 512 × 1
= 2,621,440 bytes ≈ 2.62 MB

该项与长上下文 N 无关;还未计入 SWA 的尺度、对齐、元数据等。1 MB = 10⁶ bytes;2,621,440 bytes 也就是 2.5 MiB。

LAB 04长历史的线性项,与局部窗口的固定项
890.00global KV / MB
2.62SWA 数值本体 / MB
892.62两项相加 / MB
892.62两项均摊 / B per token

在这组条件下,1M token 的 global KV 为 890 MB;局部窗口另计。该和数不含权重、工作区及其他状态。

条件算例,非产品显存实测。缩短 N 时,局部窗口项不随 N 缩小,所以总量除以 N 会高于 890 B/token。这里假定 N 至少覆盖完整窗口。

统计对象纳入什么不能由此推出什么
890 B/token四个源的 main KV、indexer K 及所列尺度,按压缩比加权。整模型显存、实际吞吐或 API 价格。
约 2.62 MB/序列上述假设下,各层 SWA 的固定窗口数值本体。全部局部状态与工程开销。
SSD 持久化体积写盘时实际选取的状态、快照及其编码方式。不能用显存态的 890 自动验证“为前代 1/8”。
完整部署资源还要加权重、Engram、激活、工作区、并发和通信缓冲等。不能用不到 1 GB 的 global KV,推导模型能放进 1 GB 显卡。
18
小王 · 问

K 和 V 真的能用同一个向量吗?

大王 · 答

K 和 V 描述两种用途:一个参与匹配,一个参与汇总。模型可以被设计成让同一个存储向量承担这两种用途;此时就少保存一份数值。技术摘录说 V4.1 的 kv_shared 同时用于匹配与输出乘法,账本按这一条件计算。[9]

FIG 14同一份向量承担 K 和 V,位置编码也进入读取结果
↔ 左右滑动查看整张图
同一份向量承担 K 和 V,位置编码也进入读取结果带旋转位置编码的共享 K/V 思想图。为清晰起见,使用完整二维旋转的数学记号;实际内核可能只旋转部分维度。它解释输出逆旋转的作用,不声称逐行还原 V4.1 内核。历史位置 j内容向量 cⱼ保存一次mⱼ = Rⱼ cⱼ作为 Key:算权重αᵢⱼ 来自 Q 与 mⱼ作为 Value:被混合o′ᵢ = Σ αᵢⱼ mⱼ将输出转到当前查询位置的坐标系oᵢ = Rᵢ⁻¹ o′ᵢ = Σ αᵢⱼ Rⱼ₋ᵢ cⱼ仍含相对位置 j − i;不等同于把每个 Value 都恢复成 cⱼ。
带旋转位置编码的共享 K/V 思想图。为清晰起见,使用完整二维旋转的数学记号;实际内核可能只旋转部分维度。它解释输出逆旋转的作用,不声称逐行还原 V4.1 内核。[9]

旋转位置编码(RoPE)会按 token 位置旋转一部分向量,使匹配能表达相对位置。如果作为 Key 的旋转向量同时也是 Value,那么汇总后的输出也带有这些旋转。对查询位置 i 的输出乘 Ri−1,可以把它表达在当前位置的坐标系里。

Ri−1 ∑j αij Rjcj
= ∑j αij Rj−icj

使用同一组 RoPE 频率时,旋转矩阵满足 Rᵢ⁻¹Rⱼ = Rⱼ₋ᵢ。这个等式是线性代数推导。

同理,num_key_value_heads = 1 本身只说明 KV 头少,不能单独证明 K 与 V 是同一个向量。是否共用,需要继续看实际投影、张量引用和两个矩阵乘法的输入。

19
小王 · 问

只重放最后 128 个 token,就能精确恢复吗?

大王 · 答

分成两个问题看:不保存的局部状态怎么补回来;补回来的状态需要多精确。 单层只看最近 W 个位置,多层叠加后的理论依赖范围通常会扩大。对窗口包含当前位置的简单 L 层堆叠,单个输出位置的感受野可达到 1 + L(W−1)。

FIG 15重放一个窗口,未必重建完整的多层状态
↔ 左右滑动查看整张图
重放一个窗口,未必重建完整的多层状态h²[t] 需要 h¹[t−2];而 h¹[t−2] 又依赖原始位置 t−4、t−3。只提供 t−2、t−1、t 的原始输入且没有额外边界状态时,这条依赖无法自动补齐。图为确定性依赖反例,非 V4.1 的误差实测。一个最小反例:窗口含当前位置,W = 3;只画两层依赖t−4t−3t−2t−1t第 1 层h¹[t−2]第 1 层h¹[t−1]第 1 层h¹[t]第 2 层目标h²[t]仅重放最后 3 个:t−2,t−1,t但较早输入仍可能影响它
h²[t] 需要 h¹[t−2];而 h¹[t−2] 又依赖原始位置 t−4、t−3。只提供 t−2、t−1、t 的原始输入且没有额外边界状态时,这条依赖无法自动补齐。图为确定性依赖反例,非 V4.1 的误差实测。

技术摘录将 V4.1 的 SWA Bounded Replay 描述为:为避免保存各层 SWA 状态,只重放一个窗口来近似恢复。摘录同时指出,报告承认实际有效依赖可能短于理论感受野,并将这类恢复列为需要进一步刻画的鲁棒性边界。[9]

小王那“Prefill 只跑前半模型”也要加条件吗?

大王:需要。YOCO 的可见参考实现有 skip_cross_decoder,可以沿共享 KV 的依赖关系跳过许多历史位置的后半计算。V4.1 的技术摘录则区分了两条路径:

参考实现 · 按摘录

对输入位置逐层运行

Transformer.forward 仍执行全部 40 层。这个行为能展示模型计算,不足以复现部署侧早退。

报告描述 · 按摘录

前半编码 + 短尾部重放

省去大段后半历史位置,保留生成首个输出所需的路径,并用有限重放补局部状态。

[8][9]
O(NL) → O(NL/2 + W · L/2)

表达“运行了多少层 × 多少位置”的结构账本;这里将每个层位置的工作近似同权。它不自动包含全部注意力扫描、稀疏索引、通信和排队代价。

因此,从“架构允许短路径”走到“服务端实际更快”,还要经过调度实现与测量。最后一个输入位置需要产生首个新 token 的概率;逐位置打分或训练任务也不能直接套用只生成下一个 token 的跳过方式。

20
小王 · 问

百万 token 存得下,就一定算得起吗?

大王 · 答

还差一个决定速度的问题:每生成一个 token,究竟需要触碰多少历史状态? 把所有 KV 从 10 GB 压到 1 GB 会减少容量压力;如果仍在许多层反复扫描完整历史,读取和匹配依然会很贵。

FIG 16把反复搜索全历史,改成先粗筛、再重排
↔ 左右滑动查看整张图
把反复搜索全历史,改成先粗筛、再重排候选数和层号采用所提供的 V4.1 技术摘录。候选池不足时取实际可用数;图中 16K = 16,384。仅说明 decoder 侧后续索引复用的结构,不包含模型全部层、局部窗口或其他算子。一百万个位置可以保存下来,但每步不必全部精读global KV / index memory覆盖 N 个历史位置一次全局粗筛:选择 2,048 个块每块 8 个位置 → 最多 16,384 个候选后续 indexer:只在候选池中重排摘录中的层号:24 / 28 / 32 / 36选出 Top-512 的 global 位置主注意力精读;局部窗口另计后续重排的范围固定;最初那次全局筛选仍与 N 有关。
候选数和层号采用所提供的 V4.1 技术摘录。候选池不足时取实际可用数;图中 16K = 16,384。仅说明 decoder 侧后续索引复用的结构,不包含模型全部层、局部窗口或其他算子。[9]

根据技术摘录,layer 20 先建立候选池;后面的若干 indexer 只在这个池内挑选 top-512,另一些层复用选择结果。这样就能把“每层都查一遍全历史”的重复工作减掉。[9]

Tdecoder-index(N) ≈ Tglobal(N) + m · Trerank(C)
C ≤ 16,384;m 为执行重排的层数

只是结构表达。候选选择的具体算法、Top-K 内核和常数仍决定真实时间。

稀疏读取还有能力取舍:相关位置如果没有进入候选池,后续精排通常无法把它凭空找回来。因此需要同时看候选召回、答案质量与真实延迟。共享 KV 解决“存几份”,共享候选解决“找几遍”,稀疏读取解决“每次读多少”。

21
小王 · 问

Engram 是不是让 hash 直接找到“知识”?

大王 · 答

Engram 给模型增加一种“遇到某个局部组合,就读取已学习向量”的能力。官方公开实现先把 n-gram 映射到若干哈希地址,再取嵌入向量,随后通过投影、由当前隐藏状态控制的门以及局部融合,注入主干网络。[10]

FIG 17两种“记忆”:请求状态与训练得到的查找表
↔ 左右滑动查看整张图
两种“记忆”:请求状态与训练得到的查找表Engram 的一般机制依据官方演示代码:NgramHashMapping、MultiHeadEmbedding、Engram.forward。演示代码有自身配置,不能直接当作 V4.1 的生产配置;图省略了多头、短卷积等展开细节。前缀缓存:保存当前请求曾经算出的状态整段前缀身份包含历史条件缓存目录找已计算过的条目动态 KV / 恢复状态随本次上下文变化Engram:按局部模式,读出训练得到的向量最近 n 个 token固定长度的局部模式多头 hash 寻址确定要读取的表项训练得到的向量表属于模型参数投影、门控、局部融合结合当前 hidden state 再计算同样用 hash:上面找已算出的状态,下面找已训练的参数。
Engram 的一般机制依据官方演示代码:NgramHashMapping、MultiHeadEmbedding、Engram.forward。演示代码有自身配置,不能直接当作 V4.1 的生产配置;图省略了多头、短卷积等展开细节。[10]

这里的 n-gram,指连续 n 个 token 的局部组合。固定模型和表大小后,每个位置只读取有限数量的表项;地址仅依赖局部 token,因此可以在相应深层计算发生前准备,并适合预取。官方论文还讨论了将大型表放在主机内存中的系统安排。[11]

“把每个 n-gram 都存下来”也需要限定:哈希表容量有限,不是给所有可能组合各建一个无碰撞的槽。不同组合可以落到相同地址,多头哈希和训练共同处理这种压缩。与精确 KV 缓存尽量避免身份误匹配的目标相比,这里的碰撞本身属于表示设计要面对的条件。[10]

V4.1 技术摘录列出在层号 1、14 使用 2/3/4-gram 记忆。这描述的是特定装配;原始 Engram 演示代码有另一组配置。无论具体装在哪层,它都不会绕过“上下文不同,高层 KV 可能不同”的事实:静态表项与动态隐藏状态融合后,仍能产生不同结果。[9][10]

于是,模型里的“记忆”至少包含两类东西:过去这个请求算出了什么,以及训练阶段为常见模式学到了什么。一个由缓存系统管理生命周期,一个通常跟随模型权重;两者可以同时存在。

06 / PRACTICE · 回到最初的问题

理解之后,怎样用到自己的 Agent?

最终目标很具体:让正确的任务以更低成本、更可接受的等待时间完成。漂亮的缓存指标,只是中间证据。

22
小王 · 问

实际调用 API 时,我应该改什么、看什么?

大王 · 答

先画出每次请求的真实输入顺序,标出最早变化的位置。对于追加式对话,尽量保留已有内容的原样顺序;对固定资料的不同问题,把稳定资料与动态后缀分开,并根据服务规则放置缓存边界。

随后看实际 usage,区分“读缓存”“写缓存”和“普通输入”。不要把供应商同名的 input_tokens 想当然当作同一种总量。

Claude · 常见 input-token 总量口径
read  = usage.cache_read_input_tokens
write = usage.cache_creation_input_tokens
fresh = usage.input_tokens

total_input = read + write + fresh
hit_ratio = read / total_input if total_input else 0

Claude 文档的这个 input_tokens 口径不包含上述读缓存和写缓存部分。服务端工具或多轮聚合还应对照相应 usage 结构。[4]

DeepSeek · 按命中与未命中统计
hit  = usage["prompt_cache_hit_tokens"]
miss = usage["prompt_cache_miss_tokens"]

hit_ratio = hit / (hit + miss) if hit + miss else 0

字段定义来自 DeepSeek 上下文缓存文档。批量统计时,用“全部命中 token 之和 / 全部输入 token 之和”;不同长度请求的命中百分比不宜直接做简单平均。[6]

看到的现象优先检查别急着下的结论
每轮都大量写缓存是不是把边界放在变化后缀之后?旧入口是否超出查找范围?“模型没在缓存。”
只命中很短一段第一个不同 token 在哪里?工具或系统内容是否改了?“后面资料完全相同,服务端一定有 bug。”
相同请求仍未命中写入是否完成、是否到期、隔离/路由、恢复状态、服务保证。“token 相同就一定必须命中。”
命中高但仍很慢输出长度、工具等待、缓存加载、后续注意力计算、排队。“缓存应该让整个请求零计算。”

再把费用和验收放回同一张表。如果仅作一个忽略写入溢价的输入价格模型,输入量为 N、命中率为 h,那么:

输入费用 = N × [(1 − h)pmiss + hphit]

价格 p 按每 token 计。真实账单还要加输出、缓存写入或存储等费用,且输入量 N 本身也会变化。

更有价值的最终指标是:评测期间的总费用 ÷ 通过验收的任务数,同时报告成功率、任务类型,以及耗时的中位数和较慢尾部。冷缓存与已预热的缓存应分开测,避免把不同条件混成一个漂亮平均值。

对于带近似恢复或稀疏读取的方案,评估还应覆盖恢复前后的一致性、候选遗漏带来的任务失败,以及长会话累积表现。优化目标始终是通过同一套验收;节省的字节和扫描次数,需要最终落到任务成本与完成时间上。

THE COMPLETE PICTURE

小王的一个问题,最后展开成四个层次

身份:这段历史是谁?
hash 与目录可以用短键区分路径,键后面仍要有真正可用的数据。

复用:以后能从哪里接着算?
缓存系统负责入口、准入、保留与恢复;精确复用和近似重建要分开评估。

状态:需要留下多少东西?
跨层共享、K/V 共享、位置压缩和量化,共同决定历史的体积。

读取:下一步要触碰多少历史?
分层候选、稀疏注意力与共享检索,决定“放得下”之后能否“读得起”。

Engram 又补上一条旁路:把部分已学习的模式放进可寻址的参数表。
怎样少算重复的历史?怎样找回它?怎样让它更轻?怎样只读取真正需要的部分?

READ THE SOURCE

资料、代码与算例口径

产品规则记录于 2026 年 9 月 10 日。官方文档、公开代码、所提供的技术摘录和本文推导分别标明;链接可能随上游更新。全部图形与交互用于说明机制;V4.1 条件账本不等于产品实测。

01
Hugging Face Transformers · Caching ↗

Q/K/V、逐步生成与 KV 缓存的基础机制。本文对第一层与高层依赖关系的说明,是基于该计算结构的推导。

02
vLLM · Automatic Prefix Caching 设计文档 ↗

块缓存、父块 hash、身份信息与完整块复用规则。实际引擎版本、注意力后端和混合架构可能带来额外条件。

03
vLLM · kv_cache_utils.py ↗

直接读取的公开代码;定位 hash_block_tokens,可见父块 hash 与当前 token 的组合。当前源码还出现部分块长度相关字段;正文的完整块算例有意限定为只复用整块的方案,不宣称所有新版本都只有这一种行为。

04
Anthropic · Prompt caching ↗

完整前缀、显式/自动缓存、20 个内容块的回看规则、生命周期和 usage 口径。API 行为可能随版本变化。

05
SGLang · radix_cache.py ↗

源码定位:RadixKey.match、page_aligned、RadixCache.match_prefix。存在 page_size=1 分支;匹配粒度与树节点压缩可以分别处理。

06
DeepSeek · Context Caching ↗

官方 API 的三类持久化位置、完整缓存单元匹配、A+B / A+C / A+D 示例,以及 best-effort 和 usage 字段。它是服务契约说明,不是完整缓存系统源码。

07
YOCO · You Only Cache Once(NeurIPS 2024) ↗

原始架构、共享全局 KV、前半部分的有界状态及复杂度分析。论文中的性能倍数不能直接当作 DeepSeek V4.1 的测量结果。

08
Microsoft UniLM · YOCO 参考实现 ↗

源码定位:SelfDecoder、CrossDecoder.forward、skip_cross_decoder。本文关于 Prefill 能省哪些位置的解释,与其 K/V 生成和跨层读取依赖对应。

09
DeepSeek V4.1 技术摘录 · 随文材料

来自本篇所依据的外部技术反馈,记录日期为 2026 年 9 月 10 日。该材料转述 config.json、model.py、kernel.py 和技术报告;本次没有取得这些原始文件或固定版本。专属字段及实现描述按“摘录给定条件”使用;890 字节及窗口数值为本文独立算术复算,依赖与旋转公式为条件推导。

原始模型入口:deepseek-ai/DeepSeek-V4.1-Flash ↗。仓库元数据不能代替文件内容。

展开随文技术摘录的参数与声明口径

下列为从所提供技术反馈整理的参数清单,非完整 config.json,也不声称是直接下载的原始文件。

layers = 40
kv_source_layer_ids = [2, 8, 14, 20]
source_compress_ratios = [2, 2, 2, 1]
first_two_layers_global_kv = False

main_kv_width = 512
main_kv_bits = 4
main_scale_group = 16
main_scale_bytes = 1  # E4M3(摘录)
indexer_k_width = 128
indexer_k_bits = 4
index_scale_group = 32
index_scale_bytes = 1  # UE8M0(摘录)

sliding_window = 128
swa_payload_bytes_per_value = 1
index_source_layer_ids = [2, 8, 14, 20, 24, 28, 32, 36]
decoder_candidate_blocks = 2048
candidate_block_size = 8
decoder_top_k = 512
engram_layer_ids = [1, 14]
engram_ngram_orders = [2, 3, 4]

行为转述:只有源层独立保留共享 global 状态;同一个 kv_shared 用于 K 和 V,输出应用逆 RoPE;minimal 推理循环未实现报告中的 Prefill 跳过;SWA Bounded Replay 以有效依赖范围为依据进行近似重建;报告的 SSD 体积比较使用不同于 HBM 的持久化口径。这些转述与本文可独立复算的数字分开看待。

10
DeepSeek · Engram 官方演示代码 ↗

直接读取的公开代码。定位:NgramHashMapping._get_ngram_hashes、MultiHeadEmbedding、Engram.forward、TransformerBlock.forward。可以看到多头取模寻址、训练向量表、投影、门控与短卷积;它是演示实现,不是 V4.1 的完整推理实现。

11
Engram · Conditional Memory via Scalable Lookup ↗

原论文的公开摘要与官方仓库说明:条件记忆、固定规模的 n-gram 查找、确定性寻址和主机内存预取。本文不借用其特定实验倍率来估算 V4.1 的收益。

从一个 token 的缓存,到一份共享的记忆图形与四个实验均内置于本页