blob: bf47643676c2f7a70d88f873e3818a6e4216b6d5 [file] [view] [edit]
# Chapter 6: Lowering to LLVM and CodeGeneration
[TOC]
In the [previous chapter](Ch-5.md), we introduced the
[dialect conversion](../../DialectConversion.md) framework and partially lowered
many of the `Toy` operations to affine loop nests for optimization. In this
chapter, we will finally lower to LLVM for code generation.
## Lowering to LLVM
For this lowering, we will again use the dialect conversion framework to perform
the heavy lifting. However, this time, we will be performing a full conversion
to the [LLVM dialect](../../Dialects/LLVM.md). Thankfully, we have already
lowered all but one of the `toy` operations, with the last being `toy.print`.
Before going over the conversion to LLVM, let's lower the `toy.print` operation.
We will lower this operation to a non-affine loop nest that invokes `printf` for
each element. Note that, because the dialect conversion framework supports
[transitive lowering](../../../getting_started/Glossary.md/#transitive-lowering),
we don't need to directly emit operations in the LLVM dialect. By transitive
lowering, we mean that the conversion framework may apply multiple patterns to
fully legalize an operation. In this example, we are generating a structured
loop nest instead of the branch-form in the LLVM dialect. As long as we then
have a lowering from the loop operations to LLVM, the lowering will still
succeed.
During lowering we can get, or build, the declaration for printf as so:
```c++
/// Create a function declaration for printf, the signature is:
/// * `i32 (ptr, ...)`
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");
}
```
Now that the lowering for the printf operation has been defined, we can specify
the components necessary for the lowering. These are largely the same as the
components defined in the [previous chapter](Ch-5.md).
### Conversion Target
For this conversion, aside from the top-level module, we will be lowering
everything to the LLVM dialect. `LLVMConversionTarget` is a `ConversionTarget`
that already marks the LLVM dialect as legal.
```c++
mlir::LLVMConversionTarget target(getContext());
target.addLegalOp<mlir::ModuleOp>();
```
### Type Converter
This lowering will also transform the MemRef types which are currently being
operated on into a representation in LLVM. To perform this conversion, we use a
TypeConverter as part of the lowering. This converter specifies how one type
maps to another. This is necessary now that we are performing more complicated
lowerings involving block arguments. Given that we don't have any
Toy-dialect-specific types that need to be lowered, the default converter is
enough for our use case.
```c++
LLVMTypeConverter typeConverter(&getContext());
```
### Conversion Patterns
Now that the conversion target has been defined, we need to provide the patterns
used for lowering. At this point in the compilation process, we have a
combination of `toy`, `affine`, `arith`, `memref`, and `func` operations.
Luckily, the `affine`, `arith`, `memref`, and `func` dialects already provide
the set of patterns needed to transform them into LLVM dialect. These patterns
allow for lowering the IR in multiple stages by relying on
[transitive lowering](../../../getting_started/Glossary.md/#transitive-lowering).
```c++
mlir::RewritePatternSet patterns(&getContext());
mlir::populateAffineToStdConversionPatterns(patterns);
mlir::populateSCFToControlFlowConversionPatterns(patterns);
mlir::arith::populateArithToLLVMConversionPatterns(typeConverter, patterns);
mlir::populateFinalizeMemRefToLLVMConversionPatterns(typeConverter, patterns);
mlir::cf::populateControlFlowToLLVMConversionPatterns(typeConverter, patterns);
mlir::populateFuncToLLVMConversionPatterns(typeConverter, patterns);
// The only remaining operation, to lower from the `toy` dialect, is the
// PrintOp.
patterns.add<PrintOpLowering>(&getContext());
```
### Full Lowering
We want to completely lower to LLVM, so we use a `FullConversion`. This ensures
that only legal operations will remain after the conversion.
```c++
mlir::ModuleOp module = getOperation();
if (mlir::failed(
mlir::applyFullConversion(module, target, std::move(patterns))))
signalPassFailure();
```
Looking back at our current working example:
```mlir
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
}
```
We can now lower down to the LLVM dialect, which produces the following code:
```mlir
llvm.func @free(!llvm.ptr)
llvm.mlir.global internal constant @nl("\0A\00") {addr_space = 0 : i32}
llvm.mlir.global internal constant @frmt_spec("%f \00") {addr_space = 0 : i32}
llvm.func @printf(!llvm.ptr, ...) -> i32
llvm.func @malloc(i64) -> !llvm.ptr
llvm.func @main() {
%0 = llvm.mlir.constant(6.000000e+00 : f64) : f64
%1 = llvm.mlir.constant(5.000000e+00 : f64) : f64
%2 = llvm.mlir.constant(4.000000e+00 : f64) : f64
%3 = llvm.mlir.constant(3.000000e+00 : f64) : f64
%4 = llvm.mlir.constant(2.000000e+00 : f64) : f64
%5 = llvm.mlir.constant(1.000000e+00 : f64) : f64
...
^bb16: // pred: ^bb15
%162 = llvm.extractvalue %22[1] : !llvm.struct<(ptr, ptr, i64, array<2 x i64>, array<2 x i64>)>
%163 = llvm.mlir.constant(2 : i64) : i64
%164 = llvm.mul %155, %163 overflow<nsw, nuw> : i64
%165 = llvm.add %164, %160 overflow<nsw, nuw> : i64
%166 = llvm.getelementptr inbounds|nuw %162[%165] : (!llvm.ptr, i64) -> !llvm.ptr, f64
%167 = llvm.load %166 : !llvm.ptr -> f64
%168 = llvm.call @printf(%148, %167) vararg(!llvm.func<i32 (ptr, ...)>) : (!llvm.ptr, f64) -> i32
%169 = llvm.add %160, %159 : i64
llvm.br ^bb15(%169 : i64)
...
^bb18: // pred: ^bb13
%172 = llvm.extractvalue %56[0] : !llvm.struct<(ptr, ptr, i64, array<2 x i64>, array<2 x i64>)>
llvm.call @free(%172) : (!llvm.ptr) -> ()
%173 = llvm.extractvalue %39[0] : !llvm.struct<(ptr, ptr, i64, array<2 x i64>, array<2 x i64>)>
llvm.call @free(%173) : (!llvm.ptr) -> ()
%174 = llvm.extractvalue %22[0] : !llvm.struct<(ptr, ptr, i64, array<2 x i64>, array<2 x i64>)>
llvm.call @free(%174) : (!llvm.ptr) -> ()
llvm.return
}
```
Each `memref` value has been lowered to an LLVM struct, the memref descriptor,
holding the allocated pointer, the aligned pointer, an offset, and the size and
stride of each dimension. See
[LLVM IR Target](../../TargetLLVMIR.md/#ranked-memref-types) for more in-depth
details on lowering to the LLVM dialect.
## CodeGen: Getting Out of MLIR
At this point we are right at the cusp of code generation. We can generate code
in the LLVM dialect, so now we just need to export to LLVM IR and setup a JIT to
run it.
### Emitting LLVM IR
Now that our module is comprised only of operations in the LLVM dialect, we can
export to LLVM IR. To do this programmatically, we can invoke the following
utility:
```c++
llvm::LLVMContext llvmContext;
std::unique_ptr<llvm::Module> llvmModule =
mlir::translateModuleToLLVMIR(module, llvmContext);
if (!llvmModule)
/* ... an error was encountered ... */
```
Exporting our module to LLVM IR generates the following. The target triple,
data layout, and debug location metadata have been elided for brevity:
```llvm
@nl = internal constant [2 x i8] c"\0A\00"
@frmt_spec = internal constant [4 x i8] c"%f \00"
declare void @free(ptr)
declare i32 @printf(ptr, ...)
declare ptr @malloc(i64)
define void @main() {
...
87: ; preds = %84
%88 = extractvalue { ptr, ptr, i64, [2 x i64], [2 x i64] } %8, 1
%89 = mul nuw nsw i64 %81, 2
%90 = add nuw nsw i64 %89, %85
%91 = getelementptr inbounds nuw double, ptr %88, i64 %90
%92 = load double, ptr %91, align 8
%93 = call i32 (ptr, ...) @printf(ptr @frmt_spec, double %92)
%94 = add i64 %85, 1
br label %84
...
98: ; preds = %80
%99 = extractvalue { ptr, ptr, i64, [2 x i64], [2 x i64] } %24, 0
call void @free(ptr %99)
%100 = extractvalue { ptr, ptr, i64, [2 x i64], [2 x i64] } %16, 0
call void @free(ptr %100)
%101 = extractvalue { ptr, ptr, i64, [2 x i64], [2 x i64] } %8, 0
call void @free(ptr %101)
ret void
}
```
If we enable optimization on the generated LLVM IR, we can trim this down quite
a bit:
```llvm
@frmt_spec = internal constant [4 x i8] c"%f \00"
; Function Attrs: nofree nounwind
declare noundef i32 @printf(ptr noundef readonly captures(none), ...) local_unnamed_addr #0
; Function Attrs: nofree nounwind
define void @main() local_unnamed_addr #0 {
.preheader5:
%0 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 1.000000e+00)
%1 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 1.600000e+01)
%putchar = tail call i32 @putchar(i32 10)
%2 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 4.000000e+00)
%3 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 2.500000e+01)
%putchar.1 = tail call i32 @putchar(i32 10)
%4 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 9.000000e+00)
%5 = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @frmt_spec, double 3.600000e+01)
%putchar.2 = tail call i32 @putchar(i32 10)
ret void
}
; Function Attrs: nofree nounwind
declare noundef i32 @putchar(i32 noundef) local_unnamed_addr #0
attributes #0 = { nofree nounwind }
```
The full code listing for dumping LLVM IR can be found in
`examples/toy/Ch6/toyc.cpp` in the `dumpLLVMIR()` function:
```c++
static int dumpLLVMIR(mlir::ModuleOp module) {
// Register the translation to LLVM IR with the MLIR context.
mlir::registerBuiltinDialectTranslation(*module->getContext());
mlir::registerLLVMDialectTranslation(*module->getContext());
// Convert the module to LLVM IR in a new LLVM IR context.
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();
// Configure the LLVM Module
auto tmBuilderOrError = llvm::orc::JITTargetMachineBuilder::detectHost();
if (!tmBuilderOrError) {
llvm::errs() << "Could not create JITTargetMachineBuilder\n";
return -1;
}
auto tmOrError = tmBuilderOrError->createTargetMachine();
if (!tmOrError) {
llvm::errs() << "Could not create TargetMachine\n";
return -1;
}
mlir::ExecutionEngine::setupTargetTripleAndDataLayout(llvmModule.get(),
tmOrError.get().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;
}
```
### Setting up a JIT
Setting up a JIT to run the module containing the LLVM dialect can be done using
the `mlir::ExecutionEngine` infrastructure. This is a utility wrapper around
LLVM's JIT that accepts `.mlir` as input. The full code listing for setting up
the JIT can be found in `Ch6/toyc.cpp` in the `runJit()` function:
```c++
static int runJit(mlir::ModuleOp module) {
// Initialize LLVM targets.
llvm::InitializeNativeTarget();
llvm::InitializeNativeTargetAsmPrinter();
// Register the translation from MLIR to LLVM IR, which must happen before we
// can JIT-compile.
mlir::registerBuiltinDialectTranslation(*module->getContext());
mlir::registerLLVMDialectTranslation(*module->getContext());
// 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.
mlir::ExecutionEngineOptions engineOptions;
engineOptions.transformer = optPipeline;
auto maybeEngine = mlir::ExecutionEngine::create(module, engineOptions);
assert(maybeEngine && "failed to construct an execution engine");
auto &engine = maybeEngine.get();
// Invoke the JIT-compiled function.
auto invocationResult = engine->invokePacked("main");
if (invocationResult) {
llvm::errs() << "JIT invocation failed\n";
return -1;
}
return 0;
}
```
You can play around with it from the build directory:
```shell
$ echo 'def main() { print([[1, 2], [3, 4]]); }' | ./bin/toyc-ch6 -emit=jit
1.000000 2.000000
3.000000 4.000000
```
You can also play with `-emit=mlir`, `-emit=mlir-affine`, `-emit=mlir-llvm`, and
`-emit=llvm` to compare the various levels of IR involved. Also try options like
[`--mlir-print-ir-after-all`](../../PassManagement.md/#ir-printing) to track the
evolution of the IR throughout the pipeline.
The example code used throughout this section can be found in
test/Examples/Toy/Ch6/llvm-lowering.mlir.
So far, we have worked with primitive data types. In the
[next chapter](Ch-7.md), we will add a composite `struct` type.