Prhub

#36253 config: resolution reads the declarations, not the fields

原始 PR 作者 ch-wan 合并时间 2026-08-26 20:05 文件变更 34 提交数 7 评论 3 代码增减 +1865 / -1495

执行摘要

resolution 链读取改走声明 stash,ServerArgs 回归原始输入

Resolution 本质上是一条链:一个 resolver 决定某字段,下一个 resolver 读取该决定。PR body 指出:"Resolution is a chain: one resolver decides a field, the next one reads that decision. Today that works only because declare_resolution writes the field as a side effect, so the record doubles as the scratchpad for a half-finished resolution. That side effect is what keeps ServerArgs from being what it should be — the raw user input — and it makes 'who decided this value' unanswerable after the fact." 因此本 PR 把 resolution 期间的所有读取移到声明缝(declaration seam)上,让字段写入不再是链的通信渠道。

值得精读。重点看 ResolvingConfigResolvedView 的分工、声明 stash newest-first 遍历的语义,以及"先迁移读取、后翻转写入"的双阶段策略——这是把命令式副作用逐步改造成声明式决议的示范性做法。若团队后续要做类似的配置系统演进,可直接借鉴其测试探针与 A/B 验证方法。

讨论亮点

该 PR 没有可展示的 review 评论内容(review_comments_count=3,但材料中未包含具体评论文本)。从 PR body 与提交序列可辨识三个核心设计决策:

  • 实时视图 vs 快照视图ResolvingConfig 每次读都遍历 stash,而 ResolvedView 在构造时快照 overlay——前者给解析期读者,后者给后处理 pass,两者语义区分清晰。
  • 两处有意行为变化:SM100 + EmbeddingGemma 的 no-KV 路径与 DSpark + DP attention + waterfill 的启动拒绝都被视为修复,且二者均不在 CPU 测试套件可达范围内,作者明确标注为"deliberately not identical"。
  • 三个 reader 延迟迁移get_attention_backendsdescribe_kv_events_publishercompute_world_size 留在 record 上等最后一步翻转,是"先改读、后翻写"双阶段策略的一部分,降低单点风险。

实现拆解

  1. 引入实时读视图 ResolvingConfig:在 python/sglang/srt/arg_groups/overrides.py 中新增 ResolvingConfig 类与 resolving_view(server_args) 工厂。它每次属性读取都从 _resolved_overrides 声明 stash 按 newest-first 遍历取最新值,未命中时 fallback 到原始字段;__setattr__ 抛异常强制只读。与已有的 ResolvedView(构造时快照 overlay)互补:解析器中"声明后再读取"需要实时答案,后处理 pass 需要槽位快照。
  2. 迁移 resolution 链上的全部读取:a) arg_groups 各钩子(speculative_hook.pypd_disaggregation_hook.pydeepseek_v4_hook.pykimi_k3_hook.pyexpert_pack_hook.py 等)统一先取 cfg = resolving_view(server_args) 再读;b) server_args.py 中 88 个 dispatcher 可达 handler 的约 779 处 self.<field> 读改为 cfg = resolving_view(self);c) 解析期被调用的辅助函数(ModelConfig.from_server_args 的 23 处读、CP strategy binder、BCG predicate、adaptive-spec 支持检查)以及 record 自身成员函数(如 max_speculative_num_draft_tokensis_ep_joiner)同样迁移;d) type dispatcher 中 get_device_memory_capacity(self.device) 改读 resolution result,避免在平台默认声明 device 后用字段 auto 计算显存。
  3. 断言与测试改为读 resolution result:断言和 test_server_args.py 中检查 resolution 中间值的 101 处读取改为读声明结果而非字段;新增 test_resolution_reads_the_declarations.py,用探针固定"arg_groups/* 下接收 config 的函数与每个 dispatcher 可达 handler 零直接字段读",作为该边界的护栏。
  4. A/B 验证与行为对齐:通过 reproducibility suite 对比,所有 launch shape 的 resolved projection 前后一致;有两处有意行为变化:SM100 + EmbeddingGemma 因 _attention_backend_default 声明 trtllm_mha 而不再进入 prefill-only no-KV 路径;DSpark + DP attention + waterfill 因 _a2a_backend_overrides 声明 deepep 而在启动时拒绝该非法组合。另保留 get_attention_backendsdescribe_kv_events_publishercompute_world_size 三个 mid-resolution 读取在 record 上,值不变,待系列最后一个 PR 翻转时一并迁移。
  5. 测试与文档配套:除上述两个测试文件外,还更新了多个与服务参数相关的测试文件(server args、model overrides、reproducibility 等),保证测试断言语义切换到"读决议结果"。
文件 模块 状态 重要度
python/sglang/srt/arg_groups/overrides.py 参数解析 modified 8.65
python/sglang/srt/server_args.py 参数解析 modified 7.4
python/sglang/srt/arg_groups/speculative_hook.py 解析钩子 modified 7.0
python/sglang/srt/configs/model_config.py 模型配置 modified 6.83
python/sglang/srt/arg_groups/deepseek_v4_hook.py 解析钩子 modified 6.36
python/sglang/srt/arg_groups/pd_disaggregation_hook.py 解析钩子 modified 6.31
python/sglang/srt/arg_groups/kimi_k3_hook.py 解析钩子 modified 5.91
python/sglang/srt/arg_groups/expert_pack_hook.py 解析钩子 modified 5.95
test/registered/unit/server_args/test_server_args.py 参数测试 modified 5.92
test/registered/unit/server_args/test_resolution_reads_the_declarations.py 参数测试 added 6.0

关键符号

resolving_view ResolvingConfig.__getattr__ ResolvingConfig.__setattr__ handle_speculative_decoding _disable_overlap_schedule_for_cpu handle_pd_disaggregation validate_deepseek_v4_mega_moe_token_budget ModelConfig.from_server_args _kimi_k3_overrides

关键源码片段

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

本 PR 的核心:新增 ResolvingConfig 实时读视图与 resolving_view 工厂,定义 " 解析期读声明 stash、fallback 原始字段 " 的语义,并让各钩子切换为经 cfg 读取。

class ResolvingConfig:
    """实时读视图:每次读取都从声明 stash 中取最新值。    与 ResolvedView(构造时快照 overlay)不同,解析器可能在声明之后
    再调用其他会声明的函数,因此需要"当前答案"而不是某一时刻的快照。
    该视图遍历 ``_resolved_overrides``(newest-first),未命中的属性
    fallback 到原始字段——字段此时仍是用户的原始输入。
    """
​
    __slots__ = ("_server_args",)
​
    def __init__(self, server_args: Any):
        # 通过 object.__setattr__ 绕开只读约束,初始化内部引用
        object.__setattr__(self, "_server_args", server_args)
​
    def __getattr__(self, name: str) -> Any:
        server_args = object.__getattribute__(self, "_server_args")
        # 从最新声明向旧声明遍历,保证 " 后声明者覆盖先声明者 "
        for _source, declared in reversed(
            getattr(server_args, "_resolved_overrides", None) or ()
        ):
            if name in declared:
                return declared[name]
        # 声明中不存在时读取原始字段,即用户输入
        return getattr(server_args, name)
​
    def __setattr__(self, name: str, value: Any) -> None:
        # 视图只读:resolution 阶段的所有写入必须走 declare_resolution
        raise AttributeError(
            "ResolvingConfig is read-only; resolution writes through declarations"
        )
​
​
def resolving_view(server_args: Any) -> ResolvingConfig:
    """解析进行中使用的实时读视图,提供"已决议到当前进度"的值。"""
    return ResolvingConfig(server_args)
python/sglang/srt/arg_groups/speculative_hook.py core-logic

典型钩子改造样本:handle_speculative_decoding 等函数从 server_args.* 直读切换为 cfg = resolving_view(server_args),展示声明式读取在投机解码参数归一化中的应用。

def handle_speculative_decoding(server_args: ServerArgs) -> None:
    # 先取实时读视图:后续所有读取都从声明 stash 取最新决议,
    # 而不是直接读字段(字段可能仍是用户原始输入或旧值)
    cfg = resolving_view(server_args)
​
    if (
        cfg.speculative_draft_model_path is not None
        and cfg.speculative_draft_model_revision is None
    ):
        declare_resolution(
            server_args,
            "handle_speculative_decoding",
            speculative_draft_model_revision="main",
        )
​
    # 该逻辑已迁移到 resolution 管线,这里在旧 slot 上按原顺序调用
    from sglang.srt.arg_groups.overrides import (
        _speculative_moe_runner_default,
        run_post_process_pass,
    )
​
    run_post_process_pass(server_args, _speculative_moe_runner_default)
​
    if cfg.speculative_algorithm is not None:
        declare_resolution(
            server_args,
            "handle_speculative_decoding",
            speculative_algorithm=cfg.speculative_algorithm.upper(),
        )
​
    # 后续读取(decrypted_draft_config_file、trust_remote_code、
    # speculative_draft_window_size 等)全部经 cfg 访问,略
python/sglang/srt/configs/model_config.py data-contract

ModelConfig.from_server_args 是解析期被调用的关键辅助函数,23 处 server_args 字段读整体切换为 cfg = resolving_view(server_args),保证模型配置在解析中途构造时看到正确的决议值。

@staticmethod
def from_server_args(
    server_args: ServerArgs,
    model_path: str = None,
    model_revision: str = None,
    is_draft_model: bool = False,
    context_length: Optional[int] = None,
    **kwargs,
):
    # 从解析视图读取:ModelConfig 可能在解析尚未完全结束时被构造,
    # 需要一个 " 跟随声明进度 " 的实时视图,而不是字段的静态值
    from sglang.srt.arg_groups.overrides import resolving_view
​
    cfg = resolving_view(server_args)
    quantization = (
        cfg.speculative_draft_model_quantization
        if is_draft_model
        else cfg.quantization
    )
    override_config_file = (
        cfg.decrypted_draft_config_file if is_draft_model else cfg.decrypted_config_file
    )
    return ModelConfig(
        model_path=model_path or cfg.model_path,
        trust_remote_code=cfg.trust_remote_code,
        revision=model_revision or cfg.revision,
        context_length=(
            context_length if context_length is not None else cfg.context_length
        ),
        model_override_args=cfg.json_model_override_args,
        is_embedding=cfg.is_embedding,
        enable_multimodal=cfg.enable_multimodal,
        dtype=cfg.dtype,
        quantization=quantization,
        # 其余字段均改为经 cfg 读取,略
        **kwargs,
    )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 核心路径变更server_args.pyarg_groups/* 是所有启动路径(CPU/GPU/NPU/AMD、TP/PP/DP/DCP/PD 拆分、DeepSeek V4/DSpark/Kimi-K3 等)的公共入口,约 1900 行改动中存在漏改字段读的风险;test_resolution_reads_the_declarations.py 的零字段读探针是主要防线。
  2. 两处行为变化:SM100 + EmbeddingGemma 可能因声明提前生效而改变注意力后端选择;DSpark + DP attention + waterfill 组合将从"启动后运行在 deepep"变为"启动即报错",依赖旧行为的用户会直接失败(但旧行为本身是 bug)。
  3. 性能ResolvingConfig.__getattr__ 每次读取遍历声明 stash(O(声明数)),只发生在解析阶段,量级可忽略。
  4. 只读约束ResolvingConfig.__setattr__ 抛异常,任何在解析阶段尝试写 view 的代码都会立即失败,这是有意但可能暴露隐藏写路径的风险。
  5. 系列后续依赖:三个保留 reader 与最后一步翻转存在一致性风险,若该系列未合入则本 PR 的部分意图仍悬空。

用户:无直接可见的值变化;两个组合(SM100+EmbeddingGemma、DSpark+DP+waterfill)的行为被修复,启动更安全。系统ServerArgs 向"只存原始输入"演进,resolution 的"谁决定了此值"变得可追溯,为后续声明式翻转铺路。团队:引入 resolving_view/ResolvedView 双视图约定与零字段读测试护栏,后续 arg_groupsserver_args 的修改须遵循"解析期读视图、写走声明"的纪律;model_config.pyexpert_pack_runtime.pydllm/config.py、lora policy 等下游读取者同步切换。

核心路径变更 跨模块重构 两处行为变化 大规模机械替换 依赖后续翻转

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论