一个 token 的缓存,
如何通向
一份共享的记忆?
从 KV Cache、前缀命中和 Claude 的缓存点,一路追问到 YOCO、共享状态与稀疏读取。用 22 个问答,把“存得下”和“算得起”放进同一幅图。
读完全文,要能分清:身份、复用、状态体积,以及每一步读取的代价。
阅读目录 · 6 个章节 · 22 个问答
小王:“计算好一个 token,就保存一个 token。以后用 hash 找回来,为什么还需要那么多缓存规则?”
大王:“这个方案可以成立。接下来要分别回答:存下了哪些状态,索引怎样组织,旧状态能否恢复,以及每次还要读多少历史。”
不满足于术语,追问每一步究竟能不能实现。
用计算、代码边界和反例,把问题一层层拆开。
我们的讨论从 DeepSeek V4.1 Flash 的效率出发,经过 Claude API 的 cache_control,碰到一个关键反问:前缀身份明明可以用 hash 表示,为什么总说“必须包含整个前缀”? 再向前走,YOCO 把问题推进了一层:假如每次都要保存很多层的历史,能否让这些层共用一份?
图中的 A、B、“苹果”等均为教学用 token;实际中文如何分词,取决于模型的分词器。模拟数值与真实 API 指标会明确区分。
先弄清:KV 到底是什么?
缓存的价值来自少做重复计算。理解这件事,需要先区分“读入已有输入”和“生成新的内容”。
为什么评价一个新模型,会一路聊到缓存?
设想一个会读代码、调用终端并修复错误的助手。第一次给它项目资料,第二次带回测试结果,第三次继续修改。同一份工具说明、项目约定和历史对话,会反复出现在请求中。
输入一次 → 得到一段回答
人容易只关注回答是否聪明、每秒输出多少字。
反复带回历史 → 多次计算 → 验收
同一份上下文的重复处理成本,也会影响任务能否做得起。
因此,看待 Flash 类模型,一个有用的问题是:在达到同样验收标准的前提下,完整任务花了多少钱、多久,失败后还要重试几次? 更低的输入处理成本可能改变这个答案。
KV Cache 保存的,究竟是什么?
先用大白话说:模型读过一个位置后,会留下方便后续位置查询的信息。注意力机制里,K(Key)参与决定“关注哪个位置”,V(Value)提供被读出的内容;Q(Query)表示当前这次查询。 它们都是数字向量,不是原文、回答摘要或普通数据库记录。[1]
先计算当前查询与各个 Key 的匹配程度,再按权重汇总 Value。
一次调用通常包含两种工作:Prefill 是处理已经给定的一长段输入,通常能批量、并行地计算许多位置;Decode 是在通常的自回归生成中,根据前面的结果继续产生下一个 token。缓存让后续计算可以直接使用已生成的 K/V,免去反复编码旧输入。[1]
这里的“每个 token 有对应状态”,描述的是逻辑上的对应关系。它不意味着 Prefill 要把整段输入逐 token 串行运行,也不意味着每个 token 都进行一次独立的 GPU 内存分配。
“KV 相同就命中”为什么还不够准确?
把这句话作为复用的目标没有问题。但服务端需要在重新计算 KV 之前,就判断能否复用。常见方法是给输入前缀建立可快速查找的身份,例如前缀树或 hash,再去找对应的缓存状态。[2]
对于固定权重、相同输入表示、位置与注意力规则的因果模型,相同前缀为精确复用提供了一个容易检验的充分条件。实际系统还要考虑模型或适配器身份、多模态内容,以及租户隔离等条件。[3]
充分条件不等于数学上唯一可能的条件。 某些不同输入也可能碰巧产生相同的中间值,特殊模型还可能有更强的状态等价关系。但普通前缀缓存无需证明所有等价情形;它选择一种容易保证正确的查找规则。
整个前缀,究竟要不要“整个存下来”?
这里的核心区别,是前缀的身份与前缀的计算状态。把它们分开,hash、单 token 缓存和块缓存就能放进同一张图。
一次请求,不应该只有“命中”或“没命中”吗?
一次请求可以只复用输入的前半段。假设这次总共输入 110,000 个 token,其中 100,000 个来自已有缓存,剩余 10,000 个需要重新处理,那么按 token 统计的命中率为:
这是一个教学算例。实际指标要先说明分子、分母和统计范围。
“发生过任意命中的请求占比”和“全部输入 token 中命中的占比”是两个不同指标。一百次请求每次只命中一个 token,前者也能是 100%;它未必省下了多少计算。
16 个位置里只改了第 3 个,但普通精确前缀缓存只能直接复用前 2 个。
这也解释了一个常见的工程建议:在不破坏任务语义的前提下,把稳定资料尽量放在前面,把时间戳和本轮问题等变化内容放在后面。一个很早出现的动态字段,可能让后面很长的相同文本无法直接复用。[4]
同一个“苹果”,为什么不能直接使用同一份 KV?
比较“我 喜欢 苹果”和“我 讨厌 苹果”。末尾 token 相同、位置相同,但模型处理“苹果”时读到的历史不同。前面注意力层得到的表示,会继续成为后面层计算 K/V 的输入。
F 表示模型的计算关系;它与用来查目录的 hash 函数是两件事。
因此,仅用 token_id 或 (token_id, position) 作为通用的高层 KV 缓存键,会把不同的上下文混为一谈。需要把此前的条件也区分开。
但这里还没有推出“缓存键必须把整段前缀明文存进去”。那正是下一个问题。
“B 在 A 之后”可以用 hash map 表示。这个反问对吗?
对。你给出的 parent hash 方案成立。 前缀身份完全可以递归表示。令 h₀ 编码初始身份条件,对每个 token 计算:
键只需代表“这条历史路径”,无需反复携带整段前缀明文。
vLLM 的 hash_block_tokens 就把父块 hash、当前块 token 与附加身份信息一起交给 hash 函数。这与“parent_hash + current_token”的思想一致,只是它通常以块为单位。[3]
这份状态属于谁?
Hash(parent, token) 是查找用的短标识。严格的生产实现还要处理碰撞风险和隔离条件。
这段历史计算后留下什么?
目录找到的是实际的 KV 存储或其引用。继续运行模型时,仍需要读取相关数值。
你提出的 hash map 可以指向真实 KV。这个想法并未要求“用几十字节替代全部模型状态”。关键边界是:压短标识很容易;要让模型只靠一个小状态继续工作,还需要另外的计算结构。
把 KV_B 与 Hash(A,B) 直接写成等号,会混淆二者。清楚的写法是 cache[key] → KV 或恢复状态的引用。
那为什么不每算好一个 token,就建立一个可复用入口?
可以这样做。 SGLang 的公开 RadixCache 代码支持 page_size = 1 的匹配分支;在普通单 token 键模式下,前缀匹配可以精确到一个 token。这足以说明:单 token 级索引没有普遍的理论障碍。[5]
粗一点的粒度通常是在做成本权衡。把一段连续的 K/V 放进同一个块,索引、引用计数、传输和淘汰操作就可以成组处理。在只复用完整块的简化方案中,公共前缀长为 p、块大小为 b 时:
仅由块对齐造成的额外重算 = p − r < b
这个界限只约束“粒度造成的损失”;缓存过期或状态不完整时,损失可能远大于 b−1。
理论可复用 999 个,按 16 对齐后可复用 992 个。额外重算 7 个;最后 1 个属于新后缀。
vLLM 的前缀缓存文档描述了一种完整块复用方案。b = 16 时,999 个相同 token 只能按该规则复用到 992,多算 7 个,换来更少的目录项。[2]
但不能进一步把“单 token 索引”想成“每个 token 调一次 GPU 内存分配”。物理内存池、匹配粒度与树节点表示可以分别设计;压缩前缀树也不必给每个 token 分配一个独立的树节点。更小的物理页甚至能减少尾部空洞,代价需要按实际系统测量。
大王:短键省下了重复保存整段前缀的空间,但链式构造仍有前后依赖。vLLM 的 hash_block_tokens 接受 parent_block_hash;新块的键需要先知道父块的键。对一条首次建立索引、长度为 N 的序列,块大小为 b 时,大约有 N/b 次依次相连的键构造。把 b 从 16 改为 1,就把这条链上的步骤数扩大到约 16 倍。[3]
| 块大小 b | N = 1,000,000 的完整块数 | 单个分叉边界最多多算 | 边界余数均匀时的平均值 |
|---|---|---|---|
| 1 | 1,000,000 | 0 token | 0 token |
| 16 | 62,500 | 15 token | 7.5 token |
| 64 | 15,625 | 63 token | 31.5 token |
仅当公共前缀在块内的余数近似均匀时成立。严格上界是 b − 1;b/2 只是平均量级。
这里比较的是一个分叉边界的额外 token 数与整条新路径的索引工作量。不能据此直接宣布“常数收益一定抵不过线性代价”:一个 token 的注意力成本也可能随历史长度增加,缓存还可能被许多请求重复利用。
还有两个限定:哈希仍需读取当前块的 token,扫描输入的总工作通常仍是 O(N);已有会话可以从现成父 hash 增量续算,新增索引主要对应 ΔN。压缩树、批处理和不同哈希方案也可能有不同实现。因此,应当把串行链视作这类实现的具体成本,而非所有单 token 缓存的必然缺陷。
有相同前缀,还差哪些条件才能命中?
缓存目录得找到它,数据得还在,而且留下来的状态必须足以从那个位置继续。API 的缓存点就处在这一层。
Claude 的 breakpoint 到底在“截断”什么?
更合适的中文是缓存边界或恢复点。“截断点”容易让人误会后面的内容不再输入给模型。
把 cache_control 放在某个内容块上,表达的是:从整个有效提示的开头,到这个块结束的前缀,成为候选缓存范围。Claude 文档规定相关顺序为 tools → system → messages。[4]
底层 K/V 的产生并不等待这个标记。API 标记决定服务层怎样保存、查找和计费,不能拿来推断 GPU 何时开始拥有 K/V。
多个缓存点可以对应不同稳定程度:工具和系统指令几乎不变,项目资料偶尔改变,对话不断追加。较长的入口失效时,较短的入口仍可能有效。但它们都表示“从开头到某处”,不会把后面相同的文本自动变成独立于前文的缓存片段。
既然有自动缓存,还需要手动放缓存点吗?
关键要看请求怎样变化。Claude 当前文档支持顶层 cache_control,会把缓存点放到最后一个可缓存内容块。对于不断在旧对话后追加内容的请求,这很方便。[4]
第 1 次:A + B
第 2 次:A + B + C
旧的完整前缀仍在。自动向前移动缓存点,容易找到上次的写入。
第 1 次:A + 问题 X
第 2 次:A + 问题 Y
如果只存过 A+X,A 本身未成为缓存入口,稳定资料仍可能无法命中。
第二种情况下,把显式边界放在稳定资料 A 的末尾通常更合适。自动缓存不会替你发现所有稳定片段并自动建立入口。 当前规则的回看范围也有限,最多检查每个边界附近 20 个内容块位置,查找的是先前真正写入的入口。[4]
response = client.messages.create(
model=model_id,
max_tokens=1024,
cache_control={"type": "ephemeral"},
system=stable_system,
messages=growing_history,
)
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]
为什么没有报错,usage 却全是 0?
缓存前缀必须达到模型的最小长度。当前官方文档给出的部分门槛如下;低于门槛仍会正常处理请求,不会因为未缓存而报错。[4]
| 模型 | 最小缓存前缀 |
|---|---|
| Claude Opus 5 | 512 token |
| Claude Sonnet 5 / Sonnet 4.6 | 1,024 token |
| Claude Haiku 4.5 | 4,096 token |
记录日期:2026 年 9 月 10 日。门槛看被标记的可缓存前缀长度,不只看整个请求是否够长。若 cache_creation_input_tokens 和 cache_read_input_tokens 都为 0,可先查这个条件;平台适配与模型版本仍应对照对应文档。
DeepSeek 是怎样提高实际缓存命中率的?
DeepSeek 的上下文缓存文档公开了三类持久化位置:请求输入和输出的结束位置、多个请求的共同前缀、长输入或输出中的固定 token 间隔。 后续请求要完整匹配一个已经保存的缓存前缀单元,才能复用它。[6]
用一个抽象式表达就很清楚。设两次请求客观共有的前缀长度为 p,已经保存且可恢复的边界集合为 E。在这一完整单元规则下,最长的直接恢复位置可以写成:
输入不变时,p 不由缓存策略决定;策略可以改变 E,从而改变实际可恢复到的 r。
请求边界抓住自然的继续点;公共前缀检测补上真实出现的复用位置;固定间隔为长序列提供额外覆盖。它们都在增加“可以找回并使用的状态”,不会让两个原本不同的前缀 变成相同。
上图是服务行为的解释。DeepSeek 未在这份 API 文档中公开完整缓存目录的数据结构,不能据此认定服务端一定采用某种 radix tree、hash 链或特定 checkpoint 间距。
大王:多保留一个入口,会占用索引、写入带宽、存储空间,某些模型还需要额外的恢复快照。这就需要决定“哪些状态值得占着资源”,也就是缓存准入策略(cache admission policy)。
公共前缀被重复使用,是比“它曾经出现过”更强的复用信号。资源有限时,优先保留这类前缀,可以减少只用一次的内容对热缓存的挤占。DeepSeek 文档也提示缓存构建需要数秒,因此刚完成计算与持久化入口立即可用之间,还可能存在时间差。[6]
这是解释取舍的成本模型,非 DeepSeek 公布的实际准入公式。
“再次使用后更值得保存”与 cache-on-second-access 的思想相通。DeepSeek 同时还保存请求边界和固定间隔入口,不能把整个系统概括为严格的二次访问缓存,也不能声称所有前缀都必须等重复两次才落盘。
已经缓存 A+B,为什么不能直接把 B 切掉,拿到 A?
这是比“索引太多”更深入的一问。对于保存了每层完整历史 K/V 的标准实现,这个想法可以成立: 只要数据还在,取出前 A 个位置即可;是否能找到相应入口,是另一层问题。
但很多长上下文架构会主动丢掉旧的局部状态。DeepSeek 文档也明确指出,滑动窗口注意力使它的缓存存储与匹配规则发生变化。[6]
所以,“存在 A+B 的压缩恢复状态” 并不自动推出 “A 的全部恢复状态仍可被切片拿出”。模型采用什么记忆结构,直接影响缓存系统能提供多细的恢复边界。
SSD 和 KV 压缩,又怎样影响命中率?
同一个前缀,可能因为缓存未写完、已经被淘汰、过期或隔离条件不同而无法命中。DeepSeek 当前文档描述的是默认开启的硬盘缓存,并说明构建需要时间、采用尽力而为策略,不承诺 100% 命中。[6]
同一状态占用更少空间
来自 KV 量化、压缩、共享等设计;这些方法作用的维度并不相同。
同样容量容纳更多历史
在其他条件相同的假设下,更少的缓存内容被提前淘汰。
再次访问时更可能还在
实际命中机会可能增加;存储带宽和恢复开销仍然需要计入。
上面是一条有条件的因果链,并非产品命中率提升的实测结论。缓存小了,可以选择多保留历史,也可以把资源拿去服务更多并发;最终怎么分配,取决于系统策略。
这几个条件需要同时成立。它们互相影响,不能随意当作独立概率相乘。
到这里,hash 已经完成了它擅长的事:快速识别身份。下一个问题开始碰模型架构——为什么那份需要保存的状态会那么大?
YOCO:多层能否共用一份长期记忆?
服务系统在回答“过去的计算能不能找回来”。YOCO 进一步改变“模型会留下几份长期状态”,以及哪些历史位置还需要继续计算。
传统 KV Cache 为什么会随“长度 × 层数”膨胀?
普通的全历史 Transformer 中,各层使用各自的中间表示产生 K/V。每一层都要记住整个历史,于是序列越长、层数越多,缓存越大。
2 表示 K 和 V;L 是层数,N 是长度,H_KV 是 KV 头数,d_head 是每头维度,s 是每个数值的字节数。忽略批量、元数据和其他状态。
这个公式只描述标准全历史 K/V 的一个简化形状。它也让几种优化的位置变得直观:减少 KV 头数,减少每个状态的维度,降低数值精度,减少保留的位置,或减少重复保存长期 KV 的层数。YOCO 主要从最后一项切入。[7]
少一些 KV 头
多个查询头共享少量 KV 头,例如 GQA / MQA。
窄一些状态
用更小的向量或潜在表示承载历史。
少一些比特
低精度保存数值;量化尺度也要占空间。
少一些位置
合并、压缩或只保留部分历史位置。
少一些重复层
让多个层共享同一个长期状态源。
共享 K 与 V 的载体
模型若让同一向量承担两种用途,前面的 ×2 也需要重写。
第六项要通过架构与训练来实现。普通模型中的 K 和 V 可以不同,无法在服务端把两份向量直接并成一份而保持原计算。第 18 问会用一个带位置旋转的共享方案,把这个边界讲清楚。
YOCO 的 Once,究竟是哪一种“只做一次”?
YOCO 的全称是 You Only Cache Once。原论文把模型分成前半的 Self-decoder 和后半的 Cross-decoder:前半先得到上下文表示,由它生成共享的全局 K/V;后半各层通过各自的查询读取这份记忆。[7]
“Once”说的是全局历史 K/V 只保留一套,供多个后半层复用。它不表示一个 token 在整个模型里只计算一次,不表示模型跨请求只运行一次,也不表示前半部分完全没有状态。原论文专门在脚注中说明了后一点。[7]
YOCO 的窗口变体:O(N + LselfW)
为突出长度与层数,省略头数、维度和精度。W 为局部窗口宽度;共享全局 KV 仍随 N 增长。
相同每-token状态宽度下,100,000 token 的长期记忆由 40 份变为 1 份;局部窗口另计。
因此 YOCO 也没有把整个百万 token 历史压成一个固定长度 hash。它减少的是长期记忆在层与层之间的重复保存。越长的上下文,固定大小的局部窗口所占比例越小。
两部分仍按因果顺序运行:当前位置只能使用允许看到的历史,后续新 token 还会再经过前半和后半。这里的“前半像编码器”不意味着它可以双向读到未来。[7]
为什么共享 KV,还能让 Prefill 跳过一些层?
在普通 Transformer 中,历史位置必须经过前面的层,才能生成后面每层要使用的 K/V。即便你不需要那些历史位置的输出,也很难跳过这些计算。
YOCO 改变了依赖关系:后半各层需要的历史 K/V 直接由前半部分输出提供。于是,已经给定的历史位置不必再经过整个后半部分,只为建立未来要用的缓存。公开实现中,CrossDecoder.forward 会先生成并追加共享 K/V,再根据 skip_cross_decoder 决定是否继续跑后半层。[8]
这个设计要按位置理解。若提示词有 N 个位置,要得到第一个新 token 的概率,仍需用最后一个输入位置的表示运行后半层。能够省去的是许多此前历史位置的后半计算。
只需要从已知前缀继续生成
大量历史位置只负责建立记忆。它们的后半表示既不需要输出,也不再用于生成未来的 K/V。
训练或需要逐位置输出分数
要计算各个位置的预测和损失时,相关后半计算仍然必要。具体接口能否省略,取决于它要求返回什么。
此外,前半部分采用高效的局部或递归结构,也会降低长输入处理成本。论文中出现的倍数,依赖于对照模型、序列长度与实现;不能把它们直接贴到另一个产品上。
从 YOCO 到 V4.1:存什么、重算什么、读什么?
把跨层共享、890 字节、有限重放与稀疏读取放到各自的统计口径里。每一步都区分设计条件、数学推导与运行证据。
V4.1 案例 · 资料口径
以下 V4.1 专属配置、代码行为和报告表述,依据随文提供的技术摘录;本文按这些参数复算内存,并推导依赖与复杂度。原始 config.json、model.py、kernel.py 及报告正文未在本次取得,因此这些内容不标为已独立核实的源码事实,也不作为运行实测。通用机制与已直接查到的官方 API 规则另列来源。[9]
V4.1 的“YOCO-like”,具体体现在哪里?
它可以具体展开为:哪些层生产历史,哪些层共享历史,哪些状态仍归各层自己保留。 按技术摘录的配置,40 层分为 0–19 与 20–39 两部分;global KV 源为 [2, 8, 14, 20],前两层只保留滑窗。[9]
摘录把后半段的 global KV 联系到前半段的最终表示,同时保留了各层自身的局部滑窗状态。这与 YOCO 的跨层记忆共享有明确的结构联系;前半部分内部也可以分组共享。比较模型时,应看实际的依赖边,而不只看“Encoder / Decoder”两个名字。[7][9]
| 问题 | 这组材料能够说明什么 | 仍需分清的边界 |
|---|---|---|
| 谁生产 global KV? | 摘录列出 4 个源与对应层组。 | 同源表示、同一投影与同一缓存对象,要分开辨认。 |
| Prefill 能跳过后半吗? | 报告转述描述了部署侧缩短路径;参考实现转述仍逐层执行。 | 架构可省、报告声称省、公开代码已实现省,是不同证据。 |
| 890 B/token 怎样算? | 按宽度、精度、尺度和四个源的压缩比可复算。 | 它度量 global KV 斜率,未涵盖全部显存。 |
| 128 token 能恢复吗? | 摘录将 Bounded Replay 表述为近似状态重建。 | 近似恢复需要质量评估;不能宣称数值等价。 |
890 bytes/token,究竟省了哪些东西?
先明确计算单位:每增加一个原始输入 token,需要多保留多少 global KV。 这与“整条序列共占多少显存”不同。按给定的量化和共享条件,每个压缩位置需要 356 字节。[9]
= 890 N bytes
这是长序列下的线性项;短序列需按实际压缩组、取整和内存布局计算。
这里的尺度,是把低精度数值还原到正确大小所需的系数。FP4 的每个数值占 0.5 字节,但不能只乘 0.5 就结束:main KV 的尺度贡献 32 字节,indexer K 的尺度再贡献 4 字节。报告摘录将它们分别称为 E4M3 与 UE8M0 尺度。[9]
再看局部窗口:按 40 层、每层 128 个位置、512 维且每数值 1 字节的 FP8 载体估算,SWA 的数值本体为:
= 2,621,440 bytes ≈ 2.62 MB
该项与长上下文 N 无关;还未计入 SWA 的尺度、对齐、元数据等。1 MB = 10⁶ bytes;2,621,440 bytes 也就是 2.5 MiB。
在这组条件下,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 显卡。 |
K 和 V 真的能用同一个向量吗?
K 和 V 描述两种用途:一个参与匹配,一个参与汇总。模型可以被设计成让同一个存储向量承担这两种用途;此时就少保存一份数值。技术摘录说 V4.1 的 kv_shared 同时用于匹配与输出乘法,账本按这一条件计算。[9]
旋转位置编码(RoPE)会按 token 位置旋转一部分向量,使匹配能表达相对位置。如果作为 Key 的旋转向量同时也是 Value,那么汇总后的输出也带有这些旋转。对查询位置 i 的输出乘 Ri−1,可以把它表达在当前位置的坐标系里。
= ∑j αij Rj−icj
使用同一组 RoPE 频率时,旋转矩阵满足 Rᵢ⁻¹Rⱼ = Rⱼ₋ᵢ。这个等式是线性代数推导。
同理,num_key_value_heads = 1 本身只说明 KV 头少,不能单独证明 K 与 V 是同一个向量。是否共用,需要继续看实际投影、张量引用和两个矩阵乘法的输入。
只重放最后 128 个 token,就能精确恢复吗?
分成两个问题看:不保存的局部状态怎么补回来;补回来的状态需要多精确。 单层只看最近 W 个位置,多层叠加后的理论依赖范围通常会扩大。对窗口包含当前位置的简单 L 层堆叠,单个输出位置的感受野可达到 1 + L(W−1)。
技术摘录将 V4.1 的 SWA Bounded Replay 描述为:为避免保存各层 SWA 状态,只重放一个窗口来近似恢复。摘录同时指出,报告承认实际有效依赖可能短于理论感受野,并将这类恢复列为需要进一步刻画的鲁棒性边界。[9]
大王:需要。YOCO 的可见参考实现有 skip_cross_decoder,可以沿共享 KV 的依赖关系跳过许多历史位置的后半计算。V4.1 的技术摘录则区分了两条路径:
对输入位置逐层运行
Transformer.forward 仍执行全部 40 层。这个行为能展示模型计算,不足以复现部署侧早退。
前半编码 + 短尾部重放
省去大段后半历史位置,保留生成首个输出所需的路径,并用有限重放补局部状态。
表达“运行了多少层 × 多少位置”的结构账本;这里将每个层位置的工作近似同权。它不自动包含全部注意力扫描、稀疏索引、通信和排队代价。
因此,从“架构允许短路径”走到“服务端实际更快”,还要经过调度实现与测量。最后一个输入位置需要产生首个新 token 的概率;逐位置打分或训练任务也不能直接套用只生成下一个 token 的跳过方式。
百万 token 存得下,就一定算得起吗?
还差一个决定速度的问题:每生成一个 token,究竟需要触碰多少历史状态? 把所有 KV 从 10 GB 压到 1 GB 会减少容量压力;如果仍在许多层反复扫描完整历史,读取和匹配依然会很贵。
根据技术摘录,layer 20 先建立候选池;后面的若干 indexer 只在这个池内挑选 top-512,另一些层复用选择结果。这样就能把“每层都查一遍全历史”的重复工作减掉。[9]
C ≤ 16,384;m 为执行重排的层数
只是结构表达。候选选择的具体算法、Top-K 内核和常数仍决定真实时间。
稀疏读取还有能力取舍:相关位置如果没有进入候选池,后续精排通常无法把它凭空找回来。因此需要同时看候选召回、答案质量与真实延迟。共享 KV 解决“存几份”,共享候选解决“找几遍”,稀疏读取解决“每次读多少”。
Engram 是不是让 hash 直接找到“知识”?
Engram 给模型增加一种“遇到某个局部组合,就读取已学习向量”的能力。官方公开实现先把 n-gram 映射到若干哈希地址,再取嵌入向量,随后通过投影、由当前隐藏状态控制的门以及局部融合,注入主干网络。[10]
这里的 n-gram,指连续 n 个 token 的局部组合。固定模型和表大小后,每个位置只读取有限数量的表项;地址仅依赖局部 token,因此可以在相应深层计算发生前准备,并适合预取。官方论文还讨论了将大型表放在主机内存中的系统安排。[11]
“把每个 n-gram 都存下来”也需要限定:哈希表容量有限,不是给所有可能组合各建一个无碰撞的槽。不同组合可以落到相同地址,多头哈希和训练共同处理这种压缩。与精确 KV 缓存尽量避免身份误匹配的目标相比,这里的碰撞本身属于表示设计要面对的条件。[10]
V4.1 技术摘录列出在层号 1、14 使用 2/3/4-gram 记忆。这描述的是特定装配;原始 Engram 演示代码有另一组配置。无论具体装在哪层,它都不会绕过“上下文不同,高层 KV 可能不同”的事实:静态表项与动态隐藏状态融合后,仍能产生不同结果。[9][10]
于是,模型里的“记忆”至少包含两类东西:过去这个请求算出了什么,以及训练阶段为常见模式学到了什么。一个由缓存系统管理生命周期,一个通常跟随模型权重;两者可以同时存在。
理解之后,怎样用到自己的 Agent?
最终目标很具体:让正确的任务以更低成本、更可接受的等待时间完成。漂亮的缓存指标,只是中间证据。
实际调用 API 时,我应该改什么、看什么?
先画出每次请求的真实输入顺序,标出最早变化的位置。对于追加式对话,尽量保留已有内容的原样顺序;对固定资料的不同问题,把稳定资料与动态后缀分开,并根据服务规则放置缓存边界。
随后看实际 usage,区分“读缓存”“写缓存”和“普通输入”。不要把供应商同名的 input_tokens 想当然当作同一种总量。
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]
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,那么:
价格 p 按每 token 计。真实账单还要加输出、缓存写入或存储等费用,且输入量 N 本身也会变化。
更有价值的最终指标是:评测期间的总费用 ÷ 通过验收的任务数,同时报告成功率、任务类型,以及耗时的中位数和较慢尾部。冷缓存与已预热的缓存应分开测,避免把不同条件混成一个漂亮平均值。
对于带近似恢复或稀疏读取的方案,评估还应覆盖恢复前后的一致性、候选遗漏带来的任务失败,以及长会话累积表现。优化目标始终是通过同一套验收;节省的字节和扫描次数,需要最终落到任务成本与完成时间上。
小王的一个问题,最后展开成四个层次
身份:这段历史是谁?
hash 与目录可以用短键区分路径,键后面仍要有真正可用的数据。
复用:以后能从哪里接着算?
缓存系统负责入口、准入、保留与恢复;精确复用和近似重建要分开评估。
状态:需要留下多少东西?
跨层共享、K/V 共享、位置压缩和量化,共同决定历史的体积。
读取:下一步要触碰多少历史?
分层候选、稀疏注意力与共享检索,决定“放得下”之后能否“读得起”。
Engram 又补上一条旁路:把部分已学习的模式放进可寻址的参数表。
怎样少算重复的历史?怎样找回它?怎样让它更轻?怎样只读取真正需要的部分?
资料、代码与算例口径
产品规则记录于 2026 年 9 月 10 日。官方文档、公开代码、所提供的技术摘录和本文推导分别标明;链接可能随上游更新。全部图形与交互用于说明机制;V4.1 条件账本不等于产品实测。
Q/K/V、逐步生成与 KV 缓存的基础机制。本文对第一层与高层依赖关系的说明,是基于该计算结构的推导。
块缓存、父块 hash、身份信息与完整块复用规则。实际引擎版本、注意力后端和混合架构可能带来额外条件。
直接读取的公开代码;定位 hash_block_tokens,可见父块 hash 与当前 token 的组合。当前源码还出现部分块长度相关字段;正文的完整块算例有意限定为只复用整块的方案,不宣称所有新版本都只有这一种行为。
完整前缀、显式/自动缓存、20 个内容块的回看规则、生命周期和 usage 口径。API 行为可能随版本变化。
源码定位:RadixKey.match、page_aligned、RadixCache.match_prefix。存在 page_size=1 分支;匹配粒度与树节点压缩可以分别处理。
官方 API 的三类持久化位置、完整缓存单元匹配、A+B / A+C / A+D 示例,以及 best-effort 和 usage 字段。它是服务契约说明,不是完整缓存系统源码。
原始架构、共享全局 KV、前半部分的有界状态及复杂度分析。论文中的性能倍数不能直接当作 DeepSeek V4.1 的测量结果。
源码定位:SelfDecoder、CrossDecoder.forward、skip_cross_decoder。本文关于 Prefill 能省哪些位置的解释,与其 K/V 生成和跨层读取依赖对应。
来自本篇所依据的外部技术反馈,记录日期为 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 的持久化口径。这些转述与本文可独立复算的数字分开看待。
直接读取的公开代码。定位:NgramHashMapping._get_ngram_hashes、MultiHeadEmbedding、Engram.forward、TransformerBlock.forward。可以看到多头取模寻址、训练向量表、投影、门控与短卷积;它是演示实现,不是 V4.1 的完整推理实现。
原论文的公开摘要与官方仓库说明:条件记忆、固定规模的 n-gram 查找、确定性寻址和主机内存预取。本文不借用其特定实验倍率来估算 V4.1 的收益。