kerikoの行星观察笔记
返回文章列表
22684 字114 分钟

UCM 缓存系统:面向混合模型的分层设计报告

项目笔记#UCM / KV Cache / 混合模型 / vLLM / 设计文档

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 检查点与保留策略(快照数据的“何时存、何时失效”)

保留策略(在哪些位置创建检查点,三触发):

  1. 请求结束(仅当有新增计算/新状态;完全命中、无新增内容则跳过);
  2. 同一前缀第二次“未被服务”的出现 → 在公共边界存(命中过缓存的不算,它已享受);
  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_size pattern 槽位归组、不足补 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-LLMblockRadixTree 块基数树(叶子块才能逐出);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 五条铁律(每条一句论证)

  1. 可寻址性是唯一稳定分类轴(积木/罐头),不由模型名字决定——所有引擎(对照见附录 C 深度材料)都在用不同语言说同一件事;
  2. 几何写进规格,不写进任务,几何变化 = 新实例;
  3. 组间命中取交集(错命比漏命贵) = 读不存在块/错位数据 = 静默错乱;漏命 = 重算,幂等安全;
  4. 状态 = 块对齐快照 + CoW、不可拼接,能复用的只有对齐边界处的快照,取用前复制防污染;
  5. 侧车跟随源池,不投票,源组命中了它才可用;不进入 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 被系统性排除):

  1. 内容寻址 ⇒ 数据不能移动 = 内容的哈希,槽位 = 地址;移动数据 = 换地址 = 失去“按内容找地址”的性质,所以淘汰只有一条路——原地覆盖;
  2. 定长槽 ⇒ O(1) 寻址:DataAt = base + index × nodeSize 是纯算术;槽宽可变则退化为扫描,或按最大宽分配(容量税),或引入二级索引(几何塌);
  3. LRU 被约束排除 需要访问序结构——双向链表(环的定长头部布局装不下跨进程可变结构)或每槽时间戳(驱逐时求全局最小 = O(n));两者都破坏“槽 = 数据、别处无状态”;
  4. FIFO 是几何内最便宜的选择(一个指针 freeHead++,旧实现的真实样子),但零 recency miss 立刻挤掉(批量加载活锁,实测踩过);
  5. 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 现在的做法

引擎保证的调用顺序(连接器可以依赖的输入契约):

  1. 开算前一次电话:start_load_kv(必须早于第一层);
  2. 每层计算前一个电话:wait_for_layer_load(k)——“第 k 层 KV 到齐了吗?没到齐就先等着,到齐才开算这一层”;
  3. 每层计算后一个电话:save_kv_layer(k)——“第 k 层算完了,拿去存”(异步,不等它存完);
  4. 全部结束一个电话: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) 未建模

四个关键澄清(常被误解,逐条说清):

  1. “组”共享的是块表,不是数据内容“块号→物理槽”映射,但内容永远按层存;真正共享内容只有 YOCO 一类;
  2. SWA 可以投票,但被窗口截断 的块数据真实存在、可以参与“存在性”投票,但只有窗口覆盖范围内的数据才有复用价值——命中长度会被滑动窗口边界截断(v0240 补丁注释原文 的 decode 场景因窗口很小,SWA 前缀命中恒为 0)。算例 A/图 13 是 prefill 场景、窗口未截断,所以 SWA 的 l_g = 5000/3000 成立;
  3. 索引器,但块 id 与同组的 MLA 共享(Ascend 显式声明、块表共享)。物理存放位置是容量决策,可与“块 id 归属”不同——如示例图 9/图 13 把索引器并入 SWA 实例存放,但它的块 id 仍跟随 MLA 组(身份/投票语义不变);
  4. 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 的三个直接影响:

  1. 触发时机“层→单位”映射决定——引擎只在每行的最后一个 full-attn 层调用 wait(中文转写 HLA 代码注释:“vLLM 只在 full-attn 层(每行的最后一层)调用 wait_for_layer_load,所以第 0 行必须在线性注意力开始之前加载”);
  2. 完成粒度“单位 × 请求”,不是“层 × 请求”——一个 row 的整行 dump 只在行内最大层号那层触发,等待该 row 完成;
  3. 就绪语义 的就绪判定按 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 开放问题(研究结论)

  1. CUDA graph / ACL graph 捕获-回放下逐层 wait 的语义(capture 执行、replay 不重跑)——layerwise 与图推理组合是否被显式保证?
  2. EAGLE 类 draft 先于 target 写 KV 时,“首次访问内容”去重是否存到含 draft token 的 KV?
  3. VLLM_PP_LAYER_PARTITION 自定义分区下,全局 shard 映射是否仍可推导?
  4. 上游 “External KV connector is not verified yet” 断言解除后,mamba align + 外部 token 的完整正确性(UCM 只删断言)。
  5. SGLang 无逐层 dump 挂点 SGLang,需在 attention 后端新增每层保存钩子。
  6. 层名解析与 draft/MTP 层名冲突。
  7. UCM 四个布局(通用逐层 / SharedIndexer / HLA row / FAWA)如何收敛到“单位映射 + 双原语”的单一模型——这是存取单位模型落地的最大工程量;
  8. SGLang 位置键 vs UCM 层键/单位键的桥接(跨引擎复用同一份缓存时的语义对齐);
  9. vLLM v0.26 的 packed 布局(offset/block_stride,DSV4 默认)接入后,块内字节级层交错如何映射到存取单位;
  10. YOCO 跟随单位的落地细节(引擎声明接入方式、跟随单位与秩规则的交互)待验证(方向见 7.3 冲击 7)。
  11. 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 如果今天要上(现状路径)

  1. 新写一个 Connector 类(或扩 can_handle);
  2. mamba/KDA 状态折进哈希(无位置键、无 CoW,命中只能整段退化);
  3. 引擎打 3–5 个新补丁;
  4. 索引器再特判一次。 → 每模型 = 一类 + 几号补丁 + 几个特判,成本随模型数量线性涨。

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 五条闭环检验

  1. 分类完备,要么随前缀单调(→chain),要么只在精确点成立(→snapshot),要么不参与复用(→none)——不存在第四种(由“复用域”定义推出);
  2. 身份完备 + 位置入键 → 键空间零冲突;
  3. 决策完备 = 单函数、每存储一查、可单测(fig13 算例即测试用例);
  4. 语义完备 + 竞态窗口闭合(load 完成即持有副本);
  5. 演化完备 = 规格表行 + 引擎注册;没有“新 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 门槛,数字为设计目标,上线前以真实模型与硬件校准):

  1. X-Hybrid-96 端到端走通 10 行 + 引擎注册 + 双原语全链路可跑,裁决/取数/逐单位存取与算例 A/B/C 数字一致;
  2. 多轮对话命中率 轮对话(共享系统提示 + 历史),持久层命中率 ≥ 90%(无缓存基线为 0;现状 hash 折叠路径作为对照,状态层命中率提升 ≥ 20 个百分点);
  3. TTFT 对比(≥4K 复用)较全重算基线下降 ≥ 40%;较现状 hash 折叠路径下降 ≥ 20%;
  4. 正确性 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_rowrow_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.ccucm/store/detail/template/task_wrapper.hucm/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.pykv_cache_utils.pysingle_type_kv_cache_manager.pykv_cache_coordinator.pygpu_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.pykv_cache_coordinator.pysingle_type_kv_cache_manager.pykv_offloading_usage.md)、github.com/vllm-project/vllm-ascend(patch/platform/patch_kv_cache_coordinator.pymodels/deepseek_v4/compressor.py)、github.com/sgl-project/sglang(mem_cache/unified_cache/mamba_radix_cache.pymamba_checkpoint_pool.py)、github.com/NVIDIA/TensorRT-LLM(docs/source/features/kv-cache-connector.mdkv_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_byAscendSFAIndexerCacheSpechybrid_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-slot errorCode,全部原子且 static_assert lock-free;FindAtref==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)。

评论