graph-autofusion 入门详解——CANN 图自动融合工具帮你消除算子间冗余显存搬运开销
前言在昇腾NPU上执行推理时一个典型的深度学习模型的计算图中相邻算子之间的数据搬运往往占据了总耗时的相当比例。假设一个卷积层后面紧跟 BatchNorm 和 ReLU原始计算图会分别执行三个独立算子每个算子都需要把输入从显存读进来、计算、再把结果写回显存。三次读写意味着三次完整的显存访问周期而实际上卷积的输出完全可以留在片上缓存中直接交给 BatchNorm 处理BatchNorm 的结果再直接送给 ReLU。CANNCompute Architecture for Neural Networks提供的 graph-autofusion 工具正是为了解决这类问题而设计的——它能自动扫描计算图识别出可以融合的算子模式将多个独立算子合并为一个复合算子从而减少显存读写次数、降低 Kernel Launch 开销、提升整体吞吐。graph-autofusion 是 CANN 图编译优化管线中的一个关键环节。它位于模型解析之后、算子编译之前工作在计算图的 IR中间表示层面。与手动写融合规则不同graph-autofusion 采用模式匹配加代价估算的方式自动发现融合机会开发者不需要对每种算子组合都手写一条融合规则。这种自动化的融合策略特别适合模型结构频繁迭代的场景——当模型架构从 ResNet 换成 ConvNeXt或者从标准 Transformer 换成带 SwiGLU 的变体时graph-autofusion 不需要修改任何配置就能自动适应新的算子组合。在昇腾 NPU 的硬件架构中AI Core 的计算单元包括 Cube 单元和 Vector 单元和片上统一缓存UBUnified Buffer构成了计算密集型任务的核心资源。HBM高带宽内存虽然带宽远高于传统 DDR但与片上缓存相比仍然有数量级的差距。一次从 HBM 读取 1MB 数据的延迟约等于片上缓存读取的数十倍。因此把相邻算子融合后让中间结果在片上缓存中流转是从硬件层面提升性能的最有效手段之一。graph-autofusion 的本质就是利用这个硬件特性来加速模型执行。一、算子融合的基本原理要理解 graph-autofusion 的工作方式首先需要理解算子融合解决的核心问题是什么。在深度学习框架的计算图中每个算子都是一个独立的计算单元有自己的输入 Tensor、输出 Tensor 和执行逻辑。框架在执行计算图时通常会按照拓扑排序逐个调度算子执行。每调度一个算子就需要完成以下步骤从显存中读取输入数据到片上缓存、执行计算、将结果写回显存。这些步骤被称为算子的生命周期。当两个相邻算子比如 Conv 和 BN执行时第一个算子的输出需要写回显存第二个算子再从显存中读取这个中间结果。这个写回再读出的过程就是算子融合要消除的开销。如果把这两个算子融合成一个复合算子Conv 的计算结果直接留在片上缓存中BN 在同一片缓存上继续计算整个过程只需要一次从显存读输入、一次向显存写最终结果。中间结果完全不需要经过 HBM。从硬件资源的角度来看融合带来的收益来自三个维度。第一个维度是显存带宽节省。假设一个 Conv 算子的输出 Tensor 大小为 N×H×W×C融合后省去了对这个 Tensor 的一次写和一次读带宽节省为 2×N×H×W×C×sizeof(float) 字节。第二个维度是 Kernel Launch 开销减少。每个算子作为独立的 Kernel 执行时都需要通过驱动层进行一次 Launch这个开销虽然单次不大微秒级但在算子数量众多时会累积成可观的时间。融合后多个算子合并为一个 KernelLaunch 次数从 N 降到 1。第三个维度是片上缓存利用率提升。单个算子执行时片上缓存的利用率往往不高因为每个算子只需要缓存自己的输入输出融合后多个算子共享同一块缓存空间数据局部性更好缓存命中率更高。graph-autofusion 支持的融合模式可以粗分为两类线性模式和非线性模式。线性模式指的是算子按照固定的顺序排列每个算子只有一个输入和一个输出融合时只需要把多个算子的计算逻辑按顺序拼接。典型例子包括 ConvBNReLU、ConvAddReLU、MatMulAddReLU 等。非线性模式指的是算子之间存在分支或汇聚融合时需要处理多个输入路径的同步问题。这类模式的融合更复杂但收益也更大因为它可以消除分支点的中间结果存储。典型的非线性模式包括 Element-wise 算子的链式组合AddMulAdd 等。二、graph-autofusion 的架构设计graph-autofusion 在 CANN 的编译管线中扮演着图优化 Pass的角色。CANN 的编译流程大致可以分为几个阶段模型导入从 ONNX 或 MindIR 格式解析计算图、图级别优化包括常量折叠、死代码消除、公共子表达式消除、算子融合等、算子级别优化Tiling 策略选择、双缓冲、流水线等、最终代码生成。graph-autofusion 负责的就是图级别优化阶段的算子融合部分。graph-autofusion 的内部架构可以分为三个模块模式定义模块、模式匹配模块和融合执行模块。模式定义模块负责声明哪些算子组合可以被融合以及融合后如何生成新的复合算子。这些模式以声明式的规则语言描述每条规则包含匹配条件和生成模板。模式匹配模块负责在计算图上搜索符合规则的算子子图采用自底向上的遍历策略从叶子节点开始逐步构建候选融合区域。融合执行模块负责对匹配到的子图执行实际的融合操作包括替换算子节点、重连数据边、更新属性信息等。这种三模块分离的设计有一个重要优势增加新的融合模式时只需要在模式定义模块中添加新规则不需要修改匹配和执行的逻辑。这意味着社区贡献者可以方便地贡献新的融合模式而不需要深入了解匹配算法的内部实现。在实际使用中这种可扩展性非常重要因为不同模型架构可能存在特殊的融合机会只有领域专家才能识别出来。graph-autofusion 的代价估算机制也值得一提。并非所有可融合的模式都应该融合——在某些情况下融合后的算子可能因为片上缓存空间不足而无法容纳中间结果需要回退到逐个执行的模式或者融合后的算子过于复杂导致寄存器溢出和性能下降。graph-autofusion 在匹配到融合候选后会进行一次代价估算综合考虑算子计算量、中间结果大小、片上缓存容量、寄存器压力等因素只有当估算的融合收益大于开销时才执行融合。这种保守的策略避免了越优化越慢的问题。三、支持的融合模式详解graph-autofusion 内置了对多种常见融合模式的支持覆盖了卷积网络和 Transformer 模型中最典型的算子组合。了解这些模式的具体内容和适用条件有助于开发者在建模时有意地构造可融合的算子序列从而获得更好的自动融合效果。卷积网络中最常见的融合模式是 ConvBNReLU。这个模式的计算逻辑是卷积运算的输出直接作为 BatchNorm 的输入BatchNorm 的输出再直接作为 ReLU 的输入。在原始计算图中这对应三个独立算子节点融合后变成一个名为 FusedConvBNReLU 的复合算子。融合后的算子在执行时卷积的输出不写回 HBM而是直接在 UB 中完成 BN 的均值方差归一化和 ReLU 的激活判断最后才将最终结果写回 HBM。对于典型的 ResNet 瓶颈结构中的 1×1 卷积BNReLU融合可以省去两次中间 Tensor 的 HBM 访问。另一个常见的融合模式是 MatMulAddReLU这是全连接层在网络中的典型结构。矩阵乘法的输出加上偏置后通过 ReLU 激活三步操作融合为一个算子。这种融合在全连接层密集的模型中如 BERT 的前几层收益尤其显著因为全连接层的中间结果往往比较大省去 HBM 访问带来的带宽节省非常可观。# 使用 graph-autofusion 的典型配置方式# 这段代码展示了如何在 CANN 的图编译流程中启用自动融合importcann.graph.compilerascompilerdefbuild_fused_model(onnx_path):# WHY: 加载 ONNX 模型后graph-autofusion 会作为编译 Pass 自动执行# 开发者不需要手动指定哪些算子需要融合工具会自动扫描整个计算图。# 这是 graph-autofusion 与手动融合规则的核心区别# 手动方式需要开发者对每种模型架构逐一编写融合规则# 而 graph-autofusion 通过模式匹配自动发现融合机会。graphcompiler.load_onnx(onnx_path)# WHY: enable_autofusion 标志控制是否启用自动融合 Pass。# 默认情况下这个标志是开启的但某些调试场景下# 可能需要关闭它来对比融合前后的性能差异。# 关闭自动融合后计算图会保留原始的逐算子执行结构# 有助于定位性能瓶颈是否来自算子间的数据搬运开销。configcompiler.CompilerConfig()config.enable_autofusionTrue# WHY: 设置融合的代价估算阈值。# 阈值越高只有收益非常明显的融合才会被执行# 编译速度更快但优化程度较低# 阈值越低更多边界情况下的融合也会被尝试# 优化程度更高但编译耗时增加且个别融合可能因# 缓存不足而回退到逐算子模式反而增加开销。config.fusion_threshold0.1optimized_graphcompiler.optimize(graph,config)returnoptimized_graph除了上述两种线性模式外graph-autofusion 还支持 Element-wise 算子的链式融合。例如 AddMulAddReLU 这样的组合当这些算子的输入输出维度完全一致时可以融合为一个复合的 Element-wise 算子。融合后的算子只需要遍历一次数据同时完成加法、乘法、再加法、ReLU 激活四种运算。这种融合虽然在单个算子的计算量上没有减少总运算量不变但数据遍历次数从四次降到一次减少了四次 HBM 读取操作对于大尺寸的 Feature Map 效果显著。在 Transformer 模型中graph-autofusion 可以处理 LayerNorm 附近算子的融合。标准的 Transformer Block 中LayerNorm 后面通常紧跟 Attention 或 FFN 的线性层再接残差连接Add。这种结构中的 LayerNormLinearAdd 组合可以被融合为一个复合算子避免了 LayerNorm 输出的中间存储。在多头注意力部分QKV 投影的三个线性变换也可以被合并为一个大矩阵乘法再配合后续的注意力计算实现更深层次的融合。四、融合效果的量化分析算子融合的收益体现在多个指标上单次推理延迟、吞吐量QPS、显存带宽利用率、Kernel Launch 次数等。以下通过对比表格展示融合前后典型指标的差异。需要说明的是这些数据来自典型模型在昇腾 NPU 上的测试结果具体数值会因模型结构、Batch Size、输入尺寸等因素而有所不同仅供参考。指标使用前使用后变化幅度单次推理延迟ResNet-50Batch 32约 1.6ms约 1.1ms降低约 30%吞吐量BERT-BaseBatch 8Seq 128约 850 seq/s约 1100 seq/s提升约 29%显存带宽峰值利用率约 60%约 78%提升约 18ppKernel Launch 总次数ResNet-50约 340 次约 120 次减少约 65%中间 Tensor 显存占用ResNet-50约 280MB约 95MB减少约 66%从上表可以看出算子融合对 Kernel Launch 次数和中间 Tensor 显存占用的改善最为显著。Kernel Launch 次数的减少主要来自多个小算子合并为大算子减少了驱动层的调度开销。中间 Tensor 显存占用的减少则直接来自融合后中间结果不需要持久存储到 HBM——它们在片上缓存中完成计算后就被释放了。显存带宽利用率的提升说明融合后每个 Kernel 的工作量更大、数据局部性更好使得 HBM 带宽被更充分地利用而不是浪费在频繁的小块数据搬送上。单次推理延迟和吞吐量的改善幅度相对温和这是因为融合只消除了部分开销数据搬运和 Launch而计算本身的时间没有变化。对于一个计算密集型的模型计算时间占总时间的比例较高融合能消除的那部分开销占比有限但对于计算与访存比较平衡的模型如中等深度的 CNN 或中小型 Transformer融合带来的收益就比较可观。值得注意的是融合的收益与 Batch Size 之间存在非线性关系。当 Batch Size 较小时单个算子的计算量不大Launch 开销和数据搬运开销占比更高融合的收益更明显。当 Batch Size 增大到一定程度后计算时间占比大幅提升融合能消除的额外开销占比下降收益趋于平缓。这也解释了为什么在实际部署中小 Batch 场景如在线推理、实时检测对算子融合的需求更迫切。五、从模式匹配到代价估算的融合流程graph-autofusion 的融合流程可以分为五个阶段图预处理、模式发现、候选生成、代价估算、融合执行。每个阶段都有明确的目标和输出整体形成一个流水线式的处理流程。图预处理阶段对输入的计算图做规范化处理包括合并相同类型的连续算子如两个连续的 ReLU 合并为一个、消除冗余的 Identity 节点、简化常量折叠等。这些预处理步骤为后续的模式匹配创造了更干净的图结构减少了匹配的复杂度。例如如果两个 Conv 之间插了一个没有实际作用的 Identity 节点模式匹配可能会因为中间多了一个节点而无法识别出 ConvConv 的模式经过 Identity 消除后匹配就能顺利进行。模式发现阶段使用基于后缀自动机的匹配算法在计算图上搜索符合已知融合模式的子图。graph-autofusion 的模式库中预定义了数十种常见融合模式每种模式描述了一组算子的拓扑结构约束和属性约束。例如ConvBNReLU 模式要求第一个算子类型为 Conv2D第二个算子类型为 BatchNorm第三个算子类型为 Relu且三者按顺序连接。当计算图中的某个子图满足这些约束时就被标记为融合候选。# 自定义融合规则示例# 当内置模式不满足需求时开发者可以声明自己的融合规则fromcann.graph.autofusionimportPattern,FusionRule# WHY: 自定义融合规则让开发者能针对特定模型架构添加融合模式。# 比如 SwiGLU 激活函数由 MulSiluMul 三步组成# 如果模型中频繁出现这种模式内置规则可能无法覆盖。# 通过声明式规则语言定义新模式# graph-autofusion 的匹配引擎会自动将其纳入扫描范围# 不需要修改匹配算法的核心代码。# 这种可扩展性是 graph-autofusion 区别于固定规则融合方案的关键优势。# 定义 MulSiluMul 融合模式swiglu_patternPattern(SwiGLU)swiglu_pattern.add_node(mul1,op_typeMul)swiglu_pattern.add_node(silu,op_typeSilu)swiglu_pattern.add_node(mul2,op_typeMul)swiglu_pattern.add_edge(mul1,silu)swiglu_pattern.add_edge(silu,mul2)# WHY: 约束条件确保只融合真正的 SwiGLU 模式# 而不会误融合其他巧合的 MulSiluMul 组合。# 例如如果两个 Mul 使用的是不同的输入 Tensor# 那它们可能不是 SwiGLU 的一部分不应该被融合。# 通过约束条件可以精确控制模式的适用范围。swiglu_pattern.add_constraint(mul1,silu,lambdan1,n2:n1.output_shapen2.input_shape)swiglu_pattern.add_constraint(silu,mul2,lambdan1,n2:n1.output_shapen2.input_shape)ruleFusionRule(swiglu_pattern,fused_op_nameFusedSwiGLU)compiler.register_fusion_rule(rule)候选生成阶段对每个融合候选进行合法性检查。某些看起来可以融合的模式可能因为数据依赖关系而不能融合——例如如果 Conv 的输出除了送给 BN 之外还被另一个分支的算子引用那么融合后 Conv 的输出不再单独存在会导致那个分支算子找不到输入。这种情况下graph-autofusion 会放弃融合这个候选保留原始计算图结构。候选生成阶段的另一个重要任务是处理重叠的融合区域——当两个融合候选共享部分算子时需要选择性地放弃其中一个以避免融合后的图结构出现冲突。代价估算阶段是 graph-autofusion 与简单规则融合方案的核心区别。对于每个合法的融合候选graph-autofusion 会估算融合前后的执行代价。估算考虑以下因素融合后复合算子的计算时间基于算子计算量和硬件计算能力、融合后省去的显存带宽开销中间结果大小和访问次数、融合后增加的片上缓存压力中间结果是否能在 UB 中容纳、融合后可能增加的寄存器压力复合算子中同时活跃的变量数。通过这些因素的加权评估graph-autofusion 会给每个候选一个融合收益评分只有评分超过阈值的候选才会进入最终的融合执行阶段。融合执行阶段对通过代价估算的候选执行实际的图变换操作。这包括删除被融合的原始算子节点、创建新的复合算子节点、重新连接数据边、更新算子属性如名称、类型、形状推断信息。融合执行后graph-autofusion 还会做一次图一致性检查确保变换后的图在拓扑结构上是合法的没有悬挂的边或缺失的输入。六、实际使用中的注意事项graph-autofusion 的自动融合能力在大多数场景下表现良好但在一些边界情况下需要开发者注意。首先是融合后的调试难度增加问题。当多个算子被融合为一个复合算子后如果模型的数值结果出现异常很难判断是融合导致的精度问题还是模型本身的问题。CANN 提供了 Debug 模式可以在 Debug 模式下关闭自动融合逐算子执行来对比结果。建议在开发阶段先用 Debug 模式验证模型正确性确认无误后再开启自动融合进行性能优化。其次是融合与模型量化之间的交互。当模型同时使用融合和量化优化时需要确保融合不会破坏量化的精度。例如如果 BN 的融合发生在量化之后BN 的均值和方差可能已经被量化融合后的计算逻辑需要正确处理量化后的参数。CANN 的编译管线中量化和融合的执行顺序有明确的规范通常先执行量化再执行融合以确保融合后的算子使用正确的量化参数。第三点是融合对自定义算子的影响。如果模型中使用了自定义算子通过算子注册机制添加的非标准算子graph-autofusion 默认不会尝试融合自定义算子与标准算子的组合。如果开发者希望让自定义算子参与融合需要通过自定义融合规则的方式显式声明融合模式。这种设计是出于安全考虑——自定义算子的行为可能不符合标准算子的假设盲目融合可能导致错误结果。最后是融合的编译时间开销。graph-autofusion 的模式匹配和代价估算需要遍历整个计算图并评估多个候选这会增加编译时间。对于小模型这个增加通常可以忽略毫秒级对于超大规模模型如数千层的 Transformer编译时间可能会增加数秒到数十秒。在实际部署中由于模型只需要编译一次、运行无数次这个一次性开销通常是可以接受的。但如果需要频繁切换模型如 AutoML 搜索场景可以考虑关闭自动融合以加速编译迭代在最终模型确定后再开启。# 融合诊断与对比分析工具# 用于理解 graph-autofusion 对具体模型做了哪些融合决策fromcann.graph.autofusionimportFusionDiagnosticdefdiagnose_fusion(optimized_graph,original_graph):diagnosticFusionDiagnostic(original_graph,optimized_graph)# WHY: 融合决策报告列出了所有被融合的算子组及其收益评估# 帮助开发者理解 graph-autofusion 的行为。# 当某个预期中的融合没有发生时# 可以通过这个报告查看是否因为代价估算不达标而被跳过# 或者是否因为数据依赖冲突而被放弃。# 这是排查融合效果不如预期时的第一手诊断信息。reportdiagnostic.generate_report()# WHY: 输出详细的融合日志到文件包含每个候选的模式匹配结果、# 代价估算得分、最终决策融合/跳过/回退及其原因。# 对于性能调优场景这些信息可以帮助判断# 是否需要调整融合阈值或添加自定义融合规则。withopen(fusion_report.log,w)asf:f.write(report.to_text())returnreport七、graph-autofusion 与其他图优化 Pass 的协同graph-autofusion 并非独立工作它是 CANN 图优化管线中众多优化 Pass 中的一个。在实际编译流程中它与常量折叠、死代码消除、公共子表达式消除、内存布局优化、算子 Tiling 等其他 Pass 协同工作共同完成从原始计算图到可执行代码的转换。这些 Pass 之间的执行顺序对最终的优化效果有显著影响。例如常量折叠应该在算子融合之前执行因为折叠后的常量算子不再需要参与融合匹配可以减少匹配的工作量。而内存布局优化如 NHWC 到 NCHW 的格式转换应该在算子融合之后执行因为融合后的复合算子可能对数据格式有特定的要求提前做格式转换可能会破坏融合的机会。项目地址https://atomgit.com/cann/graph-autofusion