执行摘要
- 一句话:支持 HiSparse host pool 页大小大于 1
- 推荐动作:值得详细阅读。此 PR 展示了如何在保持向后兼容的前提下移除硬编码假设,并通过 Mixin 模式进行优雅抽象。
HiSparseHostPoolMixin 和 alloc_paged_token_slots 的设计可供其他需要 page 分配的场景参考。同时,其中关于 GPU-CPU 同步的讨论也值得关注。
功能与动机
PR body 指出:当前 hisparse host pool 硬编码为 page=1,导致 page 调度在此中断,与 HiCache 集成时的数据传输性能严重下降。解除此限制是提升 hisparse 与 HiCache 结合性能的关键。
实现拆解
步骤 1:新增 HiSparseHostPoolMixin(memory_pool_host.py)
包含 _round_up_to_page_size、alloc_paged_token_slots、allocated_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_slots 和 test_pd_decode_prealloc_hisparse_host_slots,验证 page 分配、padding 和跨行索引的正确性。
关键文件:
python/sglang/srt/mem_cache/memory_pool_host.py(模块 内存池;类别 source;类型 core-logic;符号 HiSparseHostPoolMixin, _round_up_to_page_size, alloc_page, alloc_paged_token_slots): 核心文件:新增 HiSparseHostPoolMixin 类,提供 page 粒度分配接口;修改 MLATokenToKVPoolHost 继承该 Mixin,改变 host pool 的分配语义。
python/sglang/srt/managers/hisparse_coordinator.py(模块 协调器;类别 source;类型 core-logic): HiSparse 协调器使用新的 page 分配接口,修改了 admit_request_into_staging 和 _eager_backup_previous_token,移除对 alloc 的直接调用;新增 req_to_host_pool_allocated_len 数组。
python/sglang/srt/disaggregation/decode.py(模块 解码引擎;类别 source;类型 core-logic): 修改了 _init_kv_manager、_pre_alloc、pop_preallocated 等函数,统一 page_size 处理,移除 hisparse 特殊分支。
python/sglang/srt/disaggregation/mooncake/conn.py(模块 传输连接;类别 source;类型 core-logic;符号 send_kvcache_hisparse): 移除了 send_kvcache_hisparse 方法以及 KVArgsRegisterInfo.enable_hisparse 字段,简化传输协议。
python/sglang/srt/disaggregation/common/conn.py(模块 传输连接;类别 source;类型 core-logic): 移除了 hisparse 的 page size 特殊校验,改为严格的 page size 一致检查。
python/sglang/srt/mem_cache/hisparse_memory_pool.py(模块 内存池;类别 source;类型 core-logic;符号 DeepSeekV4SingleKVPoolHost): 修改 DeepSeekV4SingleKVPoolHost 的 page_size 初始化,从固定 1 改为参数化。
test/registered/unit/managers/test_hisparse_unit.py(模块 测试;类别 test;类型 test-coverage;符号 test_single_node_staging_allocates_paged_host_slots, test_pd_decode_prealloc_hisparse_host_slots): 新增两个单元测试:test_single_node_staging_allocates_paged_host_slots 和 test_pd_decode_prealloc_hisparse_host_slots,验证 page 分配和 padding 正确性。
关键符号: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
核心文件:新增 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
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,
)
# 后续传输操作 ...
评论区精华
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 实现,获得作者采纳。
- 异构 page size 的灵活性 (design): 作者回应已通过 HiSparseHostPoolMixin 抽象了 page 分配逻辑,未来可以通过扩展 Mixin 支持异构配置。
- GPU-CPU 同步优化 (performance): 作者承认存在同步开销,但此版本保持 pragma,后续迭代优化。审核者要求 benchmark 对比。
- 抽象至 memory_pool_host (design): 作者提取了 HiSparseHostPoolMixin 并放置在 memory_pool_host.py 中,实现了该建议。
风险与影响
- 风险:
- 核心路径变更风险:host pool 分配接口从
alloc() 切换到 alloc_paged_token_slots(),所有调用点均已更新,但若存在内部或外部未跟踪的调用(如子类重写),可能导致运行时错误。
- GPU-CPU 同步增加:
alloc_paged_token_slots 内部通过 int(req_to_host_pool_allocated_len[req_pool_idx]) 读取 CPU 端张量,每次分配引入同步,在高频小分配场景可能影响解码延迟。
- 传输路径简化风险:移除
send_kvcache_hisparse 后,所有 hisparse 传输走统一路径。若旧版本解码节点仍发送旧协议消息,会触发协议不匹配错误。但代码中已确保 hisparse 启用时 page_size 一致。
- 测试覆盖:新增单元测试覆盖了主要路径,但缺少对超大 page size、分配失败重试、并发请求等边界场景的覆盖。
- 影响:对 HiSparse 用户:host pool 行为透明改变,接口向后兼容;提升与 HiCache 结合时的吞吐。对 PD 分离部署:传输路径简化,减少一层索引转换,降低 CPU 开销。对代码可维护性:消除了大量特殊分支,使 host/device 页大小一致,降低认知负荷。潜在影响:若用户依赖旧版 page=1 的精确行为(如 host 索引连续性),可能需调整监控。
- 风险标记:核心路径变更, GPU-CPU 同步增加, 传输路径简化风险, 缺少测试覆盖
关联脉络
- PR #27489 Fix TP deadlock in unified radix cache writing_check / loading_check: 同为 HiCache/HiSparse 相关,修改了 unified_radix_cache.py,可能涉及相似的 host/device 同步问题。
- PR #26922 [PD][MoRI] Drive KV transfers with a sharded synchronous worker pool: 同为 disaggregation 传输优化,涉及 MoRI 引擎,与本 PR 的 PD 传输路径有交互。
参与讨论