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