Prhub

#33565 [Fix] Restore data_parallel_rank alias on native /generate (dp-aware gateway routing is silently dropped)

原始 PR 作者 sshleifer 合并时间 2026-08-08 14:59 文件变更 2 提交数 2 评论 5 代码增减 +41 / -0

执行摘要

恢复 /generate 的 data_parallel_rank 别名,修复 dp 路由静默失效

sgl-model-gateway 的 dp-aware 模式在 DPAwareWorker::prepare_request 中向每个转发请求注入 data_parallel_rank 字段(其单元测试也断言该拼写),但 #29214 删除了 GenerateReqInput 上的该别名,而 OpenAI 兼容层的 _migrate_deprecated_dp_rank 与 Engine.generate() 仍保留迁移逻辑,原生 /generate 成为唯一丢失别名的入口。由于 FastAPI 按 pydantic dataclass 解析并忽略未知键,失败完全静默;PR body 原话:'dp-aware routing over native /generate is currently a silent no-op.'。问题仅以性能退化形式暴露:生产环境前缀缓存命中率约 0.49(路由被丢弃)vs 约 0.97(路由生效),agentic 工作负载 decode 吞吐差约 2 倍,且难以察觉和 bisect。

值得精读。代码改动极小(41 行),但 PR body 呈现了教科书式的“静默失败根因分析 + 兼容矩阵 + 选项空间”论证,适合在跨组件契约维护中借鉴。可重点关注两点:一是 normalize_batch_and_arguments() 作为归一化喉点的选择理由(覆盖所有构造路径、与 #19268 原始映射一致);二是作者关于 gateway 在弃用窗口内双发字段的建议是否已作为 follow-up 落实。

讨论亮点

本 PR 没有 review 评论(ispobock 直接 APPROVED),核心讨论集中在 PR body 的 "Alternatives considered" 与 CI 交互中:

  • 作者用兼容矩阵论证了为什么不能只修 gateway:"The gateway/router is released and deployed separately from the server, so every already-shipped gateway keeps emitting the old name... The server-side alias repairs all released gateway versions at once on the next server upgrade." 发布节奏解耦决定了兼容层必须放在接收侧。
  • 对严格解析方案(拒绝未知字段)的评价:"Strict parsing would have turned this silent no-op into a loud 422 and would catch future drift... It's a breaking change for clients that send extra keys, so out of scope here." 方案有价值但属破坏性变更,建议单独讨论。
  • 作者主动提出复发守卫:"A cheap recurrence guard would be a test asserting that every key DPAwareWorker::prepare_request injects is accepted by GenerateReqInput",并表明愿意补上,但本 PR 未落地。
  • CI 侧,ispobock 通过 /rerun-test 重跑 test_io_struct.py,github-actions 报告 1-gpu-h100 上 30 个测试全部通过。

实现拆解

整个修复分三步:

  1. 恢复字段声明:在 python/sglang/srt/managers/io_struct.pyGenerateReqInput 中,紧挨 routed_dp_rank 之后新增 data_parallel_rank: Optional[int] = None,注释明确其来源(sgl-model-gateway dp-aware 模式注入、OpenAI 入口与 Engine.generate() 仍接受该拼写),使 pydantic 不再静默丢弃该键。

  2. 接入归一化喉点:在 normalize_batch_and_arguments() 方法开头新增兼容分支——先发 DeprecationWarning;仅当 routed_dp_rank 为 None 时把旧值回填过去;随后将 data_parallel_rank 清空为 None,保证 batch 分裂(GenerateReqInput.__getitem__)产生的子项不携带冗余旧字段。选择该函数而非 HTTP 边界,是因为它是 #19268 原始映射所在位置,且能覆盖 /generate、Vertex 端点、Engine API 等所有构造路径的既有归一化喉点。

  3. 测试配套:在 test/registered/unit/managers/test_io_struct.py 新增 3 个用例,分别覆盖别名映射到 routed_dp_rank、别名不覆盖显式 routed_dp_rank、别名传播到 batch 子项;本地 30 个测试通过,CI 在 1-gpu-h100 上重跑也全部通过。

文件 模块 状态 重要度
python/sglang/srt/managers/io_struct.py 请求结构 modified 6.25
test/registered/unit/managers/test_io_struct.py 请求结构 modified 5.81

关键符号

normalize_batch_and_arguments test_data_parallel_rank_alias_maps_to_routed_dp_rank test_data_parallel_rank_alias_does_not_override_routed_dp_rank test_data_parallel_rank_alias_propagates_to_batch_items

关键源码片段

python/sglang/srt/managers/io_struct.py dependency-wiring

核心修复文件:恢复 GenerateReqInput.data_parallel_rank 废弃字段,并在 normalize_batch_and_arguments() 中加入映射逻辑,这是修复静默丢弃路由字段的关键。

# python/sglang/srt/managers/io_struct.py
# GenerateReqInput 以 pydantic dataclass 形式被 FastAPI 解析,
# 未知键会被静默丢弃——这正是本次 bug 的根源:gateway 注入的
# data_parallel_rank 若不在此声明,路由决策就无声无息地消失。class GenerateReqInput:
    # For DP routing —— 外部 router 指定具体 DP worker
    routed_dp_rank: Optional[int] = None
​
    # Deprecated alias for routed_dp_rank,仍被接受,因为
    # sgl-model-gateway 的 dp-aware 模式在每个转发的请求中注入该拼写
    # (DPAwareWorker::prepare_request),OpenAI 入口与
    # Engine.generate() 也仍接受它。
    data_parallel_rank: Optional[int] = None
​
    def normalize_batch_and_arguments(self):
        """归一化 batch 与参数:映射废弃字段、校验输入、解析并行采样。"""
        # 兼容层:旧 gateway 的 data_parallel_rank 在此映射到
        # routed_dp_rank;显式给定的 routed_dp_rank 优先,避免
        # 覆盖新客户端的路由意图。
        if self.data_parallel_rank is not None:
            import warnings
​
            warnings.warn(
                "'data_parallel_rank' is deprecated, use 'routed_dp_rank' instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            if self.routed_dp_rank is None:
                self.routed_dp_rank = self.data_parallel_rank
            # 归一化后清空旧字段,保证后续 batch 分裂
            # (GenerateReqInput.__getitem__)产生的子项不携带冗余字段
            self.data_parallel_rank = None
​
        self._validate_inputs()
        self._determine_batch_size()
        # ... 后续校验与并行采样展开逻辑不变
test/registered/unit/managers/test_io_struct.py test-coverage

测试配套:3 个新用例锁定兼容层契约(映射、优先级、batch 传播),防止该别名再次被静默移除。

# test/registered/unit/managers/test_io_struct.py
# 新增的 3 个用例覆盖兼容层的契约:映射、优先级、batch 传播def test_data_parallel_rank_alias_maps_to_routed_dp_rank(self):
    # 旧拼写必须被接受并映射到新字段
    req = GenerateReqInput(text="Hello", sampling_params={}, data_parallel_rank=2)
    req.normalize_batch_and_arguments()
    self.assertEqual(req.routed_dp_rank, 2)
    self.assertIsNone(req.data_parallel_rank)def test_data_parallel_rank_alias_does_not_override_routed_dp_rank(self):
    # 两个字段同时出现时,显式 routed_dp_rank 必须胜出
    req = GenerateReqInput(
        text="Hello", sampling_params={}, data_parallel_rank=2, routed_dp_rank=1
    )
    req.normalize_batch_and_arguments()
    self.assertEqual(req.routed_dp_rank, 1)def test_data_parallel_rank_alias_propagates_to_batch_items(self):
    # batch 请求的每个子项都应继承映射后的路由 rank
    req = GenerateReqInput(
        text=["Hello", "World"],
        sampling_params=[{}, {}],
        rid=["id1", "id2"],
        data_parallel_rank=3,
    )
    req.normalize_batch_and_arguments()
    self.assertEqual(req[0].routed_dp_rank, 3)
    self.assertEqual(req[1].routed_dp_rank, 3)

评论区精华

备选方案权衡:服务器端别名 vs 修 gateway vs 严格解析 设计

PR body 详细列出 4 个选项:仅修 gateway 因发布节奏解耦而不充分;HTTP 边界映射覆盖不全(Vertex/engine 路径);严格解析可把静默失败变 422 但属破坏性变更;什么都不做则所有已部署 gateway 继续静默失效。

结论:选择恢复 GenerateReqInput 别名并接入 normalize_batch_and_arguments(),一次修复所有已发布 gateway;gateway 双发字段作为 follow-up 建议提出,尚未落地。 · 待处理

test_io_struct.py CI 重跑 测试

ispobock 通过 /rerun-test 命令重跑 test/registered/unit/managers/test_io_struct.py;github-actions 报告 1-gpu-h100 上 30 个测试全部通过。

结论:测试在单卡 H100 环境全部通过,随后 PR 获 APPROVED 并合并。 · 已解决

Gemini Code Assist 审查告警 other

gemini-code-assist[bot] 两次提示其 GitHub 消费者版本已 sunset,代码审查活动正式停止。

结论:无实际审查产出,不影响本 PR。 · 已解决

风险与影响

  • 兼容回归风险极低:纯增量恢复,显式 routed_dp_rank 优先,映射后清空旧字段,不会覆盖新客户端意图。
  • 静默失败根因未根治:除 data_parallel_rank 外,其他未知字段仍会被 pydantic dataclass 悄悄忽略;严格解析方案被推迟,gateway 未来新增注入字段仍可能无提示丢失。
  • 长期维护负担:这是又一层 DeprecationWarning 兼容代码;将来移除别名必须等 gateway 新版本先发布,否则会重演 #29214 的顺序错误(PR body 明确指出这一点)。
  • 无性能影响:归一化阶段仅多一次字段判断。
  • 用户影响:dp-aware gateway 配合原生 /generate 的部署恢复预期路由行为,session-keyed 手动路由不再退化为负载均衡。
  • 系统影响:作者生产实测前缀缓存命中率从约 0.49 回升到约 0.97,decode 吞吐约 2 倍提升。
  • 团队影响:这是一次典型的跨仓库契约漂移事故,暴露了 Rust gateway 与 Python 服务器之间缺乏“注入键↔接受字段”的契约测试;作者建议的复发守卫尚未落地,值得跟进。
静默降级已修复 跨组件契约漂移 兼容层维护负担 严格解析待评估

关联 Issue

#29214 [Cleanup] IPC struct renames, better typing, and SenderWrapper removal

完整报告

参与讨论