| # 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 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 don’t 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`. 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` method, 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. |
| |
| ```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 (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. |