AutoFuse Schedule 模块设计解析:循环合并、TilingCase 解空间生成、并行与内存优化
2026/9/18 16:39:15 网站建设 项目流程

AutoFuse Schedule 模块设计解析:循环合并、TilingCase 解空间生成、并行与内存优化

【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion

导读

Schedule 是 AutoFuse(CANN graph-autofusion 项目的自动算子融合组件)中连接计算定义与高效代码生成的核心模块:它接收用户通过 AscIR 定义的 HintGraph,在不改变计算语义的前提下,通过循环变换、Tiling 解空间生成、并行优化、内存优化与多模板生成等调度手段,输出表达分裂与内存关系的多份 ImplGraph,为 Codegen 与 Auto Tiling 生成高性能 Kernel 提供基础。本文以官方设计文档为主体,结合仓库源码(autofuse/optimize/autoschedule/等)展开,帮助读者掌握 Schedule 的调度原语、xgroup/ygroup/rgroup 分组机制、TilingCase 生成原理以及并行/内存优化手段。

Schedule 在 AutoFuse 编译流水线中的定位

AutoFuse 的编译流水线为:图优化(Optimize)→ 调度(Schedule)→ 代码生成(Codegen)→ 性能调优(ATT/Auto Tiling)。Schedule 是其中承上启下的关键一环,其职责定义见 AutoFuse 架构介绍:

  • 输入:用户通过 AscIR 定义的、描述 Scalar 计算逻辑的 HintGraph(如数据加载、逐元素运算、归约运算等);
  • 过程:基于硬件特性对计算实施调度优化,包括计算重排、循环合并、并行优化、内存优化与多模板生成;
  • 输出:生成多份 ImplGraph,表达分裂(Split)与内存(Mem)关系,供 Codegen 解析并生成 Host/Device 代码,供 Auto Tiling 在 Tiling 阶段求解具体的分裂尺寸与模板选择。

从源码结构看,调度相关的核心实现集中在autofuse/optimize/autoschedule/目录,包括autoschedule.cppschedule.cpptiling_group.cpptiling_group.h等文件;TilingCase 的候选生成与分组则在autofuse/optimize/task_generator/schedule_task_generator.cpp等任务生成模块中协同完成。Schedule 阶段只做"结构性决策"(哪些轴如何分裂、选哪种模板),而具体的分裂数值(如z0z1Tb_size)由 Auto Tiling 在 Tiling 阶段按实际 Shape 计算——这正是 Codegen 设计文档中"Codegen 只生成代码的形,不生成 Tiling 的值"这一理念的上游来源。

循环合并(Loop Merging)

原理与收益

循环合并是一种重要的循环变换技术。其核心作用在于:在不改变计算结果的前提下,通过重构循环结构减少内存访问次数、降低控制开销、改善数据局部性,并为后续优化铺路,最终提升程序执行效率

文档以两层循环为例:对C[i][j] = A[i][j] + B[i][j]的逐元素加法,原本需要两层嵌套循环:

for i in range(N): for j in range(M): C[i][j] = A[i][j] + B[i][j]

由于 Add 是逐元素运算,可以合并为单层线性循环:

for fused in range(N * M): i = fused // M j = fused % M C[i][j] = A[i][j] + B[i][j]

合并后的循环减少了循环嵌套层数与分支开销,同时让相邻迭代访问的内存地址连续,为向量化等后续优化创造有利条件。

AscIR 表达:合并前与合并后

合并前,Load/Add 算子各自声明两个调度轴z0z1,并通过repeats(重复次数)与strides(步长)描述张量访问模式:

z0 = graph.create_axis("z0", N) z1 = graph.create_axis("z1", M) Load load0("load_0"); load0.x = data0.y; load0.attr.sched.axis = {z0.id, z1.id}; load0.y.axis = {z0.id, z1.id}; load0.y.repeats = {N, M}; load0.y.strides = {M, 1}; Load load1("load_1"); load1.x = data1.y; load1.attr.sched.axis = {z0.id, z1.id}; load1.y.axis = {z0.id, z1.id}; load1.y.repeats = {N, M}; load1.y.strides = {M, 1}; Add add("add"); add.x1 = load0.y; add.x2 = load1.y; add.attr.sched.axis = {z0.id, z1.id}; add.y.axis = {z0.id, z1.id}; add.y.repeats = {N, M}; add.y.strides = {M, 1};

合并后,两个轴z0z1被合并为单一轴z0z1repeats变为{N * M}strides变为{1}(即连续线性访问):

Load load0("load_0"); load0.x = data0.y; load0.attr.sched.axis = {z0z1.id}; load0.y.axis = {z0z1.id}; load0.y.repeats = {N * M}; load0.y.strides = {1}; Load load1("load_1"); load1.x = data1.y; load1.attr.sched.axis = {z0z1.id}; load1.y.axis = {z0z1.id}; load1.y.repeats = {N * M}; load1.y.strides = {1}; Add add("add"); add.x1 = load0.y; add.x2 = load1.y; add.attr.sched.axis = {z0z1.id}; add.y.axis = {z0z1.id}; add.y.repeats = {N * M}; add.y.strides = {1};

注意:循环合并不是无条件的。仅当相邻轴在内存上连续、且合并后不破坏各算子的数据访问语义时才能执行。对于 reduce 类算子,归约轴与逐元素轴的合并需要额外约束,这正是下文 xgroup/ygroup/rgroup 分组机制要解决的问题。

生成 TilingCase(Tiling 解空间生成)

核心目标

在自动融合技术中,Tiling 解空间生成是实现高效计算调度的关键环节。其核心目标是为复杂计算任务提供多样化的 Tiling 策略选项,使后续优化器从中筛选出最优解。简单来说,解空间生成过程可理解为对输入数据或计算任务"分块可能性"的系统化枚举,每个解空间称为一个TilingCase

从 Auto Tiling 设计文档(features/auto_tiling.md)可以看到,ATT 在编译期枚举 ScheduleGroup/TilingCase 候选,并用硬件资源约束与 API 性能模型进行求解;在运行期,生成的 Tiling 函数依据实际输入选择 TilingData。因此,Schedule 阶段生成的 TilingCase 质量直接决定了最终 Tiling 方案的上限。

按 compute_type 抽象算子

分裂方法的设计与算子实现特征密切相关。为对多样算子实施系统化分裂策略枚举,首先依据实现特征将算子抽象为9 种 compute_type(compute 算子与 view 算子,如下图所示)。同一 compute_type 的算子具有相似的计算逻辑与数据访问模式,因此可以共享一套 Tiling 分裂策略框架。

轴归一化:xgroup / ygroup / rgroup

为了将策略框架落地,需要归一化并分组算子的轴,把所有轴统一划分到三个维度集合(xgroup、ygroup、rgroup)中,具体定义如下:

  • xgroup:专为 Concat 这类 view 算子设计的分组。以 Concat 为例,沿 Concat 轴进行切分,Concat 轴之前的轴划入 xgroup,Concat 轴及其后的轴划入 ygroup;
  • ygroup:对应 Elementwise、Broadcast 等类型算子的循环轴分组;
  • rgroup:Reduce 运算通常对归约轴有特殊分裂要求,因此所有归约轴单独放入 rgroup。

注意:引入 xgroup、ygroup、rgroup 的核心原因是为了支撑复杂场景下的"双重分裂"需求。例如在包含混合 reduce 的计算图中,ygroup 控制逐元素方向的循环分裂,rgroup 中的轴控制归约方向的循环分裂,二者互不干扰。

分组合并(Merge)与两个核心能力

完成单个算子的轴分组后,需要通过预设的合并规则(Merge)将计算图中所有算子的分组策略合并,合并结果作为适用于整张图的统一分裂策略,为后续解空间生成提供基础。这一分组与合并机制带来两个核心能力:

  • 筛选出适用于计算图中所有节点的分裂方式,形成有效的 TilingCase;
  • 通过判断不同 AscGraph 的 tiling 分组能否成功合并,验证两张图的可融合性——这与autofuse/optimize/autoschedule/tiling_group.cpptiling_group.h中 TilingGroup 的实现直接对应。

示例:Load → Abs → Max 的 TilingCase 生成

文档以 Load、Abs、Max 三算子链为例说明 TilingGroup 合并生成 TilingCase 的原理。AscIR 声明如下(z0轴大小为s0z1轴大小为s1):

z0 = graph.create_axis("z0", s0) z1 = graph.create_axis("z1", s1) data = ascir.ops.Data('data', graph) data.y.dtype = ascir.dtypes.float32 # Declare the Load operator load = ascir.ops.Load('load') load.attr.sched.axis = [z0, z1] # Scheduling axis load.x = data.y load.y.axis = [z0, z1] # Tensor output axis load.y.size = [s0, s1] # Tensor output size load.y.strides = [s1, 1] # Tensor output stride # Declare the Abs operator abs = ascir.ops.Abs('abs') abs.attr.sched.axis = [z0, z1] abs.x = load.y abs.y.axis = [z0, z1] abs.y.size = [s0, s1] abs.y.strides = [s1, 1] # Declare the Max operator max = ascir.ops.Max('max') max.attr.sched.axis = [z0, z1] max.x = abs.y max.y.axis = [z0, z1] max.y.size = [s0, 1] # Reduce operation on z1 axis max.y.strides = [1, 0]

其中max.y.size = [s0, 1]max.y.strides = [1, 0]表明 Max 在z1轴上做归约(repeats 为 1、stride 为 0)。

elewise 分裂:Abs 是 elewise 算子,各轴在计算上没有差别,因此只要内存连续,多个轴可以合并为一个轴再分裂。如下图所示,先对轴分块——以 15 为例分成 3 块,block0 为紫色部分,再在 block0 内做 tiling 分块。此时 tiling 块并未填满 block0 分配的区域,因此块内还需要一层额外的 for 循环:

reduce 分裂:Reduce 的分裂更复杂,实现上需要双重分裂:行方向是 elewise 轴,列方向是 reduce 轴;先在行方向分块,在块内写循环,再在列方向加一层 for 循环:

TilingGroup 合并规则:Abs 的(z0, z1)轴原本属于 ygroup,Max 的归约轴z1属于 rgroup。通过合并规则:

()(z0,z1)() Merge ()(z0)(z1) => ()(z0)(z1)

(z0, z1)被拆分,其中z1轴被调整到 rgroup。这样做的核心目的是让 Abs 与后续 reduce 型算子保持统一的分裂策略——Abs 需要在 ygroup 方向与 Max 的逐元素循环对齐,同时在 rgroup 方向跟随 Max 的归约分裂,从而形成整图一致、可直接生成 TilingCase 的调度方案。

并行优化

循环分裂(Loop Splitting)

循环分裂的核心作用是:通过引入新的循环层次,明确适合并行的外层循环适合向量化的内层循环之间的划分。对于每个 TilingCase,xgroup、ygroup、rgroup 中存在的轴都会被分裂为 ub_out 与 ub_in 两部分:

例如轴集合{z0, z1, z2}z1上分裂,z1被拆成z1T(ub_out,UB 外层)与z1t(ub_in,UB 内层)。这一分裂树(ORIGINAL → TILE_OUT/TILE_IN → BLOCK_OUT/BLOCK_IN)会完整写入 ImplGraph,成为 Codegen 生成循环结构的依据——如 features/codegen.md 所述,Codegen 只处理三类轴:BLOCK_OUT(核间并行、不生成 for 循环)、BLOCK_IN(生成核内偏移)、其余轴(生成 for 循环)。

向量化(Vectorization)

向量化是利用硬件 SIMD(单指令多数据流)单元提升数据并行计算效率的关键技术:将单元素运算转换为向量运算,显著减少指令执行条数、提高硬件利用率。例如对以下循环:

for (int i = 0; i < 256; i++) { c[i] = a[i] + b[i]; }

非向量化执行需要 256 条加法指令,而向量化执行只需 1 条加法指令。

在每个分组中选定一个轴作为 UB 分裂轴后,ub_in 及其内部轴被用作向量化轴。由于分组是按 xyr 顺序生成的,按该顺序产生的向量化轴与内存布局的轴顺序不一致,会引入非连续数据搬运,因此需要按输出轴顺序重排:

例如输出张量轴顺序为(a, b, c, d),轴分组为(a, c),(b, d)(),按分组顺序生成的向量化轴为(a_in, c, b_in, d),需要调整为(a_in, b_in, c, d)以匹配内存布局。

注意:Schedule 阶段为整张图设置相同的向量化轴。对某些 API,由于指令限制,并非所有向量化轴都能真正向量化,此时 CodeGen 阶段需要把无法向量化的轴"抛出"并用 for 循环处理。例如向量化轴为[z1, z2, z3],等价于 3 层循环;若指令只支持 2 层循环,CodeGen 需要生成如下代码:

for (i in z1) { vector[z2, z3] }

循环合并与 Loop BindCore

循环合并与 loop bindcore 通常配合使用。循环分裂阶段在每个分组中产生 ub_out 轴后:

  1. 合并循环:将所有非归约轴合并为一个轴,所有归约轴合并为一个轴,以减少循环嵌套层数;
  2. 分裂合并后的循环:得到外层与内层;
  3. 绑定多核:将分裂后的外层循环绑定到多个 block 上实现并行,内层通过循环消化。

示例:对{z0, z1T, z1t, z2}z0z1T先合并为z0z1T,再对z0z1T做多核分裂。由于z0z1T可能超过参与计算的逻辑 AI Core 核数,核分裂后还需要额外一层循环,z0z1T会被进一步拆分为z0z1TB(核间外层)与z0z1Tb(核间内层)。这些轴的具体数值由 Auto Tiling 在 Tiling 阶段计算(对应 TilingData 中的z0z1Tb_size等字段),Schedule 阶段只确定分裂结构。

内存优化

当前内存复用主要基于节点引用关系实现。为提升复用效果,尺寸相近的内存尽量分配到同一分组,再在组内进行复用。

内存复用的伪代码如下:

for (node in all nodes) { for (output in node.outputs) { # Mark the number of dependencies of the tensor output->sch.depends = output->anchor->GetPeerInDataNodesSize(); # try reuse from free queue } for (input in node.inputs) { input->sch.depends--; if (input->sch.depends == 0) { Enqueue(input->opt.reuse_id); # Mark as freeTensor, can be reused by subsequent nodes } } }

核心思想是:每个张量记录其依赖计数(被多少下游节点使用);当一个节点的输入张量依赖计数降为 0(不再被后续节点使用)时,将其标记为 freeTensor 并放入空闲队列,后续节点的输出可以申请复用这块内存。这一机制与仓库中autofuse/optimize/buffer_allocate/目录下的内存分配与复用实现(如buf_que_allocator.cppmem_reuse_manager.cpp)相呼应——Schedule 阶段确定复用关系,Buffer 分配阶段落实具体地址规划。

此外,对某些 API,输出可以直接复用输入。这类 API 可采用Inplace 复用,即输出直接复用输入内存。如下图所示,Inplace 复用前需要 3 块内存:

Inplace 复用后仅需 2 块内存:

在 UB(统一缓冲区)资源受限的昇腾芯片上,内存复用直接决定单次 Tiling 能承载的数据量,是影响向量化利用率和整体性能的关键优化。

多模板生成(Multi-template Generation)

对一个计算图,可能存在多种实现方式。以尾部轴(tail-axis)concat 为例:

  • 方式一(UB concat):先在 UB 上通过 ub_concat 把多个小包合并成一个大包,再整体搬出;
  • 方式二(Graph rewrite):直接在 GM(全局内存)上用非连续数据搬运完成重排。

UB concat 模板

Graph rewrite 模板

前者在小 Shape 场景下能显著提升 MTE(Memory Transfer Engine,AI Core 的数据搬运引擎)效率,从而获得更好的性能;但 ub_concat 要求内轴被完整加载,这使其在部分场景下不可用。当 Schedule 阶段无法确定选择哪个模板时,通常同时生成一个适用于任意 Shape 的通用模板和一个面向特定场景的性能优化模板,由 Auto Tiling 模块在 Tiling 阶段根据实际 Shape 决定使用哪个模板。

多模板机制与 Codegen 的 tiling_key 分发机制直接衔接:Schedule 为不同分裂策略生成多份 ImplGraph 模板,Codegen 逐份解析生成模板函数,并在 Kernel 入口按tiling_key分发(如if (t.tiling_key == 0) { ... } else if (t.tiling_key == 1) { ... });tiling_key的具体取值由 Auto Tiling 在 Tiling 阶段计算并写入AutofuseTilingData。这样既保证了代码对任意 Shape 的鲁棒性,又能在特定场景下拿到优化模板的性能收益。

总结:Schedule 的输出如何驱动下游

综上,Schedule 模块完成了从"计算定义"到"调度策略"的转换,其核心产出可归纳为:

优化手段产出/效果下游消费方
循环合并减少循环嵌套、提升访存局部性Codegen 循环结构生成
TilingCase 解空间生成基于 xyr 分组与 Merge 生成多样 TilingCaseAuto Tiling 候选枚举与求解
循环分裂 + 向量化明确 ub_out/ub_in 与向量化轴Codegen 生成 for 循环与 vector 调用
循环合并 + BindCore核间并行划分(BLOCK_OUT/BLOCK_IN)Codegen 生成多核调度代码
内存优化依赖计数复用与 Inplace 复用Buffer 分配与 UB 容量规划
多模板生成通用模板 + 性能优化模板Codegen tiling_key 分发、Auto Tiling 运行时选择

Schedule 生成的 ImplGraph 是连接 Schedule 与 Codegen 的唯一接口:轴分裂树(ORIGINAL → TILE/BLOCK OUT/IN)、各节点的 repeats/strides/vectorized_strides、UB 队列与内存信息(mem/que)等全部固化在 ImplGraph 上;Auto Tiling 则在 Tiling 阶段依据实际 Shape 求解各分裂轴的具体尺寸并选择模板。三者协同,最终在昇腾芯片上产出适配任意 Shape 且兼顾特定场景性能的高质量融合 Kernel。

延伸阅读

  • AutoFuse Architecture Introduction:Schedule 在 AutoFuse 流水线中的整体定位与模块职责
  • Codegen 特性设计:ImplGraph 轴分裂树约定、tiling_key 多模板分发与生成骨架
  • Auto Tiling 特性设计:TilingCase 候选求解、性能模型与模板选择机制
  • 调度与分组源码:autofuse/optimize/autoschedule/autoschedule.cpp、autofuse/optimize/autoschedule/schedule.cpp、autofuse/optimize/autoschedule/tiling_group.cpp、autofuse/optimize/autoschedule/tiling_group.h
  • 内存分配实现:autofuse/optimize/buffer_allocate/buf_que_allocator.cpp、autofuse/optimize/buffer_allocate/mem_reuse_manager.cpp

【免费下载链接】graph-autofusionGraph-autofusion 是一个面向昇腾(Ascend)芯片的轻量级、解耦式组件集合,旨在通过自动融合技术加速模型执行。 目前已开源 SuperKernel 组件和 Autofuse 组件,未来将持续开放更多自动融合相关模块。项目地址: https://gitcode.com/cann/graph-autofusion

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询