[Mlir-commits] [mlir] [mlir][bufferization] Add loop-iter-arg-destination-folding pass (PR #213207)

Jianhui Li llvmlistbot at llvm.org
Fri Jul 31 07:34:30 PDT 2026


https://github.com/Jianhui-Li updated https://github.com/llvm/llvm-project/pull/213207

>From fad923fc40eda3f0cab7602da3f0791c9411a98c Mon Sep 17 00:00:00 2001
From: Jianhui Li <jian.hui.li at intel.com>
Date: Fri, 31 Jul 2026 04:42:02 +0000
Subject: [PATCH 1/2] [mlir][bufferization] Add
 loop-iter-arg-destination-folding pass

Adds a pre-bufferization pass that turns a loop-carried value into an in-place
update by redirecting the destination of its yielded whole-tensor
`vector.transfer_write` from a fresh `tensor.empty` onto the corresponding
`scf.for` iter_arg, when the iter_arg is read-then-fully-overwritten (all
in-loop reads properly precede the write).

After vectorization, a tiled reduction threads an accumulator as a read-only
iter_arg and writes the update into a separate scratch tensor that is yielded.
Because the yielded value is not the iter_arg, one-shot bufferization allocates
and copies a fresh buffer every iteration. Folding the write destination onto
the iter_arg exposes in-place reuse (yield == iter_arg), which bufferization
keeps in place and canonicalization then drops as loop-invariant.

The rewrite is conservative and safe on two levels: it only fires for a
whole-tensor write (no mask, in-bounds, identity map, zero indices) into an
outside-the-loop tensor.empty, and only when every in-loop read of the iter_arg
precedes the write. One-shot bufferization's in-place analysis remains the final
correctness arbiter and reinserts a copy if reuse would be unsound, so the
transform never changes program semantics (verified by execution).

On a Flash-Attention-style kernel this reduces the loop from three memref
iter_args to one and removes one of two per-iteration copies; the remaining
carried buffer is a genuine rotation (its iter_arg is read after the update) and
is correctly left alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply at anthropic.com>
---
 .../Bufferization/Transforms/Passes.td        |  18 ++
 .../Bufferization/Transforms/CMakeLists.txt   |   2 +
 .../LoopIterArgDestinationFolding.cpp         | 168 ++++++++++++++++++
 .../loop-iter-arg-destination-folding.mlir    | 117 ++++++++++++
 4 files changed, 305 insertions(+)
 create mode 100644 mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
 create mode 100644 mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir

diff --git a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
index 8408315dda607..4e44cc0f08b7f 100644
--- a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
+++ b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
@@ -303,6 +303,24 @@ def BufferLoopHoistingPass : Pass<"buffer-loop-hoisting", "func::FuncOp"> {
   }];
 }
 
+def LoopIterArgDestinationFoldingPass : Pass<"loop-iter-arg-destination-folding"> {
+  let summary = "Fold a yielded write's destination onto its loop iter_arg";
+  let description = [{
+    Redirects the destination of a whole-tensor `vector.transfer_write` that
+    produces a loop-carried yield value from a fresh `tensor.empty` onto the
+    corresponding `scf.for` iter_arg, when the iter_arg is read-then-overwritten
+    (all in-loop reads precede the write). This exposes in-place reuse of the
+    carried buffer so one-shot bufferization keeps it in place instead of
+    allocating and copying a fresh buffer each iteration.
+
+    The rewrite is a hint: one-shot bufferization's in-place analysis remains the
+    correctness arbiter and reinserts a copy if the reuse is unsound, so the
+    transform never changes program semantics.
+  }];
+  let dependentDialects = ["tensor::TensorDialect", "vector::VectorDialect",
+                           "scf::SCFDialect"];
+}
+
 def BufferResultsToOutParamsPass
     : Pass<"buffer-results-to-out-params", "ModuleOp"> {
   let summary = "Converts memref-typed function results to out-params";
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
index 006fcd1ce0ec7..c9a853c9667e9 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
+++ b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
@@ -9,6 +9,7 @@ add_mlir_dialect_library(MLIRBufferizationTransforms
   EmptyTensorElimination.cpp
   EmptyTensorToAllocTensor.cpp
   FuncBufferizableOpInterfaceImpl.cpp
+  LoopIterArgDestinationFolding.cpp
   LowerDeallocations.cpp
   OneShotAnalysis.cpp
   OneShotModuleBufferize.cpp
@@ -41,6 +42,7 @@ add_mlir_dialect_library(MLIRBufferizationTransforms
   MLIRSideEffectInterfaces
   MLIRSubsetOpInterface
   MLIRTransforms
+  MLIRVectorDialect
   MLIRViewLikeInterface
   MLIRSupport
 )
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp b/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
new file mode 100644
index 0000000000000..7fd4ef7f35910
--- /dev/null
+++ b/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
@@ -0,0 +1,168 @@
+//===- LoopIterArgDestinationFolding.cpp - Reuse iter_arg buffers ---------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements a pre-bufferization rewrite that turns a loop-carried
+// value into an in-place update by folding the destination of its yielded write
+// onto the loop's iter_arg.
+//
+// After vectorization, a tiled reduction typically threads an accumulator as a
+// read-only `scf.for` iter_arg and writes the updated value into a *fresh*
+// `tensor.empty` that is then yielded:
+//
+//   %r = scf.for ... iter_args(%acc = %init) -> (tensor<...>) {
+//     %v = vector.transfer_read %acc[...]        // read the incoming value
+//     ... compute %new ...
+//     %e = tensor.empty()
+//     %w = vector.transfer_write %new, %e[...]   // write into a fresh tensor
+//     scf.yield %w                                // yield != iter_arg
+//   }
+//
+// Because the yielded tensor is not the iter_arg, one-shot bufferization must
+// allocate a fresh buffer and copy into it every iteration (its result may not
+// alias a buffer defined outside the loop other than its own init operand).
+// When the iter_arg is read-then-fully-overwritten, the same buffer can serve
+// both roles, so redirecting the write destination to the iter_arg exposes the
+// reuse:
+//
+//     %w = vector.transfer_write %new, %acc[...]  // destination = iter_arg
+//     scf.yield %w                                 // yield == iter_arg (in place)
+//
+// This folds the iter_arg to loop-invariant reuse; bufferization then keeps it
+// in place with no per-iteration copy.
+//
+// Correctness: one-shot bufferization's in-place analysis is the final arbiter.
+// If the reuse would be unsound (e.g. the iter_arg is read again *after* the
+// write), the analysis declines the in-place update and reinserts the copy, so
+// this rewrite is a hint that never changes program semantics. The legality
+// check below is nonetheless conservative so the pass only fires where the
+// reuse is expected to be honored.
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Dialect/Bufferization/Transforms/Passes.h"
+
+#include "mlir/Dialect/SCF/IR/SCF.h"
+#include "mlir/Dialect/Tensor/IR/Tensor.h"
+#include "mlir/Dialect/Vector/IR/VectorOps.h"
+#include "mlir/IR/Dominance.h"
+
+namespace mlir {
+namespace bufferization {
+#define GEN_PASS_DEF_LOOPITERARGDESTINATIONFOLDINGPASS
+#include "mlir/Dialect/Bufferization/Transforms/Passes.h.inc"
+} // namespace bufferization
+} // namespace mlir
+
+using namespace mlir;
+using namespace mlir::bufferization;
+using namespace mlir::scf;
+
+/// Returns the whole-tensor `vector.transfer_write` that produces `value` and
+/// writes into a loop-invariant `tensor.empty`, or nullptr if `value` is not
+/// such a write. A whole-tensor write has all-zero constant indices, an
+/// identity permutation, and all dims in-bounds, so it fully defines the tensor
+/// and its destination's prior contents are dead.
+static vector::TransferWriteOp
+getFoldableYieldWrite(Value value, ForOp loop) {
+  auto write = value.getDefiningOp<vector::TransferWriteOp>();
+  if (!write)
+    return nullptr;
+  // The write must be inside the loop body (it defines the yielded value).
+  if (!loop->isProperAncestor(write))
+    return nullptr;
+  // Destination must be a tensor.empty defined outside the loop: a pure scratch
+  // whose contents are undefined, so this write fully defines the result and
+  // redirecting only this write's destination operand is safe regardless of the
+  // empty's other uses.
+  auto empty = write.getBase().getDefiningOp<tensor::EmptyOp>();
+  if (!empty || loop->isProperAncestor(empty))
+    return nullptr;
+  // Whole-tensor write: no mask, in-bounds, identity permutation, zero indices.
+  if (write.getMask())
+    return nullptr;
+  if (!write.getPermutationMap().isIdentity())
+    return nullptr;
+  if (llvm::any_of(write.getInBoundsValues(), [](bool b) { return !b; }))
+    return nullptr;
+  if (!llvm::all_of(write.getIndices(), [](Value idx) {
+        return matchPattern(idx, m_Zero());
+      }))
+    return nullptr;
+  return write;
+}
+
+/// Checks that folding the yielded write for iter_arg index `idx` onto the
+/// iter_arg is legal: the iter_arg's every in-loop read must not observe the
+/// write, i.e. all reads must properly precede the write in the (single-block)
+/// loop body. A read after the write would, once the buffer is reused, observe
+/// this iteration's own store instead of the incoming value.
+static bool readsPrecedeWrite(ForOp loop, unsigned idx,
+                              vector::TransferWriteOp write,
+                              DominanceInfo &dominance) {
+  BlockArgument iterArg = loop.getRegionIterArgs()[idx];
+  for (OpOperand &use : iterArg.getUses()) {
+    Operation *user = use.getOwner();
+    // The yield use is the loop-carry itself; ignore it.
+    if (isa<scf::YieldOp>(user) && user->getParentOp() == loop)
+      continue;
+    // Any read must strictly dominate the write within the body.
+    if (!dominance.properlyDominates(user, write.getOperation()))
+      return false;
+  }
+  return true;
+}
+
+/// Attempts to fold the yielded write of iter_arg `idx` onto the iter_arg.
+/// Returns true if the IR was modified.
+static bool tryFoldIterArg(ForOp loop, unsigned idx, DominanceInfo &dominance) {
+  // Only shaped (tensor) iter_args participate.
+  BlockArgument iterArg = loop.getRegionIterArgs()[idx];
+  if (!isa<TensorType>(iterArg.getType()))
+    return false;
+
+  auto yieldOp = cast<scf::YieldOp>(loop.getBody()->getTerminator());
+  Value yielded = yieldOp.getOperand(idx);
+
+  // Already in place.
+  if (yielded == iterArg)
+    return false;
+
+  vector::TransferWriteOp write = getFoldableYieldWrite(yielded, loop);
+  if (!write)
+    return false;
+
+  // The write must produce exactly the yielded value (single use into yield).
+  if (!write.getResult().hasOneUse())
+    return false;
+
+  // Types must match so the destination swap is a pure rewrite.
+  if (write.getBase().getType() != iterArg.getType())
+    return false;
+
+  if (!readsPrecedeWrite(loop, idx, write, dominance))
+    return false;
+
+  // Redirect the write's destination from the fresh empty to the iter_arg.
+  // The now-dead empty is left for later DCE/canonicalization.
+  write.getBaseMutable().assign(iterArg);
+  return true;
+}
+
+namespace {
+struct LoopIterArgDestinationFoldingPass
+    : public bufferization::impl::LoopIterArgDestinationFoldingPassBase<
+          LoopIterArgDestinationFoldingPass> {
+  void runOnOperation() override {
+    DominanceInfo dominance(getOperation());
+    getOperation()->walk([&](ForOp loop) {
+      for (unsigned i = 0, e = loop.getInitArgs().size(); i < e; ++i)
+        (void)tryFoldIterArg(loop, i, dominance);
+    });
+  }
+};
+} // namespace
diff --git a/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir b/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir
new file mode 100644
index 0000000000000..63590bd350fca
--- /dev/null
+++ b/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir
@@ -0,0 +1,117 @@
+// RUN: mlir-opt %s -loop-iter-arg-destination-folding -split-input-file | FileCheck %s
+
+// A read-then-fully-overwritten iter_arg whose yielded value is a whole-tensor
+// transfer_write into an outside tensor.empty has its write destination folded
+// onto the iter_arg, making the carry in-place.
+
+// CHECK-LABEL: func.func @fold_read_then_write
+//       CHECK:   scf.for {{.*}} iter_args(%[[A:.*]] = %{{.*}})
+//       CHECK:     vector.transfer_read %[[A]]
+//       CHECK:     %[[W:.*]] = vector.transfer_write %{{.*}}, %[[A]]
+//       CHECK:     scf.yield %[[W]]
+func.func @fold_read_then_write(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> tensor<128xf32> {
+  %c0 = arith.constant 0 : index
+  %scratch = tensor.empty() : tensor<128xf32>
+  %r = scf.for %i = %lb to %ub step %st iter_args(%a = %init) -> (tensor<128xf32>) {
+    %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %n = arith.addf %v, %v : vector<128xf32>
+    %w = vector.transfer_write %n, %scratch[%c0] {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    scf.yield %w : tensor<128xf32>
+  }
+  return %r : tensor<128xf32>
+}
+
+// -----
+
+// The iter_arg is read a SECOND time after the write, so folding would make the
+// later read observe this iteration's own store. Must NOT fold: the write keeps
+// its scratch destination.
+
+// CHECK-LABEL: func.func @no_fold_read_after_write
+//       CHECK:   %[[S:.*]] = tensor.empty() : tensor<128xf32>
+//       CHECK:   scf.for {{.*}} iter_args(%[[A:.*]] = %{{.*}})
+//       CHECK:     vector.transfer_write %{{.*}}, %[[S]]
+//       CHECK:     vector.transfer_read %[[A]]
+func.func @no_fold_read_after_write(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> tensor<128xf32> {
+  %c0 = arith.constant 0 : index
+  %scratch = tensor.empty() : tensor<128xf32>
+  %r = scf.for %i = %lb to %ub step %st iter_args(%a = %init) -> (tensor<128xf32>) {
+    %v1 = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %n = arith.addf %v1, %v1 : vector<128xf32>
+    %w = vector.transfer_write %n, %scratch[%c0] {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    %v2 = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %use = arith.subf %v2, %n : vector<128xf32>
+    "test.keep"(%use) : (vector<128xf32>) -> ()
+    scf.yield %w : tensor<128xf32>
+  }
+  return %r : tensor<128xf32>
+}
+
+// -----
+
+// Multiple carried values sharing one outside empty: each safe slot is folded
+// independently; the empty's other uses are untouched.
+
+// CHECK-LABEL: func.func @fold_two_of_two
+//       CHECK:   scf.for {{.*}} iter_args(%[[A:.*]] = %{{.*}}, %[[B:.*]] = %{{.*}})
+//       CHECK:     vector.transfer_write %{{.*}}, %[[A]]
+//       CHECK:     vector.transfer_write %{{.*}}, %[[B]]
+//       CHECK:     scf.yield
+func.func @fold_two_of_two(%i0: tensor<128xf32>, %i1: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> (tensor<128xf32>, tensor<128xf32>) {
+  %c0 = arith.constant 0 : index
+  %s0 = tensor.empty() : tensor<128xf32>
+  %s1 = tensor.empty() : tensor<128xf32>
+  %r:2 = scf.for %i = %lb to %ub step %st iter_args(%a = %i0, %b = %i1) -> (tensor<128xf32>, tensor<128xf32>) {
+    %va = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %na = arith.addf %va, %va : vector<128xf32>
+    %wa = vector.transfer_write %na, %s0[%c0] {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    %vb = vector.transfer_read %b[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %nb = arith.addf %vb, %vb : vector<128xf32>
+    %wb = vector.transfer_write %nb, %s1[%c0] {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    scf.yield %wa, %wb : tensor<128xf32>, tensor<128xf32>
+  }
+  return %r#0, %r#1 : tensor<128xf32>, tensor<128xf32>
+}
+
+// -----
+
+// The yielded write is a partial (masked) write, so it does not fully define the
+// destination and reuse could drop live elements. Must NOT fold.
+
+// CHECK-LABEL: func.func @no_fold_partial_write
+//       CHECK:   %[[S:.*]] = tensor.empty() : tensor<128xf32>
+//       CHECK:   scf.for
+//       CHECK:     vector.transfer_write %{{.*}}, %[[S]]
+func.func @no_fold_partial_write(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32, %mask: vector<128xi1>) -> tensor<128xf32> {
+  %c0 = arith.constant 0 : index
+  %scratch = tensor.empty() : tensor<128xf32>
+  %r = scf.for %i = %lb to %ub step %st iter_args(%a = %init) -> (tensor<128xf32>) {
+    %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %n = arith.addf %v, %v : vector<128xf32>
+    %w = vector.transfer_write %n, %scratch[%c0], %mask {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    scf.yield %w : tensor<128xf32>
+  }
+  return %r : tensor<128xf32>
+}
+
+// -----
+
+// The write destination is defined inside the loop (not an outside scratch), so
+// there is no external buffer to fold away. Must NOT fold.
+
+// CHECK-LABEL: func.func @no_fold_inside_empty
+//       CHECK:   scf.for
+//       CHECK:     %[[E:.*]] = tensor.empty() : tensor<128xf32>
+//       CHECK:     %[[W:.*]] = vector.transfer_write %{{.*}}, %[[E]]
+//       CHECK:     scf.yield %[[W]]
+func.func @no_fold_inside_empty(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> tensor<128xf32> {
+  %c0 = arith.constant 0 : index
+  %r = scf.for %i = %lb to %ub step %st iter_args(%a = %init) -> (tensor<128xf32>) {
+    %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : tensor<128xf32>, vector<128xf32>
+    %n = arith.addf %v, %v : vector<128xf32>
+    %scratch = tensor.empty() : tensor<128xf32>
+    %w = vector.transfer_write %n, %scratch[%c0] {in_bounds = [true]} : vector<128xf32>, tensor<128xf32>
+    scf.yield %w : tensor<128xf32>
+  }
+  return %r : tensor<128xf32>
+}

>From 186f6b5d49badb8d2c599cbc15bfdd71890470fc Mon Sep 17 00:00:00 2001
From: Jianhui Li <jian.hui.li at intel.com>
Date: Fri, 31 Jul 2026 04:54:14 +0000
Subject: [PATCH 2/2] [mlir][bufferization] Sink yielded write to fold
 read-after-write iter_args

Extends loop-iter-arg-destination-folding to also handle iter_args that are read
*after* the yielded write in program order. Since the yielded write feeds only
the terminator, it is first moved to just before the yield so it follows every
read of the iter_arg, then its destination is folded onto the iter_arg. All
reads still observe the incoming value; only the final store defines the next
iteration.

This makes symmetric reductions fold uniformly. On the Flash-Attention kernel,
the running-max accumulator (read twice: once for max(), once for the rescale
term) previously blocked folding while the running-sum did not; both now fold,
taking the loop from three memref iter_args to zero. Verified semantics-
preserving by execution on a read-after-write reduction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply at anthropic.com>
---
 .../LoopIterArgDestinationFolding.cpp         | 48 +++++++++++--------
 .../loop-iter-arg-destination-folding.mlir    | 21 ++++----
 2 files changed, 39 insertions(+), 30 deletions(-)

diff --git a/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp b/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
index 7fd4ef7f35910..c64a60c219156 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
+++ b/mlir/lib/Dialect/Bufferization/Transforms/LoopIterArgDestinationFolding.cpp
@@ -49,7 +49,6 @@
 #include "mlir/Dialect/SCF/IR/SCF.h"
 #include "mlir/Dialect/Tensor/IR/Tensor.h"
 #include "mlir/Dialect/Vector/IR/VectorOps.h"
-#include "mlir/IR/Dominance.h"
 
 namespace mlir {
 namespace bufferization {
@@ -96,22 +95,27 @@ getFoldableYieldWrite(Value value, ForOp loop) {
   return write;
 }
 
-/// Checks that folding the yielded write for iter_arg index `idx` onto the
-/// iter_arg is legal: the iter_arg's every in-loop read must not observe the
-/// write, i.e. all reads must properly precede the write in the (single-block)
-/// loop body. A read after the write would, once the buffer is reused, observe
-/// this iteration's own store instead of the incoming value.
-static bool readsPrecedeWrite(ForOp loop, unsigned idx,
-                              vector::TransferWriteOp write,
-                              DominanceInfo &dominance) {
-  BlockArgument iterArg = loop.getRegionIterArgs()[idx];
-  for (OpOperand &use : iterArg.getUses()) {
+/// The yielded value's write only produces the loop-carried result, so it may
+/// be moved down to just before the terminator. Doing so places it after every
+/// other operation in the (single-block) body, in particular after every read
+/// of the iter_arg, which is what makes the in-place fold sound: the reads still
+/// observe the incoming value and only the final store defines what the next
+/// iteration reads. Moving down is legal because the write's result feeds solely
+/// the yield (checked by the caller) and its operands dominate the terminator
+/// (they are defined earlier in the same block).
+///
+/// Returns false only if the write cannot be scheduled after all reads, i.e. a
+/// read of the iter_arg transitively *depends on* the write's result. That
+/// cannot happen here (the result is yield-only), but the check is kept explicit
+/// for safety against future callers.
+static bool canScheduleWriteLast(ForOp loop, unsigned idx,
+                                 vector::TransferWriteOp write) {
+  // The write's result must not be consumed by anything other than the yield,
+  // otherwise moving it could break a dependency. The caller already ensures a
+  // single use that is the yield; re-verify defensively.
+  for (OpOperand &use : write.getResult().getUses()) {
     Operation *user = use.getOwner();
-    // The yield use is the loop-carry itself; ignore it.
-    if (isa<scf::YieldOp>(user) && user->getParentOp() == loop)
-      continue;
-    // Any read must strictly dominate the write within the body.
-    if (!dominance.properlyDominates(user, write.getOperation()))
+    if (!(isa<scf::YieldOp>(user) && user->getParentOp() == loop))
       return false;
   }
   return true;
@@ -119,7 +123,7 @@ static bool readsPrecedeWrite(ForOp loop, unsigned idx,
 
 /// Attempts to fold the yielded write of iter_arg `idx` onto the iter_arg.
 /// Returns true if the IR was modified.
-static bool tryFoldIterArg(ForOp loop, unsigned idx, DominanceInfo &dominance) {
+static bool tryFoldIterArg(ForOp loop, unsigned idx) {
   // Only shaped (tensor) iter_args participate.
   BlockArgument iterArg = loop.getRegionIterArgs()[idx];
   if (!isa<TensorType>(iterArg.getType()))
@@ -144,11 +148,14 @@ static bool tryFoldIterArg(ForOp loop, unsigned idx, DominanceInfo &dominance) {
   if (write.getBase().getType() != iterArg.getType())
     return false;
 
-  if (!readsPrecedeWrite(loop, idx, write, dominance))
+  if (!canScheduleWriteLast(loop, idx, write))
     return false;
 
-  // Redirect the write's destination from the fresh empty to the iter_arg.
+  // Move the write to just before the terminator so it follows every read of
+  // the iter_arg, then redirect its destination from the fresh empty to the
+  // iter_arg. Reads still see the incoming value; the store defines the carry.
   // The now-dead empty is left for later DCE/canonicalization.
+  write->moveBefore(yieldOp);
   write.getBaseMutable().assign(iterArg);
   return true;
 }
@@ -158,10 +165,9 @@ struct LoopIterArgDestinationFoldingPass
     : public bufferization::impl::LoopIterArgDestinationFoldingPassBase<
           LoopIterArgDestinationFoldingPass> {
   void runOnOperation() override {
-    DominanceInfo dominance(getOperation());
     getOperation()->walk([&](ForOp loop) {
       for (unsigned i = 0, e = loop.getInitArgs().size(); i < e; ++i)
-        (void)tryFoldIterArg(loop, i, dominance);
+        (void)tryFoldIterArg(loop, i);
     });
   }
 };
diff --git a/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir b/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir
index 63590bd350fca..3beceda36e7b0 100644
--- a/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir
+++ b/mlir/test/Dialect/Bufferization/Transforms/loop-iter-arg-destination-folding.mlir
@@ -1,4 +1,4 @@
-// RUN: mlir-opt %s -loop-iter-arg-destination-folding -split-input-file | FileCheck %s
+// RUN: mlir-opt %s -loop-iter-arg-destination-folding -split-input-file -allow-unregistered-dialect | FileCheck %s
 
 // A read-then-fully-overwritten iter_arg whose yielded value is a whole-tensor
 // transfer_write into an outside tensor.empty has its write destination folded
@@ -23,16 +23,19 @@ func.func @fold_read_then_write(%init: tensor<128xf32>, %lb: index, %ub: index,
 
 // -----
 
-// The iter_arg is read a SECOND time after the write, so folding would make the
-// later read observe this iteration's own store. Must NOT fold: the write keeps
-// its scratch destination.
+// The iter_arg is read a SECOND time after the write. Folding is still legal:
+// the write only feeds the yield, so it is sunk below every read of the iter_arg
+// before its destination is folded onto the iter_arg. Both reads therefore still
+// observe the incoming value; only the final store defines the next iteration.
 
-// CHECK-LABEL: func.func @no_fold_read_after_write
-//       CHECK:   %[[S:.*]] = tensor.empty() : tensor<128xf32>
+// CHECK-LABEL: func.func @fold_read_after_write_via_sink
 //       CHECK:   scf.for {{.*}} iter_args(%[[A:.*]] = %{{.*}})
-//       CHECK:     vector.transfer_write %{{.*}}, %[[S]]
-//       CHECK:     vector.transfer_read %[[A]]
-func.func @no_fold_read_after_write(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> tensor<128xf32> {
+//       CHECK:     %[[V1:.*]] = vector.transfer_read %[[A]]
+//       CHECK:     %[[V2:.*]] = vector.transfer_read %[[A]]
+//       CHECK:     arith.subf %[[V2]]
+//       CHECK:     %[[W:.*]] = vector.transfer_write %{{.*}}, %[[A]]
+//       CHECK:     scf.yield %[[W]]
+func.func @fold_read_after_write_via_sink(%init: tensor<128xf32>, %lb: index, %ub: index, %st: index, %pad: f32) -> tensor<128xf32> {
   %c0 = arith.constant 0 : index
   %scratch = tensor.empty() : tensor<128xf32>
   %r = scf.for %i = %lb to %ub step %st iter_args(%a = %init) -> (tensor<128xf32>) {



More information about the Mlir-commits mailing list