Prhub

#36789 config: the resolution pipeline moves out of the record

原始 PR 作者 ch-wan 合并时间 2026-08-29 01:17 文件变更 30 提交数 10 评论 1 代码增减 +6898 / -5674

执行摘要

解析管线迁出 ServerArgs,巨型类拆分至 arg_groups 钩子模块

ServerArgs 膨胀到 11327 行,其中约 3000 行是 483 个字段声明(即 record),其余大部分是住在同一类内的解析管线。PR body 的核心论点是:"a decision no longer has to live next to the field it decides — it can live next to its family"——record 只保存操作者的原始输入,解析以声明(declare_resolution)而非写入的方式产生结果。PR 明确声明 "Nothing here changes what resolution decides",这是一次纯结构迁移。关联 Issue #36618 是五 PR 系列的第一篇,本 PR 建立在其成果(record 只持有原始输入、解析答案进入声明库)之上,把解析体整体搬出 record,并顺带修复两个通道仍能写回字段的历史遗留。

值得精读。这是处理巨型配置类的教科书式重构:声明式解析(declare_resolution)、槽位保留签名、测试守卫跟随迁移、快照式行为等价验证(62 启动形状 × 478 字段 provenance)都是可复用的方法论。重点阅读 model_hook.pycuda_graph_hook.py 的写法,以及 PR body 中两条"the hard way 学到的规则"。同时留意 codex P2 评论暴露的测试可达性盲区,后续系列 PR 需要补上对 server_args._… 调用的追踪。

讨论亮点

仅有一条来自 chatgpt-codex-connector[bot] 的 review 评论(P2),针对测试工具的可达性分析盲区:

在递归进入 hook 目标后,reaches() 仍只跟随接收者拼写为 self 的调用,而迁移后的 hooks 通过 server_args._… 调用兄弟 record 方法(例如 handle_cuda_graph_config 调用 server_args._parse_cuda_graph_config()model_hook 调用 server_args._set_default_dsa_backends())。因此可达性图在那些 hook 体处停止,model-config 排序守卫可能静默漏掉声明。

该评论未获作者回复,PR 已合并,问题在合并时仍为开放状态。

实现拆解

实现拆解

  1. 迁移基础设施(前序已建)arg_groups/ 在前序 PR 中已建立 declare_resolutionresolving_viewresolved_view 原语、pass registry 以及 run_post_process_pass。本 PR 的每个处理函数以 server_args 为唯一入参,通过 resolving_view 只读原始配置、通过 declare_resolution 声明解析结果,绝不直接写字段。

  2. 按族迁移 15 个钩子模块(13 个新增):每个 commit 移动一个功能族,并在原槽位留下调用。模块与职责对应如下:

模块 决定什么
kv_cache_hook.py KV-cache dtype、池兼容性、page-major 布局
parallel_hook.py context / decode-context 并行、DP、DWDP、elastic EP、EPLB
serving_hook.py SSL、ASR、multimodal 配置、崩溃转储环境变量、媒体 URL 安全、grammar 后端等
mamba_hook.py Mamba 后端、int8 checkpoint 规则、额外 buffer 检查
cuda_graph_hook.py --cuda-graph-* 解析、capture 后端兼容规则、DeepEP bucket 对齐、分架构 prefill 默认值
hicache_hook.py DCP 与存储布局兼容性、host-memory 模式、ratio 默认值
platform_hook.py NPU / MPS / AMD / XPU / CPU / HPU 默认值、NCCL pre-warm
moe_hook.py MoE 内核规则、a2a 后端、DeepEP-v2 与 cutedsl 预算
model_path_hook.py 权重来源、加载格式、传输引擎
attention_hook.py 后端兼容性、linear attention、multi-item scoring、确定性推理
model_hook.py per-model / per-capability 调整、mamba radix cache、language-model-only 规则
memory_hook.py GPU 内存预算
dllm_hook.py diffusion-LM 推理
lora_hook.py LoRA 参数检查与 LoRA/speculative 规则
validation_hook.py 跨族校验:check_server_args、bucket 规则解析、IB 设备列表、experimental marlin 门控、prefill/decode 间隔、two-batch overlap
  1. 槽位纪律(两条规则):① 迁移函数对 record 仍拥有的东西继续调用 record——兄弟处理器通过槽位、server_args._resolved_attention_backends()server_args.get_model_config();内联这些会产生生产环境一致的代码,但会静默破坏测试 patch 的 seam。② 槽位保留方法自身签名和返回值——_handle_mamba_radix_cache 接受参数、_validate_ib_devices 返回值,丢弃任一会让槽位变成静默错误答案而非崩溃。

  2. 测试守卫跟随迁移:移动 handler 也移动监视它的 guards。test_model_config_reads_resolved_input 改为跟随槽位进入 arg_groups/test_resolution_reads_no_bagtest_resolution_is_reproducible 从整个包开始 seed walk;test_chain_read_ratchet 也在 arg_groups/ 下扫描 _late_resolution 调用点;test_resolution_reads_no_bag 还修正了 utils.common.get_device 与同名 device bag 访问器的混淆。

  3. 随迁修复的预存缺陷:三个检查迁入 arg_groups/ 后落在原类中不适用的 guard 下,暴露出真实缺陷——check_load_publish_args 读原始字段而非解析视图;两个 mamba 校验器被传入整个 record(一个什么都不需要、一个只需要一个数字)。另修复已合并代码的 bug(codex 在 #36618 报告):run_post_process_pass 在运行 pass 前拒绝已发布的 record,导致 Engine(server_args=sa)Engine.shutdown() 后第二次启动报错。

  4. 验证体系:62 种启动形状运行 resolve_once() + check_server_args() 并对比异常类型与完整消息(28 个触发本 PR 移动的检查);24 种启动形状 × 478 字段对比解析值与 provenance;159 个提及 record 的注册测试在 tip 与 base 上失败集合一致。

文件 模块 状态 重要度
python/sglang/srt/server_args.py 参数解析 modified 7.92
python/sglang/srt/arg_groups/model_hook.py 模型解析 added 9.28
python/sglang/srt/arg_groups/serving_hook.py 服务配置 added 8.98
python/sglang/srt/arg_groups/parallel_hook.py 并行配置 added 8.98
python/sglang/srt/arg_groups/model_path_hook.py 路径解析 added 9.08
python/sglang/srt/arg_groups/cuda_graph_hook.py 图捕获 added 8.98
python/sglang/srt/arg_groups/validation_hook.py 跨族校验 added 8.98
test/registered/unit/server_args/test_model_config_reads_resolved_input.py 测试守卫 modified 5.0

关键符号

handle_model_specific_adjustments handle_model_capability_adjustments handle_modelscope_paths resolve_hf_gguf_model_path handle_attention_backend_compatibility parse_cuda_graph_config apply_cuda_graph_compatibility handle_context_parallelism handle_moe_kernel_config check_server_args run_post_process_pass handle_hicache

关键源码片段

python/sglang/srt/arg_groups/model_hook.py data-contract

新增 856 行,承载最复杂的 per-model / per-capability 调整逻辑,是本 PR 数据契约(data-contract)的核心示例:resolving_view 只读、declare_resolution 声明、通过槽位调用兄弟处理函数并保留签名。

def handle_model_specific_adjustments(server_args: Any):
    # resolving_view 是只读窗口:读到的仍是操作者的原始输入,
    # 任何解析产物都必须通过 declare_resolution 声明,绝不直接写字段
    cfg = resolving_view(server_args)
​
    if cfg.enable_deterministic_inference:
        # 声明式解析:第一个参数是槽位名,后续是待声明的解析结果
        declare_resolution(
            server_args,
            "_handle_model_specific_adjustments",
            enforce_disable_flashinfer_allreduce_fusion=True,
        )
​
    declare_resolution(
        server_args,
        "_handle_model_specific_adjustments",
        uses_mamba_radix_cache=False,
    )
​
    # 实例连接器(instance connector)没有 hf_config,无法按模型架构做覆盖
    if parse_connector_type(cfg.model_path) == ConnectorType.INSTANCE:
        return
​
    model_config = server_args.get_model_config()
    hf_config = model_config.hf_config
    model_arch = hf_config.architectures[0]
​
    if model_arch == "InternS2MobiusForConditionalGeneration":
        unsupported = []
        if cfg.pp_size != 1:
            unsupported.append("pipeline parallelism (--pp-size must be 1)")
        if cfg.ep_size != 1:
            unsupported.append("expert parallelism (--ep-size must be 1)")
        if unsupported:
            raise ValueError(
                "Intern-S2-Mobius does not support: " + "; ".join(unsupported) + "."
            )
​
    # 通过 record 槽位调用兄弟处理函数并保留其入参签名;
    # 若内联这段逻辑,测试就失去可以 patch 的 seam
    _hybrid_spec = get_linear_attn_spec_by_arch(model_arch)
    if _hybrid_spec is not None and _hybrid_spec.uses_mamba_radix_cache:
        server_args._handle_mamba_radix_cache(model_arch=model_arch)
​
    # 从声明库收集该架构的模型覆盖项(registry),覆盖只作用于原始配置;
    # server_args 本身不被修改 —— 解析中间过程的读取方通过 resolved_view
    # 看到已声明的值,运行时读取方通过 flags 层级看到
    from sglang.srt.arg_groups.overrides import (
        collect_model_override_declarations,
        validate_declarations,
    )
​
    model_overrides = collect_model_override_declarations(
        model_arch, server_args, hf_config
    )
    validate_declarations(server_args, model_overrides)
    server_args._resolved_overrides.extend(model_overrides)
​
    if model_arch in ("KimiLinearForCausalLM", "KimiK3ForConditionalGeneration"):
        from sglang.srt.arg_groups.kimi_k3_hook import (
            apply_kimi_k3_linear_attn_defaults,
            apply_kimi_k3_spec_backend_defaults,
        )
​
        apply_kimi_k3_linear_attn_defaults(server_args)
        apply_kimi_k3_spec_backend_defaults(server_args)
python/sglang/srt/arg_groups/model_path_hook.py data-contract

新增 306 行,负责权重来源解析(HF/GGUF 引用、ModelScope 缓存回退与下载、对象存储 URI),含真实副作用(下载),是 data-contract 迁移的另一个代表。

def handle_modelscope_paths(server_args: Any):
    """尽可能从本地 ModelScope 缓存解析模型 / tokenizer / 草稿模型路径,
    磁盘上不存在的路径回退到 snapshot_download。    注意:speculative_token_map 故意不在这里处理,因为它的值用的是
    repo_id/filename 语义而不是纯 repo ID;那部分解析在
    sglang.srt.speculative.spec_utils.load_token_map 里。
    """
    cfg = resolving_view(server_args)
    ms_root = None
    ms_snapshot_download = None
​
    def _resolve_or_download(
        path: Optional[str],
        ignore_patterns: Optional[list] = None,
        revision: Optional[str] = None,
    ) -> Optional[str]:
        # 延迟导入 modelscope,避免非 ModelScope 场景的启动开销;
        # 模块级引用缓存避免重复 import
        nonlocal ms_root, ms_snapshot_download
        if path is None:
            return None
        if not path or os.path.exists(path):
            return path
​
        if ms_snapshot_download is None:
            from modelscope.hub.snapshot_download import (
                snapshot_download as _ms_snapshot_download,
            )
            from modelscope.utils.file_utils import get_model_cache_root
​
            ms_snapshot_download = _ms_snapshot_download
            ms_root = get_model_cache_root()
​
        # 先查 ModelScope 默认缓存,再查用户指定的 download_dir,最后才下载
        cached = os.path.join(ms_root, path)
        if os.path.exists(cached):
            return cached
        if cfg.download_dir:
            alt = os.path.join(cfg.download_dir, path)
            if os.path.exists(alt):
                return alt
​
        # 缓存未命中 —— 从 ModelScope hub 下载
        return ms_snapshot_download(
            path,
            cache_dir=cfg.download_dir,
            revision=revision,
            **({"ignore_patterns": ignore_patterns} if ignore_patterns else {}),
        )
​
    # 解析结果通过 declare_resolution 声明,record 本身保持原始输入不变
    declare_resolution(
        server_args,
        "_handle_modelscope_paths",
        model_path=_resolve_or_download(cfg.model_path, revision=cfg.revision),
    )
    # tokenizer_path / speculative_draft_model_path 走同样的 _resolve_or_download 声明,
    # 此处省略;每个槽位保留原方法的槽位名以兼容既有测试 patch 点
python/sglang/srt/arg_groups/cuda_graph_hook.py dependency-wiring

新增 455 行,展示最复杂的配置解析模式:`--cuda-graph-*` 参数按优先级合并成 CudaGraphConfig,并用 `_cuda_graph_config_locked` 集合记录非默认来源,使自动禁用级联尊重用户显式设置。

def parse_cuda_graph_config(server_args: Any):
    """把 --cuda-graph-* 参数解析成 CudaGraphConfig。    优先级(从高到低):显式 JSON > 便捷开关 > 旧式全局开关 > 默认值。
    同时填充 server_args._cuda_graph_config_locked —— 来自非默认来源的
    (phase, key) 集合;自动禁用级联会尊重这个锁(即旧
    --enforce-piecewise-cuda-graph 语义的泛化)。
    """
    cfg = resolving_view(server_args)
    raw_input = cfg.cuda_graph_config
    if isinstance(raw_input, CudaGraphConfig):
        explicit_input = raw_input.to_dict()
    else:
        explicit_input = raw_input or {}
    config = default_cuda_graph_config()
    locked: set = set()
​
    def _set(phase: str, key: str, value: Any) -> None:
        # 局部写入辅助:既改 config 又登记锁定,保证后续兼容性级联
        # 不会覆盖用户显式指定的值
        setattr(getattr(config, phase), key, value)
        locked.add((phase, key))
​
    # ---- 旧式全局开关(比默认值高,比便捷开关低)----
    if cfg.disable_cuda_graph:
        _set(Phase.DECODE, "backend", Backend.DISABLED)
        _set(Phase.PREFILL, "backend", Backend.DISABLED)
​
    # ---- 按相位关闭布尔开关 ----
    if cfg.disable_prefill_cuda_graph:
        _set(Phase.PREFILL, "backend", Backend.DISABLED)
    if cfg.disable_decode_cuda_graph:
        _set(Phase.DECODE, "backend", Backend.DISABLED)
​
    # ---- 按相位便捷开关 ----
    if cfg.cuda_graph_backend_decode is not None:
        _set(Phase.DECODE, "backend", cfg.cuda_graph_backend_decode)
    if cfg.cuda_graph_backend_prefill is not None:
        _set(Phase.PREFILL, "backend", cfg.cuda_graph_backend_prefill)
    if cfg.cuda_graph_max_bs_decode is not None:
        _set(Phase.DECODE, "max_bs", cfg.cuda_graph_max_bs_decode)
    if cfg.cuda_graph_max_bs_prefill is not None:
        _set(Phase.PREFILL, "max_bs", cfg.cuda_graph_max_bs_prefill)
​
    # ---- 显式 JSON 配置(最高优先级)----
    for phase, phase_config in explicit_input.items():
        if not isinstance(phase_config, dict):
            continue
        for key, value in phase_config.items():
            _set(phase, key, value)
​
    # 声明解析结果;locked 集合留给后续兼容性级联读取
    declare_resolution(
        server_args,
        "_parse_cuda_graph_config",
        cuda_graph_config=config,
    )
    server_args._cuda_graph_config_locked = locked

评论区精华

迁移后的钩子调用在测试可达性图中丢失 测试

codex reviewer 指出,在递归进入 hook 目标后,`reaches()` 仍只跟随接收者拼写为 `self` 的调用,而迁移后的 hooks 通过 `server_args._parse_cuda_graph_config()`、`server_args._set_default_dsa_backends()` 等调用兄弟 record 方法,导致可达性图在 hook 体处停止,model-config 排序守卫可能静默漏掉声明。

结论:无作者回复,PR 已合并;该测试盲区需要在后续系列 PR 中补上对 `server_args._…` 调用形式的追踪,否则新声明的解析顺序依赖可能绕过守卫。 · 待处理

风险与影响

  1. 测试工具可达性盲区(P2,未解决)test_model_config_reads_resolved_input.pyreaches() 只跟随 self.xxx 调用,而迁移后的钩子通过 server_args._parse_cuda_graph_config() 等调用兄弟方法,可达性图在 hook 体处截断,可能静默漏掉声明,直接削弱本 PR "移动 handler 也移动监视它的 guard" 这条纪律的完整性。
  2. 行为一致性依赖快照验证:62 启动形状只覆盖 28 个触发检查的形状的消息字符串;环境变量设置、对象存储下载、ModelScope 缓存回退等非异常行为的等价性主要靠 159 个注册测试兜底。
  3. 大文件迁移的评审难度:单模块新增量高达 400-900 行(serving_hook.py 906 行、model_hook.py 856 行),机械搬运的回归风险集中在无人值守的角落分支。
  4. 随迁修复改变了行为check_load_publish_args 与两个 mamba 校验器从读原始字段改为读解析视图,属行为变更,可能影响依赖旧行为的存量调用方。

影响范围覆盖所有通过 ServerArgs 启动的推理路径(模型加载、并行配置、attention / CUDA-graph / MoE / KV-cache 选择、serving 面校验),但本 PR 声明并验证了行为不变,实际影响主要在组织与维护层面:ServerArgs 从 11327 行减至 6028 行,配置逻辑按族归位到 arg_groups/,后续定位与新增配置项更清晰。对团队而言,这是五 PR 系列(#36618 起)的关键一步,直接为 #36792(移除转发槽位)和 #36791、#36790 的后续裁剪提供结构基础;对用户无感知,但配置校验错误的定位路径发生变化。

核心配置路径大规模重构 测试守卫可达性盲区(P2) 行为一致性依赖快照验证 随迁修复 3 个预存缺陷 五 PR 系列后续跟进

关联 Issue

#36618 config: resolution declares, and nothing writes a field

完整报告

参与讨论