Prhub

#35177 feat(unified-memory): three sub-pools for mamba + hybrid-SWA models

原始 PR 作者 caihuali95 合并时间 2026-09-01 06:10 文件变更 14 提交数 18 评论 2 代码增减 +4780 / -182

执行摘要

unified 池泛化至 N 子池,新增浮动池支撑 mamba+SWA 三态 KV

PR body 明确指出:"Unified memory currently supports exactly two sub-pools, so it cannot serve a model whose KV state comes in three kinds — full-attention KV, sliding-window KV, and recurrent (mamba/conv) state. Inkling is the first such model in tree." 静态池在 boot 时按固定比例砌墙,idle 容量无法互相调剂;request mix 在 boot 时不可知,拆分是否合理无法预先确定。因此需要把 unified pool 泛化到 N 并让边界随运行负载移动。

值得精读。三个设计决策尤其值得关注:一是 _CapacityField 用数据描述符 + epoch 让容量 memo 失效"按构造"发生,避免了散落的失效钩子;二是 _float_open_short_side 的 demand-vector 策略,使单带宽短供、耦合多带分配、未来组合准入向量统一为一种表达;三是 _relieve_for_alloc 将 flush/relocate/evict 收敛为单一 ladder。建议结合 test_unified_capacity_memo.py 与 test_multi_ended_allocator.py 的测试方法论阅读,这两份测试的防御深度(属性测试 + 反射扫描 + 伪装写入探测)本身就值得借鉴。

讨论亮点

PR 页面无公开 review 评论(review_comments_count = 0),核心讨论全部沉淀在提交历史与 ch-wan 的 CI 操作中。最有价值的交锋是 commit ae178552 的 "Adversarial review":ch-wan 对本 PR 与该系列早期重构做了对抗性审查,发现 6 个缺陷(最严重的是联合容量按 raw bytes 定价 float 扩展,而 take_physical_pages 只能整页消费,导致 boot 后首次 alloc 触发 alloc_with_virtual 断言),每个修复都带一个回滚即失败的测试。其次是路由顺序问题:mambaish 分支先检查会吞掉 Inkling 类模型,使其 SWA KV 按 full lifetime 存储、丢失窗口回收。此外还暴露了测试自身缺陷——logprob 断言因参数顺序错误从未真正执行,以及 move-gate 测试 stub 在链式化后腐烂。

实现拆解

变更按五层推进:

  1. 池层泛化到 N 子池(python/sglang/srt/mem_cache/unified_memory_pool.py):SubPoolSpec 新增 _allowed_grow_directions ClassVar,grow_directionup/down 扩展出 floatUnifiedKVPool.__init__len(sub_pool_specs) == 2 断言放宽为 >= 2,且强制恰好一个 grow-up 端、恰好一个 grow-down 端,float 数量任意,并按 [up, floats(输入序), down] 规范化链序;_reserved_floor_bytes 覆盖 float 的 page-0 预留信封(mamba 保持 page_size=1 不计 page 项)。输入顺序从此无关,2 池布局 byte-identical(由 test_unified_npool_sweep.py 钉住)。

  2. 分配器链式化(python/sglang/srt/mem_cache/multi_ended_allocator.py):_peer 单指针改为 low_peer/high_peer 链,bind_peer 保留为 2 池 END 对兼容 shim(拒绝 float 成员),新增 bind_low_peer/bind_high_peer_current_gap_bytes 等所有 peer 咨询路径改走透明链 walk(_chain_low_frontier_above_bytes/_chain_high_frontier_below_bytes/_is_frontier_transparent),空 float 被跳过、首个不透明成员截断 gap。test_multi_ended_allocator.py 的 golden identity 确保 2 池下链式 walk 与旧单 peer 闭式公式一致。

  3. FloatMultiEndedAllocator 与统一短供 ladder:float 中池 holes-first——free 在内部打洞零拷贝,alloc 先复用洞、再向更大间隙一侧扩展边界,空池停在双端 frontier 的中点;make_room/compact_holes 是独立的数据搬运器,仅在邻居短供时按需搬迁。短供处理收敛为模块级 _relieve_for_alloc:flush → 复查 → 让 float 滑动 → 复查 → 交给 evict/retract;_float_open_short_side 由 per-band demand vector 驱动,单带短供(如 mamba 态不足)与复合体耦合多带分配走同一条路径。

  4. 容量视图 memo 化:链式 walk 使容量计算比双端对更贵,而调度器每步读 O(queue) 次;_CapacityField 数据描述符在每次 rebind 时推进 _capacity_epochavailable_size/schedulable_available_size 及复合 joint view 全部 epoch 键控缓存,失效由构造保证。约束是这些字段只能 rebind、不能原地 mutate——test_unified_capacity_memo.py 专门验证绕过描述符的写入会被 verify_byte_accounting 的 idle 检查捕获。

  5. 路由、接线与配套:python/sglang/srt/mem_cache/kv_cache_configurator.py 在 mambaish && is_hybrid_swa && !dsv4 时优先走新增的 _init_unified_mamba_swa_pools(PD 组合显式拒绝);mamba 池的物理移动契约统一为 move_kv_cache(修复了 src/dst 参数顺序相反的隐患);per-step SWA free 接收 start_pos 并用算术派生 page 代表,移除 torch.unique 带来的 host sync,2 池 SWA 复合同样受益。测试配套为 7+ 文件:CPU 单测(tri pool 契约、容量 memo 属性测试、N-pool 几何 sweep、no-host-sync 反射扫描)+ GPU e2e(test_inkling_unified.py)。

文件 模块 状态 重要度
python/sglang/srt/mem_cache/multi_ended_allocator.py 分配器 modified 8.84
python/sglang/srt/mem_cache/unified_memory_pool.py 内存池 modified 8.42
python/sglang/srt/mem_cache/kv_cache_configurator.py 配置器 modified 7.48
test/registered/models_e2e/test_inkling_unified.py 端到端测试 added 7.96
test/registered/unit/mem_cache/test_unified_tri_pool.py 三池单测 added 7.48
test/registered/unit/mem_cache/test_unified_capacity_memo.py 容量测试 added 7.48
python/sglang/srt/managers/scheduler_components/invariant_checker.py 调度校验 modified 6.08

关键符号

_CapacityField _float_open_short_side _relieve_for_alloc FloatMultiEndedAllocator.make_room FloatMultiEndedAllocator.compact_holes UnifiedMambaSWATokenToKVPoolAllocator init_unified_mamba_swa_pools _init_unified_mamba_swa_pools move_kv_cache conserve_full_available_size conserve_swa_available_size _is_frontier_transparent

关键源码片段

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

UnifiedKVPool 从恰好 2 子池泛化为 N 子池并支持 float 中间池;新增 init_unified_mamba_swa_pools 三池工厂与 move_kv_cache 统一移动契约。

class UnifiedKVPool:
    """一个 uint8 字节缓冲共享给 N 个子池:两个 END 池(grow-up / grow-down)
    各占一端向内生长,可选若干个 float 中间池活在两端 frontier 之间。
    分配器保证字节区间不相交;这里不做使用追踪。
    """
​
    def __init__(self, *, total_bytes, sub_pool_specs, device,
                 enable_memory_saver, page_size=1):
        assert page_size >= 1, f"page_size must be >= 1; got {page_size}"
        assert len(sub_pool_specs) >= 2, (
            f"UnifiedKVPool needs >= 2 sub-pools; got {len(sub_pool_specs)}"
        )
        names = [s.name for s in sub_pool_specs]
        assert len(set(names)) == len(names), (
            f"sub-pool names must be unique; got {names}"
        )
​
        # 方向合法性已在每个 spec 的 __post_init__ 中校验过;
        # 这里只保证链结构:恰好一个 up 端 + 一个 down 端,float 任意多个。
        up_specs = [s for s in sub_pool_specs if s.grow_direction == "up"]
        down_specs = [s for s in sub_pool_specs if s.grow_direction == "down"]
        float_specs = [s for s in sub_pool_specs if s.grow_direction == "float"]
        assert len(up_specs) == 1 and len(down_specs) == 1, (
            f"UnifiedKVPool needs exactly one grow-up and one grow-down END "
            f"sub-pool; got directions {[s.grow_direction for s in sub_pool_specs]}"
        )
​
        # 规范链序(低字节端 -> 高):grow-up END, float 中间池(保持输入顺序),
        # grow-down END。分配器邻居接线按此顺序;其余访问按名字,输入顺序无关。
        self.sub_pool_specs: List[SubPoolSpec] = [
            up_specs[0],
            *float_specs,
            down_specs[0],
        ]
        self._page_size = page_size
        self._specs_by_name: Dict[str, SubPoolSpec] = {
            s.name: s for s in sub_pool_specs
        }
        # ... 视图构建与 reserved-floor 计算随后按 self.sub_pool_specs 统一进行,
        # float 的 page-0 dummy-write 信封也计入预留,保证 mamba 等 page_size=1
        # 的子池只按单 slot 计费。

评论区精华

mambaish 路由优先级导致 Inkling 误入 2-pool 分支 正确性

原路由 gate 先检查 mambaish,Inkling 类模型会进 2-pool mamba 分支;该分支读 HF config 的 full_attention_layer_ids(对 Inkling 是全部层),所有 SWA 层 KV 按 full lifetime 存储,丢失窗口回收。commit acea0e90 记录了这一顺序问题。

结论:新增 tri-pool 分支放在最前:mambaish && is_hybrid_swa && !dsv4 优先路由,并显式拒绝 PD 组合(无 SWA 子池的 whole-envelope 传输方案)。 · 已解决

Adversarial review 发现的 6 个缺陷 正确性

commit ae178552 记录 ch-wan 的对抗性审查:最严重的是联合容量按 raw bytes 定价 float 扩展,而 take_physical_pages 只能整页消费,byte budget 记入的页在 float 网格上不可用,boot 后首次 alloc 触发 alloc_with_virtual 断言。

结论:6 个缺陷逐一修复,每个修复附带回滚即失败的测试;包括 region bounds 页取整、chain walk 计数、capacity memo 覆盖等。 · 已解决

容量 memo 的失效机制设计 设计

链式化后容量视图计算变贵,调度器 O(queue) 次读取;选择 _CapacityField 描述符 + epoch 记忆化,使失效 " 按构造 " 发生。测试专门验证绕过描述符的写入(fa.__dict__["watermark_physical"])会被 verify_byte_accounting 的 idle 检查捕获。

结论:采用 epoch memo;约束是容量字段只能 rebind、不能原地 mutate,未来新增变异点由测试兜底。 · 已解决

SWA per-step free 移除 host sync 性能

per-step SWA window ratchet 的 free 接收 start_pos,算术派生 page 代表,避免 torch.unique(数据相关形状 = host sync)和 .item();测试 mock 掉 torch.unique 和 item 证明同步点消失,该行为同时影响现存 2-pool SWA 复合。

结论:采用 start_pos 路径;不可对齐时用 _page_reps_pieces 补第二段,保持无同步;与 dedup fallback 的端状态一致性有测试 pin 住。 · 已解决

logprob 测试断言从未执行 测试

commit 4c578a12 记录:assert_logprobs_match(self, self.base_url, self.model) 参数顺序错误——TestCase 传给 base_url 位置、base_url 传给阈值表位置,在发请求前就抛 TypeError,prefill-vs-decode KV 一致性从未被真正检查。

结论:修复调用签名为 assert_logprobs_match(self.base_url, {model: {"kl_div": 1e-2}}, self.model),测试真正生效。 · 已解决

注释 ASCII 规范化 style

comment-style.md 要求 ASCII,PR 新增的 39 行带 em-dash;只处理 PR 自己拥有的行,父提交上的原样保留(commit 6dcd6fe1 说明这是第一次做这个扫描时搞错的检查点)。

结论:全部新增注释转 ASCII。 · 已解决

风险与影响

  1. 核心路径回归风险:multi_ended_allocator.py 改动 +1406/-130,覆盖所有 unified pool 分配路径。链式 walk 若在 2 池下与旧封闭式公式产生偏差,会整体改变容量判断;test_multi_ended_allocator.py 用 golden identity 钉住 2 池数学,但边界组合(lazy/eager、分组 free、跨池搬迁)仍多。
  2. 容量 memo 静默失效:epoch 机制依赖"字段只能 rebind 不能原地 mutate"的契约,未来新增变异点若绕过 _CapacityField,过/欠准入不会崩溃、只会静默;测试用 verify_byte_accounting 兜底,但这是运行时校验而非编译期保证。
  3. float 数据搬迁:make_room/compact_holes 涉及 live 页重定位 + v2p/p2v 重建 + KV 数据拷贝,正确性依赖 inverse_history 与 pending_reuse 的事件同步;6 个已修复缺陷表明此类边界条件密集。
  4. 现有 2 池 SWA 行为变化:per-step free_swa 改为 start_pos 算术派生 page 代表,影响所有 SWA 复合(即使非 tri),改动虽小但在 decode 热路径。
  5. e2e 依赖外部 checkpoint(thinkingmachines/Inkling test revision),CI 稳定性受上游可用性影响;parity lane 需 INKLING_UNIFIED_PARITY=1 才运行,回归探测有滞后。
  6. PD 组合被显式拒绝:hybrid-SWA 三池与 disaggregation 同时使用会抛 ValueError,功能面收窄是有意为之。

用户与模型面:Inkling 首次获得 unified-memory 支持(此前直接 boot 崩溃);2 池模型(Falcon-H1、Qwen3.5、Kimi-Linear、gpt-oss 等)布局 byte-identical,GSM8K 精度中位差在 ±0.75pp 内、速度中位 ITL 变化在 -2.85% ~ +2.98% 内,基本无回归。系统面:调度器容量语义从 byte 协调视图切换到 slot-conservation 视图(conserve_full_available_size/conserve_swa_available_size),invariant checker 与 pool stats observer 同步调整;SWA per-step free 去掉 host sync,decode 路径少一个同步点。团队面:mem_cache 成为 N 池链式架构,后续新子池(如 spec-decode scratch band 已预留 float-only 收窄)有明确扩展点;测试方法论(AST 扫描 tombstone、随机操作属性测试、e2e parity lane)可复用。

核心路径变更 容量一致性风险 跨池数据搬迁 外部模型依赖 调度器语义变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论