ge 图引擎做算子融合的时候融合策略从哪来一部分来自手动标注——开发者显式告诉编译器「convbnrelu 这三个可以融」。但手动标注覆盖不了所有情况。一个 Transformer 模型里有上千个算子靠人一个个标哪些能融合、哪些不能既不现实也不灵活——换个模型结构就得重新标一遍。graph-autofusion 就是解决这个问题的。它是 CANN 的算子自动融合框架输入一张原始计算图输出一张标注了融合分组的结果图——哪些算子应该打包在一起、用什么样的融合 kernel 执行全部由引擎自动决策。融合不是万能的先说清楚什么算子能融合、什么不能。两个算子能融合的前提条件只有一个融合后的性能收益大于融合本身的额外开销。拆开来看收益来自减少 HBM 读写开销来自融合后的 kernel 更复杂、寄存器压力更大、可能触发额外的同步。如果两个算子之间没有数据复用机会融合反而变慢。能融合的典型模式 MatMul → BiasAdd → ReLU 收益3次HBM读写 → 1次省了2次搬运 不能融合的典型模式 MatMul → Reshape → MatMul 收益0。Reshape只是改了shape元数据没有实际计算融合不省HBM读写 开销融合后两个MatMul的tile策略互相干扰可能都变慢graph-autofusion 的核心工作就是在整张计算图上做这个「收益 vs 开销」的全局最优决策。三阶段决策流程阶段一模式匹配引擎维护一个融合模式库里面存的是已知的高效融合组合// graph-autofusion 的模式库简化// 每个模式定义算子序列 融合条件 融合kernel类型structFusionPattern{vectorOpTypesequence;// 算子类型序列FusionCondition cond;// 融合条件shape/数据类型约束FusionKernelType kernel;// 融合后用什么 kernel};// 模式库里的条目FusionPattern conv_bn_relu{.sequence{OP_CONV2D,OP_BIAS_ADD,OP_RELU},.cond[](constvectorTensorShapeshapes){// 条件conv 输出只被 bias_add 消费单消费者// 且 conv 输出 shape 和 bias_add 输出 shape 兼容returnshapes[0].IsSingleConsumer()shapes[0].Match(shapes[1]);},.kernelFUSED_CONV_BN_RELU};FusionPattern matmul_gelu{.sequence{OP_MATMUL,OP_GELU},.cond[](constvectorTensorShapeshapes){returnshapes[0].dtypeDTYPE_FP16shapes[0].IsSingleConsumer();},.kernelFUSED_MATMUL_GELU};// 模式库有几十个条目覆盖主流的融合组合// 包括convbnrelu, matmulbiasgelu, layernormresidual, attention 全融合 等模式匹配的过程就是在计算图上做子图同构——遍历图中的每个算子节点看它和后续节点能不能匹配到某个模式。匹配到了就标记为「候选融合组」。阶段二收益估算匹配到的融合组不一定真的要融。引擎会对每个候选组做一次收益估算// 收益估算融合前后 HBM 读写量对比structFusionBenefit{int64_thbm_reads_before;// 融合前每个算子各读一次输入int64_thbm_writes_before;// 融合前每个算子各写一次输出int64_thbm_reads_after;// 融合后只有第一个算子读输入int64_thbm_writes_after;// 融合后只有最后一个算子写输出int64_tnet_benefit(){// 正值表示融合有收益int64_tsaved(hbm_reads_beforehbm_writes_before)-(hbm_reads_afterhbm_writes_after);// 减去融合开销更大的kernel意味着更高的寄存器压力// 估算为融合组总计算量的 5%int64_toverheadtotal_flops*0.05/peak_bandwidth;returnsaved-overhead;}};// 如果 net_benefit 0融合有利保留这个候选// 如果 net_benefit 0放弃融合保持原始独立算子这一步过滤掉了大量「看起来能融但实际没收益」的组合。Reshape、Transpose、Cast 这类元数据操作经常被过滤掉——它们本身不计算融合后不省 HBM 流量。阶段三冲突消解多个融合组可能互相冲突——一个算子同时出现在两个候选组里但只能加入其中一个。算子序列A → B → C → D 候选组1A B C融合收益 120MB 省搬运 候选组2B C D融合收益 100MB 省搬运 B 和 C 同时属于两个组只能选一个。graph-autofusion 用贪心策略消解冲突按收益从大到小排序优先选收益高的组。选完后被占用的算子从其他候选组里移除重新计算剩余组的收益// 冲突消解vectorFusionGroupresolve_conflicts(vectorFusionCandidatecandidates){// 按收益降序排列sort(candidates.begin(),candidates.end(),[](autoa,autob){returna.benefitb.benefit;});setOpNode*assigned;// 已分配给某个融合组的算子vectorFusionGroupresult;for(autocand:candidates){// 检查候选组的算子是否已经被占用了boolconflictfalse;for(auto*op:cand.ops){if(assigned.count(op)){conflicttrue;break;}}if(conflict)continue;// 没冲突加入结果result.push_back(cand.to_fusion_group());for(auto*op:cand.ops){assigned.insert(op);}}returnresult;}和 ge 的协作关系graph-autofusion 输出的是融合决策不是融合后的可执行代码。实际执行由 ge 的编译器模块完成PyTorch 计算图 ↓ graph-autofusion模式匹配 → 收益估算 → 冲突消解 ↓ 输出带融合标注的 IR ge 编译器根据标注生成融合 kernel → 编译为 NPU 可执行指令 ↓ Runtime下发执行graph-autofusion 决定「哪些算子应该融」ge 决定「融出来的 kernel 怎么跑」。分工清晰互不越界。手动标注的融合策略仍然有效——它们被当作高优先级的模式库条目优先级高于自动匹配的结果。开发者可以在模型代码里用 decorator 或配置文件标注# 手动标注融合策略覆盖自动决策importtorch_nputorch_npu.npu.fuse(patternmatmulgelu)classFusedMatmulGELU(torch.nn.Module):defforward(self,x,weight,bias):returntorch.nn.functional.gelu(torch.nn.functional.linear(x,weight,bias))# graph-autofusion 会优先使用手动标注的融合模式# 没有手动标注的部分自动决策补上融合效果LLaMA-7B 在 Ascend 910 上graph-autofusion 开启前后对比指标无自动融合自动融合手动自动融合融合算子数04752HBM 读写总量38.2 GB26.7 GB24.1 GB推理延迟batch142ms33ms31msCube 利用率58%74%78%纯自动融合从 38.2GB 压到 26.7GB效果已经很可观。加上手动标注的 5 个高优先级融合模式后再挤一点空间出来。这说明大部分融合机会是通用的自动引擎就能抓住少部分需要人工判断的场景手动标注兜底。算子融合的难点从来不是「怎么把两个 kernel 拼在一起」——那是编译器的事。真正的难点是「该不该融」和「融谁不融谁」。graph-autofusion 把这个决策从人工经验变成了可计算的收益估算问题再通过冲突消解保证全局近似最优。自动融合不是完美解——贪心策略不保证全局最优模式库也不可能穷举所有组合——但在实际模型上它已经能覆盖 80% 以上的融合机会剩下 20% 交给手动标注补齐。