This is an archive of the discontinued LLVM Phabricator instance.

[mlir][Analysis][NFC] Split FlatAffineConstraints class
ClosedPublic

Authored by springerm on Aug 8 2021, 11:00 PM.

Details

Summary
  • Extract "value" functionality of FlatAffineConstraints into a new derived FlatAffineValueConstraints class. Current users of FlatAffineConstraints can use FlatAffineValueConstraints without additional code changes, thus NFC.
  • FlatAffineConstraints no longer associates dimensions with SSA Values. All functionality that requires this, is moved to FlatAffineValueConstraints.
  • FlatAffineConstraints no longer makes assumptions about where Values associated with dimensions are coming from.

Depends On: D107814

Diff Detail

Event Timeline

springerm created this revision.Aug 8 2021, 11:00 PM
springerm requested review of this revision.Aug 8 2021, 11:00 PM
Herald added a project: Restricted Project. · View Herald TranscriptAug 8 2021, 11:00 PM
springerm added inline comments.Aug 8 2021, 11:02 PM
mlir/include/mlir/Analysis/AffineStructures.h
642–648

I changed this comment a bit. Is it correct?

mlir/lib/Analysis/AffineStructures.cpp
2711–2730

Is this good?

ftynse added a comment.Aug 9 2021, 8:10 AM

Generally looks good, two high-level comments:

  1. Keeping the notion of "id" or "identifier" in the (new) FlatAffineConstraints class sounds misleading, "id" is a Value and the base class should be free of this concern. For naming purposes, I suppose we can call the generalization of "dimension" and "symbol" a variable, so we have a system of constraints on variables which can be dimensions, symbols or local variables. Since we know it's a flat list of constraints, we can also use "column" to refer to coefficients for each variable in all constraints.
  2. We shouldn't use RTTI / dynamic_cast. Thinking about this more, now that you looked at all the users of FlatAffineConstraints, do you think we actually need inheritance? That is, do we have a lot of functions that take a pointer/reference to FlatAffineConstraints and are expected to work transparently on FlatAffineValueConstraints? If not, we may also consider FlatAffineValueConstraints to contain FlatValueConstraints instead of deriving it. The former can expose a const reference accessors to constraints if necessary. This is the approach AffineValueMap uses, by the way, so it can also have the consistency argument.
mlir/include/mlir/Analysis/AffineStructures.h
237

I was surprised that we still have something with "Id" in the name at the FlatAffineConstraints level since it is not supposed to have IDs anymore. Actually, this just swaps the coefficients associated with dimensions or symbols identified by their (absolute) position. Maybe we can rename this to swapColumns or something similar?

242–245

Ideally, we would drop the notion of "Id" from this class entirely, it will only create confusion otherwise. addDim and addSymbol are fine, addId can become addColumn.

642
mlir/lib/Analysis/AffineStructures.cpp
282

The "dimensional identifier" part looks incorrect on a generic addId, it's not necessarily a dimension but a column of the specified kind.

2711–2730

Nope. LLVM must build without RTTI. If necessary, we can set this up to use LLVM's dyn_cast mechanism - https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html - but with MLIR-style TypeID instead of fixed enum.

Thinking about this more, now that you looked at all the users of FlatAffineConstraints, do you think we actually need inheritance? That is, do we have a lot of functions that take a pointer/reference to FlatAffineConstraints and are expected to work transparently on FlatAffineValueConstraints?

There is one that comes to mind: getFlattenedAffineExprs. E.g., it takes a FlatAffineConstraints in FlatAffineConstraints::composeMatchingMap and a FlatAffineValueConstraints in FlatAffineValueConstraints::addLowerOrUpperBound and FlatAffineValueConstraints::composeMap. But I think I can reimplement composeMap in terms of composeMatchingMap, similar to addLowerOrUpperBound in D107729, so that getFlattenedAffineExprs is always called with FlatAffineConstraints. Anyway, these are all "internal" users, so there should always be a way to refactor it. External users always use either FlatAffineConstraints or FlatAffineValueConstraints.

The former can expose a const reference accessors to constraints if necessary.

Requires a few more code changes, but should be possible. FlatAffineConstraints::fourierMotzkinEliminate etc. (everything that removes/adds columns) must be adapted when called through FlatAffineValueConstraints to ensure that the Value is also removed. (In the current revision, I just have a virtual removeId function.)

I'll give it a try!

springerm edited the summary of this revision. (Show Details)Aug 10 2021, 4:23 AM
springerm updated this revision to Diff 365426.Aug 10 2021, 4:29 AM
springerm edited the summary of this revision. (Show Details)

rebase

springerm edited the summary of this revision. (Show Details)Aug 10 2021, 4:31 AM

There is one that comes to mind: getFlattenedAffineExprs.

Hmm, I would have expected this to work without ids. AffineExprs don't know about those. The reason why it may be needing ids is some additional simplification of the map before/after the flattening. It should be possible to do this simplification in a separate call.

E.g., it takes a FlatAffineConstraints in FlatAffineConstraints::composeMatchingMap and a FlatAffineValueConstraints in FlatAffineValueConstraints::addLowerOrUpperBound and FlatAffineValueConstraints::composeMap. But I think I can reimplement composeMap in terms of composeMatchingMap, similar to addLowerOrUpperBound in D107729, so that getFlattenedAffineExprs is always called with FlatAffineConstraints. Anyway, these are all "internal" users, so there should always be a way to refactor it. External users always use either FlatAffineConstraints or FlatAffineValueConstraints.

Sounds good! Internal uses can also be function templates as long as the member functions they call have identical signatures. This will remove the need for virtual dispatch.

Requires a few more code changes, but should be possible. FlatAffineConstraints::fourierMotzkinEliminate etc. (everything that removes/adds columns) must be adapted when called through FlatAffineValueConstraints to ensure that the Value is also removed. (In the current revision, I just have a virtual removeId function.)

I'll give it a try!

Same idea as above. If we have removeColumn and template <MapTy> void fmEliminateImpl(...) that uses MapTy::removeColumn, we should be good to go.

springerm added a comment.EditedAug 10 2021, 9:39 PM

I ran into an inconvenience with the "composition" approach.

In the composition design, there are two classes:

class FlatAffineConstraints { /* ... */ };

class FlatAffineValueConstraints {
public:
  /* ... */
  const FlatAffineConstraints &getConstraints() const { return fac; }

private:
  FlatAffineConstraints fac;
  SmallVector<Optional<Value>, 8> ids;
};

FlatAffineValueConstraints contains the functions that require Value association (i.e., access to ids). All other functions can be called through obj.getConstraints().someFunc(). getConstraints returns a const reference, so users cannot call obj.getConstraints().addDimId(...). This would bring the state of fac and the FlatAffineValueConstraints object out of sync. Columns must be added/removed/swapped/... through the same functions defined on FlatAffineValueConstraints, which calls the fac implementation and ensures ids is updates correctly. So far so good.

The problem is, a large number of functions of FlatAffineConstraints is non-const. They may not even add/remove columns but just modify the constraints. Such functions cannot be called through getConstraints(). Instead, I need a wrapper function on FlatAffineValueConstraints that sometimes just calls the fac implementation and does nothing more. I'm mostly done with this refactoring and I found myself reimplementing/forwarding a quite big part of the API of FlatAffineConstraints.

For reference, these are functions that must be defined on FlatAffineValueConstraints, either with added functionality or as pure wrappers to get around the constness issue:

reset (2 overloads), append, &atEq, &atIneq, addAffineForOpDomain, addDomainFromSliceMaps, addAffineIfOpDomain, addLowerOrUpperBound (2 overloads), getSliceBounds, addSliceBounds, addInequality, addEquality, addConstantLowerBound (2 overloads), addConstantUpperBound (2 overloads), addLocalFloorDiv, setIdToConstant (2 overloads), swapId, addDimId, addSymbolId, addLocalId, addId, addInductionVarOrTerminalSymbol, composeMap, composeMatchingMap, getIneqAsAffineValueMap, projectOut (2 overloads), removeId, removeEquality, removeInequality, setDimSymbolSeparation, convertLoopIVSymbolsToDims, setAndEliminate, constantFoldId, constantFoldIdRange, unionBoundingBox, areIdsAlignedWithOther, mergeAndAlignIdsWithOther, getIds (2 overloads), getId (2 overloads), getIdValue, getIdValues, getAllIdValues, setIdValue, setIdValues, clearAndCopyFrom, removeIndependentConstraints, removeTrivialRedundancy, removeTrivialInequalities, removeRedundantConstraints, clearConstraints

E.g., functions such as addConstantLowerBound or removeTrivialRedundancy should not be part of the API of FlatAffineValueConstraints IMO.

And these are the functions that would have to be added to FlatAffineValueConstraints if we go for the "derived class" design, either as overridden functions or as completely new functions:

reset (3 overloads), addAffineForOpDomain, addDomainFromSliceMaps, addAffineIfOpDomain, addLowerOrUpperBound, getIneqAsAffineValueMap, addSliceBounds, setIdToConstant, findId, containsId, swapId, addDimId, addSymbolId, addId (2 overloads), addInductionVarOrTerminalSymbol, composeMap, projectOut, convertLoopIVSymbolsToDims, unionBoundingBox, unionBoundingBox, areIdsAlignedWithOther, clearAndCopyFrom, getIds (2 overloads), getId (2 overloads), getIdValue, hasIdValue, hasIdValues, getIdValues, getAllIdValues, setIdValue, setIdValues

The above functions are only the public ones. Maybe some of them could be made private, which solve the constness issue. But in the derived class design, even private functions remain a bit simpler IMO because I can just call a virtual function instead of extracting the function impl into a static function with a function template, as you suggested above. (Also, functions such as removeId are sometimes not called directly but via another function, so maybe I would have to forward the template etc...)

Another concern I have is with code maintenance. There is an implicit assumptions that every call to addDim etc. (everything that modifies the set of columns) must go through FlatAffineValueConstraints. If someone makes a change to FlatAffineConstraints which modifies columns, but does not update FlatAffineValueConstraints, there's a chance that the ids go out of sync. This should be prevented by the fact that getConstraints() returns a const reference. So this is mainly a problem in case of changes to functions that I had to split into two parts as part of this refactoring: E.g., unionBoundingBox is split into two parts: It computes the union in FlatAffineConstraints, whereas the FlatAffineValueConstraints aligns the columns, then calls the fac implementation. But then, nothing prevents other people from making changes to columns in FlatAffineConstraints::unionBoundingBox.

Overall, I think the derived class design would be better here. What do you think?

springerm added inline comments.Aug 11 2021, 1:56 AM
mlir/include/mlir/Analysis/AffineStructures.h
237

"id" seems to be used quite a lot in here, e.g.: addDimId, addLocalId, swapId, etc.

I think whenever "id" refers to a value, it should be renamed. E.g.: containsId --> containsValue or findId --> findValue. This would be a rather small change.

We could also rename all the other uses of "id", e.g.: swapId --> swapColumn. But to be consistent, we should then also rename stuff such as addDimId --> addDimColumn or areIdsAligned --> areColumnsAligned.

I'll prepare a new (dependent) commit that renames id to value where appropriate. (I think it should be a separate commit to make the review easier, because right now code is simply copied/moved around, which is easy to review.)

Not sure what to do with id --> column. Is it worth it? Either one is fine with me.

springerm added inline comments.Aug 11 2021, 3:16 AM
mlir/include/mlir/Analysis/AffineStructures.h
237

There is a actually a difference between "ids" and "columns".

numCols = numIds + 1

There is one column for the constant value, which is not an "identifier" (or whatever you may call it). So "column" may not be the right word in general. But I think in many places the "id" part can simply be dropped.

Overall, I think the derived class design would be better here. What do you think?

Generally, whatever requires the least non-trivial code. Overrides are fine as long as we don't need RTTI, the cost of a virtual call is neglectable when we compute things like Fourier-Motzkin.

ftynse added inline comments.Aug 11 2021, 2:04 PM
mlir/include/mlir/Analysis/AffineStructures.h
237

Not sure what to do with id --> column. Is it worth it? Either one is fine with me.

I don't feel strongly about this, just want to avoid potential confusion.

There is one column for the constant value, which is not an "identifier" (or whatever you may call it). So "column" may not be the right word in general. But I think in many places the "id" part can simply be dropped.

"Variable" is another alternative that generalizes "dimension" and "symbol" and clearly excludes constants. Anything that makes it easy to differentiate whether the method touches Value parts or not sounds helpful here. At the same time, the smaller the API change, the better.

springerm marked 2 inline comments as done.Aug 11 2021, 11:31 PM

Overall, I think the derived class design would be better here. What do you think?

Generally, whatever requires the least non-trivial code. Overrides are fine as long as we don't need RTTI, the cost of a virtual call is neglectable when we compute things like Fourier-Motzkin.

OK, then this commit is ready as it.

mlir/include/mlir/Analysis/AffineStructures.h
237

I decided to leave the term identifier. I think this is OK and well documented in the class comment of FlatAffineConstraints.

I renamed id to value when it refers to an associated SSA value.

The changes are in a separate commit, so that this commit doesn't get any longer (D107947).

springerm marked 4 inline comments as done.Aug 11 2021, 11:38 PM
springerm added inline comments.
mlir/lib/Analysis/AffineStructures.cpp
282

Removed the comment entirely. (There's already a header file comment.)

springerm updated this revision to Diff 365938.Aug 12 2021, 2:14 AM
springerm marked an inline comment as done.

update

bondhugula added inline comments.Aug 14 2021, 9:29 AM
mlir/include/mlir/Analysis/AffineStructures.h
61

I'd just avoid mentioning RTTI (although this is custom RTTI) -- it's confusing and unnecessary. This comment can instead be replaced with a line actually documenting the sub-class kinds.

62

Doc comment on the kind or the enum class mentioning about the specialization.

104

Triple /// comments. I know some of the other methods below missed it.

484

Nit: No need of the comma in between I think.

528

Triple /// comment.

springerm updated this revision to Diff 366537.Aug 15 2021, 6:00 PM
springerm marked 3 inline comments as done.

address comments

Great, thanks for refactoring for better reuse, LGTM!
Do you plan on further separating APIs that require particular dim/sym interpretation?

mlir/include/mlir/Analysis/AffineStructures.h
681

Nit: I'd add some newlines and align the left braces.

This revision is now accepted and ready to land.Aug 16 2021, 10:03 AM
springerm marked an inline comment as done.Aug 16 2021, 6:07 PM

Do you plan on further separating APIs that require particular dim/sym interpretation?

No further plans at the moment. Is there a particular refactoring that you have in mind?

mlir/include/mlir/Analysis/AffineStructures.h
681

Updating as part of D107947.

This revision was automatically updated to reflect the committed changes.
springerm marked an inline comment as done.