This is an archive of the discontinued LLVM Phabricator instance.

[mlir] Prevent SubElementInterface from going into infinite recursion
ClosedPublic

Authored by myhsu on Jun 10 2022, 2:20 PM.

Details

Summary

Since only mutable types and attributes can go into infinite recursion inside SubElementInterface::walkSubElement, and there are only a few of them (mutable types and attributes), we introduce a new flag in AbstractType/Attribute to indicate if a type or attribute is mutable. Then, inside SubElementInterface, we use a set to record visited mutable types and attributes that have been visited before.

NOTE: None of the code is using SubElementType/AttrInterface with potential recursion at this time, thus I don't attach any test case. However, we can merge this patch with D127539, which applies SubElementTypeInterface to LLVM dialect's types and provide chances to exercise the changes here, if that's the best way to move forward.

Diff Detail

Event Timeline

myhsu created this revision.Jun 10 2022, 2:20 PM
Herald added a project: Restricted Project. · View Herald Transcript
myhsu requested review of this revision.Jun 10 2022, 2:20 PM
myhsu edited the summary of this revision. (Show Details)Jun 10 2022, 2:28 PM

Ah, I didn't think this was a possibility. It's unfortunate that walkSubElements has to be slowed down in general. It's particularly hot due to SymbolTable.

I think we can optimize for the common case and have the first N recursions not track a set (1, 2?). Also, is it possible to have nested types stack overflow the walk function?

mlir/lib/IR/SubElementInterfaces.cpp
71

What about SmallPtrSet<Pointer union<Type,Attribute>>?

Yeah, the set should be unnecessary in 99% of cases. This is something that is only necessary for mutable attributes/types, given that those are the only ones that could be recursive. Conceptually we could detect mutability on registration, add a bit for that to the AbstractAttribute/Type classes, and check that here.

myhsu updated this revision to Diff 436865.Jun 14 2022, 11:27 AM

This is a major revision.

  • Add an isMutable flag on both AbstractType/Attribute to indicate whether the underlying type/attribute (storage) is mutable. This is done by checking if there is a member function LogicalResult mutate(StorageAllocator &, ...) defined in the storage class via SFINAE facilities.
  • SubElementTypeInterface now only does bookkeeping on mutable types or attributes.
  • Use unit test to test the new mutable type flag. Which effectively moves part of the unit test infrastructure from D127539 to here.
myhsu added inline comments.Jun 14 2022, 11:30 AM
mlir/lib/IR/SubElementInterfaces.cpp
71

Given that these sets are not on the hot path anymore, do you think it's still better to change the container type? I feel like two separate sets are more readable.

myhsu edited the summary of this revision. (Show Details)Jun 14 2022, 11:31 AM

Could mutability not be a trait instead?

mlir/include/mlir/IR/StorageUniquerSupport.h
79

LLVM has a utility for this. LLVM::is_detected. You can find an example in AsmParser

mlir/lib/IR/SubElementInterfaces.cpp
19

I would have two separate recursive functions. One with the set and one without

71

Yeah no preference then

80–83

Drop llvm::

Mogball requested changes to this revision.Jun 14 2022, 11:59 AM
This revision now requires changes to proceed.Jun 14 2022, 11:59 AM
myhsu added a comment.Jun 14 2022, 1:01 PM

Could mutability not be a trait instead?

It is possible, for instance an opaque LLVMStruct cannot be mutated. But to have such granularity, we need to rely on non-static information, which is something AbstractType/AbstractAttribute cannot carry. Of course, we can have isMutable exposed on Type and Attribute, but I'm not sure about that. What do you think?

Mogball added a comment.EditedJun 14 2022, 1:55 PM

I'm not sure I understand what you're getting at. Mutability is tied to the abstract type/attribute implementation. Instead of abstractType->isMutable(), could we not have abstractType->hasTrait<TypeTrait::Mutable>()?

myhsu added a comment.Jun 15 2022, 9:38 AM

I'm not sure I understand what you're getting at. Mutability is tied to the abstract type/attribute implementation. Instead of abstractType->isMutable(), could we not have abstractType->hasTrait<TypeTrait::Mutable>()?

I see, I thought you were referring to "trait" as a general C++ concept rather than MLIR's Trait.

IIRC trait needs to be manually annotated on a type or attribute, while my current approach detects mutability out of the box. The reason I chose the current way was because TypeStorage/AttributeStorage use the same way to see if a type/attribute can be mutated. And I'm a little worry that using trait might make such property out-of-sync with TypeStorage/AttributeStorage, if, let's say, a dialect forgets to annotate their mutable types or attributes with trait.

I think @ftynse introduced mutable types and attributes and may be able to weigh in on this.

MLIR doesn't really "detect" mutability, more so "mutate" fails to compile if the type doesn't implement the function. Now that we're trying to query mutability, storing the results of a static check in a bit doesn't seem like the way to go. Traits make sense in this regard. To keep the two in sync, mutate can static assert that the concrete type implements the trait and the trait's verifier can static assert that the concrete type implements the function.

I'm not sure I understand what you're getting at. Mutability is tied to the abstract type/attribute implementation. Instead of abstractType->isMutable(), could we not have abstractType->hasTrait<TypeTrait::Mutable>()?

I see, I thought you were referring to "trait" as a general C++ concept rather than MLIR's Trait.

IIRC trait needs to be manually annotated on a type or attribute, while my current approach detects mutability out of the box. The reason I chose the current way was because TypeStorage/AttributeStorage use the same way to see if a type/attribute can be mutated. And I'm a little worry that using trait might make such property out-of-sync with TypeStorage/AttributeStorage, if, let's say, a dialect forgets to annotate their mutable types or attributes with trait.

Having a MutableTypeInterface/MutableAttrInterface for specifying this seems like a good step forward. Manual annotation isn't necessarily a bad thing here, ODS can always sugar this better (though ODS doesn't support mutable attribute/types right now). We should consider the desired end state of specifying mutability for attributes and types.

myhsu added a comment.Jun 16 2022, 9:51 AM

I'm not sure I understand what you're getting at. Mutability is tied to the abstract type/attribute implementation. Instead of abstractType->isMutable(), could we not have abstractType->hasTrait<TypeTrait::Mutable>()?

I see, I thought you were referring to "trait" as a general C++ concept rather than MLIR's Trait.

IIRC trait needs to be manually annotated on a type or attribute, while my current approach detects mutability out of the box. The reason I chose the current way was because TypeStorage/AttributeStorage use the same way to see if a type/attribute can be mutated. And I'm a little worry that using trait might make such property out-of-sync with TypeStorage/AttributeStorage, if, let's say, a dialect forgets to annotate their mutable types or attributes with trait.

Having a MutableTypeInterface/MutableAttrInterface for specifying this seems like a good step forward. Manual annotation isn't necessarily a bad thing here, ODS can always sugar this better (though ODS doesn't support mutable attribute/types right now). We should consider the desired end state of specifying mutability for attributes and types.

Yes, ideally we can have MutableTypeInterface/MutableAttrInterface with a mutate function. But mutate functions in existing TypeStorage/AttrStorage relies on template-parameterized function arguments, which I don't think it can fit into the facilities for interface (An interface's Concept declares virtual functions but virtual functions can't have template).

Right now I'm planning to use a simple TypeTrait/AttrTrait to specify mutability to go forward on this patch.

myhsu updated this revision to Diff 437737.Jun 16 2022, 3:40 PM
myhsu marked 5 inline comments as done.

Use a TypeTrait/AttributeTrait to indicate mutability instead of a flag in AbstractType/AbstractAttribute. I also added static asserts to make sure it syncs up with mutate function defined in the Type/Attribute storage

myhsu added inline comments.Jun 16 2022, 3:42 PM
mlir/include/mlir/IR/StorageUniquerSupport.h
81

FYI: For variadic functions, we can't use the normal way (like decltype(declval<T>().foo(...))) to match function signature.

mlir/lib/IR/SubElementInterfaces.cpp
19

If we split into two versions, the function without set can only call the same variant* (i.e. the one without set), which effectively makes an assumption that all sub-types of an immutable type are immutable -- I'm afraid this is not the case, a good counterexample is !llvm.ptr<struct<...>> (LLVM pointer is immutable while LLVM struct is mutable).

*: Even if we use a "local" set to record visited types like

void walkImmutableImpl(...) {
  set<Type> localVisited;
  if (mutable)
    walkMutableImpl(..., localVisited);
  else
    walkImmutableImpl(...);
}

It still can't prevent infinite recursion.

myhsu added inline comments.Jun 16 2022, 3:50 PM
mlir/include/mlir/IR/AttributeSupport.h
266–267 ↗(On Diff #437737)

I forgot to change this, just a second.

myhsu updated this revision to Diff 437743.Jun 16 2022, 3:55 PM

Fix minor issues.

Mogball added inline comments.Jun 16 2022, 3:56 PM
mlir/include/mlir/IR/AttributeSupport.h
265 ↗(On Diff #437737)

This should be somewhere else... In one of the mutate function wrappers?

myhsu added inline comments.Jun 16 2022, 4:09 PM
mlir/include/mlir/IR/AttributeSupport.h
265 ↗(On Diff #437737)

Doing checks in one of the mutate wrappers allows up to only check for IsMutableType trait (because the wrapper will only be compiled / compilable if mutate function is defined) but we can't check the other direction: If IsMutableType is present, whether mutate function is defined. Thus, the check I wrote here checks for both at the same time (it's an "inverse XOR" check so it will only complain if only either mutate function or trait is present), in that sense I feel like putting it in the registration function is better

Mogball added inline comments.Jun 16 2022, 6:48 PM
mlir/include/mlir/IR/AttributeSupport.h
265 ↗(On Diff #437737)

Can't you static_assert from the Trait::verify? Some other traits do that.

rriddle requested changes to this revision.Jun 16 2022, 6:59 PM
rriddle added inline comments.
mlir/include/mlir/IR/AttributeSupport.h
265 ↗(On Diff #437737)

Can't you static_assert from the Trait::verify? Some other traits do that.

Attribute and Type traits don't have verifiers.

265 ↗(On Diff #437743)

Can we pass the type of the trait to StorageUserBase instead? and have the static assert inside of StorageUserBase::mutate?

mlir/include/mlir/IR/Attributes.h
243

The Attr is redundant and doesn't add much.

mlir/include/mlir/IR/StorageUniquerSupport.h
79

We should be able to avoid this complexity. We really just need a static assert inside of StorageUserBase::mutate that it's only called when the parent is mutable. If necessary, we could do something similar to [1] that makes the static assert dependent on the template types of the mutate function (but ignores them)

[1]: https://github.com/llvm/llvm-project/blob/32bd0c1714b4733af7abb9f570e961ffc889c793/mlir/include/mlir/IR/PatternMatch.h#L823

mlir/include/mlir/IR/Types.h
234

Same here, I'd drop the Type.

This revision now requires changes to proceed.Jun 16 2022, 6:59 PM
myhsu updated this revision to Diff 438477.Jun 20 2022, 2:24 PM
myhsu marked 7 inline comments as done.
  • Rename mutable attributes.
  • Move static checks into StorageUserBase::mutate.
Mogball added inline comments.Jun 20 2022, 4:05 PM
mlir/include/mlir/IR/Attributes.h
30

Doesn't this mark all attributes with IsMutable?

Mogball added inline comments.Jun 20 2022, 4:11 PM
mlir/include/mlir/IR/Attributes.h
30

Ah, nevermind.

Mogball added inline comments.Jun 20 2022, 4:21 PM
mlir/include/mlir/IR/StorageUniquerSupport.h
186

This seems a little invasive to me -- that you have to pass in MutableTrait from the implementing classes. Can we not have a generic StorageUserTrait::IsMutable? That way every user of StorageUserBase doesn't need to also define its own mutability trait that's identical to all the other ones.

myhsu updated this revision to Diff 438753.Jun 21 2022, 10:47 AM
myhsu marked 3 inline comments as done.

Use a single StorageUserTrait::IsMutable to replace TypeTrait::IsMutable and AttributeTrait::IsMutable. The latter two, however, remain type aliases to StorageUserTrait::IsMutable for ease of use.

mlir/include/mlir/IR/StorageUniquerSupport.h
186

I agree that a single IsMutable trait is easier and less intrusive. But I feel like many dialect authors might find StorageUserTrait a little confusing since they don't necessarily know what storage is in this context, as it's an implementation detail. So I'm turning the Attribute/TypeTrait::IsMutable here into aliases to StorageUserTrait::IsMutable

myhsu added a comment.Jun 22 2022, 9:17 AM

Hi @rriddle do you have any further comments on this?

Can we have a test that checks that we no longer go into infinite recursion? Otherwise, LG!

(Sorry, coming back from OOO)

myhsu added a comment.Jun 27 2022, 1:09 PM

Can we have a test that checks that we no longer go into infinite recursion? Otherwise, LG!

(Sorry, coming back from OOO)

right, this patch is effectively preparing for D127539, which will go into infinite recursion without it. Otherwise, I don't think any of the type or attribute (as of the current HEAD) with this interface will go into infinite recursion. Thus, I think it's more difficult to write such test in this patch (D127539 has tests to make sure we don't go into infinite recursion though). What do you think?

rriddle accepted this revision.Jun 27 2022, 1:12 PM

I would prefer if we could test this outside of the LLVM dialect if possible, given that it should just be a matter of using the Recursive type in the test dialect right?

This revision is now accepted and ready to land.Jun 27 2022, 1:12 PM
myhsu added a comment.Jun 27 2022, 1:19 PM

I would prefer if we could test this outside of the LLVM dialect if possible, given that it should just be a matter of using the Recursive type in the test dialect right?

Yes I think this is possible, I forgot there is a test dialect where we can exercise this part of the code by making type alias, which calls SubElementInterface, on its recursive type.
I will update this diff accordingly later.

myhsu updated this revision to Diff 440382.Jun 27 2022, 2:13 PM

Add generic tests to check if SubElemenTypeInterface goes into infinite recursion. Please take another look.

Note that since SubElementAttributeInterface shares most of the logics with SubElemenTypeInterface (including the newly introduced IsMutable trait), plus we don't have recursive attribute in TestDialect right now, I don't add a similar test for attribute.