Prhub

#29352 [bug2] skip swa recovery on locked full kv

原始 PR 作者 yaof20 合并时间 2026-07-01 07:07 文件变更 4 提交数 10 评论 23 代码增减 +167 / -34

执行摘要

修复 SWA 恢复时覆盖锁定 Full KV

PR body 说明:"This PR fixes an SWA recovery path that could overwrite Full KV still protected by an active request. SWA may try to recover a tombstoned SWA component from fresh insert data. That recovery must not replace the node's Full KV if the Full component is still locked." 属于一系列 radix cache 正确性修复的一部分(源自 #29349)。

值得精读,尤其是对 radix cache 和 SWA 组件交互边界感兴趣的同学。新增的 _restore_device_value_with_locked_full_recover_tombstone_keeping_locked_full 设计体现了在锁定资源下做部分恢复的通用思路。建议同时阅读关联 PR #29353 理解完整修复上下文。

讨论亮点
  1. 正确性担忧:hanming-lu 反馈 "the server crashes for me",要求先解决 crash 再合并。yaof20 解释修复源自内部 RL 任务验证,并指出需结合事务回滚 PR(#29353)才能完全避免不一致。后续进一步调试和补充修改后 crash 问题解决。
  2. 精度影响:hanming-lu 提出 "minor accuracy implications ... full and swa kv are from different forward",但认为没有更干净的方法且与 mamba radix cache 已有做法一致,最终接受。
  3. 同步修改 swa_radix_cache.py:hanming-lu 要求对传统的 SWA radix cache 应用完全相同逻辑,并附上 diff patch。yaof20 采纳后修改并通过测试。

实现拆解

  1. swa_radix_cache.py:在 _insert_helper 的 Branch 1 和 Branch 2(SWA tombstone 恢复分支)中增加 node.full_lock_ref > 0 判断。当 Full 被锁定时,调用新增方法 _recover_tombstone_keeping_locked_full 保留原 Full KV 值,仅从传入 KV 恢复 SWA 组件并更新映射,同时释放传入的 Full KV 切片。
  2. unified_cache_components/swa_component.py:新增 _restore_device_value_with_locked_full 方法,实现类似逻辑:利用旧 Full 值调用 set_full_to_swa_mapping 建立映射,释放传入 Full KV,再恢复 SWA 设备值。在 update_component_on_insert_overlap 的两个 Branch 中插入对应的锁检查分支。
  3. allocator/swa.py:统一 set_full_to_swa_mapping 中的 dtype 转换:始终将 full_indices 转为 int64,swa_indices 转为 mapping 表 dtype,移除之前针对 NPU 的条件分支。
  4. 测试:新增 test_swa_unfinished_recovery_preserves_locked_full_value,构造节点 SWA 被 evict、Full 被锁定的场景,调用 cache_unfinished_req 触发恢复,断言 Full KV 保持不变且 SWA 正确恢复。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/swa_radix_cache.py SWA 缓存 modified 7.3
python/sglang/srt/mem_cache/unified_cache_components/swa_component.py SWA 组件 modified 7.05
test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py 单元测试 modified 6.11
python/sglang/srt/mem_cache/allocator/swa.py 分配器 modified 5.48

关键符号

_recover_tombstone_keeping_locked_full _restore_device_value_with_locked_full set_full_to_swa_mapping test_swa_unfinished_recovery_preserves_locked_full_value

关键源码片段

python/sglang/srt/mem_cache/swa_radix_cache.py core-logic

核心修复文件,在传统 SWA 缓存插入路径中增加 locked-Full 保护逻辑

# python/sglang/srt/mem_cache/swa_radix_cache.py
# Branch 1: 所有 SWA token 未 evicted,直接恢复
if swa_evicted_seqlen <= total_prefix_length:
    if node.full_lock_ref > 0:
        # Full KV 仍被活跃请求锁定,保留原 Full 值,
        # 仅从传入 value 恢复 SWA 并更新映射,
        # 同时释放传入的 Full 切片避免泄漏。
        self._recover_tombstone_keeping_locked_full(
            node, value[:prefix_len]
        )
    else:
        # 经典路径:释放原 Full token,用新值覆盖节点
        self.token_to_kv_pool_allocator.free(node.value[:prefix_len])
        node.value = value[:prefix_len].clone()
        node.swa_tombstone = False
        self.swa_lru_list.insert_mru(node)
        self.swa_evictable_size_ += len(node.value)
elif swa_evicted_seqlen < total_prefix_length + prefix_len:
    # Branch 2: 部分 SWA token evicted,需 split 后恢复
    start_update_idx = swa_evicted_seqlen - total_prefix_length
    if node.full_lock_ref > 0:
        # 先 split,然后对后缀部分调用 locked-Full 恢复
        self._split_node(node.key, node, start_update_idx)
        self._recover_tombstone_keeping_locked_full(
            node, value[start_update_idx:prefix_len]
        )
        self.token_to_kv_pool_allocator.free(value[:start_update_idx])
    else:
        # 经典路径:释放被覆盖的 Full 切片,split 后设置新值
        self.token_to_kv_pool_allocator.free(
            node.value[start_update_idx:prefix_len]
        )
        self._split_node(node.key, node, start_update_idx)
        node.value = value[start_update_idx:prefix_len].clone()
        self.token_to_kv_pool_allocator.free(value[:start_update_idx])
        node.swa_tombstone = False
        self.swa_lru_list.insert_mru(node)
        self.swa_evictable_size_ += len(node.value)
python/sglang/srt/mem_cache/unified_cache_components/swa_component.py core-logic

统一缓存 SWA 组件的插入重叠处理,增加 locked-Full 恢复方法

# python/sglang/srt/mem_cache/unified_cache_components/swa_component.pydef _restore_device_value_with_locked_full(
    self,
    node: UnifiedTreeNode,
    full_value: torch.Tensor, # 节点已有的锁定 Full KV
    incoming_full_value: torch.Tensor, # 插入请求的新 Full 切片
) -> None:
    """当 Full 被锁定时,保留原有 Full 值,只从 incoming 恢复 SWA。"""
    allocator = self.cache.token_to_kv_pool_allocator
    # 将 incoming Full 转换为 SWA 索引
    swa_value = self._translate_full_to_swa(incoming_full_value)
    # 建立从原 Full 到新 SWA 的映射
    allocator.set_full_to_swa_mapping(full_value, swa_value)
    # 清理 incoming 的 Full 索引映射(避免残留)
    allocator.full_to_swa_index_mapping[incoming_full_value.to(torch.int64)] = 0
    # 释放 incoming 的 Full 分配(不需要保留)
    allocator.full_attn_allocator.free(incoming_full_value)
    # 将 SWA 值写入节点并恢复 LRU 状态
    self._restore_device_value(node, swa_value)# 在 update_component_on_insert_overlap 中使用(简略):
if swa_evicted_seqlen <= total_prefix_len:
    if full_cd.lock_ref > 0:
        self._restore_device_value_with_locked_full(node, full_cd.value, value_slice)
        return 0
    # ... 经典路径

评论区精华

修复导致 crash,需要先验证正确性 正确性

hanming-lu: "May I ask how you tested the fix correctness? the server crashes for me." 要求先解决 crash 再合并。

结论:yaof20 解释修复源自内部 RL 任务,后续补充调试和修改后 crash 问题解决。 · 已解决

精度影响:Full 和 SWA 来自不同 forward 正确性

hanming-lu: "There might be minor accuracy implications for this approach where full and swa kv are from different forward due to indeterministic inference, but I couldn't think of a cleaner approach."

结论:yaof20 认为与 mamba radix cache 现有做法一致,且可以接受。hanming-lu 最终同意并合并。 · 已解决

需要为 swa_radix_cache.py 应用相同逻辑 设计

hanming-lu 提议 "Can we apply the same logic to swa_radix_cache.py? then we can merge." 并附上 patch。

结论:yaof20 采纳补丁并修改、提交,后续测试通过。 · 已解决

风险与影响

  1. 精度风险:Full 和 SWA KV 可能来自不同 forward 导致推理结果微小差异,但该做法与 mamba radix cache 已有策略一致,风险可控。
  2. 依赖事务回滚:单独应用本 PR 可能在某些边界情况(match_prefix 在缺少 SWA 时拒绝匹配)下仍存在不一致,需与 #29353 事务回滚配合使用。
  3. 锁定状态误判full_lock_ref > 0 的逻辑是否正确涵盖所有锁定场景?新增的单元测试只覆盖特定配置(page_size=1, sliding_window_size=4),其他配置可能需额外验证。

直接解决使用 SWA(Sliding Window Attention)并启用 radix cache 时可能发生的 KV 数据损坏问题,提升推理稳定性。影响所有在 sglang SRT 中使用滑动窗口注意力的模型(如 DeepSeek 系),在并发请求共享前缀场景下避免跨请求 KV 泄露。

可能精度影响 需要依赖事务回滚 锁定状态判断关键

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论