1# Chapter 3: High-level Language-Specific Analysis and Transformation
2
3[TOC]
4
5Creating a dialect that closely represents the semantics of an input language
6enables analyses, transformations and optimizations in MLIR that require
7high-level language information and are generally performed on the language AST.
8For example, `clang` has a fairly
9[heavy mechanism](https://clang.llvm.org/doxygen/classclang_1_1TreeTransform.html)
10for performing template instantiation in C++.
11
12We divide compiler transformations into two categories: local and global. In
13this chapter, we focus on how to leverage the Toy Dialect and its high-level
14semantics to perform local pattern-match transformations that would be difficult
15in LLVM. For this, we use MLIR's
16[Generic DAG Rewriter](../../GenericDAGRewriter.md).
17
18There are two methods that can be used to implement pattern-match
19transformations: 1. Imperative, C++ pattern-match and rewrite 2. Declarative,
20rule-based pattern-match and rewrite using table-driven
21[Declarative Rewrite Rules](../../DeclarativeRewrites.md) (DRR). Note that the
22use of DRR requires that the operations be defined using ODS, as described in
23[Chapter 2](Ch-2.md).
24
25## Optimize Transpose using C++ style pattern-match and rewrite
26
27Let's start with a simple pattern and try to eliminate a sequence of two
28transposes that cancel out: `transpose(transpose(X)) -> X`. Here is the
29corresponding Toy example:
30
31```toy
32def transpose_transpose(x) {
33  return transpose(transpose(x));
34}
35```
36
37Which corresponds to the following IR:
38
39```mlir
40func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
41  %0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64>
42  %1 = toy.transpose(%0 : tensor<*xf64>) to tensor<*xf64>
43  toy.return %1 : tensor<*xf64>
44}
45```
46
47This is a good example of a transformation that is trivial to match on the Toy
48IR but that would be quite hard for LLVM to figure. For example, today Clang
49can't optimize away the temporary array, and the computation with the naive
50transpose is expressed with these loops:
51
52```c++
53#define N 100
54#define M 100
55
56void sink(void *);
57void double_transpose(int A[N][M]) {
58  int B[M][N];
59  for(int i = 0; i < N; ++i) {
60    for(int j = 0; j < M; ++j) {
61       B[j][i] = A[i][j];
62    }
63  }
64  for(int i = 0; i < N; ++i) {
65    for(int j = 0; j < M; ++j) {
66       A[i][j] = B[j][i];
67    }
68  }
69  sink(A);
70}
71```
72
73For a simple C++ approach to rewrite, involving matching a tree-like pattern in
74the IR and replacing it with a different set of operations, we can plug into the
75MLIR `Canonicalizer` pass by implementing a `RewritePattern`:
76
77```c++
78/// Fold transpose(transpose(x)) -> x
79struct SimplifyRedundantTranspose : public mlir::OpRewritePattern<TransposeOp> {
80  /// We register this pattern to match every toy.transpose in the IR.
81  /// The "benefit" is used by the framework to order the patterns and process
82  /// them in order of profitability.
83  SimplifyRedundantTranspose(mlir::MLIRContext *context)
84      : OpRewritePattern<TransposeOp>(context, /*benefit=*/1) {}
85
86  /// This method is attempting to match a pattern and rewrite it. The rewriter
87  /// argument is the orchestrator of the sequence of rewrites. It is expected
88  /// to interact with it to perform any changes to the IR from here.
89  mlir::LogicalResult
90  matchAndRewrite(TransposeOp op,
91                  mlir::PatternRewriter &rewriter) const override {
92    // Look through the input of the current transpose.
93    mlir::Value transposeInput = op.getOperand();
94    TransposeOp transposeInputOp =
95        llvm::dyn_cast_or_null<TransposeOp>(transposeInput.getDefiningOp());
96
97    // Input defined by another transpose? If not, no match.
98    if (!transposeInputOp)
99      return failure();
100
101    // Otherwise, we have a redundant transpose. Use the rewriter.
102    rewriter.replaceOp(op, {transposeInputOp.getOperand()}, {transposeInputOp});
103    return success();
104  }
105};
106```
107
108The implementation of this rewriter is in `ToyCombine.cpp`. The
109[canonicalization pass](../../Canonicalization.md) applies transformations
110defined by operations in a greedy, iterative manner. To ensure that the
111canonicalization pass applies our new transform, we set
112[hasCanonicalizer = 1](../../OpDefinitions.md#hascanonicalizer) and register the
113pattern with the canonicalization framework.
114
115```c++
116// Register our patterns for rewrite by the Canonicalization framework.
117void TransposeOp::getCanonicalizationPatterns(
118    OwningRewritePatternList &results, MLIRContext *context) {
119  results.insert<SimplifyRedundantTranspose>(context);
120}
121```
122
123We also need to update our main file, `toyc.cpp`, to add an optimization
124pipeline. In MLIR, the optimizations are run through a `PassManager` in a
125similar way to LLVM:
126
127```c++
128  mlir::PassManager pm(module.getContext());
129  pm.addNestedPass<mlir::FuncOp>(mlir::createCanonicalizerPass());
130```
131
132Finally, we can run `toyc-ch3 test/transpose_transpose.toy -emit=mlir -opt` and
133observe our pattern in action:
134
135```mlir
136func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
137  %0 = toy.transpose(%arg0 : tensor<*xf64>) to tensor<*xf64>
138  toy.return %arg0 : tensor<*xf64>
139}
140```
141
142As expected, we now directly return the function argument, bypassing any
143transpose operation. However, one of the transposes still hasn't been
144eliminated. That is not ideal! What happened is that our pattern replaced the
145last transform with the function input and left behind the now dead transpose
146input. The Canonicalizer knows to clean up dead operations; however, MLIR
147conservatively assumes that operations may have side-effects. We can fix this by
148adding a new trait, `NoSideEffect`, to our `TransposeOp`:
149
150```tablegen
151def TransposeOp : Toy_Op<"transpose", [NoSideEffect]> {...}
152```
153
154Let's retry now `toyc-ch3 test/transpose_transpose.toy -emit=mlir -opt`:
155
156```mlir
157func @transpose_transpose(%arg0: tensor<*xf64>) -> tensor<*xf64> {
158  toy.return %arg0 : tensor<*xf64>
159}
160```
161
162Perfect! No `transpose` operation is left - the code is optimal.
163
164In the next section, we use DRR for pattern match optimizations associated with
165the Reshape op.
166
167## Optimize Reshapes using DRR
168
169Declarative, rule-based pattern-match and rewrite (DRR) is an operation
170DAG-based declarative rewriter that provides a table-based syntax for
171pattern-match and rewrite rules:
172
173```tablegen
174class Pattern<
175    dag sourcePattern, list<dag> resultPatterns,
176    list<dag> additionalConstraints = [],
177    dag benefitsAdded = (addBenefit 0)>;
178```
179
180A redundant reshape optimization similar to SimplifyRedundantTranspose can be
181expressed more simply using DRR as follows:
182
183```tablegen
184// Reshape(Reshape(x)) = Reshape(x)
185def ReshapeReshapeOptPattern : Pat<(ReshapeOp(ReshapeOp $arg)),
186                                   (ReshapeOp $arg)>;
187```
188
189The automatically generated C++ code corresponding to each of the DRR patterns
190can be found under `path/to/BUILD/tools/mlir/examples/toy/Ch3/ToyCombine.inc`.
191
192DRR also provides a method for adding argument constraints when the
193transformation is conditional on some properties of the arguments and results.
194An example is a transformation that eliminates reshapes when they are redundant,
195i.e. when the input and output shapes are identical.
196
197```tablegen
198def TypesAreIdentical : Constraint<CPred<"$0.getType() == $1.getType()">>;
199def RedundantReshapeOptPattern : Pat<
200  (ReshapeOp:$res $arg), (replaceWithValue $arg),
201  [(TypesAreIdentical $res, $arg)]>;
202```
203
204Some optimizations may require additional transformations on instruction
205arguments. This is achieved using NativeCodeCall, which allows for more complex
206transformations either by calling into a C++ helper function or by using inline
207C++. An example of such an optimization is FoldConstantReshape, where we
208optimize Reshape of a constant value by reshaping the constant in place and
209eliminating the reshape operation.
210
211```tablegen
212def ReshapeConstant : NativeCodeCall<"$0.reshape(($1.getType()).cast<ShapedType>())">;
213def FoldConstantReshapeOptPattern : Pat<
214  (ReshapeOp:$res (ConstantOp $arg)),
215  (ConstantOp (ReshapeConstant $arg, $res))>;
216```
217
218We demonstrate these reshape optimizations using the following
219trivialReshape.toy program:
220
221```c++
222def main() {
223  var a<2,1> = [1, 2];
224  var b<2,1> = a;
225  var c<2,1> = b;
226  print(c);
227}
228```
229
230```mlir
231module {
232  func @main() {
233    %0 = toy.constant dense<[1.000000e+00, 2.000000e+00]> : tensor<2xf64>
234    %1 = toy.reshape(%0 : tensor<2xf64>) to tensor<2x1xf64>
235    %2 = toy.reshape(%1 : tensor<2x1xf64>) to tensor<2x1xf64>
236    %3 = toy.reshape(%2 : tensor<2x1xf64>) to tensor<2x1xf64>
237    toy.print %3 : tensor<2x1xf64>
238    toy.return
239  }
240}
241```
242
243We can try to run `toyc-ch3 test/trivialReshape.toy -emit=mlir -opt` and observe
244our pattern in action:
245
246```mlir
247module {
248  func @main() {
249    %0 = toy.constant dense<[[1.000000e+00], [2.000000e+00]]> : tensor<2x1xf64>
250    toy.print %0 : tensor<2x1xf64>
251    toy.return
252  }
253}
254```
255
256As expected, no reshape operations remain after canonicalization.
257
258Further details on the declarative rewrite method can be found at
259[Table-driven Declarative Rewrite Rule (DRR)](../../DeclarativeRewrites.md).
260
261In this chapter, we saw how to use certain core transformations through always
262available hooks. In the [next chapter](Ch-4.md), we will see how to use generic
263solutions that scale better through Interfaces.
264