Constant Propagation vs Canonicalization
-sccp
Sparse Conditional Constant Propagation 是稀疏条件常数传播,它试图推断 op 何时具有常量输出,然后用常量值替换 op 。重复这个过程,它在程序中尽可能地“传播”这些常量。
例如对于如下的函数
|
|
-sccp
优化后的结果如下:
|
|
需要注意的是:sccp 不会删除死代码;这里没有展示的是 sccp 的主要作用,它可以通过控制流 (if 或者 loop) 传播常量。
一个相关的概念是 canonicalization,--canonicalize
pass 隐藏了 MLIR 中的许多繁重工作。它与 sccp 有一点重叠,因为它也计算常量并在 IR 中具体化它们。例如,在上面的 IR 上使用 ——canonicalize
pass 的结果如下
|
|
中间的常量都被修剪掉了,剩下的只是返回值,没有任何 op. 规范化不能通过控制流传播常量。
这两者都是通过折叠 (folding) 来支持的,折叠是采取一系列 op 并将它们合并在一起为更简单的 op 的过程。它还要求我们的方言具有某种常量 op ,该 op 与折叠的结果一起插入。
以这种方式支持折叠所需的大致步骤是:
- 添加一个常量 op.
- 添加实例化钩子。
- 为每个 op 添加 folders.
Making a Constant Operation
我们目前只支持通过 from_tensor
op 从 arith.constant
创建常量。
|
|
一个常量 op 可以将上述两个操作简化成一个 op. from_tensor
op 还可以用于根据数据 (而不仅仅是常数) 构建一个多项函数,因此即使在我们实现了 poly.constant
之后,它也应该保留。
|
|
fold 可以用于向 sccp 等 pass 传递信号,表明 op 的结果是常量,或者它可以用于说 op 的结果等效于由不同 op 创建的预先存在的值。对于常量的情况,还需要一个 materializeConstant
钩子来告诉 MLIR 如何获取常量结果并将其转化为适当的 IR op. 常量 op 的定义如下
def Poly_ConstantOp: Op<Poly_Dialect, "constant", [Pure, ConstantLike]> {
let summary = "Define a constant polynomial via an attribute.";
let arguments = (ins AnyIntElementsAttr:$coefficients);
let results = (outs Polynomial:$output);
let assemblyFormat = "$coefficients attr-dict `:` type($output)";
}
ConstantLike
trait 标记的 op 被视为常量值生成 op ,可以在编译时进行常量折叠等优化。arguments
定义 op 的输入是一个具有 AnyIntElementsAttr
的值,使得 op 可以处理任意包含整数的集合,而不仅仅是特定位宽的整数。
Adding Folders
我们为定义的 op 都加上 let hasFolder = 1;
它在 .hpp.inc 中添加了如下形式的声明。FoldAdaptor
定义为 GenericAdaptor
类型的别名,而 GenericAdaptor
包含了一个 Attribute
数组的引用,这个数组提供了对 op 属性的访问接口。
Attribute 类的核心作用是:
- 表示常量值:Attribute 用于表示操作的静态、不可变的常量值,例如整数、浮点数、字符串、类型信息等。这些值在编译期已知且不可更改。
- 支持编译器优化:通过提供常量值的表示,Attribute 支持 MLIR 的优化流程,如折叠 (folding) 、规范化 (canonicalization), 常量传播 (constant propagation) 等。
- 跨方言的通用接口:Attribute 是一个抽象接口,允许不同方言 (dialects) 定义自己的常量表示,同时通过统一的 API 进行操作。
- 轻量级和高效:Attribute 是一个值类型 (passed by value) ,内部仅存储指向底层存储的指针,依赖 MLIRContext 的唯一化机制 (uniquing) 确保内存效率和一致性。
|
|
我们需要在 PolyOps.cpp
中实现这个函数。如果 fold
方法决定 op 应被替换为一个常量,则必须返回一个表示该常量的 Attribute
,该属性可以作为 poly.constant
操作的输入。FoldAdaptor
是一个适配器,它具有与操作的 C++ 类实例相同的方法名称,但对于那些已经被折叠的参数,会用表示其折叠结果常量的 Attribute
实例替换。这在折叠加法和乘法操作时尤为重要,因为折叠的实现需要立即计算结果,并且需要访问实际的数值来完成计算。
对于 poly.constant
我们只需要返回输入的 attribute.
|
|
对于 from_tensor 我们需要有一个额外的强制转换作为断言,因为张量可能是用我们不希望作为输入的奇怪类型构造的。如果 dyn_cast
结果是 nullptr
, MLIR 将其强制转换为失败的 OpFoldResult
.
|
|
BinOp 稍微复杂一些,因为这些 fold 方法中的每一个 op 都接受两个 DenseIntElementsAttr
作为输入,并期望我们为结果返回另一个 DenseIntElementsAttr
.
对于 elementwise op 的 add/sub,我们可以使用现有的方法 constFoldBinaryOp
,它通过一些模板元编程技巧,允许我们只指定元素 op 本身。
|
|
对于 mul,我们手动的通过循环计算每个系数。getResult()
方法来自于 OneTypedResult
类模板及其内部类 Impl
是一个 MLIR Trait,它主要用于那些返回单一特定类型结果的 op 。
|
|
Adding a Constant Materializer
最后我们添加常量实例化函数,这是一个 dialect 级别的特性,我们在 PolyDialect.td
中添加 let hasConstantMaterializer = 1;
则会在 .hpp.inc 中添加如下形式的声明。
|
|
该函数作用是将给定 Attribute (上面每个折叠步骤的结果) 的单个常量 op 实例化为所需的结果 Type.
|
|