Prhub

#34819 config: the post-publish consumers of the supplied-instance surface read the bags

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

执行摘要

配置读取从 server_args 迁移到 bags,覆盖 17 个消费点

PR body 明确指出这是 step-12 配置迁移的一部分:supplied-instance surface 上的读取应改为从已发布的 bags 读取,否则 ServerArgs 一旦持有用户原始输入,读到已解析字段的调用方会看到 raw 值而非 effective 值。PR 提到一个关键反例:dspark worker 在实例读取 server_args.page_size 下方三十行却读 get_exec().graph.cuda_graph_config.decode.bs,同一文件对同一值来源自相矛盾。另有一个 Codex 捕获的真实崩溃:ModelRunner 独立构造(python -m sglang.benchmark.one_batch、手动 runner 测试)时 constructor 自身的 publish 位于本次读取之下,触发 ValueError: config namespace 'schedule' not published

值得精读。它是 sglang 配置系统迁移(supplied-instance → bags)的样板 PR:转换边界划分清晰(四批 + 保留清单),每批都有明确理由;对“何时读 bag、何时读 record”给出了可复用的判别标准(post-publish 纯消费读 bag;build-from-record 契约与 publish 前路径保留)。特别值得关注 ModelRunner publish 上移的时点论证和 dspark worker 的自相矛盾读取案例。建议结合 #34267(暴露面钉测)和 #34269(bag 契约测试)一起读,理解整个迁移栈的闭环。

讨论亮点

本 PR 无 GitHub review 评论(review_comments_count=0),讨论主要沉淀在 PR body 与被替换的 #34268 旧线程中。可从 body 提炼的要点包括:

  • dspark worker 自相矛盾的读取:文件内三十行之下已读 get_exec().graph.cuda_graph_config.decode.bs,上方却读 server_args.page_size,是同一值来源不一致的最清晰例证。
  • review nit:deep-gemm warmup 原本五次重读 bag,现改为每个分支绑定一次局部变量。
  • Codex 捕获的独立构造崩溃ModelRunner 独立构造(benchmark/one_batch、手动 runner 测试)时 constructor publish 位于读取之下,触发 ValueError: config namespace 'schedule' not published,本 PR 将 publish 上移。
  • 刻意保留清单:PR body 明确列出保留不转换的工厂/helper 及其理由(契约是 build from record、运行在 publish 之前等)。

实现拆解

实现按四批转换推进,每批对应 disposition 表的一行:

  1. 七个 speculative worker 构造器的 page_size 读取dspark_worker_v2.pyfrozen_kv_mtp_worker_v2.pydflash_worker_v2.pymulti_layer_eagle_worker_v2.pyngram_worker.pystandalone_worker_v2.pyeagle_worker_v2.pyself.page_size = server_args.page_size 改为 get_schedule().page_size。这些 worker 都在 publish 之后运行,且只是拷贝一个进程级值,因此是纯消费转换。
  2. post-publish 的 chunked_prefill_size 消费者expert_distribution.py(EPLB recorder 缓冲大小)、compile_utils.pyupdate_deep_gemm_config(deep-gemm 编译 warmup 的五处读取)、kv_cache_builder.pybuild_kv_cache(effective size)、ngram_embedding_manager.pyNgramEmbeddingManager.from_model(assert)全部改为 get_schedule().chunked_prefill_size;deep-gemm warmup 顺带把五次重复读取收敛为每个分支绑定一次局部变量(review nit 要求)。
  3. graph/limit 消费者dspark_worker_v2.pyDsparkVerifyEpilogue(max_bs=...) 改读 get_exec().graph.cuda_graph_config.decode.bsdspark_planner.py 的 SPS table bound(max_running_requests)经 build_sps_cost_table 的调用方测试改为通过 bag override 提供,lora_manager.pyinit_lora_cuda_graph_moe_buffers 改读 get_exec().graph...
  4. 三个进程级构造器的 page_sizemodel_runner.pyscheduler.pydecode_kvcache_offload_manager.pyModelRunner.__init__set_global_server_args_for_scheduler(server_args) 从构造函数尾部上移到 self.page_size = get_schedule().page_size 之前(仅 target worker,is_draft_worker 分支不变),保证独立构造也能先发布再读取;offload manager 顺带删除了对 server_args 的整对象驻留。

测试配套test/registered/spec/dspark/test_dspark_sps_table.py_build_sps_cost_table_for 不再注入 SimpleNamespace 伪记录,而是通过 get_context().override_server_args(...)speculative_dspark_sps_table_pathmax_running_requests 发布到 bags,再取 get_server_args() 传给 build_sps_cost_table,并在 addCleanup 中恢复 override,确保 override 不泄漏到其他测试。

刻意保留不转换的:契约是“从交给你的 record 构建 X”的工厂(create_kt_config_from_server_argsDllmConfig.from_server_argsCanaryLaunchCapacities.from_argsbuild_compilation_config)、resolution pipeline 以 resolved_view 调用的 helper(utils/common 的 topk/page 谓词)、由调用方传入配置的 allocation_sizing、per-instance 边界(CudaVmmFeatureTransport、DP controller、encode-server 家族)以及历史上先于 publish 运行的 initialize_moe_config

文件 模块 状态 重要度
python/sglang/srt/model_executor/model_runner.py 模型执行 modified 6.52
python/sglang/srt/layers/deep_gemm_wrapper/compile_utils.py 编译预热 modified 6.19
test/registered/spec/dspark/test_dspark_sps_table.py 测试 modified 5.28
python/sglang/srt/speculative/dspark_worker_v2.py 推测解码 modified 6.0
python/sglang/srt/speculative/frozen_kv_mtp_worker_v2.py 推测解码 modified 5.35
python/sglang/srt/model_executor/model_runner_components/ngram_embedding_manager.py 嵌入管理 modified 5.37
python/sglang/srt/disaggregation/decode_kvcache_offload_manager.py KV 卸载 modified 5.23
python/sglang/srt/lora/lora_manager.py LoRA modified 5.23
python/sglang/srt/mem_cache/kv_cache_builder.py KV 缓存 modified 5.23
python/sglang/srt/speculative/dflash_worker_v2.py 推测解码 modified 5.23

关键符号

ModelRunner.__init__ update_deep_gemm_config NgramEmbeddingManager.from_model DSparkWorkerV2.__init__ FrozenKVMTPDraftWorker.__init__ FrozenKVMTPWorkerV2.__init__ DFlashWorkerV2.__init__ build_kv_cache init_lora_cuda_graph_moe_buffers DecodeKVCacheOffloadManager.__init__ _build_sps_cost_table_for

关键源码片段

python/sglang/srt/model_executor/model_runner.py data-contract

核心构造器:`page_size` 读取从 `server_args` 改为 `get_schedule()`,且 `set_global_server_args_for_scheduler` 从构造尾部上移到首次 bag 读取之前,修复独立构造(benchmark/one_batch、手动 runner 测试)时的 not-published 崩溃;publish 时点调整是全 PR 最敏感的控制流变更。

# python/sglang/srt/model_executor/model_runner.py
# __init__ 开头部分的关键时序调整:
# 旧代码在构造函数尾部才调用 set_global_server_args_for_scheduler,
# 而 page_size 读取发生在其之前;当 ModelRunner 被独立构造时
# (python -m sglang.benchmark.one_batch、手动 runner 测试)
# 没有更早的 publish,会触发 ValueError: config namespace 'schedule' not published。
# 新代码把发布动作上移,确保任何 bag 读取前都已发布。def __init__(
    self,
    model_config: ModelConfig,
    mem_fraction_static: float,
    gpu_id: int,
    ps: ParallelState,
    nccl_port: int,
    server_args: ServerArgs,
    is_draft_worker: bool = False,
    req_to_token_pool: Optional[ReqToTokenPool] = None,
    token_to_kv_pool_allocator: Optional[BaseTokenToKVPoolAllocator] = None,
    memory_pool_config: Optional[MemoryPoolConfig] = None,
    draft_model_idx: Optional[int] = None,
    draft_attention_backend: Optional[str] = None,
):
    # ... 前置字段赋值省略 ...
​
    # 提前设置 scheduler 进程的全局 server_args(仅 target worker),
    # 放在构造器最早期的 bag 读取之前:独立构造没有更早的 publish 来源。
    if not is_draft_worker:
        set_global_server_args_for_scheduler(server_args)
​
    # ... 中间初始化省略 ...
​
    # page_size 从已发布的 schedule bag 读取,而不是从传入的 supplied record 读取;
    # 这样 post-publish 的 override 能像影响其他消费者一样影响本构造器。
    self.page_size = get_schedule().page_size
python/sglang/srt/layers/deep_gemm_wrapper/compile_utils.py dependency-wiring

deep-gemm 编译 warmup 的五处 `chunked_prefill_size` 读取全部从 `server_args` 切换到 `get_schedule()`,并按 review nit 收敛为每个分支一次绑定,避免重复读 bag。

# python/sglang/srt/layers/deep_gemm_wrapper/compile_utils.py
# update_deep_gemm_config 的 warmup 分支:
# 原先五处读取都从 server_args(supplied record)取 chunked_prefill_size,
# 现改为从已发布的 schedule bag 读取,并在每个分支开头绑定一次局部变量,
# 避免对同一 bag 的重复读取(review nit)。def update_deep_gemm_config(gpu_id: int, server_args: ServerArgs):
    global _BUILTIN_M_LIST
    global _DO_COMPILE_ALL
    global _IS_FIRST_RANK_ON_NODE
​
    _BUILTIN_M_LIST = []
​
    if _FAST_WARMUP:
        # 快速预热:覆盖 decode 常用小 batch,再按步长采样覆盖大 batch。
        _BUILTIN_M_LIST += list(range(1, 1025))
​
        next_m, sample_step = 1024, 2
        # 每个分支只读一次 bag,后续计算复用局部变量。
        chunked_prefill_size = get_schedule().chunked_prefill_size
        max_prefill_bs = (
            min(chunked_prefill_size, 32 * 1024)
            if chunked_prefill_size >= 1
            else 16 * 1024
        )
        while next_m < max_prefill_bs:
            _BUILTIN_M_LIST += list(range(next_m, 2 * next_m, sample_step))
            next_m = next_m * 2
            sample_step = sample_step * 2
        _BUILTIN_M_LIST.append(max_prefill_bs)
        _BUILTIN_M_LIST = sorted(list(set(_BUILTIN_M_LIST)))
    else:
        # 非快速预热:按 m_max 覆盖全部 M;chunked_prefill_size 决定上限。
        m_max = 1024 * 16
        chunked_prefill_size = get_schedule().chunked_prefill_size
        if chunked_prefill_size < 1:
            m_max = 1024 * 64
        elif chunked_prefill_size > 8192:
            m_max = chunked_prefill_size * 2
        m_max = min(1024 * 128, m_max)
        _BUILTIN_M_LIST += list(range(1, m_max + 1))
​
    _IS_FIRST_RANK_ON_NODE = server_args.base_gpu_id == gpu_id
    _DO_COMPILE_ALL = _IS_FIRST_RANK_ON_NODE
test/registered/spec/dspark/test_dspark_sps_table.py test-coverage

唯一的测试配套改动:`_build_sps_cost_table_for` 从注入 `SimpleNamespace` 伪记录改为通过 `get_context().override_server_args(...)` 发布到 bags,并在 `addCleanup` 恢复,验证了本 PR 的消费方转换在测试中的正确接线方式。

# test/registered/spec/dspark/test_dspark_sps_table.py
# 被测函数 build_sps_cost_table 现在从已发布的 bags 读取 max_running_requests,
# 因此不能再像以前那样注入 SimpleNamespace 伪记录;
# 测试改为通过 override 机制发布真实配置,并在用例结束后恢复,
# 避免 override 泄漏到同进程的其他测试。def _build_sps_cost_table_for(testcase, *, sps_table_path):
    from sglang.srt.runtime_context import get_context, get_server_args
    from sglang.srt.speculative.dspark_components.dspark_planner import (
        build_sps_cost_table,
    )
​
    # 表格上界读取的是已发布的 bag 值,所以测试发布它;
    # table 路径仍由 build_sps_cost_table 从传入的 record 读取,保持不变。
    override = get_context().override_server_args(
        speculative_dspark_sps_table_path=sps_table_path,
        max_running_requests=4,
    )
    override.install()
    testcase.addCleanup(override.restore)
    return build_sps_cost_table(
        server_args=get_server_args(), verify_num_draft_tokens=5
    )

评论区精华

dspark worker 内同一值来源自相矛盾 设计

PR body 指出 dspark worker 三十行之下已读 `get_exec().graph.cuda_graph_config.decode.bs`,上方却读 `server_args.page_size`,同一文件对同一类值来源不一致。

结论:两类读取统一改为从 bags 读取:`page_size` 走 `get_schedule()`,graph 配置走 `get_exec()`。 · 已解决

deep-gemm warmup 重复读取 bag style

review nit:`update_deep_gemm_config` 原本对 `chunked_prefill_size` 的重读五处,要求收敛。

结论:每个分支绑定一次局部变量,后续计算复用。 · 已解决

ModelRunner 独立构造时 not-published 崩溃 正确性

Codex 捕获:`ModelRunner` 独立构造(benchmark/one_batch、手动 runner 测试)时 constructor 自身 publish 位于 page_size 读取之下,触发 `ValueError: config namespace 'schedule' not published`。

结论:`set_global_server_args_for_scheduler` 上移到构造器早期、首次 bag 读取之前;scheduler 路径不受影响。 · 已解决

刻意保留不转换的工厂与 helper 设计

PR body 列出保留清单:resolution pipeline 的 helper(`utils/common` 的 topk/page 谓词)、build-from-record 契约的工厂(`create_kt_config_from_server_args`、`DllmConfig.from_server_args`、`CanaryLaunchCapacities.from_args`、`build_compilation_config`)、`allocation_sizing`、per-instance 边界、以及 publish 前运行的 `initialize_moe_config`。

结论:这些读取按契约保留在 record 上,不纳入本批转换。 · 已解决

测试改用 override 发布而非 SimpleNamespace 伪记录 测试

`test_dspark_sps_table.py` 的 `_build_sps_cost_table_for` 原先注入 `SimpleNamespace`,因 `build_sps_cost_table` 现读 bag 值而失效;改为 `get_context().override_server_args(...)` 发布并 `addCleanup` 恢复。

结论:采用真实发布 + 恢复的接线方式,override 不泄漏出测试。 · 已解决

风险与影响

主要风险点:

  • publish 时点依赖ModelRunnerset_global_server_args_for_scheduler 上移到构造器早期,若未来有人在该调用之前新增 bag 读取(如 enable_hisparse 等仍在下方),会再次触发 not-published 崩溃。当前 page_size 读取已在 publish 之后,但 ModelRunner 中其余 server_args.* 读取(如 elastic_ep_backendenable_hisparse)未一并转换,存在部分读取走 record、部分走 bag 的混合态。
  • 覆盖不彻底:census 共 314 对,本 PR 只转换 17 对,剩余 297 对靠 #34267 的 ratchet 钉住;任何漏网读取在 ServerArgs 持有 raw 输入后可能静默读到未解析值。
  • 测试泄漏风险test_dspark_sps_table.py 改用 override.install() 发布全局状态,虽用 addCleanup(override.restore) 保证恢复,但若 override 机制本身有异常路径,可能污染同进程其他测试。
  • bag 未发布即读取:若存在跳过 scheduler publish 的启动路径(除已知的 standalone ModelRunner 外),get_schedule() 会抛错;PR 已修复已知路径,但未全面枚举所有启动入口。

影响范围:

  • 对用户:无用户可见行为变化,纯内部配置读取路径重构;唯一功能语义变化是 post-publish override 现在能影响此前从 record 读取的 17 个消费点,这正是预期目标。
  • 对系统:涉及 speculative decoding(EAGLE、DFlash、DSpark、Frozen-KV MTP、ngram、standalone)、MoE、LoRA、KV cache 构建、EPLB 调度、deep-gemm 编译 warmup、disaggregation offload 等多个子系统,覆盖面广但每个改动都是单一字段的来源替换,回归面可控。
  • 对团队:为 step-12 配置迁移扫清了生产侧障碍,使后续 #34267 的暴露面钉测能以最终形态落位;同时确立了“进程级值读 bags、record 契约读 record”的处置规范,后续新增代码有章可循。
配置读取路径重构 publish 时点敏感 混合读取态残留 测试依赖全局 override

关联 Issue

#34267 config: pin the supplied-instance surface that a raw record would change

完整报告

参与讨论