MLIR多层中间表示——用MLIR构建编译器(中)
1.3. 玩具语言IR方言
玩具语言方言:方言
在TableGen中声明性指定
def Toy_Dialect : Dialect {
let summary = Toy IR Dialect;
let description = [{
这是对玩具语言方言的一个更长的描述
...
}];
// 方言的命名空间.
let name = toy;
// 方言类定义所在的C++命名空间
let cppNamespace = toy;
}
从定义方言开始,然后将考虑如何处理运算等。
MLIR的许多方面都是以声明的方式指定的,以减少样板,并更容易扩展。例如,方言的详细文档是与可用的内置标记生成器一起指定的。对于那些不熟悉tablegen的人表示歉意,tablegen是这里声明中使用的语言。这是一种特定于LLVM的语言,在许多情况下用于帮助以声明的方式生成C++代码。
玩具语言方言:方言
从定义方言开始,然后将考虑如何处理运算等。
MLIR的许多方面都是以声明的方式指定的,以减少样板,并更容易扩展。例如,方言的详细文档是与可用的内置标记生成器一起指定的。对于那些不熟悉tablegen的人表示歉意,tablegen是这里声明中使用的语言。这是一种特定于LLVM的语言,在许多情况下用于帮助以声明的方式生成C++代码。
玩具语言方言:运算
#对未知形状参数进行运算的用户定义的泛型函数
def multiply_transpose(a, b) {
return transpose(a) * transpose(b);
}
def main() {
var a<2, 2> = [[1, 2], [3, 4]];
var b<2, 2> = [1, 2, 3, 4];
var c = multiply_transpose(a, b);
print(c);
}
现在,需要决定如何将Toy语言映射到一个高级中间形式,该形式适合想要执行的分析和转换类型。MLIR提供了很大的灵活性,但在定义抽象时仍应小心,使其有用但不笨拙。
玩具语言方言:运算
$ bin/toy-ch5 -emit=mlir example.toy
#对未知形状参数进行运算的用户定义的泛型函数
def multiply_transpose(a, b) {
return transpose(a) * transpose(b);
}
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
让首先来看通用的multiply_transpose函数。这里有一个容易提取的运算:转置、乘法和返回。对于类,将使用内置张量来表示多维数组。它支持需要的所有功能,所以可以直接使用它。*表示一个未排序的张量,在这里不知道维度是多少或有多少。f64是元素类型,在这种情况下是64位浮点或双类型。(请注意,调试位置在这个片段中被省略了,因为否则很难在一张幻灯片中显示。)
$ bin/toy-ch5 -emit=mlir example.toy
玩具语言方言:运算
def main() {
var a<2, 2> = [[1, 2], [3, 4]];
var b<2, 2> = [1, 2, 3, 4];
var c = multiply_transpose(a, b);
print(c);
}
func @main() {
%0 = toy.constant() { value: dense<[[1., 2.], [3., 4.]]> : tensor<2x2xf64> }
: () -> tensor<2x2xf64>
%1 = toy.reshape(%0) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%2 = toy.constant() { value: dense<tensor<4xf64>, [1., 2., 3., 4.]> }
: () -> tensor<4xf64>
%3 = toy.reshape(%2) : (tensor<4xf64>) -> tensor<2x2xf64>
%4 = toy.generic_call(%1, %3) {callee: @multiply_transpose}
: (tensor<2x2xf64>, tensor<2x2xf64>) -> tensor<*xf64>
toy.print(%4) : (tensor<*xf64>) -> ()
toy.return() : () -> ()
}
接下来是main函数。此函数创建一些常量,调用通用的multiply_transpose并打印结果。当查看如何将其映射到中间形式时,可以看到常量数据的形状被重新整形为变量上指定的形状。还可能注意到,常量的数据是通过内置的密集元素属性存储的。该属性有效地支持浮点元素的密集存储,这正是所需要的。
玩具语言方言:不变的运算
玩具语言方言:不变的运算
玩具语言方言:不变的运算
玩具语言方言
注册后,运算现在得到充分验证,
$ cat test/Examples/Toy/Ch3/invalid.mlir
func @main() {
toy.print() : () -> ()
}
$ build/bin/toyc-ch3 test/Examples/Toy/Ch3/invalid.mlir -emit=mlir
loc(test/invalid.mlir:2:8): 错误:toy.print运算需要一个运算数
1.4. 玩具语言高水平转化
特点
● 定义属性/运算/类型的附加功能、属性和验证的混合
● 通过分析/转换不透明地检查存在
● 示例(用于运算):
○ 可交换的
○ Terminator:如果运算终止了一个块
○ 零运算数/单运算数/无运算数
特征本质上是混合元素,为它们所附加的实体提供一些额外的属性和功能,无论是属性、运算还是类型。特征的存在也可以通过不透明的方式进行检查。因此,如果存在简单的二进制属性,那么特征是一种有用的建模机制。一些例子包括数学性质,如交换性质,以及结构性质,如运算是否为终止符。甚至使用特征来描述运算的最基本属性,例如运算数。这些特性为运算类上的运算数提供了有用的访问器。
接口
● 抽象类不透明地运算MLIR实体
○ 具有由属性/方言/运算/类型提供的实现的方法组
○ 不要依赖C++继承,类似于C中的接口#
● MLIR可扩展性和Pass可重用性的基石
○ 最初经常定义接口以满足转换的需要
○ 方言实现接口以启用和重用通用转换
● 示例(用于运算):
○ 调用运算/调用运算(调用图建模)
○ LoopLike
○ 副作用
特性在将新属性附加到实体时很有用,但在不透明地检查附加到实体的属性或对其进行转换时不会提供太多。因此定义了接口的用途。这些类本质上是抽象类,不依赖于C++继承。它们允许不透明地调用类型擦除上下文中实体定义的方法。考虑到类(如MLIR中的运算)的类似视图的性质,不能依赖于现有对象的实例。因此,MLIR中的接口在范围上与C#中的接口有些相似。接口如何用于运算的几个例子是:对调用图、循环和运算的副作用进行建模。
示例问题:形状推理
● 确保所有动态玩具语言阵列变为静态形状
○ CodeGen/优化变得更容易了
○ 教程友好
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
那么,让来看看在玩具语言中面临的一个示例问题。形状推断。main之外的所有玩具语言数组目前都是动态的,因为函数是通用的。希望有静态形状,使代码生成/优化变得更容易,并且本教程更省时。那么该怎么办呢?
示例问题:形状推理
● 确保所有动态玩具语言阵列变为静态形状
○ CodeGen/优化变得更容易了
○ 教程友好
● 程序间形状传播分析?
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
可以写一个过程间形状传播分析。
示例问题:形状推理
● 确保所有动态玩具语言阵列变为静态形状
○ CodeGen/优化变得更容易了
○ 教程友好
● 程序间形状传播分析?
● 功能专业化?
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
还可以为每个调用站点生成每个泛型函数的专用化。
示例问题:形状推理
● 确保所有动态玩具语言阵列变为静态形状
○ CodeGen/优化变得更容易了
○ 教程友好
● 程序间形状传播分析?
● 功能专业化?
● 内联所有内容!
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
让让它对来说很容易,只需内联所有内容,因为这总是最好的策略。
示例问题:嵌入所有内容
https://mlir.llvm.org/docs/Tutorials/Toy/Ch-4/#inlining
MLIR提供了一个定义接口的内联过程,Toy方言只需要实现内联接口:
● 定义内联Toy运算的合法性
● 向调用图显示toy.generic_call
MLIR提供了方言可以立即使用的通用内联过程。对于Toy,需要提供正确的接口,以便:generic_call被识别为调用图的一部分,Toy运算对于内联是合法的。
示例问题:嵌入所有内容
这个类定义了用于处理Toy运算内联的接口。简化了从基本接口类的继承,并覆盖了必要的方法。
struct ToyInlinerInterface : public DialectInlinerInterface {
using DialectInlinerInterface::DialectInlinerInterface;
bool isLegalToInline(Operation *, Region *,
BlockAndValueMapping &) const final {
return true;
}
void handleTerminator(
Operation *op, ArrayRef<Value> valuesToRepl) const final {
// Only toy.return needs to be handled here.
ReturnOp returnOp = cast<ReturnOp>(op);
for (auto it : llvm::enumerate(returnOp.getOperands()))
valuesToRepl[it.index()].replaceAllUsesWith(it.value());
}
Operation *materializeCallConversion(
OpBuilder &builder, Value input, Type resultType,
Location conversionLoc) const final {
return builder.create<CastOp>(conversionLoc,
resultType, input);
}
};
https://mlir.llvm.org/docs/Tutorials/Toy/Ch-4/#inlining
示例问题:嵌入所有内容
示例问题:嵌入所有内容
示例问题:嵌入所有内容
示例:嵌入所有内容
func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
-> tensor<*xf64> {
%0 = toy.transpose(%arg0) : (tensor<*xf64>) -> tensor<*xf64>
%1 = toy.transpose(%arg1) : (tensor<*xf64>) -> tensor<*xf64>
%2 = toy.mul(%0, %1) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.return(%2) : (tensor<*xf64>) -> ()
}
func @main() {
%0 = toy.constant() { value: dense<[[1., 2.], [3., 4.]]> : tensor<2x2xf64> }
: () -> tensor<2x2xf64>
%1 = toy.reshape(%0) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%2 = toy.constant() { value: dense<tensor<4xf64>, [1., 2., 3., 4.]> }
: () -> tensor<4xf64>
%3 = toy.reshape(%2) : (tensor<4xf64>) -> tensor<2x2xf64>
%4 = toy.generic_call(%1, %3) {callee: @multiply_transpose}
: (tensor<2x2xf64>, tensor<2x2xf64>) -> tensor<*xf64>
toy.print(%4) : (tensor<*xf64>) -> ()
toy.return() : () -> ()
}
示例:嵌入所有内容
func @main() {
%0 = toy.constant() { value: dense<[[1., 2.], [3., 4.]]> : tensor<2x2xf64> }
: () -> tensor<2x2xf64>
%1 = toy.reshape(%0) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%2 = toy.constant() { value: dense<tensor<4xf64>, [1., 2., 3., 4.]> }
: () -> tensor<4xf64>
%3 = toy.reshape(%2) : (tensor<4xf64>) -> tensor<2x2xf64>
%4 = toy.cast(%3) : (tensor<2x2xf64>) -> tensor<*xf64>
%5 = toy.cast(%1) : (tensor<2x2xf64>) -> tensor<*xf64>
%6 = toy.transpose(%4) : (tensor<*xf64>) -> tensor<*xf64>
%7 = toy.transpose(%5) : (tensor<*xf64>) -> tensor<*xf64>
%8 = toy.mul(%6, %7) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
toy.print(%8) : (tensor<*xf64>) -> ()
toy.return() : () -> ()
}
示例:程序内形状推断
1.构建一个工作列表,其中包含返回动态形状张量的所有运算
2.在工作列表上迭代:
○ 查找要处理的运算:工作列表中的下一个就绪运算的所有参数都是非泛型的
○ 如果没有找到运算,则中断循环
○ 从工作列表中删除该运算
○ 从参数类型推断其输出的形状
=>使用接口使Pass独立于方言并可重复使用。
3.如果工作列表为空,则算法成功
示例:形状推理界面
● 运算界面
○ 描述
def ShapeInferenceOpInterface : OpInterface<ShapeInference> {
let description = [{
Interface to access a registered method to infer the
return types for an operation that can be used during
type inference.
}];
}
示例:形状推理界面
示例:形状推理界面
示例:形状推理过程
func @main() {
%0 = toy.constant() { value: dense<[[1., 2.], [3., 4.]]> : tensor<2x2xf64> }
: () -> tensor<2x2xf64>
%1 = toy.reshape(%0) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%2 = toy.constant() { value: dense<tensor<4xf64>, [1., 2., 3., 4.]> }
: () -> tensor<4xf64>
%3 = toy.reshape(%2) : (tensor<4xf64>) -> tensor<2x2xf64>
%4 = toy.transpose(%3) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%5 = toy.transpose(%1) : (tensor<2x2xf64>) -> tensor<2x2xf64>
%6 = toy.mul(%4, %5) : (tensor<2x2xf64>, tensor<2x2xf64>) -> tensor<2x2xf64>
toy.print(%6) : (tensor<2x2xf64>) -> ()
toy.return() : () -> ()
}
参考文献链接
https://llvm.org/devmtg/2020-09/slides/MLIR_Tutorial.pdf