Affine方言(Affine Dialect)是MLIR(Multi-Level Intermediate Representation)中专门为高效表达和优化计算密集型循环嵌套而设计的一种中间表示(IR)。它基于多面体模型(Polyhedral Model),通过仿射约束(Affine Constraints)精确描述循环迭代空间和数据访问模式,为编译器提供强大的分析和变换能力。
“affine”(仿射)最核心的思想:线性 + 平移
可以把“仿射”理解为一种**“线性变换”加上一个“平移”**的操作。它保持了“线性”结构的大部分良好性质(比如直线变换后还是直线,平行线变换后还是平行线),但不再要求必须通过原点。
拆解说明:
线性变换:
- 想象一个网格坐标纸。
- 线性变换包括:缩放(把坐标纸拉大或缩小)、旋转(转动坐标纸)、剪切(像推斜一叠卡片那样变形坐标纸)。
- 关键限制: 线性变换有一个重要特性——原点 (0,0) 变换后还是原点 (0,0)。所有变换都是围绕原点进行的。可以参考这里的熊猫图片变换。
仿射变换 = 线性变换 + 平移:
- 在进行了上述的缩放、旋转、剪切(线性部分)之后,再整体把整个坐标纸挪动一下位置。这个“挪动”就是平移。
- 关键突破: 仿射变换解除了原点固定的限制。变换后,原点可以跑到任何地方去了。
- 结果: 直线变换后还是直线(不会变弯),平行线变换后还是平行线(不会相交),但整个图形的位置可以自由移动了。
- 数学表示: 一个仿射函数通常写成
f(x) = A * x + b
的形式。A * x
: 这是线性变换部分(矩阵乘法,完成缩放、旋转、剪切)。+ b
: 这是平移部分(加上一个常数向量,完成整体移动)。
生活中的例子:
- 地图绘制: 把地球这个曲面画到一张平面的地图上(比如墨卡托投影),这个过程就是一个仿射变换的近似(虽然地球是曲面,但在小区域近似适用)。它包含了缩放(决定比例尺)、旋转(决定地图朝向)、剪切(某些投影会有)和平移(决定地图中心点在哪里)。
- 图像处理: 在Photoshop里平移、旋转、缩放一张图片,这些操作通常都是通过仿射变换来实现的。
🔍 一、基本定义与定位
核心目标
Affine方言专注于表示规则循环嵌套(如for
循环)及其关联的内存访问模式,适用于科学计算、图像处理、深度学习算子(如矩阵乘、卷积)等计算密集场景。- 例如,它将循环边界和数组索引表示为仿射函数(线性函数 + 常量),如
affine.for %i = 0 to 100 step 2
。
- 例如,它将循环边界和数组索引表示为仿射函数(线性函数 + 常量),如
关键限制
- 不支持非常量边界或条件跳转(如
while
循环、动态索引),确保其分析在编译时可判定。 - 不支持非结构化控制流(如
goto
),保持循环嵌套的数学可分析性。
- 不支持非常量边界或条件跳转(如
⚙️ 二、核心特性与操作
仿射循环结构
affine.for
:定义带仿射边界的循环,支持并行化标记(如parallel
)。affine.if
:基于仿射条件的分支,用于循环内的条件执行。
内存访问抽象
affine.load
/affine.store
:确保内存访问地址是仿射表达式(例如A[i+1][2*j]
),便于依赖分析和变换。
数据搬运与映射
affine.apply
:计算仿射表达式结果,用于地址计算或索引映射。affine.vector_load
/affine.vector_store
:支持SIMD向量化访问。
🛠️ 三、典型应用场景
计算图优化
在深度学习编译器中(如TensorFlow MLIR),高级算子(如linalg.matmul
)常被降低到Affine方言,以应用循环分块(tiling)、融合(fusion)等优化。affine.for %i = 0 to 128 { affine.for %j = 0 to 128 { affine.for %k = 0 to 128 { %val = affine.load %A[%i, %k] * affine.load %B[%k, %j] affine.store %val, %C[%i, %j] } } }
多面体模型集成
- 与外部工具(如Pluto)结合,实现自动并行化、数据局部性优化。
- 例如,将C/C++循环转换为Affine方言 → Pluto优化 → 生成优化后的Affine IR。
硬件加速器支持
在定制硬件(如FPGA、ASIC)中,Affine方言可精确描述数据搬运模式,指导硬件缓冲区设计。
为什么在编译器(MLIR的Affine方言)里这么重要?
Affine方言的核心就是利用仿射函数的数学特性来描述循环和内存访问:
- 可预测性与可分析性:
- 循环的边界(起始点、结束点、步长)被要求是仿射表达式(例如
lower = 0
,upper = N
,step = 2
或者upper = M + 10
)。这意味着边界可以依赖于常数、循环外的变量(如数组大小N、M),甚至是外层循环的索引(比如for j = i to N
),但必须是线性的组合加上常数偏移(a*i + b*j + c
)。 - 数组的索引(访问
A[i][j]
的位置)也必须是仿射表达式(例如A[2*i + 3][j - 1]
或B[k]
)。
- 循环的边界(起始点、结束点、步长)被要求是仿射表达式(例如
- 强大的优化基础:
- 因为边界和索引都是仿射形式,编译器可以利用多面体模型这种强大的数学工具进行精确的依赖关系分析(判断不同循环迭代之间读写数据是否有冲突)。
- 基于精确的分析,编译器就能安全地进行复杂的自动化优化:
- 循环变换: 改变循环顺序(
loop interchange
)、把大循环拆成小块(loop tiling
/blocking 提升缓存局部性)、把循环拆开(loop unrolling
)。 - 并行化: 判断哪些循环迭代之间没有数据依赖,可以安全地并行执行(在CPU多核或GPU上)。
- 向量化: 判断是否可以将循环体中对连续数据的操作,打包成SIMD指令一次处理多个数据。
- 循环变换: 改变循环顺序(
- 限制带来效率:
- 要求“仿射”是一个故意施加的限制。它排除了非常复杂的、不可预测的循环边界(比如
while (condition)
中condition
在运行时才确定)和非线性的索引(比如A[i*j]
或A[non_linear_function(i)]
)。 - 这个限制牺牲了一部分通用性(不能表示所有可能的程序),但换来了对它所表示的这部分规则循环进行极其高效和强大的自动化分析与优化的能力。对于科学计算、图像处理、深度学习算子(矩阵乘、卷积等)这类核心是规则嵌套循环的计算,Affine方言非常高效。
- 要求“仿射”是一个故意施加的限制。它排除了非常复杂的、不可预测的循环边界(比如
🔥 四、优化能力
循环变换
- 分块(Tiling):将大循环拆分为小块,提升缓存利用率。
- 循环交换(Interchange):调整循环顺序以优化数据局部性。
- 并行化(Parallelization):标记可并行循环,映射到多核/GPU。
依赖分析
通过仿射约束系统,静态判定循环间的数据依赖关系(如无冲突时可安全并行)。内存提升
将临时张量(Tensor)转换为缓冲区(MemRef),显式管理内存分配(如memref.alloc
)。
💎 总结
Affine方言是MLIR中连接高级算子与底层硬件的“数学桥梁”:
✅ 优势:提供严格数学保证的循环优化框架,适用于规则计算;
⚠️ 局限:无法处理动态控制流或非仿射访问。
在MLIR编译流程中,它常作为中间过渡层,承接来自Tensor/Linalg等高级方言的代码,并进一步降低到LLVM IR或硬件指令(如GPU/FPGA)。