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