
我前几天在排查一个视觉模型的端到端推理耗时和预期差距不小。用 Profiling 工具导出的算子耗时列表里排在前面的是大量几十微秒级别的小算子有加有乘有激活单个看都不贵可加起来就是好几百次 kernel 调度和一遍遍的中间数据写盘时间。那一刻我意识到问题不在某个算子而在算子之间缺少合并——这正是 CANN Graph-Autofusion 自动算子融合该出场的地方。这篇内容我打算写清楚Graph-Autofusion 是什么时候干的活、靠什么决策、适用哪些常见的算子组合、怎么验证收益以及我在调优过程中踩到的一些坑。做模型部署、推理性能优化或者刚接触 AI 编译器工程的开发者应该都能从中拿走一点直接能用的东西。1. 先算清性能账融合到底把哪些开销省掉了很多人对算子融合的理解停留在减少 kernel 数量所以更快这个层面这个方向是对的但不够。如果不把开销拆开你就很难判断自己的模型到底值不值得依赖自动融合也很难解释为什么有时候看起来明明融合了性能反而没变化。1.1 单算子执行的固定成本如果把一个算子在 NPU 上的执行看作一次完整工序它的耗时大致可以分成三块调度下发、数据搬运、计算本身。调度下发对应 kernel 的启动和硬件任务分配数据搬运指张量在外部存储和片上缓存之间的进出计算才是真正在那个算子逻辑上花的时间。问题出在小算子身上。一个 Add 或者 ReLU计算量本身就很小计算时间可能只占整个算子耗时的很小一部分剩下的时间几乎全耗在调度和数据反复搬运上。这就像你在工厂流水线上给每个零件配一个独立的加工台零件每走一步都要先运到仓库、再运回来加工只花十秒搬运却用了两分钟。融合的核心思路就是把多个加工台合并成一条产线让半成品留在工作台上直接流转。1.2 访存账一次融合省下多少 DDR 读写为了搞清楚收益到底有多大我习惯先估算一笔访存账。拿最简单的组合来举例t3 t1 t2接着t4 relu(t3)。假设每个张量都是 N 个元素分开执行时外部存储侧看到的数据流动大约是写 t3N 个元素读 t3N 个元素写 t4N 个元素融合成一个算子后t3 可以直接在片上被计算并消耗掉不需要先写回外部存储再读回来省下的就是两笔 N 的移动。如果 N 是 128MB而目标机器的有效访存带宽是每秒 512GB那么这两笔省下来的时间大约是128MB * 2 / 512GB/s ≈ 0.5ms。单个融合点省 0.5ms 看起来不多但一个模型里可能有几十上百个这样的模式叠加起来就非常可观。注意融合省的是中间结果往返外部存储的时间它不会凭空减少最终输入和输出的数据量。判断一个模型适不适合靠融合优化首先要看它在 Profiling 里是访存密集还是计算密集。如果整体是访存瓶颈融合往往立竿见影如果早就被计算塞满融合收益就会小很多。2. Graph-Autofusion 在 CANN 图编译流水线中的位置与触发条件我刚开始接触这功能时有个误区以为它是运行时的一个调度策略或者要在模型代码里显式调用什么 Fusion API。后来才搞明白它属于图编译阶段的内置优化项位置很靠前。2.1 从模型文件到离线模型的链路当你把训练好的模型部署到基于 CANN 的硬件上时会先经过一段编译流程原始模型文件进入图准备阶段做算子映射和基本校验再进入图优化阶段这里就是各类图变换 Pass 的舞台优化完的图中算子会被调度并生成离线模型文件运行时引擎再加载离线模型执行。Graph-Autofusion 就坐落在图优化阶段。它拿到的是一个算子级 DAG输出是经过合并的融合算子集合。整个过程并不需要你在训练代码里写任何标记框架侧的导出、转换流程走完编译工具链会自动扫描整个图寻找可融合的算子组合。这带来一个很实际的收益同一个模型在框架侧不需要任何改动只要改变编译选项就能让部署包里的算子数量与访存模式发生明显变化。这也是我在性能调优时最先检查的项目。2.2 图 IR 层面的融合 Pass 工作原理融合的核心机制是模式匹配 合法性约束。编译器遍历图寻找形如 A 算子的输出直接作为 B 算子输入这样的连续子结构然后集中检查几件事算子的数据类型、形状语义是否兼容中间结果是否只有唯一消费方。一旦某个中间张量被多个下游算子同时使用把它并进单一融合子图就可能引发重复计算或额外拷贝算子本身是否可融合。像带随机数、有状态更新、依赖全局内存语义的算子通常会被排除融合后的片上资源需求是否超出目标核的限制包括寄存器、缓存和缓冲区上限。全部检查通过后编译器把这组算子折叠成一个融合子图并在代码生成阶段把对应逻辑拼成一段融合 kernel。整个过程通常会以多趟扫描的方式反复进行直到当前图中的可融合模式都被处理完。2.3 什么情况自动融合会主动放弃有一个问题大家经常会问既然融合能省访存为什么不把所有算子全并成一个超级大的算子答案是过度融合往往得不偿失。典型的不融合场景包括融合后需要额外引入临时空间来适配某一种数据格式转换中间结果被多个分支消费合并会导致同一份数据被重复计算规约类算子带来的维度收缩太复杂索引重映射的开销大于访存收益。编译器对这类情况会保守处理。所以不要用融合了多少对算子来衡量工具好不好这是调优心态问题。图优化阶段有成本收益模型它决定的是整体性能最优而不是融合数量最大。3. 自动融合的主要算子组合模式与收益场景Graph-Autofusion 不是只处理某一种组合而是对多种常见模式都有匹配规则。下面这张表是我在实际调优中最常遇到的几类融合模式典型算子组合解决的核心问题常见出现位置逐元素链融合Add、Mul、ReLU、Clamp、Sigmoid 等连续算子调度次数过多中间张量反复读写激活前后、残差相加、归一化后处理规约类融合上游逐元素算子 Mean/Sum/Max/Min规约前的整块中间结果不再写回全局池化、通道统计、打分输出Format 转换融合TransData/Transpose 计算算子框架层看不到的排布转换开销被消除数据进入输出前的排布切换、前后处理卷积链融合Conv 激活 / Scale多个访存敏感的算子合并为单次执行CNN 主干中每个卷积块之后3.1 逐元素链融合最常见的甜点逐元素链几乎在每个模型里都存在。比如一个典型片段是t3 t1 t2 t4 relu(t3) t5 t4 * scale三个算子分开跑t3 和 t4 都会先写外部存储再被下一步读回每一笔都是 N 量级的搬移。融合成一步之后t3 和 t4 都只存在于片上暂存区逻辑上等价于一次完成t5 fused_add_relu_mul(t1, t2, scale)这类融合对小张量可能感觉不到但在特征图比较大的浅层特别明显。宽泛地说在访存密集的逐元素链上融合后这部分耗时能降 30% 到 60%。这也是 Graph-Autofusion 收益最直观的一类场景。3.2 规约类融合省掉整份中间结果Mean、Sum、Max 这类规约算子的特点是输出极小。比如一个[1, 2048, 7, 7]的特征图经过全局均值池化后变成[1, 2048, 1, 1]输出数据量只有输入的几十分之一。如果不做融合上游激活要把这整块[1, 2048, 7, 7]的数据写进外部存储规约算子再完整地读一遍——这是典型的为了搬一块砖先运一整卡车砂石到仓库再把砖挑出来。融合后上游算子的输出直接进入规约逻辑一边计算一边丢弃中间的大块结果完全不需要落地。这类收益在检测模型里的全局特征聚合、注意力打分归一化之前尤其明显。我遇到的一些模型仅仅把最后一个全局池化前的激活链正确融合端到端时延就能改善好几个百分点。3.3 Format 与 TransData 融合框架里看不见的开销这一类容易被忽视因为大部分训练框架里根本没有对应的算子概念。框架模型里的数据排布通常是 NCHW、NHWC 之类但 NPU 要发挥张量单元的效率运行时往往需要把数据转成更适合向量化计算的内部排布。这个转换在图里就是一个完整算子且伴随全量数据搬移。Graph-Autofusion 会尝试把这种排布转换算子与相邻的计算算子合并让转换在计算过程中顺带完成而不是先转换、再计算、各走一遍完整数据流。很多模型在部署后第一轮优化里收益最直接的部分往往不是计算算子本身而是省掉了大量隐藏的格式转换搬移。4. 端到端实测从 Profiling 到确认融合收益理论分析终归要落到数据上。我自己的习惯是拿到一个性能不达标的模型先不急着改模型结构而是先看 Profiling把访存和调度的账算清楚再决定要不要信任自动融合、要不要配合手动优化。4.1 怎么读取热点算子指标用 CANN 配套的 Profiling 工具导出算子级耗时数据后我会重点看三个信号耗时的分布形态。如果排名靠前的是大量耗时占比很小的算子说明调度和访存开销可能比想象中大中间张量的数据量与访问次数。如果中间张量总量明显大于输入输出总量说明存在大量不必要的中间写回AI Core 利用率。如果利用率不高但数据搬移占比很高说明模型整体是访存瓶颈。这几个信号同时出现基本可以断定自动融合能带来可感知的收益。4.2 对比实验怎么做要验证 Graph-Autofusion 的贡献不能只拿一个开启融合的模型看绝对性能你需要做隔离对比。我的做法是固定同一份权重和输入开启和关闭图编译融合优化分别导出两个离线模型然后各跑若干轮取稳定后的数据。对比维度包括端到端时延、算子或子图数量、中间数据搬移量。第一轮推理结果要丢弃硬件频率、内存状态稳定后再统计。这里有个容易被忽略的细节关闭融合后的模型也可能因为编译路径不同产生额外差异所以不要只测一次多跑几轮取中位数更稳妥。4.3 一组来自实际项目的脱敏数据拿我最近处理的一个分类网络举例。原始图中大约有 300 个算子开启自动融合后算子数量降到 190 左右。关闭融合时中间数据的往返访问量非常大开启后这部分数据搬移大约下降了四成。在固定输入尺寸、单卡单测的条件下端到端时延改善了约 25%。这个比例只代表我当前环境下的典型情况不同模型、不同输入规模差异很大。但有一点是稳定的这类以访存开销为主的模型中图编译期的自动融合几乎是投入产出比最高的一步优化。5. 自动融合的常见坑精度偏移、动态形状和过度融合自动融合不是没有代价。我用了很久之后才发现它有几个隐藏问题建议做部署优化的人提前知道。5.1 融合后精度出现微小偏移第一个坑是精度变化。融合本质上是把一串算子的计算顺序和中间表示做了重排浮点运算的累加顺序一旦变化舍入结果就可能不一样。比如把A B C变成(A C) B或者把规约累加顺序调整数值可能差一点点。对于绝大多数推理场景这点偏差在可接受范围内。但如果模型对数值变化很敏感比如某些关键路径上的 logits 分布非常窄就需要认真排查。我的排查手段是同一权重、同一输入分别跑开启与关闭融合的模型导出中间张量对比相似度。如果某个融合子图的输出差异偏大就针对它单独关闭融合或限制融合范围。5.2 动态 Shape 导致融合静默失效第二个坑和动态形状有关。很多推理服务为了灵活性允许输入尺寸动态变化这在图编译期带来了一个问题如果一个融合模式需要编译期确定临时缓冲区大小而尺寸在编译期不可知编译器就会保守地放弃融合。结果就是你明明开启了自动融合实测性能却没有想象中好。这个静默失效很难一眼发现因为工具不会报错。我的建议是在业务允许的范围内尽可能固定输入尺寸如果必须支持动态尺寸就给动态维度设定一个合理区间让编译器有信息推导出缓冲区尺寸。5.3 过度融合反而变慢第三个坑是我真正踩过的。有一次我为了验证自动融合的极限把一个分支结构里所有的 Add 算子都配成可融合模式结果编译出来的融合算子内部资源压力过大编译器被迫在融合 kernel 内部插入多次中间写回端到端性能反而比不融合还差。后来我把这个超大融合子图拆成更小的组合性能立刻回来了。这说明融合度不是越高越好。遇到开启融合后反而变慢的情况优先怀疑某个超大融合子图内部的缓存溢出问题而不是怀疑融合方向本身。5.4 融合后的热点定位难度上升融合之后原来三百个小算子变成几十个大算子Profiling 输出里每个融合算子占据一块很大的耗时但工具默认不会展开融合子图内部的细节。这给后续热点定位带来麻烦你知道一个组合算子慢但你不知道慢在哪一个内部环节。我的做法是保留一个关闭融合的离线模型作为定位专用版本先用它定位到热点算子组再回到融合模型里看对应融合子图的整体行为。这个过程虽然多一道工序但能显著缩短排查时间。6. 自动融合之外的补充手段与一点经验Graph-Autofusion 帮我们消化了通用算子组合但性能调优不会止步于此。它之外还有一些手段可以配合使用有时能达到单靠自动融合达不到的效果。6.1 什么时候值得手动融合自动融合覆盖的是通用模式。如果你的模型里有业务自定义的特殊组合比如自定义注意力掩码加 Softmax 这种有特定语义的链路编译器可能没有对应的匹配规则这时手动融合的价值就出来了。手动融合有两种典型做法一种是在算子开发层面直接编写融合 kernel把两三个算子合成一个自定义算子另一种是在图上把几个连续算子显式封装成一个复合算子再交给编译工具链做后续优化。我的建议是优先尝试后者因为它保留了一定可维护性只有当收益上限确实高、热点明确集中在某段自定义链路上时再考虑手写 kernel。手动融合的代价是推导参数、调试和后续维护除非收益足够大不建议全面铺开。6.2 Layout 选择要前置到模型侧优化做得越早图编译期的补救就越少。如果训练模型时选择的目标排布与推理硬件的偏好一致编译器就不需要在图里插入额外的排布转换算子自动融合的压力也会小很多。另外很多框架模型写得很碎一个小算子接一个小算子。虽然自动融合能处理但太碎的图会增大匹配和验证成本也可能让一些本可以合并的语义被冲散。在模型侧把逻辑上属于同一个操作的算子尽量合并表达能给编译器留出更大的优化空间。6.3 对自动融合的个人体会回到开头说的那个性能排查。后来我把模型重新编译并开启默认优化后Graph-Autofusion 帮我消化了绝大部分小算子链端到端时延也回到了预期范围。但让我收获最大的不是那 25% 的提升而是理解了它背后的收益边界。自动融合本质上就是把手工做算子合并这件事搬到编译器里由工具自动完成。它能帮你解决通用模式但不负责替你思考业务逻辑。做部署优化的人与其把自动融合当成黑盒不如把它当成一个懂算子合并、但需要你设定合理边界和验证指标的协作对象。了解它能做什么、不能做什么你才能真正用好它。