已合并
【PR】feat: broadcast backward #2059
【PR】feat: broadcast backward #2059
已合并
czways创建于 9月11日
czways
czways成员
9月11日

Pull Request

broadcast backward

描述

请清晰准确地描述本次 Pull Request 的意图和变更内容。

变更类型

请选择本次引入的变更类型:

关联的Issue

如何测试

描述测试此变更的步骤和前提条件:
1.
2.

核对清单

其他信息

在此添加任何其他关于本次 PR 的说明。

BroadcastBackwardPass:GE 侧与 graph-autofusion PR 侧对比分析

简介

目的

本文档对比 GE 仓库与 graph-autofusion PR #2059 中 broadcast_backward_pass 的实现差异与相同之处,为代码审查、后续维护和跨仓库同步提供参考。

版本说明:PR #2059(head 0ae756e6)是 BroadcastBackwardPass 落地 graph-autofusion 的最终完整版实现,覆盖早期的 PR #1830 及后续修复(IndirectLoad 下游防护、pass 注册顺序调整、UT/ST 测试框架)。本文所有 PR 侧行号均以 PR #2059 为准。

范围

  • GE 侧:ge_0728/compiler/graph/optimize/autofuse/autofuse/post_process/pass/broadcast_backward_pass.cpp(1515 行)。该文件已从 ge_0728 工作树删除(git status 显示 D,pass 调用在 asc_graph_pass.cpp:21 处注释停用),本文 GE 侧行号基于删除前最后一个完整版本(commit 97f8a3246)。
  • PR 侧(PR #2059):
    • graph-autofusion/autofuse/optimize/graph_pass/broadcast_backward_pass.cpp(1477 行)
    • graph-autofusion/autofuse/optimize/graph_pass/broadcast_backward_pass.h(24 行)
    • graph-autofusion/autofuse/optimize/graph_pass/broadcast_backward_shared_split.cpp(388 行)
    • graph-autofusion/autofuse/optimize/graph_pass/broadcast_backward_shared_split.h(20 行)
    • graph-autofusion/autofuse/optimize/platform/v1/pass_runner_v1.h + v35/optimize/pass_runner_v2.h(pass 注册顺序调整)
    • 测试框架:tests/framework/broadcast_backward/(broadcast_backward_test_utils.h 99 行 + broadcast_backward_ut_utils.h 378 行)、UT tests/ut/optimize/test_broadcast_backward_pass.cpp(1955 行,73 个用例)、ST tests/st/optimize/test_broadcast_backward_pass.cpp(132 行,8 个用例)
  • 仅覆盖 Broadcast 后移 pass 本身及其注册,不含 optimize.cpp 重构、frontend_shape_vars 移除等 PR 中的其他变更。

总体概述

PR 侧本质上是 GE 侧的移植 + 增强:

  1. 去掉 asc_adapt:: 适配层,直接使用 af::GraphUtils、af::NodeUtils、ScheduleUtils:: 原生接口。
  2. 移除 topo id 管理(约 190 行),改为 pass 入口和结尾统一 TopologicalSorting。
  3. 新增 broadcast_backward_shared_split 预处理模块(388 行)。
  4. 重写 GetBroAxisFromNode 轴匹配逻辑,支持 axis 不对齐和隐式广播维度。
  5. CheckBackwardCommon 新增控制边、IndirectLoadBoundary、RemovePad 三个屏障。
  6. MoveBroadcastAfterMerge 支持多后继。
  7. CollectBroNodes 新增控制边检查,ProcessOriginalBackwardLogic 新增起点去重。
  8. (最终版新增)IndirectLoad 下游防护:ChainReachesIndirectLoad 全下游可达性 DFS + HasMultipleInputsBroadcastBehind 兄弟 broadcast 检测,在两个改写入口整体拒绝到达 IndirectLoad 的链路。
  9. (最终版新增)pass 注册顺序:BroadcastBackwardPass 注册进 V1/V2 两个 pass runner,CSE 类 pass(DuplicateElewiseCse、SameSourceBroadcastCse)调整到其之前执行。
  10. (最终版新增)独立测试框架:graph builder 工具 + 73 个 UT 用例 + 8 个 ST 用例。

差异分析

一、架构层差异(适配层不同)

维度 GE 侧 PR 侧
命名空间 namespace ge namespace optimize
图操作封装 全部通过 asc_adapt:: 适配层(GetPeerInNodes、GetPeerOutNode、IsSingleInAndOutNode、GetOutputTensorAttr、CreateBroadcastNode 等 20+ 函数) 直接使用 af::GraphUtils、af::NodeUtils、ScheduleUtils:: 原生接口,无中间层
节点类型 ge::NodePtr 直接使用 af::NodePtr 基类指针,需 ToAscNode() 向下转型 std::dynamic_pointer_cast<AscNode>
类型常量 kTransposeType、kBroadcastType 等全局宏 Transpose::Type、Broadcast::Type 等类静态成员
数据类型 DataType(GE 命名空间) af::DataType
接口入口 Run(ComputeGraphPtr) → asc_adapt::ProcessAscBackendNodes 遍历 RunPass(AscGraph &) 直接操作(broadcast_backward_pass.cpp:1471)

原因分析:

  • 命名空间:graph-autofusion 是独立组件,不属于 GE 命名空间,使用自己的 optimize 命名空间隔离代码。
  • 图操作封装:GE 侧 AscGraph 是 GE ComputeGraph 的子图,节点操作需通过 asc_adapt:: 适配层桥接两种图模型;graph-autofusion 中 AscGraph 是原生图结构,直接调用 af::GraphUtils 即可,无需中间层。
  • 节点类型:GE 侧节点类型统一在 ge::Node 下;graph-autofusion 中 af::NodePtr 是基类指针,AscNode 是携带 tensor attr 的派生类,需 dynamic_pointer_cast 转型后才能访问 outputs[i].attr 等扩展字段。
  • 类型常量:graph-autofusion 使用 ASCIR op 定义体系,类型常量绑定在 op 类上(如 Broadcast::Type),比全局宏更符合面向对象设计且避免命名冲突。
  • 接口入口:GE 侧 pass 注册在 ComputeGraph 层级,需通过 ProcessAscBackendNodes 遍历找到 AscBackend 子图再执行;graph-autofusion 的 pass runner 直接在 AscGraph 层级注册和调用,无需遍历。

二、PR 侧新增功能(GE 侧没有)

1. broadcast_backward_shared_split 模块(388 行,全新)

PR 侧在 BroadcastBackward 入口最先调用:

GE_ASSERT_SUCCESS(broadcast_backward_shared_split::SplitSharedBroadcastBranches(graph));
GE_ASSERT_SUCCESS(broadcast_backward_shared_split::SplitSharedBroadcastConsumers(graph));

功能:在执行后移前,先克隆拆分共享同一条 Broadcast 链的多分支/多消费者场景,避免改写时边冲突。GE 侧无此预处理步骤,直接在原图上改写。

原因分析:

graph-autofusion 的图结构中,一条 Broadcast 链的输出可能同时被多个分支或多个消费者引用。如果不先拆分,后移时 ReorderBroadcasts 需要断开 Broadcast 输出边,但该边连接了多个消费者,无法同时断开所有边,会导致改写失败或图结构损坏。通过预处理阶段克隆 Broadcast 链,使每个分支/消费者拥有独立的 Broadcast 副本,后续后移可以独立操作每条链。GE 侧可能这类共享场景较少,或通过其他 pass 在更早阶段已经拆分。

根本原因:GE 融合反推机制不可移植到 AF

GE 的 Broadcast 处理与子图融合深度绑定,采用 "消除 → 融合 → 恢复" 三段式流程:

GE:  UpdateBroadcastInfoToLoad(消除Brc,写入Load)  →  MergeAscGraphByLoop(融合两个子图)  →  OptimizedFallback(从Load读出,按消费者重建Brc)
  • 融合前(UpdateBroadcastBeforeMerge):沿 Load → Broadcast 路径,把广播轴信息"折叠"进 Load 输出属性(repeats[i]=1, strides[i]=0),然后删除 Brc 节点。目的是让不同子图在融合前统一布局。
  • 融合(MergeAscGraphByLoop):按循环轴合并两个 AscGraph。
  • 融合后(OptimizedFallback):遍历融合后图中每个计算节点的每个输入,从 Load 属性和图级 AscGraphAttr 倒推 broadcast_info,按消费者实际需求重新插入 Brc。

OptimizedFallback 的核心是 从消费者需求倒推(bottom-up):对每个消费者输入,比较消费者所需布局 vs Load 当前布局,差异轴即为 broadcast_info,若非空则在消费者输入前新建 Brc。一个共享源有多个消费者时,每个消费者独立判断需要哪些广播轴,只插需要的轴,不产生冗余:

原始:  P → B{A,B} → C0(需要轴A)        P → B{A,B} → C1(需要轴B)
GE 重建:P → B0{A} → C0                    P → B1{B} → C1

GE 这一机制依赖大量 GE 特有结构:

GE 反推机制 依赖的 GE 特有结构
UpdateBroadcastInfoToLoad AscBackendSubGraphFusionDecider、MergeAscGraphByLoop、NodeFuseInfo、ViewOpAttrInfo
TuningSubgraphBeforeMerge 融合决策器、AscGraphAxisMapping
OptimizedFallbackPro 融合后 AscGraphAttr、AutoFuseAttrs、FusedAscBackend 节点、ProcessAscBackendNodes

这些结构在 AF 中均不存在。AF 不走子图融合路径,直接在单个 AscGraph 上做图优化和 codegen。因此 GE 的融合反推机制 不可移植到 AF。

当前 GE 侧的 BroadcastBackwardPass 已被注释停用(asc_graph_pass.cpp:21)且源文件已从 ge_0728 工作树删除,AF PR #2059 是在 AF 侧重新实现,替代 GE 侧旧实现。broadcast_backward_shared_split 正是为了弥补 GE 融合反推机制缺失后的共享链场景处理:GE 通过融合后 OptimizedFallback 按消费者需求精确重建 Brc,AF 无法走这条路径,只能在融合前先把共享 Brc 链拆分为独立副本,再逐条执行后移。

2. CheckBackwardCommon 新增三个屏障

PR 侧 broadcast_backward_pass.cpp:484-509:

if (!HasNoControlEdges(next_node)) { return false; }       // 控制边屏障 L485-487
if (next_node->GetType() == kStoreType) { return false; }
if (IsIndirectLoadBoundary(next_node)) { return false; }   // IndirectLoad 边界屏障(增强)L493-495
if (ScheduleUtils::IsRemovePad(next_node)) { return false; }
if (IsNextViewOp(next_node)) { return false; }
if (IsDtypeNotSupportOp(next_node, output_dtype)) { return false; }

注意:最终版 CheckBackwardCommon 签名为 bool CheckBackwardCommon(const NodePtr &next_node),output_dtype 在函数内部声明,不再通过参数外传。

GE 侧(L308-325)只检查 Store、ViewOp、DtypeNotSupport 三种,无控制边和 IndirectLoad/RemovePad 屏障。PR 侧额外屏蔽以下三类:

屏障 PR 实现 说明
HasNoControlEdges L485-487 节点存在控制边时不后移,避免控制依赖被破坏
IsIndirectLoadBoundary L493-495(实现 L146-162) 不仅检查 next_node 本身是 IndirectLoad,还检查 next_node 的输出是否喂给 IndirectLoad(遍历所有输出锚点的 peer in 节点)
IsRemovePad L496-498 padding 逆操作边界

原因分析:

  • 控制边屏障:graph-autofusion 的 AscGraph 允许计算节点之间存在控制依赖边。Broadcast 后移会断开并重连数据边,如果节点同时有控制边,后移后控制依赖的语义可能不再成立。GE 侧的 asc_adapt:: 适配层在 IsSingleInAndOutNode 中隐含了控制边检查(GE 的 AscGraph 不允许控制边出现在 Brc 链中),PR 侧显式检查更清晰。
  • IndirectLoad 边界(增强):IsIndirectLoadBoundary(L146-162)不仅判断 next_node 自身是否为 IndirectLoad,还遍历 next_node 的所有输出锚点,检查是否有 peer in 节点是 IndirectLoad。注释说明:"直接喂给 IndirectLoad 的 compute 同样不能后移,否则 broadcast 会紧贴 IL"。IndirectLoad 的输出轴属于独立的物理视图(通过索引间接加载,轴语义与普通 Load 不同),Broadcast 后移跨过此边界会破坏轴对应关系。GE 侧不存在此算子类型。
  • RemovePad:该算子涉及 padding 的逆操作,Broadcast 后移跨过此边界会导致 padding 区域的广播语义不一致。GE 侧不存在此算子类型。

这三个屏障是 graph-autofusion 特有图结构和算子语义决定的,不是通用优化,无法回传 GE 侧。

3. MoveBroadcastAfterMerge 支持多后继

GE 侧 L1089-1108 PR 侧 L1045-1064
merge 后继 只处理 at(0) 单个后继 复制 GetPeerInDataAnchors() 全部后继,逐个断开、逐个重连

PR 侧支持 merge_node 输出连接到多个下游节点,GE 侧只处理单后继。

原因分析:

graph-autofusion 的图结构允许 merge 节点(如 Add)输出后继续 fork 到多个下游分支(fork-join-fork 模式),这在融合图中较常见。GE 侧假设 merge 后只有单后继,这在 GE 的图结构中成立(GE 的融合图通常在 merge 后接 Store)。如果 merge 有多个后继,GE 侧只重连第一个后继会导致其他后继丢失数据边,造成图改写不等价。

根因定位:GE 侧 MoveBroadcastAfterMerge 的 Bug

PR 侧支持多后继的直接原因是修复了从 GE 移植时发现的 MoveBroadcastAfterMerge Bug。以下是根因分析:

图结构:

Load_0 → Broadcast_0 ─┬→ Abs_0 → Cast_0 ──┐
                       └→ Cast_1 → Relu_0 ──┤→ Add_0(merge) ─┬→ Sigmoid_0 → Store_0
                                           └→ Sqrt_0 → Store_1

触发路径:

  1. 多引用后移触发:CollectCandidateMultiRefNodes(L897)选中 Broadcast_0(单输出锚点、2 个消费者、类型是 Broadcast)。
  2. 分支汇聚判定通过:CheckAllBranchesCanBackward(L978)从 Abs0 和 Cast1 分别 trace,两条分支都在 Add0 处汇聚(Add0 是多输入→merge),first_merge_node == current_merge_node == Add_0 → 返回 true。
  3. 执行后移:BackwardMultiRefBroadcast(L1066)调用 MoveBroadcastAfterMerge(L1045)。

Bug:GE 侧 MoveBroadcastAfterMerge 只处理了 merge 的第一个消费者:

// GE broadcast_backward_pass.cpp:1094
auto merge_next_in_anchor = merge_out_anchor->GetPeerInDataAnchors().at(0);  // 只取 Sigmoid_0
GE_ASSERT_GRAPH_SUCCESS(GraphUtils::RemoveEdge(merge_out_anchor, merge_next_in_anchor));
// Add_0 → Broadcast_0 → Sigmoid_0  ✓
// Add_0 → Sqrt_0(仍然直连)         ✗ 没有被处理

Add_0 有两个消费者 Sigmoid_0 和 Sqrt_0,但 GetPeerInDataAnchors().at(0) 只取了第一个(Sigmoid_0)。Sqrt0 仍然直接连在 Add0 上。

  1. 属性更新导致不一致:UpdateComputeNodesAscTensorAttr(L706)把 Add_0 的输出 attr 更新为 Load0 的 attr(z1 维 repeat=1,广播前)。Sqrt0 的输入继承 Add0 的新输出 attr(repeat=1),但 Sqrt0 的输出 attr 仍保持原来的 repeat=18 → codegen 校验失败:
Node Sqrt: input tensor 0 vectorized_axis 3 repeat: 1
  and output tensor 0 vectorized_axis 3 repeat: 18 are not equal or broadcastable

修复方向:

MoveBroadcastAfterMerge(L1045)需要处理 merge 节点的所有下游消费者,对每个消费者都插入 Broadcast(或克隆 Broadcast 链),而不是只处理 .at(0)。PR 侧已修复:复制 GetPeerInDataAnchors() 全部后继锚点(L1050-1051),逐个断开(L1052-1054)、统一接入 Broadcast 链输入(L1056-1057)、逐个重连到 Broadcast 链输出(L1060-1062)。

4. 控制边检查与起点去重

PR 侧在以下两处新增了 GE 侧没有的防护逻辑:

(a)CollectBroNodes 控制边检查(L652-669)

if (cur_node->GetType() == kBroadcastType && !HasNoControlEdges(cur_node)) {
    return SUCCESS;  // 控制边存在时停止收集
}
while (IsSingleInAndOutNode(next_node) && (next_node->GetType() == kBroadcastType)) {
    if (!HasNoControlEdges(next_node)) {
        return SUCCESS;  // 控制边存在时停止收集
    }
    ...
}

GE 侧 CollectBroNodes(L508-525)不检查控制边,直接按 IsSingleInAndOutNode 收集。PR 侧在收集 Brc 链时,如果遇到带控制边的节点就停止收集,避免将控制依赖节点卷入后移改写。同理,ProcessMultiRefBroadcastBackward 在提取链后也会用 HasNoControlEdges 过滤候选(L1131-1133)。

(b)ProcessOriginalBackwardLogic 起点去重(L1400)

RemoveDuplicates(start_nodes);

GE 侧 ProcessOriginalBackwardLogic(L1427-1463)不去重 start_nodes。PR 侧在收集后移起点后调用 RemoveDuplicates(L191-196,基于 unordered_set 的一次遍历去重),因为 CollectBackwardStartNodes 可能通过不同路径收集到同一个节点,重复处理会导致冗余改写。

原因分析:

graph-autofusion 的 AscGraph 允许控制边存在,且 CollectBackwardStartNodes 的遍历逻辑可能产生重复起点。GE 侧的图模型中控制边不出现在 Brc 链中(由适配层隐式保证),且起点收集逻辑不会产生重复。PR 侧显式处理这两个场景,更健壮。

5. IndirectLoad 下游防护(最终版新增,GE 侧没有)

PR 侧最终版新增了跨 CheckBackwardCommon 单点屏障之外的全下游可达性防护。这是 indirect_load_user_side_input_fanout_simd E2E 精度问题回归测试暴露的缺陷修复(详见 docs/bugshot/indirect_load_user_side_input_fanout_simd_failure_analysis.md)。

(a)ChainReachesIndirectLoad(L164-189,全新函数)

bool ChainReachesIndirectLoad(const NodePtr &start_node) {
  std::unordered_set<NodePtr> visited;
  std::vector<NodePtr> pending = {start_node};
  while (!pending.empty()) {
    // ... DFS,遍历 start_node 的全部输出锚点的全部消费者 ...
    if (IsIndirectLoadBoundary(cur_node)) {
      return true;
    }
    // ... 继续向下游扩散 ...
  }
  return false;
}

从任意节点出发做 DFS,遍历其所有输出锚点的所有 peer in 节点;任一可达节点命中 IsIndirectLoadBoundary 即返回 true。带 visited 集合防止环和多路径重复访问。

与 CheckBackwardCommon 中的单层 IsIndirectLoadBoundary(next_node) 检查的区别:单层检查只能拦住"后移边界直接贴着 IndirectLoad"的场景;当 IndirectLoad 与后移链尾之间隔着多跳计算节点(如 broadcast → sub → minimum → neg → … → where → add1 → IndirectLoad)时,单层检查全部放行,改写后 codegen 将 IndirectLoad 的索引视图错误标量化,索引 16 解析为 0。DFS 防护覆盖任意深度的下游链路。

(b)HasMultipleInputsBroadcastBehind(L454-483,全新函数)

检测 next_node 的兄弟输入(peer out 非当前 cur_node 的输入)之前是否存在与当前 broadcast 链等价的兄弟 broadcast(IsSameBroNodes 同轴判定)或可通过公共轴收集出公共轴的场景。这类"同轴兄弟 broadcast"会被 IsMulInputsCanBackward 删除并登记部分后移(mul_input_nodes),是实际触发索引损坏的改写路径:IsSameBroNodes(L327-337)按广播轴索引(bro_axis_idx)而非节点身份比较,同轴兄弟链走直接后移路径。

(c)两个整体拒绝点

拒绝点 位置 防护内容
IsMulInputsCanBackward 入口 L593-598 多输入节点的同轴兄弟 broadcast 删除与部分后移登记都发生在本函数内;若该节点下游到达 IndirectLoad,任何改写都会破坏索引链视图,在此整体拒绝
ProcessOriginalBackwardLogic 后移执行前 L1417-1426 后移会把 compute 链输出改写为后移视图;若链尾下游到达 IndirectLoad(ChainReachesIndirectLoad)或存在兄弟 broadcast(HasMultipleInputsBroadcastBehind),整体 continue 跳过该起点

两个拒绝点都是保守方向:拒绝只损失融合收益(broadcast 保留原位),不会产生错误 kernel。

GE 侧对照:GE 侧无此防护。GE 的 Broadcast 处理发生在子图融合后的 OptimizedFallback 按消费者重建阶段,不存在"Brc 链后移越过索引节点"的改写动作;且 GE 的 BroadcastBackwardPass 已停用。该防护是 AF 图结构(IndirectLoad 索引视图 + 单图内链式后移)特有的正确性要求。

6. Pass 注册顺序(最终版新增)

BroadcastBackwardPass 注册进两个平台的 pass runner,且 CSE 类 pass 调整到其之前执行:

V1(platform/v1/pass_runner_v1.h:28-35):

PowEquiv → BrcConstToStore → ScalarTo1D → DupElewiseCse → BroadcastBackward → ScalarBrcOpt → MaskedFill → ExpandDims

V2(v35/optimize/pass_runner_v2.h:32-43):

PowEquiv → BrcConstToStore → ScalarTo1D → SameSourceBrcCse → DupElewiseCse → BroadcastBackward
→ ScalarBrcOpt → MaskedFill → ExpandDims → ContinuesBrc → GatherToLoad → SplitConcat

相对原有顺序的调整:DuplicateElewiseCse(V1)与 SameSourceBroadcastCse + DuplicateElewiseCse(V2)提前到 BroadcastBackward 之前,BroadcastBackward 插入到 ScalarBroadcastOptimization 之前。

原因分析:

  • CSE 提前:BroadcastBackward 的同轴兄弟判定(IsSameBroNodes)、兄弟 broadcast 删除(IsMulInputsCanBackward)依赖图中 broadcast 链的规整视图;重复 elewise/broadcast 节点会干扰判定并产生冗余改写。先去重、后改写,保证改写在稳定图上进行。
  • BBP 在 ScalarBroadcastOptimization 之前:后移产生的布局(repeat/stride 视图)是标量广播优化的输入,顺序颠倒会导致标量广播优化在旧布局上做出错误决策。

三、算法层差异(核心逻辑不同)

7. GetBroAxisFromNode — 轴匹配方式重写

这是最核心的算法差异。

GE 侧(L93-127):按索引对齐匹配,要求 bro_attr_axis.size() == pre_bro_axis.size()

GE_ASSERT_TRUE(bro_attr_axis.size() == pre_bro_axis.size());
for (auto index = 0U; index < bro_attr_axis.size(); index++) {
    if (IsEqOne(pre_bro_repeats[index]) && IsEqZero(pre_bro_strides[index])) {
        if (!IsEqOne(bro_repeats[index])) {
            bro_axis = bro_attr_axis[index];
            return SUCCESS;
        }
    }
}

PR 侧(L254-294):按 axis 值查找匹配,允许大小不对齐

GE_ASSERT_TRUE(pre_bro_repeats.size() == pre_bro_strides.size());
GE_ASSERT_TRUE(bro_repeats.size() == bro_attr_axis.size());
GE_ASSERT_TRUE(bro_strides.size() == bro_attr_axis.size());
GE_ASSERT_TRUE(pre_bro_repeats.size() == pre_bro_axis.size());
for (size_t index = 0U; index < bro_attr_axis.size(); index++) {
    if (IsEqOne(bro_repeats[index])) { continue; }
    const auto pre_axis_iter = std::find(pre_bro_axis.begin(), pre_bro_axis.end(), bro_attr_axis[index]);
    if (pre_axis_iter == pre_bro_axis.end()) {
        // A missing input axis is a scalar/implicit broadcast dimension.
        bro_axis = bro_attr_axis[index];
        return SUCCESS;
    }
    const size_t pre_index = static_cast<size_t>(std::distance(pre_bro_axis.begin(), pre_axis_iter));
    if (IsEqOne(pre_bro_repeats[pre_index]) && IsEqZero(pre_bro_strides[pre_index])) {
        bro_axis = bro_attr_axis[index];
        return SUCCESS;
    }
}

差异要点:

  • GE 侧要求 bro 和 pre_bro 的 axis 列表等长且按序对齐,通过索引直接访问。
  • PR 侧允许 bro 的 axis 在 pre_bro 中不存在(标量/隐式广播维度),直接返回该轴。
  • PR 侧通过 std::find 按 axis 值匹配,支持两侧 axis 顺序不一致。
  • 判断顺序不同:GE 先查 pre_bro 的 repeats/strides,PR 先跳过 bro_repeats==1 的轴。

原因分析:

graph-autofusion 的图结构允许 Broadcast 的输入 axis 与前驱节点的 axis 不对齐,典型场景包括:

  • 前驱节点有额外 axis:如 Load 的 data axis 可能比 Broadcast 的 input axis 多,两侧 axis 列表长度不同。
  • Broadcast 引入新 axis:隐式广播维度(前驱 axis 列表中不存在的轴),对应标量输入被广播的场景。
  • axis 顺序不一致:图改写 pass 可能重排 axis 顺序,导致两侧 axis 列表顺序不同。

GE 侧假设两侧 axis 等长且按序对齐,这在 GE 的图结构中成立(GE 的前端已保证 axis 对齐)。graph-autofusion 的图结构更灵活,需要按值查找匹配并处理 axis 不存在的情况。PR 侧先跳过 bro_repeats==1 的轴是因为这些轴不产生实际广播,无需查找前驱对应轴。

8. Topo ID 管理 — PR 侧完全移除

GE 侧有完整的 topo id 维护机制(因 GE 要求 pass 内拓扑序正确):

函数 GE 侧 PR 侧
CollectBroNodes 收集 topo_list 无此参数
CollectCmpNodes 收集 topo_list 无此参数
UpdateTopoId 调序后重排 topo id(L588) 不存在
UpdateTopoIdsForMultiRefBackward multi-ref 场景批量更新 topo id(L1021) 不存在
CreateAndUpdateBroadcastNode 调用 UpdateNodeTopoInfo 无 topo id 逻辑
InsertBroadcastNode 调用 asc_adapt::UpdateTopoId 无
BackwardMultiRefBroadcast 调用 UpdateTopoIdsForMultiRefBackward 不调用(L1066-1117)
BroadcastBackwardReally 调用 UpdateTopoId 不调用(L756-763)

PR 侧策略:pass 内不管 topo id,在 RunPass 入口(L1471-1472)和 BroadcastBackward 结尾(L1464-1466)统一调用 ScheduleUtils::TopologicalSorting(graph) 重排。GE 侧在 BroadcastBackward 结尾调用 asc_adapt::TopologicalSorting。

这也是 PR 侧行数差异的主要原因之一(1515 → 1477,减少约 112 行 topo 管理代码;但 PR 侧新增的本地 helper、IndirectLoad 防护和注释抵消了大部分缩减)。

原因分析:

GE 侧的 pass 框架要求 pass 执行后图内拓扑序仍然正确,因为 GE 后续 pass(如 codegen)直接依赖节点 topo id 顺序遍历,如果 topo id 错乱会导致代码生成顺序错误。因此 GE 侧在每次改写(调序、插入、删除节点)后都需即时更新 topo id。

graph-autofusion 的 pass 框架在 pass 结束后统一调用 TopologicalSorting 重排,后续 pass 基于拓扑排序结果而非节点 id 遍历,不依赖 pass 内的 topo id 正确性。因此 PR 侧可以移除所有 topo id 维护代码,简化实现。代价是 pass 内如果需要按拓扑序遍历,必须等待 pass 结束后的统一排序。

9. CreateAndUpdateBroadcastNode — 节点创建方式不同

GE 侧(L1300-1325):通过适配层创建

const auto b_node = asc_adapt::CreateBroadcastNode(asc_graph, node, broadcast_info, index);
asc_adapt::UpdateBroadcastNodeSchedInfo(b_node, tensor_info.sched_axis);
asc_adapt::FromDtypeToOtherDtype(b_node, DT_FLOAT, tensor_info.dtype);  // 从 FLOAT 验证

PR 侧(L1268-1316):内联创建,手动设置属性

Broadcast brc_op(brc_name.c_str());
auto b_node = asc_graph.AddNode(brc_op);
brc_op.attr.sched = node->attr.sched;
brc_op.attr.api.compute_type = af::ComputeType::kComputeBroadcast;
brc_op.attr.api.type = af::ApiType::kAPITypeCompute;
UpdateBroadcastNodeSchedInfo(b_node, ref_node);  // L1255-1259 直接复制整个 sched
FromDtypeToOtherDtype(b_node, tensor_info.dtype, tensor_info.dtype);  // L1260-1267 相同 dtype 验证

配套的 PR 侧辅助函数:GetTensorInfo(L1223-1233)、UpdateBroadcastNodeAttrs(L1234-1254)、UpdateBroadcastNodeSchedInfo(L1255-1259)、FromDtypeToOtherDtype(L1260-1267)、InsertBroadcastNode(L1317-1328)。

dtype 验证语义不同:GE 从 DT_FLOAT 验证到目标 dtype,PR 用目标 dtype 自验证。

原因分析:

  • 节点创建方式:GE 侧通过 asc_adapt::CreateBroadcastNode 封装,因为 GE 的节点创建需要适配层处理 OpDesc、anchor 注册等 GE 框架细节;graph-autofusion 的 AscGraph::AddNode 接口更直接,传入 op 对象即可完成创建。
  • sched 信息:GE 侧通过 tensor_info.sched_axis 单独传递调度轴信息,因为适配层需要按 GE 的方式设置;PR 侧直接复制整个 ref_node->attr.sched,因为 graph-autofusion 的 sched 结构可以直接整体复制。
  • dtype 验证:GE 侧从 DT_FLOAT 验证到目标 dtype,确认 Broadcast 算子支持从 float 转换到实际 dtype(GE 的 Cast 节点可能从 float 中间态转换);PR 侧用目标 dtype 自验证(from_dtype == to_dtype),因为 graph-autofusion 中 Broadcast 节点的输入输出 dtype 一致,只需确认该 dtype 被 Broadcast 支持,无需从 float 转换。

四、Scalar 判定差异

GE 侧 PR 侧
GetNodeScalarInputList L293-301:af::IsScalarInputBySerializedExprs(SerializeExprs(repeats)) — 序列化后判断 L442-450:ascgen_utils::IsScalarInput(repeats) — 直接判断
ProcessOtherInputBranches 同上 ascgen_utils::IsScalarInput(repeats)
标量支持判定 — ascgen_utils::IsNodeSupportsScalarInput(CheckNodeSupportsScalarInput L555 调用)

原因分析:

GE 侧缺少直接判断标量输入的工具接口,只能通过序列化表达式字符串后用 IsScalarInputBySerializedExprs 比较字符串来判断(间接方式,性能较差,GE 侧 SerializeExprs helper 位于 L283)。graph-autofusion 的 ascgen_utils 工具库提供了 IsScalarInput 接口,直接对 Expression 对象做符号化判断,无需序列化中间步骤,更高效且语义更清晰;标量输入支持判定也下沉为 ascgen_utils::IsNodeSupportsScalarInput 统一接口。

相同的逻辑(移植保持一致)

以下函数两侧算法逻辑完全相同,仅适配层调用方式不同(PR 行号以 PR #2059 为准):

函数 GE 行号 PR 行号 说明
GetSingleNextNode L27-39 L200-211 获取唯一后继
GetPeerOutNodeSafe L44-63 L213-227 安全获取前驱
IsNextViewOp L65-69 L229-232 View 算子判定
IsDtypeNotSupportOp L71-82 L234-243 dtype 支持判定
ReverseCollectBrcNodes L83-91 L245-253 逆序收集 Brc 链
GetBroAxisFromNode L93-127 L254-294 广播轴推断(算法重写,见差异 7)
GetBroAxises L129-138 L296-309 收集广播轴
GetBroAxisesIndex L140-154 L311-326 收集广播轴索引
IsSameBroNodes L156-167 L327-337 判断 Brc 链等价(按轴索引比较,见二.5)
RemoveAndRelinkNodeEdge L169-181 L338-351 断边重连
RemoveBroadcastOneByOne L183-192 L352-362 逐个删除 Brc
RemoveBroadcasts(单链) L194-204 L363-373 删除 Brc 链
FindSubSet L206-228 L374-396 求公共子集
CollectSameBrcAxis L230-281 L397-441 收集相同 Brc 轴
GetNodeScalarInputList L293-301 L442-450 标量输入列表(判定方式不同,见差异四)
CheckBackwardCommon(除新增屏障) L308-325 L484-509 后移公共检查
CanBackwardSimplified L333-347 L511-522 简化后移判定
IsScalarInput L352-370 L524-538 标量输入判定
CheckNodeSupportsScalarInput L375-404 L540-561 标量输入支持检查
CheckScalarInputSupport L409-435 L563-584 标量输入支持检查
IsMulInputsCanBackward L437-487 L586-636 多输入后移判定(PR 新增 IndirectLoad 入口防护)
CanBackward L489-506 L638-651 后移判定
CollectBroNodes L508-525 L652-669 收集 Brc 节点(PR 新增控制边检查)
CollectCmpNodes L527-540 L670-679 收集计算节点
ReorderBroadcasts L550-574 L680-705 调序
UpdateComputeNodesAscTensorAttr L608-641 L706-739 更新计算节点属性
UpdateBroadcastNodesDataType L643-658 L740-755 更新 Brc dtype
BroadcastBackwardReally L669-686 L756-763 执行后移
CollectBranchBroadcastNodes L688-699 L764-776 收集分支 Brc
HasCommonBroadcastAxis L704-711 L777-785 公共轴判定
GetPreBroadcastNode L716-726 L786-797 获取 Brc 前驱
ProcessSingleInputBranch L731-754 L798-818 处理单输入分支
ProcessOtherInputBranches L759-779 L819-835 处理其他输入分支
JudgeNextCompOpSupportsScalarInput L784-834 L836-877 判断后继标量支持
ContainsBroadcastNode L839-846 L878-896 包含 Brc 判定
CollectCandidateMultiRefNodes L851-869 L897-912 收集多引用候选
ExtractBroadcastChainFromNode L874-899 L913-934 提取 Brc 链
TraceBranchToMergeNode L904-945 L935-968 追踪到 merge
CheckBranchNodesSupportBackward L950-957 L969-977 分支后移检查
CheckAllBranchesCanBackward L962-994 L978-1007 全分支后移检查
CheckAllBranchesSupportBackward L999-1016 L1008-1025 全分支支持检查
DisconnectBranchesFromBroadcast L1066-1084 L1026-1044 断开分支
MoveBroadcastAfterMerge L1089-1108 L1045-1064 merge 后重连(PR 支持多后继,见二.3)
BackwardMultiRefBroadcast(除 topo) L1113-1183 L1066-1117 多引用后移
ProcessMultiRefBroadcastBackward L1188-1223 L1119-1152 多引用处理
CollectBackwardStartNodes L1228-1248 L1153-1170 收集后移起点
CollectBackwardSatisfyStartNodes L1253-1265 L1171-1182 收集满足条件起点
RemoveBroadcasts(多链) L1267-1286 L1183-1201 删除多链 Brc
GetBackwardBrcNodes L1288-1298 L1202-1222 获取后移 Brc
CreateAndUpdateBroadcastNode L1300-1325 L1268-1316 创建 Brc 节点(方式不同,见差异 9)
InsertBroadcastNode L1326-1340 L1317-1328 插入 Brc 节点
UpdateOutputTensor L1341-1358 L1329-1347 更新输出 tensor
JudgePartBackward L1368-1422 L1348-1396 部分后移判定
ProcessOriginalBackwardLogic L1427-1463 L1397-1433 原始后移逻辑(PR 新增 RemoveDuplicates + IndirectLoad 防护)
BroadcastBackward 主循环 L1473-1507 L1435-1468 主入口

主循环结构对比

两侧 BroadcastBackward 主循环结构完全相同:

1. Cube 检查 → 跳过
2. [PR 新增] SplitSharedBroadcastBranches + SplitSharedBroadcastConsumers
3. while (has_multi_ref_change):
   3.1 ProcessOriginalBackwardLogic → is_changed, mul_input_nodes
   3.2 if mul_input_nodes: JudgePartBackward → is_changed
   3.3 ProcessMultiRefBroadcastBackward → multi_ref_changed
   3.4 if multi_ref_changed: has_multi_ref_change = true
4. if is_changed: TopologicalSorting

GE 侧无步骤 2(shared split 预处理),其余流程一致。

共有遗留问题

以下问题在 GE 侧和 PR 侧(#2059 最终版)都存在,是从 GE 移植时继承的:

  1. view_op_type 全局变量未声明为 const static(PR L51)— 两侧相同
  2. FindSubSet 原地修改输入参数(PR L374-396)— 两侧相同
  3. BroadcastBackward while 循环无迭代上限(PR L1446)— 两侧相同
  4. JudgePartBackward 递归调用可能栈溢出(PR L1392 自递归)— 两侧相同
  5. GetPeerOutNodeSafe 冗余工作(PR L213-227)— 两侧相同(GE 通过 asc_adapt::GetPeerOutNode 二次获取,PR 通过 GetPeerOutNode 二次获取)

原因分析:

这些问题在 GE 侧原始代码中就存在,PR 侧移植时为保持算法一致性未做修复。具体原因:

  1. view_op_type 未 const static:GE 侧原始代码遗留,每次进入匿名命名空间时重新构造 vector,存在轻微性能开销和可变性风险。移植时保持一致未修改。
  2. FindSubSet 原地修改参数:GE 侧原始设计为节省内存,参数按非 const 引用传入。当前调用方传入局部变量无 bug,但接口契约易错。移植时保持一致。
  3. while 循环无上限:GE 侧假设每次改写后 multi-ref 机会单调递减,最终收敛。但理论上可能存在 oscillation(改写 A 产生 B 的机会,改写 B 又产生 A 的机会)。移植时保持一致。
  4. 递归调用:GE 侧 JudgePartBackward 通过递归处理级联 mul_input 链,深层图可能导致栈溢出。移植时保持一致。
  5. GetPeerOutNodeSafe 冗余工作:GE 侧原始代码在验证 anchor 后又调用 GetPeerOutNode 重复获取,是代码冗余。PR 侧移植时保持了相同结构。

建议:这些问题应在后续维护中统一修复,且修复时 GE 侧和 graph-autofusion 侧应同步,避免两侧进一步分叉。

测试框架(最终版新增)

PR #2059 附带独立的 BroadcastBackwardPass 测试基础设施:

文件 规模 内容
tests/framework/broadcast_backward/broadcast_backward_test_utils.h 99 行 图构建工具:BuildCommonAxisGraphPrefix(符号化 loops/data/load/broadcast 前缀)等
tests/framework/broadcast_backward/broadcast_backward_ut_utils.h 378 行 断言与图工厂:IsConnected、AreConnectedTensorAttrsEqual、IsEdgeAttrConsistent、BuildUnaryGraph/BuildBinaryGraph/BuildDtypeAwareBinaryGraph、BuildDirectFanOutGraph/BuildMultiNodeFanOutGraph/BuildScalarForkJoinGraph、ExpectDirectFanOutMoved 等
tests/ut/optimize/test_broadcast_backward_pass.cpp 1955 行 73 个 TEST(BroadcastBackwardPass, ...) 用例:单链后移、各算子支持、barrier 拒绝(Reduce/VectorizedLayout/多消费者等)、fan-out 场景、IndirectLoad 防护(SkipsPartialBackwardReachingIndirectLoadIndex、MovesPartialBackwardWithoutIndirectLoadDownstream)等
tests/st/optimize/test_broadcast_backward_pass.cpp 132 行 8 个 ST 用例(端到端 pass 序列验证)

测试框架覆盖了 GE 侧缺失验证能力:GE 侧该 pass 无独立 UT(GE 仓库 tests/autofuse 中无对应用例),AF 侧的防护(控制边、IndirectLoad、shared split)均有针对性用例。

总结

类别 数量 影响 差异原因
架构适配差异 6 项 预期内(GE→graph-autofusion 平台迁移) 两套框架的图模型、节点类型体系、命名空间归属不同
PR 新增功能 6 项 shared_split 预处理、CheckBackwardCommon 三屏障(控制边/IndirectLoadBoundary/RemovePad)、多后继 merge 支持、控制边检查与起点去重、IndirectLoad 下游防护(ChainReachesIndirectLoad + HasMultipleInputsBroadcastBehind + 两个拒绝点)、pass 注册顺序调整 graph-autofusion 特有的图结构(共享链、控制边、特有算子、fork-join-fork 模式、IndirectLoad 索引视图)要求
核心算法差异 2 项 GetBroAxisFromNode 轴匹配重写、topo id 管理移除 图结构更灵活(axis 不对齐/隐式维度)+ pass 框架不依赖 pass 内 topo id
完全相同的逻辑 55+ 函数 算法等价,仅适配层调用方式不同 移植时保持算法一致性
共有遗留缺陷 5 项 从 GE 继承,PR 侧未修复 移植时为保持一致性未修改,建议后续统一修复
测试基础设施 4 个文件(约 2560 行) GE 侧无独立 UT;AF 侧 73 UT + 8 ST 覆盖防护逻辑 AF 改写场景复杂(共享链/控制边/IndirectLoad),需独立回归能力

PR 侧本质上是 GE 侧的移植+增强:去掉了 asc_adapt 适配层和 topo id 管理,新增了 shared split 预处理、控制边/IndirectLoad/RemovePad 屏障、起点去重、IndirectLoad 下游防护和更宽松的轴匹配逻辑,并调整 pass 注册顺序使 CSE 在后移前执行。核心后移算法(单链后移、多输入后移、多引用后移、部分公共轴后移)的流程完全一致。

差异的根本原因在于两套框架的图模型和 pass 框架设计不同:GE 侧 AscGraph 是 GE ComputeGraph 的子图,受 GE 框架约束(适配层、topo id、单后继假设、无控制边),且 Broadcast 处理绑定在子图融合流程中(已停用);graph-autofusion 中 AscGraph 是原生图结构,更灵活(axis 不对齐、多后继、共享链、控制边、IndirectLoad 索引视图),且 pass 框架简化了拓扑序管理。

likedislike
Pull Request已成功合入, 合并人@CANN-robot
(感谢 czways 的贡献)
czwaysczways成员
9月11日 添加了label:enhancement
czwaysczways成员
9月11日 创建了 pull request,commit 8ad30aad
CANN-robotCANN-robot成员
9月11日 添加了label:cann-cla/yes
CANN-robot
CANN-robot成员
9月11日 评论:

Thanks for your pull-request.
The full list of commands accepted by me can be found at here.
You can get sig-info at here.
You can self-configure the PR merge rules for this repository. For more details, please refer to Here.
For more, you also can visit HICANN.


PR Approval Progress

✅ Congratulations! All modules have met the lgtm and approve requirements.

Module Approval Details

module lgtm status approve status
repo-cann/graph-autofusion ✅ yangyongqiang0606, xchu42, liyuewei (3/2) ✅ yangyongqiang0606 (1/1)

💡 Tip:

  • Committer can comment /approve or /lgtm
  • Commenting /approve implies both code review (lgtm) and intent to merge (approve)

CLA Signature Pass

czways, thanks for your pull request. All authors of the commits have signed the CLA. 👍

likedislike
CANN-robotCANN-robot成员
9月11日 将niuyuhu,yangyongqiang0606,liyuewei,xchu42,zhang_shengjie,depeng1994,wqtshg_wt,peiyang,shengnan666,XuebinYang,sjtulxh,zhanj,zhujingjing,xuyafei,wangxiaotian995设为评审人
CANN-robotCANN-robot成员
9月11日 将niuyuhu,yangyongqiang0606,liyuewei,xchu42,zhang_shengjie,depeng1994,wqtshg_wt,peiyang,shengnan666,XuebinYang,sjtulxh,zhanj,zhujingjing,xuyafei,wangxiaotian995设为审查人
czways
czways成员
9月11日 评论:

compile

likedislike
czwaysczways成员
9月11日 预合并成功(commit_id: 8fe00a766cca64ff4767b2f75002f58b733cbb82)
czwaysczways成员
9月11日 关联了issue:[需求] BroadcastBackwardPass: Broadcast 节点沿 elementwise 计算链向后移动优化
CANN-robot
CANN-robot成员
9月11日 评论:
🚀 CI 流水线已启动
📋 执行详情: 点击查看流水线
likedislike
CANN-robotCANN-robot成员
9月11日 添加了label:ci-pipeline-running
CANN-robotCANN-robot成员
9月11日 删除了label:ci-pipeline-running
CANN-robotCANN-robot成员
9月11日 添加了label:ci-pipeline-passed
czwaysczways成员
9月11日 修改了pull request 的描述
xchu42
xchu42成员
9月11日 评论:

/lgtm

likedislike
liyuewei
liyuewei成员
9月11日 评论:

/lgtm

likedislike
CANN-robotCANN-robot成员
9月11日 添加了label:lgtm
yangyongqiang
yangyongqiang成员
9月11日 评论:

/approve

likedislike
CANN-robotCANN-robot成员
9月11日 添加了label:approved
CANN-robotCANN-robot成员
9月11日 合入了pull request