Prhub

#34265 config: a named entry point for the resolution pipeline, and the last dynamic config read

原始 PR 作者 ch-wan 合并时间 2026-08-15 15:38 文件变更 4 提交数 2 评论 2 代码增减 +67 / -40

执行摘要

命名解析管线入口并清理死动态配置读取

PR body 明确说明:__post_init__ 是一个 200 行调度器,让"解析在哪里运行"变成了关于 dunder 的问题;step-12 将把解析调用从构造移动到 publish,使记录保持用户原始输入,每个进程在自己的 bag 中派生解析值。同时,census 无法追踪的 _is_dsa_active 读取了一个从未在 ServerArgs 上存在的属性(来自 #27313 的占位符),getattr 默认值一直决定谓词结果,"看起来像活决策,实际是死的",需要把它写成它实际求值的常量。

值得精读。重点有两个:一是"把动态读取改写成它实际求值的常量"的处理方式——对 census 无法追踪的死读取不强行修复而是诚实标注占位,避免了在错误层次做决策;二是用 AST 静态测试钉住"唯一调用者"来保护重构 seam,这种让测试成为重构安全网的做法(配合反向验证)在大型重构中非常有借鉴价值。建议顺带阅读 #34266、#34269、#34819 了解配置 bag 迁移的完整脉络。

讨论亮点

唯一的实质性 review 评论来自 chatgpt-codex-connector[bot](P2):指出最初的钉测实现只记录"外围函数名不同"的调用且从不断言期望值,因此删除 __post_init__ 中的调用、在其中重复调用、或给无关类添加同名 __post_init__ 都可能漏检——而这些正是该测试声称要钉住的回归。ch-wan 回复确认并修复:测试改为收集全部调用并与期望的单一调用者 ("srt/server_args.py", "ServerArgs.__post_init__") 全量比较,并反向验证了重复调用与删除场景。作者后续三次 re-review(head 152aa59b92)均判定 clean,无遗留问题。

实现拆解

  1. 提取解析入口:在 python/sglang/srt/server_args.py 中,__post_init__ 方法体(docstring、调度顺序契约、各 _handle_* 调用)原封不动地移动到新方法 _run_resolution_pipeline()__post_init__ 只剩一行 self._run_resolution_pipeline()。body 逐字节一致,构造时仍只解析一次。这是 step-12 要移动的 seam——届时 publish 而非构造会调用它。
  2. 清除死动态读取:在 python/sglang/srt/layers/cp/base.py 中,_is_dsa_active()get_parallel().enable_prefill_cp and getattr(get_server_args(), "_is_dsa_model_arch", False) 改为常量 False。因为 _is_dsa_model_arch 从未被设置,getattr 默认值一直决定结果,常量 False 与历史行为完全等价;注释写明真正的判断(本进程是否运行 DSA 模型架构)应由 CP 路径负责,目前唯一消费者 ContextParallelStrategy.per_layer_attn_cp_comm 尚无读取者。
  3. 收紧 seam 钉测:在 test/registered/unit/server_args/test_resolution_is_reproducible.py 新增 TestTheResolutionSeamHasOneCaller,用 AST 扫描整个 sglang 包,收集所有 _run_resolution_pipeline 调用的文件与完整(类、函数)作用域链,断言唯一调用者是 ("srt/server_args.py", "ServerArgs.__post_init__")。删除调用、在 __post_init__ 内重复调用、其他类出现同名 __post_init__ 都会失败。作者做了反向验证(手动引入重复调用和删除调用均被测试捕获)。
  4. 简化 ratchet:在 test/registered/unit/test_global_config_read_ratchet.py 中删除 _INERT_DYNAMIC_READS 豁免列表、_collect()inert 参数和 counted() 过滤函数,_DIRECT_BASELINE / _ALIAS_BASELINE 保持 0,三种读取形态(直接、getattr、属性别名)的探测仍报告。
文件 模块 状态 重要度
python/sglang/srt/server_args.py 配置解析 modified 5.9
test/registered/unit/server_args/test_resolution_is_reproducible.py 配置测试 modified 6.25
test/registered/unit/test_global_config_read_ratchet.py 配置测试 modified 5.73
python/sglang/srt/layers/cp/base.py 上下文并行 modified 5.0

关键符号

_run_resolution_pipeline _is_dsa_active test_only_post_init_runs_the_pipeline _collect

关键源码片段

python/sglang/srt/server_args.py core-logic

核心变更:`__post_init__` 从 200 行调度器收敛为一行 `self._run_resolution_pipeline()`,创建了 step-12 要移动的命名 seam,是配置重构系列的支点。

# python/sglang/srt/server_args.py
# 变更后:__post_init__ 只剩一行,解析管线的全部调度逻辑移到命名方法里。
# 这是 step-12 要移动的 seam——届时由 publish 而非构造调用,
# 让记录保持用户原始输入,每个进程在自己的 bag 中派生解析值。
def __post_init__(self):
    # 唯一入口:解析管线只在构造时运行一次,后续逻辑都在
    # _run_resolution_pipeline 中按依赖域顺序调度各 _handle_* 步骤。
    self._run_resolution_pipeline()
​
​
def _run_resolution_pipeline(self):
    """
    Orchestrates the handling of various server arguments, ensuring proper configuration and validation.    Dispatcher style principles:
    1. Keep this method as an ordered dispatcher. Each step should be a
       named self._handle_* call; put imports, conditionals, mutations, and
       raises inside helpers instead of inline here.
    2. Keep the dummy-model boundary as early as correctness allows. Only
       model-independent bootstrap, API/network/protocol validation, and
       errors that should fire for dummy models should run before it.
    3. Order handlers by dependency domains, not by historical insertion:
       internal/bootstrap, API/network/protocol, model source/path
       resolution, hardware/platform, model-specific adjustment,
       parallelism, kernel/attention backend, cuda graph, memory/cache,
       and advanced/debug features.
    4. Hide narrow integrations behind general handler names. The
       dispatcher should say what phase is being handled, not expose a
       vendor-, hook-, or feature-specific implementation detail.
    5. Give each handler one clear contract: what state it expects, what it
       may mutate, and whether it validates only. Long ordering comments
       belong in the helper or signal that the helper should be split.
    """
​
    # Declaration stash for the override/post-process passes. Set before any
    # short-circuit (none/dummy model paths) so run_post_process_pass and
    # direct handler invocations can rely on it even when
    # _handle_model_specific_adjustments never runs.
    self._resolved_overrides = []
​
    self._handle_moe_runner_backend_alias()
    self._handle_return_hidden_states_mode()
    # ... 其余 _handle_* 调用保持原顺序,逐字节未变 ...
test/registered/unit/server_args/test_resolution_is_reproducible.py test-coverage

新增 `TestTheResolutionSeamHasOneCaller`,用 AST 全量扫描钉住 `_run_resolution_pipeline` 的唯一调用者,保护 step-12 迁移的 seam;codex review 的 P2 反馈在此得到完整落实。

# test/registered/unit/server_args/test_resolution_is_reproducible.py
class TestTheResolutionSeamHasOneCaller(CustomTestCase):
    """The pipeline is entered from exactly one place.    Step 12 moves the call from ``__post_init__`` to ``publish`` so the record
    stays raw; that is a one-line move only while the seam has a single caller.
    A second entry point would also mean resolution could run twice on one
    instance, which the strict ``__setattr__`` guard turns into an
    ``AttributeError`` rather than a silent re-resolve.
    """
​
    def test_only_post_init_runs_the_pipeline(self):
        import ast
        from pathlib import Path
​
        import sglang
​
        package_root = Path(next(iter(sglang.__path__)))
        callers = []
        for path in sorted(package_root.rglob("*.py")):
            try:
                tree = ast.parse(path.read_text())
            except SyntaxError:
                continue
            # 构造完整的 ( 类 , 函数 , ...) 作用域链,
            # 使断言能精确说明 " 唯一调用者是 ServerArgs.__post_init__",
            # 而不是 " 没有调用发生在名为 __post_init__ 的函数之外 "。
            scopes = {}
            for node in ast.walk(tree):
                own = scopes.get(id(node), ())
                if isinstance(
                    node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
                ):
                    own = own + (node.name,)
                for child in ast.iter_child_nodes(node):
                    scopes[id(child)] = own
            for node in ast.walk(tree):
                if (
                    isinstance(node, ast.Call)
                    and isinstance(node.func, ast.Attribute)
                    and node.func.attr == "_run_resolution_pipeline"
                ):
                    rel = path.relative_to(package_root).as_posix()
                    callers.append((rel, ".".join(scopes.get(id(node), ()))))
        # 与期望的完整列表逐项比较:调用被删除、在 __post_init__ 中重复、
        # 或其他类出现同名 __post_init__ 都会使断言失败。
        self.assertEqual(
            [("srt/server_args.py", "ServerArgs.__post_init__")],
            callers,
            "the resolution pipeline must be entered exactly once, from "
            f"ServerArgs.__post_init__; found: {callers}",
        )
python/sglang/srt/layers/cp/base.py dependency-wiring

`_is_dsa_active()` 从读取不存在的 `_is_dsa_model_arch` 属性改为常量 `False`,清除 census 唯一无法追踪的动态读取,并保留占位注释说明真实判定应由 CP 路径负责。

# python/sglang/srt/layers/cp/base.py
def _is_dsa_active() -> bool:
    # Placeholder: a real answer needs the model architecture, not config.
    # 历史实现读取 getattr(get_server_args(), "_is_dsa_model_arch", False),
    # 但该属性从未在 ServerArgs 上定义(来自 #27313 的占位符),
    # 因此 getattr 默认值一直决定结果,整个谓词实际恒为 False。
    # 这里按它实际求值的常量写出来;真实判定(本进程是否运行 DSA 模型架构)
    # 是 CP 路径的职责,目前唯一消费者 per_layer_attn_cp_comm 尚无读取者。
    return False

评论区精华

分辨率 seam 钉测应全量比较而非仅记录不同函数名的调用 测试

codex bot(P2)指出:最初的测试实现只记录外围函数名不同于 `__post_init__` 的调用,从不断言期望值——删除 `__post_init__` 中的调用、在其中重复调用、或给无关类添加同名 `__post_init__` 都会漏检,而这些正是该测试声称要钉住的回归。

结论:ch-wan 回复已修复:测试现在收集所有 `_run_resolution_pipeline` 调用(含文件与完整作用域链),与期望的唯一调用者 `("srt/server_args.py", "ServerArgs.__post_init__")` 全量比较,并反向验证了重复调用与删除场景均会被捕获。 · 已解决

风险与影响

  1. AST 钉测的脆弱性test_only_post_init_runs_the_pipeline 依赖 AST 语法匹配 ast.Call + ast.Attribute 且属性名为 _run_resolution_pipeline。如果未来有人用别名引用(如 from ... import _run_resolution_pipeline as f)或通过 getattr(obj, "_run_resolution_pipeline")() 调用,测试会误报;相反这种写法本身也该被禁止,所以风险可控。
  2. _is_dsa_active 常量化后的行为冻结:真实 DSA 判定目前缺失,若未来 CP 路径实现真实谓词但忘记更新此函数,会静默保持 False。注释已写明这是占位符,但缺少强制提醒。
  3. step-12 迁移风险:本 PR 把 seam 收敛到唯一调用点,迁移时若 publish 路径的调用时序不当(如解析前就有代码读取了派生值),可能引发 AttributeError(严格 __setattr__ 守卫)或配置未就绪问题。测试已钉住"构造时只解析一次",但未钉住 publish 调用后的行为。

对用户无行为影响(纯重构、零逻辑变化)。对系统的影响在架构层面:_run_resolution_pipeline 成为配置解析的唯一名命入口,为 step-12 把解析从构造移动到 publish 铺平道路;_INERT_DYNAMIC_READS 豁免机制的删除意味着配置读取 ratchet 以后不再允许"不可见的动态读取",普查基线完全严格化。对团队的影响是配置分层重构系列中关键的 seam 钉测和死代码清理,后续步骤(#34266、#34267、#34269、#34819 等)可直接在此之上推进。

配置解析核心路径变更 AST 静态测试脆弱性 未来 DSA 判定占位

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论