TLDR: Design a new (additional) dialect conversion driver without the quirks of the current dialect conversion driver.
Dialect Conversion Recap
The dialect conversion is a pattern-based IR rewrite driver that:
- traverses the IR from top to bottom,
- looks for “illegal” ops (as per
ConversionTarget
), - rewrites illegal ops by applying a ConversionRewritePattern,
- keeps track of all IR modifications, so that already applied patterns can be rolled back and different patterns can be tried (backtracking).
To allow for rollback of IR modifications, the dialect conversion materializes some IR modifications in a delayed fashion at the end of the dialect conversion process. E.g., ops are not erased immediately and uses of op results are not replaced immediately.
What’s wrong with the current Dialect Conversion?
It is slow. @mehdi_amini micro-benchmarked parts of MLIR (see “Efficient Idioms in MLIR” keynote at EuroLLVM 2024; do we have the slides somewhere?) and the dialect conversion was by far the slowest part. The main reason for that is the extra housekeeping that enables rollbacks. You may have noticed when running the test suite that one the dialect conversion-heavy NVVM tests (I think it’s this one) is quite slow.
It is too complicated. Even for long-standing MLIR contributors. And to an extent that contributors have been reluctant to touch the code base and rather implemented a standalone infrastructure (1:N dialect conversion).
It reuses common MLIR API but imposes partly undocumented restrictions that make it difficult to use correctly. Some examples:
ConversionRewritePattern
derives fromRewritePattern
, but it is generally unsafe to use a conversion pattern in a greedy rewrite or rewrite pattern in a dialect conversion.- Walking IR and/or SSA use-def chains is generally unsafe in a
ConversionRewritePattern
because some IR modifications are applied to the IR only at the end of the dialect conversion. (In particular: Users can still reach/see IR that was already scheduled for erasure.) It is unclear/undocumented what exactly is allowed in aConversionRewritePattern
. - Because of the delayed materialization of certain IR modifications,
ConversionRewritePattern
s cannot assume that the IR is in a valid state at the beginning of a pattern application; making it difficult to writeConversionRewritePattern
s. It is unclear/undocumented what patterns can assume. (Side note: Regular rewrite patterns can assume that IR is valid before each pattern application, and we have assertions for that.) - The dialect conversion driver calls op folders, which generally assume that the IR is in a valid state. Using op folders in a dialect conversion is generally unsafe.
It is difficult to use and to debug. Some examples:
- IR dumps during a dialect conversion (before/during/after a pattern application) are broken: ops that were already scheduled for erasure are still shown, value replacements have not manifested yet (they are tracked in an
IRMapping
instead). - A discourse search for “failed to legalize operation” yields 46 results since Nov. 2020.
- A bug in a
ConversionRewritePattern
may become apparent only much later, at the end of the dialect conversion.
Proposal: One-Shot Dialect Conversion (name TBD)
Provide a new, simpler dialect conversion driver. (In addition to the existing dialect conversion driver. No changes needed for existing users of the current dialect conversion driver, if they do not want to use the new driver.)
- Rollbacks (the main source of complexity in the current dialect conversion) are no longer supported. Once a pattern has been applied, it can no longer be undone.
- All state is materialized in IR. In particular,
unrealized_conversion_cast
ops are eagerly materialized every time a value is replaced with a value of different type. No more state inIRMapping
. This means that IR is valid after each pattern application and can be dumped at any time. - In essence, the new dialect conversion is a simple walk over the IR. A conversion pattern is applied for each illegal op, in the same way as regular
RewritePattern
s are applied. The only difference is thatreplaceOp
/replaceAllUsesWith
are relaxed so that replacement values can have a different type. The type converter is not needed for this process; it is only needed to convert block signatures. - Provide only a new driver, but reuse the existing infrastructure. In particular,
ConversionRewritePattern
andTypeConverter
can be reused as is. ExistingConversionRewritePattern
s can be used with the new dialect conversion driver. (But patterns designed for the new driver cannot be used with the old driver, as they may assume that IR is valid when they start executing.)
Implementation Outline
A new OneShotConversionPatternRewriter
that derives from ConversionPatternRewriter
will be provided. Along with a thin driver that walks the IR and applies patterns.
OpBuilder
^
|
RewriterBase
^
|
PatternRewriter
^
|
ConversionPatternRewriter
^
|
OneShotConversionPatternRewriter // this one is new
The new rewriter behaves mostly like a PatternRewriter
, but allows ops/values to be replaced with ops/values of different type, upon which an unrealized_conversion_cast
is automatically inserted. The new rewriter must be a subclass of ConverisonPatternRewriter
so that ConversionRewritePattern
can be reused. All functions that are overwritten in the ConversionPatternRewriter
(except for replaceOp
/replaceAllUsesWith
) are overwritten in OneShotConversionPatternRewriter
and dispatch to the original PatternRewriter
/RewriterBase
implementation.
The conversion pattern “adapter” object is populated by taking the operands from the unrealized_conversion_cast
op (instead of querying them from IRMapping
).
One change is needed for existing conversion patterns: they must not modify the IR if they return failure
. (Same as ordinary rewrite patterns.) Most patterns should already be written in such a way. for all other patterns, that should be a simple fix.
Open Questions
The main idea of the proposal is to design a dialect conversion driver without rollback/backtracking. I’d like to hear from dialect conversion users whether they actually depend on the rollback mechanism or not. I am not aware of any passes that depend on the backtracking.
What if multiple patterns can apply to an illegal op? Which one should be chosen? A ConversionRewritePattern
must erase, replace or in-place modify the illegal root op, so there is generally no chance for a second pattern to be applied. (In practice, there may only be one applicable pattern in most cases.)
The current dialect conversion has a cost model that guides the selection of patterns. Depending on how we handle the case where multiple patterns can apply, this cost model may still or may no longer be needed.
For compatibility reasons, I proposed reusing existing infrastructure and having OneShotConversionPatternRewriter
derive from ConversionPatternRewriter
. We could alternatively, completely decouple the new dialect conversion and make more radical changes. E.g., ConversionRewritePattern
should ideally not derive from RewritePattern
because it is not safe to use with other RewritePattern
-based infrastructure such as the greedy pattern rewrite driver. However, this also means that we cannot easily reuse existing functionality such as the PatternApplicator
.
Next Steps
Based on the feedback for this proposal, I plan to prepare a prototype / proof of concept. But I’d like to first fix important design decisions (e.g., whether to reuse existing infrastructure, etc.), as those are difficult to change later.
@ftynse @ingomueller-net @jpienaar @mehdi_amini @Mogball @nicolasvasilache