Prhub

#36790 config: the derived parallel widths are computed from the leaves

原始 PR 作者 ch-wan 合并时间 2026-08-29 01:18 文件变更 4 提交数 2 评论 4 代码增减 +316 / -20

执行摘要

派生并行宽度统一为叶子推导加落印,修正 get_parallel 读取

PR body 指出 get_parallel() 的统一规则被六个名字破坏:"get_parallel() 的回答方式应该是:操作符配置的内容来自已发布的 parallel bag,只有进程组建立后才存在的内容来自 groups。但有六个名字违反了这条规则——它们都不是 parallel 叶子,而是叶子们的商,且没有 flag 直接设置它们,此前却是从刚刚由这些叶子构建的 group coordinator 上读回的"。这使得 get_parallel().tp_size 抛 ValueError 而 get_parallel().attn_tp_size 抛 group getter 的裸 AssertionError,调用点无法区分。更关键的是弹性 EP:scale 时 ep_size / dp_size 会在已发布 bag 上被改写而 coordinators 保留构造宽度,每次读取现算公式会得到缩水甚至为 0 的宽度,因此必须"组构建时落印"。

值得精读。PR body 对两个设计决策的论证非常清晰:为什么"落印而不是每次读取现算"(弹性 EP 下公式失效),以及为什么 world_size 排除在落印集合之外(live getter 在每个时刻都正确)。建议关注 Codex 未解决的两个 P2(reset_context 与 snapshot_context 对 _derived 的处理),并在后续 PR 中补齐。

讨论亮点

四条 review 评论均来自 chatgpt-codex-connector[bot],全部标注为 P2。其中三条针对早期 commit,一条涉及 world_size 落印的设计边界:Codex 建议不要落印局部 world_size(否则 scale joiner 会丢失 expanded WORLD 大小),作者在 PR body 中已论证 world_size 刻意不纳入落印集合——它不是叶子的商,live getter 在每个时刻都正确,并有 test_the_world_size_is_not_stamped 钉死该行为。disable_dp_size 相关的评论在 head 版本中已解决(改用 override 包裹)。reset_context 未清理 _derived 与 snapshot_context 按引用快照 _derived 两条则未在 PR 内处理。

实现拆解

  1. 集中派生算术(python/sglang/srt/runtime_context.py):新增模块级函数 derive_attention_widths 与 derive_parallel_widths,把六个派生宽度的商计算收敛到一处。attn_dp_size 直接透传(已是有效宽度,DP 注意力关闭时为 1),避免二次消费 enable_dp_attention 开关;DCP 关闭时 attn_dcp_size 取 1 而非 0,保证除法与比较安全。

  2. ParallelContext 增加落印机制(runtime_context.py):slots 新增 _derived,并新增 stamp_derived_widths、clear_derived_widths、_derived_width 三个方法。attn_tp_size 等六个属性从 _v(override 或 live getter)改为 _derived_width(override → stamp → live group),fallback 分支保证未经过 initialize_model_parallel 安装组的进程仍能工作,两者都缺失时给出指明缺失方的命名错误。

  3. 组构建与销毁同步落印/清印(python/sglang/srt/distributed/parallel_state.py):initialize_model_parallel 先用 derive_parallel_widths 算出字典,attn_tp_size 与 moe_tp_size 均取自该字典构建 _ATTN_CP、_MOE_TP 等组,最后 stamp_derived_widths(**derived_widths) 一次落印;destroy_model_parallel 调用 clear_derived_widths 清印。

  4. DP 注意力路径联动(python/sglang/srt/layers/dp_attention.py):initialize_dp_attention 直接从 compute_dp_attention_world_info 取回 attn_dp_size 并落印,删除此前重复的两行赋值;update_dp_attention_post_scale 在弹性 EP 切换 WORLD 时重落印 attn_dp_size;compute_dp_attention_world_info 复用 derive_attention_widths,只保留逐进程的 rank 计算(rank 不属于落印集合);disable_dp_size 改为用 get_parallel().override(attn_dp_size=1) 包裹,使 scope 内两种拼法一致。

  5. 测试配套(test/registered/unit/test_runtime_context.py):新增 TestDerivedWidths,覆盖商值来自叶子、world_size 不落印、stamp 胜过 group、override 胜过 stamp、无 stamp 时 live group 兜底、双缺失时错误命名原因、rank helper 与 stamp 在三种拓扑上一致,并断言 parallel_state.py 与 dp_attention.py 不再携带第二份商算术副本。PR body 报告 159 个涉及 record 的注册测试与 base 失败集一致,24-shape 解析 dump 逐字段相同。

文件 模块 状态 重要度
python/sglang/srt/runtime_context.py 运行时上下文 modified 8.58
python/sglang/srt/layers/dp_attention.py 注意力并行 modified 6.31
python/sglang/srt/distributed/parallel_state.py 并行状态 modified 5.97
test/registered/unit/test_runtime_context.py 单元测试 modified 7.1

关键符号

derive_attention_widths derive_parallel_widths stamp_derived_widths clear_derived_widths _derived_width initialize_model_parallel destroy_model_parallel update_dp_attention_post_scale compute_dp_attention_world_info initialize_dp_attention disable_dp_size

关键源码片段

python/sglang/srt/runtime_context.py core-logic

变更核心:新增 derive_attention_widths / derive_parallel_widths 集中全部派生算术,ParallelContext 增加 _derived 落印机制与 _derived_width 读取链,六个派生宽度属性从 group getter 改为落印读取。

# runtime_context.py:派生宽度的单一算术来源。
# 这些名字没有任何 flag 直接设置,都是叶子配置的商;
# 此前从刚构建的 group coordinator 上读回,现在改为
# 组构建时由 derive_parallel_widths 落印(stamp)。
def derive_attention_widths(
    *, tp_size: int, attn_cp_size: int, dp_size: int, enable_dp_attention: bool
) -> tuple:
    """返回 (attn_dp_size, attn_tp_size),由叶子推导。"""
    attn_dp_size = dp_size if enable_dp_attention else 1
    return attn_dp_size, tp_size // attn_dp_size // attn_cp_size
​
​
def derive_parallel_widths(
    *,
    tp_size: int,
    attn_cp_size: int,
    attn_dp_size: int,
    moe_ep_size: int,
    moe_dp_size: int,
    dcp_size: int,
    dcp_enabled: bool,
) -> dict:
    """全部派生宽度,供组构建与落印共用。"""
    return {
        # attn_dp_size 已是有效宽度(DP 注意力关闭时为 1),
        # 直接透传;这里不再消费 enable_dp_attention 开关,
        # 否则调用方传入原始 dp_size 会得到 tp/dp/cp 而非 tp/1/cp。
        "attn_dp_size": attn_dp_size,
        "attn_tp_size": derive_attention_widths(
            tp_size=tp_size,
            attn_cp_size=attn_cp_size,
            dp_size=attn_dp_size,
            enable_dp_attention=True,
        )[1],
        "moe_ep_size": moe_ep_size,
        "moe_tp_size": tp_size // moe_ep_size // moe_dp_size,
        "dcp_enabled": dcp_enabled,
        # DCP 关闭时取 1 而不是 0,保证除法与比较安全。
        "attn_dcp_size": dcp_size if dcp_enabled else 1,
    }
​
​
class ParallelContext:
    __slots__ = ("_overrides", "_config", "_derived")
​
    def stamp_derived_widths(self, **widths) -> None:
        """组按这些宽度构建完成后记录;读取方以落印为准。"""
        self._derived.update(widths)
​
    def clear_derived_widths(self) -> None:
        self._derived.clear()
​
    def _derived_width(self, name, getter):
        # 读取顺序固定:override -> stamp -> live group。
        # 弹性 EP 会在已发布 bag 上改写 dp_size / ep_size,
        # 但 coordinator 保留构造宽度,每次现算公式会得到
        # 缩水甚至为 0 的宽度;落印保证读到的就是组构建时的宽度。
        overrides = self._overrides
        if name in overrides:
            return overrides[name]
        if name in self._derived:
            return self._derived[name]
        # 兜底:未经过 initialize_model_parallel 安装组的进程仍可工作;
        # 两者都不存在时(组未初始化会由 getter 抛出断言),
        # 读路径会转化为指明缺失方的命名错误而非裸断言。
        return getter()
python/sglang/srt/layers/dp_attention.py core-logic

DP 注意力宽度与落印机制的联动点:初始化时落印 attn_dp_size,弹性 EP scale 后重落印,compute_dp_attention_world_info 复用 derive_attention_widths 消除第二份算术副本,disable_dp_size 用 override 保证 scope 内读取一致。

# dp_attention.py:弹性 EP 后重落印 + DP 禁用 scope 的一致性。
def update_dp_attention_post_scale(new_dp_size: int, new_dp_rank: int):
    global _ATTN_DP_SIZE, _ATTN_DP_RANK
    _ATTN_DP_SIZE = new_dp_size
    _ATTN_DP_RANK = new_dp_rank
    # scale-up 后 coordinators 仍保持构造宽度,必须同步重落印,
    # 否则 get_parallel().attn_dp_size 会回答旧宽度。
    get_parallel().stamp_derived_widths(attn_dp_size=new_dp_size)
    get_flags().dp.use_world_group_for_gather = True
    logger.debug(
        "[Elastic EP] dp_attention switched to WORLD: dp_size=%d dp_rank=%d",
        new_dp_size,
        new_dp_rank,
    )
​
​
@contextmanager
def disable_dp_size():
    """DP 注意力在 scope 内被禁用。    供 speculative decoding 的 draft worker 使用:draft 模型与
    target 模型以不同宽度运行。这里同时替换模块级全局量与
    runtime context 的派生宽度,确保同一个名字的两种拼写
    (get_attention_dp_size 与 get_parallel().attn_dp_size)
    在 scope 内不会产生分歧。
    """
    global _ATTN_DP_SIZE
    assert _ATTN_DP_SIZE is not None, "dp attention not initialized!"
    old_dp_size = _ATTN_DP_SIZE
    _ATTN_DP_SIZE = 1
    try:
        # override 优先于 stamp,scope 内读取方恒为 1。
        with get_parallel().override(attn_dp_size=1):
            yield
    finally:
        _ATTN_DP_SIZE = old_dp_size

评论区精华

scale joiner 的 world_size 是否应保留 expanded WORLD 设计

Codex P2:scale joiner 以非零 ep_join_rank_offset 启动时,bootstrap.py 用 rank_offset + tp_size * pp_size 初始化 WORLD,而 initialize_model_parallel 在 recovered_rank=True 时把本地 world_size 替换为 tp_size * pp_size,若落印该值,get_parallel().world_size 会退回 joining cohort 的宽度,丢失扩展后的 WORLD 大小。

结论:作者在 PR body 中已论证 world_size 刻意不纳入落印集合:它不是叶子的商,live getter 在每个时刻都正确(try_admit_scale_ranks 扩展 WORLD 后、scale-joiner 以 tp * pp 布局而 WORLD 为 ep_join_rank_offset + tp * pp 时),并有 test_the_world_size_is_not_stamped 钉死该行为。 · won't fix(设计有意)

reset_context 未清理 _derived 可能导致跨生命周期状态泄漏 正确性

Codex P2:reset_context 重置 _config、flags 等状态但保留 _derived;由于 _derived_width 优先于 live getter,后续测试或重新发布的上下文可能静默观察到上一生命周期的拓扑宽度。

结论:未在 PR 内处理;建议后续在 reset_context 中同步调用 clear_derived_widths。 · 待处理

disable_dp_size scope 内 attn_dp_size 应反映禁用状态 正确性

Codex P2(针对早期 commit):进入 disable_dp_size 后 live _ATTN_DP_SIZE 改为 1,但 stamped attn_dp_size 优先返回,scope 内 get_parallel().attn_dp_size 仍回答 target 模型宽度。

结论:head 版本已解决:disable_dp_size 改为 with get_parallel().override(attn_dp_size=1) 包裹,override 优先于 stamp,scope 内两种拼法一致。 · 已解决

snapshot/restore 对 _derived 按引用快照导致 restore 不还原 正确性

Codex P2:snapshot_context 保留 _derived 的引用,stamp_derived_widths / clear_derived_widths 原地修改同一对象,restore 无法还原先前拓扑。

结论:未在 PR 内处理;建议快照时拷贝 _derived 字典(与其它可变上下文状态一致)。 · 待处理

风险与影响

  1. 核心路径变更:initialize_model_parallel / destroy_model_parallel 是分布式初始化必经路径,本 PR 在其中增加落印/清印,任何组构建流程改动都可能影响 stamp 一致性。
  2. 异常语义变化:派生宽度未初始化时的错误从 group getter 的裸 AssertionError 变为命名错误,依赖旧异常类型的调用方需要适配。
  3. 生命周期状态残留:reset_context 不清理 _derived、snapshot_context 对 _derived 按引用快照(Codex P2 未解决),测试隔离或 restore 场景可能读到过期拓扑宽度。
  4. 弹性 EP 同步面:目前只有 update_dp_attention_post_scale 重落印 attn_dp_size,未来若新增其他 scale 路径而漏掉重落印,会读到旧宽度;moe_ep_size 改为 stamp 后,若调度器需要"实时 ep_size"需改用其他 API。
  5. 回归证据:PR 自报 159 个注册测试失败集与 base 一致、24-shape 解析 dump 逐字段相同,正常路径风险较低。

对普通推理用户无行为感知;对系统而言,所有经 get_parallel() 读取派生宽度的代码路径从"依赖组存在时刻"变为"可提前且一致读取",弹性 EP scale 后读取更稳定,speculative decoding 的 disable_dp_size scope 保持两种拼法一致。对团队而言,本 PR 确立了"叶子发布 + 商派生 + 组构建时落印"的配置读取模式,与 config 系列重构(PR 36791、36792)同一演进方向,后续新增宽度类配置时有明确范本。

核心路径变更 弹性 EP 宽度读取语义变化 生命周期状态清理缺失 异常语义变化

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论