Prhub

#23606 [HiSparse & PD] Support hisparse memory pool host page > 1

原始 PR 作者 huangtingwei9988 合并时间 2026-05-19 16:29 文件变更 7 提交数 17 评论 21 代码增减 +253 / -147

执行摘要

支持 HiSparse host pool 页大小大于 1

PR body 指出:当前 hisparse host pool 硬编码为 page=1,导致 page 调度在此中断,与 HiCache 集成时的数据传输性能严重下降。解除此限制是提升 hisparse 与 HiCache 结合性能的关键。

值得详细阅读。此 PR 展示了如何在保持向后兼容的前提下移除硬编码假设,并通过 Mixin 模式进行优雅抽象。HiSparseHostPoolMixinalloc_paged_token_slots 的设计可供其他需要 page 分配的场景参考。同时,其中关于 GPU-CPU 同步的讨论也值得关注。

讨论亮点

ShangmingCai(审核者):指出此改动放弃了异构 page size 的用例(hetero page size config),询问未来是否应保留灵活性。作者回应已通过 HiSparseHostPoolMixin 抽象出 page 分配逻辑,后续可根据需要扩展。

hzh0425(审核者):指出 PR 中包含大量 GPU-CPU 同步操作(alloc_paged_token_slots 内部的 int() 调用),建议后续优化减少同步;同时要求在 PD 模式下对比 page size=1 与 page size>1 的性能,确认无退化。

xiezhq-hermann(审核者):建议将 ensure_host_slots 的逻辑抽入 memory_pool_host 中,最终通过 HiSparseHostPoolMixin 实现,获得作者采纳。

实现拆解

步骤 1:新增 HiSparseHostPoolMixin(memory_pool_host.py)
包含 _round_up_to_page_sizealloc_paged_token_slotsallocated_host_indices 方法,提供 page 粒度的分配和查询接口。alloc_paged_token_slots 内部按需请求新页,并以非阻塞方式将索引复制到 req_to_host_pool,同时更新 allocated_len 数组。

步骤 2:修改 MLATokenToKVPoolHost 继承 Mixin(memory_pool_host.py)
将类声明改为 class MLATokenToKVPoolHost(HiSparseHostPoolMixin, HostKVCache),使其自动获得 page 分配能力。

步骤 3:修改 hisparse_coordinator.py
admit_request_into_staging_eager_backup_previous_token 中的直接 alloc 调用替换为 alloc_paged_token_slots;新增 req_to_host_pool_allocated_len 张量记录每请求已分配长度;同时将 host pool 的 page_size 初始化为 device pool 的 page_size,去除硬编码。

步骤 4:修改 decode.py
_init_kv_manager 中统一使用 token_to_kv_pool.page_size 作为 kv_args.page_size,不再区分 hisparse 路径;在 pop_preallocated 中移除对 hisparse 的 page_size=1 特殊处理;在 _pre_alloc 中改用 alloc_paged_token_slots 分配 host 索引。

步骤 5:简化 mooncake/conn.py 和 common/conn.py
移除 KVArgsRegisterInfo.enable_hisparse 字段和对应的序列化逻辑;彻底删除 send_kvcache_hisparse 方法,统一使用 send_kvcache;common/conn.py 中移除 hisparse 的 page size 特殊校验,改为严格的 page size 一致检查。

步骤 6:补充单元测试(test_hisparse_unit.py)
新增 test_single_node_staging_allocates_paged_host_slotstest_pd_decode_prealloc_hisparse_host_slots,验证 page 分配、padding 和跨行索引的正确性。

文件 模块 状态 重要度
python/sglang/srt/mem_cache/memory_pool_host.py 内存池 modified 8.37
python/sglang/srt/managers/hisparse_coordinator.py 协调器 modified 7.08
python/sglang/srt/disaggregation/decode.py 解码引擎 modified 6.85
python/sglang/srt/disaggregation/mooncake/conn.py 传输连接 modified 7.47
python/sglang/srt/disaggregation/common/conn.py 传输连接 modified 6.4
python/sglang/srt/mem_cache/hisparse_memory_pool.py 内存池 modified 6.29
test/registered/unit/managers/test_hisparse_unit.py 测试 modified 6.74

关键符号

HiSparseHostPoolMixin._round_up_to_page_size HiSparseHostPoolMixin.alloc_paged_token_slots HiSparseHostPoolMixin.allocated_host_indices HiSparseCoordinator.admit_request_into_staging HiSparseCoordinator._eager_backup_previous_token MooncakeKVManager.send_kvcache_hisparse (deleted) KVArgsRegisterInfo.from_zmq (modified) CommonKVManager.try_ensure_parallel_info (modified) Decode._init_kv_manager (modified) Decode._pre_alloc (modified) MLATokenToKVPoolHost.__init__ (modified)

关键源码片段

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

核心文件:新增 HiSparseHostPoolMixin 类,提供 page 粒度分配接口;修改 MLATokenToKVPoolHost 继承该 Mixin,改变 host pool 的分配语义。

class HiSparseHostPoolMixin:
    def _round_up_to_page_size(self, size: int) -> int:
        # 将 size 向上取整到 page_size 的倍数
        return (size + self.page_size - 1) // self.page_size * self.page_size
​
    def alloc_page(self, num_pages: int) -> Optional[torch.Tensor]:
        # 分配 num_pages 个页,返回连续页的索引张量
        return self.alloc(num_pages * self.page_size)
​
    def alloc_paged_token_slots(
        self,
        req_to_host_pool: torch.Tensor,
        req_to_host_pool_allocated_len: torch.Tensor,
        req_pool_idx: int,
        start_pos: int,
        num_tokens: int,
    ) -> torch.Tensor:
        # 以 page 为粒度分配 host token 槽位,返回 token 级别的索引
        device = req_to_host_pool.device
        if num_tokens <= 0:
            return torch.empty((0,), dtype=torch.int64, device=device)
​
        allocated_len = int(req_to_host_pool_allocated_len[req_pool_idx])
        end_pos = start_pos + num_tokens
        page_end = self._round_up_to_page_size(end_pos)
        assert start_pos <= allocated_len
​
        if page_end > allocated_len:
            # 需要分配新的页
            num_new_pages = (page_end - allocated_len) // self.page_size
            host_locs = self.alloc_page(num_new_pages)
            if host_locs is None:
                raise RuntimeError(
                    f'HiSparse host mem pool alloc failed for {num_new_pages} pages'
                )
​
            # 将新页的索引复制到 req_to_host_pool 中
            req_to_host_pool[req_pool_idx, allocated_len:page_end] = host_locs.to(
                device=device, non_blocking=True
            )
            req_to_host_pool_allocated_len[req_pool_idx] = page_end
​
        return req_to_host_pool[req_pool_idx, start_pos:end_pos]
​
    def allocated_host_indices(
        self,
        req_to_host_pool: torch.Tensor,
        req_pool_idx: int,
        allocated_len: int,
    ) -> torch.Tensor:
        # 返回已分配的有效 host 索引(按页对齐截断后过滤掉 -1)
        allocated_len = int(allocated_len)
        host_len = min(
            self._round_up_to_page_size(allocated_len),
            req_to_host_pool.shape[1],
        )
        host_indices = req_to_host_pool[req_pool_idx, :host_len]
        return host_indices[host_indices >= 0]
python/sglang/srt/managers/hisparse_coordinator.py core-logic

HiSparse 协调器使用新的 page 分配接口,修改了 admit_request_into_staging 和 _eager_backup_previous_token,移除对 alloc 的直接调用;新增 req_to_host_pool_allocated_len 数组。

    def __init__(self, ...):
        ...
        # 使用 device pool 的 page_size 代替硬编码的 1
        if self.is_dsv4_hisparse:
            self.mem_pool_host = DeepSeekV4SingleKVPoolHost(
                self.mem_pool_device,
                host_size,
                page_size=self.mem_pool_device.page_size, # 原来为 1
            )
        else:
            self.mem_pool_host = MLATokenToKVPoolHost(
                device_pool=self.mem_pool_device,
                host_to_device_ratio=host_to_device_ratio,
                host_size=0,
                page_size=self.mem_pool_device.page_size, # 原来为 1
                layout='layer_first',
                override_kv_cache_dim=self.mem_pool_device.kv_cache_dim,
            )
        self.page_size = self.mem_pool_device.page_size
​
        # 新增每请求已分配 host 长度跟踪
        self.req_to_host_pool_allocated_len = torch.zeros(
            max_num_req_slots, dtype=torch.int64, device='cpu'
        )
​
    def admit_request_into_staging(self, req: Req) -> None:
        # 使用新的 page 分配接口替代直接 alloc
        host_indices = self.mem_pool_host.alloc_paged_token_slots(
            self.req_to_host_pool,
            self.req_to_host_pool_allocated_len,
            req.req_pool_idx,
            0,
            prefill_len,
        )
        # 后续传输操作 ...

评论区精华

异构 page size 的灵活性 设计

ShangmingCai 指出改动放弃了异构 page size 的用例,询问未来是否考虑。

结论:作者回应已通过 HiSparseHostPoolMixin 抽象了 page 分配逻辑,未来可以通过扩展 Mixin 支持异构配置。 · 已解决

GPU-CPU 同步优化 性能

hzh0425 指出 PR 中包含许多 GPU-CPU 同步(alloc_paged_token_slots 中 int() 读取 CPU 张量),需要优化并验证性能。

结论:作者承认存在同步开销,但此版本保持 pragma,后续迭代优化。审核者要求 benchmark 对比。 · unresolved

抽象至 memory_pool_host 设计

xiezhq-hermann 建议将 ensure_host_slots 逻辑抽入 memory_pool_host 中。

结论:作者提取了 HiSparseHostPoolMixin 并放置在 memory_pool_host.py 中,实现了该建议。 · 已解决

风险与影响

  1. 核心路径变更风险:host pool 分配接口从 alloc() 切换到 alloc_paged_token_slots(),所有调用点均已更新,但若存在内部或外部未跟踪的调用(如子类重写),可能导致运行时错误。
  2. GPU-CPU 同步增加alloc_paged_token_slots 内部通过 int(req_to_host_pool_allocated_len[req_pool_idx]) 读取 CPU 端张量,每次分配引入同步,在高频小分配场景可能影响解码延迟。
  3. 传输路径简化风险:移除 send_kvcache_hisparse 后,所有 hisparse 传输走统一路径。若旧版本解码节点仍发送旧协议消息,会触发协议不匹配错误。但代码中已确保 hisparse 启用时 page_size 一致。
  4. 测试覆盖:新增单元测试覆盖了主要路径,但缺少对超大 page size、分配失败重试、并发请求等边界场景的覆盖。

对 HiSparse 用户:host pool 行为透明改变,接口向后兼容;提升与 HiCache 结合时的吞吐。对 PD 分离部署:传输路径简化,减少一层索引转换,降低 CPU 开销。对代码可维护性:消除了大量特殊分支,使 host/device 页大小一致,降低认知负荷。潜在影响:若用户依赖旧版 page=1 的精确行为(如 host 索引连续性),可能需调整监控。

核心路径变更 GPU-CPU 同步增加 传输路径简化风险 缺少测试覆盖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论