UCM 缓存系统:面向混合模型的分层设计报告
UCM 缓存系统
版本 V6(混合模型主线版,含设计哲学×现实矛盾清单 6.6)| 2026-08-28 | 代码基线:
ModelEngine-Group/unified-cache-management@5cc8fdf本报告的主线只有一个。 一切内容围绕“模型结构混搭之后,缓存系统该怎么设计”展开——模型结构 → 缓存语义分类 → 协调器 → 双原语 → 适配路径。 不在主线内的内容 的内部机制优化(内存淘汰、多 rank 调度、磁盘 GC、I/O 布局、等待语义等)与混合模型无直接因果,统一移入附录 E(Store 内核优化备忘),主线正文不展开。 写给谁、对代码不熟、想理解“混合模型时代 UCM 怎么演进”的读者。 阅读约定;图片编号 figNN 与正文同级目录;每个问题按“现有设计 → 遇到问题 → 优化思路 → Gap 与限制”展开。
第 0 章 三分钟看懂
0.1 背景 Cache 与“为什么必须搬出 GPU”
大模型推理分预填充(prefill,整段提示词并行算一次 K/V)与解码(decode,逐 token 生成、读缓存),缓存的历史 K/V 叫 KV Cache。它的量级——两种口径:①按本报告的规格表自算(4.1),X-Hybrid-96 的 16 层 MLA 每层每 token 约 512 B,1M token ≈ 8 GB;②按 DeepSeek 论文口径,MLA 相对等效 MHA 的 KV 缩减 93%–96%(见 6.1,论文 URL 见附录 C)。无论哪种口径,GPU 都放不下,必须“搬出去存、要用时取回”。
命中(hit) → 这段 KV 已在缓存 → 跳过这段 prefill 计算。收益锚点 个用户共享 2000 token 系统提示词时,有缓存 = 999 次 prefill 直接免掉。

0.2 混合模型(本报告的问题起点)
2026 年的新模型(DeepSeek-V4-Flash、Kimi K3、GLM-5.3-Flash、Qwen3.8-Flash-Next、MiniMax M3)不再是“每层同一种注意力”,而是多层混搭(MLA 全注意力层 + 滑动窗口层 + Mamba 状态层交替出现)。对缓存系统,这等于数据的种类变了:
| 数据类型 | 代表 | 复用规则 | 比喻 |
|---|---|---|---|
| 链式数据 | 普通 KV、MLA 潜向量、压缩 KV、滑动窗口 KV | 前缀相同即可复用,可拼接 | 积木 |
| 快照数据 | Mamba / KDA(记忆编辑状态)/ 循环状态 | 只在精确位置有效 | 罐头 |
| 不缓存的数据 | 跨注意力(cross-attention) | 不缓存 | 不缓存 |
为什么快照数据“位置不对就废”“前面全部历史”递推出来的隐函数——前缀换了,值就不同;“位置 P 的状态”只能服务“恰好从 P 续上”的请求。这就是位置键(position-keyed)的由来。
侧车(sidecar)(稀疏/压缩注意力按块产出的“该关注哪些 key”的 top-k 元数据,见 6.1)、量化缩放因子等附属数据——没有自己的复用边界,跟着主组走、不参与命中投票;“源组”指块 id 归属的组(身份与投票语义随它),物理存放实例可另指(容量决策,示例见 7.3 澄清 3),规格表上与源组共用同一行。
0.3 关键正确性直觉 vs 漏命
- 漏命——安全,只多花计算;
- 错命、按它跳过计算——可能读到不存在的块/错位数据,模型静默输出错误结果。
所以所有命中判定取最保守的公共交集(AND),绝不错误复用。这是第 4 章“组件投票取最小”的推导根基。
0.4 本报告的结构
第 1 章看全局地图;第 2–5 章按层深入(引擎 → 接口层 → 协调器 → 存储双原语);第 6 章看模型世界全景、业界对照与设计哲学 × 现实的矛盾清单(6.6);第 7 章讲逐层流水(Layerwise)适配——混合模型绕不开的时序课题;第 8 章用一个虚构复杂模型把全部串起来;第 9 章路线图。store 内部机制见附录 E。
0.5 一个请求的完整旅程(先读这篇散文,再读技术细节)
时间基准声明目标架构(协调器 + 存取单位(7.3,一次取数的对象)+ 双原语)为时间基准叙述;其中“引擎打电话”的调用顺序与现状一致(7.2 的契约不变),现状与目标的差异在各章相应位置标注——读时若见到“适配器/单位映射表”等词,它们属于目标形态,不是当前代码。
假设一个多轮对话的续写请求到来,它的前缀和之前某个请求完全一样。vLLM 的调度器先在内存里算出“本地最多能复用多少”(这是本地的,不是最终答案);然后通过接缝问 UCM 的协调器,持久层里到底有没有?协调器翻开规格表——这个模型有四个组,链式组三个、快照组一个(为讲清楚,此处用简化模型;真实的 X-Hybrid-96 有两个快照组,裁决规则见 4.2)——分别问每个链式存储“前缀最长存在到哪”,取最小、对齐到公共刻度,再查检查点目录找到最深的可用检查点,最后回答 2304 个 token 的计算,从 2304 续上,状态也直接接着用。
引擎收到这个答案,开始逐层推进。第一层的 KV 数据必须立刻拿到——适配器在引擎开算之前就把第一个存取单位(一次取数的对象、几层共享的一行、或一个窗口,7.3 细讲;这里先记个印象)的取数任务交了出去;每算完一层,引擎打电话说“这层算完了,拿去存”,适配器顺手把下一层的取数任务交出去;仓库在后台同时做三件事——把刚算完的层存进磁盘、把下一层的 KV 搬回 GPU、为再下一层腾出暂存空间……直到全部算完,引擎最后确认一次“该存的都存好了”,请求结束。
如果这一层的“货”是 mamba 状态(第 0 章说的“罐头”),仓库走的是快照通道,而不是按前缀存块——位置不对,罐头就不能开。如果它是共享张量(几层的 KV 合在一个大张量里、按“行”划分,7.3 的 row)的一行,那么这一行要等行内所有层都算完,才由行尾那层统一触发存取。
整条链路里,引擎只认识“层”,协调器只认识“组”,存储只认识”(块号, 分片)“;把它们串起来的,是适配器手里的那张”单位映射表”(第 7 章)。每一层都各司其职,谁也不用懂别人的世界——这就是本报告的哲学,也是它想让你带走的那句话。
第 1 章 分层地图

1.1 现状(七层,L0–L6)与目标(五层,L0–L4)
| 现状层 | 职责 | 目标层 | 职责 |
|---|---|---|---|
| L0 引擎 | 模型本体与调度 | L0 引擎 | 不变 |
| L1 Connector(12 个类) | 翻译 + 判断,混杂 | L1 适配器(薄) | 只翻译 |
| L2 工厂 + pybind | 组装 | └→ L2 协调器(新增) | 懂模型、不碰字节 |
| L3 Pipeline / L4 实现群 | 流水线 + 后端 | L3 存储(双原语) | 哑字节容器 |
| L5 传输 / L6 磁盘 | 搬数据 / 存放 | L4 传输 | 搬数据 |
Connector 是什么(3.1/3.3 细讲) 挂在 vLLM 接缝(KVConnectorBase_V1 钩子)上的桥接组件——把引擎生命周期事件翻译成存储操作,并计算 KV 块的 16 字节编号。现在有 12 个类,构成见 3.3。
为什么要新增“协调器”“懂模型”的代码散落在 Connector 的 12 个类中(每个新模型结构 = 一个新类,组合爆炸,FAWA 就是例证)。把“懂模型但不碰数据”抽成独立一层后,模型的特殊性只以**数据(规格表行)存在,不再以代码(新类)**存在。
什么是 FAWA + Window 双存储连接器——为 DeepSeek-V4 式混合注意力专门手写的一个类(组判断/哈希域/容量拆分全在该类内)。它是“手搓特判”的反面样板“每模型一个类”会组合爆炸。
每层的红线(边界),全篇最重要的一张表:
| 层 | 必须管 | 绝不管 | 为什么 |
|---|---|---|---|
| 引擎 | 语义、调度、计算 | 存储细节 | 太忙,每版本在变 |
| 适配器 | 生命周期翻译、完成粒度 | 命中判断、保留决策 | 判断留在懂模型的那层 |
| 协调器 | 规格表、命中裁决、检查点保留、秩规则 | 字节搬运、槽位分配 | 一碰字节就退化回“手搓 FAWA” |
| 存储 | 字节存在性、搬运、本地淘汰 | 模型语义、会话 pin | 一懂模型就会被每模型改造 |
| 传输 | 字节搬运 | 内容 | 无状态 |
部署形态(协调器目标为独立组件仍同机);存储在本机(跨 rank 共享内存池 + 本地 NVMe);磁盘/NFS 可多机共享;DramPool(远端 DRAM 内存池服务,#1284)为可选服务;跨机走 P2P/RDMA(图 17)。

名词(张量并行,一层切多卡)/ PP(流水并行,层分段)/ rank(参与进程,一张卡)。MLA 的 KV 内容跨 rank 相同(共享池去重成立);非 MLA 各 rank 哈希盐含 rank 编号,各存各的。
第 2 章 引擎层
2.1 引擎侧的两个关键抽象(不熟代码也能懂)
# vLLM 侧(vllm/v1/kv_cache_interface.py,简化):class KVCacheGroupSpec: layer_names: list[str] # 哪些层属于这一组 kv_cache_spec: KVCacheSpec # 组的形状class KVCacheSpec: block_size: int # 一个缓存块装多少个 token # MambaSpec 还有 tokens_per_state:几个 token 压缩成一个状态(快照数据的粒度)关键点“组”把混合模型的层分好、每组带形状信息——协调器规格表的原材料是现成的(哪些可信、哪些要自己推导,见图 14)。

2.2 引擎侧的混合模型机制(2026 现状,一手源核实)
- 匹配 token 数 = 各组取交集(AND)“新请求能复用多少前缀”,混合模型下各组命中取公共交集,按各组块大小的**最小公倍数(LCM)**对齐(块边界不同,必须对齐到公共刻度才能逐块比对);
- 状态对齐 状态按“块对齐快照”保存——状态本身是固定大小的张量,但按 token 块分页存放(一个块容纳固定的 token 数;状态存在最后 1+数个投机块——投机解码预取的额外 token 块;约定“运行中的状态总记在最后一个块上”——状态随序列末尾 token 走,续算时从最后一块接着写,不必回填中间块);
- 接缝:
KVConnectorBase_V1钩子(start_load_kv/save_kv_layer/get_num_new_matched_tokens),UCM 挂在钩子上做跨请求持久化。
引擎本地匹配(radix)与 UCM 裁决的分工 AND 是”本地内存里能不能复用”(提案);协调器的 min 是”持久层里到底有没有”(确认)。顺序 L → 问协调器 → 以协调器 (l,p*) 为准。
2.3 问题(引擎层)
| 现有设计 | 引擎每 2–3 个月发一版;0.11 只支持“恰好两种注意力组”,0.18+ 放开为 N 种 |
|---|---|
| 遇到问题 | UCM 要跟着每版改;改不动只能打补丁(3.3);引擎还没支持的新注意力类型无法接入 |
| 优化思路 | 只依赖接缝接口;必须改引擎内部的降到最少;推动上游(已把 UCM 薄壳合入 vllm-ascend 主线) |
| Gap 与限制 | 引擎侧永远被动;补丁树 0.9.2…v0270(200 个补丁文件)是现实成本 |
第 3 章 接口层(Connector)
3.1 它是什么
桥 + 哈希器,并计算 KV 块的 16 字节编号。
3.2 哈希“身份隔离”机制
# ucm/integration/vllm/ucm_connector.py:RequestHasher(简化+注释)meta = f"{model_name}:{tp_size}:{dtype}:{rank_id}{spec_info}{sparse_info}"# ^模型名 ^张量并行度 ^精度 ^rank ^投机/稀疏开关 → "盐"# 注:MLA 组在构造时 rank 恒取 0,等效"盐不含 rank"(见下方"MLA 例外");# 非 MLA 的 worker 侧再用真实 rank 二次哈希隔离。block_id = md5(meta + 父块编号 + 本块token列表) # 前缀链式哈希
- 为什么父块编号入哈希,编号必须不同,否则前缀缓存会错用;
- 组粒度隔离 kv_cache_group 还有独立哈希链种子(组间零别名;实现 = hla_connector.py 的 KVCacheGroupManager,以
(UCM_GROUP_SEED, 基础盐, 组号)派生每组建种子)——这使“同 token 前缀在 C4 压缩组、MLA 组、SWA 组各算各的号”,互不干扰。MLA 例外 组的盐不含 rank——内容跨 rank 相同,必须同号才能共享;非 MLA 的 worker 侧再做一次 rank 二次哈希隔离; - 存储层不认识 token,只认 16 字节编号——模型语义与存储被哈希彻底隔离。
3.3 现状问题 个类与组合爆炸
UCMConnector 门面按场景分派到 9 个分派实现 + 2 个死代码 = 1 门面 + 9 实现 + 2 死代码 = 12 个类(活类共 10 个)。9 个实现一句话(默认,load→forward→save 同步式)、LayerWise(逐层流水)、CP(上下文并行分片)、FAWA(DS-V4 双存储特判)、HLA 与 HLA-LayerWise(线性注意力混合及其逐层版)、Lite(轻量)、Mock(测试)、监控(推理时长观测)。PD/Blend 是历史遗留(原为 PP 解码 / 稀疏混合 KV 设计),已被后续实现取代且无引用,属死代码。

为什么新模型“被迫”新写类(推演) X → can_handle 判断要认 X 的形状 → 判断写进现有类 → 现有类的内部流程(如逐层 dump)按已知形态编写,X 对不上 → 改现有类有风险,新写类隔离风险 → 历史选了新写类(FAWA 即例证)→ 类越来越多。根因(形状)与场景逻辑(时机)写在同一个类里,两个变化轴(模型 × 场景)只有一个维度(类)表达。
补丁问题 个版本目录、约 200 个补丁文件(含 __init__.py 共 203 个,口径见附录 C)直接改引擎源码。补丁要分两类看待:生命周期类(改引擎的调用时序,如“谁在何时调 wait/save”→ 每个引擎版本重打一遍)收敛为“每版本一个适配器”;模型类(认新模型形状,如 Mamba 对齐)→ 等引擎上游吸收,不该永远留在补丁里。
3.4 优化后的接口层
| 管 | 事件翻译、哈希计算(盐)、完成粒度选择(整批/逐层就绪) |
|---|---|
| 不管 | 命中判断、保留策略、存储内部 |
第 4 章 协调器(L2)
4.1 规格表
规格表 = 每个 kv_cache_group 一行,启动时算一次、运行期只读。示例(全部使用同一模型 X-Hybrid-96,与图 8 图 9 口径一致):
SPEC_TABLE = [ # 组名 kind block 每层字节 种子 秩规则 ("mla", "chain", 128, [512]*16, "S_mla", "all_union"), ("csa_c16", "chain", 128, [fp8+scale]*16, "S_c16", "all_union"), ("swa", "chain", 128, [...], "S_swa", "all_union"), ("mamba2", "snapshot",64, [512]*20, "S_m2", "all_union"), ("kda_perslot","snapshot",1, [...], "S_kda", "chk=interval"), ("cross", "none", None, None, None, None),]# kind: chain(积木)/snapshot(罐头)/none(不缓存)# 种子: 哈希隔离(每组的独立哈希链,防组间撞号)# 秩规则: 见下(4.2 的"秩规则(定义)"段)# 每层字节: 每 token 字节数(示例值;csa_c16 的 16 = 示例压缩比,真实模型见 6.1 的 1/4、1/128)
4.2 命中裁决
def resolve_hit(candidate_L): # candidate_L: 引擎本地匹配的候选长度(仅作首轮裁剪) l = INF for g in chain_groups: # ② 对每个链式组(多组共用存储实例时仍按组建查询) l_g = BlockStore[g].lookup_on_prefix(请求的前缀哈希链) # 按该请求的块哈希链问"持久层最长存在到哪" l = min(l, l_g) # ③ 组件投票 = 取交集(错命比漏命贵) l = floor_to_lcm(l) # 对齐到各组块大小的公共刻度(LCM) p_star = INF for s in snapshot_groups: # ④ 每个快照组查自己的检查点目录(键含前缀哈希,见 4.3) p_s = checkpoint_dir[s].deepest_candidate(l) # "最深保留检查点 ≤ l" p_star = min(p_star, p_s) # ④' 跨快照组取最小:所有组的检查点都就位才可跳过 return (l, p_star)数字例题见 fig13(3000 token 2560、csa_c16 2432(整块规则)、SWA 3000 → 交集对齐 = 2432;检查点 p* = 2304;引擎跳过 [0,2304) 完整计算,[2304,2432) 续算,之后全重算)。

秩规则(定义):“这一组由哪些 rank 负责写盘、跨 rank 聚合取并集还是交集”——MLA(内容跨 rank 相同) rank dump 成功即可,聚合取并集(all_union);非 MLA(各 rank 内容不同) rank 必须自己 dump,聚合取交集;快照组的秩规则与链式组相同(交/并);规格表里的 chk=interval 是保留策略参数(见 4.3),不是秩规则——两列语义不同。
现状 vs 目标(重要分界)(代码)里 MLA 是 rank0 单点 dump 全部层(见 7.4),单点失败即整块丢失;目标语义是 all_union——任一 rank dump 成功即可,用冗余换可用率(算例 B 是目标语义,7.4 是现状观察;7.0 三句话里的“只让 rank0 存”也是现状)。
多个快照组怎么裁决(4.2 的补全);引擎要“跳过 [0,p*] 的完整计算”需要所有快照组在 p* 处都可用——所以 p* = min over 快照组(各自最深的可用检查点)(伪代码中的 ④′)。算例 A 只让 mamba2 组参与打分(简化;X-Hybrid-96 的两个快照组规则相同,见 8.1),p* = 4096 的结论不变。
两个一致性问题的回答:
- 引擎 AND 与协调器 min 的关系 AND = 本地提案,协调器 min = 持久层确认;顺序,以协调器为准;上收 = UCM 侧散落实现统一为单函数,引擎内部照跑。
- 裁决 → 执行的竞态窗口 load 到自己的 GPU 槽;
load完成(数据进 GPU)后便是本地副本,之后缓存层怎么淘汰都不影响本次请求。正确性锚点 = “load 完成”,不是“裁决时点”。
4.3 检查点与保留策略(快照数据的“何时存、何时失效”)
保留策略(在哪些位置创建检查点,三触发):
- 请求结束(仅当有新增计算/新状态;完全命中、无新增内容则跳过);
- 同一前缀第二次“未被服务”的出现 → 在公共边界存(命中过缓存的不算,它已享受);
- 定间隔/输出每隔 N token 存一个。
Put 与登记这一环的执行者 由适配器在 save_kv_layer(状态层)时执行(它持有快照地址与组前缀哈希);完成后经回调把 (组, 位置, 前缀哈希) 登记进协调器目录——目录由引擎侧写入、协调器持有,与“协调器不碰字节”不冲突(它只收登记结果,不发起传输)。
检查点有效性(为什么),取决于它依赖的前缀链式块还在。两种做法(存储淘汰时通知协调器 = 跨层事务 + 有状态协调器)vs 惰性失效(采用)“有效标志”,用时现算——min 结果天然 = 链式块最长存在到哪,检查点目录里“最深的 ≤ l 的位置”即有效。块没了 ⇒ 检查点自动够不着 ⇒ 零通知、零跨层协议,淘汰永远不产生错误命中。
目录的键必须是 (组, 位置, 前缀哈希) p* 候选——多前缀在同一位置各存各的快照时,“位置对、内容错”的跨前缀错命不会发生(位置 4096 的检查点只对“前缀哈希链匹配到 4096 的请求”可见)。

检查点目录崩溃恢复。方案①(缺省) miss → 引擎重算并重新 Put(检查点是加速,不是正确性);方案②(可选) 加“枚举键”接口,重启全量重建。多机部署同理,盘面可共享;跨机命中 = 目录 miss → 首次 Get 失败 → 重算并按需重建,与崩溃恢复同构。快照条目自身被存储淘汰 = 下次 Get miss → 该目录项自然作废,引擎退化为“该段状态重推”(漏命安全)。
4.4 问题(协调器层,四分法)
| # | 现有设计 | 遇到问题 | 优化思路 | Gap 与限制 |
|---|---|---|---|---|
| C1 | 无规格表;模型知识散在 can_handle + 两份不一致半成品表(GroupInfo hla_connector.py / KVCacheGroupMeta hma_connector.py) |
同模型多处判断;新模型改多处 | 合一为规格表,链表驱动 | 字段对齐;迁移期双跑——双跑期以旧逻辑为准,新表只记账比对,不一致即告警并冻结切新 |
| C2 | 组件投票雏形在 lookup_external_hit_tokens(hla_connector.py;HLA——Hybrid Linear Attention,线性注意力混合连接器——的类内) |
别的 Connector 用不上;和引擎两套实现 | 上收为协调器单函数 | 行为等价回归 |
| C3 | 检查点语义缺失(状态 hash 折叠,无位置键) | 无法位置查询/CoW;命中只能整段退化 | 检查点目录 + 惰性失效(4.3) | 依赖 SnapshotStore(第 5 章) |
| C4 | 秩规则特判(is_mla 布尔 + FAWA 手搓聚合) |
三处判断不一致 | 规格表“秩规则”行 | 存量迁移 |
4.5 算例集(把规格表与裁决“算给你看”)
算例 A + 快照组不投票(5000 token 前缀,X-Hybrid-96 前三组 + 快照组 + 侧车)
| 步骤 | 数字 |
|---|---|
| 链式组 l_g | MLA(块128) 4608(块 36);csa_c16(块128) 4480(块 35,complete-block 整块规则);swa(块128) 5000(窗口内) |
| 对齐与取交 | LCM = lcm(128,128,128) = 128;min(4608,4480,5000) = 4480;4480 % 128 = 0 → l = 4480 |
| 快照组不投票 | 只经 p* 参与 {4096, 4608};4608 > l 够不着,4096 ≤ l → p* = 4096 |
| 返回 | (4480, 4096) [0,4096) 完整计算+状态;[4096,4480) 只算 KV、状态重推;4480 之后全重算 |
| 错命演示 | 若只看 MLA 的 4608 就跳过 → [4480,4608) 的 csa 块不存在 → 读空/错位数据 → 静默错乱;取交集最多重算 → 安全 |
注直接给定的,以聚焦裁决数学;目录的产生过程见 4.3 保留策略与 4.5 算例 C。本例为 prefill 场景,假设 SWA 窗口 ≥ 5000(窗口未截断);窗口不足时命中长度被窗口截断(7.3 澄清 2)。
算例 B“正确性 × 冗余策略”(非 MLA 交集 vs MLA 并集)
- 非 MLA 组 4 个 rank 为例(数字与 TP 解耦,TP=8 时结论形式不变)——各 rank 持有同一逻辑块的不同分片(哈希盐含 rank,内容不同),4 片全在才命中该块。设每 rank dump 失败概率 p = 0.1(独立) = (1−p)⁴ ≈ 65.6%;任一 rank 失败,该块对整前缀不可用——宁可漏命重算,不可错用(intersection);
- MLA 组 rank 相同(共享池去重),任一 rank dump 成功即可 = 1−p⁴ ≈ 99.99%(union,4 份冗余);rank 失败只损失冗余,不损失可用性;
- 结论,是语义选项——非 MLA 以冗余换可用率,MLA 以冗余换容错;规格表里的一行,决定“这块数据用交还是并”。
- MLA 跨 rank 相同的前提 共享/复制(如 DP、或多 rank 共享同一 kv 头);若 TP 按头切分(内容不同),该组秩规则应改 intersection——前提由部署决定、在规格表“秩规则”列显式声明,不是架构假定。
算例 C → 检查点目录如何增长(10000 token 长输入,间隔 1024)
| 事件 | 检查点目录变化 | 说明 |
|---|---|---|
| 请求1 完整跑完 | {1024, 2048, …, 9216, 10000} | 定间隔触发 |
| 请求2 相同前缀,链式块与检查点全部命中(到 10000) | 不变 | 已被服务过,不触发“二次未见” |
| 请求3 前缀相同但只到 5000(从未被缓存服务过该边界) | 链式块命中 5000,但检查点 @5000 缺失 → 从最深检查点 4096 续算;请求结束新增 {5000} | 按需补点 |
| 请求4 与请求3 同前缀(第二次未见 @5000) | 已存在,不重复 | 二次未见触发的是“创建”,不是“重复创建” |
注(如 64 的倍数)对齐;实际实现中检查点位置会按网格对齐(4.2 多快照组裁决段)。
4.6 算例小结
以上算例把第 4 章的三件事串起来(min)决定“长度”,检查点目录决定“深度”,秩规则决定“交还是并”。三者的组合就是
resolve_hit的全部输出 (l, p*)——引擎拿到后只需做 fig13/算例 A 里的分段执行。
第 5 章 存储层(混合模型视角)
5.1 什么数据进什么原语(主线的唯一问题)
| 原语 | 键 | 服务的数据 | 操作 |
|---|---|---|---|
| BlockStore | (组, 块哈希) |
全部链式数据 潜向量、压缩 KV、滑动窗口 KV、索引器(侧车) | Dump/Load(批量)、LookupOnPrefix、Touch |
| SnapshotStore | (组, 位置, 前缀哈希) |
全部快照数据/GDN/Mamba 状态、未压缩压缩尾 | Put、Get(=CoW)、Donate(零拷贝移交)、Touch |
-
逻辑键 vs 物理键(统一全文的说法)/规格表层面用逻辑键(组, 块哈希)(组间经独立种子隔离,3.2);落到 store 是物理键 = 16 字节 BlockId,存储层按 (块号, shard) 寻址。7.2 的”(块号, shard)“(shard 眼下叫”第几层”)、7.3 的”(块, shard)“都是同一物理键的说法——shard 眼下叫”层”,目标架构叫“存取单位”,语义变宽、机制不变;
-
SnapshotStore 键里的“位置” 序号(对齐到检查点间隔);位置差一个 token,快照不可用——“罐头”语义;
-
CoW(拷贝后写),取用前复制到私有,防互相污染;
-
Donate(零拷贝移交),省一次拷贝;
-
为什么快照也不需要“每任务 size” per-model 静态(规格表携带),任务描述保持
{块号, 分片, 地址}即可——“几何=配置,语义=任务”纪律的体现(机制详见附录 E5);快照组的 block_size 是检查点网格粒度(mamba2 token 一格;kda token),不是链式块大小,也与tokens_per_state(状态压缩粒度)不同; -
同一内容只存一份 owner/非 owner 机制(谁先锁到空槽谁取数,其余等待)是 store 内核细节,见附录 E6;
-
存储层只收 Touch 脉冲,不收 pin 指令,存储无会话元数据(三义澄清见附录 D Q8)。
5.2 缺什么(现状问题)
| 现有设计 | 状态(Mamba)折进哈希(seq_len 揉进编号);为它加了 LookupOnReverse(9 个存储实现)——LookupOnReverse 的语义 = “从右向左找最长存在的连续段”(状态类数据的反向依赖专用),目标架构中由位置键取代,无需再反查 |
|---|---|
| 遇到问题 | 表达不了“位置”;无 CoW(共用被改会数据串扰);每加一种查询语义改 9 个文件 |
| 优化思路 | SnapshotStore(位置键 + CoW/Donate);LookupOnReverse 删除(位置在键里,不需反查) |
| Gap 与限制 | 全新原语;状态淘汰策略(位置价值)要设计 |
第 6 章 模型世界全景与业界对照
6.1 模型波次 → 缓存种类(本报告的分类轴)

| 年份 | 里程碑 | 模型/论文 | 对缓存系统的冲击 |
|---|---|---|---|
| 2024 | MLA | DeepSeek-V2 | KV ↓93%,缓存 = 压缩潜向量 |
| 2025 | DSA 稀疏注意力 | DeepSeek-V3.2 | 索引器(FP4)+top-k;prefill 常数化 |
| 2026 | CSA+HCA 压缩注意力 | DeepSeek-V4 | 压缩 KV 池(1/4、1/128)+ FP8 缩放;complete-block 规则(前缀命中≤最后一个完整压缩块,尾部重算);SWA 每层分支 |
| 2026 | KDA+MLA 同池 | Kimi K3 | 状态与 MLA KV 共用同一个分页块池(统一字节页);检查点只在稀疏边界;命中 = 边界检查点 + MLA 块 CoW |
| 2026 | 块稀疏 GQA | MiniMax M3 | 57/60 层块稀疏(top-16 块);无状态池 |
| 2026 | KDA+DSA 混排 | GLM-5.3-Flash | 34 KDA 线性层 + 11 DeepSeek 稀疏注意力(MoE);索引器 top-k 2048 |
| 2026 | GDN+QSA | Qwen3.8-Flash-Next(Qwen4 预览) | 36 Gated DeltaNet + 12 QSA 微块稀疏;索引器预算/token 粒度 |
6.2 四大引擎的混合模型实现(2026-08-27 主分支核实)
| 维度 | vLLM | vllm-ascend | SGLang | TensorRT-LLM |
|---|---|---|---|---|
| 寻址 | 分页块 + spec 分组;BlockHash 链式 |
同左 + storage_block_size=block_size//compress_ratio |
token 级 radix 树 | 分页块 + V2 块基数树 |
| 对齐 | lcm(组块大小);hash 粒度 = gcd |
lcm_block_size + 硬约束 kernel_block_size=128 |
mamba_checkpoint_grid=lcm(chunk,page) |
chunk 为块整数倍;池间无跨型 LCM |
| 命中 | HybridKVCacheCoordinator 定点迭代,组间 AND;per-group 长度暴露给 connector |
同左 + DS-V4 双 FA 组各自截断 | radix 公共前缀 + 检查点网格着陆 | 按池 block-hash 匹配 |
| 状态 | MambaManager “align” 块快照 + _pending_partial_tail_offloads(CoW 边界交 connector) |
同左 + AscendCompressorStateCache(compress_ratio∈{4,128}) |
donate/ping-pong + Int8CheckpointStore(2×容量) |
周期快照/显式边界;V2 才支持块复用 |
| 索引器 | DeepseekV4IndexerCache(tokens_per_state=compress_ratio) |
AscendDeepseekV4IndexerCache + csrc compressor |
DS-V4 压缩状态池(pad 到 LCM) | — |
| offload | KVConnectorBase_V1(Mooncake/LMCache/NIXL/FlexKV/HF3FS/Moriio) |
kv_pool/kv_p2p + UCM shim | storage/ 后端(LMCache/Mooncake/NIXL/HiCache…) | kvCacheConnector + NIXL transceiver |
注“hash 粒度 = gcd”指各组块大小的最大公约数(最小哈希单位,粗块可复用细哈希);“状态保存在最后 1+数个投机块”的机制见 2.2。
逐家细读(2026-08-27 各主分支 commit 核实):
- vLLM n 模式切分(
_get_kv_cache_groups_uniform_page_sizepattern 槽位归组、不足补 padding——如 10 个全注意层 + 20 个滑窗层按 1 的 pattern 重复 10 次 = 30 层,每 pattern 一组的槽位合并即得 3 组,每组 10 层);0.11 曾硬断言“恰好两种组”,0.18+ 改为定点迭代循环支持 N 型;每层AttentionLayerBase.get_kv_cache_spec()自己产 spec(0.18 下沉);coordinator 通过find_longest_cache_hit_per_group把每组的命中长度暴露给 connector(hit_diverged= 组间命中不一致)——这是我们的“组件投票”在引擎侧的原型;MambaManager的 “align” 快照把状态记在最后1+num_speculative_blocks块,_pending_partial_tail_offloads把 CoW 边界块直接交给 connector 做 sub-block 前缀 offload——这是我们 SnapshotStore 的 CoW 语义的先行者。 - vllm-ascend:
storage_block_size = block_size // compress_ratio(Ascend 内核吃压缩页,调度器按原始 token 单位算);kernel_block_size = 128的硬件硬约束(注意力页必须 ≥ mamba 页,再 pad 对齐);DS-V4 路径两个全注意力组(C4/C128)各自截断后取共同学位;Compressor+AscendCompressorStateCache(kv_state + score_state,float32)是压缩注意力的状态缓存;kv_pool/ucm_connector里就是我们 UCM 的薄壳。 - SGLang 级 radix 树(trie);Unified Radix Cache 的“组件投票”——“match boundary is only advanced when all validators return True”(FULL 路径锁/SWA 窗口锁/MAMBA 单节点锁,级联逐出 Full>SWA>Mamba,tombstone 恢复);
donate_mamba_ping_pong_slot把活动槽直接捐给缓存、Int8CheckpointStore(per-head-k 对称 int8)让检查点容量翻倍;检查点必须落在mamba_checkpoint_grid = lcm(chunk,page)网格上——与我们的 p* 对齐是同一思想。 - TensorRT-LLM 的
blockRadixTree块基数树(叶子块才能逐出);mamba 状态支持“周期快照”或“显式边界”,但要状态与注意力块复用必须用CppMambaHybridCacheManager(V2 才把状态并入 C++ 块池);卸载接缝kvCacheConnector+ NIXL transceiver(Python 侧)。
四个统一观察(它们直接支撑本报告设计):①LCM/网格对齐是通用语言——四家全在做;②组间命中 = AND/min——四家全在做(我们做成协调器单函数);③状态 = 块对齐快照 + CoW——四家全在做(我们立为第二原语);④每家都开了外部存储接缝——证明“缓存系统”是行业需求。“组件投票”不是我们的发明,是引擎侧已有机制的 UCM 侧确认。
6.3 主流卸载/分层系统对照(缓存放不下的地方,业界怎么做)
| LMCache | SGLang URC + HiCache | NIXL + vLLM NixlConnector | Mooncake | Dynamo KVBM | DeepSeek 生产 | |
|---|---|---|---|---|---|---|
| 寻址 | chunk 前缀链 hash(默认 256 token)+cache_salt | radix 树 token 跨度 + per-component 值 | 内存段描述符,无 key | 前缀链块 hash(512 token) | GPU 块 ID + chunk hash 事件 | 64-token 前缀单元,complete-unit 匹配 |
| 分层 | L1(GPU+CPU DRAM/CXL)→L2(磁盘/GDS/S3/Redis…) | GPU L1→host L2→分布 L3(Mooncake/HF3FS/NIXL) | 内存+存储抽象(DRAM/VRAM/NVMe-oF/文件/对象) | GPU→CPU DRAM 池→SSD(NoF-oF/DISK) | GPU→CPU→SSD→远端(经 NIXL) | 磁盘默认开启;仅输入前缀命中 |
| 转移 | 批量 chunk DMA + CUDA kernel,分层流水 | page-first 零拷贝 + IO kernel | UCX(IB/RoCE/TCP/SM/cuda-ipc/NVLink);异步 RD/WR | TransferEngine RDMA/TCP(87–190 GB/s) | NIXL 传输 | 内部(构建需数秒) |
| 淘汰 | per-backend LRU/LFU/FIFO + fleet 配额;pin/move | refs + 会话软保护 + 组件 LRU + 级联逐出 + tombstone | 调用方负责(block lease/TTL) | 池 LRU/LFU/LengthAware;soft/hard pin;热点复制 | lru/arc;store_threshold | best-effort,数小时~数天清理 |
| 混合/状态 | 无(经 connector 布局适配) | 是/SWA/MAMBA 组件投票、CoW 状态、网格对齐 | 传输任意架构(含 Mamba/SWA) | 无(经 SGLang group 语义) | SWA/SSM 块占位事件 | SWA 形状前缀单元;MLA 原生 |
六个系统其实在回答三个问题“数据住哪”——LMCache 的分层(L1 显存/内存 → L2 磁盘/远端)、Mooncake 的“GPU→CPU 池→SSD”、DeepSeek 的磁盘默认开启,全是“分层”的不同分法;第二个问题“怎么找到它”——LMCache 用 chunk 哈希、Mooncake 用前缀链块哈希、SGLang 用 radix 树、NIXL 干脆不找(只搬段),各自的“寻址轴”不同、有的按位置、有的按段;第三个问题“淘汰谁”——从 LRU/LFU 到会话软引用、从 pin 到 best-effort,差别在于“热度信号从哪来”。我们的设计把第一个问题留给存储双原语的实例划分,把第二个问题收敛成“链式/快照”两轴(6.5 铁律 1),把第三个问题交给“Touch 脉冲”——三个问题上都取业界的主流形态,再收进自己的哲学里。
6.4 业界做法 → 我们的设计映射(取什么、拒什么)
| 业界做法 | 出处 | 我们的处理 |
|---|---|---|
| 组件投票(“所有 validator 都接受才推进”) | SGLang URC / vLLM coordinator | 取,做成 resolve_hit 纯函数 + 每存储一查(4.2) |
| 状态 = 块对齐快照 + CoW + 边界交 connector | vLLM MambaManager / _pending_partial_tail_offloads |
取形立原语 SnapshotStore(第 5 章) |
| donate / 移交所有权 | SGLang donate_mamba_ping_pong_slot |
取,SnapshotStore 的 Donate 操作 |
| soft/hard pin(持久保留指令) | Mooncake | 拒 是元数据,违背“存储无会话语义”;改用 Touch 脉冲(见附录 D Q8) |
| 完整前缀单元 + 三触发保留 | DeepSeek 生产 | 取同源(4.3)与 complete-block 规则 |
| chunk 寻址(256 token) | LMCache | 不冲突,不影响“积木/罐头”分类轴 |
| 段式内存 + 描述符传输 | NIXL / Mooncake TransferEngine | 取 L4 传输层(与 #1277 P2P 同构) |
6.5 五条铁律(每条一句论证)
- 可寻址性是唯一稳定分类轴(积木/罐头),不由模型名字决定——所有引擎(对照见附录 C 深度材料)都在用不同语言说同一件事;
- 几何写进规格,不写进任务,几何变化 = 新实例;
- 组间命中取交集(错命比漏命贵) = 读不存在块/错位数据 = 静默错乱;漏命 = 重算,幂等安全;
- 状态 = 块对齐快照 + CoW、不可拼接,能复用的只有对齐边界处的快照,取用前复制防污染;
- 侧车跟随源池,不投票,源组命中了它才可用;不进入 resolve_hit 循环。
6.6 设计哲学 × 现实的七对矛盾与适配
设计哲学从来不是免费的“现实中的一处张力”为代价,混合模型把这些张力放大。本节把全部张力摆到同一张桌上,每条按“哲学 → 现实/矛盾 → 适配 → 代价”展开。6.6.1/6.6.2 把最长的那条因果链——无 size → 环效率 → 协调器——讲透;6.6.3 是其余六对的速查表;6.6.4 算总账。
6.6.1 第一对 size 与环效率——FIFO 到 CLOCK 的证明(为什么不是 LRU)
哲学 size 字段,几何只以配置存在(E5)。现实(O(1) 读写查找)的成立条件,改动它会让五处几何同时塌(E5 ①–⑤)。混合模型的冲击(MLA 512 B vs FP8 压缩 vs 状态张量)、位置键数据(状态检查点)、共享行(跨层粒度)——环只吃同构几何 + 纯内容键。
证明链(为什么淘汰只能是 FIFO→CLOCK,而 LRU 被系统性排除):
- 内容寻址 ⇒ 数据不能移动 = 内容的哈希,槽位 = 地址;移动数据 = 换地址 = 失去“按内容找地址”的性质,所以淘汰只有一条路——原地覆盖;
- 定长槽 ⇒ O(1) 寻址:
DataAt = base + index × nodeSize是纯算术;槽宽可变则退化为扫描,或按最大宽分配(容量税),或引入二级索引(几何塌); - LRU 被约束排除 需要访问序结构——双向链表(环的定长头部布局装不下跨进程可变结构)或每槽时间戳(驱逐时求全局最小 = O(n));两者都破坏“槽 = 数据、别处无状态”;
- FIFO 是几何内最便宜的选择(一个指针
freeHead++,旧实现的真实样子),但零 recency miss 立刻挤掉(批量加载活锁,实测踩过); - CLOCK = FIFO 指针 + 每槽 1 bit 1 → 清 0 放行(二次机会),扫到 0 → 选为 victim;摊还 O(1)、几何不破(bit 长在槽头上),recency 窗口 = 一个指针周期。这是“地址 = 位置、槽 = 数据”约束下可达到的最近似 LRU——不是懒得做,是 LRU 家族(链表/堆/时间戳)在此被排除;2-bit 变体(再扫一轮)是继续逼近 LRU 的方向(OS 页面置换是同一套论证)。
适配:①按几何分实例(每个 BlockStore 一种几何,实例化 = 参数);②位置键数据不进环 → 第二原语 SnapshotStore(第 5 章);③“哪个组什么几何”需要权威 → 规格表(4.1)。
6.6.2 为什么必然推出协调器(不是架构偏好)
- 环的约束在 store 内部不可放松(放松 = 五处几何塌或失去内容寻址),所以混合模型的三样新东西只能在 store 外消化;
- 于是需要一个“懂模型但不碰字节”的层回答三个问题、怎么复用、何时保留——规格表 +
resolve_hit+ 检查点目录 = 协调器(第 4 章); - 反证,模型知识只能留在 Connector(12 类组合爆炸)或漏进 store(
LookupOnReverse已漏过一次),两条路都被历史验证是坏味道; - 边界点破 给 TaskDesc 加“完成粒度”字段不算违背纪律——它是时序信号(何时通知谁),不是几何(size);“几何=配置,语义=任务”约束的是数据语义,不约束时序信号。这条区分让 store 侧的最小改动(E4)与哲学自洽。
6.6.3 其余六对(速查表)
| # | 哲学 | 现实/混合模型的矛盾 | 适配 | 代价/开放项 |
|---|---|---|---|---|
| 2 | 内容寻址、无索引(找数据 = 算哈希 → 查路径) | “位置”不是内容 token 处无法用前缀哈希表达 → 被逼出 LookupOnReverse(9 个存储实现,每加一种查询语义改 9 个文件)——哲学的第一次违约 |
SnapshotStore 键 = (组, 位置, 前缀哈希),把位置放回键里 → 查询语义消失,LookupOnReverse 可删 |
全新原语;位置价值淘汰策略要设计(5.2) |
| 3 | 存储无会话语义(只收瞬时 Touch,不收 pin) | 热度信号表达不了“位置价值”(状态“未来可能被续上”的复用概率),而快照数据的价值恰是位置价值;存储侧也非真无状态(CLOCK 位/ref/mtime 真实存在,Q8 三义) | 会话语义上收给协调器的保留策略(4.3);Touch 只做翻译后的脉冲下发(5.1) | SnapshotStore 的位置价值淘汰 = 开放项 |
| 4 | 组间取交集,错命 > 漏命(铁律 3) | 组越多交集越短(算例 A vs 单组 4608);SWA 的 decode 场景命中恒为 0(v0240 补丁注释)——命中率最大化与零错命本质冲突 | 有意的取舍,业界四引擎同一选择(6.2);检查点 p* 分段消化“组的短板”;验收里程碑按 prefill/长前缀场景定口径(第 9 章基线口径段) | 场景口径必须写进验收,否则数字对不上 |
| 5 | 内容寻址天然去重(“同一内容只存一份”) | 物理份数由秩规则决定 MLA 盐含 rank → N 份;MLA 跨 rank 同号 → 1 份 + 冗余——去重与冗余是同一硬币的两面 | 算例 B 65.6% vs 并 99.99%;去重只是内容寻址的副产品,份数语义显式声明在规格表“秩规则”列(4.5 算例 B 末段) | 前提(跨 rank 共享 kv 头)由部署声明,非架构假定 |
| 6 | 容量配置期钉死(硬上限,不协商) | 多组共享总容量还是各自容量?FAWA 手搓双存储容量拆分(组语义漏进 store 配置);快照被逐出 = 静默劣化命中 | 按几何分实例 ⇒ 容量也是规格表参数,手搓拆分被配置行取代 | 存量迁移;快照容量与位置价值冲突需策略 |
| 7 | “store 不懂模型”是目标,不是现状 | 现状系统已有五处“懂模型”的特判,散落在 store 及其周边:LookupOnReverse(状态,store 层)、FAWA 双存储(组,Connector 层)、Compressor(压缩,引擎侧)、prerequisite event(时序,store 层)、HotnessTracker(会话热度,store 层) |
协调器 = 把“懂”从各层逐处扒出去的收敛动作;先按上表列清单,扒一处减一个特判 | 收敛期双跑与行为等价回归(第 9 章阶段 1) |
6.6.4 代价小结,一对付学费
- 第 2–7 对的适配都是收敛/参数化/上收,不新增结构复杂度;唯一新增复杂度的是第一对逼出的协调器本身(检查点目录在内存),崩溃恢复只能靠惰性失效(首次 miss → 重算 → 重新 Put,4.3),多机同理——这是“用一个新的有状态组件换掉 N 个特判”的净复杂度账;
- 判定准则(给 store 加会话元数据、加 size、加第二索引)都直接违背上面某一对矛盾,应回到本节拒掉。
第 7 章 逐层流水(Layerwise)适配
7.0 三句话版本(只想懂个大概,读这里就够了): 模型是一层一层算的。KV 存在外面,取回来、存出去都要时间。逐层流水的意思是:算第 1 层的同时,后台把第 2 层的 KV 取回来、把第 0 层的 KV 存出去——像工厂流水线,加工和搬运同时进行,谁也不用干等。 为什么混合模型绕不开它:①模型被切成多段放在不同卡上(PP),每张卡只有“自己的那几层”,所以必须按层存取;②哪一层由谁负责存,是规则决定的(MLA 只让 rank0 存);③mamba 状态层这种“罐头”数据也是按层出现的。 现状的毛病(一整层所有请求一起等,一个慢全等)、仓库线程少、队列满了会悄悄丢掉该存的层(你以为存了,其实没有)、有的连接器(FAWA)压根没用逐层、投机解码会重复存同一层。改进方向“层×请求”给就绪信号、一层一个任务、失败必须报出来、状态层放进 SnapshotStore。
7.1 什么是逐层流水(用大白话讲)
KV Cache 是模型记住历史输入的“笔记”,太大放不下 GPU,要存在外面。取回笔记、存回笔记都要花时间。模型是一层一层算的(第 0 层、第 1 层、第 2 层……每层都要读前面的 KV)。
两种笨办法:
- 先全取再算 KV 全部取回来 → 开头干等(存储延迟全暴露);
- 先算完再全存 → 结尾干等(batch 尾部被存储拖住)。
逐层流水 = 第三种办法:边算边取边存。时间线(一个 3 层模型的例子):
| 时间 | 引擎(计算的一方)在干嘛 | 仓库(UCM)在后台干嘛 |
|---|---|---|
| t0 | 通知仓库:“我要开始算了,把第 0 层 KV 取回来”(start_load_kv) |
开始取第 0 层 |
| t1 | 问:“第 0 层到了吗?”(wait_for_layer_load(0)),到了 → 开始算第 0 层 |
取完第 0 层后,马上去取第 1 层(超前一层) |
| t2 | 第 0 层算完,通知仓库:“拿去存!”(save_kv_layer(0)),并给一个“我算完了”的信号 |
收到信号后,开始把第 0 层存出去;同时第 1 层的 KV 已经在路上 |
| t3 | 问:“第 1 层到了吗?”→ 开始算第 1 层 | 存第 0 层、取第 2 层 |
| t4 | 第 1 层算完 → 通知存第 1 层 | …… |
| t5 | 全算完,问:“都存完了吗?”(wait_for_save) |
确认所有层都安全存好 |
这就是”算 k 层的同时,取 k+1 层的货、存 k-1 层的货“——三角流水,谁也不干等。
引擎怎么知道在哪个时刻打电话?靠“钩子”(hook) UCM 提供的固定函数。这些调用顺序是引擎保证的契约(见 7.2)。
7.2 引擎的“四个电话”与 UCM 现在的做法
引擎保证的调用顺序(连接器可以依赖的输入契约):
- 开算前一次电话:
start_load_kv(必须早于第一层); - 每层计算前一个电话:
wait_for_layer_load(k)——“第 k 层 KV 到齐了吗?没到齐就先等着,到齐才开算这一层”; - 每层计算后一个电话:
save_kv_layer(k)——“第 k 层算完了,拿去存”(异步,不等它存完); - 全部结束一个电话:
wait_for_save+ 收尾——“该存的都安全存完了吗?”(这通电话保证 内存里的旧 KV 在被覆盖之前,已经存走了)
保证的具体细节:①每层的电话严格按层号从小到大打;②同一个 batch 里,同一层可能被叫到好几次(投机解码的原因),但只需要等一次、存一次;③存出去的顺序 = 算的顺序,但存完的顺序不保证——所以不能假设“先存的先完成”。
UCM 现在的做法(代码里的实现):
- 开算前只提交第 0 层的取数任务;
- 每次等完一层,立刻顺手提交下一层的取数任务(超前量 = 恰好 1 层);
- 存档时,引擎会传一个“这层算完了”的事件(prerequisite event),仓库收到这个事件才开始拷贝——保证不会拷到还没算完的脏数据;
- 等存档,攒到请求结束再确认(把等待推迟,让计算不被存储卡住)。
层在存储里怎么编号(哈希只含模型/精度/rank/组),层的身份由“分片号(shard)“承载 KV 块的物理键 = (块号, shard),shard 眼下就是”第几层”(7.3 之后 = 存取单位)。PP 下不同卡共享同一批块号,靠 shard 区分“这是谁的层”。

进阶读者.3(存取单位模型)、7.4(混合模型交叉)、7.5(十个问题)、7.6(设计规约)、7.7(开放问题)保留技术细节,配合附录 A 术语表阅读。
7.3 存取单位(access unit)
核心修正.1/7.2 把“层”当默认存取单位,但混合模型里“层”和“存取单位”不是一回事。一份数据可能、几层共享一份、按窗口滚动、或干脆跟着别人(侧车/共享)。必须先回答“存取单位是什么”,再谈“哪个钩子触发哪个单位的存取”。
四类存取单位(引擎与 UCM 代码共同证实):
| 类型 | 结构 | 引擎如何表达(细节可跳过) | UCM 现状 |
|---|---|---|---|
| 每层独立 (层, 块) | MHA/GQA/MLA、线性注意力(GDN)、mamba 状态、KDA、cross-attn | group 块表 per spec;shared_by=[该层] |
通用 KVCacheLayout(行=层;强制层同构,否则报错) |
| 窗口/组级(与层解耦) | SWA(每层内容、同 spec 层共享窗口槽位)、压缩池 C4/C128(各自组)、索引器块 id 与 MLA 组共享 | SlidingWindowSpec 合组;MLAAttentionSpec(compress_ratio);Ascend 显式:“share block ids with the MLA cache in the same UniformType group” | FAWA 用 canonical 边界做 WA 行(canonical 边界 = FAWA 的哈希块边界,默认 256 token/块,hma_connector.py:321);它与规格表的组块大小(4.1 的 128 等)是两个刻度 store 块号与 LCM 对齐的依据(算例 A 的“块 36”按 128 数),canonical 只负责 WA 行折叠——层只体现在行内偏移 |
| 跨层共享张量(row) | 混合 n pattern(GDN+full 交替);引擎一个 raw 张量被整组层按各自组块表子划分寻址 | KVCacheTensor.shared_by 多成员;register_cross_layers_kv_cache(跨层 uniform 布局;含 Mamba 时禁用) |
HLA 的 row:layer_name_to_row 把多层映射到同一行;row_save_layer = 行内层号最大者触发整行 dump |
| 数据真共享(YOCO) | 层别名跟随目标层,不存自己的数据 | kv_sharing_target_layer_name / shared_kv_cache_layers(不走 shared_by) |
未建模 |
四个关键澄清(常被误解,逐条说清):
- “组”共享的是块表,不是数据内容“块号→物理槽”映射,但内容永远按层存;真正共享内容只有 YOCO 一类;
- SWA 可以投票,但被窗口截断 的块数据真实存在、可以参与“存在性”投票,但只有窗口覆盖范围内的数据才有复用价值——命中长度会被滑动窗口边界截断(v0240 补丁注释原文 的 decode 场景因窗口很小,SWA 前缀命中恒为 0)。算例 A/图 13 是 prefill 场景、窗口未截断,所以 SWA 的 l_g = 5000/3000 成立;
- 索引器,但块 id 与同组的 MLA 共享(Ascend 显式声明、块表共享)。物理存放位置是容量决策,可与“块 id 归属”不同——如示例图 9/图 13 把索引器并入 SWA 实例存放,但它的块 id 仍跟随 MLA 组(身份/投票语义不变);
- SGLang 的“层”不在键里 键 = 位置(token 前缀),一个节点 = 全部 L 层状态栈;UCM 的“层键”与 SGLang 的“位置键”是两个坐标系(跨引擎复用语义时要桥接)。
UCM 四个布局各自只覆盖一类,互不通用(核心缺口):
- 通用
KVCacheLayout(ucm_connector.py)“每层独立”,层规格不一致直接报错; - HLA 的 row(
HybridLinearAttentionLayout,hla_connector.py +layer_name_to_row)shared_by非空的跨层共享张量——“既有共享又有独享”的模型,独享层被静默跳过; FAWA(hma_connector.py) canonical 边界折叠 per-layer 窗口(n_win/压缩比取组代表层的值),且没有 SharedIndexer 的“幽灵槽”(空槽位掩码)机制;SharedIndexerKVCacheLayout(ucm_connector.py)(nullptr 掩码)表达“这层借别层的索引器”,但索引器被每个持有层重复落盘(冗余),没有“索引器属于哪个层”的元数据。
对 Layerwise 的三个直接影响:
- 触发时机“层→单位”映射决定——引擎只在每行的最后一个 full-attn 层调用 wait(中文转写 HLA 代码注释:“vLLM 只在 full-attn 层(每行的最后一层)调用 wait_for_layer_load,所以第 0 行必须在线性注意力开始之前加载”);
- 完成粒度“单位 × 请求”,不是“层 × 请求”——一个 row 的整行 dump 只在行内最大层号那层触发,等待该 row 完成;
- 就绪语义 的就绪判定按 key(哈希块),与“单位”不是一回事——WA 只存最终边界时,“部分边界可用”的中间态没有表达。
冲击与解法(深入讨论)
先给结论:存储层不用改——store 的键本来就是 (块, shard),而 shard 恰好就是“存取单位”(层号/行号/窗口边界号都是 shard 的具体形态)。要改的是两处用法:①连接器别再“把 shard 当层号”,而是“把规格表声明的单位映射成 shard”;②规格表明确声明“单位”。
逐个冲击与解法:
| 冲击 | 现象 | 解法 |
|---|---|---|
| 1. 规格表里“组”≠“单位” | 一个组可能含多个单位(同 spec 多层=每层一个单位);索引器单位跨组跟随 MLA | 规格表 group 行加“单位列表”字段(类型/窗口组/共享 row/跟随),由引擎 shared_by/组/YOCO 声明驱动,R9 |
| 2. 命中裁决是“组级投票”,取数是“单位级” | resolve_hit 的 l_g 是组级前缀存在;但取数要按单位(WA 只取最终边界、row 只取需要的行) |
两层解耦(4.2 不动);新增“取数计划”——裁决给出 (l,p*) 后,适配器按单位生成“哪些单位、每个单位从哪个键取”;WA“部分边界可用”的中间态由取数计划显式表达。取数计划 = 单位表声明的配置驱动展开(属适配器,8.3),不是新代码 |
| 3. 触发时机按层会错 | 共享 row 只在行尾层触发整行存取;引擎却会在行中每层调用钩子 | “层→单位”解析器(7.3 映射表)决定钩子动作 = 只记账,行尾层 = 触发整行;HLA 的 _dumped_row_ids 去重上升为通用机制(R9 驱动) |
| 4. 秩规则按层不按单位 | MLA 的“只 rank0 dump”、HLA 的“非 rank0 只 dump KDA 部分”都是单位级切分,却写在层逻辑里 | 秩规则行改为按单位(HLA _mla_split_scope 已是单位级切分,把它表驱动化;PP stage = 单位按层区间归段) |
| 5. 完成粒度错配 | 等的是(层/行,请求),store 就绪按 key | store 的 per-shard-ready 就是“单位就绪” 的 shard 集 = 单位(R2),完成信号按 shard 报(R1) |
| 6. 共享 row 的数据写谁 | 一行多分片共享一个 store key(block_size_override 已支持) | 保持 key 对应含 N 个单位分片的块,每次 DMA 只写/读一个 shard——机制现成,单位化只是把连接器传给 store 的分片号(shard)语义正名 |
| 7. 数据真共享(YOCO) | 某层不存自己的数据,跟随目标层 | 新增“跟随”单位类型 = 目标层的单位;规格表声明,连接器不重复落盘 |
白话收束,是“shard”的正名——store 从头到尾都以 (块, shard) 为最小存取单元,混合模型只是让“shard 的语义”从“第几层”变成“哪个单位(层/行/窗口边界/共享张量分片)”。设计改动 = 规格表声明单位 + 适配器按单位映射 shard + 完成/秩规则按单位,存储层零改动。
7.4 混合模型下的 Layerwise 交叉(主线关联)
| 交叉点 | 机制 | 对我们的设计意味着什么 |
|---|---|---|
| PP × Layerwise | 每 rank 只 dump/load 自己 stage 的层;块 id 全 stage 共享 | 秩规则“层归属”行的实例(见 R5) |
| 秩规则 × 层 | MLA 单 rank dump 全部层(现状);HLA 内非 rank0 只 dump KDA 部分(_mla_split_scope) |
秩规则必须按单位(层是单位的一种)表达(目标见 4.2/7.3 冲击 4) |
| 状态层(mamba) | 块对齐快照;状态对齐拷贝(compute 流)必须先于 load DMA(store 流),start_load_kv 内置 synchronize 建立全序——上游 “do not copy here, since kv_transfer still not load” 正是这个坑,UCM 用 mamba_copy_order_patch 修复(补丁文件/integration/vllm/patch/v0210/vllm_ascend/mamba_copy_order_patch.py) |
快照层与链式层的存取必须串行化在同一批块上 |
| 投机解码 | 同层多次访问 → 去重(首次提交获胜,#1243);wait_for_save 延后覆盖 draft | 去重语义 = “首次访问内容”假设(EAGLE 下待验证) |
| 行式混合(vLLM 引擎内的 Qwen3Next GDN 模型;区别于 6.1 的 Qwen3.8-Flash-Next) | 按 row 预取(hybrid_layerwise_prefetch_rows=2);引擎只在 full-attn 层调用 wait → 行 0 必须在 linear_attn 前加载 |
预取深度成为适配器参数 |
| 上下文并行(CP) | 引擎按 token 段切分序列,每个 rank 持有自己的段;现状 UCMCPConnector=false 时钩子空实现、走 bulk 路径,block_size *= cp_world_size,按 current_rank :: cp_world_size 切片 load/dump |
CP 切“序列维”,单位切“结构维”——两个正交维度 CP 表现为”(单位 × 本段 token 范围)“的二维分片;单位键不变,段边界由 CP 自己的块表承载,见 R10 |
| 上游未验证 | vLLM “External KV connector is not verified yet” 断言——mamba align + 外部 KV 传输,UCM 仅删断言未改算法 | 正确性待补验 |
把这张表读成故事 决定“每一段层归谁管”,秩规则决定“每个单位由谁写”,状态层决定“哪几步必须串行”,投机解码决定“哪些层会被重复访问”,行式混合决定“触发点不在层上而在行尾”,上下文并行决定“每个序列段由谁持有其中的单位”——Layerwise 不是“按层流水”这么简单,它是这些约束同时作用的结果。设计的难点不在单条约束,而在它们叠加 MLA+GDN+SWA+CP 的模型,同一时刻有按层的、按行的、按窗口的、按序列段的取数在飞,而它们的完成信号必须各归各的单位。
7.5 现有问题(四分法)
| # | 现有设计 | 遇到问题 | 优化思路 | Gap 与限制 |
|---|---|---|---|---|
| L1 | 完成粒度只有“整任务” | 单个慢的 D2H(设备→主机拷贝,术语表)拖住整层全部请求;无 per-request 完成通道 | store 任务加完成粒度字段,层×请求粒度就绪(附录 E4) | 接口变更 |
| L2 | lookahead 不对称 | 非 hybrid 严格 1 层(Direct=0、hybrid=2 行),无统一预取窗口配置 | 预取深度上升为适配器参数 | 过度预取浪费带宽 |
| L3 | 每方向单 dispatcher+单搬运线程 | 层数越多越串行;队列满时 Submit 失败 = 该层 dump 静默丢失 | 任务级批量 + 队列满可感知(失败进失败集合) | 与批量提交交互要验证 |
| L4 | FAWA 无逐层流水(钩子空实现且选择器优先于 layerwise) | DS-V4 上逐层能力被吞掉;WA dump 还同步等待 | 选择器顺序调整;FAWA 走规格表后自然获得逐层 | 行为等价回归 |
| L5 | HLA 的 wait_for_save 全同步 | batch 末仍被 dump 拖住(未用 #989 延迟完成) | 统一延迟完成机制到所有逐层连接器 | — |
| L6 | 去重“首个提交获胜” | spec 回滚后同层内容有差异 → 存的是旧快照(#1243 的代价) | 层内容版本化或按最终提交 | EAGLE 语义待验证 |
| L7 | PP 半支持 | wait_for_save / dump 聚合的 PP-aware TODO 未实现 |
秩规则行带 stage 归属 | — |
| L8 | 每模型手工插桩 | Kimi-K3 钩子承认重叠变小;qwen3_next/mamba 各版本补丁拼装 | 规格表 + 引擎上游吸收 | 结构性负债 |
| L9 | CUDA graph 捕获-回放 × 逐层 wait 未验证 | capture 时执行、replay 不重跑(依赖“无 metadata 跳过”守卫) | 专项验证/文档化 | 开放问题 |
| L10 | 层名解析 extract_layer_index |
draft/MTP/共享 KV 层命名含数字段时可能冲突 | 解析器加固 | 开放问题 |
7.6 目标架构下的设计规约(layerwise × 存取单位 × 我们的设计)
| # | 规约(不变量) |
|---|---|
| R1 | 完成粒度 = 存取单位 × 请求 任务按“逐分片就绪”模式交付;wait_for_layer_load(k) 映射到”k 所属单位的完成信号”(附录 E4 的 per-shard-ready) |
| R2 | 提交形态 = 每单位一批 TaskDesc(该单位全部请求的块整批),后端对该单位做 I/O 编排;等待粒度 = 单位(批量只改提交、不改等待) |
| R3 | 前序事件通用化 event 上升为通用“单位数据就绪”信号;dump/状态层 Put 都依赖它 |
| R4 | 状态层走 SnapshotStore(状态层) = Put(位置, 前缀哈希);load = Get;CoW 天然适配;单位内去重 |
| R5 | 秩规则 × 存取单位“秩规则”行扩展为“按存取单位的回写归属”;PP stage = 按全局层区间切分的实例(每个单位落在某 stage 内) |
| R6 | 协调器只管首个单位 保证“首个单位在 start_load_kv 时可得”;逐单位推进由适配器按引擎调用序驱动;p* 之后的单位逐单位 Get |
| R7 | 失败必须可感知 load 失败 → 请求级失败标记 + batch 输出对齐(已有);dump 队列满不再静默丢 |
| R8 | 引擎调用序契约(7.2 的四个电话 + 三条保证)是适配器输入 |
| R9 | 层→单位映射必须来自引擎声明 shared_by / 组 / kv_sharing_target_layer_name 驱动,禁止 UCM 从层名推断;规格表每个 group 行新增“单位类型”字段(每层独立 / 窗口组 / 跨层共享 row / 数据真共享) |
| R10 | CP × 单位正交 CP 表现为“每个 CP rank 处理(单位 × 本段 token 范围)的分片”——单位键不变,段边界由 CP 块表承载;CP 下秩规则表现为“段主 rank 拥有本段内单位的写盘归属”;CP 与 DP/PCP 的叠加语义由规格表“秩规则”列声明 |
7.7 开放问题(研究结论)
- CUDA graph / ACL graph 捕获-回放下逐层 wait 的语义(capture 执行、replay 不重跑)——layerwise 与图推理组合是否被显式保证?
- EAGLE 类 draft 先于 target 写 KV 时,“首次访问内容”去重是否存到含 draft token 的 KV?
VLLM_PP_LAYER_PARTITION自定义分区下,全局 shard 映射是否仍可推导?- 上游 “External KV connector is not verified yet” 断言解除后,mamba align + 外部 token 的完整正确性(UCM 只删断言)。
- SGLang 无逐层 dump 挂点 SGLang,需在 attention 后端新增每层保存钩子。
- 层名解析与 draft/MTP 层名冲突。
- UCM 四个布局(通用逐层 / SharedIndexer / HLA row / FAWA)如何收敛到“单位映射 + 双原语”的单一模型——这是存取单位模型落地的最大工程量;
- SGLang 位置键 vs UCM 层键/单位键的桥接(跨引擎复用同一份缓存时的语义对齐);
- vLLM v0.26 的 packed 布局(
offset/block_stride,DSV4 默认)接入后,块内字节级层交错如何映射到存取单位; - YOCO 跟随单位的落地细节(引擎声明接入方式、跟随单位与秩规则的交互)待验证(方向见 7.3 冲击 7)。
- CP × 单位正交的叠加语义(序列段)与 DP/PCP(副本)同时存在时,MLA 共享池的“段主 rank 写盘归属”如何与秩规则组合,需要专项验证(R10 已定义正交原则,具体组合语义待实测)。
第 8 章 复杂模型适配
8.1 压测模型 X-Hybrid-96(承接 2026 波次全部要素)

96 层 = 16 MLA + 16 压缩注意力(CSA,FP8+缩放)+ 24 滑动窗口 + 20 Mamba2 状态 + 8 KDA 逐 token 状态 + 8 跨注意力 + 4 DSA 稀疏索引层;TP=8、PP=2 → PP 两段各 48 层;TP 切参数不切层,每个 rank 持有所在段的全部层(旧版“每 rank 每段 6 层”是把 TP 误当层切,已更正;fig09 同步修订)。
存取单位映射(7.3 的四类在本模型全覆盖,除 YOCO):
| 组件 | 存取单位 | 触发与完成语义 |
|---|---|---|
| MLA(16) | 每层独立 (层, 块) | 每层钩子触发该层存取;完成 = 该层×请求 |
| CSA 压缩注意力(16) | 每层独立;索引器与其共享块 id(同一 UniformType 组) | 压缩态缓存随该层存取;索引器是侧车 |
| SWA(24) | 窗口组级(同 spec 层共享窗口槽位;canonical 边界) | decode 场景前缀命中恒 0(prefill 场景窗口内可投票,见 7.3 澄清 2);只按窗口边界存取 |
| Mamba2(20) | 每层独立状态(每层每序列分页) | 状态层走 SnapshotStore(R4) |
| KDA 逐 token(8) | 每层独立状态(检查点密度参数化) | 同上;保留策略压密度 |
| Cross(8) | 无(不缓存) | — |
| DSA 索引器(4) | 每层物理缓存 + 块 id 跟随 MLA 组 | 侧车,不投票 |
8.2 如果今天要上(现状路径)
- 新写一个 Connector 类(或扩
can_handle); - mamba/KDA 状态折进哈希(无位置键、无 CoW,命中只能整段退化);
- 引擎打 3–5 个新补丁;
- 索引器再特判一次。 → 每模型 = 一类 + 几号补丁 + 几个特判,成本随模型数量线性涨。
8.3 设计后的路径(口径 10 行;零改动仅指协调器/存储/适配器)
| 步骤 | 内容 | 工作量 |
|---|---|---|
| 引擎侧注册 | 8 处(规格类、每层 spec 生产、分组、管理器、注意力后端、协调器参与、模型执行器、Ascend 特化)——改代码/填钩子,不可避免,引擎 0.18+ 已支持 N 类型 | 引擎侧 |
| 规格表 | 约 10 行(4.1 示例为 6 行,完整含全部组的细节参数约 10 行) | 纯配置 |
| 协调器 | 裁决函数零改动;保留策略对“逐 token 状态”组做密度参数化 | 参数 |
| 存储 | BlockStore 零新代码(3 组链式数据按几何分实例,每组一个);SnapshotStore 按组几何实例化(每块网格一种实例 的 mamba2(64 一格)、kda(逐 token)各一个;实例化 = 参数,不是新代码;状态大小 per-model 静态 ⇒ 不需 per-task size) | 0 |
| 适配器 | 单位映射表(配置 shared_by/组展开成 shard 映射);取数计划 = 该表的配置驱动展开;零新类 |
少量配置 |
8.4 五条闭环检验
- 分类完备,要么随前缀单调(→chain),要么只在精确点成立(→snapshot),要么不参与复用(→none)——不存在第四种(由“复用域”定义推出);
- 身份完备 + 位置入键 → 键空间零冲突;
- 决策完备 = 单函数、每存储一查、可单测(fig13 算例即测试用例);
- 语义完备 + 竞态窗口闭合(load 完成即持有副本);
- 演化完备 = 规格表行 + 引擎注册;没有“新 Connector 类”动作。
第 9 章 路线图(混合模型能力主线)
| 阶段 | 内容 | 依赖 | 验证(可验收的数字) |
|---|---|---|---|
| 1 收敛 | 规格表合一、清理死代码(12→10)、完成粒度字段 | 无 | FAWA/HLA 行为等价回归 |
| 2 新原语 | SnapshotStore + 检查点目录(含恢复方案①) | 阶段 1 | 位置键/CoW/Donate 单测;mamba 端到端命中率对比 hash 折叠基线 |
| 3 体验补齐 | GPU 模式 Touch、futex、dump 持久化语义(见附录 E) | 阶段 1 | 唤醒延迟 <1ms(基线 ~10ms) |
| 4 结构性 | 补丁收敛为每版本一适配器;P2P/DramPool 接入两原语的数据搬运路径 | 阶段 2 | 补丁文件数下降率 |
主线端到端验收里程碑(阶段 2/3 门槛,数字为设计目标,上线前以真实模型与硬件校准):
- X-Hybrid-96 端到端走通 10 行 + 引擎注册 + 双原语全链路可跑,裁决/取数/逐单位存取与算例 A/B/C 数字一致;
- 多轮对话命中率 轮对话(共享系统提示 + 历史),持久层命中率 ≥ 90%(无缓存基线为 0;现状 hash 折叠路径作为对照,状态层命中率提升 ≥ 20 个百分点);
- TTFT 对比(≥4K 复用)较全重算基线下降 ≥ 40%;较现状 hash 折叠路径下降 ≥ 20%;
- 正确性 token 一致(错命 = 0);引擎侧地址/长度校验作兜底(FAQ Q6);投机解码/PP/CP 叠加场景各跑 1000 请求无静默错误。
基线口径(让数字可落地):“现状 hash 折叠路径” = 本仓库当前 develop 分支的现行连接器代码(HLA/FAWA 等,未应用本报告改动)。验收流程 = 阶段 0 先用同一请求集与采集脚本(输出 token 生成序列 + 命中长度日志)冻结三组基线(全重算、hash 折叠、以及它们各自的 TTFT/命中率),此后每个阶段用同一脚本复跑对比——任何数字回归先查基线再看新代码。
依赖说明(阶段 1)→ load 逐片就绪(阶段 2);原语(阶段 2)→ 字节面接入传输(阶段 4)。store 内核类优化(CLOCK/旋转/GC 等)不在主线上,见附录 E,可与阶段 1–3 并行推进。
9.1 动手点清单(文件 → 构件 → 动作)——主线新构件每一步落在哪里,供直接开工:
| 构件 | 现状落点 / 建议落点 | 第一步动作 |
|---|---|---|
| 规格表 | 胚胎两份:GroupInfo(hla_connector.py)、KVCacheGroupMeta(hma_connector.py)→ 合一为协调器单表(建议新建 ucm/integration/vllm/kv_spec_table.py) |
双跑,新表记账比对,不一致告警冻结(4.4 C1) |
| 组件投票 | lookup_external_hit_tokens(hla_connector.py)→ 上收为 resolve_hit 纯函数(4.2) |
搬运 + 以 4.5 算例 A/B/C 的数字为单测断言 |
| SnapshotStore | 建议在 ucm/store/ 下新建 store 类 (组, 位置, 前缀哈希),Put / Get(=CoW)/ Donate / Touch |
阶段 2 位置键/CoW/Donate 单测;mamba 端到端对比 hash 折叠基线 |
| 检查点目录 | 建议随规格表同文件的协调器内存组件 | 单测断言 = 4.5 算例 C 的四行目录变化 |
| 取数计划 | HLA 的 row 展开逻辑(layer_name_to_row、row_save_layer hla_connector.py)表驱动化 |
单位表声明 + 配置驱动展开(7.3 冲击 2),零新类 |
| 完成粒度 | TaskDesc 加“完成粒度”字段(store 侧,附录 E4) |
阶段 1,打通 per-shard-ready 通道 |
附录 A 术语表(白话版)
| 术语 | 白话 |
|---|---|
| KV Cache | 模型记住历史输入的“笔记” token 的 K/V 向量 |
| prefill / decode | 预填充(整段并行算一次)/ 解码(逐 token 生成) |
| 命中(hit) | 新请求前缀与已缓存请求相同 → 跳过这段 prefill |
| 错命 / 漏命 | 错误命中(读不存在/错位数据=静默错乱)/ 错过命中(重算=安全) |
| 前缀缓存 | 跨请求复用相同开头的 KV |
| 内容寻址 | 缓存块编号 = 内容哈希;找数据 = 算编号 → 查路径 |
| BlockId | 16 字节缓存块编号(盐化 md5 链式哈希) |
| 前缀链式哈希 | block_id = md5(盐 + 父块 + 本块 token) |
| Shard / 分片 | 一块数据切成的小份(按层切) |
| TaskHandle | 任务单号;开单即返回,之后查/等 |
| 规格表 | 协调器的“每个注意力组长什么样”表(kind/block/每层字节/种子/秩规则) |
| 秩规则 | 每组“谁负责 dump、跨 rank 聚合取交还是并” |
| 组件投票 | 命中判定 = 所有组答案取交集(AND) |
| LCM | 各组块大小的最小公倍数(对齐公共刻度) |
| 检查点 | 快照数据的可用边界位置;有效性惰性派生 |
| 保留策略 | 在哪创建检查点(请求结束/二次未见/定间隔) |
| 侧车(sidecar) | 跟随源组、不投票的附属数据(索引器/缩放因子) |
| CoW / Donate | 拷贝后写(取用前复制防污染)/ 零拷贝移交所有权 |
| owner / 非 owner | 共享池里第一个装载某槽的 rank / 等待别人装好的 rank(机制见附录 E6) |
| Touch 脉冲 | 协调器给存储的瞬时热度信号;非 pin |
| 投机块 | 投机解码预取的额外 token 块 |
| 完成粒度 | 任务完成在“整批”还是“逐分片”层面通知(见附录 E4) |
| complete-block 规则 | 压缩注意力的前缀命中只能到最后一个完整压缩块,尾部重算 |
| UE8M0 | 压缩 KV 的每 token 8 位无符号指数缩放因子 |
| TP / PP / rank | 张量并行(一层切多卡)/ 流水并行(层分段)/ 参与进程(一张卡) |
| radix | 引擎进程内的本地前缀匹配结构(跨请求复用,仅引擎内存) |
| FAWA | FULL+Window 双存储连接器(为 DS-V4 手写)——“手搓特判”的反面样板 |
| MLA / Mamba / KDA / CSA / DSA | 潜压缩注意力 / 循环状态 / 记忆编辑状态 / 压缩注意力 / 稀疏注意力 |
| SWA | 滑动窗口注意力 w 个 token,窗口外无复用价值 |
| HCA / QSA | 高压缩率注意力(DS-V4 的 C128)/ 稀疏注意力(Qwen 系微块稀疏) |
| CP | 上下文并行(把序列切段分到多卡) |
| D2H / H2D | 设备→主机 / 主机→设备 的数据拷贝 |
| MTP / EAGLE | 两种投机解码方案(草稿模型会先写 KV) |
| YOCO | 一类“层共享 KV”的架构,跟随目标层 |
| 单位(shard) | 存取的最小单位 / 行号 / 窗口边界号(7.3) |
| Mooncake | 分布式 KV 共享系统 的可挂载后端之一,也是外部对标对象 |
| pin(钉住) | 强制保留某缓存的指令;存储层只收 Touch,不收 pin |
附录 B 图清单
主线图(正文引用)、02、07、08、09、11、12、13、14、15、16、17、18(逐层流水,第 7 章)。
Store 内核图(附录 E 引用)(dump 时序)、fig04(load 时序)、fig05(CLOCK)、fig06(磁盘布局)、fig10(共享池 owner)。
全部 18 张均有 .excalidraw 源文件(可拖入 excalidraw.com 编辑)。
附录 C 深度材料索引
- 机制与代码锚点、审计盘点(12 connector / 9 store / 200 补丁 / 规格表胚胎两份) V2/V4 可查 git;
- 四引擎 × 混合模型实现对照、六大卸载系统对照(LMCache/SGLang URC+HiCache/NIXL/Mooncake/Dynamo KVBM/DeepSeek 生产)与外部一手来源 §5(该历史版本仅存在于本仓库的 git 历史,邮件分发版不含);
- dump/load 逐行代码:
ucm/store/cache/cc/{dump,load}_queue.cc、ucm/store/detail/template/task_wrapper.h、ucm/store/posix/cc/{space_layout,shard_gc}.cc; - 补丁树清单:
ucm/integration/vllm/patch/(0.9.2…v0270;11 个 v* 版本目录 + 2 个旧式 .patch 目录,版本目录内含__init__.py共 203 个文件——正文“约 200”按此口径); - 引擎注册 8 处明细:
vllm/v1/kv_cache_interface.py、kv_cache_utils.py、single_type_kv_cache_manager.py、kv_cache_coordinator.py、gpu_model_runner.py、attention backend、vllm_ascend/...。
正文出现的 #NNNN = 本仓库 PR/issue 编号,对照(标题据 git log):
- #989 defer UCM KV dump waiting until request completion(已合,7.5 L5);#1128 store health checks and rank consistency recovery(已合);#1204 旋转调度(已合,附录 E2);#1243 投机解码重复 dump 修复(已合,7.4 / 7.5 L6);#1265 futex(OPEN 未实现,附录 E7);#1267 precise GC(已合,附录 E3);#1274 lock-free TransBuffer + CLOCK(已合,附录 E1);#1277 P2P transport(已合,6.4);#1284 DramPool(已合,基线 HEAD,1.1)。
外部一手来源(URL 清单,供核验本报告的外部断言):
- 引擎.com/vllm-project/vllm(
vllm/v1/kv_cache_interface.py、kv_cache_coordinator.py、single_type_kv_cache_manager.py、kv_offloading_usage.md)、github.com/vllm-project/vllm-ascend(patch/platform/patch_kv_cache_coordinator.py、models/deepseek_v4/compressor.py)、github.com/sgl-project/sglang(mem_cache/unified_cache/、mamba_radix_cache.py、mamba_checkpoint_pool.py)、github.com/NVIDIA/TensorRT-LLM(docs/source/features/kv-cache-connector.md、kv_cache_manager_v2/); - 卸载与传输.org/abs/2510.09665(LMCache)、github.com/LMCache/LMCache、github.com/ai-dynamo/nixl、arxiv.org/abs/2407.00079(Mooncake)、github.com/kvcache-ai/Mooncake、github.com/ai-dynamo/dynamo(docs, KVBM);
- 模型.org/abs/2606.19348(DeepSeek-V4)、2607.24653(Kimi K3)、2606.13392(MiniMax M3)、2602.15763(GLM-5);DeepSeek Context Caching 文档.deepseek.com/guides/kv_cache。
- 核对方法 = 按 URL 打开对应源码/论文,检索本报告 6.2/6.3/6.4 引用的机制名(如
shared_by、AscendSFAIndexerCacheSpec、hybrid_layerwise_prefetch_rows、_mamba_block_aligned_split)。
附录 D FAQ
Q1. 裁决返回后到引擎使用之间,块被淘汰了怎么办? 答.2 竞态窗口闭合——裁决后引擎立刻 load,数据进 GPU(Synchronize)后才使用;load 完成即持有本地副本,之后淘汰不影响本次请求。
Q2. 检查点目录(协调器内存)崩溃了怎么办? 答.3 恢复方案① miss → 重算并重新 Put(检查点是加速不是正确性);方案② 加“枚举键”接口全量重建。
Q3. 引擎的 AND 和协调器的 min,两套实现谁说了算? 答 AND = 本地提案;协调器 min = 持久层确认;顺序,以协调器为准。
Q4. 旋转调度下两个 rank 同时装同一块,不会装两份吗? 答——槽位由内容哈希唯一决定;旋转只改处理顺序,不改槽归属(附录 E2/E6)。
Q5. 为什么 KDA 逐 token 状态特别难? 答:“罐头”粒度是 1 token,检查点密度需求极高 → 保留策略按“位置价值 = 频率×时间”压密度(8.3)。
Q6. 为什么用 md5、16 字节会不会撞号? 答 md5 + 前缀链双重约束;碰撞 ≈ 错命,引擎侧有地址映射与长度校验兜底;定性评估亿级块规模可忽略,定量评估为待办。
Q7. Mooncake 是内部后端还是外部竞品?
答——UCM 挂载它为后端(Mooncake|Posix),同时它是分布式 KV 系统对标物。
Q8. “存储无元数据”是否自相矛盾(CLOCK 位/mtime 不是状态吗)? 答:三义澄清:①无索引(内容寻址)②无会话语义(只收瞬时脉冲)③淘汰侧有热状态(CLOCK/mtime 真实存在)——矛盾只出现在把三义混为一谈时。
附录 E Store 内核优化备忘(全量版,非混合模型主线,独立演进)
这些优化与混合模型无直接因果,是 store 内核的独立演进线。每条按“现状机制 → 问题 → 方案 → 规约 → 状态”展开,可在主线之外独立推进。
这些条目虽然独立,但并非与主线毫无交集(E4)正是第 7 章 R1/R2 的存储侧支撑;GPU 热度(E9)是 5.1/6.4 “触觉即流量”哲学的实现通道;owner 机制(E6)是 MLA 共享池(7.4 秩规则)的地基;size 纪律(E5)是“几何=配置”这条全篇纪律的存储侧展开。建议按需阅读,不必按编号顺序。
E1 内存淘汰 二次机会 + 锁无关状态机 + 预分配
-
现状机制:
TransBuffer是定长槽哈希池(nNode = bufferCapacity/shardSize),每槽shardSize字节;旧实现FetchNode= 纯环形指针freeHead++,槽内reference引用计数钉住在途数据,就绪与否是两个独立布尔。 -
问题:①无 recency——新装载的数据可能被下一个 miss 立刻挤掉(批量加载活锁);②两个布尔在多进程共享内存下读“半新半旧”会误判(并发正确性问题);③槽位分配只靠“环形指针 + ref>0 跳过”,无访问历史。
-
方案:①每个槽加
accessed_原子位(命中FindAt与新装载Alloc都置位 → 新数据有免疫窗口);nodeCursor原子环形扫描=1 → 清 0 跳过(二次机会),位=0 → 选为 victim,两轮无果退化兜底;②状态收敛为单状态机LOADING/READY/FAILED+ per-sloterrorCode,全部原子且 static_assert lock-free;FindAt遇ref==0 && FAILED自动重置接管重试;③dispatcher 空闲窗口Prealloc(owner, shard+1)预热下一分片槽(预测落空 = 一个无害空闲槽)。 -
规约(
DataAt = data + nodeSize*i);在途槽(ref>0)永不被选;失败槽仅在 ref==0 时被接管;跨进程 shm 布局与 magic 版本不破;预分配槽保留 access 位,防被 CLOCK 扫掉(预分配槽与 CLOCK 扫描指针的交互需显式定义)。
-
为什么是 CLOCK 而不是 LRU(证明见 6.6.1) ⇒ 数据不能移动(换槽 = 换地址 = 失去内容寻址),LRU 需要顺序结构(双向链表/堆/每槽时间戳),环的定长几何装不下;FIFO 是几何内最便宜但零 recency(旧实现即如此);CLOCK = FIFO + 每槽 1 bit,是“地址 = 位置”约束下可达到的最近似 LRU,2-bit 变体(再扫一轮)是继续逼近方向。
-
状态:✅ 已落地(#1274);共享版为原子游标(对比旧全局锁)。
E2 多 rank 锁步竞争
- 现状机制 rank 共享一个缓存池(MLA/DP 场景,
SharedBufferStrategy),DispatchOneTask按自然序 0,1,2,… 逐分片处理——所有 rank 同一时刻挤同一个 (block, shard) → 同一把桶锁,且只有 owner 取数、其余空转。 - 问题 1 路,TP 越大越亏;锁步竞争。
- 方案:
RearrangeIndex(n, deviceId, localRankSize)rank 从deviceId%N起步、按步长 N 取、再轮转——同一时刻不同 rank 落在不同桶、各自当 owner,后端并发 1→N。 - 规约:①copier 对分片就绪顺序无依赖;②旋转只改处理顺序,不改槽归属(同一内容永远同一槽,owner 机制见 E6);③单 rank 恒等退化;④与批量提交(E4)的交互需重验证。
- 状态:✅ 已落地(#1204)。
E3 磁盘 GC 全局热度模式
-
现状机制 内容寻址扁平文件,GC 按 shard 目录独立扫描、各自回收最旧 mtime;容量以文件数表达(
maxFileCount_ = capacityBytes / blockSize),后台线程定时(interval)+ 采样触发(ratio)。 -
问题 各自为政——文件少的目录永不回收、文件多的目录被误杀;冷热不均。
-
方案:
ExecutePrecise(shard_gc.cc:170) 全部目录候选(跳过.tmp)→ 合并 →std::nth_element按 mtime 全局选 victim(候选比例 = recycle% + candidate_extra%)→ 按目录归组删除,gcLimited循环直到追上进度。 -
规约;
.tmp永不入候选;容量仍是文件数表达;GPU 主路径下 mtime = 最后写入时间(热度缺口见 E9)。
-
状态:✅ 已落地(#1267)。
E4 完成粒度 × 等待粒度解耦
-
现状机制 任务的完成只有“整任务”(Latch 挂在最后分片;dump 提交即完成);load 每分片单独提交后端(N 次提交 N 次等),dump 反而整批提交。
-
问题:①layerwise 需要“每层数据就绪就通知”,没有通道;②批量提交不敢做——整批 Wait 会破坏任务内
read(B) ∥ H2D(A)流水(层就绪时间从max_i(read_i+h2d_i)劣化为read_all + h2d_all),H2D 突发可能超出单层计算窗口。 -
方案——提交形态(逐片/整批给后端 open/iocb 全局编排)与等待粒度(整批/逐分片就绪 必须“整批提交 + 逐分片等待”);
TaskDesc加“完成粒度”字段,完成按 shard 上报。 -
规约、不许改等待粒度;部分失败(GC 竞态缺文件)以 per-shard 结果码带出,失败槽走
MarkFailed→FindAt 接管重试;旋转序在批内保持(index 自带)。
-
状态;与 7.6 的 R1/R2 联动;图 4 的流水注释即此约束。
E5 size 纪律=配置,语义=任务
- 现状机制:
TaskDesc = Shard{块号, 分片, addrs}无 size 字段;每层字节数来自配置(tensor_size_list),CopyStream按配置逐层拷贝。 - 问题(为什么不能给任务加 size)——①槽位算术(
DataAt线性映射、环形游标);②跨进程 shm 布局(header/位阵/meta/data 的固定偏移、magic 版本);③磁盘文件偏移(块文件 = 定长,分片在index*shardSize);④GC 容量核算(maxFileCount_=capacity/blockSize);⑤预分配/reserved 池语义。size 一旦喂给槽分配器或磁盘偏移,五处同时塌。 - 方案 = 配置期定死(规格表/
tensor_size_list);size 只在拷贝层流动(CopyStream本就吃vector<size_t>);未来的动态长度用“存储按上界规划 + 拷贝层实际长度”解决(上界与实际的差值 = 有界容量税)。 - 规约 永不进入槽位/磁盘/寻址决策;任务描述保持精简。与 E4 的边界 可以加“完成粒度”字段(时序信号),但永远不加 size(几何信号)——“语义=任务”管的是数据含义,不是时序,两条同时成立。无 size 为何必须坚持 O(1) 读写查找的成立前提(证明见 6.6.1),任何“给任务加 size”的提议都先回到那条证明链。
- 状态(全篇引用)。
E6 共享池 owner / 非 owner
-
现状机制 rank 共享池中,同一 (块号,分片号) → 哈希 → 唯一槽位(内容寻址保证“同一内容只存一份”);第一个拿到槽锁且槽为空的 rank 成为 owner 负责取数;其他 rank 发现槽已占用(ref>0)且未就绪 → 非 owner 等待(现状轮询状态位,目标 futex,见 E7);owner 失败 → 槽状态 FAILED → 下个请求在 ref==0 时接管重试。
-
问题 语义此前未显式文档化,“旋转调度(改顺序)会装两份吗”的疑问源于此——答案,槽归属唯一。
-
方案/规约 + 原子 CAS 抢 owner + 状态机接管;旋转不改归属;非 owner 等待 = 就绪状态等待(状态机已 lock-free)。

-
状态 #1274 落地(唯一槽 + ref==0 接管重试);本条是把隐式语义显式文档化为规约,与 E2 规约②互为印证。
E7 非 owner 等待 条件等待
- 现状机制,非 owner 等别人装好 = 忙等轮询(上锁-读状态-解锁-睡,循环);状态是锁无关原子,但仍是忙等。
- 问题 空转;唤醒延迟 ~10ms;TP 越大越浪费。
- 方案 进程内 futex word(shm 内)+ 版本号协议“写状态 → 递增版本 → futex_wake”;等待方“记版本 → futex_wait”,期间任何完成必先递增版本 → 无丢失唤醒;挂起 waiter 零 CPU。
- 规约 word 必须进程共享可见;owner 死亡必须有超时 + recheck 兜底(或接入
failureSet_);等待语义不变。 - 状态:⚠️ 上游 #1265 OPEN(未实现)。
E8 dump 失败不传播
-
现状机制:
wait(dump)在“移交后端”即返回(dump_queue.cc:111);真正落盘由 dumper 线程backend_->Wait收尾;dumper 失败只打日志、不进 failureSet、无重试。 -
问题,盘上其实没有;无闭环、无重试。
-
方案(已提交 / 在后端 / 已落盘);新增
wait_durable();backend dump 失败 → failureSet + 有限重试(如 2 次)+ 指标/告警闭环。 -
规约 wait 语义不变(兼容);缓存语义下 durable 失败 = 可接受 miss,但必须可见。

-
状态;投入前先量化
cache_backend_dump_wait_errors_total。
E9 GPU 模式磁盘热度缺失 Touch 脉冲
- 现状机制 CLOCK 位(内存热度);磁盘层
HotnessTracker(utime 刷 mtime)只在deviceId==-1(无 GPU)启用(space_manager.cc:34),GPU 主路径(Cache|Posix)下 Prefetch 是 no-op——磁盘淘汰 = 最后写入时间,不是访问时间。 - 问题 GC 误删;协调器想让磁盘保留热数据没有通道。
- 方案——协调器把会话/单位热度翻译成批量 Touch 脉冲(内存置 CLOCK 位 / 磁盘批量 utime,节流合并;“流量”指瞬时信号流,不落状态,与“pin 元数据”相对);GPU 模式启用;级联契约转发下游。
- 规约 是瞬时信号,永不产生 pin 状态;存储无会话元数据(三义澄清见 FAQ Q8)。
- 状态。
E10 多实例协作盲区
- 现状机制,可在内存记账“盘上有什么”加速查询(布隆过滤器方案)。
- 问题 多实例共享盘(NFS 等) GC 删了文件,别的实例不知道 → 记账会给错误“存在”(错命 = 静默错乱)。
- 方案/决策;查询走 stat/路径判断(正确性优先);若未来有跨实例协调协议再议。
- 状态:已决策。
关联 4–7 章;这些条目可独立排期、与主线并行推进(路线图阶段 1 引用 E4、阶段 3 引用 E7/E8/E9)。