Browse by type
Kexing Zhou(周可行)
北京大学
zhoukexing@pku.edu.cn
MLIR 在于设计一套可复用的编译管线,包括可复用的 IR、Pass 和 IO 系统。在 IR 中,多个 Dialect 可以混合存在。MLIR 已经定义好了一套 Dialect Translation Graph:

MLIR 的 Dialect 是相对独立的,下面列举一些常见的 dialect:
MLIR的编译从高层次的 tensor 到 低层次的 scf,cf,每个阶段都是多个 dialect 的混合体,每次 lowering 往往只针对一个 dialect 进行。
这里简单举例,dialect 是如何混合的。
例如,我是 pytorch,生成了一些神经网络,我想要表示这些运算:
接下来,将其逐渐 lower 到 LLVM:
可见,MLIR 编译有一个特点:不同 dialect 是独立的。
MLIR 可以从各个层次优化 IR:例如:
MLIR 的 insight 在于“及时做优化”。很明显,linalg 层次,我们很容易发现矩阵被转置了两次,但一旦 lower 到 scf,所有转置操作都变成循环,优化就很难进行了。
我们使用 MLIR,主要也是想要复用别人已经写好的代码,一般包括:
MLIR 也有缺点:
MLIR 是 树形结构,每个节点是 Operation,Op 可以组成 Block,Block 组成 Region,而 Region 又可以嵌套在 Op 内部。
MLIR 的基本块使用 “基本块参数” 来取代“phi函数”,如下面的例子:
module {
func.func @foo(%a: i32, %b: i32, %c: i32) -> i32 {
%cmp = arith.cmpi "sge", %a, %b : i32
cf.cond_br %cmp, ^add(%a: i32), ^add(%b: i32)
^add(%1: i32):
%ret = llvm.add %1, %c : i32
cf.br ^ret
^ret:
func.return %ret : i32
}
}
module: 默认情况下,mlir 最外层是 builtin.module,作为 IR 的根。
构建第一个 mlir 项目往往非常困难,下面给一个我常用的工程模板:
mlir-tutorial
├── install # Install Prefix,把 MLIR 编译后安装到这里
├── llvm-project # MLIR 项目
└── mlir-toy # 自己的 MLIR 工程
首先,按照 MLIR getting started 的方法,安装 MLIR。注意,安装的时候要设置 PREFIX 为 install 目录,如下面所示,和 getting start 上的略有区别:
git clone https://github.com/llvm/llvm-project.git
cd llvm-project
git checkout 186a4b3b657878ae2aea23caf684b6e103901162 # 本教程使用的版本
mkdir build && cd build
cmake -G Ninja ../llvm \
-DCMAKE_INSTALL_PREFIX=/mlir-tutorial/install \
-DLLVM_ENABLE_PROJECTS=mlir \
-DLLVM_BUILD_EXAMPLES=ON \
-DLLVM_TARGETS_TO_BUILD="Native;NVPTX;AMDGPU" \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_ENABLE_ASSERTIONS=ON
在 build 完之后,安装到 prefix
ninja install
现在,mlir 把所有二进制文件、库文件都装到了 install 目录下。现在可以 export PATH=/mlir-tutorial/install/bin:$PATH,方便调用 bin 里面的 mlir-opt 等程序。
install
├── bin
├── examples
├── include
├── lib
└── share
接下来,在 mlir-toy 里面建立一个简单的工程
mlir-toy
├── CMakeLists.txt
└── main.cpp
其中 CMakeLists.txt 文件写法比较固定:
cmake_minimum_required(VERSION 3.13.4)
project(mlir-toy VERSION 0.0.0)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # 生成 compile_commands.json 便于代码高亮
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
find_package(MLIR REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(TableGen)
include(AddLLVM)
include(AddMLIR)
include(HandleLLVMOptions)
include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS})
add_executable(mlir-toy main.cpp)
在 main.cpp 里面写一个 main 函数,然后先 build 一下。注意,必须要写上 CMAKE_INSTALL_PREFIX,这样cmake 可以自动找到 MLIR。
cd build
cmake .. -GNinja -DCMAKE_INSTALL_PREFIX=/mlir-tutorial/install
ninja
使用 vscode 默认的 lint 工具跑 mlir 会非常卡,建议使用 clangd。
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON(上面的CMakeLists.txt 已经加上了)yaml
CompileFlags:
Remove:
- -fno-lifetime-dse测试用 mlir:
func.func @test(%a: i32, %b: i32) -> i32 {
%c = arith.addi %a, %b : i32
func.return %c : i32
}
最简单的读入输出:
#include "mlir/IR/AsmState.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/Parser/Parser.h"
#include "mlir/Support/FileUtilities.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "llvm/Support/raw_ostream.h"
using namespace mlir;
int main(int argc, char ** argv) {
MLIRContext ctx;
// 首先,注册需要的 dialect
ctx.loadDialect<func::FuncDialect, arith::ArithDialect>();
// 读入文件
auto src = parseSourceFile<ModuleOp>(argv[1], &ctx);
// 输出dialect,也可以输出到 llvm::errs(), llvm::dbgs()
src->print(llvm::outs());
// 简单的输出,在 debug 的时候常用
src->dump();
return 0;
}
需要连接上所有依赖的文件:
target_link_libraries(
ex1-io
MLIRIR
MLIRParser
MLIRFuncDialect
MLIRArithDialect
)
测试方式:
./ex1-io ../ex1-io/ex1.mlir
#include "mlir/IR/AsmState.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/Parser/Parser.h"
#include "mlir/Support/FileUtilities.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "llvm/Support/raw_ostream.h"
using namespace mlir;
int main(int argc, char ** argv) {
MLIRContext ctx;
ctx.loadDialect<func::FuncDialect, arith::ArithDialect>();
// 创建 OpBuilder
OpBuilder builder(&ctx);
auto mod = builder.create<ModuleOp>(builder.getUnknownLoc());
// 设置插入点
builder.setInsertionPointToEnd(mod.getBody());
// 创建 func
auto i32 = builder.getI32Type();
auto funcType = builder.getFunctionType({i32, i32}, {i32});
auto func = builder.create<func::FuncOp>(builder.getUnknownLoc(), "test", funcType);
// 添加基本块
auto entry = func.addEntryBlock();
auto args = entry->getArguments();
// 设置插入点
builder.setInsertionPointToEnd(entry);
// 创建 arith.addi
auto addi = builder.create<arith::AddIOp>(builder.getUnknownLoc(), args[0], args[1]);
// 创建 func.return
builder.create<func::ReturnOp>(builder.getUnknownLoc(), ValueRange({addi}));
mod->print(llvm::outs());
return 0;
}
如何寻找 builder.create 的参数:builder.create 内部是调用 Op::build 函数的,你可以 Ctrl + 鼠标点击找到 func::FuncOp 的定义,然后找里面的 build 函数,看参数表。
MLIR 的一个 Operation 里可以包含下面的一些东西:
MLIR 中,Attribute 是高度灵活的,允许插入原来不存在的 attr,允许不同 dialect 互相插入 attribute。
Attribute 和 Operand 有一些区别。Attribute 指的编译器已知的量,而 Operand 指只有运行时才能知道的量。
如下面的这个Op,0 是一个 Attribute 而不是一个 Operand
%c0 = arith.constant 0 : i32
Value 必然包含 Type,Type 也可以作为 Attribute 附加在 Operation 上。
例如函数 Op,虽然 %a, %b 出现在了参数表里,但它们实际上是函数类型的一部分,算是 Type Attribute。
func.func @test(%a: i32, %b: i32) -> i32 {
%c = arith.addi %a, %b : i32
func.return %c : i32
}
mlir-opt --mlir-print-op-generic 来打印这里的代码,得到下面的代码。参数名被隐去,只有 function_type 作为 attribute 保留了下来。
mlir
"builtin.module"() ({
"func.func"() <{function_type = (i32, i32) -> i32, sym_name = "test"}> ({
^bb0(%arg0: i32, %arg1: i32):
%0 = "arith.addi"(%arg0, %arg1) : (i32, i32) -> i32
"func.return"(%0) : (i32) -> ()
}) : () -> ()
}) : () -> ()MLIR 的所有 Op 都有一个统一的储存格式,叫 Operation。Operation 里面存了 OpName 和所有的 operands, results, attributes 和其它的东西。
用户定义的 arith.addi 等等 Op,本质上都是 Operation 的指针。但与 Operation* 不同的是,AddIOp 定义了 Operation 里储存的数据的解释方式。如 AddOp,自己是一个 Operation 的指针
$ claude mcp add mlir-tutorial \
-- python -m otcore.mcp_server <graph>