# PR #36253 完整报告

- 仓库：`sgl-project/sglang`
- 标题：config: resolution reads the declarations, not the fields
- 合并时间：2026-08-26 20:05
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/36253

---

# 执行摘要

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

# 功能与动机

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）上，让字段写入不再是链的通信渠道。

# 实现拆解

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.py`、`pd_disaggregation_hook.py`、`deepseek_v4_hook.py`、`kimi_k3_hook.py`、`expert_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_tokens`、`is_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_backends`、`describe_kv_events_publisher`、`compute_world_size` 三个 mid-resolution 读取在 record 上，值不变，待系列最后一个 PR 翻转时一并迁移。
5. **测试与文档配套**：除上述两个测试文件外，还更新了多个与服务参数相关的测试文件（server args、model overrides、reproducibility 等），保证测试断言语义切换到 " 读决议结果 "。

关键文件：
- `python/sglang/srt/arg_groups/overrides.py`（模块 参数解析；类别 source；类型 core-logic；符号 ResolvingConfig, resolving_view, __init__, __getattr__）: 本 PR 的核心：新增 ResolvingConfig 实时读视图与 resolving_view 工厂，定义 " 解析期读声明 stash、fallback 原始字段 " 的语义，并让各钩子切换为经 cfg 读取。
- `python/sglang/srt/server_args.py`（模块 参数解析；类别 source；类型 core-logic）: 改动量最大的文件（+949/-851）：约 779 处 self.<field> 读取（88 个 resolution handler）与断言全部改为经 resolving_view/resolution_result 读取，是迁移主战场。
- `python/sglang/srt/arg_groups/speculative_hook.py`（模块 解析钩子；类别 source；类型 core-logic；符号 handle_speculative_decoding, _disable_overlap_schedule_for_cpu）: 典型钩子改造样本：handle_speculative_decoding 等函数从 server_args.* 直读切换为 cfg = resolving_view(server_args)，展示声明式读取在投机解码参数归一化中的应用。
- `python/sglang/srt/configs/model_config.py`（模块 模型配置；类别 source；类型 data-contract；符号 ModelConfig.from_server_args）: ModelConfig.from_server_args 是解析期被调用的关键辅助函数，23 处 server_args 字段读整体切换为 cfg = resolving_view(server_args)，保证模型配置在解析中途构造时看到正确的决议值。
- `python/sglang/srt/arg_groups/deepseek_v4_hook.py`（模块 解析钩子；类别 source；类型 dependency-wiring；符号 validate_deepseek_v4_mega_moe_token_budget）: DeepSeek V4 MegaMoE token budget 校验全部改为经 cfg 读取，涉及 tp_size/dp_size/attn_cp_size/chunked_prefill_size 等并行参数组合判断。
- `python/sglang/srt/arg_groups/pd_disaggregation_hook.py`（模块 解析钩子；类别 source；类型 dependency-wiring；符号 handle_pd_disaggregation）: PD 分离参数归一化钩子全面切换为 cfg 读取，处理 mooncake_tcp 强制 TCP、DCP 与后端组合校验等分支。
- `python/sglang/srt/arg_groups/kimi_k3_hook.py`（模块 解析钩子；类别 source；类型 dependency-wiring）: Kimi-K3 钩子同步切换为 cfg 读取，确保 DCP 消歧、RaggedVerifyMode 校验读取到已决议值。
- `python/sglang/srt/arg_groups/expert_pack_hook.py`（模块 解析钩子；类别 source；类型 dependency-wiring）: 专家打包钩子切换为 cfg 读取，保持系列改造在 arg_groups 内的完整性。
- `test/registered/unit/server_args/test_server_args.py`（模块 参数测试；类别 test；类型 test-coverage）: 测试断言语义从 " 读字段 " 切换为 " 读 resolution result"，101 处读取随迁，是验证迁移后行为未变的主要测试面。
- `test/registered/unit/server_args/test_resolution_reads_the_declarations.py`（模块 参数测试；类别 test；类型 test-coverage）: 新增护栏测试：固定 arg_groups/* 下接收 config 的函数与每个 dispatcher 可达 handler 零直接字段读，是本次改造边界的守门员。

关键符号：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`

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

```python
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`

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

```python
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`

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

```python
@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,
    )

```

# 评论区精华

该 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_backends`、`describe_kv_events_publisher`、`compute_world_size` 留在 record 上等最后一步翻转，是 " 先改读、后翻写 " 双阶段策略的一部分，降低单点风险。

 - 暂无高价值评论线程

# 风险与影响

- 风险：
 1. **核心路径变更**：`server_args.py` 与 `arg_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_groups` 与 `server_args` 的修改须遵循 " 解析期读视图、写走声明 " 的纪律；`model_config.py`、`expert_pack_runtime.py`、`dllm/config.py`、lora policy 等下游读取者同步切换。
 - 风险标记：核心路径变更 , 跨模块重构 , 两处行为变化 , 大规模机械替换 , 依赖后续翻转

# 关联脉络

- PR #36255 config: ServerArgs holds the raw input: 同系列前序 PR，让 ServerArgs 只存原始输入；本 PR 正是建立在该前提上，把解析期读取从字段切到声明 stash。
- PR #36254 config: the runtime readers take the published bags: 同系列 PR，把运行时读者切换到发布的配置 bags；本 PR 处理 resolution 期间的读者，二者互补覆盖不同阶段的读取路径。
- PR #36250 config: spell the parallel config tier at the call site: 同系列 PR，显式隔离 parallel 配置层与 live 拓扑，与 server_args/arg_groups 有文件交叠，共同推进声明式配置改造。