# PR #36973 完整报告

- 仓库：`sgl-project/sglang`
- 标题：config: six more runtime readers ask the bags
- 合并时间：2026-08-29 19:19
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/36973

---

# 执行摘要

- 一句话：运行时 6 个模块改为从配置 bag 读参数
- 推荐动作：值得精读。这是 config 系列重构的关键一环，展示了「先理清数据流、再逐层去掉参数透传」的系统性方法。重点学习：
 1) 以「读取是原字段还是命名空间叶子」为判据决定可否迁移；
 2) 用 `is_config_namespace_published` 把「None 用默认」重语义化为「未发布用默认」；
 3) 对构造函数签名变更做全仓库调用点普查（含 `srt/` 之外）。建议在阅读时对照系列内 #36896、#36972、#36974、#36975 一起看，才能理解完整演进。

# 功能与动机

PR body 指出：`srt/` 全树普查发现 53 个文件中存在 295 处对持有的 `ServerArgs` 的原始字段读取，而这些字段全部已在发布的命名空间中。上一 PR 已把 resolution 包从 record 上摘除，本 PR 处理 runtime 部分。其中 `expert_distribution.py` 是最典型例子：`ExpertDistributionRecorder` 被传入 `ServerArgs` 并穿过 6 个构造函数只为 4 处读取，而文件本身已经通过 `get_exec().moe` 读取 recorder 模式，record 只是与 bag 并行旅行。去除 record 依赖是系列目标（系列 PR #36896→#36975）的一部分。

# 实现拆解

1. **EPLB 记录器去参**：`python/sglang/srt/eplb/expert_distribution.py` 中 `ExpertDistributionRecorder.init_new`、`_ExpertDistributionRecorderReal.__init__`、`_Accumulator.init_new`、`_SinglePassGatherer.init_new` 及子类共 6 个构造函数移除 `server_args` 参数；4 处读取改为 `runtime_context` 的 `reports_expert_balancedness()` 等 bag 访问器；`model_runner.py` 中 5 处调用点同步更新。
2. **三个 balancedness 谓词发布为 runtime_context 访问器**：`runtime_context.py` 新增 `reports_expert_balancedness`、`logs_expert_balancedness_to_server_log`、`exports_expert_balancedness_to_prometheus`，均读取 `get_exec().moe.expert_balancedness_report_mode`；对应删除 `ServerArgs.should_log_expert_balancedness_to_server_log` 与 `should_export_expert_balancedness_to_prometheus`。metrics_collector.py 与 metrics_reporter.py 改用新访问器。
3. **resolve_collector_class 改为读 bag**：`observability/metrics_collector.py` 中该函数签名从 `(server_args, role, default_cls)` 改为 `(role, default_cls)`，内部通过 `get_context().is_config_namespace_published("observability")` 判断命名空间是否发布，再读 `get_observability().stat_loggers`。三个调用点（含 `init_new`）同步更新。
4. **其余 6 个 runtime reader 迁移**：`hybrid_pool_assembler` 与 `scheduler` 改读 `get_memory().hicache_host_memory_mode`；`kt_ep_wrapper` 的 `create_kt_config_from_server_args` 改读 `get_exec().moe.kt_*` 与 `get_schedule().chunked_prefill_size`；`expert_backup_client` 改读 `get_parallel()`；`dspark_planner` 的 `build_sps_cost_table` 删除关键字参数并改读 `get_spec()` / `get_schedule()`；`prefill_delayer` 删除构造参数与存储属性，直接读 bag。
5. **调用点连带修复**：DSpark worker 构造 `DSparkVerifyPlanner` 的调用点（P1 级，否则启动即 `TypeError`），以及 registered 测试中的 prefill-delayer 与 SPS-table 用例改为通过 `override_server_args` 发布测试配置；删除不再需要的 `_StubArgs` 替身并改用真实 bag 发布。
6. **测试配套**：`test_stat_loggers_di.py` 重写 `TestResolveCollectorClass`，用 `get_context().override_server_args()` 注入 `stat_loggers`，新增 `test_returns_default_when_nothing_is_published`；62-shape 解析探针与基线字节级一致，受影响套件 3038 通过，失败集与 `main` 相同。

关键文件：
- `python/sglang/srt/observability/metrics_collector.py`（模块 指标采集；类别 source；类型 core-logic；符号 resolve_collector_class）: 核心 DI 解析函数 `resolve_collector_class` 从接收 `server_args` 改为从已发布的 observability bag 读取，涉及 3 个调用点，是本 PR 语义变化最集中的地方。
- `python/sglang/srt/runtime_context.py`（模块 运行时上下文；类别 source；类型 core-logic；符号 reports_expert_balancedness, logs_expert_balancedness_to_server_log, exports_expert_balancedness_to_prometheus）: 新增 3 个 expert-balancedness 发布访问器，替代 ServerArgs 上的方法，成为该配置的唯一读取入口。
- `python/sglang/srt/eplb/expert_distribution.py`（模块 专家分布；类别 source；类型 dependency-wiring）: 最典型的「record 旅行」消除案例：ServerArgs 参数从 6 个构造函数中移除，存储属性删除，读取全部走 bag。

关键符号：resolve_collector_class, reports_expert_balancedness, logs_expert_balancedness_to_server_log, exports_expert_balancedness_to_prometheus, ExpertDistributionRecorder.init_new, _Accumulator.init_new, _SinglePassGatherer.init_new, create_kt_config_from_server_args, build_sps_cost_table, PrefillDelayer.__init__, ExpertBackupClient.__init__

## 关键源码片段

### `python/sglang/srt/observability/metrics_collector.py`

核心 DI 解析函数 `resolve_collector_class` 从接收 `server_args` 改为从已发布的 observability bag 读取，涉及 3 个调用点，是本 PR 语义变化最集中的地方。

```python
# python/sglang/srt/observability/metrics_collector.py

def resolve_collector_class(role: str, default_cls: type) -> type:
    """返回 `role` 在已发布的 observability bag 中注册的收集器子类，
    未注册或命名空间未发布时返回 `default_cls`。

    注意：语义从「server_args=None 用默认」迁移为「命名空间未发布用默认」，
    因此调用方必须保证在 config 发布之后调用。
    """
    from sglang.srt.runtime_context import get_context, get_observability

    # 命名空间未发布时直接兜底，避免 get_observability() 访问未初始化字段
    if not get_context().is_config_namespace_published("observability"):
        return default_cls
    stat_loggers = get_observability().stat_loggers
    if not stat_loggers:
        return default_cls
    # 按角色（scheduler / tokenizer / storage / radix_cache / expert_dispatch）查表
    return stat_loggers.get(role, default_cls)

```

### `python/sglang/srt/eplb/expert_distribution.py`

最典型的「record 旅行」消除案例：ServerArgs 参数从 6 个构造函数中移除，存储属性删除，读取全部走 bag。

```python
# python/sglang/srt/eplb/expert_distribution.py

class ExpertDistributionRecorder(ABC):
    """全局专家分布记录器入口。"""

    @staticmethod
    def init_new(expert_location_metadata, rank):
        # 之前这里还接收 server_args：它只服务于下面 4 处读取，
        # 而这些字段全部已在 exec.moe 命名空间中，因此直接读 bag
        if get_exec().moe.expert_distribution_recorder_mode is not None:
            assert expert_location_metadata is not None, (
                "ExpertLocationMetadata is required for expert distribution recording. "
                "One possible reason is that you are using a model that does not support "
                "expert distribution recording. Try setting "
                "`get_model_config_for_expert_location` in your model."
            )
            return _ExpertDistributionRecorderReal(expert_location_metadata, rank)
        else:
            return _ExpertDistributionRecorderNoop()

```

# 评论区精华

Codex 机器人提交了 4 条 review 评论，但 reviewed commit 为 `97f9b5bb`，与本 PR head（`22f94e00`）不一致，内容涉及 `sgl_kernel` 的 `flash_ops`/`flashmla_ops`/`spatial_ops` 副作用导入被替换为 `pass`、以及 notebook 中删除 `import os` 的问题——这些文件不在本 PR 的变更范围内，可能是机器人对其他 commit 的评论被误挂。若这些评论实际属于本系列后续 PR（#36975 涉及 lazy imports），其技术要点仍值得注意：
> P1: Restore the FlashAttention extension registration import — the empty `try` block never imports `sgl_kernel.flash_ops`, whose initialization registers `torch.ops.sgl_kernel.fwd` and `get_scheduler_metadata`.

> P1: Restore the FlashMLA extension registration import — this `pass` sets `_flashmla_import_error` to `None` without loading `sgl_kernel.flashmla_ops`, so the backend reaches an unregistered operator.

> P1: Restore the spatial extension registration import — `create_greenctx_stream_by_value()` reaches the missing operator.

> P2: Keep `os` available for the documented token setup in `docs/demo/deepseek_v4_flash.ipynb`.

结论：这些意见指向“把副作用导入替换为 `pass` 会破坏 TORCH 扩展注册”的真实风险，但在本 PR 中无对应改动，建议在后续 PR 中核实。

- Codex P1：flash_attn.py 中副作用导入被替换为 pass 导致 FA3 算子未注册 (correctness): 评论针对 commit 97f9b5bb，与本 PR head 不符，疑似误挂；若属于系列后续 PR，应恢复副作用导入并改用 lint 抑制。
- Codex P1：flash_mla.py 中副作用导入被替换为 pass 导致 FlashMLA 算子未注册 (correctness): 同上，疑似与本 PR 无关；若相关应恢复导入并保持错误上报语义。
- Codex P1：spatial.py 中副作用导入被替换为 pass 导致 green-context 流算子未注册 (correctness): 同上，疑似与本 PR 无关；若相关应保留副作用导入。
- Codex P2：deepseek_v4_flash.ipynb 删除 import os 导致文档示例 NameError (documentation): 与 PR 无关（notebook 不在变更列表），但文档可执行性问题值得在后续维护中修复。

# 风险与影响

- 风险：主要风险是 **构造函数签名变更的连锁遗漏**：删除 `server_args` 参数后，任何漏改的调用点都会在运行时抛 `TypeError`。PR 已处理 `srt/` 外 3 个调用点（DSpark worker 是 P1 级，否则每次 DSPARK 启动即崩溃），但需人工确认其覆盖完整。其次，`resolve_collector_class` 的语义从「`server_args=None` 用默认值」变为「命名空间未发布用默认值」，依赖调用时机：若在 config 发布前调用，行为可能从「读到用户配置」变为「返回默认」，需确保所有调用点都在 publish 之后。`kt_ep_wrapper` 等核心路径（MoE 推理）改读 `get_exec()` 也可能在早期初始化阶段因命名空间未发布而取不到值。`scheduler.py` 是调度热路径，虽然 bag 读取是 dataclass 属性读取，性能风险低，但存在索引键（如 `hicache_host_memory_mode`）拼写错误导致的兜底失败。测试方面，PR 自述部分套件（`observability`、`mem_cache`、`managers`、`spec`、`server_args`）与 `main` 失败集一致，但某些迁移（如 `expert_backup_client`、`prefill_delayer`）未见新增针对性测试，存在覆盖缺口。
- 影响：影响范围集中在 `sglang/srt` 的配置读取路径：EPLB 专家均衡记录、Prometheus 指标采集、KT 专家并行、HiCache 混合池组装、调度器、弹性 EP 备份、DSPark 投机解码和 prefill 延迟。对用户无功能影响（行为不变），但为后续 #36974 删除死参数和 #36975 消除惰性导入铺路。对团队而言，该系列确立了「运行时从 bag 读配置、`ServerArgs` 只作为发布前载体」的新契约，后续新增配置读取应优先走 `runtime_context` 访问器。规模上 20 个文件、净删 46 行，属于跨模块中等规模重构。
- 风险标记：核心路径变更 , 跨模块签名变更 , 调用点遗漏风险 , 部分迁移缺少新增测试 , 代码评审疑点未落地

# 关联脉络

- PR #36896 the resolution pipeline's dispatcher leaves the record: 系列第 1 步：将 resolution 包从 ServerArgs record 上摘除，定义 bag 读取模式。
- PR #36972 the callbacks into the record go to zero: 系列第 2 步：消除回到 record 的回调，为运行时读取方迁移铺路。
- PR #36974 the dead record parameters go: 系列第 4 步：本 PR 迁移完成后，删除 ServerArgs 中 10 个死参数及 18 个调用点，直接依赖本 PR 的清理成果。
- PR #36975 the lazy imports that buy nothing become eager: 系列第 5 步：清理惰性导入，与 Codex 评论中关于副作用导入的讨论相关。
- PR #36925 CI vehicle — runs the whole series against main: 系列 CI 验证载体，确保每个 PR 独立绿。