MLIR-Running and Testing a Lowering
Dialects and Lowerings
MLIR 中两个重要的概念是 dialects 和 lowerings. dialects 是是编译器代码中程序的文本或数据结构描述,lowerings 是用来将特定 dialect 的 IR 转换成另一种 dialect 的 IR 的过程。
在 MLIR 的工作流程
- 定义高级或者高级 dialects. 它们都是由由一系列的类型,操作,元数据和语义组成。
- 编写一组 lowering passes,将程序的不同部分从高级 dialect 逐渐转换为越来越低级的 dialect,直到变成机器码。在此过程中,将运行 optimizing passes 使代码更高效。
高级 dialect 的存在使得可以轻松编写 optimizing passes. lowering passes 和 optimizing passes 之间没有特别的区别,它们在 MLIR中 都被称为 passes,并且是通用的 IR 重写模块。
Two Example Programs
下面的一段代码用了 math dialect 中定义的 counting leading zeros (ctlz) 操作。计算输入的前导零的数量并直接返回。
1 |
|
变量名前缀为 %
表示它是一个 SSA 值。函数前缀为 @
. 程序中每个变量都有对应的类型 (i32
),而 func
的类型是 i32 -> i32
.
每个声明都围绕着 math.ctlz
等表达式锚定,该表达方式指定 dialect math
和 ctlz 操作。操作的其余语法由 dialect 定义的 parser 确定,因此许多操作将具有不同的语法。末尾的:i32 表示输出类型。
func
自己也是一个 dialect,func.func
被看作一个操作,括号和函数体是他 syntax 的一部分。在 MLIR 中,被括号包裹的一系列操作称作一个 region. 一个操作可以没有或者有很多 regions.
简单来说 operations 可能会被包括在 regions 中 (例如 for 循环的 body). 每个 region 包含一系列的 blocks (显式或者隐式). 一个 block 由一系列操作组成,并且只有一个入口和一个出口。
在 MLIR 中,多个 dialects 经常共存在同一个程序中,因为它会逐渐 lowering 到某个最终的后端设备。
下面的代码包含了一个 ctlz 函数的实现并且在主函数中调用它。
1 |
|
scf
dialect 定义了 structured control flow 相关的操作: scf.if
是一个条件分支,scf.for
是一个循环。scf.yield
表示该控制流返回值。 %2:2
表示返回的变量是一个包含两个值的 tuple. %4#0, %4#1
分别表示返回变量的第一个和第二个值。index
类型专门用于表示索引、内存地址和循环计数,是与平台无关的类型,通常与平台指针大小一致。
scf.for
控制流中 %arg1
是循环变量,%c1 to %c32
表示循环将从 %c1
直迭代到 %c32
(不包括). step %c1
表示循环变量每次迭代增加值。 iter_args
示迭代参数,在每次迭代中,%arg2
和 %arg3
会根据 scf.yield
的值更新,并在下一次迭代时继续传递。
Lowerings and the math-to-funcs Pass
上一节提供了 2 个版本的 ctlz 程序。一般大部分机器支持计算前导 0 指令,这样就可以直接将 match.ctlz
转换成对应的指令。否则就要采用更低级的操作,正如第二个版本所做的一样。
mlir-opt 工具将会解析 .mlir 文件并进行优化,使用方式是 mlir-opt path/to/xxx.mlir
. 我们对第一个版本直接使用该命令生成的优化后的代码如下。
1 |
|
我们也可以用 mlir 自带的 convert-math-to-funcs
pass 进行 lowering. 生成的正好是我们第二个版本。CL 命令为 mlir-opt --convert-math-to-funcs=convert-ctlz ./test/ctlz.mlir
Lit and FileCheck
Lit 全称为 LLVM Integrated Tester. 它通常用来运行 .mlir 文件的测试,来验证某些 Pass 的输出是否符合预期。Lit 测试通常会结合 FileCheck 工具来检查命令的输出。
FileCheck 是 LLVM 项目中的一个文本匹配工具。它通过特殊的注释标记 (CHECK, CHECK-NEXT, CHECK-LABEL .etc) 来指定预期的输出,来验证程序的行为是否符合预期。这些标记在 .mlir 文件中作为注释,用于表示某些期望的输出。
以第一个版本的 lit 测试文件举例。一个lit测试文件包含一些行,RUN:
作为注释的开头,后面的文本描述了要运行的shell 脚本,并用一些字符串指示 lit 进行替换。在本例中,%s
指当前文件路径。FILECHECK 接受传递给 stdin 的输入,扫描作为 CLI 参数传递的文件中的 CHECK 注释,它执行一些逻辑来确定断言是否通过。
1 |
|
FileCheck 可以做更多的事情,比如使用正则表达式捕获变量名,然后在以后的 CHECK 断言中引用它们。
lit.py
通常位于 /path/to/llvl-project/llvm/utils/lit
目录下。我们需要在要测试的文件目录下配置一个 lit.cfg
. 然后可以运行命令 python /path/to/llvm-project/llvm/utils/lit/lit.py /test/path
1 |
|
Functinal Testing
在 MLIR 中,lowering 过程本身是 syntactic. CHECK 不能检查生成的代码是否有错误,或者保证生成正确的结果。我们可以用 Lit 进行 functinal testing. 我们可以编写一些 lit 测试来验证程序的行为是否符合预期。Lit 测试可以运行在不同的平台上,并且可以指定不同的参数。
解决这个问题的一种方法是继续通过 LLVM 将 MLIR 代码向下编译为机器码并运行它,断言有关输出的某些内容。虽然 RUN
可以运行任何东西,但需要引入更多的依赖项。实现这一目标的一个稍微轻量级的方法是使用 mlir-cpu-runner
,它是一些最低级别的 MLIR dialect (特别是 llvm dialect) 的解释器。
在 MLIR 到 LLVM IR 的编译过程中,最终会将 MLIR 中的 llvm dialect 直接映射或转换为标准的 LLVM IR 之后,LLVM IR 就可以进一步被转换为目标机器码,或者通过 JIT 编译直接执行。
下面一段程序用 lit 检验 i32 格式下的 7 前导 0 个数为 29.
1 |
|
这条命令具体做了以下几件事情:
mlir-opt %s
:运行mlir-opt
工具,%s
代表当前的.mlir
文件。这个工具会将 MLIR 文件中定义的内容按顺序进行优化和转换。--pass-pipeline
:定义了一个 Pass 管道(Pipeline)。这表示一系列的 Pass 要按顺序执行。这些 Pass 包括:convert-math-to-funcs{convert-ctlz}
:将math.ctlz
操作转换为一个函数调用。convert-scf-to-cf
和convert-arith-to-llvm
:分别将结构化控制流(scf
)和算术操作(arith
)转换为控制流方言(cf
)和 LLVM IR 方言(llvm
)。convert-func-to-llvm
:将标准的func
转换为llvm
方言。convert-cf-to-llvm
:将控制流方言中的操作转换为llvm
方言中的等效操作。reconcile-unrealized-casts
:处理类型转换,以确保所有操作的输入输出类型一致。
mlir-cpu-runner
:接收转换后的 MLIR 代码并执行。这里使用了-e test_7i32_to_29
指定入口函数@test_7i32_to_29
,并使用-entry-point-result=i32
表示函数返回一个i32
类型的结果。重定向输出到
%t
:将运行结果重定向到一个临时文件%t
。FileCheck %s --check-prefix=CHECK_TEST_7i32_TO_29 < %t
:使用FileCheck
来验证运行结果。--check-prefix=CHECK_TEST_7i32_TO_29
表示使用CHECK_TEST_7i32_TO_29
前缀的检查指令。
经过以上一系列 pass 的优化后生成的 mlir 如下
1 |
|
^bb
是 MLIR 中用于表示基本块 (basic block) 的符号。基本块是程序中一组有序的指令,具有一个入口点,且在执行时指令是按顺序执行的,直到通过 llvm.br
(branch) 跳转到其他基本块。 注释中的 // pred
表示该块的前驱块。