执行摘要
- 一句话:并行配置叶子直读,消除 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。因此本次让这类叶子直接裸读。
实现拆解
- 使能
__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__ 前的属性探测递归)。
- AST 驱动的调用点重写:118 个生产文件中的 301 处
get_parallel().config.<leaf> 改写为 get_parallel().<leaf>(296 处全量拼写 + 5 处经局部别名读取,别名绑定一并重写);接收者被解析为 get_parallel() 或绑定到它的局部变量,且只重写 config-only 叶子集合内的名字。5 个有 live 对应物的遮蔽 size 本次不动,留给后续 PR #36621 统一拼写。
- Dynamo 兼容性验证:gate helpers 会在编译 forward 内读取 parallel 叶子,
object.__getattribute__ 会 graph-break;作者实测裸访问在 torch.compile(fullgraph=True) 下可追踪,并新增回归测试同时覆盖 .config 与裸拼写两种读法。
- 测试与正确性护栏:修复了机械重写把 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 处调用点重写都依赖这里的语义。
# 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 构造。
# 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 未发现主要问题,恒真断言已在本次修复并纳入测试。
风险与影响
- 风险:
__getattr__ 成为 301 处调用点唯一的新读取入口,bag 发布时序若有回归,会以 ValueError 在启动路径成规模浮现,排查面较大。
- 无 GPU 精度运行,所有验证都是 CPU 侧(resolution dump、guards、单测);真实多进程拓扑下的读取行为(如
data_parallel_controller.py 的 worker 拉起、conn.py 的跨节点注册)未被覆盖。
- 机械重写可能引入静默测试退化,本次恒真断言已演示该风险;后续 sweep 需要额外护栏。
- 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 承载。
参与讨论