Prhub

#34160 Revert parallel request lifecycle tracking from #32588

原始 PR 作者 merrymercy 合并时间 2026-08-09 18:10 文件变更 5 提交数 2 评论 10 代码增减 +59 / -424

执行摘要

回退 #32588 请求生命周期跟踪,恢复旧 RID 流程

PR body 明确指出:lifecycle 变更引入了双向映射、per-request 生命周期 token、dispatch 标志和贯穿 tokenization 的 abort 检查点,大部分复杂度仅用于在并行采样使用内部 child RID 时保留调用方可感知的 logical RID。旧有的展开式 RID 行为本质上更简单,因此回退;若未来需要稳定的分组取消,应作为显式 request-group 标识通过 scheduler IPC 传递,而不是嵌入 TokenizerManager 的状态所有权中。

值得精读,尤其是对 sglang 请求入口和 TokenizerManager 状态管理感兴趣的同学。这个 revert 展示了“在复杂度与功能之间做收敛”的工程决策——当一项特性的维护成本超过其收益时,果断回退并保留可独立复用的部分。重点观察 _normalize_rid 的展开式逻辑、abort_request 的简化,以及 BaseException 保留清理路径的设计取舍。

讨论亮点

review 中的核心讨论:

  • alexnails 对 _normalize_rid(num) 提出两个疑问:“we accidentally will have extra ReqState objects here?”和“do we break any format here?”。merrymercy 回应“no, this pr is a revert so i believe it does not make things worse”,即回退不会比现状更差。
  • alexnails 指出 except BaseException 的语义:“we may have to check BaseException vs Exception behavior as BaseException is lower than Exception”。merrymercy 同意并在 commit 2779f59 中保留 BaseException,明确“cancellation and generator-exit paths still clean up request state”。
  • alexnails 对 _init_req_state 的逐个插入校验表示担忧:“I think we can loop existing here which will cause state issues?”(指批量重复 RID 可能留下部分状态);merrymercy 反问“what is existing?”,该顾虑未完全闭合,但作为回退未进一步扩大改动。

实现拆解

实现拆解如下:

  1. 恢复 GenerateReqInput 的展开式 RID 归一化python/sglang/srt/managers/io_struct.py):_normalize_rid(num) 改为按展开后的数量 num 生成或扩展 RID(字符串前缀直接生成 rid_0rid_1 …),删除原来“每原始 batch item 一个 logical RID”的逻辑;regenerate_rid() 去掉 prefix 参数,直接生成 uuid;__getitem__ 不再按 i % batch_size 回退到 logical RID,而是直接用 self.rid[i]

  2. 移除 TokenizerManager 生命周期跟踪python/sglang/srt/managers/tokenizer_manager.py):删除 RequestAbortedError 类、ReqState 上的 abort_requested/lifecycle_id/dispatched 字段、logical_rid_to_child_ridschild_rid_to_logical_rid 双映射,以及 _register_child_rid_init_child_req_state_remove_req_state_raise_if_logical_rid_aborted 等方法;generate_request_handle_batch_request 中的多处 abort 检查点删除。

  3. 简化 abort 路由与状态清理abort_request 恢复为旧版——单 tokenizer 且 RID 不在 rid_to_state 时直接返回,不再 fan-out 到 child RID;_handle_abort_finish_reason 直接 del self.rid_to_state[...]_discard_pending_req_states 不再接收 lifecycle_ids,也不再区分是否已 dispatched。

  4. 保留 #32588 的独立部分:gRPC 生成控制(seed、priority、reasoning、guided decoding、max thinking tokens)、strict-thinking 校验,以及 batch response 任务清理(取消并关闭 sibling waiters)完整保留。

  5. 同步调整测试:删除 test_tokenizer_manager_rid_cleanup.py 中的 lifecycle 专属用例(如并行子请求清理、stale cleanup、TestParallelAbortRouting、TestParallelRidReuse),将 test_io_struct.py 断言改为展开式 RID;test_gpu_feature_transport.py 移除对 state.dispatched 的断言。

文件 模块 状态 重要度
python/sglang/srt/managers/tokenizer_manager.py 请求管理器 modified 8.84
python/sglang/srt/managers/io_struct.py 请求结构 modified 7.75
test/registered/unit/managers/test_tokenizer_manager_rid_cleanup.py 清理测试 modified 7.09
test/registered/unit/managers/test_io_struct.py 结构测试 modified 5.79
test/registered/unit/multimodal/test_gpu_feature_transport.py GPU 传输 modified 3.84

关键符号

generate_request regenerate_rid _normalize_rid _init_req_state _discard_pending_req_states abort_request _handle_batch_request _handle_abort_finish_reason

关键源码片段

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

核心请求状态管理:移除 lifecycle 双映射、RequestAbortedError、ReqState 扩展字段和大量 abort 检查点,恢复旧状态清理流程,是本次回退的主体。

# 回退后的 generate_request 主流程:恢复直接 _init_req_state,
# 去掉贯穿 tokenization 的 lifecycle abort 检查点
async def generate_request(self, obj, request=None):
    self.auto_create_handle_loop()
​
    # Normalize the request
    obj.normalize_batch_and_arguments()
    self._set_default_priority(obj)
    if (
        isinstance(obj, GenerateReqInput)
        and obj.max_thinking_tokens is not None
        and not self.server_args.enable_strict_thinking
    ):
        # max_thinking_tokens 校验属 #32588 独立部分,回退后保留
        raise ValueError(
            "max_thinking_tokens requires the server to be launched with "
            "--enable-strict-thinking"
        )
​
    self._init_req_state(obj, request)
    try:
        if self.server_args.language_only:
            self._handle_epd_disaggregation_encode_request(obj)
        self.request_logger.log_received_request(obj, self.tokenizer, request)
​
        async with self.is_pause_cond:
            await self.is_pause_cond.wait_for(lambda: not self.is_pause)
​
        async with self.model_update_lock.reader_lock:
            await self._validate_and_resolve_lora(obj)
            if obj.is_single:
                tokenized_obj = await self._tokenize_one_request(obj)
                state = self.rid_to_state[obj.rid]
                if obj.return_prompt_token_ids:
                    state.prompt_token_ids = list(tokenized_obj.input_ids)
                self._send_one_request(tokenized_obj)
                async for response in self._wait_one_response(obj, request):
                    yield response
            else:
                async for response in self._handle_batch_request(obj, request):
                    yield response
    except BaseException:
        # 保留 BaseException(而非 Exception),确保取消和 generator-exit
        # 路径也能清理未到达调度器的 pending 状态,避免 rid_to_state 泄漏
        self._discard_pending_req_states(obj)
        raise
python/sglang/srt/managers/io_struct.py core-logic

RID 归一化逻辑回退:_normalize_rid 按展开数量生成 RID,regenerate_rid 去掉 prefix,__getitem__ 直接索引,是调用方可感知行为变化的主要来源。

# 回退后的 RID 归一化:并行采样展开后每个子请求都有独立 RIDdef regenerate_rid(self):
    """Generate a new request ID and return it."""
    if isinstance(self.rid, list):
        # 列表场景:为每个元素重新生成 uuid
        self.rid = [uuid.uuid4().hex for _ in range(len(self.rid))]
    else:
        self.rid = uuid.uuid4().hex
    return self.riddef _normalize_rid(self, num):
    """Normalize request IDs for batch processing."""
    if self.rid is None:
        # 未指定 rid 时按展开后的总数量 num 生成(含并行采样副本)
        self.rid = [uuid.uuid4().hex for _ in range(num)]
    elif isinstance(self.rid, str):
        # 字符串 rid 作为前缀,展开为 rid_0, rid_1, ...
        self.rid = [f"{self.rid}_{i}" for i in range(num)]
    elif isinstance(self.rid, list):
        # 列表长度必须等于原始 batch_size,并行采样展开由 tokenizer_manager 完成
        if len(self.rid) != self.batch_size:
            raise ValueError(
                "The specified rids length mismatch with the batch_size for batch processing."
            )
    else:
        raise ValueError("The rid should be a string or a list of strings.")

评论区精华

_normalize_rid(num) 是否产生额外 ReqState question

alexnails 问“we accidentally will have extra ReqState objects here?”,merrymercy 反问“when will num be a wrong number?”,未直接给出明确论证。

结论:未完全闭合,但 merrymercy 认为作为 revert 不会让情况更糟;实际展开数量与后续 _init_req_state 调用一致。 · 已解决

回退是否破坏 RID 格式 question

alexnails 问“do we break any format here?”,merrymercy 回答“no, this pr is a revert so i believe it does not make things worse”。

结论:接受回退不引入新格式破坏;但恢复展开式 RID 本身是相对 #32588 的客户端可见变化。 · 已解决

except BaseException 语义 正确性

alexnails 指出“BaseException is lower than Exception”,提醒异常范围的正确性;merrymercy 同意并修复。

结论:commit 2779f59 保留 BaseException,确保取消与 generator-exit 路径仍清理请求状态。 · 已解决

_init_req_state 逐个插入的重复 RID 状态问题 正确性

alexnails 担心“I think we can loop `existing` here which will cause state issues?”,merrymercy 反问“what is `existing`?”,未深入展开。

结论:未解决;回退后批量重复 RID 检查失去 preflight 原子性,对应测试用例被删除,存在遗留风险。 · unresolved

风险与影响

风险集中在以下方面:

  1. 并行采样 abort 行为变化:恢复展开式 RID 后,abort_request 不再将父 RID 的取消 fan-out 到子请求。依赖旧行为的客户端(通过一个 logical RID 取消所有并行样本)可能只能取消部分子请求,需要确认调度器侧的 abort 传播是否覆盖。
  2. 批量重复 RID 检查的原子性退化_init_req_state 从“先全量 preflight 校验再插入”退化为逐个插入时检查,批处理中途遇到重复 RID 可能留下已插入的部分状态。test_batch_duplicate_preflight_does_not_insert_partial_state 被删除,覆盖消失。
  3. 测试验证缺口:PR body 说明本地环境 transformers/xgrammar 版本不兼容,runtime 测试未能本地执行,只能依赖 CI。test_gpu_feature_transport.py 移除 dispatched 断言后,CUDA VMM 传输失败路径与请求状态的一致性验证变弱。
  4. 核心路径变更回归tokenizer_manager.py 是请求入口核心,净删 219 行属于大面积控制流调整,任何遗漏的调用点(如 speculative decoding 或 multimodal 传输)都可能引用已删除符号导致运行时错误。

影响范围:

  • 调用方可见行为:并行采样请求的 RID 从共享 logical RID 变为展开后的独立 RID(如 single_0single_1…),依赖 RID 做日志、追踪或取消的用户需要适配新格式;regenerate_rid(prefix=...) 的调用方(内部并行采样路径)需要改为无参调用。
  • 系统内部:TokenizerManager 的 rid_to_state 规模与展开后请求数一致,双映射和生命周期 token 移除后内存占用降低;abort_request 路由更简单,但丢失分组取消能力。
  • 团队维护:代码复杂度显著下降(净删 424 行),后续维护成本降低;同时重新引入旧展开式 RID 的既有债务,未来若实现分组取消需按 PR body 建议走显式 request-group 标识。
核心路径变更 恢复旧行为 并行采样 abort 行为变化 缺少本地运行测试 批量重复 RID 原子性退化

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论