# 前言
现在开启 MLIR 学习系列。本篇是跟着 Toy 语言学习 MLIR 的第五篇,前述章节已经介绍到 toy dialect 降级到 Affine dialect,本章节主要介绍将其降级到 LLVM 及 Codegen 生成可执行文件的过程。前述内容请参考【MLIR】跟着 Toy 语言学习 MLIR【1】Toy 语言和 Toy Dialect,【MLIR】跟着 Toy 语言学习 MLIR【2】pattern 匹配和重写,【MLIR】跟着 Toy 语言学习 MLIR【3】通过接口实现通用转换,【MLIR】跟着 Toy 语言学习 MLIR【4】部分降级到低层方言。
相关链接: LLVM Project ,MLIR 官方文档,MLIR 官网教程,【编译器】使用 llvm 编译自定义语言【1】构建 AST ,【MLIR】跟着 Toy 语言学习 MLIR【2】pattern 匹配和重写,【MLIR】跟着 Toy 语言学习 MLIR【3】通过接口实现通用转换,【MLIR】跟着 Toy 语言学习 MLIR【4】部分降级到低层方言。
作为初学者,错误在所难免,还望不吝赐教。
# 基本简介
官网 Toy 教程 展示了如何将自定义语言 Toy 借助 MLIR 一步步编译为可执行机器码的过程。Toy 是一种简单的自定义语言,为了简便,其所有数据类型定义为 fp64 类型的 Tensor,支持 +/* 操作和 transpose 等有限的操作。
以下是整个编译降级的过程:
Toy txt -> Toy AST -> Toy Dialect -> Affine Dialect -> llvm Dialect -> llvm IR -> 机器码(通过 JIT 编译)

# 本章内容
在之前的降级中,我们已经降级的大部分的操作到 Affine dialect ,现在方言的状态是:
func.func @main() { | |
//... 大量的 affine.for、affine.load/store ... | |
// 只剩下这一个 toy.print 操作 | |
toy.print %memref : memref<2x3xf64> | |
return | |
} |
接下来是对 toy.print 进行降级,教程中说我们不需要直接将 toy.print 直接降级到 llvm,这样太复杂。因为 DialectConversion 框架支持传递性降级,不需要直接生成 LLVM 方言的操作。可以先生成一个结构化的循环嵌套(循环嵌套是为了不断打印输出信息),只要我们有从循环操作到 LLVM 的降级规则,整个降级就会成功。
教程给出构建 printf 的声明:
/// Return a symbol reference to the printf function, inserting it into the | |
/// module if necessary. | |
static FlatSymbolRefAttr getOrInsertPrintf(PatternRewriter &rewriter, | |
ModuleOp module, | |
LLVM::LLVMDialect *llvmDialect) { | |
auto *context = module.getContext(); | |
if (module.lookupSymbol<LLVM::LLVMFuncOp>("printf")) | |
return SymbolRefAttr::get(context, "printf"); | |
// Create a function declaration for printf, the signature is: | |
// * `i32 (i8*, ...)` | |
auto llvmI32Ty = IntegerType::get(context, 32); | |
auto llvmI8PtrTy = | |
LLVM::LLVMPointerType::get(IntegerType::get(context, 8)); | |
auto llvmFnType = LLVM::LLVMFunctionType::get(llvmI32Ty, llvmI8PtrTy, | |
/*isVarArg=*/true); | |
// Insert the printf function into the body of the parent module. | |
PatternRewriter::InsertionGuard insertGuard(rewriter); | |
rewriter.setInsertionPointToStart(module.getBody()); | |
LLVM::LLVMFuncOp::create(rewriter, module.getLoc(), "printf", llvmFnType); | |
return SymbolRefAttr::get(context, "printf"); | |
} |
这是一个 getOrInsertPrintf ,它只是一个普通的辅助函数,它的作用是 :在模块中获取或创建 printf 函数的声明,并返回它的符号引用。
以下是 toy.print 操作降级 Pattern。
class PrintOpLowering : public OpConversionPattern<toy::PrintOp> { | |
public: | |
using OpConversionPattern<toy::PrintOp>::OpConversionPattern; | |
LogicalResult | |
matchAndRewrite(toy::PrintOp op, OpAdaptor adaptor, // 重写函数 | |
ConversionPatternRewriter &rewriter) const override { | |
auto *context = rewriter.getContext(); | |
auto memRefType = llvm::cast<MemRefType>((*op->operand_type_begin())); | |
auto memRefShape = memRefType.getShape(); | |
auto loc = op->getLoc(); | |
ModuleOp parentModule = op->getParentOfType<ModuleOp>(); | |
// Get a symbol reference to the printf function, inserting it if necessary. | |
auto printfRef = getOrInsertPrintf(rewriter, parentModule); | |
Value formatSpecifierCst = getOrCreateGlobalString( | |
loc, rewriter, "frmt_spec", StringRef("%f \0", 4), parentModule); | |
Value newLineCst = getOrCreateGlobalString( | |
loc, rewriter, "nl", StringRef("\n\0", 2), parentModule); | |
// Create a loop for each of the dimensions within the shape. | |
SmallVector<Value, 4> loopIvs; | |
for (unsigned i = 0, e = memRefShape.size(); i != e; ++i) { // 创建循环 | |
auto lowerBound = arith::ConstantIndexOp::create(rewriter, loc, 0); | |
auto upperBound = | |
arith::ConstantIndexOp::create(rewriter, loc, memRefShape[i]); | |
auto step = arith::ConstantIndexOp::create(rewriter, loc, 1); | |
auto loop = | |
scf::ForOp::create(rewriter, loc, lowerBound, upperBound, step); | |
for (Operation &nested : make_early_inc_range(*loop.getBody())) | |
rewriter.eraseOp(&nested); | |
loopIvs.push_back(loop.getInductionVar()); | |
// Terminate the loop body. | |
rewriter.setInsertionPointToEnd(loop.getBody()); | |
// Insert a newline after each of the inner dimensions of the shape. | |
if (i != e - 1) | |
LLVM::CallOp::create(rewriter, loc, getPrintfType(context), printfRef, | |
newLineCst); | |
scf::YieldOp::create(rewriter, loc); | |
rewriter.setInsertionPointToStart(loop.getBody()); | |
} | |
// Generate a call to printf for the current element of the loop. | |
auto elementLoad = | |
memref::LoadOp::create(rewriter, loc, op.getInput(), loopIvs); | |
LLVM::CallOp::create(rewriter, loc, getPrintfType(context), printfRef, | |
ArrayRef<Value>({formatSpecifierCst, elementLoad})); | |
// Notify the rewriter that this operation has been removed. | |
rewriter.eraseOp(op); // 删掉原始的 toy.print 操作 | |
return success(); | |
} | |
private: | |
/// Create a function declaration for printf, the signature is: | |
/// * `i32 (i8*, ...)` | |
static LLVM::LLVMFunctionType getPrintfType(MLIRContext *context) { | |
auto llvmI32Ty = IntegerType::get(context, 32); | |
auto llvmPtrTy = LLVM::LLVMPointerType::get(context); | |
auto llvmFnType = LLVM::LLVMFunctionType::get(llvmI32Ty, llvmPtrTy, | |
/*isVarArg=*/true); | |
return llvmFnType; | |
} | |
/// Return a symbol reference to the printf function, inserting it into the | |
/// module if necessary. | |
static FlatSymbolRefAttr getOrInsertPrintf(PatternRewriter &rewriter, | |
ModuleOp module) { | |
auto *context = module.getContext(); | |
if (module.lookupSymbol<LLVM::LLVMFuncOp>("printf")) | |
return SymbolRefAttr::get(context, "printf"); | |
// Insert the printf function into the body of the parent module. | |
PatternRewriter::InsertionGuard insertGuard(rewriter); | |
rewriter.setInsertionPointToStart(module.getBody()); | |
LLVM::LLVMFuncOp::create(rewriter, module.getLoc(), "printf", | |
getPrintfType(context)); | |
return SymbolRefAttr::get(context, "printf"); | |
} | |
/// Return a value representing an access into a global string with the given | |
/// name, creating the string if necessary. | |
static Value getOrCreateGlobalString(Location loc, OpBuilder &builder, | |
StringRef name, StringRef value, | |
ModuleOp module) { | |
// Create the global at the entry of the module. | |
LLVM::GlobalOp global; | |
if (!(global = module.lookupSymbol<LLVM::GlobalOp>(name))) { | |
OpBuilder::InsertionGuard insertGuard(builder); | |
builder.setInsertionPointToStart(module.getBody()); | |
auto type = LLVM::LLVMArrayType::get( | |
IntegerType::get(builder.getContext(), 8), value.size()); | |
global = LLVM::GlobalOp::create(builder, loc, type, /*isConstant=*/true, | |
LLVM::Linkage::Internal, name, | |
builder.getStringAttr(value), | |
/*alignment=*/0); | |
} | |
// Get the pointer to the first character in the global string. | |
Value globalPtr = LLVM::AddressOfOp::create(builder, loc, global); | |
Value cst0 = LLVM::ConstantOp::create(builder, loc, builder.getI64Type(), | |
builder.getIndexAttr(0)); | |
return LLVM::GEPOp::create( | |
builder, loc, LLVM::LLVMPointerType::get(builder.getContext()), | |
global.getType(), globalPtr, ArrayRef<Value>({cst0, cst0})); | |
} | |
}; | |
} // namespace |
以下 Pass 包装了将 当前 Affine dialect 和 toy.print 降级到 llvm dialect 的 pattern,其中 PrintOpLowering 是自定义的 print 降级 pattern。当前降级操作还要将把当前正在处理的 MemRef 类型转换为 LLVM 中的表示形式。为了完成此转换,我们使用 TypeConverter 作为降级过程的一部分。该转换器用于指定一种类型如何映射到另一种类型。
void ToyToLLVMLoweringPass::runOnOperation() { | |
// The first thing to define is the conversion target. This will define the | |
// final target for this lowering. For this lowering, we are only targeting | |
// the LLVM dialect. | |
LLVMConversionTarget target(getContext()); | |
target.addLegalOp<ModuleOp>(); | |
// During this lowering, we will also be lowering the MemRef types, that are | |
// currently being operated on, to a representation in LLVM. To perform this | |
// conversion we use a TypeConverter as part of the lowering. This converter | |
// details how one type maps to another. This is necessary now that we will be | |
// doing more complicated lowerings, involving loop region arguments. | |
LLVMTypeConverter typeConverter(&getContext()); // 类型转换器 | |
// Now that the conversion target has been defined, we need to provide the | |
// patterns used for lowering. At this point of the compilation process, we | |
// have a combination of `toy`, `affine`, and `std` operations. Luckily, there | |
// are already exists a set of patterns to transform `affine` and `std` | |
// dialects. These patterns lowering in multiple stages, relying on transitive | |
// lowerings. Transitive lowering, or A->B->C lowering, is when multiple | |
// patterns must be applied to fully transform an illegal operation into a | |
// set of legal ones. | |
RewritePatternSet patterns(&getContext()); | |
populateAffineToStdConversionPatterns(patterns); | |
populateSCFToControlFlowConversionPatterns(patterns); | |
mlir::arith::populateArithToLLVMConversionPatterns(typeConverter, patterns); | |
populateFinalizeMemRefToLLVMConversionPatterns(typeConverter, patterns); | |
cf::populateControlFlowToLLVMConversionPatterns(typeConverter, patterns); | |
populateFuncToLLVMConversionPatterns(typeConverter, patterns); | |
// The only remaining operation to lower from the `toy` dialect, is the | |
// PrintOp. | |
patterns.add<PrintOpLowering>(&getContext()); | |
// We want to completely lower to LLVM, so we use a `FullConversion`. This | |
// ensures that only legal operations will remain after the conversion. | |
auto module = getOperation(); | |
if (failed(applyFullConversion(module, target, std::move(patterns)))) | |
signalPassFailure(); | |
} | |
/// Create a pass for lowering operations the remaining `Toy` operations, as | |
/// well as `Affine` and `Std`, to the LLVM dialect for codegen. | |
std::unique_ptr<mlir::Pass> mlir::toy::createLowerToLLVMPass() { | |
return std::make_unique<ToyToLLVMLoweringPass>(); | |
} |
应用这个 Pass:
if (isLoweringToLLVM) { | |
// Finish lowering the toy IR to the LLVM dialect. | |
pm.addPass(mlir::toy::createLowerToLLVMPass()); | |
// This is necessary to have line tables emitted and basic | |
// debugger working. In the future we will add proper debug information | |
// emission directly from our frontend. | |
pm.addPass(mlir::LLVM::createDIScopeForLLVMFuncOpPass()); | |
} |
比较一下降级前后的差异性。降级之前的 toy dialect :
toy.func @main() { | |
%0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64> | |
%2 = toy.transpose(%0 : tensor<2x3xf64>) to tensor<3x2xf64> | |
%3 = toy.mul %2, %2 : tensor<3x2xf64> | |
toy.print %3 : tensor<3x2xf64> | |
toy.return | |
} |
降级到 llvm dialect :
llvm.func @free(!llvm<"i8*">) | |
llvm.func @printf(!llvm<"i8*">, ...) -> i32 | |
llvm.func @malloc(i64) -> !llvm<"i8*"> | |
llvm.func @main() { | |
%0 = llvm.mlir.constant(1.000000e+00 : f64) : f64 | |
%1 = llvm.mlir.constant(2.000000e+00 : f64) : f64 | |
... | |
^bb16: | |
%221 = llvm.extractvalue %25[0] : !llvm<"{ double*, i64, [2 x i64], [2 x i64] }"> | |
%222 = llvm.mlir.constant(0 : index) : i64 | |
%223 = llvm.mlir.constant(2 : index) : i64 | |
%224 = llvm.mul %214, %223 : i64 | |
%225 = llvm.add %222, %224 : i64 | |
%226 = llvm.mlir.constant(1 : index) : i64 | |
%227 = llvm.mul %219, %226 : i64 | |
%228 = llvm.add %225, %227 : i64 | |
%229 = llvm.getelementptr %221[%228] : (!llvm."double*">, i64) -> !llvm<"f64*"> | |
%230 = llvm.load %229 : !llvm<"double*"> | |
%231 = llvm.call @printf(%207, %230) : (!llvm<"i8*">, f64) -> i32 | |
%232 = llvm.add %219, %218 : i64 | |
llvm.br ^bb15(%232 : i64) | |
... | |
^bb18: | |
%235 = llvm.extractvalue %65[0] : !llvm<"{ double*, i64, [2 x i64], [2 x i64] }"> | |
%236 = llvm.bitcast %235 : !llvm<"double*"> to !llvm<"i8*"> | |
llvm.call @free(%236) : (!llvm<"i8*">) -> () | |
%237 = llvm.extractvalue %45[0] : !llvm<"{ double*, i64, [2 x i64], [2 x i64] }"> | |
%238 = llvm.bitcast %237 : !llvm<"double*"> to !llvm<"i8*"> | |
llvm.call @free(%238) : (!llvm<"i8*">) -> () | |
%239 = llvm.extractvalue %25[0] : !llvm<"{ double*, i64, [2 x i64], [2 x i64] }"> | |
%240 = llvm.bitcast %239 : !llvm<"double*"> to !llvm<"i8*"> | |
llvm.call @free(%240) : (!llvm<"i8*">) -> () | |
llvm.return | |
} |
# CodeGen
此时我们已处于代码生成的临界点。我们可以使用 LLVM 语法生成代码,现在只需将其导出为 LLVM IR,并设置一个即时编译器(JIT)来运行即可。那么将 llvm dialect 转为 llvm IR :
int dumpLLVMIR(mlir::ModuleOp module) { | |
// Translate the module, that contains the LLVM dialect, to LLVM IR. Use a | |
// fresh LLVM IR context. (Note that LLVM is not thread-safe and any | |
// concurrent use of a context requires external locking.) | |
llvm::LLVMContext llvmContext; | |
auto llvmModule = mlir::translateModuleToLLVMIR(module, llvmContext); | |
if (!llvmModule) { | |
llvm::errs() << "Failed to emit LLVM IR\n"; | |
return -1; | |
} | |
// Initialize LLVM targets. | |
llvm::InitializeNativeTarget(); | |
llvm::InitializeNativeTargetAsmPrinter(); | |
mlir::ExecutionEngine::setupTargetTriple(llvmModule.get()); | |
/// Optionally run an optimization pipeline over the llvm module. | |
auto optPipeline = mlir::makeOptimizingTransformer( | |
/*optLevel=*/EnableOpt ? 3 : 0, /*sizeLevel=*/0, | |
/*targetMachine=*/nullptr); | |
if (auto err = optPipeline(llvmModule.get())) { | |
llvm::errs() << "Failed to optimize LLVM IR " << err << "\n"; | |
return -1; | |
} | |
llvm::errs() << *llvmModule << "\n"; | |
return 0; | |
} |
转到 llvm ir 之后,打印部分内容:
define void @main() { | |
... | |
102: | |
%103 = extractvalue { double*, i64, [2 x i64], [2 x i64] } %8, 0 | |
%104 = mul i64 %96, 2 | |
%105 = add i64 0, %104 | |
%106 = mul i64 %100, 1 | |
%107 = add i64 %105, %106 | |
%108 = getelementptr double, double* %103, i64 %107 | |
%109 = memref.load double, double* %108 | |
%110 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double %109) | |
%111 = add i64 %100, 1 | |
cf.br label %99 | |
... | |
115: | |
%116 = extractvalue { double*, i64, [2 x i64], [2 x i64] } %24, 0 | |
%117 = bitcast double* %116 to i8* | |
call void @free(i8* %117) | |
%118 = extractvalue { double*, i64, [2 x i64], [2 x i64] } %16, 0 | |
%119 = bitcast double* %118 to i8* | |
call void @free(i8* %119) | |
%120 = extractvalue { double*, i64, [2 x i64], [2 x i64] } %8, 0 | |
%121 = bitcast double* %120 to i8* | |
call void @free(i8* %121) | |
ret void | |
} |
还可以对 llvm ir 启用优化,能够降低体积:
define void @main() | |
%0 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 1.000000e+00) | |
%1 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 1.600000e+01) | |
%putchar = tail call i32 @putchar(i32 10) | |
%2 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 4.000000e+00) | |
%3 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 2.500000e+01) | |
%putchar.1 = tail call i32 @putchar(i32 10) | |
%4 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 9.000000e+00) | |
%5 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([4 x i8], [4 x i8]* @frmt_spec, i64 0, i64 0), double 3.600000e+01) | |
%putchar.2 = tail call i32 @putchar(i32 10) | |
ret void | |
} |
# JIT 执行
int runJit(mlir::ModuleOp module) { | |
// Initialize LLVM targets. | |
llvm::InitializeNativeTarget(); | |
llvm::InitializeNativeTargetAsmPrinter(); | |
// An optimization pipeline to use within the execution engine. | |
auto optPipeline = mlir::makeOptimizingTransformer( | |
/*optLevel=*/EnableOpt ? 3 : 0, /*sizeLevel=*/0, | |
/*targetMachine=*/nullptr); | |
// Create an MLIR execution engine. The execution engine eagerly JIT-compiles | |
// the module. | |
auto maybeEngine = mlir::ExecutionEngine::create(module, | |
/*llvmModuleBuilder=*/nullptr, optPipeline); | |
assert(maybeEngine && "failed to construct an execution engine"); | |
auto &engine = maybeEngine.get(); | |
// Invoke the JIT-compiled function. | |
auto invocationResult = engine->invoke("main"); | |
if (invocationResult) { | |
llvm::errs() << "JIT invocation failed\n"; | |
return -1; | |
} | |
return 0; | |
} |
执行能够得到一下结果:
$ echo 'def main() { print([[1, 2], [3, 4]]); }' | ./bin/toyc-ch6 -emit=jit | |
1.000000 2.000000 | |
3.000000 4.000000 |
# 后记
While you're spending all this time on your own, building computers or practicing your cello, what you're really doing, is becoming interesting . | |
And when people finally do notice you, they'er gonna find someone a lot cooler than they thought. |
本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,看到一定及时回复!