blob: b73aa043981038041868b426b6caf5bc275b2009 [file] [log] [blame] [view] [edit]
# Dialect Conversion
This document describes a framework in MLIR in which to perform operation
conversions between, and within dialects. This framework allows for transforming
illegal operations to those supported by a provided conversion target, via a set
of pattern-based operation rewriting patterns.
The dialect conversion framework consists of the following components:
* A [Conversion Target](#conversion-target)
* A set of [Rewrite Patterns](#rewrite-pattern-specification)
* A [Type Converter](#type-conversion) (Optional)
[TOC]
## Modes of Conversion
When applying a conversion to a set of operations, there are several different
conversion modes that may be selected from:
* Partial Conversion
- A partial conversion will legalize as many operations to the target as
possible, but will allow pre-existing operations that were not
explicitly marked as "illegal" to remain unconverted. This allows for
partially lowering parts of the input in the presence of unknown
operations.
- A partial conversion can be applied via `applyPartialConversion`.
* Full Conversion
- A full conversion legalizes all input operations, and is only successful
if all operations are properly legalized to the given conversion target.
This ensures that only known operations will exist after the conversion
process.
- A full conversion can be applied via `applyFullConversion`.
* Analysis Conversion
- An analysis conversion will analyze which operations are legalizable to
the given conversion target if a conversion were to be applied. This is
done by performing a 'partial' conversion and recording which operations
would have been successfully converted if successful. Note that no
rewrites, or transformations, are actually applied to the input
operations.
- An analysis conversion can be applied via `applyAnalysisConversion`.
In all cases, the framework walks the operations in preorder, examining an op
before the ops in any regions it has.
## Conversion Target
The conversion target is a formal definition of what is considered to be legal
during the conversion process. The final operations generated by the conversion
framework must be marked as legal on the `ConversionTarget` for the rewrite to
be a success. Depending on the conversion mode, existing operations need not
always be legal. Operations and dialects may be marked with any of the provided
legality actions below:
* Legal
- This action signals that every instance of a given operation is legal,
i.e. any combination of attributes, operands, types, etc. are valid.
* Dynamic
- This action signals that only some instances of a given operation are
legal. This allows for defining fine-tune constraints, e.g. saying that
`arith.addi` is only legal when operating on 32-bit integers.
* Illegal
- This action signals that no instance of a given operation is legal.
Operations marked as "illegal" must always be converted for the
conversion to be successful. This action also allows for selectively
marking specific operations as illegal in an otherwise legal dialect.
Operations and dialects that are neither explicitly marked legal nor illegal are
separate from the above ("unknown" operations) and are treated differently, for
example, for the purposes of partial conversion as mentioned above.
An example conversion target is shown below:
```c++
struct MyTarget : public ConversionTarget {
MyTarget(MLIRContext &ctx) : ConversionTarget(ctx) {
//--------------------------------------------------------------------------
// Marking an operation as Legal:
/// Mark all operations within the LLVM dialect are legal.
addLegalDialect<LLVMDialect>();
/// Mark `arith.constant` op is always legal on this target.
addLegalOp<arith::ConstantOp>();
//--------------------------------------------------------------------------
// Marking an operation as dynamically legal.
/// Mark all operations within Affine dialect have dynamic legality
/// constraints.
addDynamicallyLegalDialect<affine::AffineDialect>(
[](Operation *op) { ... });
/// Mark `func.return` as dynamically legal, but provide a specific legality
/// callback.
addDynamicallyLegalOp<func::ReturnOp>([](func::ReturnOp op) { ... });
/// Treat unknown operations, i.e. those without a legalization action
/// directly set, as dynamically legal.
markUnknownOpDynamicallyLegal([](Operation *op) { ... });
//--------------------------------------------------------------------------
// Marking an operation as illegal.
/// All operations within the GPU dialect are illegal.
addIllegalDialect<GPUDialect>();
/// Mark `cf.br` and `cf.cond_br` as illegal.
addIllegalOp<cf::BranchOp, cf::CondBranchOp>();
}
/// Implement the default legalization handler to handle operations marked as
/// dynamically legal that were not provided with an explicit handler.
bool isDynamicallyLegal(Operation *op) override { ... }
};
```
### Recursive Legality
In some cases, it may be desirable to mark entire regions as legal. This
provides an additional granularity of context to the concept of "legal". If an
operation is marked recursively legal, either statically or dynamically, then
all of the operations nested within are also considered legal even if they would
otherwise be considered "illegal". An operation can be marked via
`markOpRecursivelyLegal<>`:
```c++
ConversionTarget &target = ...;
/// The operation must first be marked as `Legal` or `Dynamic`.
target.addLegalOp<MyOp>(...);
target.addDynamicallyLegalOp<MySecondOp>(...);
/// Mark the operation as always recursively legal.
target.markOpRecursivelyLegal<MyOp>();
/// Mark optionally with a callback to allow selective marking.
target.markOpRecursivelyLegal<MyOp, MySecondOp>([](Operation *op) { ... });
/// Mark optionally with a callback to allow selective marking.
target.markOpRecursivelyLegal<MyOp>([](MyOp op) { ... });
```
## Rewrite Pattern Specification
After the conversion target has been defined, a set of legalization patterns
must be provided to transform illegal operations into legal ones. The patterns
supplied here have the same structure and similar restrictions as those
described in the main [Pattern](PatternRewriter.md) documentation. The patterns
provided do not need to generate operations that are directly legal on the
target. The framework will automatically build a graph of conversions to convert
non-legal operations into a set of legal ones.
As an example, say you define a target that supports one operation: `foo.add`.
When providing the following patterns: [`bar.add` -> `baz.add`, `baz.add` ->
`foo.add`], the framework will automatically detect that it can legalize
`bar.add` -> `foo.add` even though a direct conversion does not exist. This
means that you dont have to define a direct legalization pattern for `bar.add`
-> `foo.add`.
### Conversion Patterns
Along with the general `RewritePattern` classes, the conversion framework
provides a special type of rewrite pattern that can be used when a pattern
relies on interacting with constructs specific to the conversion process, the
`ConversionPattern`.
#### Remapped Operands / Adaptor
Conversion patterns have an additional `operands` / `adaptor` argument for the
`matchAndRewrite` method. These operands correspond to the most recent
replacement values of the respective operands of the matched operation.
```c++
struct MyConversionPattern : public ConversionPattern {
/// The `matchAndRewrite` hooks on ConversionPatterns take an additional
/// `operands` parameter, containing the remapped operands of the original
/// operation.
virtual LogicalResult
matchAndRewrite(Operation *op, ArrayRef<Value> operands,
ConversionPatternRewriter &rewriter) const;
};
```
Example:
```mlir
%0 = "test.foo"() : () -> i1 // matched by pattern A
"test.bar"(%0) : (i1) -> () // matched by pattern B
```
Let's assume that the two patterns are applied back-to-back: first, pattern A
replaces `"test.foo"` with `"test.qux"`, an op that has a different result
type. The dialect conversion infrastructure has special support for such
type-changing IR modifications.
```mlir
%0 = "test.qux"() : () -> i2
%r = builtin.unrealized_conversion_cast %0 : i2 to i1
"test.bar"(%r) : (i1) -> ()
```
Simply swapping out the operand of `"test.bar"` during the `replaceOp` call
would be unsafe, because that would change the type of operand and, therefore,
potentially the semantics of the operation. Instead, the dialect conversion
driver (conceptually) inserts a `builtin.unrealized_conversion_cast` op that
connects the newly created `"test.qux"` op with the `"test.bar"` op, without
changing the types of the latter one.
Now, the second pattern B is applied. The `operands` argument contains an SSA
value with the most recent replacement type (`%0` with type `i2`), whereas
querying the operand from the matched op still returns an SSA value with the
original operand type `i1`.
Note: If the conversion pattern is instantiated with a type converter, the
`operands` argument contains SSA values whose types match the legalized operand
types as per the type converter. See [Type Safety](#type-safety) for more
details.
Note: The dialect conversion framework does not guarantee the presence of any
particular value in the `operands` argument. The only thing that's guaranteed
is the type of the `operands` SSA values. E.g., instead of the actual
replacement values supplied to a `replaceOp` API call, `operands` may contain
results of transitory `builtin.unrealized_conversion_cast` ops that were
inserted by the conversion driver but typically fold away again throughout the
conversion process.
#### Immediate vs. Delayed IR Modification
The dialect conversion driver can operate in two modes: (a) rollback mode
(default) and (b) no-rollback mode. This can be controlled by
`ConversionConfig::allowPatternRollback`. When running in rollback mode, the
driver is able backtrack and roll back already applied patterns when the
current legalization path (sequence of pattern applications) gets stuck with
unlegalizable operations.
When running in no-rollback mode, all IR modifications such as op replacement,
op erasure, op insertion or in-place op modification are applied immediately.
When running in rollback mode, certain IR modifications are delayed to the end
of the conversion process. For example, a `ConversionPatternRewriter::eraseOp`
API call does not immediately erase the op, but just marks it for erasure. The
op will stay visible to patterns and IR traversals throughout the conversion
process. As another example, `replaceOp` and `replaceAllUsesWith` does not
immediately update users of the original SSA values. This step is also delayed
to the end of the conversion process.
Delaying certain IR modifications has two benefits: (1) pattern rollback is
simpler because fewer IR modifications must be rolled back, (2) pointers of
erased operations / blocks are preserved upon rollback, and (3) patterns can
still access/traverse the original IR to some degree. However, additional
bookkeeping in the form of complex internal C++ data structures is required to
support pattern rollback. Running in rollback mode has a significant toll on
compilation time, is error-prone and makes debugging conversion passes more
complicated. Therefore, programmers are encouraged to run in no-rollback mode
when possible.
The following table gives an overview of which IR changes are applied in a
delayed fasion in rollback mode.
| Type | Rollback Mode | No-rollback Mode |
| ------------------------------------------------------- | ----------------- | ---------------- |
| Op Insertion / Movement (`create`/`insert`) | Immediate | Immediate |
| Op Replacement (`replaceOp`) | Delayed | Immediate |
| Op Erasure (`eraseOp`) | Delayed | Immediate |
| Op Modification (`modifyOpInPlace`) | Immediate | Immediate |
| Value Replacement (`replaceAllUsesWith`) | Delayed | Immediate |
| Block Insertion (`createBlock`) | Immediate | Immediate |
| Block Replacement | Not supported | Not supported |
| Block Erasure | Partly delayed | Immediate |
| Block Signature Conversion (`applySignatureConversion`) | Partially delayed | Immediate |
| Region / Block Inlining (`inlineBlockBefore`, etc.) | Partially delayed | Immediate |
Value replacement is delayed and has different semantics in rollback mode:
Since the actual replacement is delayed to the end of the conversion process,
additional uses of the replaced value can be created after the
`replaceAllUsesWith` call. Those uses will also be replaced at the end of the
conversion process.
Block replacement is not supported in either mode, because the rewriter
infrastructure currently has no API for replacing blocks: there is no overload
of `replaceAllUsesWith` that accepts `Block *`.
Block erasure is partly delayed in rollback mode: the block is detached from
the IR graph, but not memory for the block is not released until the end of the
conversion process. This mechanism ensures that block pointers do not change
when a block erasure is rolled back.
Block signature conversion is a combination of block insertion, op insertion,
value replacement and block erasure. In rollback mode, the first two steps are
immediate, but the last two steps are delayed.
Region / block inlining is a combination of block / op insertion and
(optionally) value replacement. In rollback mode, the insertion steps are
immediate, but the replacement step is delayed.
Note: When running in rollback mode, the conversion driver inserts fewer
transitory `builtin.unrealized_conversion_cast` ops. Such ops are needed less
frequently because certain IR modifications are delayed, making it unnecessary
to connect old (not yet rewritten) and new (already rewritten) IR in a
type-safe way. This has a negative effect on the debugging experience: when
dumping IR throughout the conversion process, users see a mixture of old and
new IR, but the way they are connected is not always visibile in the IR. Some
of that information is stored in internal C++ data structures that is not
visibile during an IR dump.
#### Type Safety
The types of the remapped operands provided to a conversion pattern (through
the adaptor or `ArrayRef` of operands) depend on type conversion rules.
If the pattern was initialized with a [type converter](#type-converter), the
conversion driver passes values whose types match the legalized types of the
operands of the matched operation as per the type converter. To that end, the
conversion driver may insert target materializations to convert the most
recently mapped values to the expected legalized types. The driver tries to
reuse existing materializations on a best-effort basis, but this is not
guaranteed by the infrastructure. If the operand types of the matched op could
not be legalized, the pattern fails to apply before the `matchAndRewrite` hook
is invoked.
Example:
```c++
// Type converter that converts all FloatTypes to IntegerTypes.
TypeConverter converter;
converter.addConversion([](FloatType t) {
return IntegerType::get(t.getContext(), t.getWidth());
});
// Assuming that `MyConversionPattern` was initialized with `converter`.
struct MyConversionPattern : public ConversionPattern {
virtual LogicalResult
matchAndRewrite(Operation *op, ArrayRef<Value> operands, /* ... */) const {
// ^^^^^^^^
// If `op` has a FloatType operand, the respective value in `operands`
// is guaranteed to have the legalized IntegerType. If another pattern
// previously replaced the operand SSA value with an SSA value of the
// legalized type (via "replaceOp" or "applySignatureConversion"), you
// will get that SSA value directly (unless the replacement value was
// also replaced). Otherwise, you will get a materialization to the
// legalized type.
```
If the pattern was initialized without a type converter, the conversion driver
passes the most recently mapped values to the pattern, excluding any
materializations. If a value with the same type as the original operand is
desired, users can directly take the respective operand from the matched
operation.
Example: When initializing the pattern from the above example without a type
converter, `operands` contains the most recent replacement values, regardless
of their types.
Note: When running without a type converter, materializations are intentionally
excluded from the lookup process because their presence may depend on other
patterns. Passing materializations would make the conversion infrastructure
fragile and unpredictable. Moreover, there could be multiple materializations
to different types. (This can be the case when multiple patterns are running
with different type converters.) In such a case, it would be unclear which
materialization to pass.
The above rules ensure that patterns do not have to explicitly ensure type
safety, or sanitize the types of the incoming remapped operands. More
information on type conversion is detailed in the
[dedicated section](#type-conversion) below.
## Type Conversion
It is sometimes necessary as part of a conversion to convert the set types of
being operated on. In these cases, a `TypeConverter` object may be defined that
details how types should be converted when interfacing with a pattern. A
`TypeConverter` may be used to convert the signatures of block arguments and
regions, to define the expected inputs types of the pattern, and to reconcile
type differences in general.
### Type Converter
The `TypeConverter` contains several hooks for detailing how to convert types,
and how to materialize conversions between types in various situations. The two
main aspects of the `TypeConverter` are conversion and materialization.
A `conversion` describes how a given source `Type` should be converted to N
target types. If the source type is converted to itself, we say it is a "legal"
type. Type conversions are specified via the `addConversion` method described
below.
There are two kind of conversion functions: context-aware and context-unaware
conversions. A context-unaware conversion function converts a `Type` into a
`Type`. A context-aware conversion function converts a `Value` into a type. The
latter allows users to customize type conversion rules based on the IR.
Note: context-aware type conversion functions impact the ability of the
framework to cache the conversion result. In the absence of a context-aware
conversion, all context-free type conversions can be cached. Otherwise only the
context-free conversions added after a context-aware type conversion can be
cached (conversions are applied in reverse order).
As such it is advised to add context-aware conversions as early as possible in
the sequence of `addConversion` calls (so that they apply last).
A `materialization` describes how a list of values should be converted to a
list of values with specific types. An important distinction from a
`conversion` is that a `materialization` can produce IR, whereas a `conversion`
cannot. These materializations are used by the conversion framework to ensure
type safety during the conversion process. There are several types of
materializations depending on the situation.
* Source Materialization
- A source materialization is used when a value was replaced with a value
of a different type, but there are still users that expects the original
("source") type at the end of the conversion process. A source
materialization converts the replacement value back to the source type.
- This materialization is used in the following situations:
* When a block argument has been converted to a different type, but
the original argument still has users that will remain live after
the conversion process has finished.
* When a block argument has been dropped, but the argument still has
users that will remain live after the conversion process has
finished.
* When the result type of an operation has been converted to a
different type, but the original result still has users that will
remain live after the conversion process is finished.
* Target Materialization
- A target materialization converts a value to the type that is expected
by a conversion pattern according to its type converter.
- A target materialization is used when a pattern expects the remapped
operands to be of a certain set of types, but the original input
operands have either not been replaced or been replaced with values of
a different type.
If a converted value is used by an operation that isn't converted, it needs a
conversion back to the `source` type, hence source materialization; if an
unconverted value is used by an operation that is being converted, it needs
conversion to the `target` type, hence target materialization.
As noted above, the conversion process guarantees that the type contract of the
IR is preserved during the conversion. This means that the types of value uses
will not implicitly change during the conversion process. When the type of a
value definition, either block argument or operation result, is being changed,
the users of that definition must also be updated during the conversion process.
If they aren't, a type conversion must be materialized to ensure that a value of
the expected type is still present within the IR. If a materialization is
required, but cannot be performed, the entire conversion process fails.
Several of the available hooks are detailed below:
```c++
class TypeConverter {
public:
/// Register a conversion function. A conversion function must be convertible
/// to any of the following forms (where `T` is `Value` or a class derived
/// from `Type`, including `Type` itself):
///
/// * std::optional<Type>(T)
/// - This form represents a 1-1 type conversion. It should return nullptr
/// or `std::nullopt` to signify failure. If `std::nullopt` is returned,
/// the converter is allowed to try another conversion function to
/// perform the conversion.
/// * std::optional<LogicalResult>(T, SmallVectorImpl<Type> &)
/// - This form represents a 1-N type conversion. It should return
/// `failure` or `std::nullopt` to signify a failed conversion. If the
/// new set of types is empty, the type is removed and any usages of the
/// existing value are expected to be removed during conversion. If
/// `std::nullopt` is returned, the converter is allowed to try another
/// conversion function to perform the conversion.
///
/// Conversion functions that accept `Value` as the first argument are
/// context-aware. I.e., they can take into account IR when converting the
/// type of the given value. Context-unaware conversion functions accept
/// `Type` or a derived class as the first argument.
///
/// Note: Context-unaware conversions are cached, but context-aware
/// conversions are not.
///
/// Note: When attempting to convert a type, e.g. via 'convertType', the
/// mostly recently added conversions will be invoked first.
template <typename FnT,
typename T = typename llvm::function_traits<FnT>::template arg_t<0>>
void addConversion(FnT &&callback) {
registerConversion(wrapCallback<T>(std::forward<FnT>(callback)));
}
/// All of the following materializations require function objects that are
/// convertible to the following form:
/// `std::optional<Value>(OpBuilder &, T, ValueRange, Location)`,
/// where `T` is any subclass of `Type`. This function is responsible for
/// creating an operation, using the OpBuilder and Location provided, that
/// "casts" a range of values into a single value of the given type `T`. It
/// must return a Value of the converted type on success, an `std::nullopt` if
/// it failed but other materialization can be attempted, and `nullptr` on
/// unrecoverable failure. It will only be called for (sub)types of `T`.
/// Materialization functions must be provided when a type conversion may
/// persist after the conversion has finished.
/// This method registers a materialization that will be called when
/// converting a replacement value back to its original source type.
/// This is used when some uses of the original value persist beyond the main
/// conversion.
template <typename FnT,
typename T = typename llvm::function_traits<FnT>::template arg_t<1>>
void addSourceMaterialization(FnT &&callback) {
sourceMaterializations.emplace_back(
wrapSourceMaterialization<T>(std::forward<FnT>(callback)));
}
/// This method registers a materialization that will be called when
/// converting a value to a target type according to a pattern's type
/// converter.
///
/// Note: Target materializations can optionally inspect the "original"
/// type. This type may be different from the type of the input value.
/// For example, let's assume that a conversion pattern "P1" replaced an SSA
/// value "v1" (type "t1") with "v2" (type "t2"). Then a different conversion
/// pattern "P2" matches an op that has "v1" as an operand. Let's furthermore
/// assume that "P2" determines that the converted target type of "t1" is
/// "t3", which may be different from "t2". In this example, the target
/// materialization will be invoked with: outputType = "t3", inputs = "v2",
/// originalType = "t1". Note that the original type "t1" cannot be recovered
/// from just "t3" and "v2"; that's why the originalType parameter exists.
///
/// Note: During a 1:N conversion, the result types can be a TypeRange. In
/// that case the materialization produces a SmallVector<Value>.
template <typename FnT,
typename T = typename llvm::function_traits<FnT>::template arg_t<1>>
void addTargetMaterialization(FnT &&callback) {
targetMaterializations.emplace_back(
wrapTargetMaterialization<T>(std::forward<FnT>(callback)));
}
};
```
Materializations through the type converter are optional. If the
`ConversionConfig::buildMaterializations` flag is set to "false", the dialect
conversion driver builds an `unrealized_conversion_cast` op instead of calling
the respective type converter callback whenever a materialization is required.
### Region Signature Conversion
From the perspective of type conversion, the types of block arguments are a bit
special. Throughout the conversion process, blocks may move between regions of
different operations. Given this, the conversion of the types for blocks must be
done explicitly via a conversion pattern.
To convert the types of block arguments within a Region, a custom hook on the
`ConversionPatternRewriter` must be invoked; `convertRegionTypes`. This hook
uses a provided type converter to apply type conversions to all blocks of a
given region. This hook also takes an optional
`TypeConverter::SignatureConversion` parameter that applies a custom conversion
to the entry block of the region. The types of the entry block arguments are
often tied semantically to the operation, e.g., `func::FuncOp`, `AffineForOp`,
etc.
To convert the signature of just one given block, the
`applySignatureConversion` hook can be used.
A signature conversion, `TypeConverter::SignatureConversion`, can be built
programmatically:
```c++
class SignatureConversion {
public:
/// Remap an input of the original signature with a new set of types. The
/// new types are appended to the new signature conversion.
void addInputs(unsigned origInputNo, ArrayRef<Type> types);
/// Append new input types to the signature conversion, this should only be
/// used if the new types are not intended to remap an existing input.
void addInputs(ArrayRef<Type> types);
/// Remap an input of the original signature with a range of types in the
/// new signature.
void remapInput(unsigned origInputNo, unsigned newInputNo,
unsigned newInputCount = 1);
/// Remap an input of the original signature to another `replacement`
/// value. This drops the original argument.
void remapInput(unsigned origInputNo, Value replacement);
};
```
The `TypeConverter` provides several default utilities for signature conversion
and legality checking:
`convertSignatureArgs`/`convertBlockSignature`/`isLegal(Region *|Type)`.
## Debugging
To debug the execution of the dialect conversion framework,
`-debug-only=dialect-conversion` may be used. This command line flag activates
LLVM's debug logging infrastructure solely for the conversion framework. The
output is formatted as a tree structure, mirroring the structure of the
conversion process. This output contains all of the actions performed by the
rewriter, how generated operations get legalized, and why they fail.
Example output is shown below:
```
//===-------------------------------------------===//
Legalizing operation : 'func.return'(0x608000002e20) {
"func.return"() : () -> ()
* Fold {
} -> FAILURE : unable to fold
* Pattern : 'func.return -> ()' {
** Insert : 'spirv.Return'(0x6070000453e0)
** Replace : 'func.return'(0x608000002e20)
//===-------------------------------------------===//
Legalizing operation : 'spirv.Return'(0x6070000453e0) {
"spirv.Return"() : () -> ()
} -> SUCCESS : operation marked legal by the target
//===-------------------------------------------===//
} -> SUCCESS : pattern applied successfully
} -> SUCCESS
//===-------------------------------------------===//
```
This output is describing the legalization of an `func.return` operation. We
first try to legalize by folding the operation, but that is unsuccessful for
`func.return`. From there, a pattern is applied that replaces the `func.return`
with a `spirv.Return`. The newly generated `spirv.Return` is then processed for
legalization, but is found to already legal as per the target.