执行摘要
- 一句话:unified radix tree 上为 SWA 混合模型解锁 decode 侧 KV 前缀复用
- 推荐动作:值得精读。它是 SWA 混合模型接入 decode-side radix cache 的里程碑 PR,核心看点是 SWA 双生命周期 KV 与页对齐 radix key 的交互、margin 设计如何直接消除
return_full_match 的过度设计,以及 full/SWA 锁引用计数的精细管理。想理解后续 DSV4/MiMo 扩展(#27831)的读者,应先读透本 PR 的窗口裕量与准入逻辑。
功能与动机
P/D 分离下 decode worker 可以为未完成请求缓存设备端前缀,让后续请求只从 prefill 传输增量,但混合 SWA 模型此前在启动时被直接拒绝。PR body 明确指出:混合 SWA 模型有 full-attention 与 sliding-window 两套生命周期不同的 KV——full KV 可跨请求复用,SWA KV 只对当前窗口有效,因此 decode worker 应复用 full-attention 前缀、只请求新鲜 SWA 窗口。该功能门控到实验性 unified radix tree,并声明 supersede 走默认 SWARadixCache 的 #26218。
实现拆解
实现按以下步骤展开:
-
启动期组合校验(kv_cache_builder.py 的 build_kv_cache):原逻辑对 SWA 模型一律抛错;现在仅当 SGLANG_ENABLE_UNIFIED_RADIX_TREE 或 use_mlx() 时放行 --disaggregation-decode-enable-radix-cache + SWA 组合;仍拒绝 hierarchical cache、DeepSeek-V4 DSA 压缩 KV、SWA-compress(Gemma4 / MiMo-V2)以及 Mamba/SSM。
-
decode 侧窗口裕量与容量统计(decode.py):_swa_tail_len 在启用 radix cache 时把窗口起点改为 seq_len - 1 - max(window_size, page_size) 后再页对齐,保证按页截断的 radix key 内仍包含完整 SWA 窗口;新增 _radix_full_evictable、_radix_full_protected、_radix_full_available 访问器,让混合 SWA 模型能分别统计 full-attention 与 SWA 池容量。
-
SWA 容量回收(decode.py 的 _reclaim_swa_tail_capacity):按页对齐计算 SWA 尾部需求,不足时先从树驱逐 SWA 页,仍不足则返回错误串;pop_preallocated 接到错误后仅 abort 该请求(FINISH_ABORT 清理),而不是让调度线程崩溃。
-
锁生命周期与去重转发(decode.py、unified_radix_cache.py):_release_matched_prefix_lock 通过 DecLockRefParams(swa_uuid_for_lock) 和 skip_swa=True 区分 full/SWA 锁释放;cache_unfinished_req 在插入去重后重新匹配、把 live 请求的 token table 重写到 cache-owned slots,并转移锁到最深 full-resident 节点。得益于 SWA 插入侧保留的窗口 margin,normal 匹配即可安全转发,最终无需引入 return_full_match 特例。
-
测试与 CI 配套:单元测试覆盖窗口裕量、admission 预算、reclaim 成功/失败与单请求拒绝;新增 gpt-oss-20b NIXL 端到端测试(GSM8K 0.50 阈值 + 两遍非回归校验);H20 8-GPU 测试移入 extra-b stage 缓解 runner 排队,校准混合缓存 CI 阈值,并修复 AMD 依赖脚本的引号参数传递。
关键文件:
python/sglang/srt/disaggregation/decode.py(模块 解码队列;类别 source;类型 core-logic;符号 _release_matched_prefix_lock, _reclaim_swa_tail_capacity, _radix_full_evictable, _radix_full_protected): 核心实现文件:SWA 窗口裕量计算、SWA 容量回收、full/SWA 锁释放与容量统计访问器全部在此,是 P/D 分离解码准入路径的关键改造。
python/sglang/srt/mem_cache/kv_cache_builder.py(模块 缓存构建;类别 source;类型 dependency-wiring;符号 build_kv_cache): 启动期校验的开关所在:决定 SWA 模型何时允许启用 decode 侧 radix cache,并显式拒绝未支持组合。
python/sglang/srt/mem_cache/unified_radix_cache.py(模块 前缀缓存;类别 source;类型 core-logic;符号 cache_unfinished_req): cache_unfinished_req 是去重转发与锁转移的核心,本 PR 确认其匹配注释并依赖插入侧 margin 保证 normal match 安全。
test/registered/unit/mem_cache/test_decode_radix_lock_ref.py(模块 锁引用;类别 test;类型 test-coverage;符号 test_swa_tail_len_keeps_page_aligned_matchable_window, test_swa_admission_counts_evictable_capacity, test_reclaim_swa_tail_capacity_page_rounds, test_reclaim_swa_tail_capacity_fails_before_allocation): 以 mock 方式覆盖锁引用计数平衡的关键 4 个场景,是验证 SWA 窗口裕量与 reclaim 逻辑正确性的主要单元测试。
test/registered/disaggregation/test_disaggregation_decode_radix_cache_swa.py(模块 SWA 测试;类别 test;类型 test-coverage;符号 TestDisaggregationDecodeRadixCacheSWANixl): 新增端到端 NIXL 测试,在 8×H200 上以 gpt-oss-20b 验证多轮缓存命中与 GSM8K 两遍非回归,是功能合入的主要验收证据。
test/registered/unit/disaggregation/test_decode_queue_cleanup.py(模块 队列清理;类别 test;类型 test-coverage;符号 test_swa_reclaim_failure_rejects_only_request): 验证 SWA 驱逐失败时只拒绝该请求、正确清理 kv_receiver 并 abort,覆盖 review 中要求的崩溃改拒绝行为。
test/registered/disaggregation/test_disaggregation_decode_radix_cache.py(模块 解耦测试;类别 test;类型 test-coverage): 基础 decode radix cache 测试的阈值与预算校准,避免 SWA 改动影响既有非 SWA 用例。
scripts/ci/amd/amd_ci_install_dependency.sh(模块 CI 脚本;类别 infra;类型 infrastructure): 部署脚本修复,保留带引号的 retry 参数,属于 CI 基础设施配套改动。
test/registered/radix_cache/test_mamba2_extra_buffer_kl.py(模块 Mamba 测试;类别 test;类型 test-coverage): Mamba 缓存测试的配置校准,确保新增校验逻辑不误伤 SSM 路径的既有覆盖。
test/registered/unit/mem_cache/test_hisparse_allocator.py(模块 稀疏分配器;类别 test;类型 test-coverage): 稀疏分配器测试的极小校准,与 unified cache 体系相关。
关键符号:_release_matched_prefix_lock, _reclaim_swa_tail_capacity, _radix_full_evictable, _radix_full_protected, _radix_full_available, _swa_tail_len, cache_unfinished_req, build_kv_cache
关键源码片段
python/sglang/srt/disaggregation/decode.py
核心实现文件:SWA 窗口裕量计算、SWA 容量回收、full/SWA 锁释放与容量统计访问器全部在此,是 P/D 分离解码准入路径的关键改造。
# python/sglang/srt/disaggregation/decode.py
# SWA 混合模型在 decode 侧复用 full-attention 前缀时需要的辅助逻辑。
# 核心问题:full KV 生命周期长、可跨请求复用,SWA KV 只对当前窗口有效,
# 因此 radix 匹配、容量计算与锁释放都必须能区分这两部分。
def _release_matched_prefix_lock(self, req: Req) -> None:
# decode 侧只复用 full-attention 前缀,SWA 尾部每次新分配,
# 所以释放已匹配节点锁时通过 swa_uuid_for_lock 区分两部分;
# 若 SWA 部分已提前释放(swa_prefix_lock_released),
# 则调用 skip_swa=True 只释放 full 部分的锁,避免锁计数失衡。
params = DecLockRefParams(swa_uuid_for_lock=req.swa_uuid_for_lock)
if req.swa_prefix_lock_released:
self.tree_cache.dec_lock_ref(req.last_node, params, skip_swa=True)
req.swa_prefix_lock_released = False
else:
self.tree_cache.dec_lock_ref(req.last_node, params)
def _reclaim_swa_tail_capacity(
self, swa_tail_len: int, req_id: str
) -> Optional[str]:
# 按页对齐计算 SWA 尾部需求,不足时先尝试从 radix 树驱逐 SWA 页;
# 仍不足则返回错误串,由调用方拒绝该请求(而不是在调度线程崩溃)。
page_size = self.token_to_kv_pool_allocator.page_size
required = ceil_align(swa_tail_len, page_size)
available = self.token_to_kv_pool_allocator.swa_available_size()
if available < required:
self.tree_cache.evict(EvictParams(swa_num_tokens=required - available))
available = self.token_to_kv_pool_allocator.swa_available_size()
if available < required:
return (
f"SWA eviction insufficient: needed={required}, "
f"available={available}, req={req_id}"
)
return None
def _radix_full_evictable(self) -> int:
# 混合 SWA 模型通过 full_* 访问器单独统计 full-attention 池容量,
# 普通模型退化为原有的 evictable / protected / available 统计。
if self.scheduler.tp_worker.is_hybrid_swa:
return self.tree_cache.full_evictable_size()
return self.tree_cache.evictable_size()
def _swa_tail_len(self, seq_len: int) -> int:
if not self._uses_swa_tail_prealloc() or seq_len <= 0:
return max(seq_len, 0)
window_size = self.scheduler.sliding_window_size
if window_size is None or window_size <= 0:
return seq_len
page_size = self.token_to_kv_pool_allocator.page_size
if getattr(
self.scheduler.server_args,
"disaggregation_decode_enable_radix_cache",
False,
):
# 保留窗口裕量:seq_len - 1 是最后一个已提交位置,
# 窗口起点再前移 max(window_size, page_size) 并做页对齐,
# 保证按页截断的 radix key 内仍包含完整 SWA 窗口,#29860 同款处理。
# 否则会出现 insert 已复用 full KV、而 normal match 返回 0 的悬空问题。
window_start = max(0, seq_len - 1 - max(window_size, page_size))
else:
window_start = max(0, seq_len - window_size)
window_start = (window_start // page_size) * page_size
return seq_len - window_start
python/sglang/srt/mem_cache/kv_cache_builder.py
启动期校验的开关所在:决定 SWA 模型何时允许启用 decode 侧 radix cache,并显式拒绝未支持组合。
# python/sglang/srt/mem_cache/kv_cache_builder.py
# build_kv_cache 中的启动期组合校验:
# decode-side radix cache 对 SWA 混合模型只允许走 unified radix tree,
# 并显式拒绝仍未适配的 hierarchical cache、DSA 与 SWA-compress 变体。
if (
get_disagg().disaggregation_decode_enable_radix_cache
and get_disagg().disaggregation_mode == "decode"
):
if is_hybrid_swa:
if not (envs.SGLANG_ENABLE_UNIFIED_RADIX_TREE.get() or use_mlx()):
raise ValueError(
"--disaggregation-decode-enable-radix-cache with sliding "
"window attention (SWA) models requires the unified radix "
"tree (set SGLANG_ENABLE_UNIFIED_RADIX_TREE=1)."
)
if enable_hierarchical_cache:
raise ValueError(
"SWA decode radix currently supports only device-resident "
"cache and is incompatible with --enable-hierarchical-cache."
)
if getattr(model_config, "is_deepseek_v4_arch", False):
raise ValueError(
"decode radix cache does not support DeepSeek-V4 (DSA) "
"compressed KV (c4 / c128 / indexer) yet."
)
if getattr(model_config, "is_hybrid_swa_compress", False):
raise ValueError(
"decode radix cache does not support SWA-compress models "
"(e.g. Gemma4 / MiMo-V2) yet."
)
if is_hybrid_ssm:
raise ValueError(
"--disaggregation-decode-enable-radix-cache is incompatible "
"with Mamba/SSM models."
)
评论区精华
Review 中最重要的交锋是 return_full_match 是否必要:
- hzh0425 质疑:"I think we don't actually need to add this new one. The decode-side insertions of 'full' and 'swa' should come in pairs."
- ishandhanani 先以 895 token、page_size=64、window=127 的实例反驳:insert 已去重 832 个 full tokens,而 normal match 因 SWA tombstone 返回 0,会触发
new_prefix_len <= len(new_indices) 检查失败。
- ispobock 指出 #29860 已在插入侧保留窗口 margin,作者随即承认:"With the same margin, the 895/64/127 case starts SWA at 704 instead of 768 ... I will update the decode tail calculation to use that margin and remove return_full_match." 这一收敛避免了过度设计。
其他关键讨论:
风险与影响
关联脉络
- PR #19746 P/D disaggregation decode-side radix cache: PR body 明确本 PR 是 #19746 引入的 decode-side radix cache 在 SWA 混合模型上的扩展(标题为基于上下文的推断)。
- PR #26218 SWA decode radix cache via default SWARadixCache: PR body 声明 supersede #26218,同一功能统一改走 unified radix tree(标题为基于上下文的推断)。
- PR #26828 Linear-attention/Mamba decode radix cache + MTP: issue 评论中 laixinn 提到正在做线性注意力(mamba cache)与 MTP 支持,作者建议作为本 PR 之后的分工项(标题为基于上下文的推断)。
- PR #27831 DeepSeek-V4 decode radix cache + MTP support: 评论中 zhangxiaolei123456 与 TobyMint 提到基于本 PR 方法扩展到 DSV4、MiMo-V2、Gemma4,是直接的下游演进。
- PR #29860 Keep SWA window margin at insert boundary: review 中 ispobock 指出该 PR 已在插入侧保留窗口 margin,直接决定了本 PR 移除 return_full_match 的最终方案(标题为基于上下文的推断)。
- PR #28085 Related decode radix cache fix: ShangmingCai 在 review 中提示可能与 decode_hicache_mixin 相关的 PR,需要 double-check(标题未在材料中给出,为占位推断)。
参与讨论