Prhub

#27402 [sgl] proactively release out-of-window SWA slots after chunked prefill

原始 PR 作者 bixue2010 合并时间 2026-06-12 14:17 文件变更 7 提交数 13 评论 17 代码增减 +174 / -33

执行摘要

主动释放 SWA 窗口外 KV 槽位,优化长上下文内存使用

在长上下文运行中,未完成的prefill请求中的SWA KV槽位即使已滑出窗口仍被持续占用,导致SWA内存使用不受控。PR body明确提出“bounding SWA pool usage for long-context runs”。

值得精读,尤其是组件级hook设计和页面边界保护逻辑(page_size减一策略),以及如何通过环境变量平滑引入行为变更。

讨论亮点

讨论1:环境变量 vs CLI参数
ispobock建议保持为环境变量,因为该功能目前只对unified tree生效,bixue2010同意并修改。

讨论2:hook是否应对所有组件调用
ispobock询问是否只需要SWA组件,bixue2010认为应作为通用hook,其他组件(如Mamba)未来可按需实现。结论:保留对所有组件的循环调用,目前SWA实现,其余no-op。

实现拆解

  1. 添加基类hook:在TreeComponenttree_component.py)中新增free_out_of_window_slots默认方法(pass),供子类按需重写。
  2. SWA组件实现SWAComponentswa_component.py)实现该方法,调用通用函数free_swa_out_of_window_slots,计算应释放的槽位范围(pre_len - sliding_window_size - page_size)并调用token_to_kv_pool_allocator.free_swa
  3. 驱动点注入:在UnifiedRadixCache.cache_unfinished_requnified_radix_cache.py)中,当环境变量启用时,遍历所有组件并调用free_out_of_window_slots,结果记录到insert_params.swa_evicted_seqlen
  4. 调度层重构:将ScheduleBatch._evict_swaschedule_batch.py)中的内联释放逻辑替换为调用free_swa_out_of_window_slots,消除代码重复。
  5. 测试覆盖:在test_unified_radix_cache_unittest.py中添加test_swa_eager_eviction_on_unfinished_reqtest_swa_eager_eviction_noop_when_within_window,验证释放行为和窗口内无操作。
  6. 配置:在environ.py中新增环境变量SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS,默认关闭。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/common.py 缓存层 modified 7.09
python/sglang/srt/mem_cache/unified_cache_components/swa_component.py 缓存层 modified 6.73
python/sglang/srt/mem_cache/unified_radix_cache.py 缓存层 modified 5.4
python/sglang/srt/managers/schedule_batch.py 调度器 modified 6.38
python/sglang/srt/mem_cache/unified_cache_components/tree_component.py 缓存层 modified 4.96
test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py 测试 modified 6.44

关键符号

free_swa_out_of_window_slots SWAComponent.free_out_of_window_slots UnifiedRadixCache.cache_unfinished_req ScheduleBatch._evict_swa

关键源码片段

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

新增核心函数 `free_swa_out_of_window_slots`,包含页面边界保护和槽位释放逻辑,是整个功能的计算核心。

# python/sglang/srt/mem_cache/common.pydef free_swa_out_of_window_slots(
    req: Req,
    pre_len: int,
    *,
    sliding_window_size: int,
    page_size: int,
    req_to_token_pool: ReqToTokenPool,
    token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
) -> None:
    from sglang.srt.environ import envs
​
    # cache_protected_len 必须是 page_size 对齐的
    assert req.cache_protected_len % page_size == 0, "cache_protected_len must be page aligned"
    req.swa_evicted_seqlen = max(req.swa_evicted_seqlen, req.cache_protected_len)
​
    # 减去一页 margin,避免释放达到 radix tree 插入边界,防止 tombstones 导致内存泄漏
    if envs.SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN.get():
        evict_threshold = pre_len - sliding_window_size
    else:
        evict_threshold = pre_len - sliding_window_size - page_size
​
    new_swa_evicted_seqlen = max(req.swa_evicted_seqlen, evict_threshold)
​
    # 按页对齐 evict_seqlen
    if page_size > 1:
        new_swa_evicted_seqlen = (new_swa_evicted_seqlen // page_size) * page_size
​
    if new_swa_evicted_seqlen > req.swa_evicted_seqlen:
        free_slots = req_to_token_pool.req_to_token[
            req.req_pool_idx, req.swa_evicted_seqlen : new_swa_evicted_seqlen
        ]
        token_to_kv_pool_allocator.free_swa(free_slots)
        req.swa_evicted_seqlen = new_swa_evicted_seqlen
python/sglang/srt/mem_cache/unified_cache_components/swa_component.py core-logic

实现 `free_out_of_window_slots` hook ,将 SWA 组件特定的参数传递给通用函数,并更新 insert_params。

# python/sglang/srt/mem_cache/unified_cache_components/swa_component.pyfrom sglang.srt.mem_cache.common import free_swa_out_of_window_slotsclass SWAComponent(TreeComponent):
    # ... 其他代码 ...
​
    def free_out_of_window_slots(
        self, req: Req, pre_len: int, insert_params: InsertParams
    ) -> None:
        if self.sliding_window_size is not None:
            free_swa_out_of_window_slots(
                req,
                pre_len,
                sliding_window_size=self.sliding_window_size,
                page_size=self.cache.page_size,
                req_to_token_pool=self.cache.req_to_token_pool,
                token_to_kv_pool_allocator=self.cache.token_to_kv_pool_allocator,
            )
        # 记录 evicted_seqlen 到 insert_params,后续插入时使用
        insert_params.swa_evicted_seqlen = req.swa_evicted_seqlen

评论区精华

是否应使用 CLI 参数而非环境变量 设计

ispobock 建议保持环境变量,因为该功能目前只对 unified tree 生效。bixue2010 同意并修改。

结论:改为环境变量 `SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS`。 · 已解决

hook 是否应对所有组件调用 设计

ispobock 询问是否只需要 SWA 组件调用该 hook,bixue2010 解释应该作为通用 hook,其他组件将来可按需实现。

结论:保留对所有组件循环调用,目前 SWA 实现释放逻辑,其余组件 no-op。 · 已解决

风险与影响

涉及缓存管理逻辑变更,主要风险在于页面边界和对齐条件的正确处理。free_swa_out_of_window_slots中关于cache_protected_len必须page-aligned的断言,以及page_size>1时的对齐逻辑,若假设不成立可能导致错误释放或内存泄漏。但通过单元测试和环境变量门控(默认关闭),对现有用户无影响。

对使用SWA的模型(如DeepSeek等)有正面影响,减少无效KV占用,提升长上下文时的批处理能力。非SWA模型无影响。启用环境变量后,每个chunked prefill完成后立即释放窗口外槽位,可能略微增加计算开销,但内存收益显著。

核心缓存路径变更 受环境变量控制 需测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论