[Question] Conditional OneToN conversion using `OneToNOpConversionPattern`

Hi, I have a case where I need to decompose a subset of ops (belong to same op type) into multiple ops that work on smaller size inputs.

For example,

%0 = create_tile ... {decompose} : tile<32x16xf16>
%1 = create_tile ... : tile<32x16xf16>
%2 = load_tile %0 : tile<32x16xf16> -> vector<32x16xf16>
%3 = load_tile %1 : tile<32x16xf16> -> vector<32x16xf16>

Only create_tile ops with {decompose} attribute must be 1:N converted. like so,

%0 = create_tile ... : tile<16x16xf16>
%1 = create_tile ...  : tile<16x16xf16>

%2 = create_tile ... : tile<32x16xf16>

%3 = load_tile %0 : tile<16x16xf16> -> vector<16x16xf16>
%4 = load_tile %1 : tile<16x16xf16> -> vector<16x16xf16>

%5 = load_tile %2 : tile<32x16xf16> -> vector<32x16xf16>

Is this doable using the OneToNOpConversionPattern interface? I tried for some time without any success. When I define the type converter to convert tile<32x16xf16> -> 2 * tile<16x16xf16>, materialization hooks end up inserting unrealized_cast ops for the ops that I need unchanged.

Resulting in something like,

%0 = create_tile ... : tile<16x16xf16>
%1 = create_tile ...  : tile<16x16xf16>

%2 = create_tile ... : tile<32x16xf16>
%3:2 = unrealized_cast %2 : tile<32x16xf16> -> tile<16x16xf16>, tile<16x16xf16>

%4 = load_tile %0 : tile<16x16xf16> -> vector<16x16xf16>
%5 = load_tile %1 : tile<16x16xf16> -> vector<16x16xf16>

%6 = load_tile %3:0 : tile<32x16xf16> -> vector<32x16xf16>
%7 = load_tile %3:1 : tile<32x16xf16> -> vector<32x16xf16>

I also tried using usual OpRewritePattern and somehow maintaining the type legality by using manual unrealized_cast insertion which works fine. But it would be much nicer if I can do this using OneToNConversion. :slight_smile:

Any suggestions are highly appreciated.

It looks like you need a context-specific type converter: A type converter that is only applied to some ops. Neither the 1:N nor the regular dialect conversion support that at the moment.

You could try to run the 1:N dialect conversion without a type converter (pass a nullptr type converter to the pattern constructor). That should work with the regular dialect conversion and hopefully also the 1:N dialect conversion that you need in your case.

fyi, I am in the process of merging the two dialect conversion drivers, so this part of the API is going to change slightly soon.

1 Like

Thanks for the reply.

I tried this approach, but it did not work out. Then OneToNConversion complains that in some cases there is no 1:N conversions and you have just 1:1 conversion (match in number of outputs). Looks like OneToNConversion is not designed to handle this case.

Did it trigger an assertion?

yes. If my oneToNTypeConverter does not have any 1:N type conversions added. It gives me following error,.

`newValues.size() == resultMapping.getConvertedTypes().size()' failed.

My type converter is as follows,

OneToNTypeConverter typeConverter;
typeConverter.addConversion([](Type type) { return type; });
auto addNToOneCast = [](OpBuilder &builder, Type type, ValueRange inputs,
                            Location loc) {
      auto cast = builder.create<UnrealizedConversionCastOp>(loc, type, inputs);
      return std::optional<Value>(cast.getResult(0));
    };
auto addOneToNCast = [](OpBuilder &builder, TypeRange types, Value input,
                            Location loc) {
      auto cast = builder.create<UnrealizedConversionCastOp>(loc, types, input);
      return std::optional<ValueRange>(cast.getResults());
    };
typeConverter.addSourceMaterialization(addNToOneCast);
typeConverter.addArgumentMaterialization(addNToOneCast);
typeConverter.addTargetMaterialization(addOneToNCast);

If I remove,

typeConverter.addConversion([](Type type) { return type; });

my conversion pattern does not get called. Also I would like to keep the materialization hooks, because that makes handling control-flow ops much easier with signature conversions.

P.S.
I managed to get whole thing working by just using the 1:1 OpConversionPatterns, few drawbacks there are,

  1. Need to rely on unrealized_cast to reconcile types.
  2. Bit of a pain to handle ops that require signature conversions.
  3. I had to define a conversion target also where I marked the subset of ops that needs to be decomposed as illegal. Can not reply on OpRewritePatterns because I needed a type converter to do the signature conversion on For Ops.

I believe to handle this in 1:N conversion, I need a custom 1:N type converter that allows to look at some of the properties of the op before converting some type. Is there any plan on supporting something like that?

You are manually calling OneToNPatternRewriter::replaceOp, right? That’s where the assertion is failing. It looks like you are passing an incorrect resultMapping.

Actually, I don’t see how this is related to the type converter. (Or running a conversion pattern without a type converter.) OneToNPatternRewriter::replaceOp does not require a type converter.

Hi @matthias-springer, sorry about the late reply to this thread. With recent changes in the dialect conversion drivers, is this use case supported? To recap I want to 1:N convert a subset of ops in specific op type (say load → 2 loads based on some property of the load).

I was able to do this using some hacks described above, but with recent changes in upstream this no longer works. It crashes with,

LLVM ERROR: pattern 'optimizetranspose::LoadNdOpPattern' does not support 1:N conversion

Greatly appreciate if you can help.

Thanks!

I can confirm this is doable using both a combination of close study of the framework and 1 or 2 “questionable” things; you can take a look at this PR

that I recently put together (with a great deal of help from Matthias) that does exactly what you’re trying to do. It’s a fairly complex implementation so feel free to reach out if you have trouble seeing the “forest for the trees”.

1 Like

That is awesome. Thanks! I will take a look and get back to you.

I have an implementation (bit hacky) here. But I am looking for a nice way to do this.

I’m not sure if it will work for your use case, but you can play with ConversionTarget: marking an op as illegal only if a conversion pattern should be applied. If an op is already legal, it will not be touched by the conversion driver.

I am already using a conversion target. Here is my approach in summary,

  • I define a conversion target that filter outs the subset of ops that I need to 1:N convert using addDynamicallyLegalOp
  • For the ops that need 1:N conversion, I reply on UnrealizedConversionCast Op to preserve the types. Then I move on to consumers and fix them during conversion.

Code here:

  • Tricky bit is ForOp handling. Originally I define a OneToNTypeMapping and do a signature conversion to resolve the block argument inside the ForOp. This approach is failing with recent upstream changes. For example, the block arguments are expanded after the 1:N conversion but argument materialization hook does not insert the UnrealizedConversionCast ops I need. Do you have some idea why this is happening?

Code here:

Argument materializations are deprecated. The conversion driver does not use them anymore. The callback registration function on the type converter is still there, but it will be deleted soon.

Generally speaking, any mechanism that is based on the presence of UnrealizedConversionCastOp is fragile because those are just an implementation detail of the conversion driver.

This worked out perfectly for me. Thank you very much!

Also thanks @matthias-springer for the upstream changes. My code looks much simpler now.

1 Like