Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mlir] Introduce wrapInZeroTripCheck in LoopLikeOpInterface #80331

Closed
wants to merge 9 commits into from

Conversation

pzread
Copy link
Member

@pzread pzread commented Feb 1, 2024

Adds a new method wrapInZeroTripCheck to LoopLikeOpInterface, to create zero-trip-check around the loop

The purpose is to let loop ops (e.g. scf.while, scf.for) implement their own transformations to add zero-trip-check. The zero-trip-check creates a guard (e.g. scf.if) around the loop and the condition will be true only if the loop body will run at least once. An example usage is to combine with mlir::moveLoopInvariantCode to hoist resource-intense loop invariants into that guard region so they will only run once but also not run when the loop body won't run at all.

The implementation for scf.while can be found in the follow-up change: #80349

@pzread pzread force-pushed the zero-trip-check branch 2 times, most recently from c07dce7 to 70f54b5 Compare February 1, 2024 23:14
@pzread pzread changed the title [mlir][WIP] Introduce replaceWithZeroTripCheck to LoopLikeOpInterface [mlir] Introduce replaceWithZeroTripCheck in LoopLikeOpInterface Feb 1, 2024
@pzread pzread requested a review from dcaballe February 1, 2024 23:47
@pzread pzread marked this pull request as ready for review February 1, 2024 23:47
@llvmbot llvmbot added the mlir label Feb 1, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Feb 1, 2024

@llvm/pr-subscribers-mlir-scf
@llvm/pr-subscribers-mlir-core

@llvm/pr-subscribers-mlir

Author: Jerry Wu (pzread)

Changes

Adds a new method replaceWithZeroTripCheck to LoopLikeOpInterface, to create zero-trip-check around the loop

The purpose is to let loop ops (e.g. scf.while, scf.for) implement their own transformations to add zero-trip-check. The zero-trip-check creates a guard (e.g. scf.if) around the loop and the condition will be true only if the loop body will run at least once. An example usage is to hoist resource-intense loop invariants into this guard region so they will only run once but also not run when the loop body won't run at all.

The implementation of scf.while can be found in the follow-up change: #80349


Full diff: https://github.com/llvm/llvm-project/pull/80331.diff

1 Files Affected:

  • (modified) mlir/include/mlir/Interfaces/LoopLikeInterface.td (+22)
diff --git a/mlir/include/mlir/Interfaces/LoopLikeInterface.td b/mlir/include/mlir/Interfaces/LoopLikeInterface.td
index e2ac85a3f7725..77409cb3a8274 100644
--- a/mlir/include/mlir/Interfaces/LoopLikeInterface.td
+++ b/mlir/include/mlir/Interfaces/LoopLikeInterface.td
@@ -220,6 +220,28 @@ def LoopLikeOpInterface : OpInterface<"LoopLikeOpInterface"> {
       /*defaultImplementation=*/[{
         return ::mlir::failure();
       }]
+    >,
+    InterfaceMethod<[{
+        Add a zero-trip-check around the loop to check if the loop body is ever
+        run and return the new loop inside the check. The loop body is moved
+        over to the new loop. Returns "failure" if the loop doesn't support
+        this transformation.
+
+        After the transformation, the ops inserted to the parent region of the
+        loop are guaranteed to be run only if the loop body runs at least one
+        iteration.
+
+        Note: Ops in the loop body might be rearranged because of loop rotating
+        to maintain the semantic. Terminators might be removed/added during this
+        transformation.
+      }],
+      /*retTy=*/"::mlir::FailureOr<::mlir::LoopLikeOpInterface>",
+      /*methodName=*/"replaceWithZeroTripCheck",
+      /*args=*/(ins "::mlir::RewriterBase &":$rewriter),
+      /*methodBody=*/"",
+      /*defaultImplementation=*/[{
+        return ::mlir::failure();
+      }]
     >
   ];
 


Note: Ops in the loop body might be rearranged because of loop rotating
to maintain the semantic. Terminators might be removed/added during this
transformation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also mention that loop conditions might be checked redundantly so they are expected to not have side effects and that the utility won't check that but just apply the transformation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few words to mention that.

Copy link
Collaborator

@joker-eph joker-eph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests for this?

@pzread
Copy link
Member Author

pzread commented Feb 2, 2024

Can you add some tests for this?

Done.

mlir/include/mlir/Interfaces/LoopLikeInterface.td Outdated Show resolved Hide resolved
@@ -220,6 +220,31 @@ def LoopLikeOpInterface : OpInterface<"LoopLikeOpInterface"> {
/*defaultImplementation=*/[{
return ::mlir::failure();
}]
>,
InterfaceMethod<[{
Add a zero-trip-check around the loop to check if the loop body is ever
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a "check" (it is not really clear what the "check" is; it could be an scf.if, it could be a new basic block with a conditional branch, etc.), this method could also return just the condition. (Which evaluates to true if the loop has at least one iteration.) That would keep the interface a bit simpler. What do you think?

Copy link
Member Author

@pzread pzread Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the tricky part of adding zero-trip-check is to do loop rotation when the loop condition has side-effects. So if we simply return the loop condition, callers will need to implement loop rotation by themselves for each loop op, which can be complicated.

If the kind of "check" op is a concern, maybe we can add a callback and let callers to create the check op in the callback by themselves?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to start with scf.if and generalize later as needed?

I think it's worth clarifying that loop rotation will only be possible for scf.while ops for now, that is, turning regular a "while-do" into a "do-while". That kind of rotation won't be possible at this level of abstraction for scf.for, hence the comment about redundant first iteration check. I think the rotation responsibility belongs to the interface itself.

If we want to make it extra configurable, we could separate the zero-trip-check method from the rotation method...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, I did not think of side effects.

The lambda sounds like a good idea, but I'm not sure what exactly it should look like. (Should it return a Block * into which the loop is moved?) We can generalize later if needed.

I think it would also work for scf.for. The check condition is lb < ub.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think whether the loop rotation is needed (or possible) is the implementation details of each type of loop. scf.while needs it because the before block is in the loop and can contain ops with side-effects. scf.for is simpler as the lb < ub should have no side-effect and lightweight, which can be run twice (for scf.if and the for loop)


EXPECT_TRUE(succeeded(result));
EXPECT_EQ(*result, testOp);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make it a pass: we're avoiding C++ unit-tests as much as possible.

I would implement the interface for scf.while and scf.for and have the pass just walk the IR, find all LoopLikeOpInterface, and call replaceWithZeroTripCheck.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. PTAL. I prefer to keep this change focusing on the interface definition so I only add a scf.while test with no transformation. The actual transformation and tests will be added in the follow-up change #80349

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kindly ping : )

mlir/include/mlir/Interfaces/LoopLikeInterface.td Outdated Show resolved Hide resolved
@@ -220,6 +220,31 @@ def LoopLikeOpInterface : OpInterface<"LoopLikeOpInterface"> {
/*defaultImplementation=*/[{
return ::mlir::failure();
}]
>,
InterfaceMethod<[{
Add a zero-trip-check around the loop to check if the loop body is ever
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to start with scf.if and generalize later as needed?

I think it's worth clarifying that loop rotation will only be possible for scf.while ops for now, that is, turning regular a "while-do" into a "do-while". That kind of rotation won't be possible at this level of abstraction for scf.for, hence the comment about redundant first iteration check. I think the rotation responsibility belongs to the interface itself.

If we want to make it extra configurable, we could separate the zero-trip-check method from the rotation method...

@llvmbot llvmbot added mlir:core MLIR Core Infrastructure mlir:scf labels Feb 5, 2024
@pzread pzread requested a review from joker-eph February 5, 2024 23:06
@pzread pzread changed the title [mlir] Introduce replaceWithZeroTripCheck in LoopLikeOpInterface [mlir] Introduce wrapInZeroTripCheck in LoopLikeOpInterface Feb 7, 2024
Copy link
Contributor

@MaheshRavishankar MaheshRavishankar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late to the party here, but I think these transformations dont belong in the interface. Looking at the LoopLikeOpInterface it is actually doing some very heavy weight transformations that not all loop ops that implement this interface can handle. I tried to do this too in an earlier PR, and it was rightly pointed out that those kind of transformations dont go into the interface definition.

Instead of adding oto the interface definition, maybe these are just transformation methods that are written on the interface. My read of interfaces are that they are a wrapper around the core operation to have a unified way to interpret operations. Adding transformations of this kind to an interface seem odd to me.

I am not pointing out something really off in this specific PR, but it is contributing to an aspect of the interface that I wanted to flag.

@pzread
Copy link
Member Author

pzread commented Feb 7, 2024

Late to the party here, but I think these transformations dont belong in the interface. Looking at the LoopLikeOpInterface it is actually doing some very heavy weight transformations that not all loop ops that implement this interface can handle. I tried to do this too in an earlier PR, and it was rightly pointed out that those kind of transformations dont go into the interface definition.

Instead of adding oto the interface definition, maybe these are just transformation methods that are written on the interface. My read of interfaces are that they are a wrapper around the core operation to have a unified way to interpret operations. Adding transformations of this kind to an interface seem odd to me.

I am not pointing out something really off in this specific PR, but it is contributing to an aspect of the interface that I wanted to flag.

My initial thought is to create a common interface on loop ops so users can combine with utilities like moveLoopInvariantCode to hoist loop invariants and be op-agnostic:

size_t mlir::moveLoopInvariantCode(LoopLikeOpInterface loopLike) {
return moveLoopInvariantCode(
loopLike.getLoopRegions(),
[&](Value value, Region *) {
return loopLike.isDefinedOutsideOfLoop(value);
},
[&](Operation *op, Region *) {
return isMemoryEffectFree(op) && isSpeculatable(op);
},
[&](Operation *op, Region *) { loopLike.moveOutOfLoop(op); });

I also have the same question when writing this as an interface. Do you have an example where you think this transformation should live?

@pzread
Copy link
Member Author

pzread commented Feb 7, 2024

Late to the party here, but I think these transformations dont belong in the interface. Looking at the LoopLikeOpInterface it is actually doing some very heavy weight transformations that not all loop ops that implement this interface can handle. I tried to do this too in an earlier PR, and it was rightly pointed out that those kind of transformations dont go into the interface definition.
Instead of adding oto the interface definition, maybe these are just transformation methods that are written on the interface. My read of interfaces are that they are a wrapper around the core operation to have a unified way to interpret operations. Adding transformations of this kind to an interface seem odd to me.
I am not pointing out something really off in this specific PR, but it is contributing to an aspect of the interface that I wanted to flag.

My initial thought is to create a common interface on loop ops so users can combine with utilities like moveLoopInvariantCode to hoist loop invariants and be op-agnostic:

size_t mlir::moveLoopInvariantCode(LoopLikeOpInterface loopLike) {
return moveLoopInvariantCode(
loopLike.getLoopRegions(),
[&](Value value, Region *) {
return loopLike.isDefinedOutsideOfLoop(value);
},
[&](Operation *op, Region *) {
return isMemoryEffectFree(op) && isSpeculatable(op);
},
[&](Operation *op, Region *) { loopLike.moveOutOfLoop(op); });

I also have the same question when writing this as an interface. Do you have an example where you think this transformation should live?

IMO, I can probably write this as a transformation helper like this one:

FailureOr<scf::SCFTileAndFuseResult>
mlir::scf::tileConsumerAndFuseProducersUsingSCF(
RewriterBase &rewriter, TilingInterface consumer,
const scf::SCFTileAndFuseOptions &options) {

@pzread
Copy link
Member Author

pzread commented Feb 7, 2024

As @MaheshRavishankar suggested, create #81050 to directly add a transformation for scf while loop.

@pzread pzread closed this Feb 7, 2024
@MaheshRavishankar
Copy link
Contributor

Late to the party here, but I think these transformations dont belong in the interface. Looking at the LoopLikeOpInterface it is actually doing some very heavy weight transformations that not all loop ops that implement this interface can handle. I tried to do this too in an earlier PR, and it was rightly pointed out that those kind of transformations dont go into the interface definition.
Instead of adding oto the interface definition, maybe these are just transformation methods that are written on the interface. My read of interfaces are that they are a wrapper around the core operation to have a unified way to interpret operations. Adding transformations of this kind to an interface seem odd to me.
I am not pointing out something really off in this specific PR, but it is contributing to an aspect of the interface that I wanted to flag.

My initial thought is to create a common interface on loop ops so users can combine with utilities like moveLoopInvariantCode to hoist loop invariants and be op-agnostic:

size_t mlir::moveLoopInvariantCode(LoopLikeOpInterface loopLike) {
return moveLoopInvariantCode(
loopLike.getLoopRegions(),
[&](Value value, Region *) {
return loopLike.isDefinedOutsideOfLoop(value);
},
[&](Operation *op, Region *) {
return isMemoryEffectFree(op) && isSpeculatable(op);
},
[&](Operation *op, Region *) { loopLike.moveOutOfLoop(op); });

I also have the same question when writing this as an interface. Do you have an example where you think this transformation should live?

IMO, I can probably write this as a transformation helper like this one:

FailureOr<scf::SCFTileAndFuseResult>
mlir::scf::tileConsumerAndFuseProducersUsingSCF(
RewriterBase &rewriter, TilingInterface consumer,
const scf::SCFTileAndFuseOptions &options) {

This is what I was thinking of. Thanks for addressing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
mlir:core MLIR Core Infrastructure mlir:scf mlir
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants