forked from OSchip/llvm-project
[mlir][DialectConversion] Update the documentation for dialect conversion
This revision updates the documentation for dialect conversion, as many concepts have changed/evolved over time. Differential Revision: https://reviews.llvm.org/D85167
This commit is contained in:
parent
f12db8cf75
commit
fa4b3147e3
|
@ -7,7 +7,7 @@ of pattern-based operation rewriting patterns.
|
|||
|
||||
[TOC]
|
||||
|
||||
To utilize the framework, a few things must be provided:
|
||||
The dialect conversion framework consists of the following components:
|
||||
|
||||
* A [Conversion Target](#conversion-target)
|
||||
* A set of [Rewrite Patterns](#rewrite-pattern-specification)
|
||||
|
@ -15,41 +15,44 @@ To utilize the framework, a few things must be provided:
|
|||
|
||||
## Modes of Conversion
|
||||
|
||||
When applying a conversion to a set of operations, there are several conversion
|
||||
modes that can be selected from:
|
||||
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 module in the presence of unknown
|
||||
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 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 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. Note
|
||||
that no rewrites, or transformations, are actually applied to the input
|
||||
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`.
|
||||
|
||||
## Conversion Target
|
||||
|
||||
The conversion target is the formal definition of what is considered to be legal
|
||||
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. Existing operations need not always be legal, though; see the
|
||||
different conversion modes for why. Operations and dialects may be marked with
|
||||
any of the provided legality actions below:
|
||||
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
|
||||
|
||||
|
@ -68,7 +71,7 @@ any of the provided legality actions below:
|
|||
* Illegal
|
||||
|
||||
- This action signals that no instance of a given operation is legal.
|
||||
Operations marked as `illegal` must always be converted for the
|
||||
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.
|
||||
|
||||
|
@ -123,13 +126,12 @@ struct MyTarget : public ConversionTarget {
|
|||
|
||||
### Recursive Legality
|
||||
|
||||
In some cases, it may be desirable to mark entire regions of operations as
|
||||
legal. This provides an additional granularity of context to the concept of
|
||||
"legal". The `ConversionTarget` supports marking operations, that were
|
||||
previously added as `Legal` or `Dynamic`, as `recursively` legal. Recursive
|
||||
legality means that if an operation instance is legal, either statically or
|
||||
dynamically, all of the operations nested within are also considered legal. An
|
||||
operation can be marked via `markOpRecursivelyLegal<>`:
|
||||
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 = ...;
|
||||
|
@ -149,14 +151,12 @@ 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, that do not [require type changes](#conversion-patterns), are the
|
||||
same as those described in the
|
||||
[quickstart rewrites guide](Tutorials/QuickstartRewrites.md#adding-patterns), but have a
|
||||
few additional [restrictions](#restrictions). 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.
|
||||
must be provided to transform illegal operations into legal ones. The structure
|
||||
of the patterns supplied here is the same as those described in the
|
||||
[quickstart rewrites guide](Tutorials/QuickstartRewrites.md#adding-patterns).
|
||||
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` ->
|
||||
|
@ -165,38 +165,139 @@ When providing the following patterns: [`bar.add` -> `baz.add`, `baz.add` ->
|
|||
means that you don’t have to define a direct legalization pattern for `bar.add`
|
||||
-> `foo.add`.
|
||||
|
||||
### Restrictions
|
||||
### Conversion Patterns
|
||||
|
||||
The framework processes operations in topological order, trying to legalize them
|
||||
individually. As such, patterns used in the conversion framework have a few
|
||||
additional restrictions:
|
||||
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`. For example, the conversion process does not necessarily
|
||||
update operations in-place and instead creates a mapping of events such as
|
||||
replacements and erasures, and only applies them when the entire conversion
|
||||
process is successful. Certain classes of patterns rely on using the
|
||||
updated/remapped operands of an operation, such as when the types of results
|
||||
defined by an operation have changed. The general Rewrite Patterns can no longer
|
||||
be used in these situations, as the types of the operands of the operation being
|
||||
matched will not correspond with those expected by the user. This pattern
|
||||
provides, as an additional argument to the `matchAndRewrite` and `rewrite`
|
||||
methods, the list of operands that the operation should use after conversion. If
|
||||
an operand was the result of a non-converted operation, for example if it was
|
||||
already legal, the original operand is used. This means that the operands
|
||||
provided always have a 1-1 non-null correspondence with the operands on the
|
||||
operation. The original operands of the operation are still intact and may be
|
||||
inspected as normal. These patterns also utilize a special `PatternRewriter`,
|
||||
`ConversionPatternRewriter`, that provides special hooks for use with the
|
||||
conversion infrastructure.
|
||||
|
||||
1. If a pattern matches, it must erase or replace the op it matched on.
|
||||
Operations can *not* be updated in place.
|
||||
2. Match criteria should not be based on the IR outside of the op itself. The
|
||||
preceding ops will already have been processed by the framework (although it
|
||||
may not update uses), and the subsequent IR will not yet be processed. This
|
||||
can create confusion if a pattern attempts to match against a sequence of
|
||||
ops (e.g. rewrite A + B -> C). That sort of rewrite should be performed in a
|
||||
separate pass.
|
||||
```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;
|
||||
};
|
||||
```
|
||||
|
||||
#### Type Safety
|
||||
|
||||
The types of the remapped operands provided to a conversion pattern must be of a
|
||||
type expected by the pattern. The expected types of a pattern are determined by
|
||||
a provided [TypeConverter](#type-converter). If no type converter is provided,
|
||||
the types of the remapped operands are expected to match the types of the
|
||||
original operands. If a type converter is provided, the types of the remapped
|
||||
operands are expected to be legal as determined by the converter. If the
|
||||
remapped operand types are not of an expected type, and a materialization to the
|
||||
expected type could not be performed, the pattern fails application before the
|
||||
`matchAndRewrite` hook is invoked. This ensures 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. The `TypeConverter` is used by patterns
|
||||
and by the general conversion infrastructure to convert the signatures of blocks
|
||||
and regions.
|
||||
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
|
||||
|
||||
As stated above, the `TypeConverter` contains several hooks for detailing how to
|
||||
convert types. Several of these hooks are detailed below:
|
||||
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 illegal source `Type` should be converted
|
||||
to N target types. If the source type is already "legal", it should convert to
|
||||
itself. Type conversions are specified via the `addConversion` method described
|
||||
below.
|
||||
|
||||
A `materialization` describes how a set of values should be converted to a
|
||||
single value of a desired type. An important distinction with 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.
|
||||
|
||||
* Argument Materialization
|
||||
|
||||
- An argument materialization is used when converting the type of a block
|
||||
argument during a [signature conversion](#region-signature-conversion).
|
||||
|
||||
* Source Materialization
|
||||
|
||||
- A source materialization converts from a value with a "legal" target
|
||||
type, back to a specific source type. This is used when an operation is
|
||||
"legal" during the conversion process, but contains a use of an illegal
|
||||
type. This may happen during a conversion where some operations are
|
||||
converted to those with different resultant types, but still retain
|
||||
users of the original type system.
|
||||
- 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 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 from a value with an "illegal" source
|
||||
type, to a value of a "legal" type. This is used when a pattern expects
|
||||
the remapped operands to be of a certain set of types, but the original
|
||||
input operands have not been converted. This may happen during a
|
||||
conversion where some operations are converted to those with different
|
||||
resultant types, but still retain uses of the original type system.
|
||||
- This materialization is used in the following situations:
|
||||
* When the remapped operands of a
|
||||
[conversion pattern](#conversion-patterns) are not legal for the
|
||||
type conversion provided by the pattern.
|
||||
|
||||
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 target materialization is
|
||||
required, but cannot be performed, the pattern application fails. If a source
|
||||
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
|
||||
/// Register a conversion function. A conversion function defines how a given
|
||||
/// source type should be converted. A conversion function must be convertible
|
||||
/// to any of the following forms(where `T` is a class derived from `Type`:
|
||||
/// * Optional<Type>(T)
|
||||
/// - This form represents a 1-1 type conversion. It should return nullptr
|
||||
|
@ -210,56 +311,53 @@ class TypeConverter {
|
|||
/// existing value are expected to be removed during conversion. If
|
||||
/// `llvm::None` is returned, the converter is allowed to try another
|
||||
/// conversion function to perform the conversion.
|
||||
/// 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)));
|
||||
}
|
||||
|
||||
/// Register a materialization function, which must be convertible to the
|
||||
/// following form:
|
||||
/// `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 "converts" 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 `llvm::None` if it failed but other
|
||||
/// materialization can be attempted, and `nullptr` on unrecoverable failure.
|
||||
/// It will only be called for (sub)types of `T`.
|
||||
///
|
||||
/// When attempting to convert a type, e.g. via `convertType`, the
|
||||
/// `TypeConverter` will invoke each of the converters starting with the one
|
||||
/// most recently registered.
|
||||
template <typename ConversionFnT>
|
||||
void addConversion(ConversionFnT &&callback);
|
||||
|
||||
/// Register a materialization function, which must be convertibe to the
|
||||
/// following form
|
||||
/// `Optional<Value>(PatternRewriter &, T, ValueRange, Location)`,
|
||||
/// where `T` is any subclass of `Type`. This function is responsible for
|
||||
/// creating an operation, using the PatternRewriter 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 `llvm::None`
|
||||
/// 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
|
||||
/// results in more than one type, or if a type conversion may persist after
|
||||
/// the conversion has finished.
|
||||
template <typename FnT>
|
||||
void addMaterialization(FnT &&callback);
|
||||
/// This method registers a materialization that will be called when
|
||||
/// converting an illegal block argument type, to a legal type.
|
||||
template <typename FnT,
|
||||
typename T = typename llvm::function_traits<FnT>::template arg_t<1>>
|
||||
void addArgumentMaterialization(FnT &&callback) {
|
||||
argumentMaterializations.emplace_back(
|
||||
wrapMaterialization<T>(std::forward<FnT>(callback)));
|
||||
}
|
||||
/// This method registers a materialization that will be called when
|
||||
/// converting a legal type to an illegal source type. This is used when
|
||||
/// conversions to an illegal type must 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(
|
||||
wrapMaterialization<T>(std::forward<FnT>(callback)));
|
||||
}
|
||||
/// This method registers a materialization that will be called when
|
||||
/// converting type from an illegal, or source, type to a legal type.
|
||||
template <typename FnT,
|
||||
typename T = typename llvm::function_traits<FnT>::template arg_t<1>>
|
||||
void addTargetMaterialization(FnT &&callback) {
|
||||
targetMaterializations.emplace_back(
|
||||
wrapMaterialization<T>(std::forward<FnT>(callback)));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Conversion Patterns
|
||||
|
||||
When type conversion comes into play, the general Rewrite Patterns can no longer
|
||||
be used. This is due to the fact that the operands of the operation being
|
||||
matched will not correspond with the operands of the correct type as determined
|
||||
by `TypeConverter`. The operation rewrites on type boundaries must thus use a
|
||||
special pattern, the `ConversionPattern`. This pattern provides, as an
|
||||
additional argument to the `matchAndRewrite` and `rewrite` methods, the set of
|
||||
remapped operands corresponding to the desired type. These patterns also utilize
|
||||
a special `PatternRewriter`, `ConversionPatternRewriter`, that provides special
|
||||
hooks for use with the conversion infrastructure.
|
||||
|
||||
```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;
|
||||
};
|
||||
```
|
||||
|
||||
These patterns have the same [restrictions](#restrictions) as the basic rewrite
|
||||
patterns used in dialect conversion.
|
||||
|
||||
### Region Signature Conversion
|
||||
|
||||
From the perspective of type conversion, the types of block arguments are a bit
|
||||
|
@ -268,15 +366,16 @@ 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 within the region, and all blocks that move
|
||||
into that 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 details on the operation, e.g. FuncOp, AffineForOp,
|
||||
etc. To convert the signature of just the region entry block, and not any other
|
||||
blocks within the region, the `applySignatureConversion` hook may be used
|
||||
instead. A signature conversion, `TypeConverter::SignatureConversion`, can be
|
||||
built programmatically:
|
||||
apply type conversions to all blocks within a given region, and all blocks that
|
||||
move into that region. As noted above, the conversions performed by this method
|
||||
use the argument materialization hook on the `TypeConverter`. 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 details on the operation, e.g. FuncOp,
|
||||
AffineForOp, etc. To convert the signature of just the region entry block, and
|
||||
not any other blocks within the region, the `applySignatureConversion` hook may
|
||||
be used instead. A signature conversion, `TypeConverter::SignatureConversion`,
|
||||
can be built programmatically:
|
||||
|
||||
```c++
|
||||
class SignatureConversion {
|
||||
|
@ -303,3 +402,43 @@ public:
|
|||
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 : 'std.return'(0x608000002e20) {
|
||||
"std.return"() : () -> ()
|
||||
|
||||
* Fold {
|
||||
} -> FAILURE : unable to fold
|
||||
|
||||
* Pattern : 'std.return -> ()' {
|
||||
** Insert : 'spv.Return'(0x6070000453e0)
|
||||
** Replace : 'std.return'(0x608000002e20)
|
||||
|
||||
//===-------------------------------------------===//
|
||||
Legalizing operation : 'spv.Return'(0x6070000453e0) {
|
||||
"spv.Return"() : () -> ()
|
||||
|
||||
} -> SUCCESS : operation marked legal by the target
|
||||
//===-------------------------------------------===//
|
||||
} -> SUCCESS : pattern applied successfully
|
||||
} -> SUCCESS
|
||||
//===-------------------------------------------===//
|
||||
```
|
||||
|
||||
This output is describing the legalization of an `std.return` operation. We
|
||||
first try to legalize by folding the operation, but that is unsuccessful for
|
||||
`std.return`. From there, a pattern is applied that replaces the `std.return`
|
||||
with a `spv.Return`. The newly generated `spv.Return` is then processed for
|
||||
legalization, but is found to already legal as per the target.
|
||||
|
|
Loading…
Reference in New Issue