[RFC] DialectInterface for CSE

Summary

This RFC proposes DialectInterface for OperationEquvalence and CSE to allow downstream users to ignore specific optional attributes in CSE. Prototype implementation is here: ⚙ D154512 [Not-For-Commit][mlir][CSE][OperationSupport] Introduce DialectInterfaces for CSE

Background

Previous thread: CSE with optional/mergable attributes
In CIRCT there is an optional attribute “sv.namehint” which is meant to be “hint” for Verilog Emitter to produce more readable outputs. Frontends attach namehints to operations but we don’t want to block optimizations because of the existence of namehints. Therefore we want to make CSE ignore these attributes but I think the current CSE is not extensible from the user side. For example we want to CSE these three operations and merge attributes in the specific rule:

  %0 = comb.and %a, %b {sv.namehint = "_bad_name1"} : i1
  %1 = comb.and %a, %b {sv.namehint = "good_name"} : i1
  %2 = comb.and %a, %b {sv.namehint = "_bad_name2"} : i1
  return %0, %1, %2

=>

 %0 = comb.and %a, %b {sv.namehint = "good_name"} : i1 // we want to prioritize `good_name`.
 return %0, %0, %0

Proposed solution

CSE pass basically consists of two parts; OperationEquivalence(hash operation, check operation equivalence) and CSE transform so we want to introduce DialectOperationEquivalenceInterface and DialectCSEInterface to hook these two components respectively.

DialectOperationEquivalenceInterface

This interface controls how attributes participate in the hashing or equivalence checking.

class DialectOperationEquivalenceInterface {
  /// Returns true if the given 'attr' contains attributes that can be
  /// ignored in the equivalence comparison.
  virtual bool containsNonEssentialAttribute(DictionaryAttr attr) const {
    return false;
  }

  /// Returns true if the given named attribute 'namedAttr' can be ignored
  /// in the equivalence comparison.
  virtual bool isNonEssentialAttribute(NamedAttribute namedAttr) const {
    return false;
  }
};

Hashes of attributes which isNonEssentialAttribute returns true will not be combined into an operation hash. In CIRCT, the implementation would be:

bool containsNonEssentialAttribute(DictionaryAttr attr) { return attr.contains("sv.namehint");}
bool isNonEssentialAttribute(NamedAttribute namedAttr) { return namedAttr.getName() == "sv.namehint";}`

DialectCSEInterface

This interface controls how operation attributes are merged into one to another while CSE.

class DialectCSEInterface {

  /// Returns a dictionary attribute that are combined derived from
  /// `opAttr` (attributes attached to an op that is going to be eliminated) and
  /// `existingOpAttr`.
  virtual DictionaryAttr
  combineDiscardableAttributes(DictionaryAttr opAttr,
                               DictionaryAttr existingOpAttr) const {
    return existingOpAttr;
  }
};

The CIRCT implementation would be like this:

  DictionaryAttr combineDiscardableAttributes(DictionaryAttr opAttr,
                               DictionaryAttr existingOpAttr) const override {
    if (auto src = opAttr.getAs<StringAttr>("sv.namehint")) {
      // If both has "sv.namehint" attr, set the smaller value.
      if (auto dest = existingOpAttr.getAs<StringAttr>("sv.namehint")) {
          ...
          // If `src` is better name than `dest`, then update and return attributes.
      }
    }

    return existingOpAttr;
  }

Alternative considered

  • Operational Equivalence already has a flag to ignore locations on operations or block arguments so it might make sense to extend the flag to ignore discardable (or dialect) attributes. I prefer the proposed dialect interface approach as I believe the approach is more extensible.
  • Another possible solution might be to expose CSE as a library which parametrizes SimpleOperationInfo.

Feedback is highly appreciated!

What about encoding the hint as part of the location? This seems pretty much what it is but just for different tool. So you’d have a FusedLoc with a NameLoc and whatever else. Convention on export is NameLoc corresponds to name hint.

Thank you for the suggestion! Location could work, but one issue I guess is that it would cause names to be assigned to semantically different operations since currently locations are freely fused and propagated everywhere (in both CIRCT and MLIR side). For example, rewrite rules generated by TDRR create a single fused location in the input operations, and use the location for created operations. In that case names in the location would be distributed to different operations. We want namehints to be precise if preserved until the end since incorrect names will be source of confusion for frontend users.

For fused op, how are you specifying name hint propagation today with TDRR? (TDRR also allows specifying locations instead of what the default is)

(it is definitely true that if you use arbitrary patterns outside your control that the location could be mutated by one and suddenly you don’t know what you get, but I also don’t know how name hint is preserved then)

1 Like

For fused op, how are you specifying name hint propagation today with TDRR?

We are inserting a name propagation pass through MoveNameHint in TDRR:

(it is definitely true that if you use arbitrary patterns outside your control that the location could be mutated by one and suddenly you don’t know what you get, but I also don’t know how name hint is preserved then)

It’s definitely ok to drop namehints for optimization, so we are dropping namehints which are not on root operations in canonicalizer. However we want to keep the namehints to be correct.

I would suggest something a little broader, maybe something like this?

class DialectOperationEquivalenceInterface {
  /// Returns true if the two provided operations are equivalent.
  virtual bool operationsAreEquivalent(Operation *lhs, Operation *rhs) const {
    return false;
  }
};

The reason I bring this up is that you might have operations that have attributes that are essential, but those attributes should be considered equivalent for whatever reason.

I haven’t fully thought through the implications on the CSE interface, but I assume it’d be pretty simple to incorporate.

2 Likes

Providing interface to control the behavior of analysis and transformations seems good to me, I am not sure we can have a DialectOperationEquivalenceInterface though: the “equivalence” between operation is likely context dependent. That is the answer for the purpose of CSE may be different than for other purpose, so I would focus on a CSE specific interface right now.

Thanks! The current proposed interface is intentionally limited to optional attributes right now to reduce the scope, but if more generalized version is preferred I’d be happy to make the interface more generalized one!

Providing interface to control the behavior of analysis and transformations seems good to me, I am not sure we can have a DialectOperationEquivalenceInterface though: the “equivalence” between operation is likely context dependent. That is the answer for the purpose of CSE may be different than for other purpose, so I would focus on a CSE specific interface right now.

That’s reasonable, in that case DialectCSEInterface should have hash function and equivalence checker that will be passed to corresponding SimpleOperationInfo members. We will also have to add extra callback to member functions of OperationEquivalence such as OperationEquivalence::computeHash or OperationEquivalence::isEquivalentTo to hook hash and equivalence check.

Based on the suggestions the interface DialectCSEInterface would be like this:

class DialectCSEInterface {
  /// Returns true if the operation is considered to be equivalent.
  virtual bool areOperationsEquivalent(Operation* lhs, Operation* rhs) const {
    return OperationEquivalence::isEquivalentTo(...);
  }

  /// Returns hash value for operation.
  virtual unsigned getHashValue(Operation* op) const {
    return OperationEquivalence::getHashValue(...);
  }
  
  /// Merge `op` (operation which is going to be CSEd and eliminated) to `existingOp`
  virtual void mergeOperations(Operation* op, Operation* existingOp) const {
  }
};
struct SimpleOperationInfo : public llvm::DenseMapInfo<Operation *> {
  static unsigned getHashValue(const Operation *opC) {
     auto interface = dyn_cast_or_null<DialectCSEInterface>(opC->getDialect() );
     if(interface) return interface->getHashValue(opC);
     return ...; // default implementation  
  }
  static bool isEqual(const Operation *lhsC, const Operation *rhsC) {
     auto interface = dyn_cast_or_null<DialectCSEInterface>(lhsC->getDialect() );
     if(interface) return interface->areOperationsEquivalent(lhsC, rhsC);
     return ...; // default implementation
  }

This approach makes sense to me - it’s plenty flexible and it gets at the root of the issue we’re trying to solve without touching too much extra stuff :slight_smile:

1 Like

Sorry for delay a bit. I updated the original patch ⚙ D154512 [mlir][CSE] Introduce DialectInterfaces for CSE based on OperationEquivalence changes ⚙ D155577 [mlir][OperationEquivalence] Add an extra callback to hook operation equivalence check. Both are ready for review.

Hello,

Is there any progress on these patches? These looks generally useful and we may need something similar in our pipeline soon.

Comment on code: can we make DialectCSEInterface interface methods to return optional, so dialect can override equivalence for some ops, but rely on default behavior for others?

Sorry it became a little less important in my use case and didn’t make it until the end. Nonetheless it’s still useful so I think I can allocate time next week to rebase and work on these patches. I’ll tag you on the reviews.

Comment on code: can we make DialectCSEInterface interface methods to return optional , so dialect can override equivalence for some ops, but rely on default behavior for others?

This makes sense to me. Let me explore that direction.

1 Like

Any progress? If not, I can take old patches, rebase them myself and create PRs.

Sorry I was not able to find time for it. Could you take over the patches?

Yes, I’ll do it.

1 Like

First patch [mlir][OperationEquivalence] Add an extra callback to hook operation equivalence check by Hardcode84 · Pull Request #73455 · llvm/llvm-project · GitHub

Second patch [mlir][CSE] Introduce DialectInterface for CSE by Hardcode84 · Pull Request #73520 · llvm/llvm-project · GitHub