Prhub

#27610 Avoid scattered assignment of extend_input_len and fill_len by merging them into Req.extend_range

原始 PR 作者 fzyzcjy 合并时间 2026-06-25 08:33 文件变更 14 提交数 5 评论 2 代码增减 +107 / -66

执行摘要

合并 extend_input_len/fill_len 为 Req.extend_range,消除分散赋值

原实现中 extend 范围被拆分为两个独立赋值的字段,且 start 从未显式存储,通过 len(prefix_indices) + extend_input_len 重建绝对位置,在 Disagg decode + HiCache(committed prefix 不等于 len(prefix_indices))以及 Mixed-batch running reqs(decode 步骤的 KV 未折叠进 prefix_indices)两种场景下会得出错误值。

值得精读。该 PR 展示了如何通过强类型消除隐式语义二义性,是 Req 数据结构走向更不可变、更能防御错误的良好模式。推荐跟进后续 PR #27611(内联 property)和 #27616(先决条件)了解完整演进链。

讨论亮点

唯一有人参与的讨论是作者自己在 CI 下游 PR #27611 中发现 cache_unfinished_req 的断言在 extend_range is None 时崩溃,随后在该 PR 中修复并提及。未出现设计层面的争论。

实现拆解

  1. 定义 Range 类型:在 python/sglang/srt/utils/common.py 新增 Range(NamedTuple),包含 startend 字段和只读 length 属性。
  2. 改造 Req 核心字段:在 python/sglang/srt/managers/schedule_batch.py 中将 self.fill_len: intself.extend_input_len: int 替换为 self.extend_range: Optional[Range] = None;添加 dllm_initialized: bool = False 用于 DLLM 首次轮标志。将旧字段改为 @property 只读属性,派生自 extend_range;新增 set_extend_range(start, end) 统一设置 range 并自动重算 extend_logprob_start_len
  3. 替换所有写入点:在 schedule_policy.pyscheduler_pp_mixin.pydisaggregation/decode.pyscheduler.pydllm/mixin/req.py 等 6 个源文件中,将原来分散的 set_extend_input_len + fill_len = 配对改为一条 set_extend_range(start, end)。DLLM 部分的 _init_fill_ids_for_dllm 不再写 fill_len 而是设置 dllm_initialized = True
  4. 同步测试适配:修改了 test_hisparse_unit.py(新增 _FakeReqextend_range 模拟 property)、test_decode_radix_lock_ref.pytest_prefill_adder.pytest_unified_radix_cache_unittest.pytest_scheduler_chunked_req_gate.py 共 5 个测试文件,使其 mock 或直接适配新接口。
  5. 修复后续发现的 crash:在 scheduler.pycache_unfinished_req 断言中添加 extend_range is None 守卫,避免 decode 阶段 range 为 None 时触发属性访问异常。
文件 模块 状态 重要度
python/sglang/srt/managers/schedule_batch.py 调度器 modified 7.8
python/sglang/srt/utils/common.py 通用工具 modified 6.37
test/registered/unit/managers/test_hisparse_unit.py 测试 modified 6.15

关键符号

Range.length Req.fill_len Req.extend_input_len Req.set_extend_range Req._recompute_extend_logprob_start_len

关键源码片段

python/sglang/srt/managers/schedule_batch.py core-logic

核心数据结构 Req 的改动所在,定义新字段、property、set_extend_range 和日志逻辑调整。

# python/sglang/srt/managers/schedule_batch.py (partial)# 旧字段被替换为单一 extend_range
self.extend_range: Optional[Range] = None # None 表示 decode/idle/retract 状态
self.dllm_initialized: bool = False # 替代旧的 fill_len is None 作为 DLLM 首次轮标志# 保留原属性名作为只读 property,确保所有读端无需改动
@property
def fill_len(self) -> int:
    return self.extend_range.end@property
def extend_input_len(self) -> int:
    return self.extend_range.lengthdef set_extend_range(self, start: int, end: int) -> None:
    """统一设置 extend 范围,自动更新 logprob 偏移。"""
    self.extend_range = Range(start, end)
    self._recompute_extend_logprob_start_len()def _recompute_extend_logprob_start_len(self):
    """根据 extend_range 和 logprob_start_len 计算相对偏移。"""
    if self.logprob_start_len == -1:
        logprob_start_len = len(self.full_untruncated_fill_ids)
    else:
        logprob_start_len = self.logprob_start_len
    self.extend_logprob_start_len = logprob_start_len - self.extend_range.start
test/registered/unit/managers/test_hisparse_unit.py test-coverage

测试文件中的 mock Req 改为基于 extend_range 的 property,验证新接口在 HiSparse 路径下的正确性。

# test/registered/unit/managers/test_hisparse_unit.py (partial)from sglang.srt.utils.common import Rangeclass _FakeReq(SimpleNamespace):
    """模拟 Req 的 extend_range 属性,用于单元测试。"""
    @property
    def fill_len(self) -> int:
        return self.extend_range.end
​
    @property
    def extend_input_len(self) -> int:
        return self.extend_range.lengthdef _make_req(rid="test-req-0", origin_input_ids=None, output_ids=None):
    # ... 原本使用 SimpleNamespace + set_extend_input_len lambda
    req = _FakeReq(
        rid=rid,
        # ...
    )
    req.set_extend_range = lambda start, end: setattr(req, "extend_range", Range(start, end))
    return req

评论区精华

cache_unfinished_req 断言在 extend_range is None 时 crash 正确性

作者在 CI 下游 PR #27611 中发现了该 crash,并在此 PR 中修复:在 cache_unfinished_req 的 8 个 sanity assert 位置添加 extend_range is None 的守卫条件。

结论:已修复:decode 等非 extend 状态下不再因属性访问而异常。 · 已解决

风险与影响

  1. 行为保持假设风险:所有写入点都按 start = end - old_length 计算,但若存在未覆盖的写入路径(如分叉分支或后续新增代码),可能导致 range 不一致。
  2. DLLM 标志替换风险dllm_initialized 完全替代了旧的 fill_len is None 检查,如果任一生命周期路径未设置该标志(如极端 retract 场景),可能导致阶段判断错误。
  3. 测试覆盖风险:作者注明未手动运行测试,仅依赖 CI;测试覆盖可能不充分(尤其 HiCache 和 mixed-batch 组合场景)。

内部接口重构:影响所有涉及 extend 范围赋值的调度路径,但 property 保持读接口兼容,外部模块无感知。DLLM 模块:首次轮标志改变,可能与其他依赖 fill_len 的遗存代码冲突。测试:五处测试适配,需确保 mock 行为与真实 Req 一致。影响程度中等,因为改动范围集中但全局调度依赖该数据结构。

核心路径变更 DLLM 初始化标志替换 行为保持假设 测试覆盖可能不足

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论