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