Prhub

#32700 [Scheduler] Fix to restrict the SWA chunk-cap escape hatch to true head-of-line livelock

原始 PR 作者 pengwu22 合并时间 2026-08-06 22:09 文件变更 3 提交数 3 评论 4 代码增减 +87 / -0

执行摘要

SWA 逃生舱仅限真活锁,消除 transient 压力下的 retraction 风暴

PR body 指出 #31681 的逃生舱本意是解决队头活锁(请求永远装不进 SWA 池),但实际在普通 transient SWA 压力下也会触发,“the scheduler keeps admitting prefill into the headroom that running decodes need, collapsing the SWA evictable cushion”,在高并发 hybrid-SWA 模型上观测到“roughly half the running requests got retracted and 40%+ of prefill compute was re-prefill rework”。PR 用三场景表格明确了修复边界:B < R 正常准入、R ≤ B < C 应等待、B ≥ C 才允许逃生舱。

值得精读。该 PR 是一个教科书式的“逃生舱过度触发”修复:用最小改动(新增一个判定函数 + 两处门控)恢复设计意图,并以三场景表格清晰论证边界,是调度策略类 bugfix 的典范。关注点:_swa_req_never_fits() 复用 _swa_budget_for_req_swa_chunk_cap 的预算口径,保持了单一数据源;测试用 SimpleNamespace 模拟 tree_cachetoken_to_kv_pool_allocator,验证了纯逻辑可测性。建议后续补充边界(等于 size_swa)与真实 allocator 的集成测试。

讨论亮点

该 PR 没有形成实质 review 评论线程,reviewer ispobock 直接 APPROVED。核心设计论证全部在 PR body 的三场景表格中完成:明确区分了“当前能装下”(正常准入)、“当前装不下但排空后可装下”(必须等待)与“永远装不下”(逃生舱唯一适用)三种情形。Issue 评论区只有 gemini-code-assist[bot] 的停止通知与 pengwu22ispobock 触发的 CI 命令,无技术讨论。

实现拆解

  1. 变更入口python/sglang/srt/managers/schedule_policy.pyPrefillAdder.add_one_req() 的 hybrid-SWA 分支。该分支在 swa_needed >= self.rem_swa_tokens 时此前会直接触发 _swa_chunk_cap 收缩 chunk 准入。
  2. 新增判定函数:新增 _swa_req_never_fits(extend_input_len, max_new_tokens, swa_host_hit_length),内部用 self._swa_budget_for_req(...) >= self.token_to_kv_pool_allocator.size_swa 判断请求预算是否超过整个 SWA 池容量,这是“无论池如何排空都装不下”的充要条件。
  3. 门控接入:在 add_one_req() 的锁前与锁后两处 swa_needed >= self.rem_swa_tokens 分支内,先调用 _swa_req_never_fits();返回 False 时直接 return AddReqResult.NO_TOKEN 等待,只有返回 True 才继续走 _swa_chunk_cap 收缩逻辑。两处保持对称,避免锁竞争导致的状态漂移。
  4. 测试配套test/manual/test_schedule_policy.py 新增 TestSwaChunkCapHatch 三个纯逻辑用例(transient 压力等待、预算超池触发、随 size_swa 翻转),通过 PrefillAdder.__new__ 只构造必要字段,无需 KV 池与 GPU;test/registered/unit/managers/test_prefill_adder.py 给 mock token allocator 补上默认 size_swa = 1_000_000,否则 _swa_req_never_fits 访问 MagicMocksize_swaTypeError
  5. 行为影响:无参数与配置变更,属调度准入行为修复;默认行为从“收缩准入”变为“transient 压力下等待”,从根本上消除 retraction 风暴。
文件 模块 状态 重要度
python/sglang/srt/managers/schedule_policy.py 调度器 modified 6.99
test/manual/test_schedule_policy.py 调度器 modified 6.7
test/registered/unit/managers/test_prefill_adder.py 准入控制 modified 3.42

关键符号

_swa_req_never_fits add_one_req _swa_chunk_cap _swa_adder TestSwaChunkCapHatch

关键源码片段

test/manual/test_schedule_policy.py test-coverage

新增 `TestSwaChunkCapHatch` 测试类与 `_swa_adder` 轻量构造器,直接覆盖本次修复的三个关键行为:transient 压力等待、超池触发逃生舱、随池容量翻转判定。

def _swa_adder(size_swa, sliding_window, page_size=16, rem_chunk_tokens=512):
    """返回一个只携带 `_swa_req_never_fits` 所需字段的 `PrefillAdder`。    用 `__new__` 绕过完整初始化,不依赖真实 KV 池与 GPU,
    让逃生舱门控作为纯逻辑被单独测试。
    """
    adder = PrefillAdder.__new__(PrefillAdder)
    adder.page_size = page_size
    adder.rem_chunk_tokens = rem_chunk_tokens
    adder.tree_cache = SimpleNamespace(sliding_window_size=sliding_window)
    adder.token_to_kv_pool_allocator = SimpleNamespace(size_swa=size_swa)
    return adder
​
​
class TestSwaChunkCapHatch(CustomTestCase):
    """逃生舱必须只在真正队头活锁(预算 > 整个池)时触发。"""
​
    def test_transient_pressure_request_waits(self):
        # 预算远小于池容量:decode 释放窗口后必然能容纳,必须等待而非收缩准入
        adder = _swa_adder(size_swa=1024, sliding_window=128)
        self.assertFalse(
            adder._swa_req_never_fits(extend_input_len=256, max_new_tokens=64)
        )
​
    def test_request_larger_than_whole_pool_takes_hatch(self):
        # host-hit load-back 一次把预算推到池容量之上:无论池如何排空都装不下
        adder = _swa_adder(size_swa=1024, sliding_window=128)
        self.assertTrue(
            adder._swa_req_never_fits(
                extend_input_len=256, max_new_tokens=64, swa_host_hit_length=4096
            )
        )
​
    def test_decision_is_gated_by_pool_capacity(self):
        # 同一请求在不同池容量下结论翻转,验证比较对象是 size_swa 而非错误 accessor
        req = dict(extend_input_len=256, max_new_tokens=64, swa_host_hit_length=600)
        self.assertTrue(
            _swa_adder(size_swa=512, sliding_window=128)._swa_req_never_fits(**req)
        )
        self.assertFalse(
            _swa_adder(size_swa=4096, sliding_window=128)._swa_req_never_fits(**req)
        )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 核心调度路径变更add_one_req() 是调度器准入主路径,新增门控影响所有 hybrid-SWA 模型;锁后分支同样加入 NO_TOKEN 返回,在锁竞争下行为更保守,但需关注极端压力下等待队列被 transient 请求长期占用的可能性。
  2. 边界条件>= 比较意味着预算恰好等于 size_swa 时也走逃生舱;此时 _swa_chunk_cap 会把 chunk 收缩到当前 rem_swa_tokens 内,逻辑上安全,但边界场景缺少专门测试。
  3. 测试覆盖局限TestSwaChunkCapHatch 是 CPU 纯逻辑测试,通过 __new__ 构造 adder,未覆盖真实 allocator 行为与多 rank 场景;test_prefill_adder.py 只补了 mock 属性,未新增准入断言。
  4. 性能回归风险:每次触发分支多一次 _swa_budget_for_req 调用,属 O(1) 计算,风险极低。

对用户与系统:hybrid-SWA 模型在高并发(约 0.9 池使用率)下 retraction/re-prefill 风暴被消除,decode 吞吐 +36%、p50 延迟 -41%,等效提升单机服务能力;巨大 prompt 或大 host-load-back 请求仍可通过逃生舱避免队头活锁。对团队:调度器核心准入逻辑行为变化,所有依赖 SWA 池的模型(如 sliding-window attention 系)都应回归验证;两个测试文件的新增模式(__new__ 构造纯逻辑 adder)为后续调度策略单测提供低成本模板。

核心调度路径变更 回归修复(#31681 引入) 测试仅 CPU 纯逻辑 边界条件未覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论