# PR #36620 完整报告

- 仓库：`sgl-project/sglang`
- 标题：config: a parallel leaf with no live counterpart is read bare
- 合并时间：2026-08-28 03:56
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/36620

---

# 执行摘要

- 一句话：并行配置叶子直读，消除 config 跳转并重写 301 处调用点
- 推荐动作：值得精读。重点看 `runtime_context.py` 的 `_parallel_config_leaves()` 与 `__getattr__` 设计：叶子集合从字段元数据推导而非 bag、错误语义区分 `ValueError` 与 `AttributeError`、把 Dynamo 可追踪性作为一等测试对象。对大型机械重写感兴趣的读者，PR body 中关于恒真断言的自述很有借鉴意义。

# 功能与动机

PR body 指出：`get_parallel()` 服务的是 live process groups，而解析出的 parallel 配置在 `.config` 之后一跳。`parallel` 命名空间的 40 个叶子中 35 个没有 live counterpart（如 `dp_size`、`ep_size`、`nnodes`、`enable_dp_attention` 等纯配置项），对它们而言 `.config` 跳转没有任何消歧作用——every reader paid for a distinction that does not exist。因此本次让这类叶子直接裸读。

# 实现拆解

1. **使能 `__getattr__` 裸读取 **（`python/sglang/srt/runtime_context.py`）：新增 `_parallel_config_leaves()`，用 `functools.lru_cache` 缓存，从 `namespace_of(ServerArgs)` 字段元数据推导 `parallel` 命名空间下所有叶子名（bag 未发布时不能从 bag 推导）；改写 `__getattr__`：已发布且名字在配置字段中时返回 `getattr(config, name)`；未发布但属于配置叶子时抛 `ValueError("config namespace 'parallel' not published")`；未知名与下划线名仍抛 `AttributeError`（下划线分支同时打断 pickle/copy 协议在 `__init__` 前的属性探测递归）。
2. **AST 驱动的调用点重写**：118 个生产文件中的 301 处 `get_parallel().config.<leaf>` 改写为 `get_parallel().<leaf>`（296 处全量拼写 + 5 处经局部别名读取，别名绑定一并重写）；接收者被解析为 `get_parallel()` 或绑定到它的局部变量，且只重写 config-only 叶子集合内的名字。5 个有 live 对应物的遮蔽 size 本次不动，留给后续 PR #36621 统一拼写。
3. **Dynamo 兼容性验证**：gate helpers 会在编译 forward 内读取 parallel 叶子，`object.__getattribute__` 会 graph-break；作者实测裸访问在 `torch.compile(fullgraph=True)` 下可追踪，并新增回归测试同时覆盖 `.config` 与裸拼写两种读法。
4. **测试与正确性护栏**：修复了机械重写把 launch-path guard 测试中断言改成 `bare == bare` 恒真式的问题（改为与 `resolution_result` 比较）；24 种启动形状 × 478 个共享字段的解析 dump 对比 0 差异，唯一新增字段 `grpc_worker_threads` 从非字段槽位改为声明字段，两侧值均为 4；配置 guards 与涉及测试在每个提交边界全部通过。所有验证均为 CPU 侧，无 GPU 精度运行。

关键文件：
- `python/sglang/srt/runtime_context.py`（模块 运行时上下文；类别 source；类型 core-logic；符号 _parallel_config_leaves, __getattr__）: 本 PR 的使能变更所在：新增 `_parallel_config_leaves()` 推导配置叶子集合，改写 `__getattr__` 支持裸读取，并保留未发布时的 `ValueError` 语义；所有 301 处调用点重写都依赖这里的语义。
- `python/sglang/srt/managers/scheduler.py`（模块 调度器；类别 source；类型 core-logic；符号 Scheduler.__init__, init_model_worker, init_disaggregation, get_num_allocatable_reqs）: 调度器核心路径上的典型调用点：`enable_dp_attention`、`dp_size`、`ep_size` 等配置叶子改为裸读，直接影响 DP attention 拓扑计算与 `ParallelState` 构造。
- `python/sglang/srt/model_executor/model_runner.py`（模块 模型运行器；类别 source；类型 data-contract；符号 _initialize_elastic_ep_joiner, maybe_init_expert_location_metadata, maybe_init_hisparse_coordinator, post_capture_elastic_ep_recover）: 模型运行器中的弹性 EP 与 DP attention 相关读取（如 `ep_join_rank_offset`、`enable_dp_attention`、`dwdp_size`）改为裸读，涉及分布式权重注册与线程绑定路径。
- `python/sglang/srt/managers/data_parallel_controller.py`（模块 DP 控制器；类别 source；类型 entrypoint；符号 DataParallelController.__init__, launch_dp_schedulers, launch_dp_attention_schedulers）: DP 控制器是真实多进程拉起入口，`dp_size`、`enable_dp_attention` 等读取影响 worker 数量和 DP attention 调度器启动，是 launch-path 变更的代表。
- `python/sglang/srt/managers/scheduler_pp_mixin.py`（模块 PP 调度；类别 source；类型 core-logic；符号 event_loop_pp, event_loop_pp_disagg_prefill, event_loop_pp_disagg_decode, init_pp_loop_state）: PP 异步循环深度受 `pp_async_batch_depth` 控制，该配置叶子裸读后影响 PP event loop 与循环状态初始化。
- `python/sglang/srt/disaggregation/common/conn.py`（模块 解耦连接；类别 source；类型 core-logic；符号 __init__, _sync_bootstrap_port_across_nodes, register_to_bootstrap, _should_skip_cp_replicated_state_transfer）: PD 解耦连接层用 `dist_init_addr`、`nnodes`、`load_balance_method` 等决定跨节点注册与 DP rank 分配，属配置读取面较大的启动路径。

关键符号：_parallel_config_leaves, ParallelContext.__getattr__

## 关键源码片段

### `python/sglang/srt/runtime_context.py`

本 PR 的使能变更所在：新增 `_parallel_config_leaves()` 推导配置叶子集合，改写 `__getattr__` 支持裸读取，并保留未发布时的 `ValueError` 语义；所有 301 处调用点重写都依赖这里的语义。

```python
# runtime_context.py —— 并行配置读取的使能层

# 从 ServerArgs 字段元数据推导 parallel 命名空间下的叶子集合。
# 用 lru_cache 保证只求值一次；bag 在发布前不存在，所以必须从元数据推导。
@functools.lru_cache(maxsize=1)
def _parallel_config_leaves() -> frozenset:
    # namespace_of(ServerArgs) 返回 { 字段名 : 命名空间路径 }，
    # 这里只保留路径以 "parallel" 开头的叶子。
    from sglang.srt.arg_groups.arg_utils import namespace_of
    from sglang.srt.server_args import ServerArgs

    return frozenset(
        field
        for field, path in namespace_of(ServerArgs).items()
        if path.split(".")[0] == "parallel"
    )


# __getattr__ 只在没有 live @property 与 slot 命中时被调用，
# 因此它就是 " 裸读取配置叶子 " 的入口。
def __getattr__(self, name):
    # 下划线开头保持 AttributeError：既拦截拼写错误，
    # 也打断 pickle/copy 在 __init__ 之前探测属性造成的递归。
    if name.startswith("_"):
        raise AttributeError(name)

    config = self._config
    if config is not None:
        # bag 已发布：配置叶子直接透传其值。
        if name in config._fields:
            return getattr(config, name)
    elif name in _parallel_config_leaves():
        # bag 未发布：给出专门的 ValueError，而不是误导性的 AttributeError。
        raise ValueError("config namespace 'parallel' not published")

    # 未知名字仍按普通属性缺失处理。
    raise AttributeError(f"ParallelContext has no {name!r}")

```

### `python/sglang/srt/managers/scheduler.py`

调度器核心路径上的典型调用点：`enable_dp_attention`、`dp_size`、`ep_size` 等配置叶子改为裸读，直接影响 DP attention 拓扑计算与 `ParallelState` 构造。

```python
# scheduler.py —— 调度器初始化时读取并行拓扑配置。
# 重写前这些读取是 get_parallel().config.<leaf>，现在配置叶子直接裸读。

attn_tp_rank, attn_tp_size, attn_dp_rank, attn_dp_size = (
    compute_dp_attention_world_info(
        get_parallel().enable_dp_attention,  # config-only 叶子，裸读
        tp_rank,
        get_parallel().config.tp_size,       # tp_size 是 live 字段，保持原拼写
        get_parallel().dp_size,              # dp_size 是 config-only，裸读
        get_parallel().attn_cp_size,         # attn_cp_size 是 config-only，裸读
    )
)

self.ps = ParallelState(
    tp_rank=tp_rank,
    tp_size=get_parallel().config.tp_size,   # live 字段仍走 @property + override
    pp_rank=pp_rank,
    pp_size=get_parallel().config.pp_size,
    dp_rank=dp_rank,
    dp_size=get_parallel().dp_size,
    attn_tp_rank=attn_tp_rank,
    attn_tp_size=attn_tp_size,
    attn_cp_rank=attn_cp_rank,
    attn_cp_size=get_parallel().attn_cp_size,
    attn_dcp_rank=tp_rank % get_parallel().dcp_size,
    attn_dcp_size=get_parallel().dcp_size,
    attn_dp_rank=attn_dp_rank,
    attn_dp_size=attn_dp_size,
    moe_ep_rank=moe_ep_rank,
    moe_ep_size=get_parallel().ep_size,      # ep_size 是 config-only，裸读
    moe_dp_rank=moe_dp_rank,
    moe_dp_size=get_parallel().moe_dp_size,  # moe_dp_size 仍有 live 对应物
    gpu_id=gpu_id,
)

```

# 评论区精华

本 PR 没有人工 review 评论。作者在 PR body 中主动披露了机械重写的一个真实失误：launch-path guard 测试里的断言被 AST 重写成了 `bare == bare` 恒真式，而这恰恰是用于钉住该行为的测试。其结论是：任何对 ' 拼写被测对象 ' 的测试做机械 sweep 时都存在同类隐患。Codex 自动审查给出 `Didn't find any major issues. Breezy!`，未提出修改建议。

- Codex 自动审查结论 (other): 无需修改；Codex 未发现主要问题，恒真断言已在本次修复并纳入测试。

# 风险与影响

- 风险：
 1. `__getattr__` 成为 301 处调用点唯一的新读取入口，bag 发布时序若有回归，会以 `ValueError` 在启动路径成规模浮现，排查面较大。
 2. 无 GPU 精度运行，所有验证都是 CPU 侧（resolution dump、guards、单测）；真实多进程拓扑下的读取行为（如 `data_parallel_controller.py` 的 worker 拉起、`conn.py` 的跨节点注册）未被覆盖。
 3. 机械重写可能引入静默测试退化，本次恒真断言已演示该风险；后续 sweep 需要额外护栏。
 4. Dynamo 追踪依赖 `__getattr__` 的朴素实现，虽已有回归测试钉住，但新调用点若出现在非常规编译路径仍可能 graph-break。
 - 影响：功能层面无任何可见变化：不改变解析结果、内核或调度形状，只改变配置读取位置，属于纯重构。系统层面影响 118 个生产文件，横跨调度器（`scheduler.py`）、模型运行器（`model_runner.py`）、DP 控制器（`data_parallel_controller.py`）、PP 循环（`scheduler_pp_mixin.py`）、解耦连接（`conn.py`）等核心模块，是后续 #36621（消除遮蔽 size 双拼写）与 #36622（记录不再作为对象传递）的地基。团队层面，配置读取契约从 `get_parallel().config.<leaf>` 收敛为 `get_parallel().<leaf>`，并把 `torch.compile(fullgraph=True)` 纳入配置路径回归。
 - 风险标记：跨模块机械重写（118 文件）, 启动路径真实进程组未覆盖 , 无 GPU 精度验证 , 恒真断言风险（已修复）

# 关联脉络

- PR #36621 config: a parallel size has one spelling; a patched scope declares its own: 同属五连 PR 系列（gc-p4），本 PR 刻意保留的 5 个有 live 对应物的遮蔽 size 由它统一拼写，并会删除本 PR 固定的 test_launch_path_reads_configured_sizes.py，把三处行为测试重新安置到 test_runtime_context.py。
- PR #36622 config: the record is not an object that gets passed around: 同属五连 PR 系列（gc-p5），在本 PR 与 #36621 之上继续清理 ServerArgs 记录传递链，消除死参数与非字段槽位；整个系列的 CI 由单独的 vehicle PR #36623 承载。