Prhub

#34982 [misc] Rename shared-read boundary to shared-read ends and fix wrapper delegation

原始 PR 作者 hnyls2002 合并时间 2026-08-17 05:36 文件变更 11 提交数 24 评论 0 代码增减 +156 / -176

执行摘要

修复 wrapper 后端共享读取栅栏委托,并按语义重命名 read-end

PR body 明确指出这是一个真实的调度正确性 bug:"This is a real fence bug wherever a wrapped child overrides the default. Concretely, TboAttnBackend wrapping DeepseekV4AttnBackend: the child declares POST_REPLAY for non-DSPARK target verify, the wrapper reported IN_REPLAY, so the scheduler was released while the child was still reading across graph segments." 同时作者希望把命名改得更贴合实际语义——"name the read-end concept after what it is",让 POST_REPLAY 明确标记"元数据快照未实现"而非一种设计状态,从而让剩余使用者可被审计。

值得精读。本 PR 是一个典型的数据契约修正 + 调度正确性修复结合体,重点关注:SharedReadEnds.max_of 的"按 lateness 聚合"设计如何优雅处理多后端组合;_replay_attn_backend() 的统一如何消除 pdmux 下的隐式不一致;以及 POST_REPLAY 从"设计状态"重新定义为"未实现标记"这一可审计性思路。测试部分用 create_autospec 替代手写 stub 的做法也值得借鉴。

讨论亮点

该 PR 无 review 评论(comments_count=0,review_comments_count=0)。设计权衡记录在 PR body 的 "Not in this PR" 部分,核心观点包括:

  • 默认值刻意保持 IN_REPLAYPRE_REPLAY 是逐后端审计后的优化,不应全局翻转;
  • _resolve_shared_read_ends 仍同时回答"哪个阶段拥有发布权"和"读到哪里结束"两个问题,拆分所有权谓词是下一步计划;
  • IN_REPLAY -> PRE_REPLAY 的 fallback 只会在 --debug-cuda-graph 且无 external-event 支持的平台触达(此时 barrier 本身关闭),POST_REPLAY 才是 sound 方向,代码中留有 TODO。

实现拆解

1. 契约层:SharedReadBoundary 重命名为 SharedReadEnds 并显式排序

  • python/sglang/srt/layers/attention/base_attn_backend.py 中,枚举改为显式整数取值 PRE_REPLAY=1 / IN_REPLAY=2 / POST_REPLAY=3 / UNKNOWN=4,注释直接写到两个 init hook 的相对位置(init_forward_metadata_out_graph / init_forward_metadata_in_graph),POST_REPLAY 被重新定义为"元数据快照未实现"的审计标记。
  • 新增静态方法 SharedReadEnds.max_of(items),按枚举值取最大值,语义是"最新(最保守)的结束点覆盖所有子 backend";UNKNOWN=4 最大,因此任一子后端未审计时聚合结果自动落回粗粒度 fence。
  • 基类方法 shared_read_boundary(forward_mode) 同步更名为 shared_read_ends(fm),默认逻辑不变(decode / target verify 返回 IN_REPLAY)。

2. 运行器:提取 _replay_attn_backend 并同步解析逻辑

  • decode_cuda_graph_runner.py 中,_resolve_shared_read_boundary 更名为 _resolve_shared_read_ends,参数名从 boundary 改为 declared,强调这是后端的声明而非解析结果。
  • 新增 _replay_attn_backend() 方法,统一 pdmux 下 stream 对应的后端选择逻辑:execute() 此前直接使用 self.attn_backend,而 load_batch() 使用 decode_attn_backend_group[stream_idx],两者在 pdmux 下不一致;现在 load_batch 与 execute 都通过 _replay_attn_backend() 获取后端,保证读结束判定与 metadata 初始化基于同一后端对象。
  • _publish_read_done(in_graph) 逻辑不变:in-graph 直接移交图内事件,out-of-graph 则新 record 一个设备事件。

3. 四个 wrapper backend 补上委托

  • HybridAttnBackend.shared_read_ends 委托给 _select_backend(fm) 选中的实际后端。
  • TboAttnBackend.shared_read_ends(self.primary, *self.children)max_of
  • HybridLinearAttnBackend.shared_read_endsattn_backend_listmax_of
  • MiniMaxHybridAttnBackend.shared_read_ends(self.sparse, self.dense)max_of
  • 这是修复的核心:修复前这些 wrapper 继承基类默认 IN_REPLAY,当子后端声明更晚的读结束点(如 DeepseekV4AttnBackend 非 DSPARK target verify 声明 POST_REPLAY)时,wrapper 会给出过早的栅栏释放信号。

4. 类名与方法调用的全仓同步

  • trtllm_mha_backend.pydeepseek_v4_backend.pyshared_read_event.py 同步切换到 SharedReadEnds + shared_read_ends,其中 shared_read_event.py 的 prefill 路径用 declared 局部变量表达同样的"声明"语义。

5. 测试精简(136 -> 80 行)

  • 删除 execute() 级 harness(ShapeKeyPPProxyTensorsload_batchdevice_module 等约 60 行),改为直接测试 _resolve_shared_read_ends(6 行参数化表格覆盖 mode / owns_verify / declared / has_marker / expected 五元组)和 _publish_read_done 两个分支。
  • 后端桩从 SimpleNamespace 改为 create_autospec(AttentionBackend, instance=True),方法名拼错会在测试时直接失败,而不是 CI 全绿、运行时崩。
  • test_prefill_shared_read_done.py 同步适配方法重命名。
文件 模块 状态 重要度
python/sglang/srt/model_executor/runner/decode_cuda_graph_runner.py 运行器 modified 8.11
python/sglang/srt/layers/attention/base_attn_backend.py 注意力层 modified 8.0
test/registered/unit/model_executor/runner/test_decode_cuda_graph_shared_read_fence.py 单元测试 modified 7.28
python/sglang/srt/layers/attention/tbo_backend.py 重叠后端 modified 6.49
python/sglang/srt/layers/attention/deepseek_v4_backend.py 注意力层 modified 6.71
python/sglang/srt/layers/attention/hybrid_attn_backend.py 混合后端 modified 6.07
python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py 混合后端 modified 6.14
python/sglang/srt/layers/attention/minimax_sparse_backend.py 注意力层 modified 6.2
python/sglang/srt/layers/attention/trtllm_mha_backend.py 注意力层 modified 6.53
python/sglang/srt/model_executor/runner_utils/shared_read_event.py 共享读取 modified 5.82
test/registered/unit/model_executor/runner/test_prefill_shared_read_done.py 单元测试 modified 4.82

关键符号

SharedReadEnds.max_of shared_read_ends _resolve_shared_read_ends _replay_attn_backend _publish_read_done maybe_publish_prefill_shared_read_done

关键源码片段

python/sglang/srt/model_executor/runner/decode_cuda_graph_runner.py data-contract

decode CUDA graph 重放主路径,`_resolve_shared_read_ends` 决定栅栏发布时机,新增 `_replay_attn_backend` 统一 pdmux 后端选择,修复 execute/load_batch 间的后端不一致。

# python/sglang/srt/model_executor/runner/decode_cuda_graph_runner.py
# pdmux 下每个 stream 用自己的 group member 重放,读结束判定也必须
# 基于同一个 backend,否则会拿到错误的声明。
def _replay_attn_backend(self) -> AttentionBackend:
    if self.enable_pdmux:
        return self.model_runner.decode_attn_backend_group[get_current_stream_idx()]
    return self.attn_backend
​
​
def _resolve_shared_read_ends(self, attn_backend, forward_mode) -> SharedReadEnds:
    """后端声明经降级后的结果;UNKNOWN 表示不记录事件,由调度器保留粗粒度栅栏。"""
    if forward_mode.is_target_verify():
        if not self.model_runner.spec_algorithm.is_last_shared_read_phase(forward_mode):
            return SharedReadEnds.UNKNOWN
    elif not forward_mode.is_decode():
        return SharedReadEnds.UNKNOWN
    declared = attn_backend.shared_read_ends(forward_mode)
​
    if (
        declared is SharedReadEnds.IN_REPLAY
        and self.in_graph_metadata_prep_done is None
    ):
        # TODO: 这里落到比声明更早的 PRE_REPLAY;POST_REPLAY 才是 sound 降级方向
        return SharedReadEnds.PRE_REPLAY
    return declared
​
​
def _publish_read_done(self, in_graph: bool):
    """把本阶段共享缓冲区读取结束事件交给调度器的 WAR 屏障。"""
    if in_graph:
        # 读取在图内标记处结束:直接移交图内事件,不重新 record
        self.model_runner.shared_read_done_event = self.in_graph_metadata_prep_done
    else:
        read_done = self.device_module.Event()
        read_done.record()
        self.model_runner.shared_read_done_event = read_done
python/sglang/srt/layers/attention/base_attn_backend.py core-logic

核心契约文件:SharedReadEnds 枚举显式排序并提供 max_of 聚合,基类方法更名,所有 backend 都依赖此契约。

# python/sglang/srt/layers/attention/base_attn_backend.py
# 枚举值按结束早晚排序,max_of 因此能聚合多个子 backend 的声明:
# 只要任一子 backend 读到更晚(或 UNKNOWN 未审计),整体取最保守的结束点。
class SharedReadEnds(Enum):
    PRE_REPLAY = 1 # 在 init_forward_metadata_out_graph 之后结束
    IN_REPLAY = 2 # 在 init_forward_metadata_in_graph 之后结束
    POST_REPLAY = 3 # 元数据快照未实现 -> 读到整个 replay 结束之后
    UNKNOWN = 4 # 未审计 -> 走粗粒度 whole-forward 栅栏
​
    @staticmethod
    def max_of(items: Iterable[SharedReadEnds]) -> SharedReadEnds:
        # 按 lateness 排序:最新(最保守)的结束点覆盖所有子 backend
        return max(items, key=lambda x: x.value)
​
​
class AttentionBackend(ABC):
    # ...
​
    def shared_read_ends(self, fm: ForwardMode) -> SharedReadEnds:
        """声明本后端调度器共享读取在每个模式下的结束点。
        仅覆盖经过审计的偏离;默认返回保守的 IN_REPLAY。"""
        if fm.is_decode() or fm.is_target_verify():
            return SharedReadEnds.IN_REPLAY
        return SharedReadEnds.UNKNOWN
python/sglang/srt/layers/attention/tbo_backend.py core-logic

真实 bug 所在:TboAttnBackend 包装 DeepseekV4AttnBackend 时此前继承默认 IN_REPLAY,现通过 max_of 聚合 primary + children 的声明。

# python/sglang/srt/layers/attention/tbo_backend.py
class TboAttnBackend(AttentionBackend):
    # 修复前 wrapper 继承基类 IN_REPLAY 默认值;当子后端(如
    # DeepseekV4AttnBackend 非 DSPARK target verify)声明 POST_REPLAY 时,
    # wrapper 会过早释放调度器。现在取 primary + children 的最晚结束点。
    def shared_read_ends(self, fm: ForwardMode) -> SharedReadEnds:
        return SharedReadEnds.max_of(
            b.shared_read_ends(fm) for b in (self.primary, *self.children)
        )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. wrapper 栅栏行为变化:修复后 TboAttnBackend 包装 DeepseekV4AttnBackend 在非 DSPARK target verify 下从 IN_REPLAY 变为 POST_REPLAY,调度 fence 更保守,可能轻微降低 two-batch overlap 的重叠程度,但这是正确性优先的权衡。
  2. 全局契约重命名shared_read_boundary / SharedReadBoundary 是 attention backend 基类公开 API,仓库内引用已全部同步,但 out-of-tree 自定义 backend 若覆写了旧方法名会在运行时报 AttributeError,属于可预期的兼容性代价。
  3. pdmux 行为修正execute() 之前用 self.attn_backend 而非当前 stream 的 group member,本 PR 通过 _replay_attn_backend() 统一;对于已开启 pdmux 且多 stream 使用不同后端的场景,这是一次真实更正,但也属于行为变化,需要回归观察。
  4. 测试覆盖缺口:PR body 明确承认不再覆盖"pre-replay record 落点早于 backend.replay()"这一顺序断言,该断言此前由完整 harness 保障;新测试只覆盖两个方法的单元逻辑,execute() 的集成路径缺少测试守卫。

影响范围集中在 decode CUDA graph 重放路径的调度栅栏(WAR barrier)时机:

  • 对 DeepSeek V4 + Blackwell + TBO 场景,修复了 target verify 阶段 scheduler 过早释放的问题,属于正确性修复;
  • 对 Hybrid / MiniMax / TBO 等 wrapper 后端的用户,栅栏发布点整体更保守,默认(非 target verify)路径行为不变,现有用户通常无需改动;
  • 对后端开发者,shared_read_ends 的排序语义(max_of)提供了可组合的聚合契约,降低了 wrapper 场景下遗漏声明的概率;
  • 对测试维护者,fence 测试从 136 行降到 80 行,且 create_autospec 让方法重命名这类重构能被测试直接捕获。
核心解码路径变更 wrapper 栅栏更保守 全局契约重命名 pdmux 行为修正 execute 级测试覆盖被删

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论