Skip to content

Commit 565e9fa

Browse files
[mlir][docs] Add documentation for No-rollback Conversion Driver (#164071)
Add documentation for the no-rollback conversion driver. Also improve the documentation of the old rollback driver. In particular: which modifications are performed immediately and which are delayed.
1 parent 5ac616f commit 565e9fa

File tree

1 file changed

+133
-22
lines changed

1 file changed

+133
-22
lines changed

mlir/docs/DialectConversion.md

Lines changed: 133 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@ target.markOpRecursivelyLegal<MyOp>([](MyOp op) { ... });
153153

154154
After the conversion target has been defined, a set of legalization patterns
155155
must be provided to transform illegal operations into legal ones. The patterns
156-
supplied here have the same structure and restrictions as those described in the
157-
main [Pattern](PatternRewriter.md) documentation. The patterns provided do not
158-
need to generate operations that are directly legal on the target. The framework
159-
will automatically build a graph of conversions to convert non-legal operations
160-
into a set of legal ones.
156+
supplied here have the same structure and similar restrictions as those
157+
described in the main [Pattern](PatternRewriter.md) documentation. The patterns
158+
provided do not need to generate operations that are directly legal on the
159+
target. The framework will automatically build a graph of conversions to convert
160+
non-legal operations into a set of legal ones.
161161

162162
As an example, say you define a target that supports one operation: `foo.add`.
163163
When providing the following patterns: [`bar.add` -> `baz.add`, `baz.add` ->
@@ -171,23 +171,12 @@ means that you don’t have to define a direct legalization pattern for `bar.add
171171
Along with the general `RewritePattern` classes, the conversion framework
172172
provides a special type of rewrite pattern that can be used when a pattern
173173
relies on interacting with constructs specific to the conversion process, the
174-
`ConversionPattern`. For example, the conversion process does not necessarily
175-
update operations in-place and instead creates a mapping of events such as
176-
replacements and erasures, and only applies them when the entire conversion
177-
process is successful. Certain classes of patterns rely on using the
178-
updated/remapped operands of an operation, such as when the types of results
179-
defined by an operation have changed. The general Rewrite Patterns can no longer
180-
be used in these situations, as the types of the operands of the operation being
181-
matched will not correspond with those expected by the user. This pattern
182-
provides, as an additional argument to the `matchAndRewrite` method, the list
183-
of operands that the operation should use after conversion. If an operand was
184-
the result of a non-converted operation, for example if it was already legal,
185-
the original operand is used. This means that the operands provided always have
186-
a 1-1 non-null correspondence with the operands on the operation. The original
187-
operands of the operation are still intact and may be inspected as normal.
188-
These patterns also utilize a special `PatternRewriter`,
189-
`ConversionPatternRewriter`, that provides special hooks for use with the
190-
conversion infrastructure.
174+
`ConversionPattern`.
175+
176+
#### Remapped Operands / Adaptor
177+
Conversion patterns have an additional `operands` / `adaptor` argument for the
178+
`matchAndRewrite` method. These operands correspond to the most recent
179+
replacement values of the respective operands of the matched operation.
191180

192181
```c++
193182
struct MyConversionPattern : public ConversionPattern {
@@ -200,6 +189,128 @@ struct MyConversionPattern : public ConversionPattern {
200189
};
201190
```
202191
192+
Example:
193+
194+
```mlir
195+
%0 = "test.foo"() : () -> i1 // matched by pattern A
196+
"test.bar"(%0) : (i1) -> () // matched by pattern B
197+
```
198+
199+
Let's assume that the two patterns are applied back-to-back: first, pattern A
200+
replaces `"test.foo"` with `"test.qux"`, an op that has a different result
201+
type. The dialect conversion infrastructure has special support for such
202+
type-changing IR modifications.
203+
204+
```mlir
205+
%0 = "test.qux"() : () -> i2
206+
%r = builtin.unrealized_conversion_cast %0 : i2 to i1
207+
"test.bar"(%r) : (i1) -> ()
208+
```
209+
210+
Simply swapping out the operand of `"test.bar"` during the `replaceOp` call
211+
would be unsafe, because that would change the type of operand and, therefore,
212+
potentially the semantics of the operation. Instead, the dialect conversion
213+
driver (conceptually) inserts a `builtin.unrealized_conversion_cast` op that
214+
connects the newly created `"test.qux"` op with the `"test.bar"` op, without
215+
changing the types of the latter one.
216+
217+
Now, the second pattern B is applied. The `operands` argument contains an SSA
218+
value with the most recent replacement type (`%0` with type `i2`), whereas
219+
querying the operand from the matched op still returns an SSA value with the
220+
original operand type `i1`.
221+
222+
Note: If the conversion pattern is instantiated with a type converter, the
223+
`operands` argument contains SSA values whose types match the legalized operand
224+
types as per the type converter. See [Type Safety](#type-safety) for more
225+
details.
226+
227+
Note: The dialect conversion framework does not guarantee the presence of any
228+
particular value in the `operands` argument. The only thing that's guaranteed
229+
is the type of the `operands` SSA values. E.g., instead of the actual
230+
replacement values supplied to a `replaceOp` API call, `operands` may contain
231+
results of transitory `builtin.unrealized_conversion_cast` ops that were
232+
inserted by the conversion driver but typically fold away again throughout the
233+
conversion process.
234+
235+
#### Immediate vs. Delayed IR Modification
236+
237+
The dialect conversion driver can operate in two modes: (a) rollback mode
238+
(default) and (b) no-rollback mode. This can be controlled by
239+
`ConversionConfig::allowPatternRollback`. When running in rollback mode, the
240+
driver is able backtrack and roll back already applied patterns when the
241+
current legalization path (sequence of pattern applications) gets stuck with
242+
unlegalizable operations.
243+
244+
When running in no-rollback mode, all IR modifications such as op replacement,
245+
op erasure, op insertion or in-place op modification are applied immediately.
246+
247+
When running in rollback mode, certain IR modifications are delayed to the end
248+
of the conversion process. For example, a `ConversionPatternRewriter::eraseOp`
249+
API call does not immediately erase the op, but just marks it for erasure. The
250+
op will stay visible to patterns and IR traversals throughout the conversion
251+
process. As another example, `replaceOp` and `replaceAllUsesWith` does not
252+
immediately update users of the original SSA values. This step is also delayed
253+
to the end of the conversion process.
254+
255+
Delaying certain IR modifications has two benefits: (1) pattern rollback is
256+
simpler because fewer IR modifications must be rolled back, (2) pointers of
257+
erased operations / blocks are preserved upon rollback, and (3) patterns can
258+
still access/traverse the original IR to some degree. However, additional
259+
bookkeeping in the form of complex internal C++ data structures is required to
260+
support pattern rollback. Running in rollback mode has a significant toll on
261+
compilation time, is error-prone and makes debugging conversion passes more
262+
complicated. Therefore, programmers are encouraged to run in no-rollback mode
263+
when possible.
264+
265+
The following table gives an overview of which IR changes are applied in a
266+
delayed fasion in rollback mode.
267+
268+
| Type | Rollback Mode | No-rollback Mode |
269+
| ------------------------------------------------------- | ----------------- | ---------------- |
270+
| Op Insertion / Movement (`create`/`insert`) | Immediate | Immediate |
271+
| Op Replacement (`replaceOp`) | Delayed | Immediate |
272+
| Op Erasure (`eraseOp`) | Delayed | Immediate |
273+
| Op Modification (`modifyOpInPlace`) | Immediate | Immediate |
274+
| Value Replacement (`replaceAllUsesWith`) | Delayed | Immediate |
275+
| Block Insertion (`createBlock`) | Immediate | Immediate |
276+
| Block Replacement | Not supported | Not supported |
277+
| Block Erasure | Partly delayed | Immediate |
278+
| Block Signature Conversion (`applySignatureConversion`) | Partially delayed | Immediate |
279+
| Region / Block Inlining (`inlineBlockBefore`, etc.) | Partially delayed | Immediate |
280+
281+
Value replacement is delayed and has different semantics in rollback mode:
282+
Since the actual replacement is delayed to the end of the conversion process,
283+
additional uses of the replaced value can be created after the
284+
`replaceAllUsesWith` call. Those uses will also be replaced at the end of the
285+
conversion process.
286+
287+
Block replacement is not supported in either mode, because the rewriter
288+
infrastructure currently has no API for replacing blocks: there is no overload
289+
of `replaceAllUsesWith` that accepts `Block *`.
290+
291+
Block erasure is partly delayed in rollback mode: the block is detached from
292+
the IR graph, but not memory for the block is not released until the end of the
293+
conversion process. This mechanism ensures that block pointers do not change
294+
when a block erasure is rolled back.
295+
296+
Block signature conversion is a combination of block insertion, op insertion,
297+
value replacement and block erasure. In rollback mode, the first two steps are
298+
immediate, but the last two steps are delayed.
299+
300+
Region / block inlining is a combination of block / op insertion and
301+
(optionally) value replacement. In rollback mode, the insertion steps are
302+
immediate, but the replacement step is delayed.
303+
304+
Note: When running in rollback mode, the conversion driver inserts fewer
305+
transitory `builtin.unrealized_conversion_cast` ops. Such ops are needed less
306+
frequently because certain IR modifications are delayed, making it unnecessary
307+
to connect old (not yet rewritten) and new (already rewritten) IR in a
308+
type-safe way. This has a negative effect on the debugging experience: when
309+
dumping IR throughout the conversion process, users see a mixture of old and
310+
new IR, but the way they are connected is not always visibile in the IR. Some
311+
of that information is stored in internal C++ data structures that is not
312+
visibile during an IR dump.
313+
203314
#### Type Safety
204315

205316
The types of the remapped operands provided to a conversion pattern (through

0 commit comments

Comments
 (0)