《把 KV Cache 压缩推到极限:DeepSeek-V4.1-Flash 技术报告精读》读的是 V4.1-Flash 的技术报告,算的是每 token 的体积账:常驻 HBM 的 global KV 压到 890 字节/token,比 V4-Flash 小 3.9 倍。但压缩改掉的不只是体积,还有 KV 的形态:一个 token 的状态不再是一段连续张量,而是散在一组异构池里。
先纠正一个概念。 KV Cache Offloading 这个词已经不够用了:V4 要下推的不只是 KV,还有一整类压缩器的滚动状态(state)。它们同样占显存,同样要下推到 host,恢复时也得找回来,但在匹配语义、备份单位、能否备份这三件事上跟 KV 并不一样。本文说的「池」,KV 与 state 都算。
本文读源码。SGLang 把分层缓存叫 HiCache,L1 是 GPU 显存、L2 是 host 内存、L3 是远端存储,本文只看 L1 与 L2 之间的那一段。两代 DeepSeek V4 的池形态不同,HiCache 得为它们各搭一套挂接:池的数量只差一个,组成却几乎全换了。
2026-09 | 源码:SGLang
c475ac5eaf(2026-09-19)。引用点标注为文件:行号,相对仓库根;默认的 tree core 后端是 Python 实现(tree_core_registry.py:1-6),所以正文以 Python 为准,Rust 侧的实现只在关键处并列。
一、从七个池到八个池
V4 的每一层产出不止一种状态。压缩后的主 KV、稀疏选择要用的 indexer K、压缩器的滚动状态,各自有独立的内存池。
SGLang 把这些池的名字收在同一个枚举里(hicache_storage.py:60-92),V4 相关的有十五个取值,实际注册进 host pool group 的是其中一部分。
1.1 V4:C4/C128 压缩 KV,加两个状态环
注册全在 build_deepseek_v4_hicache_stack(hybrid_pool_assembler.py:719-1013)里。V4(CUDA、FP8 配置)最终是七个:
DEEPSEEK_V4_C128_STATE 在枚举里,但故意不注册(hybrid_pool_assembler.py:949-950):
# C128 state pool is intentionally not registered with hicache.
# page_size=256 % 128 == 0, so state pool is not consumed on load.
page_size(256)能被 C128 的压缩比(128)整除,注释给出的理由是 state pool is not consumed on load:恢复时用不到它,所以不必占一份 host 侧注册。这个推理为什么只写给 C128 而不写给 C4,源码里没有第二处解释。
1.2 锚点池不装数据
注册表里最反直觉的一项是 kv。它的 host 侧是一个专门为此写的类(memory_pool_host.py:56-175):
class LogicalHostPool:
"""Pure-logical anchor pool for V4 HiCache.
The pool manages page-aligned token slots but holds no KV tensor. V4
compressed side pools use these logical FULL indices as stable page anchors.
"""
它的搬运方法全是空操作:backup_from_device_all_layer 与 load_to_device_per_layer 都是 pass(:145-148、:150-160),get_data_page 返回空张量(:162-163)。alloc 与 free 还额外强制按页对齐(:117、:134 两条 must be page-aligned 断言)。
换句话说,V4 在 host 上的真实字节全部落在别的池里,锚点只提供索引空间和「是否已备份」的记账。
1.3 V4.1:ratio 1/2 的 latent,状态环消失
两代的差别不在池的数量上,在压缩比的取值域上。compress_ratio 允许 {0, 1, 2, 4, 128}(models/deepseek_v4.py:784-792),其中 ratio 1 和 2 只有 V4.1 用(configs/deepseek_v41.py:44 的注释:the native V4 config rejects 1/2):
docs/cookbook/autoregressive/DeepSeek/DeepSeek-V4_1.mdx:89**compressed latents**, produced only at source layers and shared forward,
collapsing 2 positions into 1 in layers 2–19 and 1-to-1 from layer 20 on
V4.1-Flash 的 40 层(cookbook :87)里没有一层是 ratio 4 或 128,它用的是 ratio 1 与 2 的 latent(一个 latent 就是每个位置压出来的一条 512 宽 KV 向量,同时当 key 和 value 用;cookbook 同页说这代只有四个 KV source 层,另有八个 index source 层)。这带来两个连锁后果。
第一,C1/C2 是另一族池,由另一个函数注册。 它们不在 build_deepseek_v4_hicache_stack 的主体里,而在 _dsv4_low_ratio_entries(hybrid_pool_assembler.py:588-664),由主体在 :989-991 调用:
for ratio, names in (
(1, (PoolName.DEEPSEEK_V4_C1, PoolName.DEEPSEEK_V4_C1_INDEXER, PoolName.DEEPSEEK_V4_C1_INDEXER_SCALE)),
(2, (PoolName.DEEPSEEK_V4_C2, PoolName.DEEPSEEK_V4_C2_INDEXER, PoolName.DEEPSEEK_V4_C2_INDEXER_SCALE)),
):
低比例的 indexer 一律 force_fp4=True(deepseek_v4_memory_pool.py:1416),走 uses_aiter_fp4_layout 分支,于是 index_k_with_scale_buffer 被置为 None、拆成 payload 与 scale 两份。所以每个 ratio 出三个池而不是两个。
第二,两个状态环消失了。 注册 C4 状态池的那段(hybrid_pool_assembler.py:884 的 if not is_unified_kv:)缩进是 8,嵌在 if c4_layer_mapping:(:826,缩进 4)里面。V4.1 没有 ratio 4 的层,c4_layer_mapping 为空,整段都不执行。
于是 V4.1 是八个:
七变成八,装的东西几乎全不一样。 V4 的压缩主 KV 与压缩器状态被换成了 V4.1 的低比例 latent 与其 fp4 indexer,状态环这一类干脆不存在。
枚举里还有几个池名实践中不出现:
-
• deepseek_v4_c4_rope/deepseek_v4_c128_rope只在 ROCm 的 unified KV 布局下注册。unified KV 会把一行拆成 fp8 的 nope 池与 bf16 的 rope 池两份(hicache_storage.py:81-82),而_dsv4_rope_sibling在非 unified_kv 时直接返回None(hybrid_pool_assembler.py:667-686),所以 CUDA 上不建这两个池。 -
• draft/draft_indexer/draft_swa属于 DSpark,V4.1 自带的三阶段投机草稿(cookbook:87),不属于主干。
V4.1 还有两样东西同样不进池清单:Engram 的 n-gram 记忆表走独立的 pin 内存(layers/engram.py:547 的 _HostTable 自己 memfd_create 加 cudaHostRegister,整个文件不 import mem_cache),SWA bounded replay 改的是层的计算范围,不新增池。
1.4 单池时代留下的三条假设
不管 L2 是 host DRAM 还是别的介质,分层缓存在单池 KV 时代攒下的实现都建立在这三条上:
-
1. 一个池。备份一个节点等于把它的 KV 搬下去,恢复等于搬回来。 -
2. 节点粒度是天然的备份边界。radix tree 的一个节点就是一段前缀。 -
3. 备份状态是二元的。数据要么在 L2,要么不在。
下面三条冲突对应这三条假设。
二、三个结构性冲突
2.1 一个前缀的恢复,要求所有池同时就位
分层缓存的读路径有个硬约束:某个前缀能用,前提是属于它的每种状态都能拿到。单池 KV 下这条天然成立,因为 KV 是一个张量,搬就整体搬。
多池下不成立。一个 token 的可用状态分布在七个(V4.1 是八个)池里,缺任何一个,attention 读到的就不是这段前缀。而驱逐的粒度是树节点:host 侧有一套独立的 LRU 与叶子集合(evictable_host_leaves、host_lru_lists,unified_tree_core.py:519-520),和 device 侧那套是分开的,所以「host leaf」指的是 host 侧 LRU 上的叶子节点,不是 device 侧的叶子。
如果一个节点的多个池能被独立驱逐,就会出现「主 KV 还在、状态已经没了」的中间态,而树上仍然认为这个前缀可用。
所以节点粒度的备份边界,在多池下必须升级成跨池的原子单元。上游的答案是让驱逐本身原子化。_evict_host_leaf 的文档字符串第一句就是「Atomically evict all components on a host leaf」(unified_tree_core.py:1831)。
2.2 状态池不在树的匹配语义里
树的匹配边界由各 component 的 validator 决定。SGLang 的 ComponentType 只有四个取值(unified_cache/component_type.py:6-12):FULL、SWA、MAMBA、C128。而 CUDA 路径上 V4 只注册了两个(registry.py:164-181,V4 命中 is_hybrid_swa):
tree_components = [ComponentType.FULL]
if ctx.is_hybrid_swa:
tree_components.append(ComponentType.SWA)
也就是说,压缩状态池都没有对应的 component,不设 validator,不影响匹配边界。它们在匹配语义里是隐形的。
这是刻意的:状态是 chunks 的滚动快照,没有「前缀」这个概念。但它带来一个后果:树的匹配结果无法保证状态池也命中。恢复路径必须在别处补上这个保证。
而 SWA 的 validator 是有窗口门槛的:沿树连续累计,中途遇到缺口就清零,累计值达到 sliding_window_size 才承认边界。这条只在 Rust 侧有那个直白的变量名(components/swa.rs:402 的 contiguous_len);Python 侧用的是 dict 状态 state["len"](components/swa.py:325、:339、:340),逻辑相同、写法不同。
「不参与匹配」只适用于压缩状态池,C128 不算。 NPU 上的 C128SidecarComponent 是有 validator 的(registry.py:170-179,门槛是 req_to_c128_sidecar)。
2.3 复制型 KV 与按 rank 切分的 sidecar
sidecar 指自己不持有槽位、直接复用另一个池索引的派生池,机制在 §3.1 展开。
V4 的 num_key_value_heads 是 1(configs/deepseek_v4.py:76,models/deepseek_v4.py:679 有 assert config.num_key_value_heads == 1)。TP 切分下每个 rank 的主 KV 完全相同。上游对此有直接注释:
cache_controller.py:708-709# DeepSeekV4TokenToKVPool has compressed MLA-style rank-replicated cache
# data. storage only needs rank 0 to write it back.
但这只是问题的一半。不是所有池都复制。 同一个 TP 组里,主 KV 各 rank 相同,而按 rank 切分的那部分状态不是。上游把这层区分写在了备份路径的注释里:
hybrid_cache_controller.py:730-731# MLA KV is replicated across TP ranks and should still be written only
# by TP0. Rank-sharded sidecars still need every TP rank.
于是写路径上同时存在两条相反的规则:复制型的部分只让 TP0 写,切分型的部分每个 rank 都要写(backup_skip 的判据在 cache_controller.py:554-558)。任何一边写错,L2 里要么堆着若干份一模一样的副本、有效容量被 TP 组大小整除,要么缺掉非 0 号 rank 的状态,恢复时读到错的答案。
三、HiCache 怎么把它们接进树
3.1 池挂到树上:锚点加 sidecar
SidecarPoolSpec 是这套挂接的核心抽象(hicache_storage.py:128-133):
class SidecarPoolSpec:
"""Pool whose transfer indices are reused from one real source pool."""
pool_name: PoolName
indices_from_pool: PoolName
hit_policy: PoolHitPolicy = PoolHitPolicy.ALL_PAGES
sidecar 不自己算索引,直接复用源池的 device/host index 与 key。源池在这一步没有 transfer 时,整个 sidecar 被跳过。
这解释了 §1.2 的锚点为什么可以什么都不装:锚点提供槽位空间和树的挂载点,sidecar 提供字节。 两者共用同一套 token 级槽位,地址是派生出来的,不存在「主 KV 的备份地址和状态的备份地址对不上」这种错位。
两代的挂接关系汇总在 _sidecar_srcs(hybrid_pool_assembler.py:1610-1627)。换成树形一眼能看出谁挂在谁身上:
kv(锚点,自己不装数据)
├── V4 c4 / c4_indexer / c128
├── V4.1 c1 / c1_indexer / c1_indexer_scale
│ c2 / c2_indexer / c2_indexer_scale
└── 条件 c128 / c128_rope(C128 不是独立树组件时)
swa(SWA 环内的 KV)
└── V4 c4_state / c4_indexer_state
(c128_state 在表里,但没有 host pool 承接,永远被 entry_map 过滤掉)
hit_policy 由源池决定:SWA 派生的用 TRAILING_PAGES(只有末尾窗口必须命中),其余用 ALL_PAGES(:1632-1636)。
原子性落在分配层。HostPoolGroup.resolve_host_transfers 一次把所有未解析的 sidecar 槽位分配完,失败就整体回滚(pool_host/group.py:92-155):
"""Allocate unresolved side-pool host indices atomically.
On failure, every allocation made by this call is released and the
corresponding transfer is restored to its unresolved state.
"""
回滚本身只有四行(group.py:110-113):逐个 free、把 transfer.host_indices 置回 None。锚点那一份不在这个方法里,由调用方负责。hybrid_cache_controller.py:334-344 在 resolve_host_transfers 返回 None 时,把已分配的锚点槽位 free 掉再返回失败。
状态池根本没有分配器(memory_pool_host.py:904-919)。
raise NotImplementedError(
f"{self.pool_name} reuses SWA transfer indices and has no allocator"
)
alloc、free、available_size 三个入口全抛这个错,只有 DeepSeekV4StateHostPool(:686)用它。既然索引完全从 SWA 派生,自己再维护一张 free list 只会制造两份真相。
3.2 写路径:入了队,但还没提交
L1→L2 有两种写策略:write_through 在节点被第二次访问时(hit_count >= write_through_threshold,默认 1)就立即备份,write_back 等到 HBM 满、节点被驱逐时才写。完整对比见 KV Cache L1↔L2 数据流深度分析:那篇拆的是旧版 HiRadixCache 的路径,识别符与本节不同,但不变量的道理相通。下面这段时序在两种策略下都成立,只有设备锁那一条是 write_through 专属。
commit_backup 的调用位置决定了整条写路径的时序:
host_indices = self._execute_kv_backup(
node_id, device_value, comp_xfers, sidecar_xfers
)
if host_indices is None:
return 0
self.tree_core.commit_backup(node_id, host_indices, comp_xfers)
(unified_radix_cache.py:1554-1556,这是全仓库唯一的生产调用点。)
_execute_kv_backup 调 write() 的方式决定了这一点(:1597-1600):
# Defer submission so the next flush can merge pending node backups.
return self.cache_controller.write(
device_value, node_id=node_id, extra_pools=aux_xfers or None, flush=False
)
flush=False 的意思是只入队、不提交。write() 的文档字符串把目的写得很清楚:
hybrid_cache_controller.py:330-331"""Queue a D2H backup; flush=False leaves it queued so the caller can
merge several nodes into one start_writing() submit."""
所以 commit_backup 执行时,host 侧的副本既不在内存里、搬运也还没开始,操作只是躺在 write_queue 里等着被合并提交。窗口比「搬运在飞」更宽:
t1 与 t3 之间就是那个窗口。backuped 的判据只是主 KV 在 host 上有没有值(unified_tree_core.py:160-163):
@property
def backuped(self) -> bool:
"""Tree-level: Full KV present on host."""
return self.component_data[ComponentType.FULL].host_value is not None
窗口由三件事兜住:
设备锁。 非 write_back 路径下,节点在 commit_backup 之后立刻加设备锁,到 ack 才释放(unified_radix_cache.py:1557-1560)。
未 ack 标记。mark_write_through_pending 给每个覆盖到的节点盖上 ack id(unified_tree_core.py:2465-2479),ack 到达时按 id 匹配清除(:2552-2561)。在这之前,节点既不算「已落地」,也不会被当作「两处都有的副本」回收——_is_settled_full_host_duplicate 的判据里明确排除了两种在途状态(unified_tree_core.py:1519-1529):
"""Full KV present on both tiers with no in-flight DMA on the node's
host slots; mid-transfer nodes join the tracking at their ack."""
return (
... # 不是 root,且两层都有值
and node.write_through_pending_id is None
and node.load_back_pending_id is None
)
丢失计数。 如果驱逐真的赶在 ack 之前发生,这次备份就算作废,上游把它记成一个带原因标签的指标(metrics_collector.py:2312-2320):
name="sglang:hicache_dropped_tokens_total",
documentation="The number of logical device KV tokens destroyed "
"without a host backup. The pool label is always kv; reason is "
"host_pressure for write-back failure, or "
"write_through_unbacked_eviction when "
"eager write-through did not complete before eviction.",
上游没有消除这个窗口,只是让它可观测。
3.3 读路径:graft 与三道守卫
读路径的核心动作是把从 L2 取回的前缀接回树上,源码注释里叫 graft(unified_radix_cache.py:2172 的否定式、components/swa.py:1348 的正面式)。落在 insert_host(Python 实现 unified_cache/unified_tree_core.py:2146;Rust 侧 unified_tree_core.rs:3071,转调 :3088)。
它做四件事:走树(需要时分裂)、建新节点、写 host 值、挂 child edge。关键在第三步的切片。Rust 侧是一次 narrow(unified_tree_core.rs:3191-3197):
host_value.narrow(0, matched_length as i64, (total_len - matched_length) as i64)
Python 侧没有 narrow,它在走树循环里增量地切(unified_tree_core.py:2169-2170)。两种写法的意思一样:新节点只代表尚未匹配的那段后缀,所以 host 值必须从 matched_length 开始切。旁边按 hash 切时用的是 matched_length / page_size。host 值按 token 切、hash 按页切,两个维度不能混。
有三道守卫会让这次 graft 直接不发生:
留一个 token。 匹配长度最多到输入长度减一(schedule_batch.py:1677-1682):
def _compute_max_prefix_len(self, input_len: int) -> int:
# NOTE: the matched length is at most 1 less than the input length to enable logprob computation
max_prefix_len = input_len - 1
注释给的理由是 logprob 计算。量级是一个 token,与 page size 无关;代价是那段前缀永远不会被完全复用,每次都要重算一次。
写通的父节点必须也是备份过的。 这是 insert_host 里唯一一条「不物化」的出口(unified_tree_core.rs:3163-3171):
// write-through 下,设备侧没备份的父节点不该有只存在于 host 的后缀
if !self.is_write_back && !parent.is_root() && !parent.backuped() {
result.host_insert_dropped = true;
return Ok(result);
}
触发后调用方把整段 host 槽位连同各 component 的辅助传输一起交还(unified_radix_cache.py:2189-2195),并按 reason="dropped" 上报。
别的 load-back 已经占了这些槽位。 构建传输前会预检 load_back_pending_id,只要链路上有任何一个节点被另一条 load-back 的锚节点钉住,就返回空 spec 让调用方退避重算(unified_tree_core.rs:3390-3414)。提交时还有一道硬断言,同一个节点不能被两个锚节点同时认领(:3738-3754)。
这里说的「锚节点」是发起这次 load-back 的那个树节点,和 §1.2 里那个不装数据的「锚点池」不是一回事。
3.4 命中判定:两道阈值与前导连续段
阈值管的是另一个问题:多短的前缀不值得搬。两道阈值都在查询之前拦一道,省掉一次索引往返。
小于阈值的命中不做传输。load_back_threshold 那条守卫的注释还额外解释了 max(1, ...) 的存在理由:即使阈值被配成 0 或负数,一个完全空的 spec 也不能报成功。
命中判定本身则是「取前导连续段」(hicache_storage.py:167-171):
def count_pool_hits(results: dict[str, List[bool]]) -> dict[str, int]:
return {
name: (rs.index(False) if False in rs else len(rs))
for name, rs in results.items()
}
rs.index(False) 就是第一个失败位置,也就是前导连续成功的个数。每个池各算各的,最后在调用方取最小值(cache_controller.py:1148-1151)。跨池的收敛取最小值,以最先断掉的那个池为准。
四、V4.1 的两处例外
前面三节讲的机制对两代通用:同一套 sidecar 挂接、同一套写读路径,只是两代的池清单不同。V4.1 有两处落在这套机制之外。
4.1 request-scoped pair ring 与草稿 KV 不能备份
mem_cache/common.py:197-207 有一个专门为 V4.1 写的判据。这里说的 pair ring,是 ratio 2 那类层的待处理对环(deepseek_v4_memory_pool.py:1312 把它和一个 c128 页并列为同一类条目):
def dsv41_dspark_needs_rebootstrap(
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
) -> bool:
"""V4.1's request-scoped pair ring and draft KV cannot use CPU tensor backup."""
V4.1 的 request-scoped pair ring 与草稿 KV 不能用 CPU 张量备份。 这句话就写在判据的文档字符串里,触发条件是模型跑了 DSpark 且池里含 ratio 2 的层。
命中之后走的是另一条路。disaggregation/decode.py:692-695 在请求被 retract 时不再做 host 备份,而是强制 PD rebootstrap:
if is_retracted and dsv41_dspark_needs_rebootstrap(
self.token_to_kv_pool_allocator
):
if req.output_ids:
req.pd_rebootstrap_forced_output_id = req.output_ids.pop()
开头说「KV 与 state 并不一样」,这里是差别最极端的一处:这类状态,L2 备份从设计上就走不通,只能重新算。前面讲的 sidecar 挂接、原子分配、graft 守卫,对它一概不适用。
4.2 unified KV 布局被明确拒绝
§1.3 提到两个 ROPE 池只在 unified KV 布局下注册。而 V4.1 恰恰拒绝这个布局。arg_groups/deepseek_v4_hook.py:315 把 ("the unified KV layout", is_unified_kv_triton()) 放进不支持列表,启动就会报 DeepSeek-V4.1 does not support the unified KV layout yet。
所以那两个 ROPE 池是 V4 在 ROCm 上的东西,与 V4.1 无关。
V4.1 没有 fp4 主 KV。deepseek_v4_memory_pool.py:99 有一条硬断言:
assert kv_layout is KVLayout.V41, f"{kv_layout} is not a main-cache layout"
V41_FP4 这个布局从设计上就不能当 main cache,它只用于压缩 cache 与 indexer。
五、这套设计的边界与代价
跨实例只能走 L3。 上游文档把这条边界写得很清楚:
docs/docs/advanced_features/hicache_design.mdx:61**L2 is node-local and instance-private.** It is host memory owned by one
inference instance's process, so two instances never read each other's L2,
not even two instances on the same node. A KV cache produced by instance 0
becomes visible to instance 1 only after it reaches L3.
:63 回答的是另一个问题:能不能把几台机器的 host 内存拼成一个更大的 L2。
This is the answer to a common question: HiCache cannot pool the host memory of
several machines into one larger L2. Growing `--hicache-ratio` or
`--hicache-size` only makes each instance's own private L2 larger.
Cross-instance reuse is the job of L3, so it needs `--hicache-storage-backend`.
对应到实现:L2 路径上没有任何 hash 查索引。 匹配、插入、驱逐全走 token id 的基数树;pool_host/ 目录下 grep hash 零命中。内容寻址只出现在 L3:按页算 SHA256 链(utils.py:166 的 compute_node_hash_values),再用它去 batch_exists_v2 查(hybrid_cache_controller.py:599)。
按池的 token 指标看不到 sidecar。 指标本身是按池分的(metrics_collector.py:2301-2310 的 sglang:hicache_backup_tokens_total 带 pool 标签),但打点函数把复用索引的池排除了:
hybrid_cache_controller.py:484-486"""Per-pool token counts for a merged transfer op (anchor + extra
pools), shared by D->H write and H->D load acks; sidecar transfers
reusing another pool's indices are excluded."""
V4 注册的池里除了锚点和 swa 其余五个都是 sidecar,V4.1 是六个,所以这套 token 计数只会记到 kv 与 swa 头上。它们的字节量能在 sglang:hicache_backup_bytes_total 里看到,文档字符串明确说了那个计数器「all pools combined」。要看 sidecar 的搬运量,得用字节计数器,不能用 token 计数器。
一份写好了但没接线的去重设施。mem_cache/mla_host_dedup.py 用一条专用 NCCL 组做逐层广播:源 rank 把 staging 里的行 index_select 出去,广播之后其余 rank index_copy_ 拷回(:178-204)。但它的入口 maybe_create_mla_host_dedup_context(:248)在整个 python/ 下没有任何生产调用点——引用它的只有这个文件自己和单元测试。引入它的提交标题就写着「primitives」。这是一份预备设施,不是现行机制。
阈值的代价。 256 与 10 两个默认值都是拍出来的(prefetch_threshold 附近还留着 # todo, threshold policy for prefetching)。它们决定了多短的前缀不值得搬——设高了小前缀永远命中不了缓存,设低了 I/O 被碎片请求吃掉。上游没给出调参依据,只有这两个数字和一句 todo。
源文件索引
相关阅读
-
• 把 KV Cache 压缩推到极限:DeepSeek-V4.1-Flash 技术报告精读——本文的上篇,算的是每 token 的体积账 -
• HiCache 深入详解——分层缓存的整体架构、HiRadixTree 元数据拓扑与三种预取/写回策略 -
• KV Cache L1↔L2 数据流深度分析——write_backup / eviction / load_back 的逐操作代码路径,本文 §3.2 的时序细节在那里有更完整的展开 -
• SGLang UnifiedRadixTree:一棵树,四种注意力——组件注册表与 FULL/SWA/MAMBA 各组件的语义,本文 §2.2 的 validator 机制在那里有完整讲解 -
• SGLang KV Pool 管理:物理存储、Radix Tree 索引与请求视图——page size 如何贯穿整个栈 -
• 不同注意力类型的 KV Cache 到底长什么样——c4a/c128a 的压缩维度与逐 token 字节数 -
• vLLM 中的 DeepSeek V4:高效长上下文注意力——同一模型在 vLLM 侧的混合 KV 缓存实现
参考资料
-
• SGLang 仓库,commit c475ac5eaf(2026-09-19),本文所有源码引用的基准版本 -
• HiCache 设计文档——L1/L2/L3 的职责划分与可见性边界 -
• HiCache 最佳实践—— --hicache-ratio/--hicache-storage-backend等参数的实际用法 -
• DeepSeek-V4.1 cookbook——V4.1-Flash 的层结构、Engram 与 SWA bounded replay 的开关 -
• DeepSeek-V4.1-Flash 技术报告——global KV 每 token 体积曲线(Figure 1(b))与持久化 KV 的分层设计

