Prhub

#31811 config: make ServerArgs read-only with a single audited mutation entry

原始 PR 作者 ch-wan 合并时间 2026-07-22 16:16 文件变更 7 提交数 1 评论 10 代码增减 +171 / -14

执行摘要

ServerArgs 变为只读,新增 get_context().override 审计入口

引入结构化的配置 API,通过领域命名空间读取解析后的配置,并提供一个审计的单一路径来变更运行时配置,确保 ServerArgs 保持为启动时的只读记录,防止读写不一致。

该 PR 是配置重构系列的关键步骤,具有重要的设计决策(强制只读、单一审计入口)。推荐精读 runtime_context.py 中的 override 实现和 server_args.py 中的守卫变更。技术管理者应关注未迁移路径的及时处理,避免回归。注意到同一系列的多个 PR 正在并行推进,建议持续追踪。

讨论亮点

Review 由 Codex 机器人自动生成,主要指出:

  • P1 风险:新的无条件守卫会破坏未迁移的直接赋值路径,如模型网关工作线程、文档补丁等,必须迁移到 override;编码器和 GDN 路径已迁移但还有遗漏。
  • P2 设计问题:来源日志返回浅拷贝,调用者可篡改历史记录;重发布时覆盖日志被清空,可能导致外层覆盖丢失;只读守卫无法保护可变对象的内部变更(如列表追加)。
    • 作者已修复部分指出的路径(encoder、GDN),但其他路径(模型网关、文档补丁)未处理,存在回归风险。

实现拆解

实现按以下步骤进行:

  1. RuntimeContext 新增 override 入口:在 runtime_context.py 中添加 _overrides_log 属性和 override(source, **fields) 方法。每个字段通过 namespace_of 查找命名空间路径,定位到配置 bag,验证字段存在后一次性写入,并记录来源。若任意字段未知或未发布,则完全回滚,保证原子性。
  2. ServerArgs 守卫强化:在 server_args.py 中修改 __setattr__,移除对 SGLANG_STRICT_CONFIG_MUTATION 环境变量的判断,使解析后的裸字段赋值直接引发 AttributeError,强制开发者使用 override 方法。
  3. 生产调用迁移:将原来直接赋值 server_args 的代码改为调用 override,包括 GDN 后端默认 FlashInfer 设置(gdn_backend.py)、编码器 DP 工作线程 specialization(encode_server.py)、以及 VLM 引擎示例(token_in_token_out_vlm_engine.py 改为 CLI 参数)。
  4. 测试覆盖:新增 test_runtime_context_override.py,全面测试 override 的正向写入、跨命名空间路由、未知字段原子性、未发布时拒绝、来源日志记录、重发布重置、以及只读守卫。并更新 test_gdn_prefill_backend_policy.py 适配新 API。
文件 模块 状态 重要度
python/sglang/srt/runtime_context.py 配置层 modified 7.62
python/sglang/srt/server_args.py 配置层 modified 6.47
test/registered/unit/test_runtime_context_override.py 配置层 added 7.65
python/sglang/srt/layers/attention/linear/gdn_backend.py 注意力层 modified 5.39
python/sglang/srt/disaggregation/encode_server.py 编码器 modified 5.22
examples/runtime/token_in_token_out/token_in_token_out_vlm_engine.py 示例 modified 5.17
test/registered/unit/layers/attention/test_gdn_prefill_backend_policy.py 测试 modified 4.99

关键符号

RuntimeContext.override RuntimeContext.overrides_log ServerArgs.__setattr__ maybe_set_default_flashinfer_gdn_prefill run_dp_worker

关键源码片段

python/sglang/srt/runtime_context.py dependency-wiring

核心变更:新增 override 方法和 overrides_log 属性,是配置变更的唯一审计入口。

def override(self, source: str, **fields) -> None:
    """The business mutation entry: write resolved config
    leaves onto the namespace bags — the single source of truth. It does
    **not** touch ``server_args`` (the pristine startup record) and there is
    no write-through, so the old "wrote one store, read another" desync class
    cannot occur.    Each flat field name is routed to its bag by the ``NS`` metadata (flat
    names are unique across namespaces). Validation is all-or-nothing: an
    unknown / unprojected field aborts before any write. ``source`` is
    recorded for provenance / reproduction.
    """
    if not fields:
        return
    bags = self._config_bags
    if bags is None:
        raise ValueError("config not published; cannot override")
    from sglang.srt.arg_groups.arg_utils import namespace_of
​
    nsmap = namespace_of(type(self._server_args))
    targets = [] # (bag, leaf, value) — resolved before any write
    for name, value in fields.items():
        path = nsmap.get(name)
        if path is None:
            raise ValueError(
                f"override: unknown config field {name!r} (no NS namespace) — "
                "not a resolved config leaf"
            )
        parts = path.split(".")
        bag = bags.get(parts[0])
        if bag is None:
            raise ValueError(f"override: namespace {parts[0]!r} not published")
        for seg in parts[1:]:
            bag = object.__getattribute__(bag, "_subs").get(seg)
            if bag is None:
                raise ValueError(
                    f"override: subgroup {seg!r} missing under {path!r}"
                )
        if name not in bag:
            raise ValueError(f"override: field {name!r} not projected on {path!r}")
        targets.append((bag, name, value))
    for bag, name, value in targets:
        bag._set(name, value)
    self._overrides_log.append((source, dict(fields)))def overrides_log(self) -> list:
    """Provenance of post-publish ``override`` calls: ``[(source, {field: value})]``."""
    return list(self._overrides_log)
python/sglang/srt/server_args.py dependency-wiring

关键改动:使 ServerArgs 解析后只读,裸字段赋值无条件引发 AttributeError。

def __setattr__(self, name, value):
    # after materialization the fields are the resolved startup
    # configuration -- the pristine, READ-ONLY record. A bare assignment
    # outside ServerArgs.override() (and the resolution pipeline, which runs
    # before materialization) always raises; resolved config is mutated on
    # the context bags via get_context().override(...), not here. (Formerly
    # gated on SGLANG_STRICT_CONFIG_MUTATION; now unconditional.)
    if (
        not name.startswith("_")
        and getattr(self, "_declarations_materialized", False)
        and not getattr(self, "_in_override", False)
    ):
        raise AttributeError(
            f"server_args.{name} assigned after resolution; server_args is "
            "read-only -- use get_context().override(source, ...) to change "
            "resolved config."
        )
    object.__setattr__(self, name, value)

评论区精华

未迁移的赋值路径会崩溃 正确性

Codex 机器人指出模型网关、文档补丁、SM100 GDN fallback、编码器等路径在无条件守卫下会立即崩溃,要求迁移到 override。

结论:作者已迁移编码器和 GDN 路径,但模型网关和文档补丁未处理,存在风险。 · partially-resolved

来源日志返回浅拷贝可篡改 安全

Codex 指出 overrides_log() 返回浅拷贝,调用者可修改内部字典,破坏审计日志完整性。

结论:未在 PR 中修复,建议返回深拷贝或不可变记录。 · unresolved

重发布时覆盖日志清空导致外层覆盖丢失 正确性

Codex 指出若在 override 后重发布,set_server_args 清空 _overrides_log,外层覆盖丢失。

结论:PR 中 set_server_args 重置日志,未保留。需考虑作用域嵌套场景。 · unresolved

只读守卫对可变对象内部变更无效 正确性

Codex 指出可变对象如列表追加不会触发 __setattr__,绕过守卫。

结论:当前未保护,需额外防御性编程。 · unresolved

风险与影响

  1. 未迁移赋值路径崩溃:模型网关(launch_server.py)、文档补丁(doc_patch.py)、某些测试夹具等仍直接赋值 server_args,在解析后会触发 AttributeError,导致启动失败。
  2. 读写不一致:当前 override 只写入配置 bag,但生产代码仍从 server_args 读取(如 Scheduler 读 pp_max_micro_batch_size),导致 override 无效。
  3. 原子性局限:override 的 all-or-nothing 基于 Python 的本地决策,没有分布式事务支持。
  4. 来源记录可篡改overrides_log() 返回浅拷贝,内部字典仍是可变引用。
  5. 缺少 rollback 机制:override 写入后没有提供撤销或回滚。

对开发者:所有运行时配置变更必须通过 get_context().override(),不能直接赋值 server_args。对系统:配置变更可审计,避免 ServerArgs 被意外修改。向后兼容性:第三方代码直接赋值 server_args 会立即失败,需迁移。测试:新增专用测试文件确保行为正确。迁移范围:影响模型加载、调度器、注意力后端、编码器、示例等模块。

核心路径变更 读写不一致风险 强制修改遗留代码 缺少部分迁移

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论