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.
To utilize the framework, a few things must be provided:
When applying a conversion to a set of operations, there are several conversion modes that can be selected from:
Partial Conversion
illegal to remain unconverted. This allows for partially lowering parts of the module in the presence of unknown operations.applyPartialConversion.Full Conversion
applyFullConversion.Analysis Conversion
applyAnalysisConversion.The conversion target is the 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:
Legal
Dynamic
addi is only legal when operating on 32-bit integers.isDynamicallyLegal hook provided by ConversionTarget.Illegal
illegal must always be converted for the conversion to be successful. This action also allows for selectively marking specific operations, in an otherwise legal dialect, as illegal.An example conversion target is shown below:
struct MyTarget : public ConversionTarget { MyTarget(MLIRContext &ctx) : ConversionTarget(ctx) { //-------------------------------------------------------------------------- // Marking an operation as Legal: /// Mark all operations within the LLVM dialect are legal. addLegalDialects<LLVMDialect>(); /// Mark `std.constant` op is always legal on this target. addLegalOps<ConstantOp>(); //-------------------------------------------------------------------------- // Marking an operation as dynamically legal. /// Mark all operations within Affine dialect have dynamic legality /// constraints. addDynamicallyLegalDialects<AffineDialect>(); /// Mark `std.return` as dynamically legal. addDynamicallyLegalOp<ReturnOp>(); /// Mark `std.return` as dynamically legal, but provide a specific legality /// callback. addDynamicallyLegalOp<ReturnOp>([](ReturnOp op) { ... }); //-------------------------------------------------------------------------- // Marking an operation as illegal. /// All operations within the GPU dialect are illegal. addIllegalDialect<GPUDialect>(); /// Mark `std.br` and `std.cond_br` as illegal. addIllegalOp<BranchOp, 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 { ... } };
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, are the same as those described in the quickstart rewrites guide. 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 baz.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.
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 the general conversion infrastructure, to convert the signatures of blocks and regions.
As stated above, the TypeConverter contains several hooks for detailing how to convert types, several of these hooks are detailed below:
class TypeConverter { public: /// This hook allows for converting a type. This function should return /// failure if no valid conversion exists, success otherwise. 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. virtual LogicalResult convertType(Type t, SmallVectorImpl<Type> &results); /// This hook simplifies defining 1-1 type conversions. This function returns /// the type to convert to on success, and a null type on failure. virtual Type convertType(Type t); /// This hook allows for materializing a conversion from a set of types into /// one result type by generating a cast operation of some kind. The generated /// operation should produce one result, of 'resultType', with the provided /// 'inputs' as operands. This hook must be overridden when a type conversion /// results in more than one type, or if a type conversion may persist after /// the conversion has finished. virtual Operation *materializeConversion(PatternRewriter &rewriter, Type resultType, ArrayRef<Value *> inputs, Location loc); };
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.
struct MyConversionPattern : public ConversionPattern { /// The `matchAndRewrite` hooks on ConversionPatterns take an additional /// `operands` parameter, containing the remapped operands of the original /// operation. virtual PatternMatchResult matchAndRewrite(Operation *op, ArrayRef<Value *> operands, ConversionPatternRewriter &rewriter) const; };
From the perspective of type conversion, the entry block to a region is often special. The types of the entry block arguments are often tied semantically to details on the operation, e.g. FuncOp, AffineForOp, etc. Given this, the conversion of the types for this block must be done explicitly via a conversion pattern. To convert the signature of a region entry block, a custom hook on the ConversionPatternRewriter must be invoked applySignatureConversion. A signature conversion, TypeConverter::SignatureConversion, can be built programmatically:
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); };
The TypeConverter provides several default utilities for signature conversion: convertSignatureArg/convertBlockSignature.