Prhub

#36896 config: the resolution pipeline's dispatcher leaves the record

原始 PR 作者 ch-wan 合并时间 2026-08-29 19:16 文件变更 7 提交数 1 评论 1 代码增减 +432 / -456

执行摘要

解析调度器迁出 ServerArgs 至 arg_groups 包

PR body 明确指出:_run_resolution_pipeline was the last piece of resolution living on ServerArgs: 331 lines of ordered dispatch, plus the four handler methods it still reached. Every step it calls already lives in arg_groups/, and none of them is a member of the record — a step decides about the record, it is not part of it. Keeping the dispatcher on the class also kept the awkward direction of travel: a hook in arg_groups/ was called by a method of the very object it was extracted from. 也就是说,解析步骤的职责已经在 arg_groups/ 家族,但调度器还留在巨型类 ServerArgs 上,形成了“被提取对象反向调用提取者”的别扭依赖方向,本次变更就是要斩断这条反向依赖。

值得精读,特别是 arg_groups/pipeline.py 的 dispatcher 风格原则和测试 guard 的 AST 扫描设计。作者对“每步决定关于 record 而不是 record 的一部分”的职责边界论证清晰,且用测试把架构不变量固化下来,是配置类代码重构的一个好范例。建议结合系列 PR #36972-#36975 一起阅读,能完整看到 ServerArgs 从“上帝类”逐步收缩为纯字段记录的过程。

讨论亮点

本 PR 没有人工 review 评论,仅 Codex 自动审查完成(状态为 Completed)。PR body 中作者自述了两个值得注意的设计点:1)平台插件测试的命名空间 seam——dispatcher 移动后 current_platform 的 patch 目标必须跟着移动到 arg_groups.pipeline,否则插件默认值会静默失效;2)测试 guard 从“handler 不能读 self 字段”升级为“record 不能持有解析 handler”,因为前者逻辑上已无存在对象。作者强调整个迁移“mostly mechanical”,77 个调用顺序不变,MLX 校验仍在 dummy 返回前执行。

实现拆解

变更入口

ServerArgs.resolve_once 是唯一调用入口:它继续负责 resolved/failed 状态门控,但在 try 块中改调 arg_groups.pipeline.run_resolution_pipeline(self)

实施步骤

  1. 新建调度器模块:新增 python/sglang/srt/arg_groups/pipeline.py,把原 _run_resolution_pipeline 的 331 行按原样迁移为模块级函数 run_resolution_pipeline(server_args)self 改名 server_args)。函数负责 _raw_input 快照、_resolved_overrides stash、resolving_view 视图,并按依赖域顺序执行 77 个 handler/pass 调用;dummy 模型短路边界保持不变。
  2. 拆除类内 handler:在 server_args.py 中删除 _run_resolution_pipeline_handle_sampling_backend_handle_page_size_handle_pipeline_parallelism 三个单行转发方法(均折叠为 run_post_process_pass 调用);_handle_hardware_runtime_validation 的 MLA 检查则迁入 arg_groups/platform_hook.py 新函数 handle_hardware_runtime_validation,并在 pipeline 中 dummy 短路前调用。调用点内联导入 platform_hook,避免模块级加载。
  3. 更新测试 patch 目标test_resolution_is_reproducible.pytest_resolution_declarations.py 中的 mock 目标从 ServerArgs._run_resolution_pipeline 改为 pipeline_module.run_resolution_pipeline;“one caller”扫描从 self. 属性调用改为裸名调用匹配;惰性与 ModelConfig 顺序检查改解析 arg_groups/pipeline.py
  4. 强化架构不变量:在 test_resolution_reads_the_declarations.py 中删除 _resolution_handlers 遍历逻辑,把 test_no_handler_reads_a_field_off_self 升级为 test_the_record_hosts_no_resolution_handler,用 AST 断言 ServerArgs 不再持有任何 _handle_*/_validate_*/包含 resolution_pipeline 的方法。
  5. 同步平台插件测试命名空间test_resolution_declarations.py 中安装 out-of-tree 平台插件的 patch 目标从 server_args_module.current_platform 改为 pipeline_module.current_platform,确保写捕获仍能到达 dispatcher 运行时的命名空间。

配套改动

共 7 个文件:2 个源码核心文件(pipeline.py 新增、server_args.py 大幅删减),1 个平台 hook 扩展,4 个 registered 测试 guard 适配。无配置、schema 或部署改动;CI 状态显示三个 runner 均为失败态(PR Test / Extra / AMD ROCm 7.2),但作者未说明失败原因。

文件 模块 状态 重要度
python/sglang/srt/arg_groups/pipeline.py 解析调度 added 8.21
python/sglang/srt/server_args.py 参数解析 modified 8.75
python/sglang/srt/arg_groups/platform_hook.py 平台钩子 modified 6.07
test/registered/unit/server_args/test_resolution_reads_the_declarations.py 解析测试 modified 6.75
test/registered/unit/server_args/test_resolution_is_reproducible.py 解析测试 modified 5.32
test/registered/unit/server_args/test_resolution_declarations.py 解析测试 modified 5.29
test/registered/unit/server_args/test_model_config_reads_resolved_input.py 模型配置 modified 4.72

关键符号

run_resolution_pipeline handle_hardware_runtime_validation resolve_once _run_resolution_pipeline _handle_hardware_runtime_validation _handle_sampling_backend _handle_page_size _handle_pipeline_parallelism

关键源码片段

python/sglang/srt/arg_groups/pipeline.py core-logic

新增的解析调度器,331 行 `_run_resolution_pipeline` 从 ServerArgs 迁移至此,是本次重构的核心载体;所有解析步骤的调用顺序和 dummy 短路边界都在这。

# SPDX-License-Identifier: Apache-2.0
"""The resolution pipeline: the ordered dispatcher every publishing entry runs.``ServerArgs.resolve_once`` is the only caller. It lives here rather than on the
record because a step decides *about* the record; none of them is a member of it.
"""from __future__ import annotationsimport dataclasses
from typing import Anyfrom sglang.srt.arg_groups.overrides import (
    _page_size_default,
    _pipeline_parallel_overlap_disable,
    _sampling_backend_default,
    declare_direct_writes,
    resolving_view,
    run_post_process_pass,
)
from sglang.srt.platforms import current_platform
from sglang.srt.utils.common import get_device_memory_capacity
​
​
def run_resolution_pipeline(server_args: Any) -> None:
    """
    Orchestrates the handling of various server arguments, ensuring proper configuration and validation.    Dispatcher style principles:
    1. Keep this function as an ordered dispatcher. Each step should be a
       named call into an ``arg_groups`` family; put imports, conditionals,
       mutations, and raises inside the family instead of inline here.
    2. Keep the dummy-model boundary as early as correctness allows.
    3. Order handlers by dependency domains, not by historical insertion.
    4. Hide narrow integrations behind general handler names.
    5. Give each handler one clear contract.
    """
​
    # 在任何 handler 运行前,把“用户原始输入”完整快照下来:
    # 这是 projection 读到的 resolution 结果的基础。
    server_args._raw_input = {
        field.name: getattr(server_args, field.name)
        for field in dataclasses.fields(server_args)
    }
​
    # 声明 stash 服务于 override/post-process 两趟 pass。必须在 dummy 短路之前设置,
    # 这样即使 _handle_model_specific_adjustments 不执行,run_post_process_pass 和直接调用
    # 的 handler 也能依赖它。
    server_args._resolved_overrides = []
​
    cfg = resolving_view(server_args)
​
    # 每个步骤按依赖域排序: internal/bootstrap、API/network/protocol 等。
    # 家族模块在调用点才导入,避免模块级 import 放大启动成本。
    from sglang.srt.arg_groups.mega_moe_hook import handle_mega_moe
​
    handle_mega_moe(server_args)
    from sglang.srt.arg_groups.serving_hook import (
        handle_asr_validation,
        handle_crash_dump_env,
        handle_debug_utils,
        handle_deprecated_args,
        handle_environment_variables,
        handle_grammar_backend,
        handle_load_balance_method,
        handle_media_url_security,
        handle_missing_default_values,
        handle_multimodal,
        handle_other_validations,
        handle_prefill_delayer_env_compat,
        handle_return_hidden_states_mode,
        handle_ssl_validation,
        handle_tokenizer_batching,
    )
​
    handle_return_hidden_states_mode(server_args)
    handle_media_url_security(server_args)
​
    # 硬件运行时校验必须赶在模型路径解析、下载和 dummy 短路之前,
    # 所以 platform_hook 在这一步才 import。
    from sglang.srt.arg_groups.platform_hook import (
        handle_hardware_runtime_validation,
    )
​
    handle_hardware_runtime_validation(server_args)
​
    # dummy 短路:模型无关的引导与校验已经完成,从这里开始才进入
    # 模型相关的解析步骤。
    if cfg.model_path.lower() in ["none", "dummy"]:
        return
    # 后续步骤按依赖域继续:model source/path、hardware/platform、
    # parallelism、kernel/attention backend、cuda graph、memory/cache 等。
python/sglang/srt/arg_groups/platform_hook.py core-logic

承接原 _handle_hardware_runtime_validation(MLA opt-in 检查),加入平台家族;此函数在 dummy 短路前执行。

# SPDX-License-Identifier: Apache-2.0
"""Server-argument resolution for the per-platform backend defaults."""from __future__ import annotationsimport logging
from typing import Anyfrom sglang.srt.arg_groups.overrides import declare_resolution, resolving_view
from sglang.srt.hardware_backend.mlx.runtime import use_mlx
from sglang.srt.model_executor.cuda_graph_config import Backend, Phase, with_phase
from sglang.srt.utils.common import is_cuda, is_hip, is_host_cpu_arm64, is_npulogger = logging.getLogger(__name__)
​
​
def handle_hardware_runtime_validation(server_args: Any):
    # 这个检查刻意不依赖 server_args.device:设了 SGLANG_USE_MLX 就等于
    # 选择了 MLX 后端,环境不满足时必须立刻失败;未设 flag 时 use_mlx()
    # 保持惰性,不会真的导入 MLX 模块。
    use_mlx()
test/registered/unit/server_args/test_resolution_reads_the_declarations.py test-coverage

守卫测试升级:删除 _resolution_handlers 遍历逻辑,将“handler 不能读 self 字段”强化为“record 不能持有任何 resolution handler”。

def test_the_record_hosts_no_resolution_handler(self):
    """The pipeline and every step it runs live under `arg_groups/`.    While a step was a method, it could read a raw field off `self` and
    `test_no_handler_reads_a_field_off_self` had to say it could not. There
    is no such method left, so the invariant is now the stronger one: the
    record hosts none of them. What the steps read is checked on the
    package side, by `test_no_hook_reads_a_field_off_the_record`.
    """
    # 通过 AST 扫描 ServerArgs 的所有成员:只要名字以 _handle_/_validate_
    # 开头或包含 resolution_pipeline,就判定为“解析 handler 回到 record”。
    handlers = sorted(
        name
        for name, node in _record_members().items()
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
        and (
            name.split(".")[-1].startswith(("_handle_", "_validate_"))
            or "resolution_pipeline" in name
        )
    )
    self.assertEqual(
        handlers,
        [],
        "a resolution handler is back on the record; it belongs in an "
        "`arg_groups` family, where the package-side guards can see it:\n  "
        + "\n  ".join(handlers),
    )

评论区精华

平台插件测试的命名空间 seam 设计

PR body 标注这是本次唯一值得注意的 seam:测试通过 patch current_platform 在 dispatcher 模块中安装 out-of-tree 平台;移动 dispatcher 后命名空间随之移动,若不跟随,插件默认值会静默失效。

结论:测试改为在 arg_groups.pipeline 命名空间内安装 platform 插件(patch pipeline_module.current_platform),并保留为显式 seam,失败时大声失败。 · 已解决

测试 guard 从类级约束升级为更强的 record 级约束 测试

作者将 test_no_handler_reads_a_field_off_self 升级为 test_the_record_hosts_no_resolution_handler,理由是 record 上已无任何 handler,可以给出更强的架构不变量。

结论:新测试断言 ServerArgs 不持有任何 _handle_*/_validate_*/resolution_pipeline 方法;包侧测试继续检查 hook 只读 resolving_view。 · 已解决

风险与影响

1) 行为等价风险:331 行 dispatcher 是机械搬运,但 3 个 handler 折叠为 run_post_process_pass 调用、MLA 检查位置在 dummy 短路之前,任何顺序偏差都可能改变参数解析结果;作者声称验证过 77 个调用顺序一致,但缺少独立 review 佐证。
2) 导入图变化:resolve_once 现在在 try 内延迟导入 pipeline 模块,platform_hook 在调用点导入;若外部代码在 resolve_once 前依赖这些模块被加载,可能出现导入时序差异(实测 dummy 路径多加载两个模块约 9 ms)。
3) 测试 guard 结构敏感:新增 AST 测试对命名模式(_handle_ 前缀、resolution_pipeline 子串)敏感,未来合法的 handler 命名也可能误触发失败。
4) out-of-tree 平台插件兼容性:如果外部插件仍 patch sglang.srt.server_args.current_platform 做写捕获,将静默失效——这是内部测试 seam,对下游用户影响有限。

对用户:纯内部重构,CLI 参数解析结果与之前完全一致(62-shape resolution probe 与 base 字节一致),无行为变化。对系统:ServerArgs 类瘦身约 6.5%,解析调度逻辑集中到 arg_groups/pipeline.py,消除了“arg_groups 的 hook 被它从中提取出来的对象的方法调用”的反向依赖。对团队:测试体系对架构的限制从“handler 不能读 self 字段”升级为“record 不能持有 handler”,后续新增解析步骤必须放入 arg_groups 家族;同时为系列后续 PR(#36972 清零回调、#36973 运行时读取、#36974 删死参数、#36975 惰性导入转 eager)建立了统一结构。

核心配置解析路径变更 测试 guard 结构敏感 平台插件命名空间 seam 跨模块重构

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论