MLIR的个人总结
前端转MLIR的基本流程
前端可以是一个模型model,也可以是特定领域语言DSL。
如果为model,则需要经过以下3步:
- 需要将模型文件进行解析,得到相应的计算图以及权重参数
- 根据计算图生成相应的MLIR表示
如果为DSL转换成MLIR的表示,需要经过以下几个步骤:
- 对DSL进行语义分析和语法分析,得到其AST(参考toy1)
- 解析AST生成MLIR代码(参考toy2)
总而言之,无论是哪一种前端,接入到mlir中后,其后续的操作都是相同的。
创建MLIR项目
mlir项目的创建可分为是否自定义dialect;若没有自定义dialect,则只需要链接相应的libs即可;若含有自定义dialect,则一般使用ODS框架进行自动生成;
无自定义dialect的项目创建
# 项目结构
mlir-project
├── CMakeLists.txt
└── main.cpp
# CMakeLists.txt文件格式
cmake_minimum_required(VERSION 3.13.4)
project(mlir-project VERSION 0.0.0)
# C++标准
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
# 导入mlirconfig.cmake文件,初始化mlir
set(MLIR_CONFIG_PATH ./llvm-project/build/lib/cmake/mlir)
find_package(MLIR REQUIRED CONFIG PATHS ${MLIR_CONFIG_PATH})
# 将与mlir和llvm相关的.cmake文件添加到CMAKE_MODULE_PATH变量
list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
# 直接include导入.cmake文件
include(TableGen)
include(AddLLVM)
include(AddMLIR)
include(HandleLLVMOptions)
# 链接生成可执行文件
include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS})
add_executable(demo main.cpp)
target_link_libraries(
demo
MLIRIR
MLIRParser
MLIRFuncDialect # 这里使用了funcDialect和arithDialect
MLIRArithDialect
)
#include "mlir/IR/AsmState.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/BuiltinOps.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/IR/Visitors.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/Casting.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/ADT/DenseMap.h"
using namespace mlir;
using namespace llvm;
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;
}
// 生成的 mlir 代码
module {
func.func @test(%arg0: i32, %arg1: i32) -> i32 {
%0 = arith.addi %arg0, %arg1 : i32
return %0 : i32
}
}
这是最简单的一种mlir项目的创建方式。
有自定义dialect的项目创建
创建自定义dialect主要就是创建其所需要的特定operator,之后就可以使用自定义的dialect接入到MLIR中。
含有自定义dialect的项目需要创建声明dialect以及operator的tablegen文件(dialect和operator的声明可以写在一个tablegen文件中),在编译过程中DOS会根据tablegen文件,在build目录下生成dialect和op定义与实现的.{h,cpp}.inc文件,后续的使用需要include进自己的项目中。
# 项目结构
toy-dialect
├── CMakeLists.txt # 控制其他各个部分的 CMakeLists(1)
├── include
│ ├── CMakeLists.txt # add_subdirectory(toy)
│ ├── IR.h # 当前项目所有.h文件的include
│ └── toy
│ ├── CMakeLists.txt # 控制 Dialect 定义的 CMakeLists(2)
│ ├── ToyDialect.h # Dialect 头文件
│ ├── ToyDialect.td # Dialect TableGen 文件
│ ├── ToyOps.h # Op 头文件
│ ├── ToyOps.td # Op TableGen 文件
│ └── Toy.td # 把 ToyDialect.td 和 ToyOps.td include 到一起,用于 tablegen
└── src
├── CMakeLists.txt # 生成可执行文件的 CMakeLists(4)
├── main.cpp # 主函数,使用dialect
└── toy
├── CMakeLists.txt # 生成MLIRToy等依赖的CMakeLists(3)
└── toy.cpp # Dialect library
toy.td文件可以将ToyDialect.td和ToyOps.td文件include到一起,所以toy.td文件可以代表dialect和ops两个;那么就等同于可以将ToyDialect.td和ToyOps.td文件的内容全都写到一个toy.td文件中。
下面对cmakelists文件进行分析(对照上面目录树):
- (1)CMakeLists.txt文件(./toy-dialect/CMakeLists.txt)
cmake_minimum_required(VERSION 3.13.4)
project(test LANGUAGES CXX C)
# C++ 标准
set(CMAKE_CXX_STANDARD 17 CACHE STRING "C++ standard to conform to")
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(LLVM_BUILD_LIBRARY_DIR /home/xiebaokang/projects/mlir/llvm-project/build/lib)
set(MLIR_CONFIG_PATH /home/xiebaokang/projects/mlir/llvm-project/build/lib/cmake/mlir)
find_package(MLIR REQUIRED CONFIG PATHS ${MLIR_CONFIG_PATH})
# llvm构建中生成的runtime和lib存储位置
set(LLVM_RUNTIME_OUTPUT_INTDIR ${CMAKE_BINARY_DIR}/bin)
set(LLVM_LIBRARY_OUTPUT_INTDIR ${CMAKE_BINARY_DIR}/lib)
# 将llvm和mlir相关的所有.cmake文件路径添加到CMAKE_MODULE_PATH变量中,后续可直接include
list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(TableGen)
include(AddLLVM)
include(AddMLIR)
include(HandleLLVMOptions)
# 链接1 - include
include_directories(${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS})
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_BINARY_DIR}/include)
link_directories(${LLVM_BUILD_LIBRARY_DIR})
SET(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
add_subdirectory(include)
add_subdirectory(src)
- (2)CMakeLists.txt文件(./toy-dialect/include/toy/CMakeLists.txt)**
# 添加dialect,Toy为Toy.td的名称;toy为dialect的名称
add_mlir_dialect(Toy toy)
注:add_mlir_dialect函数其实封装了mlir_tablegen函数以及add_public_tablegen_target函数,代码如下:
add_mlir_dialect(Toy toy)
# 等价于
set(LLVM_TARGET_DEFINITIONS Toy.td) // 指定主要文件
mlir_tablegen(Toy.h.inc -gen-op-decls)
mlir_tablegen(Toy.cpp.inc -gen-op-defs)
mlir_tablegen(ToyDialect.h.inc -gen-dialect-decls)
mlir_tablegen(ToyDialect.cpp.inc -gen-dialect-defs)
add_public_tablegen_target(MLIRToyIncGen) // MLIRToyIncGen 依赖
- (3)CMakeLists.txt文件(./toy-dialect/src/toy/CMakeLists.txt)**
add_mlir_dialect_library(
MLIRToy # 生成依赖的名称lib
toy.cpp # 包含了对ToyDialect.h/ToyOps.h、{}.cpp.inc文件,以及dialect的初始化
ADDITIONAL_HEADER_DIRS
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/build/include
DEPENDS
MLIRToyIncGen # IncGen的依赖
LINK_COMPONENTS
Core
LINK_LIBS PUBLIC
MLIRIR
MLIRInferTypeOpInterface
)
- (4)CMakeLists.txt文件(./toy-dialect/src/CMakeLists.txt)
add_subdirectory(toy) # 先运行toy下的cmakelist生成MLIRToy依赖
file(GLOB SRC_FILE ./*.cpp) # 找到主文件main.cpp
add_executable(test ${SRC_FILE}) # 生成可执行文件
set(LLVM_LINK_COMPONENTS # 设置llvm需要的组件
Core
Support
nativecodegen
OrcJIT
)
# dialect_libs == MLIR_DIALECT_LIBS 依赖libs文件
get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
get_property(conversion_libs GLOBAL PROPERTY MLIR_CONVERSION_LIBS)
get_property(translation_libs GLOBAL PROPERTY MLIR_TRANSLATION_LIBS)
target_link_libraries(test # 可执行文件链接libs
PUBLIC
${dialect_libs}
${conversion_libs}
${translation_libs}
MLIRToy
MLIRAnalysis
MLIRCallInterfaces
MLIRCastInterfaces
MLIRExecutionEngine
MLIRIR
MLIRLLVMCommonConversion
MLIRLLVMToLLVMIRTranslation
MLIRMemRefDialect
MLIRParser
MLIRPass
MLIRSideEffectInterfaces
MLIRTargetLLVMIRExport
MLIRTransforms
MLIRNVVMToLLVMIRTranslation
MLIRToLLVMIRTranslationRegistration
MLIRTargetLLVMIRImport
MLIRTargetLLVMIRExport
MLIRFuncToLLVM
MLIRSupport
MLIROptLib
)
这样的项目结构,make编译时,ODS框架会根据tablegen文件事先生成.{h,cpp}.inc文件(dialect/ops/type),然后(2)cmakelists中的add_mlir_dialect_library会生成MLIRtoy的lib,最后由(4)cmakelists的target_link_libraries链接MLIRtoy的lib生成可执行文件。
模式匹配和重写pass(ch3)
优化的一种方式,对某种op进行匹配,然后重新写满足要求的op。上述项目的目录需要更新:
# 项目结构
toy-dialect
├── CMakeLists.txt # 控制其他各个部分的 CMakeList(1)
├── include
│ ├── CMakeLists.txt # add_subdirectory(toy)
│ ├── IR.h # 当前项目所有.h文件的include
│ └── toy
│ ├── CMakeLists.txt # 控制 Dialect 定义的 CMakeList(2)
│ ├── ToyDialect.h # Dialect 头文件
│ ├── ToyDialect.td # Dialect TableGen 文件
│ ├── ToyOps.h # Op 头文件
│ ├── ToyOps.td # Op TableGen 文件
│ ├── Toy.td # 把 ToyDialect.td 和 ToyOps.td include 到一起,用于 tablegen
│ └── ToyCombine.td # 重写匹配的ddr声明----------new add
└── src
├── CMakeLists.txt # 生成可执行文件的 CMakeList(4)
├── main.cpp # 主函数,使用dialect
└── toy
├── CMakeLists.txt # 生成MLIRToy等依赖的CMakeLists(3)
├── toy.cpp # Dialect library
└── toyCombine.cpp # 匹配重写的手写类以及重写登记处----------new add
匹配重写的优化过程
首先说明,匹配重写优化的步骤;
第一步,无论是DDR生成,还是手写C++,都需要有匹配重写的C++代码,格式如下:
// xxx为当前匹配重写的类名,xxxOp需要匹配出来的Op
struct xxx : public mlir::OpRewritePattern<xxxOp> {
xxx(mlir::MLIRContext *context)
: OpRewritePattern<xxxOp>(context, /*benefit=*/1) {}
// 重写函数,这里重写符合要求的op
mlir::LogicalResult matchAndRewrite(xxxOp op,
mlir::PatternRewriter &rewriter) const override {
// 这里干你想干的事情,比如冗余检测等
return success();
}
};
// 登记为 canonicalization 模式,op的优化才会生效
void xxxOp::getCanonicalizationPatterns(RewritePatternSet &results,
MLIRContext *context) {
results.add<xxx,xxx...>(context);
}
第二步,在定义完类后,需要登记为 canonicalization 模式;
// 登记为 canonicalization 模式,op的优化才会生效
void xxxOp::getCanonicalizationPatterns(RewritePatternSet &results,
MLIRContext *context) {
results.add<xxx,xxx...>(context);
}
注:既然属于创建时的优化,则需要生成和MLIRToy lib相同的操作,也就是在(3)CMakeLists.txt文件中的add_mlir_dialect_library需要添加cpp文件,如果使用DDR直接生成的匹配重写类,则需要添加incgen依赖。
第三步,在此xxxOp的声明文件(xxOps.td)中,op的声明处添加 “let hasCanonicalizer = 1;” 声明,确保启用规范化框架,应用 canonicalization pass;
def xxxOp : Toy_Op<"xxx", [NoSideEffect]> {
...
// 确保启用规范化框架,应用 canonicalization pass
let hasCanonicalizer = 1;
...
}
第四步,向passmanager中添加此类pass
mlir::PassManager pm(&context);
mlir::applyPassManagerCLOptions(pm); // 通用优化pipline
pm.addNestedPass<mlir::toy::FuncOp>(mlir::createCanonicalizerPass()); // 添加pass
pm.run(*module); // 运行pass
注:注意addNestedPass表示只在指定的op下运行此pass,createCanonicalizerPass这个pass由官方提供,在上面我们已经将我们自己的模式匹配重写的类登记为 canonicalization 模式,所以直接passrun的就可以应用此pass。
DDR生成匹配重写类
DDR的直接生成我只说一点,也就是cmake文件的格式。
因为,采用的tablegen文件声明匹配重写的类,所以CMakeLists.txt中肯定需要一个像add_mlir_dialect一样的函数来生成inc文件以及incgen的依赖,所以(2)CMakeLists.txt(./toy-dialect/include/toy/CMakeLists.txt)修改为:
add_mlir_dialect(Toy toy)
# 将ToyCombine.td文件与Toy.td文件放在一个目录下
set(LLVM_TARGET_DEFINITIONS ToyCombine.td)
mlir_tablegen(ToyCombine.inc -gen-rewriters)
add_public_tablegen_target(MLIRToyCombineIncGen) // 这里会生成ToyCombineIncGen的incgen依赖
这一步在make的时候会生成inc文件,以及ToyCombineIncGen依赖;而且,ToyCombine.cpp文件中有对匹配重写类的canonicalization 登记,所以(3)CMakeLists.txt文件(./toy-dialect/src/toy/CMakeLists.txt)代码修改为:
add_mlir_dialect_library(
MLIRToy # 生成依赖的名称lib
toy.cpp # 包含了对ToyDialect.h/ToyOps.h、{}.cpp.inc文件,以及dialect的初始化
toyCombine.cpp # canonicalization 登记处(手写类处)------new add
ADDITIONAL_HEADER_DIRS
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/build/include
DEPENDS
MLIRToyIncGen # IncGen的依赖
MLIRToyCombineIncGen # 模式匹配重写的incgen依赖------new add
LINK_COMPONENTS
Core
LINK_LIBS PUBLIC
MLIRIR
MLIRInferTypeOpInterface
)
最后,至于ddr声明的书写方式,参考官网:Table-driven Declarative Rewrite Rule (DDR)
内联和形状推断pass(ch4)
通过使用 Dialect,MLIR 可以表示多种不同等级的抽象。尽管这些不同的 Dialect 表示不同的抽象,但某些操作的算法机制十分相似,为了减少代码重复,MLIR 提供了一组通用的转换和分析,也就是这一类型的pass,mlir已经实现了接口。
内联(inline)
适用于函数调用op(callop),将一些简单的函数嵌入到调用处,,以储存空间为代价换取运行速度。
mlir中已经实现了通用函数内联的pass,我们构建一个属于toy dialect的内联函数接口,需要继承DialectInlinerInterface
来编写自己的内联接口;
形状推断(shape inference)
需要使用ODS框架来声明定义ShapeInference 的接口(写一个shapeInference.td文件,放在Toy.td相同目录下)
将此接口添加到需要的operation的声明处(ops.td),然后这些operation就具有形状推断的接口了,就需要在这些 operation 中定义对应的形状推断函数,独立定义可以保证 ShapeInferencePass 会独立地作用于该 operation。
最后添加进入passmanage中。
mlir::applyPassManagerCLOptions(pm); // 通用pipeline
pm.addPass(mlir::createInlinerPass()); // 内联pass
mlir::OpPassManager &optPM = pm.nest<mlir::toy::FuncOp>();
optPM.addPass(mlir::toy::createShapeInferencePass()); // 形状推断pass
optPM.addPass(mlir::createCanonicalizerPass()); // 匹配重写
optPM.addPass(mlir::createCSEPass()); // 公共子表达式消除(直接调用就行)
现在主要讲解项目构建,因为现在新加入一个tablegen文件(ShapeInferenceInterface.td)用于形状推断,且tablegen文件放在与Toy.td文件相同的路径下,所以有:
(2)CMakeLists.txt文件(./toy-dialect/include/toy/CMakeLists.txt)
add_mlir_dialect(Toy toy)
# 将ToyCombine.td文件与Toy.td文件放在一个目录下
set(LLVM_TARGET_DEFINITIONS ToyCombine.td)
mlir_tablegen(ToyCombine.inc -gen-rewriters)
add_public_tablegen_target(MLIRToyCombineIncGen) // 这里会生成ToyCombineIncGen的incgen依赖
# 将ShapeInferenceInterface.td文件与Toy.td文件放在一个目录下
set(LLVM_TARGET_DEFINITIONS ShapeInferenceInterface.td)
mlir_tablegen(ShapeInferenceOpInterfaces.h.inc -gen-op-interface-decls) # 这个需要生成.h文件
mlir_tablegen(ShapeInferenceOpInterfaces.cpp.inc -gen-op-interface-defs)
add_public_tablegen_target(MLIRToyShapeInferenceInterfaceIncGen)
(3)CMakeLists.txt文件(./toy-dialect/src/toy/CMakeLists.txt)
add_mlir_dialect_library(
MLIRToy # 生成依赖的名称lib
toy.cpp # 包含了对ToyDialect.h/ToyOps.h、{}.cpp.inc文件,以及dialect的初始化
toyCombine.cpp # canonicalization 登记处(手写类处)
ShapeInferencePass.cpp # 包括推断的详细过程 ------new add
ADDITIONAL_HEADER_DIRS
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/build/include
DEPENDS
MLIRToyIncGen # IncGen的依赖
MLIRToyCombineIncGen # 模式匹配重写的incgen依赖
MLIRToyShapeInferenceInterfaceIncGen # 形状推断的incgen依赖------new add
LINK_COMPONENTS
Core
LINK_LIBS PUBLIC
MLIRIR
MLIRInferTypeOpInterface
)
注:与上述匹配重写不同的点在于,这个类似于定义新的op,所以还需要一个ShapeInferenceInterface.h文件。
lowwering
lowwering也是使用pass实现,pass还是自己实现。