Prhub

#27291 [HiCache] Fix SWA L3 cache miss due to a prefetch/hit len mismatch

原始 PR 作者 vladnosiv 合并时间 2026-06-18 14:55 文件变更 4 提交数 11 评论 28 代码增减 +53 / -4

执行摘要

修复 SWA 模型 L3 缓存预取与匹配长度不一致问题

在 HiCache L3 缓存与 SWA 模型结合的场景下,预取与匹配阶段 token 长度不一致,导致即使相同 prompt 重复请求也无法命中缓存。PR body 详细描述了该现象,并指出当前测试无法通过。

建议阅读本 PR 以理解 HiCache 预取与 SWA correction 的交互,以及 _compute_max_prefix_len 在缓存一致性中的关键作用。相关测试配置方式也可作为后续架构适配的参考。

讨论亮点

核心讨论集中在预取长度是否需要与 _compute_max_prefix_len 对齐。审核者 hzh0425 提出这一问题,作者 vladnosiv 确认并修复。此外还讨论了不同模型架构(SWA vs Mamba)的缓存状态策略差异:Mamba 状态仅在 chunk 边界持久化,因此缓存命中上限不同,测试中通过 l3_prefetch_max_uncached_tokens 加以区分。

实现拆解

  1. 问题定位:在 sglang/srt/managers/scheduler.py_prefetch_kvcache 方法中,new_input_tokens = req.full_untruncated_fill_ids[matched_len:] 使用全长 token,未考虑 SWA 的 correction。
  2. 核心修复:引入 match_end = req._compute_max_prefix_len(len(req.full_untruncated_fill_ids)),并将切片终点从末尾改为 match_end,确保预取长度与后续 match 逻辑一致。
  3. 测试框架增强:在 AccuracyTwoPassMixin(位于 test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_nightly.py)中添加 test_l3_prefetch_full_prefix_hit_after_flush 方法,使用随机 token 序列,经两次 generate 和中间 flush_cache,断言 cached_tokens >= input_len - max_uncached
  4. 架构差异适配:引入类属性 l3_prefetch_page_sizel3_prefetch_prompt_pagesl3_prefetch_max_uncached_tokens,允许不同模型架构自定义预期缓存边界(例如 Mamba 的状态只存储在 chunk 边界,因此 max_uncached 需设为 chunked_prefill_size)。
  5. 测试用例配置:更新 test_unified_radix_cache_kl_mamba.pytest_unified_radix_cache_kl_dsv4.py,为相关测试类设置适当的属性值以适配新测试。
文件 模块 状态 重要度
python/sglang/srt/managers/scheduler.py 调度器 modified 5.74
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_nightly.py 缓存测试 modified 5.46
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_mamba.py Mamba 测试 modified 4.38
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_dsv4.py DSv4 测试 modified 4.06

关键符号

_prefetch_kvcache test_l3_prefetch_full_prefix_hit_after_flush _compute_max_prefix_len

关键源码片段

python/sglang/srt/managers/scheduler.py core-logic

核心修复文件,修正预取 token 长度与 match 时长度不一致的问题,是解决 cache miss 的关键变更。

# sglang/srt/managers/scheduler.py (modified _prefetch_kvcache)
def _prefetch_kvcache(self, req: Req):
    if self.enable_hicache_storage:
        req.init_next_round_input(self.tree_cache, cow_mamba=False)
        last_host_node = req.last_host_node
        if last_host_node.backuped or last_host_node is self.tree_cache.root_node:
            last_hash = last_host_node.get_last_hash_value()
            matched_len = len(req.prefix_indices) + req.host_hit_length
            # 使用 _compute_max_prefix_len 计算匹配终点,与后续 match 逻辑对齐
            match_end = req._compute_max_prefix_len(
                len(req.full_untruncated_fill_ids)
            )
            new_input_tokens = req.full_untruncated_fill_ids[matched_len:match_end]
​
            prefix_keys = (
                last_host_node.get_prefix_hash_values(last_host_node.parent)
                if self.tree_cache.hicache_storage_pass_prefix_keys
                else None
            )
            self.tree_cache.prefetch_from_storage(
                req.rid,
                last_host_node,
                new_input_tokens,
                last_hash,
                prefix_keys,
            )
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_nightly.py test-coverage

新增了验证 L3 缓存完全前缀命中率的核心测试方法,并引入架构适配属性。

# test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_nightly.py
class AccuracyTwoPassMixin:
    # ... existing code ...
    l3_prefetch_page_size: int = 64 # 默认 page_size
    l3_prefetch_prompt_pages: int = 16 # 构造 prompt 所用的 page 数
    # 架构可覆盖该值以指示最多可能未命中的 token 数
    l3_prefetch_max_uncached_tokens: int = None
​
    def test_l3_prefetch_full_prefix_hit_after_flush(self):
        from sglang.test.kl_test_utils import _flush_cache, _generate
​
        page = int(self.l3_prefetch_page_size)
        n_tokens = page * int(self.l3_prefetch_prompt_pages)
        max_uncached = int(
            self.l3_prefetch_max_uncached_tokens
            if self.l3_prefetch_max_uncached_tokens is not None
            else page
        )
​
        rng = random.Random(987)
        input_ids = [rng.randint(1, 30000) for _ in range(n_tokens)]
​
        # 第一次 generate,填充缓存
        _generate(self.base_url, [input_ids], max_new_tokens=4)
        # flush 缓存
        _flush_cache(self.base_url)
        # 第二次 generate,应命中大多数 token
        results = _generate(self.base_url, [input_ids], max_new_tokens=4)
        cached = int(results[0]["meta_info"]["cached_tokens"])
​
        expected_min = n_tokens - max_uncached
        self.assertGreaterEqual(
            cached,
            expected_min,
            f"cached_tokens={cached} < {expected_min} (= input_len - {max_uncached})",
        )

评论区精华

预取长度应与 _compute_max_prefix_len 对齐 正确性

hzh0425 指出预取长度应与后续 match 使用的 _compute_max_prefix_len 对齐,否则会导致 cache miss。

结论:作者 vladnosiv 同意并修复,将 new_input_tokens 切片终点改为 match_end。 · 已解决

Mamba 模型缓存测试为何不设置更大 prompt question

hzh0425 询问为何不在 Mamba 场景设置更大的 n_tokens 以实际验证缓存命中。

结论:vladnosiv 解释 prompt 长度已设为 chunked_prefill_size + 16,足以验证那部分 token 的命中,并通过 l3_prefetch_max_uncached_tokens 反映 Mamba 的状态缓存间隔。 · 已解决

Mamba 与 SWA 的缓存状态策略差异 设计

vladnosiv 发现 Mamba 的状态缓存不每次位置都 offload,而 SWA 每 page 都 offload,导致缓存命中期望不同。提议统一默认行为并增加配置选项。

结论:同意当前测试通过 l3_prefetch_max_uncached_tokens 区分,未来可考虑更通用的方案。 · 已解决

风险与影响

主要风险在于 _compute_max_prefix_len 的实现:如果该函数对非 SWA 模型返回了非预期值,可能导致预取范围变小甚至无法预取必要 token。但由于预取是尽力而为的优化,即使预取不足也不会影响正确性,仅影响缓存命中率。另外新增测试仅覆盖 L3 文件后端场景,L2 场景未覆盖。此外 Mamba 模型的缓存行为依赖 chunk 边界,测试断言较宽松,可能掩盖其他 bug。

直接影响使用 HiCache L3 缓存的 SWA 模型(如 DeepSeek V4 Flash、GLM-5)在 flush 缓存后重新请求相同 prompt 时的缓存命中率,从几乎 0 提升至接近 prompt 长度减去 page_size/窗口大小。对其他模型和缓存层级无影响。团队需注意不同模型架构的 l3_prefetch_max_uncached_tokens 配置。测试扩展使缓存验证更严谨。

核心路径变更 架构差异适配 测试覆盖边界

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论