I have been digging into some of the SCF canonicalization and transformation passes, and I have a question about the general design choice of some of these patterns. Mainly, when do we create a rewrite pattern and apply it as a canonicalization, when do we just use a rewrite pattern as a pass, and when do we use a more generalized function/operation pass that does some rewriting?
For example, the canonicalizations in SCF.cpp (say SimplifyTrivialLoops) use the pattern rewriter, but LoopInvariantCodeMotion does not. It also looks like the former is not inserted into the MLIR lowering pipeline at any point I can see. Is it instead meant to be run by clients of MLIR who decide when to perform the loop invariant code motion operation, whereas the canonicalizations are meant to be used both within/external to various MLIR lowerings?
Pattern rewriter is a general IR transformation utility. It is orthogonal to passes the same way, e.g.,
OpOperand::set and other IR mutation API are: passes may choose to use this utility or any other way of mutating IR to achieve their goal. There are also different drivers for applying patterns and different kinds of patterns.
The canonicalization pass chose PatternRewriter because it gives the pass an easy extensibility mechanism: each operation provides a set of patterns so the pass does not need to know about all existing operations, which would have been impossible in MLIR.
Other passes may choose to use a rewriter that fits their bill, it’s usually up to whoever writes the pass to decide what to use and to justify it if asked in review. Some transformations are quite natural to express using patterns, for example removing a known zero-iteration loop, some others are not, for example inlining that requires some sort of non-local legality and profitability analysis.
There is no “MLIR lowering pipeline”. Everybody gets to build their own.
Same for canonicalization, it’s a pass just like others. Every client decides when to call it in their pass pipeline. However, some passes may subsume the application of canonicalization patterns to a subset of operations.
Another note on canonicalization: canonicalizations should be always-desirable to apply. They should either make the IR strictly simpler, or move it towards some canonical form. The idea is to reduce the complexity and variability of IR that other passes need to deal with. For example, a conditional branch on a constant is transformed into an unconditional branch, or a constant operand to std.addi is always moved to be the second operand. One of the restrictions on the canonicalizer pass is that the transformations have to be expressible as a rewrite pattern (for the reasons Alex explains). There’s the even more restricted folding API, which is applied in even more places, but similarly should only be used for strictly simplifying operations that should always be applied.
But there are many transformations that don’t fall into this category where the decision when, how, and whether to apply them needs to be in the control of a pipeline author, so not all rewrite patterns are canonicalization patterns.
Thanks @ftynse. I think I saw some of the conversions, e.g., convert-linalg-to-loops, and just assumed there was some standard lowering pipeline, but what you described makes much more sense.
Makes sense, and helps me reason about whether a transform belongs as a canonicalization or not.
Thanks for the explanations. I am messing around with a pass on SCF (I will post about it shortly) and this helps clarify things.