执行摘要
- 一句话:修复 HiSparse 下长请求被设备池容量误截断
- 推荐动作:值得精读。核心价值在于把分散的容量判断收敛为
ModelRunner.max_token_pool_size 单一出口,并同步修正 capturer 缓冲与 decode 准入两个消费点;测试中「用 object.__new__(ModelRunner) 构造裸实例让 property 描述符正常解析」的夹具技巧也值得借鉴。阅读时建议对照 #34345 了解未覆盖的 max_req_len / init_req_max_new_tokens 路径,形成完整的容量语义地图。
功能与动机
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 场景仍然必要。
实现拆解
本 PR 以「容量语义统一」为主线,分三步落地:
- 在 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 的扩容细节,所有消费点只认一个逻辑容量。
- 修正 capturer 缓冲分配:
init_routed_experts_capturer 与 init_indexer_capturer 中的 num_tokens 由 self.max_total_num_tokens + self.page_size 改为 self.max_token_pool_size + self.page_size,避免 HiSparse 下请求长度超过设备池容量时 capturer 缓冲越界或断言失败。
- 修正 PD decode 准入检查(python/sglang/srt/disaggregation/decode.py):
DecodePreallocQueue._check_if_req_exceed_kv_capacity 在 scheduler.enable_hisparse 时改用 scheduler.tp_worker.model_runner.max_token_pool_size 作为容量上限,非 HiSparse 路径仍用 self.max_total_num_tokens,保证行为不回归。
- 配套测试(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(模块 模型执行器;类别 source;类型 data-contract;符号 max_token_pool_size, effective_max_total_num_tokens, init_routed_experts_capturer, init_indexer_capturer): 新增 max_token_pool_size property,定义 HiSparse 下 host-backed 逻辑容量语义;同时将路由专家 capturer 与 indexer capturer 的缓冲分配从 max_total_num_tokens 切换到该逻辑容量,是本 PR 的容量契约核心。
python/sglang/srt/disaggregation/decode.py(模块 解码准入;类别 source;类型 core-logic;符号 _check_if_req_exceed_kv_capacity, _rebootstrap_prefill_len): DecodePreallocQueue._check_if_req_exceed_kv_capacity 准入容量从设备池 max_total_num_tokens 切换到 HiSparse 逻辑容量,是 PD decode 侧长请求不再被误拒的关键改动。
test/registered/unit/mem_cache/test_hisparse_max_token_pool_size.py(模块 容量检测;类别 test;类型 test-coverage;符号 _make_model_runner, TestMaxTokenPoolSize, TestCheckIfReqExceedKvCapacity, _make_prealloc_queue): 新增 193 行 CPU 单测,覆盖 max_token_pool_size 的 HiSparse/SWA/fallback 分支以及 decode 准入在 host-backed 容量、超容量拒绝、非 HiSparse 不变、rebootstrap 长度计入四种场景,是本 PR 的核心回归保障。
关键符号: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
新增 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
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
评论区精华
- gemini-code-assist[bot](high):指出
max_token_pool_size 变大后,init_routed_experts_capturer 与 init_indexer_capturer 仍按 max_total_num_tokens + page_size 分配缓冲,HiSparse 下会越界或断言失败——已在后续 commit 中改为 max_token_pool_size + page_size。
- gemini-code-assist[bot](medium):建议 decode.py 直接使用
self.scheduler.enable_hisparse 而非 getattr(model_runner, "enable_hisparse", False),与文件内其他 HiSparse 判断保持一致——最终代码采用了该建议。
- huangtingwei9988:第一轮要求补单元测试;随后指出
test_non_hisparse_hybrid_swa_prefers_full_max 用 SimpleNamespace 调用 ModelRunner.max_token_pool_size 会因属性描述符不解析而抛 AttributeError,建议改用 object.__new__(ModelRunner) 让两个 property 都走正常描述符查找——作者据此重构了测试夹具。
- daii-0818:验证发现下游
TpModelWorker.max_req_len 与 Scheduler.init_req_max_new_tokens() 仍使用 GPU 驻留容量,属于本 PR 未覆盖的遗漏点,并已开跟进 PR #34345 附带 CPU 回归测试。
- capturer 缓冲分配越界风险 (correctness): 已修复:两处 num_tokens 均改用 self.max_token_pool_size + self.page_size。
- 使用 scheduler.enable_hisparse 保持一致性 (style): 已采纳,最终代码使用 self.scheduler.enable_hisparse 分支。
- 补单元测试 (testing): 已补充 test_hisparse_max_token_pool_size.py 并注册 CPU CI。
- SimpleNamespace 绕过 property 描述符导致 AttributeError (testing): 已修复:_make_model_runner 改用 object.new(ModelRunner) 并 setattr 注入属性,使两个 property 都走正常描述符查找。
- 下游仍有两处使用 GPU 驻留容量 (correctness): 本 PR 未覆盖,交由 #34345 跟进修复并附带 CPU 回归测试。
风险与影响
- 风险:
- 下游消费点未全覆盖:daii-0818 指出
TpModelWorker.max_req_len 与 Scheduler.init_req_max_new_tokens() 仍按 GPU 驻留容量计算,HiSparse 下长请求可能在其他入口被截断,需由 #34345 收尾。
- capturer 缓冲内存上升:
init_routed_experts_capturer / init_indexer_capturer 的 num_tokens 改大后,routed expert 捕获与 indexer 捕获的缓冲将按 host-backed 逻辑容量分配,在设备内存有限时可能增加显存压力。
- 契约依赖:
max_token_pool_size 依赖 allocator.size_full 是否存在及 enable_hisparse 标志是否与 allocator 实际类型一致;若初始化时序导致 HiSparse allocator 尚未接线,会静默回退到设备容量,行为正确但语义不达预期。
- 回归面:decode 准入从直接读
max_total_num_tokens 改为经 scheduler.tp_worker.model_runner 间接取容量,引入跨对象依赖,若 tp_worker 或 model_runner 未就绪会抛属性错误(测试已用 mock 覆盖主路径)。
- 影响:对 HiSparse 用户是功能性修复:长上下文请求不再被设备池上限误拒,可用请求长度提升 host_to_device_ratio 倍,直接利好超长序列离线批处理与 PD 解耦部署。对非 HiSparse 用户无行为变化,但 max_token_pool_size 成为新的统一容量出口,后续容量相关逻辑都应优先经它取值。对团队而言,本 PR 确立了「逻辑容量 vs 物理设备容量」分离的建模方式,并暴露了两处下游遗漏点(#34345),提示容量语义类改动需要全局检索消费点。
- 风险标记:下游消费点未全覆盖(#34345), capturer 缓冲内存上升, 容量契约依赖 allocator 时序, 新增跨对象依赖
关联脉络
- PR #34345 Follow-up: use GPU-resident capacity in TpModelWorker.max_req_len and Scheduler.init_req_max_new_tokens (comment-referenced): daii-0818 在 issue 评论中明确表示该 PR 是本 PR 的跟进,修复下游仍使用 GPU 驻留容量的两处遗漏点,并附带 CPU 回归测试。
- PR #33484 perf(hisparse): fuse the DSv4 value and scale swap-in copy on ROCm: 同为 HiSparse 功能线,聚焦 swap-in 数据通路性能,与本 PR 的逻辑容量语义互补,共同完善 HiSparse 长序列支持。
- PR #33085 perf(hisparse): 128-bit non-temporal swap-in copy on ROCm: HiSparse 数据通路优化的早期 PR,说明 HiSparse 主从交换路径持续演进,本 PR 补齐其容量准入语义。
参与讨论