【轻量化复刻大模型 / KIMI K3】KDA 对 AI Infra 的挑战与各家应对
【轻量化复刻大模型 / KIMI K3】KDA 对 AI Infra 的挑战与各家应对
- 【轻量化复刻大模型 / KIMI K3】KDA 对 AI Infra 的挑战与各家应对
- 0x00 概要
- 0x01. 先建立正确的心智模型
- 0x02. KDA 的优势与结构性限制
- 0x03. 为什么 Prefix Cache 与 KV Cache 不再等价
- 0x04. KDA 给推理系统带来的新挑战
- 0x05. SGLang 的应对思路
- 0x06. SGLang 源码导航
- 0x07. vLLM 的应对思路
- 0x08. vLLM 源码导航
- 0x09. 深入讨论
- 0x10. 最终结论
- 0xFF 参考
0x00 概要
KDA 的固定状态设计让标准 serving 栈的若干组件失效,因此,各大推理引擎都对其进行适配:
| 基础设施层 | 标准假设 | KDA 挑战 | 应对(参考实现) |
|---|---|---|---|
| 前缀缓存 Prefix Cache | 缓存 KV 前缀 | 需缓存"状态 S"(无逐 token 条目) | SGLang radix cache 纳入 temporal checkpoint(copy-on-write);kimi3-master prefix_cache.py 两级 hash |
| KV 池 Memory Pool | 统一 KV 页 | 混合模型(KDA+MLA)异构缓存 | SGLang MambaPool(conv + temporal)vs MLA KV 池;kimi3-master paged_cache.py::PageKind |
| 上下文并行 CP | KV 广播 | 状态跨段不可加 | KCP 前缀扫描 |
| varlen 批量 | cu_seqlens | 每序列独立状态推进 | cu_seqlens + 段偏移 |
| 投机解码 | KV 截断回滚 | 状态需回滚/分支/重放 | SGLang ReplaySSM(轨迹重放);kimi3-master ReplaySSM |
| CUDA Graph | 静态 KV 布局 | 递推状态无法标准捕获 | ReplaySSM 折叠机制 |
本文前半篇用概念性语言解释 KDA 为什么改变了推理缓存的正确性条件,以及推理框架如何在不破坏正确性的前提下节省显存和重算;后半篇给出源码导航,对应到 SGLang 与 vLLM 的具体文件与关键函数。
注:
- 项目https://github.com/RossiLH/Model-Viz 和系列博客是我的一个尝试,也是我学习的过程。不确定是否满足读者的需求,我自己会反思改进,也希望大家提出宝贵意见。
- 涉及 KIMI K3,DeepSeek V4,PI0.5,PI0.7 等模型。后续可能会跟踪新模型。
- 全 CPU 复刻,不涉及 GPU,也不涉及大规模训练(这是另外一个庞杂深邃的领域)和硬件。
- 模型的组件尽量是原始大模型的缩小版,如果组件没有开源版本,则用近似组件替代。
- 会解释每个组件在解决一个什么问题、这个问题难在哪、为什么只能这么解、解法的原理是什么,以及组件之间如何咬合成一个整体。重点不是记名词,而是理解一条主线。
- 每个主要组件都有可直接运行的 Gradio demo——无需 GPU。另有若干可视化脚本(
plot_*.py),可生成静态图。 - 所有文件行号基于本地源码快照;源码后续变化时,行号可能移动,但函数名和调用关系仍可作为定位入口。
- 为了保证每个文章可以尽量独立成文,所以有些内容会在不同文章中都存在,可能在系列层面上存在冗余,还请大家理解。
0x01. 先建立正确的心智模型
1.1 混合注意力的完整执行链
Kimi K3 的混合注意力可以粗略画成:
输入 token
├─ MLA ──> token-addressable KV/latent history
└─ KDA ──> convolution state
└── recurrent state
└── KDA 输出
└──────────> 混合输出
K3 特色
MLA 仍保留可按 token 或 block 访问的历史;
KDA 不保存每个历史 token 的完整地址,而是逐步更新一个固定大小的状态:
S_t = M_t S_{t-1} + b_t
S_{t-1}:前一时刻长期记忆;M_t:当前 token 决定旧内容保留、衰减或变换多少;b_t:当前 token 写入的内容;S_t:处理完当前 token 后的长期状态。
KDA 还常使用短卷积,因此要保存最近若干 token 的 convolution state。
区别
因此,传统 Transformer 的 KV Cache 可以看成 "按 token 编号的历史档案":
token 0 -> K,V
token 1 -> K,V
token 2 -> K,V
...
新 Query 到来时,系统可以读取任意历史 token。前缀缓存通常可以理解为:token 前缀相同,就能复用对应的 KV block。
而 KDA 则把历史持续写入一个固定大小的 recurrent state。
所以,KDA 不是简单地把 KV Cache 换成另一种张量,而是把 "模型状态" 从一种扩展成了多种语义不同的对象。
1.2 三种状态对比
| 状态 | 记住什么 | 是否随新 token 更新 | 典型恢复方式 | 是否可按 token 任意截断 |
|---|---|---|---|---|
| MLA KV | 历史 token 的可寻址表示 | 追加 | 读取 KV block | 基本可以 |
| KDA recurrent state | 整个前缀压缩后的长期摘要 | 是 | 恢复 checkpoint | 不可以随意截断 |
| KDA convolution state | 最近局部窗口 | 是 | 恢复局部窗口快照 | 需在合法边界保存 |
Recurrent state 决定长期记忆,convolution state 决定下一步局部卷积输出。两者都影响后续 token,因此不能只恢复 MLA KV。
1.3 KDA 的顺序性:可分段但必须保序
对于KDA,我们把 token 段 C 对状态的整体影响记为:
F_C(S) = M_C S + b_C
两段连续执行的合并是:
F_{C_2}(F_{C_1}(S)) = M_{C_2} M_{C_1} S + M_{C_2} b_{C_1} + b_{C_2}
中间的 M_{C_2} b_{C_1} 很重要:第一段写入的信息,还要接受第二段的衰减或擦除。
这解释了两件事:
- 可以把一段 token 浓缩为一个
(M, b),所以能 chunkwise 并行; - 不能把任意两个 token 段交换顺序,因为后一段会作用于前一段的写入。
它满足结合律,所以可以先算小段、再合并;通常不满足交换律,所以 token 顺序不能随意调换。这个 "可分段但必须保序" 的性质,正是 KDA 能并行又能编码时序的原因。
Checkpoint 保存的就是某个前缀边界的结果,例如:
token boundary 4096
├─ MLA KV prefix
├─ KDA recurrent state S_4096
└─ KDA convolution state C_4096
0x02. KDA 的优势与结构性限制
2.1 优势
KDA 把长历史压进固定大小 state,主要收益是:
- decode 不必反复扫描完整历史;
- 历史读取带宽不再随上下文长度线性增长;
- 状态可按 chunk 计算并通过仿射复合合并;
- 衰减可以表达慢遗忘;
- delta 更新可以沿当前 key 定向覆写;
- 适合作为长程 mixing 分支。
2.2 代价:固定状态不是完整档案
状态是压缩摘要,不是完整档案。一个固定 state 必须让任意长历史竞争有限容量:
很多 token
↓ 压缩
一个固定状态矩阵
↓
相似 key 可能碰撞
旧信息可能被衰减
未来 query 不能重新寻找原 token
两个相似 key 可能写入相近方向并互相干扰;一旦旧信息被衰减或覆盖,未来 query 无法像 Full Attention 那样重新找到原 token。
因此 KDA 的 state 更像 "持续更新的寄存器文件",而 Full/MLA KV 更像 "可按地址读取的缓存"。Kimi K3 同时使用两者,正是为了让 KDA 负责高效 mixing、MLA 负责精确检索。
KDA 的优势不是 "在所有任务上替代 Full Attention",而是用固定状态承担大多数长程 mixing,再由 MLA 等 token‑addressable 分支补足精确检索。
0x03. 为什么 Prefix Cache 与 KV Cache 不再等价
3.1 普通 Prefix Cache 为什么简单
普通 Transformer 的 KV Cache 近似是一个追加式数组:
prefix tokens: [A, B, C, D]
KV cache: [K_A,V_A] [K_B,V_B] [K_C,V_C] [K_D,V_D]
后续生成 E 时,只会追加 [K_E,V_E],不会修改 A、B、C、D 的 KV。因此:
token 前缀相同 ≈ 对应 KV 相同 ≈ 可以直接复用
这就是 "Prefix Cache 与 KV Cache 几乎等价" 的来源。在普通 Transformer 中,前缀的 KV 是 "写完即稳定" 的:后续 token 不会修改前面 token 的 K/V。只要 token 前缀一致,缓存内容就一致。
3.2 KDA 为什么打破这个等价关系
KDA 的 state 不是每个 token 的独立记录,而是整个前缀递推后的结果:
S_n = F_n(F_{n-1}(… F_2(F_1(S_0))))
其中每个 F_t 都代表一个 token 的状态更新。
假设两个请求共享:
请求 1: A B C D E
请求 2: A B C D X
它们在 A B C D 处的 KDA state 可以共享,但在 E 或 X 之后必须分开。共享的是一个稳定的前缀快照,不是可以被两个请求同时原地修改的活动状态。
更重要的是,若系统只知道 token 前缀命中到 D,却没有保存 S_D,就不能仅凭 D 的 token 恢复 KDA。系统必须从更早的 checkpoint 重新执行:
checkpoint at B
↓ replay C, D
↓ obtain S_D
↓ continue E or X
因此,对于 KIMI K3 来说,恢复位置 n 必须同时知道:
- MLA 在位置 n 以前的完整 KV;
- KDA 在位置 n 的 recurrent state;
- 卷积分支在位置 n 的 convolution state;
- 所有状态确实来自同一条 token 前缀和同一套模型执行路径。
单独命中 token 前缀,只能说明 "文本相同",不能证明 "递推状态已保存并且可恢复"。
常见疑问:为何 token 前缀命中并不意味着 KDA 缓存可以恢复?
Radix Tree 或 prefix hash 可能只证明 token 0 … token N 相同,但恢复还需要:
S_N已保存、C_N已保存或可重建、MLA KV_N 可用。若 state 只在 0、4096、8192 保存,而 token 匹配到 6000,系统不能凭文本直接得到S_6000,只能退到 4096 并 replay 4096‑6000。
0x04. KDA 给推理系统带来的新挑战
4.1 缓存对象从一种变成多种
系统不再只管理 KV block,而要管理:
| 状态 | 语义 | 是否可按 token 任意截断 | 典型管理方式 |
|---|---|---|---|
| MLA KV | 可寻址历史 | 基本可以 | token/block cache |
| KDA recurrent state | 前缀递推摘要 | 不可以随意截断 | checkpoint/state block |
| KDA convolution state | 局部卷积上下文 | 需在合法边界保存 | small rolling state |
挑战不只是 "多了一种 cache",而是这些对象的语义、更新方式、可共享性都不同。
一个可扩展的缓存条目至少应包含:
CacheEntry {
model_version
prefix_range
tokenization_fingerprint
state_kind: mla_kv | kda_recurrent | kda_conv
physical_location
dtype / layout
writable
parent_checkpoint
}
其中最关键的是 prefix_range 和 state_kind。没有边界,就无法判断恢复是否连续;没有类型,就无法区分 "可寻址历史" 和 "顺序摘要"。
因此,核心是把缓存键从 "token prefix" 升级为:
(model/version, tokenizer, prefix boundary,
MLA KV, KDA recurrent state, convolution state)
实际系统还要处理:
- 活动 state 与共享 checkpoint 分离;
- state 压缩和恢复;
- checkpoint 淘汰;
- 本地与远端缓存协调;
- 不同并行 rank 的对象对齐。
4.2 状态恢复必须满足边界一致性
一个合法恢复点不是 "最长 token 前缀" 这么简单,而是一个所有相关状态都可用的共同边界。若 MLA KV 恢复到 4096 token,而 KDA state 只恢复到 3968 token,后续计算就不再等价于连续执行。
混合模型的输出是多个分支共同决定的。假设:
MLA 恢复到 N=6000
KDA 恢复到 N=4096
那么两个分支看到的历史长度不同,后续混合结果自然不是连续执行在 N=6000 的结果。因此恢复对象必须像一张 "同一时间点的系统快照",而不是几个只按长度看起来相近的缓存片段。
常见疑问:为何 KDA 状态不能像 KV 一样任意截断?
截断 KV 意味着以后不再读取某些 token;剩余 KV 仍然是有效记录。截断 KDA state 则不同。state 的每个元素都可能是很多历史 token 混合后的结果。你不能从一个大 state 中随便删掉前 10% 的 "历史",因为它没有独立 token 地址。正确做法只有两种:使用已保存的更早 checkpoint,再 replay;或者使用已经在目标边界保存的完整 state。
4.3 活动态不能直接共享
MLA KV 通常写入后不变,可以被多个请求引用。KDA state 会随着 decode 继续更新,如果多个请求直接指向同一份可写 state,一个请求的更新就会污染其他请求。
因此必须把 "共享的稳定 checkpoint" 和 "请求自己的活动副本" 分开。共享 checkpoint 只读,活动 state 私有可写。
4.4 PD/EPD 分离不再只搬 KV
传统 Prefill‑Decode 分离主要传输 KV blocks。KDA 需要传输或重建 recurrent state、convolution state,结合多模态后还要考虑 Encoder 产生的视觉 embedding。于是接口抽象从:
KV Cache transfer
扩展为:
model state transfer
通信层、调度层和缓存层都必须知道状态类型、版本、边界和生命周期。
0x05. SGLang 的应对思路
SGLang 在 Radix Tree 的前缀匹配能力上增加 KDA‑aware checkpoint。Radix Tree 仍负责回答 "哪些 token 前缀匹配",但只有带有对应 KDA 状态的节点,才是可直接恢复的执行点。这相当于把缓存节点从 prefix -> KV blocks 扩展成:
prefix boundary -> MLA KV + KDA recurrent state (+ convolution state)
5.1 KDA‑aware Radix Tree
SGLang 原有 Radix Tree 擅长匹配请求之间的 token 前缀。KDA‑aware 之后,每个候选节点还要知道是否有可恢复 state:
Radix Tree
root
└─ system prompt
└─ tools
├─ user branch A
└─ user branch B
每个节点可能带:token prefix、MLA KV blocks、KDA checkpoint、checkpoint boundary。
匹配流程变成:
- 找最长 token prefix
- 沿匹配路径向前找最近合法 KDA checkpoint
- 取同一边界的 MLA KV + KDA state
- 把命中后的尾部重新计算
因此最长 token 命中点和最终恢复点可能不同。它把缓存正确性从字符串匹配提升为模型状态匹配。
常见疑问:SGLang 的 KDA‑aware Checkpoint 是什么?
普通 Radix Tree 节点回答 "前缀匹配到哪里";KDA‑aware 节点还回答 "这里有没有对应可恢复 state"。只有 token 匹配和状态匹配同时成立,节点才是有效恢复点。命中流程是:匹配 token 路径 → 回退到最近合法 checkpoint → 联合恢复 MLA/KDA → replay 剩余 token。
5.2 Sparse Checkpoint:为什么不是简单等间隔
如果每个 token 都保存完整 KDA state,空间会快速膨胀。SGLang 因而采用稀疏 checkpoint:只在部分边界保存状态,而且不是简单地全局等间隔采样。
策略包含三类位置:
- Prefill:优先 chunk 边界;
- Decode:按固定间隔生成 checkpoint;
- Radix 分叉:多条请求共享 / 分叉的位置优先保留。
原因是三类位置的复用概率不同:
- Prefill chunk 边界本来就是执行边界,保存它不需要额外制造同步点;
- Decode 每个 token 都可能成为未来前缀,但每 token 保存太贵,所以抽样;
- 分叉点已经被多个请求证明有复用价值。
之后还要限制单条路径的 checkpoint 数量,并用 LRU 回收长期未访问的状态。
因此,它本质上是在缓存预算和重算成本之间做优化,而不是简单追求最大命中率。不平均地保存所有位置,而是利用执行结构和访问模式,把 checkpoint 当成有限预算下的 "缓存投资"。
常见疑问:SGLang 的稀疏 Checkpoint 如何解决空间 / 命中率矛盾?
不保存每个 token 的 state,而是选择少量高价值边界保存。这样减少驻留开销,再用命中后的局部重算弥补较粗恢复粒度。Prefill chunk 边界天然适合保存;Decode 通过间隔控制数量;Radix 分叉点通常有更高复用概率;LRU 回收冷 checkpoint;路径预算防止单条长路径吞噬显存。
5.3 Compressed Checkpoint
并非所有 checkpoint 都同样有价值。SGLang 将状态分成:
运行时活动池:当前请求正在更新,保持原始精度
缓存池:Radix Tree 上等待复用,可将 temporal recurrent state 压缩为 INT8
局部窗口状态体积较小,通常保持原始精度。命中压缩 checkpoint 时:
INT8 checkpoint
↓ dequantize
新的活动 state slot
↓ 以原始精度继续 decode
这不是让 KDA 永久在 INT8 上递推,而是只压缩 "停放在缓存里的快照",避免连续量化 / 反量化造成误差累积。
SGLang 将正在执行的活动状态与 Radix Tree 中的缓存状态分离;不活跃 checkpoint 可放入独立缓存池,并把主要占空间的 KDA temporal state 压缩为 INT8,而较小的局部窗口状态保持原始精度。
命中压缩 checkpoint 时,先解压到新的活动 state 槽位,再以原始精度继续 token 更新。这样量化误差主要发生在缓存驻留和恢复处,不会在连续 decode 中反复累积。它仍然是有损恢复,需要在质量和容量之间权衡。
常见疑问:Compressed Checkpoint 做什么?
它降低低热度 checkpoint 的存储成本,把有限显存留给高复用状态。主要的 temporal recurrent state 使用 INT8,较小的局部窗口状态保持原始精度。命中时先解压到活动槽,再恢复正常精度执行。这样节省驻留空间,但必须接受量化恢复误差,并为不同通道保存合适的量化参数。
需要注意:INT8 checkpoint 是可选优化,不是所有 SGLang 部署都默认启用。
5.4 COW、Snapshot 与 Donate
三者共同解决 "共享缓存必须稳定、执行状态必须可变" 的矛盾。
Radix Tree shared checkpoint
├─ request A: copy-on-write -> private active state
└─ request B: copy-on-write -> private active state
- Snapshot:把请求当前状态冻结,挂到 Radix Tree,成为未来可复用快照;
- Copy‑on‑write:命中共享快照时先复制或建立私有副本,后续更新不污染共享快照;
- Donate:快照完成后直接转移 state slot 的所有权,只改索引,不再复制整块数据。
假设两个请求都命中 S_4096:
共享 checkpoint S_4096
├─ A: private S_A
└─ B: private S_B
Snapshot 负责制造稳定的共享版本;COW 负责首次写入时保护共享版本;Donate 负责把已经完成的可写 state 直接转交缓存树,避免复制。Donate 的价值在于把 "复制一大块 state" 变成 "转交一个内存槽位"。核心原则:共享版本只读,活动版本可写。
常见疑问:COW、Snapshot、Donate 如何隔离状态?
Snapshot 固定共享的历史,活动副本负责继续递推;COW 在首次写入时复制;Donate 在旧请求结束后转交可复用的活动存储。这样共享 checkpoint 不被修改,执行又无需每步全量复制。
5.5 Unified Memory
Unified Memory 把 MLA KV block 和 KDA state block 放进同一个物理显存池,但保留各自的逻辑结构和分配单位。
显存池
MLA KV blocks -----> <----- KDA state blocks
共享空闲区域/动态边界
MLA KV 和 KDA state 仍然有不同的逻辑类型、大小和生命周期;统一的是底层容量管理。这样可以避免预先把显存永久切成两个比例失衡的池。
系统可以按实时负载动态调整两者占用,避免某一类缓存独占固定容量。统一的是物理容量管理,不是把两类状态混成同一种对象。
好处是容量可以随负载倾斜;代价是底层分配器需要处理不同大小对象、空洞回填和碎片问题。
常见疑问:Unified Memory 是什么?
它不是把 MLA KV 和 KDA state 变成同一种数据,而是让它们从同一个物理显存预算中分配。KV manager 和 State manager 在逻辑上仍然是两个,但共享物理容量池。工作负载偏向 MLA 时可以多留 KV;偏向 KDA 时可以多留 state。
5.6 Mooncake 的职责边界
职责边界可以画成:
SGLang
├─ 判断什么是合法恢复点
├─ 生成/恢复 KDA checkpoint
└─ 管理本地请求状态
↓
Mooncake / HiCache
├─ 保存已确认合法的缓存对象
├─ 跨实例传输
└─ 管理更大范围的共享缓存
Mooncake 不需要重新理解 KDA 的递推数学;它只需要可靠地存储和搬运带有边界、类型和版本信息的对象。
0x06. SGLang 源码导航
本章不是概念性总结,而是源码导航。所有文件行号基于本地源码快照;源码后续变化时,行号可能移动,但函数名和调用关系仍可作为定位入口。
源码中的 KDA 支持不是一个单独的 KDACache 类,而是分散在四层:
模型层:
产生 KDA 的 q/k/v/g/beta
Kernel/backend 层:
prefill、decode、conv、recurrent update
状态池层:
recurrent state、convolution state、checkpoint、活动副本
调度/缓存层:
prefix hit、state index、checkpoint 边界、跨实例传输
最容易误解的一点是:SGLang 的 "checkpoint pool" 不是普通 KV pool。它保存的是可恢复的 KDA 执行状态。
6.1 源码地图
| 文件 | 关键函数 / 类 | 作用 |
|---|---|---|
sglang/srt/models/kimi_k3.py |
KimiK3DeltaAttention.forward_qkvbg、forward_qkvbg_fused、forward |
K3 模型侧生成 KDA 输入、接入 attention backend |
sglang/srt/layers/radix_linear_attention.py |
RadixLinearAttention.forward、unified_linear_attention_with_output |
将线性注意力结果接入 forward batch 与 cache location |
sglang/srt/layers/attention/linear/kda_backend.py |
KDAKernelDispatcher.decode、extend、target_verify |
按 decode/prefill/spec‑verify 选择 KDA kernel |
sglang/srt/mem_cache/mamba_checkpoint_pool.py |
Int8CheckpointStore、MambaCheckpointPool.store_from_active、load_to_active |
缓存状态、INT8 压缩、恢复到活动池 |
sglang/srt/mem_cache/memory_pool.py |
HybridReqToTokenPool.__init__、donate_mamba_ping_pong_slot、clear |
活动态分配、checkpoint pool 接入、Donate 与释放 |
sglang/srt/managers/schedule_batch.py |
_mamba_radix_cache_v2_req_prepare_for_extend、mamba_lazy_prealloc_at_boundary |
选择 checkpoint 边界、准备 ping‑pong state |
sglang/srt/mem_cache/unified_memory_pool.py |
UnifiedKVPool、MambaSubPoolSpec |
MLA/MHA 与 Mamba state 共享物理 byte buffer |
6.2 模型层:KimiK3DeltaAttention 不是只做矩阵乘
文件:sglang-main/python/sglang/srt/models/kimi_k3.py
关键位置:
forward_qkvbg:第 1679 行起;forward_qkvbg_fused:第 1773 行起;forward:第 1821 行起。
核心逻辑:
hidden_states
↓
投影出 mixed_qkv、gates、beta、f/a 等 KDA 输入
↓
KDA/MLA attention
↓
AttnRes、output gate、o_proj
文件顶部第 1‑7 行直接声明了 K3‑specific features,包括 Attention Residual、Latent MoE、MLA output gate 和 full‑rank KDA gate。这说明 Infra 不能只按 "一个普通 GDN 层" 处理 K3。
6.3 统一入口:RadixLinearAttention.forward
文件:sglang-main/python/sglang/srt/layers/radix_linear_attention.py:38-121
函数:RadixLinearAttention.forward(...)
简化逻辑:
如果是 extend/prefill 且启用了 piecewise CUDA graph:
走 unified_linear_attention_with_output
否则:
交给 get_attn_backend().forward(...)
源码第 86‑121 行展示了这个分支。第 142‑156 行的 unified_linear_attention_with_output 临时缩窄 forward_batch.out_cache_loc,调用 backend,再把输出写回。
工程含义:
- KDA 结果必须写入请求对应的 cache location;
- cache location 属于 batch 元数据,不是 kernel 内部的临时变量;
- CUDA graph/piecewise 路径也必须保持同一状态写入语义。
6.4 KDAKernelDispatcher:不同阶段用不同 kernel
文件:sglang-main/python/sglang/srt/layers/attention/linear/kda_backend.py:39-202
类:KDAKernelDispatcher
关键函数:
__init__:第 42‑202 行,选择 decode/prefill/verify backend;packed_decode:第 204‑235 行;decode:第 237‑264 行;target_verify:第 266‑307 行;extend:第 309‑332 行。
简化调用图:
decode:
Triton / Helion / CuTe DSL / FlashInfer
prefill:
Triton / FlashKDA / CuTe DSL / NVIDIA KDA / PTX
speculative verify:
Triton / FlashInfer / nv_cutedsl 路径
源码第 64‑190 行明确列出这些 backend 和 fallback 条件。第 237‑264 行的 decode 传入 ssm_states、cache_indices;第 309‑332 行的 extend 传入同样的状态索引。
这证明:KDA state 是 kernel 的显式输入,而不是 "模型自己在 Python 对象里隐式保存" 的东西。
6.5 SGLang 的 state 不是只有 recurrent state
kda_backend.py 第 6‑18 行导入:
causal_conv1d_fncausal_conv1d_updateTritonKDAKernelbuild_verify_intermediate_state_indices
所以一次 KDA 执行同时涉及:
卷积窗口状态
recurrent SSM/KDA 状态
speculative verify 中间状态索引
常见疑问:recurrent state 和 convolution state 是什么?SGLang 有对应吗?
Recurrent state 是 KDA 长期递推矩阵,携带压缩后的长历史;convolution state 是短卷积需要的最近窗口,携带局部连续性。两者可以采用不同存储策略:temporal state 可压缩,局部窗口通常保留原精度。SGLang 的 KDA‑aware checkpoint 主要围绕 recurrent state 展开,但合法恢复不能只恢复它;对应局部窗口状态也要恢复或按明确规则重建。
6.6 Checkpoint Pool 的真实实现
6.6.1 设计分界:共享 checkpoint 只读,活动 state 私有可写
文件:sglang-main/python/sglang/srt/mem_cache/mamba_checkpoint_pool.py:15-45
模块说明明确区分:
cached states:
radix-owned
idle
compressed
active MambaPool:
running requests
full precision
kernel-facing
这正是 Copy‑on‑write 的根本:共享 checkpoint 只读,活动 state 私有可写。
6.6.2 Int8CheckpointStore
文件:sglang-main/python/sglang/srt/mem_cache/mamba_checkpoint_pool.py:60-184
关键函数:
__init__:第 79‑113 行,分配qdata和scale;quantize:第 117‑134 行;dequantize:第 136‑138 行;store:第 142‑149 行;load:第 151‑153 行;copy_to_pool:第 155‑164 行;store_from_pool:第 166‑174 行。
数据布局:
qdata:
[layers, slots, heads, d_v, d_k] int8
scale:
[layers, slots, heads, 1, d_k]
第 127 行沿 d_v 做 max‑abs,第 128‑133 行完成对称量化。第 164 行把 checkpoint 解量化后直接写入 active pool,这就是 cache hit 时的 COW 恢复。
重要工程结论:
checkpoint 命中:
int8 → 一次 dequantize → active state
继续 decode:
active state 保持原始精度更新
不是每个 token 都在 INT8 state 上循环量化。
6.6.3 MambaCheckpointPool
文件:sglang-main/python/sglang/srt/mem_cache/mamba_checkpoint_pool.py:186-280
关键函数:
__init__:第 187‑225 行,创建 temporal int8 store、conv buffers、slot allocator;store_from_active:第 245‑251 行;load_to_active:第 253‑259 行;estimate_mem_usage_bytes:第 262 行起。
store_from_active 的语义:
active_mamba_pool.temporal → checkpoint temporal
active_mamba_pool.conv → checkpoint conv
load_to_active 的语义:
checkpoint temporal → active temporal
checkpoint conv → active conv
SGLang 确实有 convolution state 的对应存储,不是只缓存 recurrent state。
6.6.4 可选初始化
文件:sglang-main/python/sglang/srt/mem_cache/mamba_checkpoint_pool.py:300-369
关键逻辑:
如果没有 Mamba 配置:不创建
如果没有开启 enable_int8_mamba_checkpoint:不创建
否则:估算空间、检查可用显存、创建独立 checkpoint pool
这说明 INT8 checkpoint 是可选优化,不应写成所有 SGLang 部署都默认启用。
6.7 活动状态、Donate 与边界
6.7.1 HybridReqToTokenPool.__init__
文件:sglang-main/python/sglang/srt/mem_cache/memory_pool.py:1271-1289
关键逻辑:
self.mamba_allocator = MambaSlotAllocator(...)
self.mamba_map = {...}
self.mamba_ckpt_pool = maybe_init_int8_mamba_checkpoint_pool(...)
活动 state 和 checkpoint state 使用不同池:活动请求走 mamba_allocator / active pool,Radix cached checkpoint 走 mamba_ckpt_pool。
6.7.2 donate_mamba_ping_pong_slot
文件:sglang-main/python/sglang/srt/mem_cache/memory_pool.py:1488-1509
函数:donate_mamba_ping_pong_slot(req, new_slot)
简化逻辑:
找到要保留的 ping-pong slot
↓
把旧 slot 作为可缓存 state 返回
↓
把请求切换到 new_slot 继续执行
第 1491‑1495 行的 docstring 已明确写出 "donate tracked-state slot to radix cache"。这不是复制数据,而是把 ownership/slot 交给 radix cache 使用。
6.7.3 clear
文件:sglang-main/python/sglang/srt/mem_cache/memory_pool.py:1565-1577
第 1569‑1574 行特别处理 mamba_ckpt_pool.clear()。原因是清空 Radix Tree 后,checkpoint slot 也必须释放,否则会出现 "树没了、slot 还被占用" 的泄漏。
6.8 checkpoint 何时产生
6.8.1 _mamba_radix_cache_v2_req_prepare_for_extend
文件:sglang-main/python/sglang/srt/managers/schedule_batch.py:2672-2735
函数:_mamba_radix_cache_v2_req_prepare_for_extend(req)
关键逻辑:
- 读取
mamba_cache_chunk_size(); - 根据
mamba_checkpoint_grid(self.tree_cache.page_size)得到 checkpoint 网格; - 判断
extend_range.length >= checkpoint_grid; - 计算要追踪的 seqlen;
- 把 state 放到 ping‑pong track buffer。
第 2677‑2680 行说明 checkpoint 网格来自 tree page;第 2699‑2730 行计算对齐边界。它解释了为什么 KDA checkpoint 不是任意 token 都自然存在:调度器会主动选择合法追踪边界。
6.8.2 mamba_lazy_prealloc_at_boundary
文件:sglang-main/python/sglang/srt/managers/schedule_batch.py:3002-3031
函数:mamba_lazy_prealloc_at_boundary(mamba_track_interval)
第 3015‑3017 行检查请求是否落在 tracking interval;第 3026‑3031 行按需申请第二个 ping‑pong slot。
这就是 lazy ping‑pong:
平时:一个活动 slot
到 checkpoint 边界:临时申请第二个 slot,新状态写入另一侧,旧状态可安全 donate
6.9 Unified Memory 的源码证据
文件:sglang-main/python/sglang/srt/mem_cache/unified_memory_pool.py
关键位置:
MambaSubPoolSpec:第 181‑210 行;UnifiedKVPool:第 217 行起;- 构造函数:第 222‑280 行。
MambaSubPoolSpec 第 185‑205 行明确描述:conv_state_shapes、temporal_state_shape、conv_dtype、temporal_dtype。
UnifiedKVPool 第 217‑220 行说明两个 sub‑pool 共享一个 uint8 byte buffer;第 232‑243 行限制当前实现为一个向上增长、一个向下增长的两个池。
简化布局:
同一 byte buffer
MHA/MLA pool ---> 空闲区 <--- Mamba pool
注意:这是物理容量统一,不是逻辑对象统一。
6.10 SGLang 恢复流程总览
请求 prefix 命中
↓
Radix 选择/准备 KDA tracking boundary
↓
MambaCheckpointPool.load_to_active(...)
├─ temporal int8 dequantize
└─ conv native-dtype copy
↓
请求使用私有 active slot
↓
到边界时 donate/snapshot
↓
checkpoint pool + radix cache
关键证据文件:mamba_checkpoint_pool.py:245-259、memory_pool.py:1488-1509、schedule_batch.py:2672-2735。
0x07. vLLM 的应对思路
vLLM 将不同状态纳入同一调度框架,但不强行用同一种存储方式:
同一 Scheduler
├─ MLA/full-attention: Paged KV blocks
└─ KDA:
├─ recurrent-state blocks/checkpoints
└─ convolution-state blocks/checkpoints
因此 "统一" 主要指请求调度和生命周期协调;底层仍承认 KV 与 KDA state 的语义差异。
7.1 State block 是什么
State block 是物理内存管理单位,不等于一个 token,也不等于一个完整请求:
State block B
┌─────────────────────────────┐
│ 一段连续执行产生的 KDA state │
│ recurrent state │
│ convolution state │
│ 边界/版本/布局等元数据 │
└─────────────────────────────┘
它服务于分配、回收、引用计数、换入换出和远端传输。一个大 block 中可以登记多个逻辑 Prefix Entry。
在 vLLM 语境中:
- recurrent state:KDA 扫描到某个位置后得到的长期状态矩阵,携带长上下文摘要;
- convolution state:短卷积所需的最近若干 token 的局部缓存,携带局部连续性。
前者决定长期记忆,后者决定下一步局部卷积输出。两者都影响后续 token,因此不能只恢复 MLA KV。
常见疑问:vLLM 的 state block 是什么?
它是 state 的物理容器,类似 KV block 的 "内存页" 角色,但里面装的是 KDA 状态快照,不是逐 token K/V。它通常附带 state tensor、覆盖/关联的前缀范围、快照边界、dtype/layout、引用计数、是否可写。一个 state block 可以有多个逻辑恢复点,因此物理块大小不必等于 Prefix Matching 粒度。
7.2 逻辑前缀与物理 state 解耦
如果 Prefix Matching 的边界必须等于 State block 的边界,匹配粒度会被物理分块限制。vLLM 因此把两件事分开:
- 逻辑层:记录任意已完成的 token 前缀;
- 物理层:用较大的 state block 存储状态。
二者通过偏移、边界元数据和 state 版本关联。
假设物理 State block 按 4096 token 管理,但实际请求共享到 4480 token:
物理存储:
State block B: [0 ————————————————— 8192]
逻辑前缀:
Prefix Entry at 4096
Prefix Entry at 4480
如果强制 block‑aligned,只能命中 4096;如果逻辑匹配可以落在 block 内部,就能把 4480 作为更长的合法前缀。这里没有把 state 截断成 "半块",而是在大容器中保存或索引更细的有效边界。
常见疑问:逻辑前缀匹配与物理状态位置解耦是什么意思?
把 "地图上的地址" 和 "仓库里的箱子" 分开。逻辑层可以说 "命中 token 4480",而物理层可能把 0‑8192 token 放在同一个 state block B。只要 B 内登记了 state after 4480,逻辑层就能把 4480 当恢复点。否则,物理 block 的存在不能自动证明 4480 有可恢复 state。
7.3 Fine‑grained Partial Hit
假设一个 State block 能容纳 128 token 的状态,但请求前缀只命中到 96 token。朴素做法是:因为 128 token 的 block 尚未完整,96 token 不能恢复,只能从更早位置重算。
Fine‑grained Partial Hit 的做法是:仍然使用这个 128‑token 物理 block,但在其中记录多个更细的 Prefix Entry:
State block B:
entry at 32 -> state after token 32
entry at 64 -> state after token 64
entry at 96 -> state after token 96
entry at 128 -> state after token 128
于是,96 token 可以成为合法恢复点,即使它位于一个尚未填满的物理 State block 中。这里 "partial hit" 不是只恢复半个状态矩阵,而是恢复该 block 内预先记录的某个有效状态快照。
完整过程:
请求 P:
已执行 0 … 4480
产生 state S_4480
物理上:
S_4480 位于大 State block B 内
新请求:
命中 token 0 … 4480
↓
找到 Prefix Entry(4480)
↓
取出/恢复 S_4480
↓
复制到新请求私有活动槽
↓
继续生成
"Partial" 指逻辑命中位置可能只占物理 block 的一部分,不是允许系统从一个没有对应快照的中间位置硬推断 state。
它允许在一个大 State block 内登记多个细粒度 Prefix Entry。新请求命中 4480 时,不必退回到 block 起点 4096,也不必复制另一份完整物理 block。它直接定位对应 state,再复制成私有活动 state。
关键边界:partial hit 允许 "逻辑边界细于物理容器",但不允许 "从不存在的快照推断 state"。
常见疑问:Fine‑grained Partial Hit 如何工作?
大 state block 内额外记录更细粒度、链式的 Prefix Entry。比如一个 block 覆盖 4096 token,而实际共享前缀到 4480 token;逻辑匹配可以落在 block 内部的 4480 边界,并关联该位置已经生成的 KDA state,而不必把 Prefix Matching 粗化为 4096 的物理边界。它不是把一个未完成的 state "截半",而是把多个逻辑恢复点放进较大的物理容器:物理上合并存储,逻辑上细粒度命中。恢复时仍需把对应 state 复制到请求私有空间,再继续递推。
7.4 Interval‑based 与 Marconi‑style Retention
有限缓存预算下,不能保存所有位置的 checkpoint。
- Interval‑based Retention:按固定或结构化间隔保留,例如每隔若干 token 保存;稳定、可预测、实现简单。
- Marconi‑style Selective Retention:根据运行时访问频率、前缀热度和复用价值选择保留位置;更灵活,但需要在线统计和淘汰策略。
前者像规划化修路指标,后者像根据交通流量设置热门出口。
Interval-based:
0 ———— K ———— 2K ———— 3K ————
固定、可预测、适合结构化前缀
Marconi-style:
第一次看到前缀:只记访问
第二次命中:才保存 state
热点继续保留,冷点淘汰
Interval‑based 还可以额外保留 Prompt 末尾状态,因为多轮对话常会完整复用上一轮 prompt。Marconi‑style 则避免一次性长 prompt 立刻占据大量 state 缓存。
常见疑问:Interval‑based Retention 与 Marconi‑style Retention 有何区别?
Interval‑based 是预先规定路标:每 K token 一个 checkpoint,Prompt 末尾额外保留。它稳定、容易预测,适合多轮对话等边界清晰的场景。Marconi‑style 是先观察再投资:第一次出现只记录访问、不保存大 state;再次命中证明它是热点,才创建 checkpoint。它能避免一次性 prompt 浪费缓存,但需要在线统计和更复杂的淘汰逻辑。两者可以并用:结构化边界用 interval,动态热点用 selective retention。
0x08. vLLM 源码导航
8.1 源码地图
| 文件 | 关键函数 / 类 | 作用 |
|---|---|---|
vllm/models/kimi_k3/nvidia/kda.py |
KimiK3DeltaAttention.forward、_forward、_store_cache_checkpoints_kernel |
KDA conv/recurrent 执行、checkpoint 写入 |
vllm/models/kimi_k3/nvidia/kda_metadata.py |
_get_aligned_state_indices_kernel、KimiK3KDAMetadataBuilder.build |
把 block table 转成 state index,构造 checkpoint metadata |
vllm/v1/core/single_type_kv_cache_manager.py |
MambaManager.find_longest_cache_hit |
Mamba/KDA 的 prefix lookup 与细粒度命中 |
vllm/v1/core/kv_cache_coordinator.py |
KVCacheCoordinator.__init__、_cache_hit_alignment_tokens |
多 attention group 对齐、启用 partial hit |
vllm/v1/core/kv_cache_utils.py |
resolve_block_hashes 调用点 |
保留细粒度 hash 以支持 partial hit |
8.2 KDA 执行和 state cache
8.2.1 KimiK3DeltaAttention.get_kv_cache_spec
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda.py:593-599
函数:get_kv_cache_spec(vllm_config)
关键逻辑:
spec = super().get_kv_cache_spec(...)
return replace(
spec,
num_prefill_checkpoint_blocks=
int(self.kda_prefill_backend == "flashkda"),
)
KDA 的 cache spec 不只是普通 attention KV spec,还包含 prefill checkpoint block 数量。
8.2.2 forward 与 _forward
文件:
vllm-main/vllm/models/kimi_k3/nvidia/kda.py:601-638vllm-main/vllm/models/kimi_k3/nvidia/kda.py:640-710
KimiK3DeltaAttention.forward 第 601‑638 行:
投影 hidden_states
拆出 mixed_qkv、gates、beta
调用 _forward
o_proj
KimiK3DeltaAttention._forward 第 640‑710 行:
- 从 forward context 取得 KDA metadata;
- 读取
state_indices、checkpoint; - 从
self.kv_cache取得conv_state和recurrent_state; - 纯 decode 条件满足时调用 fused KDA decode。
第 681‑685 行直接解包:
conv_state, recurrent_state, *recoverssm_records = self.kv_cache
第 694‑710 行把 conv_state、recurrent_state、state_indices 传入 fused kernel。
vLLM 源码直接证明:KDA cache 至少有 convolution state 和 recurrent state 两个独立 tensor 语义。
8.2.3 prefill 中的 convolution 与 recurrent state
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda.py:820-860
关键逻辑:
- 第 831‑846 行定义
_prefill_conv; - 第 848‑850 行分别对 q/k/v 做 causal convolution;
- 第 858‑860 行用
gather_initial_states取得初始 recurrent state。
简化:
prefill input
├─ q -> causal conv -> q
├─ k -> causal conv -> k
├─ v -> causal conv -> v
└─ gather initial recurrent state
8.2.4 _store_cache_checkpoints_kernel
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda.py:236-297
函数:_store_cache_checkpoints_kernel(...)
关键逻辑:
- 第 262‑266 行读取 state index 和 checkpoint offset;
- 第 267‑285 行把对应卷积历史写入
conv_state; - 第 287‑295 行把 recurrent checkpoint 写入
recurrent_state。
vLLM 的 checkpoint 写入不是抽象描述,而是 kernel 级别同时处理:
conv checkpoint -> convolution state
recurrent checkpoint -> recurrent state
8.3 metadata 如何找到 state
8.3.1 _get_aligned_state_indices_kernel
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda_metadata.py:55-101
函数:_get_aligned_state_indices_kernel(...)
第 85 行根据 seq_lens 和 CACHE_BLOCK_SIZE 计算第一个 state slot;第 89‑100 行从 block table 读取 state indices。
它做的是:
请求长度
↓
对应 KDA state block 的起始位置
↓
state index tensor
8.3.2 _mamba_get_block_table_tensor
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda_metadata.py:104-140
函数:_mamba_get_block_table_tensor(...)
若 mamba_cache_mode 是 "all" 或 "none",第 110 行直接返回 block table;否则第 114‑140 行创建 state indices,并通过 Triton kernel 对齐到 KDA state slot。
这就是 "逻辑前缀" 和 "物理 state block" 之间的适配层。
8.3.3 KimiK3DAMetadataBuilder.build
文件:vllm-main/vllm/models/kimi_k3/nvidia/kda_metadata.py:560-717
关键逻辑:
- 第 570‑575 行:只有 prefill、启用 prefill checkpoint block、并且 cache mode 为
align时,才准备 checkpoint metadata; - 第 588‑600 行:按 sequence length、block size 计算 checkpoint split 和 checkpoint column;
- 第 602‑624 行:把 offsets 和 state indices 组织成
KDACheckpointMetadata; - 第 716 行:把 checkpoint metadata 放进最终
KimiK3DAMetadata。
这段源码说明一个重要边界:checkpoint 不是所有运行模式都自动生成;它受 prefill backend、cache mode 和对齐条件共同控制。
8.4 Fine‑grained Partial Hit 的源码证据
8.4.1 MambaManager
文件:vllm-main/vllm/v1/core/single_type_kv_cache_manager.py:1268-1345
类:MambaManager
第 1268‑1295 行:
supports_fine_grained_hash_lookup = True- 保存
mamba_cache_mode - 在
align模式下维护 checkpoint block 和 partial‑tail 相关状态。
8.4.2 find_longest_cache_hit
同文件第 1297 行起:MambaManager.find_longest_cache_hit(...)
第 1315‑1321 行调用 resolve_block_hashes,并传入:
supports_fine_grained_hash_lookup=True
alignment_tokens=alignment_tokens
第 1327‑1345 行在 alignment_tokens < block_size 时,把一个大 block 拆成多个更小 hash unit 来寻找命中。
这就是 "物理 block 大、逻辑 prefix 小" 的源码基础。
8.4.3 Coordinator 如何启用 partial hit
文件:vllm-main/vllm/v1/core/kv_cache_coordinator.py:622-662
关键逻辑:
- 第 622‑637 行检查是否存在
MambaSpec且mamba_cache_mode == "align"; - 第 638‑651 行检查其他 cache manager 是否兼容;
- 第 655‑662 行让
_cache_hit_alignment_tokens在 partial hit 时使用hash_block_size,否则使用scheduler_block_size。
换句话说:
普通命中中:只能落在 scheduler block
KDA partial hit:可以落在更细的 hash block
8.5 Retention 的源码证据
文件:
vllm-main/vllm/v1/core/kv_cache_coordinator.py:32-62vllm-main/vllm/v1/core/kv_cache_coordinator.py:152-158
函数:_validate_prefix_cache_retention_interval(...)
第 40‑45 行说明 retention 只对 SlidingWindow 和 Mamba/linear‑attention group 生效;第 56‑60 行要求 interval 非负且是 scheduler block 的倍数。
Coordinator 第 152‑158 行保存 retention_interval。
由此可知:
retention_interval = None:
不做稀疏化/保持默认密度
retention_interval = 0:
对适用 group 采用特殊的最小保留策略
正数:
按 scheduler block 对齐的周期保留
源码能够确认 interval‑based retention;Marconi‑style selective retention 若要继续考据,应在具体分支中定位其独立策略实现,不能仅凭 coordinator 的 interval 参数称二者是同一个函数。
8.6 vLLM 恢复流程总览
prefix hash lookup
↓
MambaManager.find_longest_cache_hit(...)
↓
fine-grained alignment / state block lookup
↓
KimiK3DAMetadataBuilder.build(...)
↓
state_indices + checkpoint offsets
↓
KimiK3DeltaAttention._forward(...)
├─ conv_state
└─ recurrent_state
↓
kernel decode/prefill/update
关键证据文件:single_type_kv_cache_manager.py:1297-1345、kv_cache_coordinator.py:622-662、kda_metadata.py:570-624、kda.py:640-710。
0x09. 深入讨论
9.1 为什么 SWA 的 checkpoint 方法不能直接用于 KDA
SWA (Sliding Window Attention) 只需要最近窗口内的 token。窗口向前移动时,旧 token 自然离开;如果保存每隔 128 token 的窗口边界,恢复时最多重算一个窗口之间的短区间。
窗口 [t-W, …, t]
窗口向前移动后,旧 token 被明确丢弃。保存一个窗口边界就足以说明附近的局部状态。
KDA 的 recurrent state 则是全历史压缩后的结果。位置 n 的 state 不是只由最近窗口决定,而是由从开头到 n 的所有转移和写入共同决定:
S_n = M_n … M_1 S_0 + Σ_{i=1}^{n} M_n … M_{i+1} b_i
少一个 checkpoint,就不能从任意附近的 token 直接推导出完整 state;必须从更早 checkpoint 重新顺序执行所有中间更新。
而且 KDA state 往往比 SWA 的窗口边界大得多。固定间隔越密,显存成本越高;间隔越稀,命中后重算越长。这就是 KDA 比 SWA 更难做 Prefix Cache 的根本原因。
常见疑问:为什么 KDA 不能用 SWA 的固定间隔 checkpoint?
SWA 的状态只代表一个有限窗口;KDA state 代表整个前缀的递推摘要。SWA 从最近窗口边界继续,最多补一小段窗口计算;KDA 从 checkpoint 到命中点之间的每个 token 都可能改变 state,必须按序 replay。再者,KDA state 本身体积大,固定高频保存的成本远超 SWA 的轻量边界信息。
9.2 Checkpoint 密度的两难
设 checkpoint 间隔为 K:
- 存储成本大致随
1/K增加; - 命中后的平均重算长度大致随 K 增加。
这不是精确性能公式,但能说明趋势:
checkpoint 密集 -> state 占用高、恢复精细、重算少
checkpoint 稀疏 -> state 占用低、恢复粗糙、重算多
假设只每 10k token 保存一次:
checkpoint: 0 ——————— 10k ——————— 20k
共享前缀: 0 ——————————————————— 15k
两个请求实际共享到 15k,但最近合法恢复点只有 10k。系统不能从 "命中的 15k token 文本" 直接恢复,只能:
恢复 S_10k
replay 10k … 15k
再继续新请求
本来只需计算 5k 尾部,现在要重算 5k;若命中边界更不巧,浪费会更大。
因此真正目标不是最大化 "token prefix hit rate",而是最大化:
缓存收益 − state 驻留成本 − 恢复重算成本 − 管理复杂度
这不是单纯的命中率问题,而是 state 驻留成本 + replay 成本 + 请求等待时间 的联合优化。
常见疑问:如何理解 checkpoint 密度的两难?
密集 checkpoint 用显存换恢复速度,稀疏 checkpoint 用重算时间换显存。最佳策略取决于前缀复用概率、state 大小、请求长度和显存预算,不存在脱离负载的固定最优间隔。系统 prompt、多轮对话和 Agent 工具定义通常值得高优先级保存;一次性的随机长 prompt 则不一定值得保存。
常见疑问:将 checkpoint 间隔拉大有什么问题?
间隔变大后,前缀可能在 token 层面命中很多,但在 state 层面只能恢复到更早位置。于是命中收益被 replay 吞掉。例如共享 905k token,但最近 checkpoint 在 900k,仍需重算 5k;若只在 800k 保存,就需重算 105k。token 命中率看起来很高,实际 TTFT 未必改善。
9.3 跨实例复用与 PD/EPD 分离
跨实例复用
传统系统只需寻找最长匹配 token prefix,再迁移 KV blocks。KDA 要求同时满足:
- MLA KV 在该边界可用;
- KDA recurrent state 在该边界可用;
- convolution state 在需要时可用或能被安全重建;
- 所有状态来自同一模型版本、同一 tokenization 和同一前缀。
最长 token prefix
↓
找到共同合法边界
↓
联合迁移:
MLA KV
KDA recurrent state
KDA convolution state
↓
恢复到目标实例
不同来源的缓存不能简单拼接。一个来源的 MLA KV 加另一个来源的 KDA state,即使长度相同,也不保证计算一致。不能把实例 A 的 MLA KV 与实例 B 的 KDA state 按长度拼在一起,因为它们可能来自不同 tokenization、模型版本、并行布局或前缀边界。
所以跨实例缓存查找从最长前缀搜索变成了 "最长可联合恢复前缀" 搜索。
常见疑问:跨实例复用为何不能只寻找最长 token prefix?
因为还要找到该前缀对应的 KDA state checkpoint,并确认它和 MLA KV 在同一位置、同一版本、同一布局下有效。不同实例的缓存不能按长度简单拼接。跨实例不仅要问 "文本相同到哪里",还要问:哪个边界有 KDA checkpoint?MLA KV 和 KDA state 是否同边界?远端对象是否来自同一模型 / 并行配置?恢复后的 state 能否安全转为本地活动副本?
PD 分离
传统 PD 分离传输:
Prefill → KV blocks → Decode
Prefill 端产生的不是一组 KV block,而是一组带边界和版本信息的模型状态。Decode 端需要知道:
- 状态类型;
- 对应 token 范围;
- tensor layout 和 dtype;
- checkpoint 是否可写;
- 后续是否需要 COW;
- 状态是否适用于当前并行切分。
Prefill
├─ MLA latent history
├─ KDA recurrent state
├─ KDA convolution state
└─ boundary/version/layout metadata
↓
Decode
这要求通信协议从 "搬运缓存页" 升级为 "搬运可恢复模型状态"。若为每种模型单独写通信协议,基础设施会被模型结构大量污染;统一的模型状态协议能把 "对象类型" 和 "恢复元数据" 作为一等公民。
常见疑问:PD 分离为何要从 KV Cache 传输演进为模型状态传输?
因为 Decode 需要的输入不再只有 KV,而是 MLA KV + recurrent state + convolution state。每类状态可能有不同布局、精度、所有权和恢复规则。Prefill 到 Decode 的输入现在包含三者及元数据。把它们分别塞进 KV 协议会让通信层被模型细节绑死,因此更自然的抽象是统一传输 "可恢复模型状态"。
多模态 EPD
多模态推理还要把 Encoder 输出的视觉 embedding 纳入数据平面:
Encoder embeddings
↓
Prefill 生成 MLA/KDA states
↓
Decode 使用活动副本
视觉 embedding 可能在多个请求间复用,也可能只服务当前请求;它不是 KDA state,也不能简单当成 KV block。
数据平面要支持:视觉 embedding、MLA token history、KDA recurrent snapshot、KDA convolution snapshot,并分别维护生命周期和所有权。
统一数据平面需要同时处理不同生命周期、不同大小、不同可共享性的对象,而不是把所有内容都伪装成 KV。
常见疑问:多模态 EPD 的额外挑战是什么?
除了 Prefill 生成的注意力状态,还要流动 Encoder 的视觉 embedding。它们大小、生命周期和复用方式不同,系统需要统一数据平面,同时保留类型和所有权信息。EPD 比 PD 多了 Encoder 输出,统一数据平面必须保留类型、所有权、版本和恢复边界,不能只依赖一个 token prefix hash。
9.4 概念与源码对应表
| 概念 | SGLang 源码 | vLLM 源码 |
|---|---|---|
| recurrent state | MambaCheckpointPool.temporal |
self.kv_cache 中的 recurrent_state |
| convolution state | MambaCheckpointPool.conv |
self.kv_cache 中的 conv_state |
| 活动副本 | MambaPool/allocator slot |
request 对应 state slot |
| checkpoint | MambaCheckpointPool slot |
state block + metadata/checkpoint slot |
| COW | load_to_active |
恢复到请求自己的 state slot |
| Donate | donate_mamba_ping_pong_slot |
state block/slot 生命周期管理 |
| 细粒度命中 | Radix/KDA tracking boundary | supports_fine_grained_hash_lookup |
| 稀疏 checkpoint | schedule boundary + tracking interval | retention interval / state retention |
| 物理统一内存 | UnifiedMemoryPool |
Hybrid cache manager / common block pool |
| kernel 状态索引 | backend cache indices | state_indices metadata |
9.5 复现源码时的最小阅读顺序
如果要自己实现一个教育版 KDA Infra,建议按这个顺序阅读:
- SGLang
mamba_checkpoint_pool.py:117-174:先理解量化、加载和写回; - SGLang
memory_pool.py:1271-1289,1488-1509:理解活动 slot 与 Donate; - vLLM
kda.py:640-710:理解 conv/recurrent state 如何进入 kernel; - vLLM
kda_metadata.py:570-624:理解 checkpoint offsets/state indices; - vLLM
single_type_kv_cache_manager.py:1297-1345:理解细粒度命中; - SGLang
unified_memory_pool.py:181-280:理解物理容量共享。
0x10. 最终结论
从源码看,KDA‑aware Infra 的最小正确抽象不是 prefix → KV blocks,而是:
prefix boundary
→ token/hash identity
→ MLA KV blocks
→ recurrent state slot
→ convolution state slot
→ writable/readonly ownership
→ dtype/layout/version metadata
KDA 对 AI Infra 的核心影响可以概括为:
Prefix Cache 不再只是 "找到相同 token,然后搬 KV";它变成 "找到共同前缀边界,并恢复一组语义一致、生命周期不同的模型状态"。
SGLang 更强调在 Radix Tree 上增加 KDA‑aware checkpoint、稀疏 / 压缩保存和共享状态隔离,实现重点是 "Radix + Mamba checkpoint pool + ping‑pong/COW + UnifiedKVPool";
vLLM 更强调 state block、逻辑边界与物理存储解耦,以及细粒度 partial hit 和状态保留策略,实现重点是 "Hybrid cache manager + MambaManager fine‑grained hash alignment + KDA metadata/kernel pipeline"。
两者方向不同,但共同结论一致:面向 KDA 的推理系统必须从 KV‑centric cache 演变为 state‑centric cache。只有在同一个前缀边界上,完整的模型状态都可定位、可恢复、可安全写入,Prefix Cache 才真正成立。

浙公网安备 33010602011771号