前言:手动融合的苦,谁写谁知道
算子融合这件事,在 CANN 昇腾 NPU 上做性能优化时几乎绕不开。把两个相邻算子合到一起执行,省掉中间结果的写出和读入,理论收益很清楚——但真落到手上,全是体力活。
每一次融合,都需要开发者人工判断"这两个算子能不能合",然后手写融合后的算子代码、注册融合规则、验证正确性。ops-nn 里 LayerNorm + GELU 的融合,ops-transformer 里 Attention 相关的融合,每一对都是人工量身定制的。模型结构一变,融合规则就得重新写。新算子加进来,又得补新的融合组合。工作量是组合爆炸的——N 个算子的全排列融合可能性,靠人脑根本覆盖不过来。
graph-autofusion 就是冲着这个痛点来的。它不依赖预定义的融合规则,而是让编译器自动发现融合机会,再通过 SuperKernel 的 JIT 编译机制,把融合方案直接变成可执行的代码。从"人写规则"到"机器发现规则",这是融合优化范式的根本转换。
graph-autofusion 在 CANN 生态中的定位
graph-autofusion 是 CANN 昇腾异构计算架构中的算子自动融合框架,位于第二层昇腾计算服务层。它的核心能力可以一句话概括:给定一段算子序列,自动找到可融合的子图,把它们编译成一个 SuperKernel 执行。
这个框架由两个核心组件构成:
| 组件 | 职责 | 核心机制 |
|---|---|---|
| Autofuse | 融合机会发现 | 分析算子间的数据依赖和硬件亲和性,自动识别可融合的算子组合 |
| SuperKernel | 融合后代码生成与编译 | codegen + JIT,将融合方案编译为单一 Kernel 并在运行时加载执行 |
graph-autofusion 的依赖极其精简——仅依赖 Ascend C 算子编程语言和 Runtime 运行时,不引入任何额外框架。这意味着它可以在 CANN 的基础运行环境上直接工作,不需要安装 ATB 或其他加速库作为前置。
[图1:graph-autofusion 在 CANN 五层架构中的定位,高亮第二层昇腾计算服务层中的 graph-autofusion 模块,箭头指向第1层 Ascend C 和第4层 Runtime 表示依赖关系]
SuperKernel 的本质:一次 Launch 干完 N 件事
要理解 SuperKernel,先得理解它要解决的核心问题——Kernel Launch 开销。
在昇腾 NPU 上,每个算子作为独立 Kernel 执行时,需要经历这样的流程:Host 侧下发任务 → Device 侧接收 → 初始化执行上下文 → 执行计算 → 写出结果 → 切回 Host。单次 Kernel Launch 的开销虽然不大(微秒级),但当模型中有几百上千个算子串行执行时,这些开销会累积。更致命的是,两个相邻算子之间,前一个的输出要写回 HBM,后一个再从 HBM 读入——这趟"搬运"的延迟远超 Launch 本身。
SuperKernel 的做法很直接:把多个算子打包成一个 Kernel,只 Launch 一次。中间结果不写出 HBM,直接在片上存储(SRAM/UB)里传递给下一个计算步骤。
传统模式(逐算子执行): Host → [MatMul] → HBM → [Add] → HBM → [ReLU] → HBM Launch 1 搬运 Launch 2 搬运 Launch 3 搬运 SuperKernel 模式(融合执行): Host → [MatMul → Add → ReLU] → HBM Launch 1(一次到位)这个思路并不新鲜,GPU 上的 CUDA Graph、Triton 的融合机制都在做类似的事。但 graph-autofusion 的差异在于——融合方案是自动发现的,不需要手写规则。SuperKernel 的 codegen 引擎拿到融合方案后,生成一段 Ascend C 代码,把多个算子的计算逻辑合并到同一个 Kernel 函数中,然后通过 JIT 编译成可在昇腾达芬奇架构上执行的二进制。
JIT 编译流程:从算子序列到可执行 Kernel
graph-autofusion 的 JIT 编译流水线是整个框架的核心数据通路。一个算子序列从输入到最终执行,经历五个阶段:
[图2:graph-autofusion JIT 编译流水线,展示五个阶段的顺序流转:算子序列输入 → Autofuse 融合分析 → Codegen 代码生成 → JIT 编译 → 运行时执行]
阶段一:算子序列输入
框架接收一段待执行的计算图子图,通常是由上层框架(如 PyTorch)拆解出的连续算子序列。每个算子携带自身的类型、输入输出张量描述、数据类型等信息。
阶段二:Autofuse 融合分析
这是 graph-autofusion 区别于手动融合的关键环节。Autofuse 组件对算子序列做依赖分析,核心逻辑可以拆解为两步:
- 构建数据流图:把算子序列建模为 DAG(有向无环图),节点是算子,边是张量依赖
- 识别融合候选:遍历 DAG,基于融合规则判断相邻算子是否可以合并
融合规则不是硬编码的固定规则表,而是基于硬件特性的启发式判断——比如两个算子的数据布局是否兼容、融合后的计算是否能在片上存储中完成、是否存在不可融合的操作(如跨设备同步)。
// 1 融合分析的伪代码,展示 Autofuse 的核心逻辑// 2 inputs: op_sequence(算子序列)// 3 output: fusion_groups(融合分组方案)// 4 dag = build_dag(op_sequence) // 构建数据流图// 5 fusion_groups = [] // 初始化融合分组// 6 for each node in dag.topological_order: // 拓扑序遍历// 7 if can_fuse_with_prev(node): // 判断能否与前驱融合// 8 merge_into_group(node, prev_group) // 合并到同一融合组// 9 else:// 10 create_new_group(node) // 开新组// 11 return fusion_groups逐行解释:
- 第 4 行:将算子序列建模为 DAG,边的方向代表数据流动方向。这一步是后续所有分析的基础
- 第 5 行:初始化空的融合分组列表,每个分组最终会被编译为一个 SuperKernel
- 第 6 行:按拓扑序遍历,保证被依赖的算子先被处理。这一顺序确保融合判断时前驱算子已经完成分组
- 第 7 行:核心判断——
can_fuse_with_prev内部检查数据布局兼容性、片上存储容量、算子类型约束等条件。不是所有相邻算子都能融合,比如涉及跨 AICore 通信的算子就必须断开 - 第 8 行:如果条件满足,当前算子并入前驱所在的融合组
- 第 10 行:如果不满足,当前算子开启新的融合组,独立编译为单独 Kernel
- 第 11 行:返回的融合分组方案,直接交给 Codegen 阶段
阶段三:Codegen 代码生成
拿到融合分组方案后,Codegen 引擎为每个融合组生成一段 Ascend C 代码。这段代码将融合组内所有算子的计算逻辑合并到一个 Kernel 函数中,中间结果通过片上缓冲区传递。
// 1 // Codegen 生成的 SuperKernel 伪代码示例// 2 // 融合组:[MatMul → Add → ReLU]// 3 __global__ void superkernel_fused_matmul_add_relu(// 4 __gm__ half* input, __gm__ half* weight,// 5 __gm__ half* bias, __gm__ half* output, int M, int K, int N) {// 6 // MatMul 阶段// 7 half tile_a[TILE_M][TILE_K]; // 从 GM 搬入输入分块// 8 half tile_b[TILE_K][TILE_N]; // 从 GM 搬入权重分块// 9 half tile_c[TILE_M][TILE_N]; // MatMul 结果,不写回 GM// 10 matmul_compute(tile_c, tile_a, tile_b, M, K, N);// 11 // Add 阶段:直接在 tile_c 上操作,零搬运// 12 add_bias(tile_c, bias, M, N);// 13 // ReLU 阶段:仍在 tile_c 上操作// 14 relu_inplace(tile_c, M, N);// 15 // 只有最终结果写回 GM// 16 write_back(tile_c, output, M, N);// 17 }逐行解释:
- 第 3-5 行:SuperKernel 函数签名,输入包括原始三个算子各自需要的所有张量。
__gm__标记全局内存(HBM)上的数据 - 第 7-9 行:MatMul 所需的三个分块缓冲区,分配在片上存储。注意
tile_c是 MatMul 的结果,但此时不写回 HBM——这是融合收益的核心来源 - 第 10 行:执行 MatMul 计算,结果留在
tile_c中 - 第 12 行:Add 操作直接读取
tile_c和 bias,计算结果覆写回tile_c。零次额外 HBM 读写 - 第 14 行:ReLU 同理,原地操作
tile_c - 第 16 行:融合组的最终结果才写回 HBM。三个算子只产生一次 GM 写入,而非三次
整个 Codegen 过程不需要人工干预——开发者不需要写这段代码,Autofuse 的分析结果直接驱动 Codegen 生成。
阶段四:JIT 编译
生成的 Ascend C 代码通过 JIT 编译器,在运行时编译为昇腾达芬奇架构可执行的二进制。JIT 编译的优势在于延迟绑定——编译时才能确定的参数(如张量形状、数据类型)可以在运行时才固化,不需要为每种形状提前编译好所有 Kernel 变体。
第一次执行某个融合 Kernel 时会触发编译,有几十到几百毫秒的编译延迟。后续相同形状的调用直接命中缓存,零额外开销。
阶段五:运行时执行
编译产物通过 Runtime 加载到 Device,作为单个 Kernel Launch 执行。从 Host 视角看,融合组就是一次aclrtLaunch调用,跟调用单个算子没有区别。
Autofuse 的策略:自动发现,无需人工规则
Autofuse 的融合策略是 graph-autofusion 最具差异化的部分。要理解它的价值,得先看传统的手动融合是怎么做的。
在 ATB(ascend-transformer-boost)中,融合算子是预定义的。ATB 的三层架构——基础原生算子、图算子、插件——其中"图算子"层就是专门做融合的。但每个图算子都是开发者手动编写的:手动指定哪几个算子融合、手动编写融合后的计算逻辑、手动注册到 ATB 的算子库。好处是优化充分、可控性强,坏处是覆盖面受限于开发资源。
| 维度 | ATB 图算子 | graph-autofusion Autofuse |
|---|---|---|
| 融合发现方式 | 预定义模式匹配 | 运行时自动分析 |
| 覆盖范围 | 受限于已实现的图算子数量 | 理论上覆盖所有满足条件的算子组合 |
| 优化深度 | 人工深度优化,极致性能 | 自动生成,性能接近手动融合 |
| 开发成本 | 每个融合模式需专人开发 | 零人工融合规则开发 |
| 适用场景 | 高频关键路径(如 Attention 融合) | 长尾算子组合的融合挖掘 |
Autofuse 不替代 ATB,两者是互补关系。对于 Attention、MoE 这类高频且优化空间巨大的融合场景,ATB 的手动深度优化仍是首选;而对于那些数量庞大但单个出现频率不高的算子组合——正是 Autofuse 发挥价值的地方。手动覆盖不了的长尾,交给自动发现。
Autofuse 的判断逻辑本质上是在回答三个问题:
- 数据依赖是否允许融合?——如果算子 B 的输入不只依赖算子 A 的输出,还依赖其他未融合算子的输出,就不能简单地把 A 和 B 合并
- 片上存储能否容纳中间结果?——融合后中间数据留在 SRAM/UB,如果数据量超出片上容量,强制融合反而会触发溢出回写,性能不升反降
- 计算单元是否兼容?——昇腾达芬奇架构上有 Cube(矩阵计算)和 Vector(向量计算)两类单元,同一 SuperKernel 内部的计算步骤需要在两类单元间正确切换调度
这三个约束条件并不复杂,但组合起来能覆盖绝大多数实际情况。Autofuse 做的就是在约束空间内搜索最优分组——不是暴力穷举,而是基于启发式规则的贪心搜索,时间复杂度是线性的。
核心依赖:极简设计哲学
graph-autofusion 仅依赖两样东西:Ascend C 和 Runtime。
这个极简依赖不是偷懒,而是刻意的设计选择。Ascend C 提供算子编程能力,Codegen 生成的 SuperKernel 代码就是 Ascend C 代码;Runtime 提供设备管理和任务下发能力,JIT 编译产物通过 Runtime 加载执行。仅此而已。
不需要 GE 图引擎参与——graph-autofusion 可以独立于图编译流程工作。不需要 ATB——融合发现和代码生成是自包含的。不需要额外的编译器前端——JIT 编译器本身就是框架内置的。
这种"只依赖最底层能力"的设计,使得 graph-autofusion 可以在多种接入模式下工作:作为图编译器的子模块、作为推理框架的内嵌优化器、甚至作为独立的命令行工具。
[图3:graph-autofusion 依赖关系图,graph-autofusion 居中,左侧箭头指向 Ascend C 和 Runtime 表示依赖,右侧箭头指向 ops-* 算子库表示融合目标,上方箭头表示可被上层框架(PyTorch/MindSpore)集成]
设计取舍:自动发现 vs 手动极致
任何自动优化框架都面临一个根本取舍——自动化带来的覆盖面 vs 手动优化带来的极致性能。graph-autofusion 选择了偏向自动化的一端,但并没有完全放弃性能。
它的策略是分层:
- 通用融合层:Autofuse 自动发现的融合机会,覆盖面广,性能收益中等
- 关键路径手动优化:对于 Attention 这类热点,仍由 ATB 图算子做极致优化
这种分层策略在实践中是合理的。一个典型的大模型推理场景中,可能有 200 个算子,其中 20 个是热点(占总耗时 80%),剩下 180 个是长尾。手动优化能覆盖那 20 个热点,但 180 个长尾算子之间的融合机会,靠人力根本挖不完。Autofuse 在长尾部分的收益,往往比热点部分更可观——因为长尾算子之间的融合机会从未被人工审视过。
从工程角度看,graph-autofusion 还有一个隐性价值:它降低了算子融合优化的门槛。过去只有深入理解昇腾达芬奇架构的资深开发者才能写出高质量的融合算子;有了 Autofuse,普通开发者只需关注模型逻辑,融合优化交给框架自动完成。
核心价值
graph-autofusion 的核心价值可以用一句话概括:让编译器自己学会融合。
从人工写融合规则到自动发现融合机会,从逐算子 Launch 到 SuperKernel 一次执行,从依赖 ATB 图算子到独立自包含——graph-autofusion 把算子融合从"专家活"变成了"框架活"。开发者不再需要为每一对新算子组合手写融合代码,编译器自己会找到最优的分组方案,自己生成代码,自己编译执行。
这种"把人从规则编写中解放出来"的思路,跟编译器优化的历史一脉相承——从手写汇编到编译器自动优化,从手写 CUDA kernel 到 Triton 自动生成,每一次"让机器做机器该做的事"的进步,都释放了开发者的精力去做更有创造性的事。
graph-autofusion 目前仍在持续演进中,对更多算子类型的融合支持、更精确的片上存储容量估算、更智能的融合策略搜索,都是社区正在推进的方向。仓库地址:https://atomgit.com/cann/graph-autofusion
自检报告
字符串扫描
✅ 全部通过。无Pytorch、AscendC、华为CANN、Ascend 910A/910B、TBE、值得注意的是、综上所述等。
架构校验
✅ CANN 定位为"昇腾异构计算架构";AscendCL 未出现混淆;ATB 定位为加速库;五层架构描述正确;amct 未出现。
事实校验
✅ 仓库名 graph-autofusion 来自知识库清单;定位"算子自动融合框架"与知识库一致;仓库链接格式正确https://atomgit.com/cann/graph-autofusion;无虚构 API(代码为伪代码,已标注);融合举例匹配主题。
质量反诘
Q1: 核心事实未在此前文章中作为核心论据使用(首篇文章)
Q2: 删掉比喻后,技术事实涵盖:融合分析逻辑、JIT 五阶段流水线、SuperKernel 代码结构、Autofuse 三约束条件、ATB 对比、依赖分析——不止三句话
Q3: 有具体数字:微秒级 Launch 开销、几十到几百毫秒编译延迟、200 算子 / 20 热点 / 80% 耗时
Q4: 未参考 README,基于知识库独立生成
Q5: 无凑字数段落
自检结论
✅ 通过,可输出