1# Chapter 5: Partial Lowering to Lower-Level Dialects for Optimization 2 3[TOC] 4 5At this point, we are eager to generate actual code and see our Toy language 6take life. We will use LLVM to generate code, but just showing the LLVM builder 7interface here wouldn't be very exciting. Instead, we will show how to perform 8progressive lowering through a mix of dialects coexisting in the same function. 9 10To make it more interesting, in this chapter we will consider that we want to 11reuse existing optimizations implemented in a dialect optimizing affine 12transformations: `Affine`. This dialect is tailored to the computation-heavy 13part of the program and is limited: it doesn't support representing our 14`toy.print` builtin, for instance, neither should it! Instead, we can target 15`Affine` for the computation heavy part of Toy, and in the 16[next chapter](Ch-6.md) directly target the `LLVM IR` dialect for lowering 17`print`. As part of this lowering, we will be lowering from the 18[TensorType](../../Dialects/Builtin.md/#rankedtensortype) that `Toy` operates on 19to the [MemRefType](../../Dialects/Builtin.md/#memreftype) that is indexed via 20an affine loop-nest. Tensors represent an abstract value-typed sequence of data, 21meaning that they don't live in any memory. MemRefs, on the other hand, 22represent lower level buffer access, as they are concrete references to a region 23of memory. 24 25# Dialect Conversions 26 27MLIR has many different dialects, so it is important to have a unified framework 28for [converting](../../../getting_started/Glossary.md/#conversion) between them. 29This is where the `DialectConversion` framework comes into play. This framework 30allows for transforming a set of *illegal* operations to a set of *legal* ones. 31To use this framework, we need to provide two things (and an optional third): 32 33* A [Conversion Target](../../DialectConversion.md/#conversion-target) 34 35 - This is the formal specification of what operations or dialects are 36 legal for the conversion. Operations that aren't legal will require 37 rewrite patterns to perform 38 [legalization](../../../getting_started/Glossary.md/#legalization). 39 40* A set of 41 [Rewrite Patterns](../../DialectConversion.md/#rewrite-pattern-specification) 42 43 - This is the set of [patterns](../QuickstartRewrites.md) used to convert 44 *illegal* operations into a set of zero or more *legal* ones. 45 46* Optionally, a [Type Converter](../../DialectConversion.md/#type-conversion). 47 48 - If provided, this is used to convert the types of block arguments. We 49 won't be needing this for our conversion. 50 51## Conversion Target 52 53For our purposes, we want to convert the compute-intensive `Toy` operations into 54a combination of operations from the `Affine`, `MemRef` and `Standard` dialects 55for further optimization. To start off the lowering, we first define our 56conversion target: 57 58```c++ 59void ToyToAffineLoweringPass::runOnOperation() { 60 // The first thing to define is the conversion target. This will define the 61 // final target for this lowering. 62 mlir::ConversionTarget target(getContext()); 63 64 // We define the specific operations, or dialects, that are legal targets for 65 // this lowering. In our case, we are lowering to a combination of the 66 // `Affine`, `Arithmetic`, `MemRef`, and `Standard` dialects. 67 target.addLegalDialect<AffineDialect, arith::ArithmeticDialect, 68 memref::MemRefDialect, StandardOpsDialect>(); 69 70 // We also define the Toy dialect as Illegal so that the conversion will fail 71 // if any of these operations are *not* converted. Given that we actually want 72 // a partial lowering, we explicitly mark the Toy operations that don't want 73 // to lower, `toy.print`, as *legal*. `toy.print` will still need its operands 74 // to be updated though (as we convert from TensorType to MemRefType), so we 75 // only treat it as `legal` if its operands are legal. 76 target.addIllegalDialect<ToyDialect>(); 77 target.addDynamicallyLegalOp<toy::PrintOp>([](toy::PrintOp op) { 78 return llvm::none_of(op->getOperandTypes(), 79 [](Type type) { return type.isa<TensorType>(); }); 80 }); 81 ... 82} 83``` 84 85Above, we first set the toy dialect to illegal, and then the print operation as 86legal. We could have done this the other way around. Individual operations 87always take precedence over the (more generic) dialect definitions, so the order 88doesn't matter. See `ConversionTarget::getOpInfo` for the details. 89 90## Conversion Patterns 91 92After the conversion target has been defined, we can define how to convert the 93*illegal* operations into *legal* ones. Similarly to the canonicalization 94framework introduced in [chapter 3](Ch-3.md), the 95[`DialectConversion` framework](../../DialectConversion.md) also uses 96[RewritePatterns](../QuickstartRewrites.md) to perform the conversion logic. 97These patterns may be the `RewritePatterns` seen before or a new type of pattern 98specific to the conversion framework `ConversionPattern`. `ConversionPatterns` 99are different from traditional `RewritePatterns` in that they accept an 100additional `operands` parameter containing operands that have been 101remapped/replaced. This is used when dealing with type conversions, as the 102pattern will want to operate on values of the new type but match against the 103old. For our lowering, this invariant will be useful as it translates from the 104[TensorType](../../Dialects/Builtin.md/#rankedtensortype) currently being 105operated on to the [MemRefType](../../Dialects/Builtin.md/#memreftype). Let's 106look at a snippet of lowering the `toy.transpose` operation: 107 108```c++ 109/// Lower the `toy.transpose` operation to an affine loop nest. 110struct TransposeOpLowering : public mlir::ConversionPattern { 111 TransposeOpLowering(mlir::MLIRContext *ctx) 112 : mlir::ConversionPattern(TransposeOp::getOperationName(), 1, ctx) {} 113 114 /// Match and rewrite the given `toy.transpose` operation, with the given 115 /// operands that have been remapped from `tensor<...>` to `memref<...>`. 116 mlir::LogicalResult 117 matchAndRewrite(mlir::Operation *op, ArrayRef<mlir::Value> operands, 118 mlir::ConversionPatternRewriter &rewriter) const final { 119 auto loc = op->getLoc(); 120 121 // Call to a helper function that will lower the current operation to a set 122 // of affine loops. We provide a functor that operates on the remapped 123 // operands, as well as the loop induction variables for the inner most 124 // loop body. 125 lowerOpToLoops( 126 op, operands, rewriter, 127 [loc](mlir::PatternRewriter &rewriter, 128 ArrayRef<mlir::Value> memRefOperands, 129 ArrayRef<mlir::Value> loopIvs) { 130 // Generate an adaptor for the remapped operands of the TransposeOp. 131 // This allows for using the nice named accessors that are generated 132 // by the ODS. This adaptor is automatically provided by the ODS 133 // framework. 134 TransposeOpAdaptor transposeAdaptor(memRefOperands); 135 mlir::Value input = transposeAdaptor.input(); 136 137 // Transpose the elements by generating a load from the reverse 138 // indices. 139 SmallVector<mlir::Value, 2> reverseIvs(llvm::reverse(loopIvs)); 140 return rewriter.create<mlir::AffineLoadOp>(loc, input, reverseIvs); 141 }); 142 return success(); 143 } 144}; 145``` 146 147Now we can prepare the list of patterns to use during the lowering process: 148 149```c++ 150void ToyToAffineLoweringPass::runOnOperation() { 151 ... 152 153 // Now that the conversion target has been defined, we just need to provide 154 // the set of patterns that will lower the Toy operations. 155 mlir::RewritePatternSet patterns(&getContext()); 156 patterns.add<..., TransposeOpLowering>(&getContext()); 157 158 ... 159``` 160 161## Partial Lowering 162 163Once the patterns have been defined, we can perform the actual lowering. The 164`DialectConversion` framework provides several different modes of lowering, but, 165for our purposes, we will perform a partial lowering, as we will not convert 166`toy.print` at this time. 167 168```c++ 169void ToyToAffineLoweringPass::runOnOperation() { 170 ... 171 172 // With the target and rewrite patterns defined, we can now attempt the 173 // conversion. The conversion will signal failure if any of our *illegal* 174 // operations were not converted successfully. 175 mlir::FuncOp function = getOperation(); 176 if (mlir::failed(mlir::applyPartialConversion(function, target, patterns))) 177 signalPassFailure(); 178} 179``` 180 181### Design Considerations With Partial Lowering 182 183Before diving into the result of our lowering, this is a good time to discuss 184potential design considerations when it comes to partial lowering. In our 185lowering, we transform from a value-type, TensorType, to an allocated 186(buffer-like) type, MemRefType. However, given that we do not lower the 187`toy.print` operation, we need to temporarily bridge these two worlds. There are 188many ways to go about this, each with their own tradeoffs: 189 190* Generate `load` operations from the buffer 191 192 One option is to generate `load` operations from the buffer type to 193 materialize an instance of the value type. This allows for the definition of 194 the `toy.print` operation to remain unchanged. The downside to this approach 195 is that the optimizations on the `affine` dialect are limited, because the 196 `load` will actually involve a full copy that is only visible *after* our 197 optimizations have been performed. 198 199* Generate a new version of `toy.print` that operates on the lowered type 200 201 Another option would be to have another, lowered, variant of `toy.print` 202 that operates on the lowered type. The benefit of this option is that there 203 is no hidden, unnecessary copy to the optimizer. The downside is that 204 another operation definition is needed that may duplicate many aspects of 205 the first. Defining a base class in [ODS](../../OpDefinitions.md) may 206 simplify this, but you still need to treat these operations separately. 207 208* Update `toy.print` to allow for operating on the lowered type 209 210 A third option is to update the current definition of `toy.print` to allow 211 for operating the on the lowered type. The benefit of this approach is that 212 it is simple, does not introduce an additional hidden copy, and does not 213 require another operation definition. The downside to this option is that it 214 requires mixing abstraction levels in the `Toy` dialect. 215 216For the sake of simplicity, we will use the third option for this lowering. This 217involves updating the type constraints on the PrintOp in the operation 218definition file: 219 220```tablegen 221def PrintOp : Toy_Op<"print"> { 222 ... 223 224 // The print operation takes an input tensor to print. 225 // We also allow a F64MemRef to enable interop during partial lowering. 226 let arguments = (ins AnyTypeOf<[F64Tensor, F64MemRef]>:$input); 227} 228``` 229 230## Complete Toy Example 231 232Let's take a concrete example: 233 234```mlir 235func @main() { 236 %0 = toy.constant dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64> 237 %2 = toy.transpose(%0 : tensor<2x3xf64>) to tensor<3x2xf64> 238 %3 = toy.mul %2, %2 : tensor<3x2xf64> 239 toy.print %3 : tensor<3x2xf64> 240 toy.return 241} 242``` 243 244With affine lowering added to our pipeline, we can now generate: 245 246```mlir 247func @main() { 248 %cst = arith.constant 1.000000e+00 : f64 249 %cst_0 = arith.constant 2.000000e+00 : f64 250 %cst_1 = arith.constant 3.000000e+00 : f64 251 %cst_2 = arith.constant 4.000000e+00 : f64 252 %cst_3 = arith.constant 5.000000e+00 : f64 253 %cst_4 = arith.constant 6.000000e+00 : f64 254 255 // Allocating buffers for the inputs and outputs. 256 %0 = memref.alloc() : memref<3x2xf64> 257 %1 = memref.alloc() : memref<3x2xf64> 258 %2 = memref.alloc() : memref<2x3xf64> 259 260 // Initialize the input buffer with the constant values. 261 affine.store %cst, %2[0, 0] : memref<2x3xf64> 262 affine.store %cst_0, %2[0, 1] : memref<2x3xf64> 263 affine.store %cst_1, %2[0, 2] : memref<2x3xf64> 264 affine.store %cst_2, %2[1, 0] : memref<2x3xf64> 265 affine.store %cst_3, %2[1, 1] : memref<2x3xf64> 266 affine.store %cst_4, %2[1, 2] : memref<2x3xf64> 267 268 // Load the transpose value from the input buffer and store it into the 269 // next input buffer. 270 affine.for %arg0 = 0 to 3 { 271 affine.for %arg1 = 0 to 2 { 272 %3 = affine.load %2[%arg1, %arg0] : memref<2x3xf64> 273 affine.store %3, %1[%arg0, %arg1] : memref<3x2xf64> 274 } 275 } 276 277 // Multiply and store into the output buffer. 278 affine.for %arg0 = 0 to 3 { 279 affine.for %arg1 = 0 to 2 { 280 %3 = affine.load %1[%arg0, %arg1] : memref<3x2xf64> 281 %4 = affine.load %1[%arg0, %arg1] : memref<3x2xf64> 282 %5 = arith.mulf %3, %4 : f64 283 affine.store %5, %0[%arg0, %arg1] : memref<3x2xf64> 284 } 285 } 286 287 // Print the value held by the buffer. 288 toy.print %0 : memref<3x2xf64> 289 memref.dealloc %2 : memref<2x3xf64> 290 memref.dealloc %1 : memref<3x2xf64> 291 memref.dealloc %0 : memref<3x2xf64> 292 return 293} 294``` 295 296## Taking Advantage of Affine Optimization 297 298Our naive lowering is correct, but it leaves a lot to be desired with regards to 299efficiency. For example, the lowering of `toy.mul` has generated some redundant 300loads. Let's look at how adding a few existing optimizations to the pipeline can 301help clean this up. Adding the `LoopFusion` and `MemRefDataFlowOpt` passes to 302the pipeline gives the following result: 303 304```mlir 305func @main() { 306 %cst = arith.constant 1.000000e+00 : f64 307 %cst_0 = arith.constant 2.000000e+00 : f64 308 %cst_1 = arith.constant 3.000000e+00 : f64 309 %cst_2 = arith.constant 4.000000e+00 : f64 310 %cst_3 = arith.constant 5.000000e+00 : f64 311 %cst_4 = arith.constant 6.000000e+00 : f64 312 313 // Allocating buffers for the inputs and outputs. 314 %0 = memref.alloc() : memref<3x2xf64> 315 %1 = memref.alloc() : memref<2x3xf64> 316 317 // Initialize the input buffer with the constant values. 318 affine.store %cst, %1[0, 0] : memref<2x3xf64> 319 affine.store %cst_0, %1[0, 1] : memref<2x3xf64> 320 affine.store %cst_1, %1[0, 2] : memref<2x3xf64> 321 affine.store %cst_2, %1[1, 0] : memref<2x3xf64> 322 affine.store %cst_3, %1[1, 1] : memref<2x3xf64> 323 affine.store %cst_4, %1[1, 2] : memref<2x3xf64> 324 325 affine.for %arg0 = 0 to 3 { 326 affine.for %arg1 = 0 to 2 { 327 // Load the transpose value from the input buffer. 328 %2 = affine.load %1[%arg1, %arg0] : memref<2x3xf64> 329 330 // Multiply and store into the output buffer. 331 %3 = arith.mulf %2, %2 : f64 332 affine.store %3, %0[%arg0, %arg1] : memref<3x2xf64> 333 } 334 } 335 336 // Print the value held by the buffer. 337 toy.print %0 : memref<3x2xf64> 338 memref.dealloc %1 : memref<2x3xf64> 339 memref.dealloc %0 : memref<3x2xf64> 340 return 341} 342``` 343 344Here, we can see that a redundant allocation was removed, the two loop nests 345were fused, and some unnecessary `load`s were removed. You can build `toyc-ch5` 346and try yourself: `toyc-ch5 test/Examples/Toy/Ch5/affine-lowering.mlir 347-emit=mlir-affine`. We can also check our optimizations by adding `-opt`. 348 349In this chapter we explored some aspects of partial lowering, with the intent to 350optimize. In the [next chapter](Ch-6.md) we will continue the discussion about 351dialect conversion by targeting LLVM for code generation. 352