I am in a situation where I think a generic 1:N type conversion utility would be an elegant solution, so I have looked into extending upstream for these cases. Before I spend more time, I’d like to gather input.
Problem
I have a composed type in my dialect that I want to decompose into its constituting parts. For the purpose of this discussion, we can pretend that this type is tuple
. Then “decomposing” a tuple<i32,i32>
consists of replacing it with two i32
s.
Let’s say we have undef
, insertvalue
, and extractvalue
ops for tuple
, then the “decomposition” of those essentially consists of forwarding SSA values:
func.func @testUndefInsertExtract(%value: i32) -> i32 {
%undef = tuple.undef : tuple<i32>
%inserted = tuple.insertvalue %value into %undef[0] : tuple<i32>
%extracted = tuple.extractvalue %inserted[0] : tuple<i32>
return %extracted : i32
}
Turns into:
func.func @testUndefInsertExtract(%value: i32) -> i32 {
return %value : i32
}
The above example requires some patterns specific to tuple
and undef
, insertvalue
, and extractvalue
. Further examples include “decompositions” across ops of func
and scf
. For example, the following passes a tuple
in and out of a function call:
func.func @testFuncCall(%arg : i32) -> (i32, i32) {
%undef_outer = tuple.undef : tuple<i32, tuple<i32>>
%undef_inner = tuple.undef : tuple<i32>
%inserted_inner = tuple.insertvalue %arg into %undef_inner[0] : tuple<i32>
%inserted_outer_one = tuple.insertvalue %arg into %undef_outer[0] : tuple<i32, tuple<i32>>
%inserted_outer_two = tuple.insertvalue %inserted_inner into %inserted_outer_one[1] : tuple<i32, tuple<i32>>
%call_result = func.call @testFuncFunc(%inserted_outer_two) : (tuple<i32, tuple<i32>>) -> tuple<i32, tuple<i32>>
%extracted_value_one = tuple.extractvalue %call_result[0] : tuple<i32, tuple<i32>>
%extracted_inner = tuple.extractvalue %call_result[1] : tuple<i32, tuple<i32>>
%extracted_value_two = tuple.extractvalue %extracted_inner[0] : tuple<i32>
return %extracted_value_one, %extracted_value_two : i32, i32
}
Through “decomposition” (including the corresponding func.func
and func.return
ops) this turns into:
func.func @testFuncCall(%arg0: i32) -> (i32, i32) {
%0:2 = call @testFuncFunc(%arg0, %arg0) : (i32, i32) -> (i32, i32)
return %0#0, %0#1 : i32, i32
}
Note that populateFunctionOpInterfaceTypeConversionPattern
, populateCallOpTypeConversionPattern
, populateSCFStructuralTypeConversionsAndLegality
, and similar exist to apply type conversions to some ops in func
, scf
, and similar, but AFAIK, only the latter handles 1:N conversions (and only since very recently).
Current support
While implementing a pass that does the above manually, my impression of the support for 1:N conversions in the current API was the following:
Existing support
-
TypeConverter::addConversion
with arguments of the formstd::optional<LogicalResult>(T, SmallVectorImpl<Type> &)
supports 1:N conversions. -
materialize(Argument|Source|Target)Conversion(OpBuilder &builder, Location loc, Type resultType, ValueRange inputs)
supports 1:N conversions (but that isn’t enough, see below). -
SignatureConversion
allows to express a series of 1:N conversions, and these can be created withconvertSignatureArg[s]
and similar and then applied to blocks and regions withapplySignatureConversion
and similar (though not all cases, as a comment indicates). -
populateSCFStructuralTypeConversionsAndLegality
supports 1:N type conversions for SCF ops since very recently (though with self-rolled solutions for the missing features below).
NB: The only places (except for a few unit tests) where the above is used seems to be the SparseTensor
dialect. I have added assert
s in addConversion
that fail on 1:N conversions and only those start breaking (under check-mlir
).
Missing support
-
TypeConverter::materializeTargetConversion
misses an overload for N:M (or at least N:1) conversions, i.e.,SmallVector<Value> materializeTargetConversion(OpBuilder &builder, Location loc, TypeRange resultType, ValueRange inputs)
. I believe that this is required to implement the target materialization of a 1:N conversion, where the “target” types are the N types. I think of it as the “reverse” of the corresponding source materialization, so if the latter is 1:N, the former has to be N:1. -
(Op)?ConversionPattern::matchAndRewrite(Operation *op, ArrayRef<Value> operands, ConversionPatternRewriter &rewriter)
hard-code 1:1 type conversions in their signature: each entry inoperands
corresponds to the one type resulting from the conversion from the original type at the same index. -
OpConversionPattern::(rewrite|matchAndRewrite)(SourceOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter)
hard-code 1:1 type conversions via the generated adaptors, which hard-code the fact that each accessor returns one (converted) value. -
ConversionPatternRewriterImpl::remapValues
simply does not handle 1:N conversions (because existing patterns rely on that behavior, as a comment indicates).
Questions
- Is the above analysis of the current state accurate and complete? If not, what is wrong or missing?
- What is the current plan for extending the support for 1:N type conversions? Have there been any recent discussions on the topic?
- Is anybody actively working on this topic or is planning to do so?
- What are possible/preferred solutions to the missing features above?
I could spend some time working on this topic if I get some guidance. After collecting some ideas and advice here, I can work on an RFC.
@mogball @river707 @ftynse @mehdi_amini @nicolasvasilache @PeimingLiu @aartbik