Prhub

#29882 fix: populate batch req rids and per-request http_worker_ipc for mult…

原始 PR 作者 whybeyoung 合并时间 2026-07-05 03:09 文件变更 3 提交数 4 评论 10 代码增减 +48 / -2

执行摘要

修复 multi-tokenizer 模式下 batch input_ids 路由 hang 住

Issue #29878 报告 multi-tokenizer 模式启动错误,请求 hang 住。该 bug 由 PR #29214 引入,该 PR 添加了 multi-tokenizer 支持,但 batch tokenized 路径遗漏了 routing 信息的填充,导致 scheduler 无法将结果返回给正确的 tokenizer worker。

该 PR 修复了一个重要回归,且实现简洁清晰,值得所有涉及 IPC 路由的开发者阅读。特别是 stamp_http_worker_ipc 的设计和 schema 注释的澄清,展示了 batch 与单请求路由的差异。

讨论亮点
  1. 不要使用 getattr:reviewer merrymercy 指出初始实现使用了 getattr(obj, "batch", None) 来检查 batch 属性,违反了仓库 .claude/rules/no-getattr-defensive.md 规范,建议改用 isinstance 分支。最终代码直接对 BatchTokenizedGenerateReqInputBatchTokenizedEmbeddingReqInput 进行 isinstance 检查。
  2. 避免冗余:merrymercy 质疑为什么需要单独为子请求设置 http_worker_ipc,因为 BaseBatchReq 已经有 http_worker_ipcs 列表。作者解释 scheduler 解包 batch 后按单请求处理,必须靠子请求自己的 http_worker_ipc 字段路由,否则响应无法返回。最终达成一致,并完善了 io_struct.py 的注释以澄清两种路由方式的区别。

实现拆解

  1. python/sglang/srt/managers/tokenizer_manager.pystamp_http_worker_ipc 函数中增加分支:当对象是 BatchTokenizedGenerateReqInputBatchTokenizedEmbeddingReqInput 时,遍历其 batch 列表,为每个子请求设置 http_worker_ipc。这是因为 scheduler 会解包 batch 并读取每个子请求的 http_worker_ipc 来路由响应,而不是使用父对象的 http_worker_ipcs 列表。
  2. python/sglang/srt/managers/io_struct.pyBaseBatchReq 类中补充注释,说明 http_worker_ipcs 字段仅用于 scheduler 输出等并行数组消息,而 tokenized 输入批量的路由信息存储在 batch[i].http_worker_ipc。同时在 BatchTokenizedGenerateReqInputBatchTokenizedEmbeddingReqInput 类中增加注释,明确路由方式。
  3. test/registered/tokenizer/test_multi_tokenizer.py 中添加 test_batch_input_ids_routing 测试用例,模拟发送一批 pre-tokenized input_ids(无文本),验证所有请求都能正常返回响应,而不会 hang 住。同时调整了 CI 测试的预估时间。
文件 模块 状态 重要度
python/sglang/srt/managers/tokenizer_manager.py IPC 路由 modified 5.7
python/sglang/srt/managers/io_struct.py 数据结构 modified 4.12
test/registered/tokenizer/test_multi_tokenizer.py 测试 modified 5.12

关键符号

stamp_http_worker_ipc test_batch_input_ids_routing

关键源码片段

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

核心逻辑修复:在 stamp_http_worker_ipc 中增加对 BatchTokenizedGenerateReqInput 和 BatchTokenizedEmbeddingReqInput 的处理,为每个子请求设置 http_worker_ipc。

def stamp_http_worker_ipc(obj: Any, ipc_name: str) -> None:
    # 单请求:直接设置 http_worker_ipc
    if isinstance(obj, BaseReq):
        obj.http_worker_ipc = ipc_name
    # 批量 tokenized 输入:scheduler 会解包 batch 并读取每个子请求的
    # http_worker_ipc 来路由响应,因此必须逐个设置
    elif isinstance(
        obj, (BatchTokenizedGenerateReqInput, BatchTokenizedEmbeddingReqInput)
    ):
        for req in obj:
            req.http_worker_ipc = ipc_name
    # 其他批量消息(如 scheduler 输出):使用 http_worker_ipcs 列表
    elif isinstance(obj, BaseBatchReq):
        obj.http_worker_ipcs = [ipc_name] * len(obj.rids)
python/sglang/srt/managers/io_struct.py documentation

数据结构和注释更新:在 BaseBatchReq 和 BatchTokenized* 类中添加注释,澄清路由方式,避免未来误用。

class BaseBatchReq(msgspec.Struct, tag=True, kw_only=True, array_like=True):
    # ...
    # Used by batch messages whose items are parallel arrays, such as scheduler
    # outputs. Tokenized input batches store routing on batch[i].http_worker_ipc
    # because the scheduler unpacks them into single-request handlers.
    http_worker_ipcs: Optional[List[Optional[str]]] = Noneclass BatchTokenizedGenerateReqInput(BaseBatchReq, kw_only=True):
    # The batch of tokenized requests
    # Routing for request i is batch[i].http_worker_ipc, not http_worker_ipcs[i].
    batch: List[TokenizedGenerateReqInput]class BatchTokenizedEmbeddingReqInput(BaseBatchReq, kw_only=True):
    # Routing for request i is batch[i].http_worker_ipc, not http_worker_ipcs[i].
    batch: List[TokenizedEmbeddingReqInput]
test/registered/tokenizer/test_multi_tokenizer.py test-coverage

添加回归测试 test_batch_input_ids_routing,覆盖 pre-tokenized batch input_ids 路径,确保多 tokenizer 模式下路由正确。

def test_batch_input_ids_routing(self):
    # Regression guard for sgl-project/sglang#29878 (introduced by #29214).
    #
    # A batch of pre-tokenized `input_ids` (no text / multimodal) is the one
    # case that takes the batch-tokenization path (_send_batch_request ->
    # BatchTokenizedGenerateReqInput). In multi-tokenizer mode this batch
    # must stamp each sub-request's `http_worker_ipc` so the scheduler can
    # route every reply back to its owning tokenizer worker. If it is missing,
    # the requests hang forever.
    #
    # The existing ttft test only sends *text*, so it never exercises this
    # path — this case does, and uses a short timeout so a routing hang
    # fails fast instead of stalling until the server launch timeout.
    batch_input_ids = [
        [1, 2, 3, 4, 5],
        [10, 11, 12, 13, 14],
        [20, 21, 22, 23, 24],
        [30, 31, 32, 33, 34],
    ]
    response = requests.post(
        self.base_url + "/generate",
        json={
            "input_ids": batch_input_ids,
            "sampling_params": {"max_new_tokens": 8, "temperature": 0},
        },
        timeout=60,
    )
    self.assertEqual(response.status_code, 200, response.text)
    results = response.json()
    # Every batched request must get its reply routed back — not hang.
    self.assertEqual(len(results), len(batch_input_ids))
    for result in results:
        self.assertIn("text", result)

评论区精华

禁止使用 getattr 作为防御性检查 style

merrymercy 评论:'do not use getattr' 并引用了仓库规则文件 no-getattr-defensive.md,指示应该使用 isinstance 而不是 getattr 来检查对象是否有 batch 属性。

结论:作者将 getattr 实现改为明确的 isinstance 分支,符合仓库规范。 · 已解决

是否冗余:已有 http_worker_ipcs 字段 设计

merrymercy 询问为什么需要单独为子请求设置 http_worker_ipc,因为 BaseBatchReq 已经有 http_worker_ipcs 列表。作者解释 scheduler 解包 batch 后按单请求处理,必须靠子请求自己的 http_worker_ipc 字段路由。

结论:达成一致,并在 io_struct.py 中添加注释澄清两种路由方式,避免冗余设计。最终代码避免了同时设置 http_worker_ipcs 和子请求的 http_worker_ipc(只设置子请求的)。 · 已解决

风险与影响

变更集中在 tokenizer IPC 路由的核心路径,虽然改动量小,但若其他 batch 路径(如未遍历到的子类)未正确处理,可能导致类似 hang 问题。不过当前已通过 isinstance 覆盖了所有已知批量 tokenized 输入。测试用例覆盖了关键场景,但未覆盖 embedding 路径(BatchTokenizedEmbeddingReqInput),存在一定遗漏风险。此外,注释变更无风险。

直接影响使用 --tokenizer-worker-num 选项启动 multi-tokenizer 模式的用户,修复了请求 hang 问题。其他模式无影响。测试用例的增加提高了 CI 对此类回归的检测能力。

核心路径变更 多进程通信 回归风险(其他 batch 路径未覆盖)

关联 Issue

#29878 [Bug] multi-tokenizer mode startup error.

完整报告

参与讨论