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