Skip to content

Conversation

@NexMing
Copy link
Contributor

@NexMing NexMing commented Oct 27, 2025

When a scf.if directly precedes an scf.condition in the before region of an scf.while and both share the same condition, move the if into the after region of the loop. This helps simplify the control flow to enable uplifting scf.while to scf.for.

@llvmbot
Copy link
Member

llvmbot commented Oct 27, 2025

@llvm/pr-subscribers-mlir

Author: Ming Yan (NexMing)

Changes

When an scf.if directly precedes an scf.condition in the before region of an scf.while and both share the same condition, move the if into the after region of the loop. This preserves semantics while simplifying the condition region and canonicalizing control flow for further optimizations.


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

2 Files Affected:

  • (modified) mlir/lib/Dialect/SCF/IR/SCF.cpp (+133-1)
  • (modified) mlir/test/Dialect/SCF/canonicalize.mlir (+37)
diff --git a/mlir/lib/Dialect/SCF/IR/SCF.cpp b/mlir/lib/Dialect/SCF/IR/SCF.cpp
index 9bd13f3236cfc..ddf8134d98776 100644
--- a/mlir/lib/Dialect/SCF/IR/SCF.cpp
+++ b/mlir/lib/Dialect/SCF/IR/SCF.cpp
@@ -3546,6 +3546,137 @@ LogicalResult scf::WhileOp::verify() {
 }
 
 namespace {
+/// Move an scf.if op that is directly before the scf.condition op in the while
+/// before region, and whose condition matches the condition of the
+/// scf.condition op, down into the while after region.
+///
+/// scf.while (..) : (...) -> ... {
+///  %additional_used_values = ...
+///  %cond = ...
+///  ...
+///  %res = scf.if %cond -> (...) {
+///    use(%additional_used_values)
+///    ... // then block
+///    scf.yield %then_value
+///  } else {
+///    scf.yield %else_value
+///  }
+///  scf.condition(%cond) %res, ...
+/// } do {
+/// ^bb0(%res_arg, ...):
+///    use(%res_arg)
+///    ...
+///
+/// becomes
+/// scf.while (..) : (...) -> ... {
+///  %additional_used_values = ...
+///  %cond = ...
+///  ...
+///  scf.condition(%cond) %else_value, ..., %additional_used_values
+/// } do {
+/// ^bb0(%res_arg ..., %additional_args): :
+///    use(%additional_args)
+///    ... // if then block
+///    use(%then_value)
+///    ...
+struct WhileMoveIfDown : public OpRewritePattern<WhileOp> {
+  using OpRewritePattern<WhileOp>::OpRewritePattern;
+
+  LogicalResult matchAndRewrite(WhileOp op,
+                                PatternRewriter &rewriter) const override {
+    auto conditionOp =
+        cast<scf::ConditionOp>(op.getBeforeBody()->getTerminator());
+    auto ifOp = dyn_cast_or_null<scf::IfOp>(conditionOp->getPrevNode());
+
+    // Check that the ifOp is directly before the conditionOp and that it
+    // matches the condition of the conditionOp. Also ensure that the ifOp has
+    // no else block with content, as that would complicate the transformation.
+    // TODO: support else blocks with content.
+    if (!ifOp || ifOp.getCondition() != conditionOp.getCondition() ||
+        (ifOp.elseBlock() && !ifOp.elseBlock()->without_terminator().empty()))
+      return failure();
+
+    assert(ifOp->use_empty() || (llvm::range_size(ifOp->getUsers()) == 1 &&
+                                 *ifOp->user_begin() == conditionOp) &&
+                                    "ifOp has unexpected uses");
+
+    Location loc = op.getLoc();
+
+    // Replace uses of ifOp results in the conditionOp with the yielded values
+    // from the ifOp branches.
+    for (auto [idx, arg] : llvm::enumerate(conditionOp.getArgs())) {
+      auto it = llvm::find(ifOp->getResults(), arg);
+      if (it != ifOp->getResults().end()) {
+        size_t ifOpIdx = it.getIndex();
+        Value thenValue = ifOp.thenYield()->getOperand(ifOpIdx);
+        Value elseValue = ifOp.elseYield()->getOperand(ifOpIdx);
+
+        rewriter.replaceAllUsesWith(it.getBase(), elseValue);
+        rewriter.replaceAllUsesWith(op.getAfterArguments()[idx], thenValue);
+      }
+    }
+
+    SmallVector<Value> additionalUsedValues;
+    auto isValueUsedInsideIf = [&](Value val) {
+      return llvm::any_of(val.getUsers(), [&](Operation *user) {
+        return ifOp.getThenRegion().isAncestor(user->getParentRegion());
+      });
+    };
+
+    // Collect additional used values from before region.
+    for (Operation *it = ifOp->getPrevNode(); it != nullptr;
+         it = it->getPrevNode())
+      llvm::copy_if(it->getResults(), std::back_inserter(additionalUsedValues),
+                    isValueUsedInsideIf);
+
+    llvm::copy_if(op.getBeforeArguments(),
+                  std::back_inserter(additionalUsedValues),
+                  isValueUsedInsideIf);
+
+    // Create new whileOp with additional used values as results.
+    auto additionalValueTypes = llvm::map_to_vector(
+        additionalUsedValues, [](Value val) { return val.getType(); });
+    size_t additionalValueSize = additionalUsedValues.size();
+    SmallVector<Type> newResultTypes(op.getResultTypes());
+    newResultTypes.append(additionalValueTypes);
+
+    auto newWhileOp =
+        scf::WhileOp::create(rewriter, loc, newResultTypes, op.getInits());
+
+    newWhileOp.getBefore().takeBody(op.getBefore());
+    newWhileOp.getAfter().takeBody(op.getAfter());
+    newWhileOp.getAfter().addArguments(
+        additionalValueTypes, SmallVector<Location>(additionalValueSize, loc));
+
+    SmallVector<Value> conditionArgs = conditionOp.getArgs();
+    llvm::append_range(conditionArgs, additionalUsedValues);
+
+    // Update conditionOp inside new whileOp before region.
+    rewriter.setInsertionPoint(conditionOp);
+    rewriter.replaceOpWithNewOp<scf::ConditionOp>(
+        conditionOp, conditionOp.getCondition(), conditionArgs);
+
+    // Replace uses of additional used values inside the ifOp then region with
+    // the whileOp after region arguments.
+    rewriter.replaceUsesWithIf(
+        additionalUsedValues,
+        newWhileOp.getAfterArguments().take_back(additionalValueSize),
+        [&](OpOperand &use) {
+          return ifOp.getThenRegion().isAncestor(
+              use.getOwner()->getParentRegion());
+        });
+
+    // Inline ifOp then region into new whileOp after region.
+    rewriter.eraseOp(ifOp.thenYield());
+    rewriter.inlineBlockBefore(ifOp.thenBlock(), newWhileOp.getAfterBody(),
+                               newWhileOp.getAfterBody()->begin());
+    rewriter.eraseOp(ifOp);
+    rewriter.replaceOp(op,
+                       newWhileOp->getResults().drop_back(additionalValueSize));
+    return success();
+  }
+};
+
 /// Replace uses of the condition within the do block with true, since otherwise
 /// the block would not be evaluated.
 ///
@@ -4258,7 +4389,8 @@ void WhileOp::getCanonicalizationPatterns(RewritePatternSet &results,
   results.add<RemoveLoopInvariantArgsFromBeforeBlock,
               RemoveLoopInvariantValueYielded, WhileConditionTruth,
               WhileCmpCond, WhileUnusedResult, WhileRemoveDuplicatedResults,
-              WhileRemoveUnusedArgs, WhileOpAlignBeforeArgs>(context);
+              WhileRemoveUnusedArgs, WhileOpAlignBeforeArgs, WhileMoveIfDown>(
+      context);
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Dialect/SCF/canonicalize.mlir b/mlir/test/Dialect/SCF/canonicalize.mlir
index 2bec63672e783..cfae3b34305de 100644
--- a/mlir/test/Dialect/SCF/canonicalize.mlir
+++ b/mlir/test/Dialect/SCF/canonicalize.mlir
@@ -974,6 +974,43 @@ func.func @replace_if_with_cond3(%arg0 : i1, %arg2: i64) -> (i32, i64) {
 
 // -----
 
+// CHECK-LABEL: @while_move_if_down
+func.func @while_move_if_down() -> i32 {
+  %0 = scf.while () : () -> (i32) {
+    %additional_used_value = "test.get_some_value1" () : () -> (i32)
+    %else_value = "test.get_some_value2" () : () -> (i32)
+    %condition = "test.condition"() : () -> i1
+    %res = scf.if %condition -> (i32) {
+      "test.use1" (%additional_used_value) : (i32) -> ()
+      %then_value = "test.get_some_value3" () : () -> (i32)
+      scf.yield %then_value : i32
+    } else {
+      scf.yield %else_value : i32
+    }
+    scf.condition(%condition) %res : i32
+  } do {
+  ^bb0(%res_arg: i32):
+    "test.use2" (%res_arg) : (i32) -> ()
+    scf.yield
+  }
+  return %0 : i32
+}
+// CHECK-NEXT:      %[[WHILE_0:.*]]:2 = scf.while : () -> (i32, i32) {
+// CHECK-NEXT:        %[[VAL_0:.*]] = "test.get_some_value1"() : () -> i32
+// CHECK-NEXT:        %[[VAL_1:.*]] = "test.get_some_value2"() : () -> i32
+// CHECK-NEXT:        %[[VAL_2:.*]] = "test.condition"() : () -> i1
+// CHECK-NEXT:        scf.condition(%[[VAL_2]]) %[[VAL_1]], %[[VAL_0]] : i32, i32
+// CHECK-NEXT:      } do {
+// CHECK-NEXT:      ^bb0(%[[VAL_3:.*]]: i32, %[[VAL_4:.*]]: i32):
+// CHECK-NEXT:        "test.use1"(%[[VAL_4]]) : (i32) -> ()
+// CHECK-NEXT:        %[[VAL_5:.*]] = "test.get_some_value3"() : () -> i32
+// CHECK-NEXT:        "test.use2"(%[[VAL_5]]) : (i32) -> ()
+// CHECK-NEXT:        scf.yield
+// CHECK-NEXT:      }
+// CHECK-NEXT:      return %[[VAL_6:.*]]#0 : i32
+
+// -----
+
 // CHECK-LABEL: @while_cond_true
 func.func @while_cond_true() -> i1 {
   %0 = scf.while () : () -> i1 {

@llvmbot
Copy link
Member

llvmbot commented Oct 27, 2025

@llvm/pr-subscribers-mlir-scf

Author: Ming Yan (NexMing)

Changes

When an scf.if directly precedes an scf.condition in the before region of an scf.while and both share the same condition, move the if into the after region of the loop. This preserves semantics while simplifying the condition region and canonicalizing control flow for further optimizations.


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

2 Files Affected:

  • (modified) mlir/lib/Dialect/SCF/IR/SCF.cpp (+133-1)
  • (modified) mlir/test/Dialect/SCF/canonicalize.mlir (+37)
diff --git a/mlir/lib/Dialect/SCF/IR/SCF.cpp b/mlir/lib/Dialect/SCF/IR/SCF.cpp
index 9bd13f3236cfc..ddf8134d98776 100644
--- a/mlir/lib/Dialect/SCF/IR/SCF.cpp
+++ b/mlir/lib/Dialect/SCF/IR/SCF.cpp
@@ -3546,6 +3546,137 @@ LogicalResult scf::WhileOp::verify() {
 }
 
 namespace {
+/// Move an scf.if op that is directly before the scf.condition op in the while
+/// before region, and whose condition matches the condition of the
+/// scf.condition op, down into the while after region.
+///
+/// scf.while (..) : (...) -> ... {
+///  %additional_used_values = ...
+///  %cond = ...
+///  ...
+///  %res = scf.if %cond -> (...) {
+///    use(%additional_used_values)
+///    ... // then block
+///    scf.yield %then_value
+///  } else {
+///    scf.yield %else_value
+///  }
+///  scf.condition(%cond) %res, ...
+/// } do {
+/// ^bb0(%res_arg, ...):
+///    use(%res_arg)
+///    ...
+///
+/// becomes
+/// scf.while (..) : (...) -> ... {
+///  %additional_used_values = ...
+///  %cond = ...
+///  ...
+///  scf.condition(%cond) %else_value, ..., %additional_used_values
+/// } do {
+/// ^bb0(%res_arg ..., %additional_args): :
+///    use(%additional_args)
+///    ... // if then block
+///    use(%then_value)
+///    ...
+struct WhileMoveIfDown : public OpRewritePattern<WhileOp> {
+  using OpRewritePattern<WhileOp>::OpRewritePattern;
+
+  LogicalResult matchAndRewrite(WhileOp op,
+                                PatternRewriter &rewriter) const override {
+    auto conditionOp =
+        cast<scf::ConditionOp>(op.getBeforeBody()->getTerminator());
+    auto ifOp = dyn_cast_or_null<scf::IfOp>(conditionOp->getPrevNode());
+
+    // Check that the ifOp is directly before the conditionOp and that it
+    // matches the condition of the conditionOp. Also ensure that the ifOp has
+    // no else block with content, as that would complicate the transformation.
+    // TODO: support else blocks with content.
+    if (!ifOp || ifOp.getCondition() != conditionOp.getCondition() ||
+        (ifOp.elseBlock() && !ifOp.elseBlock()->without_terminator().empty()))
+      return failure();
+
+    assert(ifOp->use_empty() || (llvm::range_size(ifOp->getUsers()) == 1 &&
+                                 *ifOp->user_begin() == conditionOp) &&
+                                    "ifOp has unexpected uses");
+
+    Location loc = op.getLoc();
+
+    // Replace uses of ifOp results in the conditionOp with the yielded values
+    // from the ifOp branches.
+    for (auto [idx, arg] : llvm::enumerate(conditionOp.getArgs())) {
+      auto it = llvm::find(ifOp->getResults(), arg);
+      if (it != ifOp->getResults().end()) {
+        size_t ifOpIdx = it.getIndex();
+        Value thenValue = ifOp.thenYield()->getOperand(ifOpIdx);
+        Value elseValue = ifOp.elseYield()->getOperand(ifOpIdx);
+
+        rewriter.replaceAllUsesWith(it.getBase(), elseValue);
+        rewriter.replaceAllUsesWith(op.getAfterArguments()[idx], thenValue);
+      }
+    }
+
+    SmallVector<Value> additionalUsedValues;
+    auto isValueUsedInsideIf = [&](Value val) {
+      return llvm::any_of(val.getUsers(), [&](Operation *user) {
+        return ifOp.getThenRegion().isAncestor(user->getParentRegion());
+      });
+    };
+
+    // Collect additional used values from before region.
+    for (Operation *it = ifOp->getPrevNode(); it != nullptr;
+         it = it->getPrevNode())
+      llvm::copy_if(it->getResults(), std::back_inserter(additionalUsedValues),
+                    isValueUsedInsideIf);
+
+    llvm::copy_if(op.getBeforeArguments(),
+                  std::back_inserter(additionalUsedValues),
+                  isValueUsedInsideIf);
+
+    // Create new whileOp with additional used values as results.
+    auto additionalValueTypes = llvm::map_to_vector(
+        additionalUsedValues, [](Value val) { return val.getType(); });
+    size_t additionalValueSize = additionalUsedValues.size();
+    SmallVector<Type> newResultTypes(op.getResultTypes());
+    newResultTypes.append(additionalValueTypes);
+
+    auto newWhileOp =
+        scf::WhileOp::create(rewriter, loc, newResultTypes, op.getInits());
+
+    newWhileOp.getBefore().takeBody(op.getBefore());
+    newWhileOp.getAfter().takeBody(op.getAfter());
+    newWhileOp.getAfter().addArguments(
+        additionalValueTypes, SmallVector<Location>(additionalValueSize, loc));
+
+    SmallVector<Value> conditionArgs = conditionOp.getArgs();
+    llvm::append_range(conditionArgs, additionalUsedValues);
+
+    // Update conditionOp inside new whileOp before region.
+    rewriter.setInsertionPoint(conditionOp);
+    rewriter.replaceOpWithNewOp<scf::ConditionOp>(
+        conditionOp, conditionOp.getCondition(), conditionArgs);
+
+    // Replace uses of additional used values inside the ifOp then region with
+    // the whileOp after region arguments.
+    rewriter.replaceUsesWithIf(
+        additionalUsedValues,
+        newWhileOp.getAfterArguments().take_back(additionalValueSize),
+        [&](OpOperand &use) {
+          return ifOp.getThenRegion().isAncestor(
+              use.getOwner()->getParentRegion());
+        });
+
+    // Inline ifOp then region into new whileOp after region.
+    rewriter.eraseOp(ifOp.thenYield());
+    rewriter.inlineBlockBefore(ifOp.thenBlock(), newWhileOp.getAfterBody(),
+                               newWhileOp.getAfterBody()->begin());
+    rewriter.eraseOp(ifOp);
+    rewriter.replaceOp(op,
+                       newWhileOp->getResults().drop_back(additionalValueSize));
+    return success();
+  }
+};
+
 /// Replace uses of the condition within the do block with true, since otherwise
 /// the block would not be evaluated.
 ///
@@ -4258,7 +4389,8 @@ void WhileOp::getCanonicalizationPatterns(RewritePatternSet &results,
   results.add<RemoveLoopInvariantArgsFromBeforeBlock,
               RemoveLoopInvariantValueYielded, WhileConditionTruth,
               WhileCmpCond, WhileUnusedResult, WhileRemoveDuplicatedResults,
-              WhileRemoveUnusedArgs, WhileOpAlignBeforeArgs>(context);
+              WhileRemoveUnusedArgs, WhileOpAlignBeforeArgs, WhileMoveIfDown>(
+      context);
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Dialect/SCF/canonicalize.mlir b/mlir/test/Dialect/SCF/canonicalize.mlir
index 2bec63672e783..cfae3b34305de 100644
--- a/mlir/test/Dialect/SCF/canonicalize.mlir
+++ b/mlir/test/Dialect/SCF/canonicalize.mlir
@@ -974,6 +974,43 @@ func.func @replace_if_with_cond3(%arg0 : i1, %arg2: i64) -> (i32, i64) {
 
 // -----
 
+// CHECK-LABEL: @while_move_if_down
+func.func @while_move_if_down() -> i32 {
+  %0 = scf.while () : () -> (i32) {
+    %additional_used_value = "test.get_some_value1" () : () -> (i32)
+    %else_value = "test.get_some_value2" () : () -> (i32)
+    %condition = "test.condition"() : () -> i1
+    %res = scf.if %condition -> (i32) {
+      "test.use1" (%additional_used_value) : (i32) -> ()
+      %then_value = "test.get_some_value3" () : () -> (i32)
+      scf.yield %then_value : i32
+    } else {
+      scf.yield %else_value : i32
+    }
+    scf.condition(%condition) %res : i32
+  } do {
+  ^bb0(%res_arg: i32):
+    "test.use2" (%res_arg) : (i32) -> ()
+    scf.yield
+  }
+  return %0 : i32
+}
+// CHECK-NEXT:      %[[WHILE_0:.*]]:2 = scf.while : () -> (i32, i32) {
+// CHECK-NEXT:        %[[VAL_0:.*]] = "test.get_some_value1"() : () -> i32
+// CHECK-NEXT:        %[[VAL_1:.*]] = "test.get_some_value2"() : () -> i32
+// CHECK-NEXT:        %[[VAL_2:.*]] = "test.condition"() : () -> i1
+// CHECK-NEXT:        scf.condition(%[[VAL_2]]) %[[VAL_1]], %[[VAL_0]] : i32, i32
+// CHECK-NEXT:      } do {
+// CHECK-NEXT:      ^bb0(%[[VAL_3:.*]]: i32, %[[VAL_4:.*]]: i32):
+// CHECK-NEXT:        "test.use1"(%[[VAL_4]]) : (i32) -> ()
+// CHECK-NEXT:        %[[VAL_5:.*]] = "test.get_some_value3"() : () -> i32
+// CHECK-NEXT:        "test.use2"(%[[VAL_5]]) : (i32) -> ()
+// CHECK-NEXT:        scf.yield
+// CHECK-NEXT:      }
+// CHECK-NEXT:      return %[[VAL_6:.*]]#0 : i32
+
+// -----
+
 // CHECK-LABEL: @while_cond_true
 func.func @while_cond_true() -> i1 {
   %0 = scf.while () : () -> i1 {

@NexMing NexMing force-pushed the dev/while-sink-if-down branch from 4d3ff6d to ecffe33 Compare October 27, 2025 09:49
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.

I am not sure this is a canonicalization. This seems like a cost-model based thing. Someone else might want to hoist the if condition

Copy link
Member

@matthias-springer matthias-springer left a comment

Choose a reason for hiding this comment

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

This preserves semantics while simplifying the condition region and canonicalizing control flow for further optimizations.

What further optimizations did you have in mind?

@NexMing
Copy link
Contributor Author

NexMing commented Oct 28, 2025

What further optimizations did you have in mind?

The motivation is that I want to lift cf to scf.for.
For example, I want to convert the following control flow into an scf.for.

  func.func @test(%arg0: memref<100xf32>) {
    %c100 = arith.constant 100 : index
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %cst = arith.constant 0.000000e+00 : f32
    cf.br ^bb1(%c0 : index)
  ^bb1(%0: index):  // 2 preds: ^bb0, ^bb2
    %1 = arith.cmpi slt, %0, %c100 : index
    cf.cond_br %1, ^bb2, ^bb3
  ^bb2:  // pred: ^bb1
    memref.store %cst, %arg0[%0] : memref<100xf32>
    %2 = arith.addi %0, %c1 : index
    cf.br ^bb1(%2 : index)
  ^bb3:  // pred: ^bb1
    return
  }

When I use the lift-cf-to-scf pass, I get:

  func.func @test(%arg0: memref<100xf32>) {
    %0 = ub.poison : index
    %c100 = arith.constant 100 : index
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %cst = arith.constant 0.000000e+00 : f32
    %1 = scf.while (%arg1 = %c0) : (index) -> index {
      %2 = arith.cmpi slt, %arg1, %c100 : index
      %3 = scf.if %2 -> (index) {
        memref.store %cst, %arg0[%arg1] : memref<100xf32>
        %4 = arith.addi %arg1, %c1 : index
        scf.yield %4 : index
      } else {
        scf.yield %0 : index
      }
      scf.condition(%2) %3 : index
    } do {
    ^bb0(%arg1: index):
      scf.yield %arg1 : index
    }
    return
  }

When I continued using test-scf-uplift-while-to-for, the attempt to lift scf.while to scf.for failed.
After this optimization, test-scf-uplift-while-to-for can successfully lift scf.while to scf.for.

I believe this optimization always simplifies the control flow, so I think it belongs to canonicalization.
I don’t quite understand in what situations there would be a need to hoist the if condition.
If needed, I will consider moving it into test-scf-uplift-while-to-for.

@NexMing
Copy link
Contributor Author

NexMing commented Nov 3, 2025

ping

@MaheshRavishankar
Copy link
Contributor

I believe this optimization always simplifies the control flow, so I think it belongs to canonicalization.
I don’t quite understand in what situations there would be a need to hoist the if condition.
If needed, I will consider moving it into test-scf-uplift-while-to-for.

It would be good to not have this as a canonicalization to start with. This seems like a pretty heavy weight transformation to add to canonicalizations.

@github-actions
Copy link

github-actions bot commented Nov 4, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@NexMing NexMing force-pushed the dev/while-sink-if-down branch from 83f42de to 05b6a0f Compare November 4, 2025 03:48
@NexMing NexMing force-pushed the dev/while-sink-if-down branch from 05b6a0f to 19a042e Compare November 4, 2025 03:49
@NexMing
Copy link
Contributor Author

NexMing commented Nov 4, 2025

Please help review it, thank you. @MaheshRavishankar

@MaheshRavishankar MaheshRavishankar dismissed their stale review November 4, 2025 17:55

Removing blockers since this isnt a canonicalization anymore.

@joker-eph
Copy link
Collaborator

This seems like a pretty heavy weight transformation to add to canonicalizations.

Which part? The transformation itself? As long as the matching phase has a "fail fast" path it seems fine to me actually.
The most important thing is to do as little work as possible in the pattern when there is nothing to do.

@MaheshRavishankar
Copy link
Contributor

This seems like a pretty heavy weight transformation to add to canonicalizations.

Which part? The transformation itself? As long as the matching phase has a "fail fast" path it seems fine to me actually. The most important thing is to do as little work as possible in the pattern when there is nothing to do.

An if-hoisting across a while does not fit in canonicalizations.

@joker-eph
Copy link
Collaborator

joker-eph commented Nov 4, 2025

An if-hoisting across a while does not fit in canonicalizations.

  1. You're stating it as a fact, but that requires some elaboration: why is it a bad canonicalization?
  2. Can you please elaborate how your stated opinion here related to it being "heavy weight"? I'm not sure I follow and you're not answering your questions.

@Groverkss
Copy link
Member

Groverkss commented Nov 7, 2025

As far as I understand, this transformation is doing:

if (%cond) {
foo()
}
cond_br %cond ^continue, ^end

^continue:
...
^end:
...
->

cond_br %cond ^continue, ^end

^continue:
foo()
...
^end:
...

I think this may be over simplifying the transformation a bit. Maybe I'm wrong (so feel free to correct my example if i'm wrong), but let me try to give an example. Let's take the case:

while {

%something = some_loop_invariant_compute_but_i_never_hoisted_it_out()
if (%cond) {
  foo(%something)
}
cond_br %cond ^continue, ^end

} while {

^continue:
    ...
^end:
   ...
}

If you decide to do the same transformation, you need to somehow also forward %something. You need to either:

  • Check you don't need to forward anything from the conditional region, which would mean check all uses inside the if block
    OR
  • Clone the entire backward chain

Both of these seem heavy weight. Note that you cannot really safely do this transformation without doing atleast one of these. Analyze a whole region or cloning an entire backward chain (or finding that backward chain) seems a bit too much for a canonicalization maybe.

@NexMing
Copy link
Contributor Author

NexMing commented Nov 7, 2025

If there is "instability and uncertainty", that's a problem to add this in the first place. Otherwise I'd like to understand the path to get this into canonicalization? What does it mean for this to "become sufficiently stable"?

What I’m currently worried about is an extreme case where the number of operands to scf.condition could grow significantly, and likewise the number of result values of scf.while could increase substantially. This might make the scf.while look more complex, and I’m not sure whether this would violate the principles of canonicalization.

@NexMing NexMing requested a review from Groverkss November 8, 2025 01:11
@jpienaar
Copy link
Member

jpienaar commented Nov 9, 2025

What I’m currently worried about is an extreme case where the number of operands to scf.condition could grow significantly, and likewise the number of result values of scf.while could increase substantially. This might make the scf.while look more complex, and I’m not sure whether this would violate the principles of canonicalization.

That is valid concern. I think you are saying appearance to also mean ability to analyze. It could result in more complicated liveness analysis than pre-transform (and to Kumar's point, trade off between recomputing or increasing high water mark).

As a general pattern though that is something that could be analyzed in different flows. It seems desirable/useful pattern but not always on/non canonical, so currently proposed as pattern seems sensible. We don't have a big corpus upstream to measure cost, so I don't think we can put that bar here (of course if folks can find cases where bad and file issues great). It seems useful pattern that can be further refined here.

…it into canonicalization.

We do not have an appropriate cost model for evaluation, and adding more constraints can ensure that it is always beneficial and does not introduce excessive compile-time overhead.
@NexMing
Copy link
Contributor Author

NexMing commented Nov 18, 2025

Ping!
Adding more restrictions ensures that the transformation is always profitable and does not introduce excessive compile-time overhead.

@github-actions
Copy link

github-actions bot commented Nov 18, 2025

🐧 Linux x64 Test Results

  • 7144 tests passed
  • 594 tests skipped

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why this restriction on the results and its use here? In particular what is the hasNUses(2) meant to cover?
This all seems very ad-hoc right now: you're matching something that seems quite overly specific to me.

Copy link
Contributor Author

@NexMing NexMing Nov 19, 2025

Choose a reason for hiding this comment

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

Because values defined in before region cannot reach the do region, I need to ensure that the cond value is not used inside scf.if (although this usually doesn’t happen). Otherwise, I would need to pass or create new values to update the use-chain.

For simplicity, I restrict the cond value to have exactly two uses (these two uses come from the scf.if and scf.condition operations).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I reiterate my previous comment/concern: to me that makes it really overly specific in an arbitrary way right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intention is mainly to cover the scf.while form produced by the lift-cf-to-scf pass. Making the pattern more specific helps us avoid additional analysis, which reduces compile-time overhead.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I understand, but my remark still stands: that is quite overly specific right now, for reasons I don't quite grasp.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I find it hard to accept the statement. Does this mean that your transformations may also produce dead values that cannot be eliminated?
Have you considered extreme cases or stress tests? Or could this actually be a hidden optimization bug that simply hasn’t been discovered yet?
Subjectively, I feel that adding result values to scf.while may introduce negative optimization effects. Maybe I just don’t understand it well enough — can you explain why it wouldn’t cause any negative optimization?

I don't see any issue with what you call "dead values" here. But let's not reverse the burden of explanation: if you claim that this is a problem, it's up to you to provide an example (you just need one), not the opposite actually (I can't provide an example for the absence of something in general...).

If I can guarantee that it always results in a positive optimization, then I think it’s fine to use it in canonicalization.

I was trying to provide you with a way to make progress right now to land your change, I suspect we may not converge on this PR right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see any issue with what you call "dead values" here. But let's not reverse the burden of explanation: if you claim that this is a problem, it's up to you to provide an example (you just need one), not the opposite actually (I can't provide an example for the absence of something in general...).

I believe that the "seems like the kind of usual transformations we do." as you referred to, generally do not generate dead values that cannot be eliminated.

in the following example

%res = scf.while {
  a large number of defined values...
  scf.if %cond {
    used a large number of values...
  }
  scf.condition %cond
} do {
  ...

The converted scf.while defines a large number of values, but most of them have no users, and it seems that other optimizations can hardly eliminate them.

%res:xxxx = scf.while {
  a large number of defined values...
  scf.condition %cond (carrying a large number of operands)
} do {
  used a large number of values...
  ...

This is my subjective guess that such a transformation might introduce optimization risks (it’s just a guess... and I believe I’m not the only one who would think so). You can question my guess, and you can also explain why there is no risk. But since there has been no reasonable explanation, it leaves me very confused...

Copy link
Contributor Author

@NexMing NexMing Nov 27, 2025

Choose a reason for hiding this comment

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

I was trying to provide you with a way to make progress right now to land your change, I suspect we may not converge on this PR right now.

I’ve already moved it into test-scf-uplift-while-to-for. Could you please help make progress this PR? Thank you very much.

Copy link
Collaborator

@joker-eph joker-eph Nov 27, 2025

Choose a reason for hiding this comment

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

You can question my guess, and you can also explain why there is no risk.

As mentioned before It's very difficult to prove or explain the absence of a problem when there is none. It's very easy to provide an example of a problem when it exists. So the onus is on you here to sustain your "guess".

and it seems that other optimizations can hardly eliminate them.

You're introducing the concept of "dead values" here, I don't have a problem with these values existing. If you want to argue something you need to look at whether it affects liveness and live ranges, or how it is treated in a lowering pipeline: what happens with such a while loop when lowered to a CFG? Hopefully the logic is already smart enough to ignore these values, but it's possible we can do better (I don't see any fundamental issue here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're introducing the concept of "dead values" here, I don't have a problem with these values existing. If you want to argue something you need to look at whether it affects liveness and live ranges, or how it is treated in a lowering pipeline: what happens with such a while loop when lowered to a CFG? Hopefully the logic is already smart enough to ignore these values, but it's possible we can do better (I don't see any fundamental issue here).

Hi, I tried to see what happens when scf.while is lowered to the CFG.
I found that when scf.condition is lowered to cf.cond_br, the “dead values” disappear. So my earlier guess was incorrect, and it doesn’t introduce optimization risks. I apologize for my mistaken "guess". I think I’ll submit a new PR and continue using the original approach. Sorry for any trouble this may have caused you.
But I do have a question: why isn’t scf.condition designed with two sets of operands—one passed to the do region and the other passed as the result values?

@Groverkss Groverkss dismissed their stale review November 18, 2025 13:34

on vacation and don't have strong concerns here

@NexMing NexMing requested a review from joker-eph November 19, 2025 02:55
@NexMing NexMing force-pushed the dev/while-sink-if-down branch from 6debfbf to a289930 Compare November 19, 2025 03:03
@NexMing
Copy link
Contributor Author

NexMing commented Nov 24, 2025

ping

@joker-eph joker-eph changed the title [MLIR][SCF] Sink scf.if from scf.while before region into after region. [MLIR][SCF] Sink scf.if from scf.while before region into after region in scf-uplift-while-to-for Nov 27, 2025
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.

LGTM

@NexMing NexMing merged commit 25d027b into llvm:main Nov 27, 2025
10 checks passed
@NexMing NexMing deleted the dev/while-sink-if-down branch November 27, 2025 15:30
@llvm-ci
Copy link
Collaborator

llvm-ci commented Nov 27, 2025

LLVM Buildbot has detected a new failure on builder clang-aarch64-sve-vla running on linaro-g3-02 while building mlir at step 7 "ninja check 1".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/17/builds/13018

Here is the relevant piece of the build log for the reference
Step 7 (ninja check 1) failure: 1200 seconds without output running [b'ninja', b'check-all'], attempting to kill
...
PASS: lit :: discovery.py (103776 of 103786)
PASS: lit :: allow-retries.py (103777 of 103786)
PASS: lit :: shtest-external-shell-kill.py (103778 of 103786)
PASS: lit :: selecting.py (103779 of 103786)
PASS: lit :: googletest-timeout.py (103780 of 103786)
PASS: lit :: shtest-timeout.py (103781 of 103786)
PASS: lit :: verbosity.py (103782 of 103786)
PASS: lit :: shtest-shell.py (103783 of 103786)
PASS: lit :: max-time.py (103784 of 103786)
PASS: lit :: shtest-define.py (103785 of 103786)
command timed out: 1200 seconds without output running [b'ninja', b'check-all'], attempting to kill
process killed by signal 9
program finished with exit code -1
elapsedTime=1526.222759

NexMing added a commit that referenced this pull request Dec 1, 2025
…er region in scf-uplift-while-to-for" (#169888)

Reverts #165216
It is implemented in #169892 .
aahrun pushed a commit to aahrun/llvm-project that referenced this pull request Dec 1, 2025
…er region in scf-uplift-while-to-for" (llvm#169888)

Reverts llvm#165216
It is implemented in llvm#169892 .
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Dec 1, 2025
…on into after region in scf-uplift-while-to-for" (#169888)

Reverts llvm/llvm-project#165216
It is implemented in #169892 .
augusto2112 pushed a commit to augusto2112/llvm-project that referenced this pull request Dec 3, 2025
…n in scf-uplift-while-to-for (llvm#165216)

When a `scf.if` directly precedes an `scf.condition` in the before
region of an `scf.while` and both share the same condition, move the if
into the after region of the loop. This helps simplify the control flow
to enable uplifting `scf.while` to `scf.for`.
augusto2112 pushed a commit to augusto2112/llvm-project that referenced this pull request Dec 3, 2025
…er region in scf-uplift-while-to-for" (llvm#169888)

Reverts llvm#165216
It is implemented in llvm#169892 .
kcloudy0717 pushed a commit to kcloudy0717/llvm-project that referenced this pull request Dec 4, 2025
…n in scf-uplift-while-to-for (llvm#165216)

When a `scf.if` directly precedes an `scf.condition` in the before
region of an `scf.while` and both share the same condition, move the if
into the after region of the loop. This helps simplify the control flow
to enable uplifting `scf.while` to `scf.for`.
kcloudy0717 pushed a commit to kcloudy0717/llvm-project that referenced this pull request Dec 4, 2025
…er region in scf-uplift-while-to-for" (llvm#169888)

Reverts llvm#165216
It is implemented in llvm#169892 .
honeygoyal pushed a commit to honeygoyal/llvm-project that referenced this pull request Dec 9, 2025
…er region in scf-uplift-while-to-for" (llvm#169888)

Reverts llvm#165216
It is implemented in llvm#169892 .
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants