前言

在昇腾NPU上进行深度学习模型部署时,算子(Operator)的执行效率是决定端到端吞吐量的核心因素。计算图中的每个算子在物理层面都对应一次内核(Kernel)下发动作,而内核下发本身存在不可忽视的固定开销——指令译码、调度队列排队、硬件资源仲裁、事件同步等阶段叠加起来,单次调度的开销在数十至数百微秒量级。当模型规模庞大、包含数十乃至数百个算子时,调度开销在总耗时中的占比会迅速攀升,成为性能瓶颈。

CANN(Compute Architecture for Neural Networks)是昇腾芯片的软件栈名称,提供了从上层模型到底层硬件的完整编译与运行时支持。Graph-AutoFusion 是 CANN 开源组件集合 graph-autofusion 中的核心技术模块,它包含了 Autofuse 和 SuperKernel 两个子组件,分别从不同的技术路径解决算子融合问题。Autofuse 通过自动识别可融合的相邻算子并生成融合内核,减少 Global Memory 到 Local Memory 的数据搬移次数;SuperKernel 则通过 JIT 即时编译将多个子算子重新编译为单一超算子,降低调度开销并通过 ICache 预取、Early-Start 等手段提升指令级并行度。

理解这两个组件的融合决策机制,对于调优工程师而言是深度性能诊断的前提。融合并非无代价地将所有算子堆叠在一起——寄存器压力、共享内存容量约束、硬件指令调度冲突、Tile 策略不兼容等问题都可能使得看似合理的融合计划在编译阶段失败或在线执行时反而性能下降。本文从算子融合的代价分析出发,系统阐述图融合的数学模型、硬件亲和性约束、多级融合策略、调试方法以及自定义 Pattern 的扩展机制,力求为 CANN 平台的调优工程师提供一份完整的技术参考。

1. 融合的必要性与代价

1.1 数据搬移减少的原理

深度学习模型中的大多数算子(卷积、矩阵乘法、激活函数等)在执行时,数据流向遵循固定的路径:输入数据从 Global Memory(片外 DRAM)读取到计算单元的本地寄存器或共享内存(Local Memory),计算结果写回 Global Memory,再由下一个算子读取。这个「Global Memory 到 Local Memory 再到 Global Memory」的循环在模型中每个相邻算子对之间都会发生一次。

在昇腾NPU的微架构中,Global Memory 访问延迟通常在数百个时钟周期,而 Aicore(昇腾计算核心)的 Vector 单元和 Cube 单元在等待数据期间处于空闲状态。对于 Memory Bound 类算子(即计算强度较低、大量时间消耗在数据读取上的算子),这种空闲等待占总执行时间的比例可以超过 60%。算子融合的核心思想是:将语义上存在数据依赖关系的相邻算子在同一个内核中顺序执行,使得中间结果的传递完全在寄存器或共享内存层面完成,无需写回 Global Memory 再重新读取。以一个典型的 elementwise 链为例:Add(Gelu(x)) 可以融合为一个内核,其中 x 的读取、Add 计算、Gelu 计算和结果写回均在一次数据加载过程中完成,中间结果无需访问外部存储。

Autofuse 组件正是针对这一场景设计的自动融合框架。基于 Ascend C 的代码生成能力,Autofuse 能够识别模型计算图中满足融合条件的算子组合,自动生成融合后的算子代码,并配合 Auto Tiling 优化确定最优的分块策略。根据 graph-autofusion 仓库中 autofuse 模块的结构定义,其 codegen 目录负责内核代码生成,att 目录负责自动 Tiling 生成,optimize 目录负责调度切分,这三个子模块协同完成从算子识别到代码生成的完整流程。

1.2 融合后寄存器和共享内存的压力变化

融合的收益并非没有上限。将多个算子合并到同一个内核中,意味着同一个内核实例需要同时持有更多算子的中间状态和临时缓冲区,这对硬件资源提出了更高要求。

昇腾 Aicore 中的寄存器文件是有限的资源。每个物理寄存器的位宽固定,而每个算子内部展开的循环体都需要一定数量的活跃寄存器来保存索引变量、累加器和中间计算结果。当融合后的算子数量超过寄存器文件的容量时,编译器需要将部分数据溢出到共享内存(UB,Unified Buffer),这会引入一种反向开销——原本用于减少 Global Memory 访问的融合策略,反而可能增加 UB 与寄存器之间的数据交换次数。

共享内存(UB)的容量同样有限。在昇腾架构中,UB 是 Vector 单元和 Cube 单元共享的高速存储区域,容量通常在几十 KB 量级。融合后的内核如果需要同时缓存多个算子的输入、输出和中间结果,UB 的占用量会线性增长。当 UB 占用超过阈值时,会导致 Tile 尺寸被迫缩小——内核需要处理更多的 Tile 迭代次数,每次迭代处理的数据量更小,循环控制开销增加,反而可能使性能劣化。

graph-autofusion 中的 optimize 模块承担调度切分职责,正是为了在寄存器压力、UB 容量和 Tile 策略之间寻找平衡点。融合决策模块会在代码生成之前评估融合后的资源占用,如果预估资源需求超过硬件限制,则触发融合边界调整或融合回退。

1.3 融合边界判定中的「收益 vs 成本」模型

融合边界(Fusion Boundary)的判定是整个自动融合系统的核心决策点。给定计算图中相邻的两个算子 A 和 B,融合引擎需要回答一个问题:将 A 和 B 融合为单个内核 A+B,在硬件资源约束下,其执行时间是否严格小于 A 和 B 分别执行的时间之和?

这个判定过程可以建模为一个成本-收益分析框架:

收益项(Benefit) 主要包括:Global Memory 读写次数减少带来的数据搬移时间节省(记为 T_mem_saved);算子调度次数减少带来的固定调度开销节省(记为 T_sched_saved);指令级并行度提升带来的潜在计算吞吐提升(记为 T_ilp_gain)。

成本项(Cost) 主要包括:融合后内核寄存器压力增加导致的额外溢出开销(记为 T_reg_spill);UB 容量压力增加导致的 Tile 数量增加开销(记为 T_tile_overhead);编译时寄存器分配和指令调度复杂度增加导致的编译时间增长(记为 T_compile)。

融合边界判定的决策函数为:

if (T_mem_saved + T_sched_saved + T_ilp_gain > T_reg_spill + T_tile_overhead):
    merge(A, B)
else:
    keep_separate(A, B)

Autofuse 的融合决策引擎在执行过程中会遍历模型中的所有算子对,对每对算子应用上述判定逻辑。实际的工程实现中,classify_rule.yaml 配置文件定义了不同算子类型的融合优先级和约束条件,系统会先根据算子类型快速过滤出候选融合对,再对候选对进行精确的资源预估计算。

1.4 效率对比分析

在引入 Autofuse 自动融合前后,模型执行的性能特征发生了系统性变化。以下从四个核心维度对比融合前后的差异:

维度 融合前(未使用 Autofuse) 融合后(使用 Autofuse) 差异来源
Global Memory 读写次数 每个算子独立加载/存储输入输出 中间结果在寄存器间传递,减少 N-1 次读写 算子间数据依赖链缩短
算子调度开销 每个算子单独下发内核,N 个算子产生 N 次调度 N 个算子融合为 M 个内核(M < N),调度次数减少 融合后内核数量减少
Vector 计算单元利用率 Memory Bound 算子频繁等待数据,利用率低 数据搬运与计算重叠度高,利用率提升 流水线级联设计
端到端 kernel 总耗时 所有单算子耗时累加 融合算子耗时降低 消除重复数据搬运

需要特别说明的是,上述差异来源中的「Vector 计算单元利用率」提升并非无条件成立。当融合后的内核变得 Compute Bound(计算受限)时,Vector 单元的利用率会接近饱和,但此时整体执行时间已经由计算本身主导,融合带来的边际收益递减。融合决策引擎需要根据算子的计算密度(Arithmetic Intensity,即每字节数据传输对应的浮点运算次数)来区分这两种状态,从而做出正确的边界判定。

2. 图融合的数学模型

2.1 算子融合问题形式化为子图同构搜索

在图计算的语境下,模型计算图是一个有向无环图(DAG),节点代表算子,边代表张量依赖关系。算子融合的目标是从这个 DAG 中找出若干个满足特定条件的子图,将每个子图替换为单个融合算子节点。

形式化地说:给定原图 G = (V, E) 和一个模式图 P = (Vp, Ep),融合搜索的任务是在 G 中找到所有与 P 同构的子图实例,并将每个实例替换为一个融合节点。这个问题在图论中被称为子图同构(Subgraph Isomorphism),其经典算法包括 Ullmann 算法、VF2 算法以及它们的改进变体。

然而,通用子图同构搜索的时间复杂度在最坏情况下是指数级的(NP-Complete),直接应用于包含数十万节点的大规模深度学习模型是不现实的。Autofuse 的实际实现采用了多层次的策略:先通过算子类型过滤器将候选空间大幅压缩,再对过滤后的子图进行精确的资源评估。

2.2 Pattern 匹配引擎的实现机制

Autofuse 的 Pattern 匹配引擎维护一个融合 Pattern 库,每个 Pattern 定义了一组算子类型序列以及它们之间的张量连接关系。匹配引擎的核心工作流程如下:

第一步是类型过滤(Type Filtering)。给定原图中一个待检查的节点序列 [op1, op2, …],引擎先查询每个算子的类型信息(如 elementwise、reduce、broadcast 等)。如果该序列中任意两个相邻算子的类型组合不在预设的类型对白名单中,则立即跳过该候选序列。autofuse 的 README 明确指出,当前版本支持 elementwise+element、elementwise+broadcast、elementwise+reduce 三类融合模式,这意味着引擎的类型过滤层只需要维护一个三行的类型兼容性矩阵即可实现高效过滤。

第二步是张量形状约束检查(Shape Constraint Checking)。即使两个算子的类型兼容,它们的输出张量形状也必须满足融合约束。例如,融合后的 Tile 策略要求输入数据的总字节数不超过 UB 容量的某个比例。引擎在这一步会查询张量的 shape、dtype、stride 等元信息,评估融合后的资源占用是否在安全范围内。

第三步是代码生成(Code Generation)。当一个候选 Pattern 通过了前两步检查后,引擎会调用 codegen 模块生成融合内核的源代码。代码生成器根据 Pattern 的定义和硬件参数(Tile 大小、Block 大小等)实例化模板代码,完成算子融合的核心工作。

以下是一个简化的 Pattern 定义示例,展示了 autofuse 中 Pattern 描述的典型结构:

# classify_rule.yaml - 融合规则配置文件
fusion_rules:
  - name: "elementwise_add_relu"
    pattern:
      - op_type: "Add"
      - op_type: "Relu"
    constraint:
      input_dtype: ["float16", "float32"]
      max_tile_size: 65536
    priority: 10

This YAML structure separates the semantic description of fusion patterns from their resource constraints, allowing the fusion engine to reason about compatibility at the type level before performing expensive shape-based validation.

2.3 DFS vs 启发式搜索的权衡

在 Pattern 匹配引擎中,遍历策略的选择直接影响融合发现的效率与覆盖率。

深度优先搜索(DFS)从计算图的某个起始节点出发,沿着张量依赖边递归探索所有可能的算子组合。DFS 的优势在于实现简单、内存占用低(只需要维护一个路径栈),在候选 Pattern 数量有限时表现良好。但 DFS 的缺点是容易陷入局部最优——一旦某个分支的融合尝试失败,DFS 可能会浪费大量时间在深度很大但最终无效的路径上。

启发式搜索(Heuristic Search)则通过评估函数对候选分支进行排序,优先探索预期收益最高的路径。典型的启发式评估函数可以考虑算子类型的融合成功率历史统计、当前候选路径的预估收益等因素。启发式搜索的缺点是需要维护一个优先级队列,内存开销高于 DFS,且启发式函数的准确性直接影响搜索质量——如果启发式函数过于乐观,会导致大量无效的深入探索;如果过于悲观,则可能遗漏有效融合。

Autofuse 的实际实现很可能采用了混合策略:在融合探索的早期阶段使用启发式搜索快速定位高收益融合区域,在确认的融合区域内部使用 DFS 精确枚举所有可能的融合组合。这种分层策略在保持搜索效率的同时避免了启发式偏差导致的遗漏。

以下伪代码展示了融合搜索的核心循环逻辑:

def find_fusion_candidates(graph):
    candidates = []
    visited = set()
    for node in graph.topological_order():
        if node in visited:
            continue
        for pattern in fusion_patterns:
            # Step 1: type filtering
            if not pattern.match_types(node, graph):
                continue
            # Step 2: shape constraint
            if not pattern.check_constraints(node, graph):
                continue
            # Step 3: add to candidate list
            candidates.append((node, pattern))
            visited.update(pattern.get_affected_nodes(node, graph))
    return candidates

def evaluate_merge(benefit, cost):
    # Cost-benefit model from Section 1.3
    net_gain = sum(benefit) - sum(cost)
    return net_gain > 0

3. 硬件亲和性约束

3.1 Tile 策略对 L1 / UB 容量的压力

昇腾 Aicore 的片上存储体系呈分层结构:L0(寄存器文件)位于计算核内部,访问延迟最低;UB(Unified Buffer,共享内存)位于 Aicore 外部但仍在片上,容量较大但访问延迟高于寄存器;L1/L2 Cache 位于片外但提供数据缓存;Global Memory(DRAM)位于芯片外部,访问延迟最高。

融合后内核的 Tile 策略直接影响 UB 的占用。Tile 是将大规模张量数据划分为小块(Tile)逐块处理的机制,每个 Tile 的数据需要完整驻留在 UB 中才能被计算单元高效访问。当融合的算子数量增加时,单个 Tile 内需要同时容纳多个算子的输入缓冲区、输出缓冲区和中间计算结果。以一个 Add + Relu 的融合为例:输入 x 和 y 需要各自的 UB 缓冲区,中间结果 Add(x, y) 需要额外的 UB 缓冲区,Relu 的输出也需要缓冲区——在极端情况下,单个 Tile 的 UB 占用可以达到单算子 Tile 的 3 到 4 倍。

如果 UB 占用超出了硬件容量限制,内核编译阶段会报错或退化为更保守的策略。autofuse 的 att(Auto Tiling)模块负责在融合后重新计算最优 Tile 参数。在融合场景下,Tile 大小的选择需要在「减少迭代次数」和「确保 UB 容量足够」之间进行折中。较小的 Tile 增加了循环迭代次数和边界检查开销,较大的 Tile 可能触发 UB 溢出导致寄存器溢出到 Global Memory,反而抵消了融合的收益。

3.2 融合边界与指令调度的冲突处理

硬件指令调度(Instruction Scheduling)是指编译器在生成目标代码时决定指令执行顺序的过程,其目标是在不改变程序语义的前提下充分利用硬件的流水线并行度,消除数据冒险(Data Hazard)和结构冒险(Structural Hazard)。

算子融合后,同一个内核中包含了原本属于不同算子的指令序列。编译器在对这个混合序列进行指令调度时,会遇到以下几类冲突:

第一类是寄存器资源冲突。不同算子的指令在编译器展开后可能同时需要访问相同的物理寄存器。如果寄存器分配器(Register Allocator)的分配策略不够精细,会导致寄存器 spilling(将寄存器内容写回内存以释放寄存器)增加,降低执行效率。

第二类是执行单元冲突。昇腾 Aicore 的 Vector 单元和 Cube 单元可以并行工作,但每种单元的流水线宽度有限。融合后的内核如果包含大量依赖 Vector 单元的 elementwise 指令和大量依赖 Cube 单元的矩阵乘法指令,指令调度器需要精心安排两类指令的交错顺序,以最大化双单元利用率。

第三类是内存访问冲突。融合后内核中的多个算子可能同时发起对 UB 或 Global Memory 的访问请求。如果两个算子的数据访问模式在某个 Tile 内形成地址冲突(多个核访问同一 Cache 行),会导致硬件级别的访问串行化。

Autofuse 的 optimize 模块负责处理调度切分问题,其核心职责之一就是在融合后重新评估指令调度可行性,必要时对融合边界进行微调以消除调度冲突。

3.3 融合失败时的回退策略

融合决策不是一锤子买卖。实际执行过程中,由于模型权重(Weight)的具体数值、输入张量的实际 Shape、编译时的资源预估误差等原因,某些在图级别判定为可融合的算子对,在内核编译阶段(甚至运行时)可能被检测为不可融合。

Autofuse 实现了完整的回退(Fallback)机制。当融合失败时,系统会回退到原始的单算子执行路径,确保模型仍然能够正常运行。具体来说,融合失败的日志中会输出类似 "Fallback aten.xxxx $reason: xx原因" 的诊断信息,用户可以通过设置 TORCH_COMPILE_DEBUG=1 环境变量将这些信息打印出来,分析失败原因并进行针对性修复。

根据失败阶段的不同,回退策略可以分为三个级别:

编译期回退(Compile-time Fallback):在内核代码生成或编译阶段发现资源不足、指令调度冲突等问题,系统自动将融合对回退为两个独立的单算子内核。这是最轻量的回退,不需要修改上层的计算图结构。

图级别回退(Graph-level Fallback):如果某个算子类型的融合尝试在多个位置反复失败,引擎会将其列入 blacklist.txt 黑名单,对该类型的算子暂时禁用融合功能。黑名单机制在 autofuse 模块根目录下的 blacklist.txt 文件中维护。

运行时回退(Runtime Fallback):即使内核编译成功,运行时也可能因为动态 Shape 不匹配、内存分配失败等原因导致执行异常。此时硬件调度器会捕获错误并回退到非融合路径执行。

4. 多级融合策略

4.1 逐层融合 vs 整图融合的适用场景

算子融合策略按照作用范围可以划分为两大类:逐层融合(Layer-by-layer Fusion)和整图融合(Whole-graph Fusion)。

逐层融合从计算图的入口节点出发,逐跳沿张量依赖边向外扩展,每一步只考虑当前节点与直接后继节点之间的融合可能性。这种策略的优点是搜索空间受限于邻接关系,融合决策的计算复杂度为 O(V),其中 V 是图中的节点数量,非常适合在线编译(Just-in-time Compilation)场景。逐层融合的缺点是可能陷入局部最优——从局部看收益最高的融合决策,从全局看可能阻止了更大范围的收益更高的融合机会。

整图融合则将整个计算图作为搜索空间,在全局视角下寻找最优的融合分区方案。这种策略的理论收益更高,因为可以发现跨越多个层次的大范围融合机会。但整图融合的计算复杂度在最坏情况下是指数级的,对于包含数十万节点的真实模型是不可接受的。

Autofuse 实际采用的是一种折中方案:通过逐层融合快速处理计算图中的高确定性融合机会(如连续的 elementwise 算子链),再对剩余的高价值区域进行局部整图搜索。这种分层策略在融合质量和编译时间之间取得了良好的平衡。

4.2 融合 Pass 的执行顺序设计

在编译器优化领域,「Pass」是指对计算图进行一次完整遍历并执行某种变换的优化阶段。多级融合通常需要多个 Pass 串联执行,而 Pass 的执行顺序对最终融合效果有显著影响。

考虑一个包含三个连续算子的计算图:Conv → Add → Relu。假设 Conv+Add 可以融合、Add+Relu 也可以融合,但 Conv+Add+Relu 三者融合在 UB 容量约束下不可行。如果先执行 Conv+Add 的融合 Pass,会生成一个 ConvAdd 融合节点;后续 Pass 再尝试 ConvAdd+Relu 融合时发现资源不足,则触发回退,最终只融合了 Conv+Add。反之,如果先执行 Add+Relu 的融合,再处理 Conv 与 AddRelu 节点的融合,可能会得到相同或不同的结果。

这说明融合 Pass 的执行顺序设计需要遵循以下原则:

互斥原则:如果两个融合 Pattern 存在重叠的节点(即使用相同的节点集合),它们不能同时生效。Pass 调度器需要维护一个「已融合节点集合」,确保后续 Pass 不会破坏已经建立的有效融合。

收益递减原则:大范围融合的边际收益通常高于小范围融合。因此应该优先尝试大范围融合,如果大范围融合失败,再尝试拆分为小范围融合。这一原则与「贪心策略」恰好相反——贪心策略倾向于先做确定性高的小融合,但可能导致后续大融合失去候选资格。

资源单调原则:融合操作只会增加资源占用,不会减少。因此如果某个候选融合在资源约束下被判定为不可行,那么所有包含该候选的更大范围融合都不可能是可行的。autofuse 的 optimize 模块可以基于这一原则进行剪枝,减少不必要的融合尝试。

以下是一个 Pass 调度器的简化实现框架:

class FusionPassScheduler:
    def __init__(self, graph, pattern_lib):
        self.graph = graph
        self.pattern_lib = pattern_lib
        # Patterns sorted by expected fusion scope (larger scope first)
        self.pattern_lib.sort_by_scope(descending=True)
        self.fused_nodes = set()

    def run(self):
        for pattern in self.pattern_lib:
            # Skip patterns that overlap with already-fused nodes
            candidates = pattern.get_candidates(self.graph)
            valid_candidates = [
                c for c in candidates
                if not self.overlaps_fused(c, self.fused_nodes)
            ]
            for candidate in valid_candidates:
                if self.evaluate_merge(candidate):
                    self.execute_merge(candidate)
                    self.fused_nodes.update(candidate.nodes)
        return self.graph

    def overlaps_fused(self, candidate, fused):
        return any(n in fused for n in candidate.nodes)

5. 融合决策的可视化与调试

5.1 导出融合前后的计算图

调试融合决策的第一步是能够观察融合前后的计算图结构。Autofuse 提供了完整的中途产物导出机制,用户可以通过设置环境变量控制是否导出以及导出哪些中间产物。

AUTOFUSE_DFX_FLAGS 是 autofuse 专用的调测环境变量,其完整配置格式如下:

export AUTOFUSE_DFX_FLAGS="--codegen_compile_debug=true;--debug_dir=/path-to-dump/"

其中 codegen_compile_debug=true 指示 autofuse 在代码生成阶段输出详细的调试信息,debug_dir 指定输出目录。导出的图结构文件格式为 pbtxt(Protocol Buffer Text Format),可以通过 netron.app(一个开源的神经网络可视化工具)打开并交互式浏览。

对于 SuperKernel 组件的图导出,torch 原生的 TORCH_COMPILE_DEBUG 环境变量控制调试信息的详细程度:

export TORCH_COMPILE_DEBUG=1
export TORCHINDUCTOR_FORCE_DISABLE_CACHES=1

启用后,执行目录下的 torch_compile_debug 子目录中会生成带有 autofused_ 前缀的子目录,每个这样的子目录对应一个融合算子的白盒结构信息。未融合的算子则对应不带此前缀的目录。TORCHINDUCTOR_FORCE_DISABLE_CACHES=1 的作用是禁用 Inductor 的编译缓存,确保每次执行都重新触发完整的融合和编译流程。

5.2 融合决策日志的解读方法

理解融合决策日志是调优工作的关键能力。当 TORCH_COMPILE_DEBUG=1 启用后,日志中会包含融合决策的详细信息。

如果某个本应被融合的算子对没有触发融合,日志中会出现 Fallback aten.xxx $reason: xx原因 的记录。aten.xxx 表示触发回退的算子类型,$reason 字段则标识了回退的具体原因。常见的回退原因包括:算子类型不匹配(不在融合 Pattern 白名单中)、Shape 不兼容(张量维度不满足融合约束)、资源不足(UB 或寄存器预估占用超限)、黑名单命中(该算子类型已被列入 blacklist.txt)。

融合成功的日志通常出现在内核代码生成阶段,会记录融合算子的输入 Shape、输出 Shape、融合类型以及生成的 Tile 参数。调优工程师可以通过对比融合前后同一算子对在不同 Shape 下的融合行为,识别出 Shape 敏感的融合边界条件。

5.3 异常融合导致的结果偏差定位

融合操作理论上不应该改变算子的数学语义,但实际实现中由于浮点运算顺序变化、精度截断差异、混合精度策略偏差等原因,融合后的执行结果可能与逐算子执行存在数值差异。这种差异在大多数情况下是微小的(由浮点运算的结合律决定),但在极端 Shape 或特殊数值分布下可能累积到不可忽视的程度。

Autofuse 支持与 CANN 精度调试工具链集成。对于精度异常,需要在融合前后分别运行相同的输入数据并记录每个算子的输入输出张量值,在精度调试工具中逐层对比,定位数值偏差首次出现的融合节点。

一个实用的调试技巧是逐步禁用融合:从完全启用融合的状态开始,逐个将关键算子对的融合禁用(二次验证法),通过二分搜索定位导致精度偏差的具体融合操作。这要求调优工程师对计算图中各个算子对的融合优先级有清晰的了解,可以通过分析 classify_rule.yaml 中的 priority 字段来建立优先级认知。

6. 自定义融合 Pattern

6.1 扩展 Pattern 库的方法

Autofuse 的 Pattern 库并非封闭系统。开发者可以通过修改 classify_rule.yaml 配置文件来添加新的融合 Pattern,无需修改核心编译代码。

添加新 Pattern 的基本步骤如下:先分析目标算子对的类型兼容性和资源约束,确定它们是否满足三类基础融合模式(elementwise+element、elementwise+broadcast、elementwise+reduce)之一;随后在 classify_rule.yaml 中的 fusion_rules 列表中添加新的规则条目。

classify_rule.yaml 中的每条规则包含以下关键字段:name 定义规则的名称(应具有描述性,便于日志识别);pattern 定义算子类型序列(按照融合顺序排列);constraint 定义融合约束(支持 input_dtype 白名单、max_tile_size 上限等参数);priority 定义融合优先级(数值越大优先级越高,在冲突情况下优先尝试高优先级规则)。

以下是一个添加自定义融合 Pattern 的示例:

# 在 classify_rule.yaml 中追加以下内容
fusion_rules:
  - name: "custom_mul_sigmoid_fusion"
    pattern:
      - op_type: "Mul"
      - op_type: "Sigmoid"
    constraint:
      input_dtype: ["float16"]
      max_tile_size: 32768
      require_contiguous: true
    priority: 15

6.2 Pattern 描述语法规则

Pattern 描述的核心是算子类型序列的声明。每个 Pattern 中的 pattern 字段是一个有序列表,定义了参与融合的算子类型及其顺序。列表中的每个元素至少包含 op_type 字段,表示算子的类型标识符。

constraint 字段中支持的参数包括:

input_dtype 定义融合后算子允许的输入数据类型列表。如果算子的实际数据类型不在白名单中,融合将被跳过并触发回退。

max_tile_size 定义融合后 Tile 允许的最大元素数量。这个参数用于在 Pattern 级别粗粒度地控制 UB 占用,避免过于激进的融合申请。

require_contiguous 是一个布尔标志,要求参与融合的张量在内存中是连续存储的。如果张量包含步幅(Stride)信息,系统会通过此标志判断是否需要先执行数据重排(Reshape/View)操作。

对于更复杂的 Pattern(如包含控制流分支或多输入多输出的融合场景),Autofuse 当前版本的能力边界由 autofuse README 明确定位:elementwise+element、elementwise+broadcast、elementwise+reduce 三类融合模式已有明确支持,concat、gather 等更复杂模式的融合支持在路线图中逐步开放。

6.3 用户自定义 Pattern 的注册与验证流程

用户添加自定义 Pattern 后,需要按照标准流程进行注册和验证,以确保新 Pattern 在实际网络中能够正常工作。

注册阶段:将新 Pattern 的 YAML 条目追加到 classify_rule.yaml 中后,重新编译 autofuse 组件。编译过程会解析 YAML 配置并生成对应的内部数据结构。可以通过 bash build.sh --pkg 命令触发增量编译,编译产物为 cann-graph-autofusion_${version}_linux-${arch}.run 安装包。

安装阶段:执行 ./build_out/cann-graph-autofusion_${version}_linux-${arch}.run --full --quiet --autofuse 安装新增的 Pattern。注意 --autofuse 标志是必需的——根据 build 文档,默认 run 包安装不包含 autofuse 组件。

验证阶段:使用 autofuse 提供的样例脚本进行功能验证。autofuse/examples 目录下包含了多个典型网络的测试用例。开发者可以选择与新 Pattern 相关的网络拓扑,运行并检查日志中的融合记录,确认新 Pattern 被成功识别和执行。如果日志中出现意外的 Fallback 记录,需要根据回退原因调整 Pattern 约束参数。

性能验证阶段:功能验证通过后,通过 Profiling 工具对比融合前后的 kernel 耗时。Autofuse 的 README 明确给出了融合提升比的计算公式:提升比 = (融合后所有算子耗时 - 融合前所有算子耗时) / 融合前所有算子耗时。同时可以关注融合算子的 aiv_mte2_time(输入数据搬运耗时)和 aiv_mte3_time(输出数据搬运耗时)指标,验证融合是否真正减少了数据搬移开销。

结尾

Graph-AutoFusion 的技术体系围绕两个核心组件——Autofuse 和 SuperKernel——构建了从算子粒度到网络粒度的完整融合加速能力。Autofuse 通过自动识别满足 elementwise+element、elementwise+broadcast、elementwise+reduce 融合条件的相邻算子对,基于成本-收益模型判定融合边界,调用 codegen 模块生成融合内核,并配合 Auto Tiling 机制处理 Tile 策略的动态调整,从而减少 Global Memory 数据搬移次数,缓解 Memory Bound 瓶颈。SuperKernel 则从调度层面入手,将整个网络重新编译为单一超算子,通过 ICache Preload、Early-Start 和同步优化等手段降低调度开销和指令缓存未命中率。


仓库地址:https://atomgit.com/cann/graph-autofusion

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐