Prhub

#28753 Fix/hisparse host backed max request length

原始 PR 作者 whybeyoung 合并时间 2026-08-10 16:58 文件变更 3 提交数 8 评论 11 代码增减 +212 / -4

执行摘要

修复 HiSparse 下长请求被设备池容量误截断

HiSparse 将完整序列驻留在 host-backed 逻辑池中,逻辑容量应体现设备池大小乘以 host_to_device_ratio 后的值,而 max_req_input_len 是从 max_total_num_tokens(设备池大小)派生的,导致长上下文请求即使能被 HiSparse 承载也仍被截断在设备容量之下。PR body 与首个 commit 均点明该问题;daii-0818 在 issue 评论中进一步确认:GPU 容量 < request <= HiSparse size_full 时本应接受而被误拒,且该语义在 PD decode 场景仍然必要。

值得精读。核心价值在于把分散的容量判断收敛为 ModelRunner.max_token_pool_size 单一出口,并同步修正 capturer 缓冲与 decode 准入两个消费点;测试中「用 object.__new__(ModelRunner) 构造裸实例让 property 描述符正常解析」的夹具技巧也值得借鉴。阅读时建议对照 #34345 了解未覆盖的 max_req_len / init_req_max_new_tokens 路径,形成完整的容量语义地图。

讨论亮点
  1. gemini-code-assist[bot](high):指出 max_token_pool_size 变大后,init_routed_experts_capturerinit_indexer_capturer 仍按 max_total_num_tokens + page_size 分配缓冲,HiSparse 下会越界或断言失败——已在后续 commit 中改为 max_token_pool_size + page_size
  2. gemini-code-assist[bot](medium):建议 decode.py 直接使用 self.scheduler.enable_hisparse 而非 getattr(model_runner, "enable_hisparse", False),与文件内其他 HiSparse 判断保持一致——最终代码采用了该建议。
  3. huangtingwei9988:第一轮要求补单元测试;随后指出 test_non_hisparse_hybrid_swa_prefers_full_maxSimpleNamespace 调用 ModelRunner.max_token_pool_size 会因属性描述符不解析而抛 AttributeError,建议改用 object.__new__(ModelRunner) 让两个 property 都走正常描述符查找——作者据此重构了测试夹具。
  4. daii-0818:验证发现下游 TpModelWorker.max_req_lenScheduler.init_req_max_new_tokens() 仍使用 GPU 驻留容量,属于本 PR 未覆盖的遗漏点,并已开跟进 PR #34345 附带 CPU 回归测试。

实现拆解

本 PR 以「容量语义统一」为主线,分三步落地:

  1. 在 ModelRunner 新增统一容量出口(python/sglang/srt/model_executor/model_runner.py):新增 max_token_pool_size property,当 enable_hisparse 为真且 token_to_kv_pool_allocator 暴露 size_full 时返回该 host-backed 逻辑容量,否则回退到已有的 effective_max_total_num_tokens(SWA 感知)。这样上层调用方无需关心 HiSparse 的扩容细节,所有消费点只认一个逻辑容量。
  2. 修正 capturer 缓冲分配init_routed_experts_capturerinit_indexer_capturer 中的 num_tokensself.max_total_num_tokens + self.page_size 改为 self.max_token_pool_size + self.page_size,避免 HiSparse 下请求长度超过设备池容量时 capturer 缓冲越界或断言失败。
  3. 修正 PD decode 准入检查(python/sglang/srt/disaggregation/decode.py):DecodePreallocQueue._check_if_req_exceed_kv_capacityscheduler.enable_hisparse 时改用 scheduler.tp_worker.model_runner.max_token_pool_size 作为容量上限,非 HiSparse 路径仍用 self.max_total_num_tokens,保证行为不回归。
  4. 配套测试(test/registered/unit/mem_cache/test_hisparse_max_token_pool_size.py,新增):注册为 CPU CI 用例,覆盖 max_token_pool_size 的 HiSparse 返回 size_full、size_full 缺失回退、非 HiSparse 委托 effective max、hybrid SWA 优先 full max 四个分支;并用 object.__new__(ModelRunner) 构造裸实例,使 property descriptor 正常解析。decode 侧覆盖 host-backed 容量内准入、超容量拒绝、非 HiSparse 仍按设备池拒绝、rebootstrap 长度(prompt + output)计入容量四种场景。
文件 模块 状态 重要度
python/sglang/srt/model_executor/model_runner.py 模型执行器 modified 7.2
python/sglang/srt/disaggregation/decode.py 解码准入 modified 5.88
test/registered/unit/mem_cache/test_hisparse_max_token_pool_size.py 容量检测 added 7.39

关键符号

ModelRunner.max_token_pool_size ModelRunner.effective_max_total_num_tokens ModelRunner.init_routed_experts_capturer ModelRunner.init_indexer_capturer DecodePreallocQueue._check_if_req_exceed_kv_capacity DecodePreallocQueue._rebootstrap_prefill_len

关键源码片段

python/sglang/srt/model_executor/model_runner.py data-contract

新增 max_token_pool_size property,定义 HiSparse 下 host-backed 逻辑容量语义;同时将路由专家 capturer 与 indexer capturer 的缓冲分配从 max_total_num_tokens 切换到该逻辑容量,是本 PR 的容量契约核心。

# python/sglang/srt/model_executor/model_runner.py
# HiSparse 容量语义的关键出口:所有上层容量判断都应经此属性取值。
@property
def max_token_pool_size(self):
    """Return the max token pool size considering hybrid swa and hisparse settings."""
    if self.enable_hisparse:
        # HiSparse 把完整序列放在 host-backed 逻辑池中,逻辑容量 = 设备池大小 *
        # host_to_device_ratio,由 allocator 以 size_full 暴露。若 allocator 尚未
        # 暴露该属性(例如初始化时序导致非 HiSparse allocator 接入),则回退到
        # SWA 感知的 effective_max_total_num_tokens,避免 AttributeError。
        size_full = getattr(self.token_to_kv_pool_allocator, "size_full", None)
        if size_full is not None:
            return size_full
    return self.effective_max_total_num_tokens
​
​
# capturer 缓冲分配同步改用逻辑容量,避免 HiSparse 下请求长度超过设备池时
# routed experts / indexer 缓冲越界或断言失败。
def init_routed_experts_capturer(self):
    # ... 省略前置检查 ...
    set_global_experts_capturer(
        RoutedExpertsCapturer.create(
            model=self.model,
            model_config=self.model_config,
            num_tokens=self.max_token_pool_size + self.page_size, # 原为 max_total_num_tokens
            max_running_requests=self.max_running_requests,
            device=self.device,
        )
    )def init_indexer_capturer(self):
    set_global_indexer_capturer(
        create_indexer_capturer(
            model_config=self.model_config,
            num_tokens=self.max_token_pool_size + self.page_size, # 原为 max_total_num_tokens
            max_running_requests=self.max_running_requests,
            device=self.device,
        )
    )
python/sglang/srt/disaggregation/decode.py core-logic

DecodePreallocQueue._check_if_req_exceed_kv_capacity 准入容量从设备池 max_total_num_tokens 切换到 HiSparse 逻辑容量,是 PD decode 侧长请求不再被误拒的关键改动。

# python/sglang/srt/disaggregation/decode.py
# PD decode 预分配队列的容量准入检查。
def _check_if_req_exceed_kv_capacity(self, req: Req) -> bool:
    # HiSparse 场景下,请求准入容量取 host-backed 逻辑容量(max_token_pool_size),
    # 而非设备池 max_total_num_tokens。二者在 HiSparse 下可能相差 host_to_device_ratio 倍,
    # 若仍按设备容量校验,会把可被 HiSparse 承载的长上下文请求误判为超限并 abort。
    if self.scheduler.enable_hisparse:
        capacity = self.scheduler.tp_worker.model_runner.max_token_pool_size
    else:
        # 非 HiSparse 路径保持原行为,避免本改动影响常规 decode 准入。
        capacity = self.max_total_num_tokens
    input_len = self._rebootstrap_prefill_len(req)
    if input_len > capacity:
        message = f"Request {req.rid} exceeds the maximum number of tokens: {input_len} > {capacity}"
        logger.error(message)
        prepare_abort(req, message, status_code=HTTPStatus.BAD_REQUEST)
        self.scheduler.output_streamer.stream_output([req], req.return_logprob)
        return True
    # ... 后续 SWA tail 预分配检查与原有逻辑保持一致 ...
    return False

评论区精华

capturer 缓冲分配越界风险 正确性

gemini-code-assist[bot] 指出 max_token_pool_size 变大后,init_routed_experts_capturer 与 init_indexer_capturer 仍按 max_total_num_tokens + page_size 分配缓冲,HiSparse 长请求可能触发越界或断言失败。

结论:已修复:两处 num_tokens 均改用 self.max_token_pool_size + self.page_size。 · 已解决

使用 scheduler.enable_hisparse 保持一致性 style

gemini-code-assist[bot] 建议 decode.py 直接用 self.scheduler.enable_hisparse 而非 getattr(model_runner, "enable_hisparse", False),与文件内其他 HiSparse 判断保持一致。

结论:已采纳,最终代码使用 self.scheduler.enable_hisparse 分支。 · 已解决

补单元测试 测试

huangtingwei9988 首轮 review 要求补充单元测试,驱动了后续 193 行 CPU 测试的编写。

结论:已补充 test_hisparse_max_token_pool_size.py 并注册 CPU CI。 · 已解决

SimpleNamespace 绕过 property 描述符导致 AttributeError 测试

huangtingwei9988 指出 test_non_hisparse_hybrid_swa_prefers_full_max 用 SimpleNamespace 调用 ModelRunner.max_token_pool_size 时,内部的 self.effective_max_total_num_tokens 读取会因描述符不解析而抛 AttributeError,建议加属性或改用 object.__new__(ModelRunner)。

结论:已修复:_make_model_runner 改用 object.__new__(ModelRunner) 并 setattr 注入属性,使两个 property 都走正常描述符查找。 · 已解决

下游仍有两处使用 GPU 驻留容量 正确性

daii-0818 验证发现 TpModelWorker.max_req_len 与 Scheduler.init_req_max_new_tokens() 仍按 GPU 驻留容量计算,HiSparse 长请求可能在其他入口被截断,并已开跟进 PR #34345。

结论:本 PR 未覆盖,交由 #34345 跟进修复并附带 CPU 回归测试。 · unresolved

风险与影响

  1. 下游消费点未全覆盖:daii-0818 指出 TpModelWorker.max_req_lenScheduler.init_req_max_new_tokens() 仍按 GPU 驻留容量计算,HiSparse 下长请求可能在其他入口被截断,需由 #34345 收尾。
  2. capturer 缓冲内存上升init_routed_experts_capturer / init_indexer_capturernum_tokens 改大后,routed expert 捕获与 indexer 捕获的缓冲将按 host-backed 逻辑容量分配,在设备内存有限时可能增加显存压力。
  3. 契约依赖max_token_pool_size 依赖 allocator.size_full 是否存在及 enable_hisparse 标志是否与 allocator 实际类型一致;若初始化时序导致 HiSparse allocator 尚未接线,会静默回退到设备容量,行为正确但语义不达预期。
  4. 回归面:decode 准入从直接读 max_total_num_tokens 改为经 scheduler.tp_worker.model_runner 间接取容量,引入跨对象依赖,若 tp_workermodel_runner 未就绪会抛属性错误(测试已用 mock 覆盖主路径)。

对 HiSparse 用户是功能性修复:长上下文请求不再被设备池上限误拒,可用请求长度提升 host_to_device_ratio 倍,直接利好超长序列离线批处理与 PD 解耦部署。对非 HiSparse 用户无行为变化,但 max_token_pool_size 成为新的统一容量出口,后续容量相关逻辑都应优先经它取值。对团队而言,本 PR 确立了「逻辑容量 vs 物理设备容量」分离的建模方式,并暴露了两处下游遗漏点(#34345),提示容量语义类改动需要全局检索消费点。

下游消费点未全覆盖(#34345) capturer 缓冲内存上升 容量契约依赖 allocator 时序 新增跨对象依赖

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论