[Mlir-commits] [mlir] [mlir][linalg] Hoist transfer pairs through disjoint subview slices (PR #220244)

Hugo Pompougnac llvmlistbot at llvm.org
Tue Sep 1 06:15:15 PDT 2026


https://github.com/qaco created https://github.com/llvm/llvm-project/pull/220244

`hoistRedundantVectorTransfers` bails whenever a transfer pair is based on a view like `memref.subview`, since another view of the same buffer could alias it. This patch hoists the pair anyway when its base resolves to a static per-dimension footprint and every other access to that buffer in the loop is either the matching read/write or a provably-disjoint slice. Anything not statically modellable keeps the old conservative behaviour. Adds lit tests for the positive case and the disjointness, dynamic-offset, non-buffer-root, and aliasing-effect negatives.

Assisted-by: Claude Code (Anthropic)

>From bacba842075af25f43f1dac4a51b7cf9cb8fdd4f Mon Sep 17 00:00:00 2001
From: Hugo Pompougnac <tech at hugo-pompougnac.fr>
Date: Tue, 1 Sep 2026 15:05:55 +0200
Subject: [PATCH] [mlir][linalg] Hoist transfer pairs through disjoint subview
 slices

---
 .../Dialect/Linalg/Transforms/Hoisting.cpp    | 128 +++++++++++-
 mlir/test/Dialect/Linalg/hoisting.mlir        | 192 ++++++++++++++++++
 2 files changed, 318 insertions(+), 2 deletions(-)

diff --git a/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp b/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
index a573b4a54dbba..30a5047685fff 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
@@ -17,11 +17,14 @@
 #include "mlir/Dialect/Affine/IR/AffineOps.h"
 #include "mlir/Dialect/Affine/Utils.h"
 #include "mlir/Dialect/Linalg/Transforms/Transforms.h"
+#include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
 #include "mlir/Dialect/SCF/IR/SCF.h"
 #include "mlir/Dialect/SCF/Utils/Utils.h"
 #include "mlir/Dialect/Vector/IR/VectorOps.h"
 #include "mlir/Dialect/Vector/Utils/VectorUtils.h"
 #include "mlir/IR/Dominance.h"
+#include "mlir/Interfaces/SideEffectInterfaces.h"
 #include "mlir/Transforms/LoopInvariantCodeMotionUtils.h"
 #include "llvm/Support/Debug.h"
 
@@ -195,6 +198,116 @@ static bool noAliasingUseInLoop(vector::TransferReadOp transferRead,
   return true;
 }
 
+namespace {
+/// Static footprint of a memref value resolved through a chain of
+/// rank-preserving, unit-stride `memref.subview` ops: the underlying root
+/// memref and, per root dimension, the `[offset, offset + size)` interval it
+/// covers.
+struct SubviewFootprint {
+  Value root;
+  SmallVector<int64_t> offsets;
+  SmallVector<int64_t> sizes;
+};
+} // namespace
+
+/// Resolve `base` through a `memref.subview` chain to its static footprint, or
+/// `std::nullopt` if it cannot be modelled statically.
+static std::optional<SubviewFootprint> resolveSubviewFootprint(Value base) {
+  SmallVector<memref::SubViewOp> chain;
+  Value source = base;
+  while (auto sv = source.getDefiningOp<memref::SubViewOp>()) {
+    chain.push_back(sv);
+    source = sv.getSource();
+  }
+  // The resolved root must be a genuine buffer, not another kind of view.
+  if (isa_and_nonnull<ViewLikeOpInterface>(source.getDefiningOp()))
+    return std::nullopt;
+
+  auto rootTy = dyn_cast<MemRefType>(source.getType());
+  if (!rootTy || !rootTy.hasStaticShape())
+    return std::nullopt;
+  SubviewFootprint fp;
+  fp.root = source;
+  // Seed with the whole-root footprint (offset 0, full extent per dimension):
+  // this is the result for a bare buffer, and each subview in the chain narrows
+  // it below.
+  fp.offsets.assign(rootTy.getRank(), 0);
+  fp.sizes.assign(rootTy.getShape().begin(), rootTy.getShape().end());
+  // Compose from the outermost subview (closest to the root) inward.
+  for (memref::SubViewOp sv : llvm::reverse(chain)) {
+    // Rank-reducing subviews would misalign the per-dimension bookkeeping.
+    if (sv.getSourceType().getRank() != sv.getType().getRank())
+      return std::nullopt;
+    ArrayRef<int64_t> offs = sv.getStaticOffsets();
+    ArrayRef<int64_t> szs = sv.getStaticSizes();
+    ArrayRef<int64_t> strides = sv.getStaticStrides();
+    if (llvm::any_of(offs, ShapedType::isDynamic) ||
+        llvm::any_of(szs, ShapedType::isDynamic) ||
+        llvm::any_of(strides, [](int64_t s) { return s != 1; }))
+      return std::nullopt;
+    for (unsigned d = 0, e = offs.size(); d < e; ++d) {
+      fp.offsets[d] += offs[d];
+      fp.sizes[d] = szs[d];
+    }
+  }
+  return fp;
+}
+
+/// True if two footprints provably cover disjoint memory: they must share the
+/// same root and be separated along at least one dimension.
+static bool footprintsDisjoint(const SubviewFootprint &a,
+                               const SubviewFootprint &b) {
+  if (a.root != b.root)
+    return false;
+  for (unsigned d = 0, e = a.offsets.size(); d < e; ++d) {
+    int64_t aHi = a.offsets[d] + a.sizes[d];
+    int64_t bHi = b.offsets[d] + b.sizes[d];
+    if (aHi <= b.offsets[d] || bHi <= a.offsets[d])
+      return true;
+  }
+  return false;
+}
+
+/// Subview-aware safety check for hoisting `read`/`write` out of `loop`.
+static bool footprintAliasingIsSafe(LoopLikeOpInterface loop,
+                                    vector::TransferReadOp read,
+                                    vector::TransferWriteOp write) {
+  std::optional<SubviewFootprint> ourFp =
+      resolveSubviewFootprint(read.getBase());
+  if (!ourFp)
+    return false;
+  // `resolveSubviewFootprint` guarantees the root is a genuine buffer.
+  Value ourBuffer = ourFp->root;
+  auto reachesOurBuffer = [&](Value operand) {
+    auto memrefOperand = dyn_cast<MemrefValue>(operand);
+    return memrefOperand && memref::skipViewLikeOps(memrefOperand) == ourBuffer;
+  };
+  WalkResult res = loop->walk([&](Operation *op) {
+    if (op == read.getOperation() || op == write.getOperation())
+      return WalkResult::advance();
+    if (auto xfer = dyn_cast<VectorTransferOpInterface>(op)) {
+      Value otherBase = xfer.getBase();
+      // A tensor transfer or a different underlying buffer cannot alias ours.
+      if (!reachesOurBuffer(otherBase))
+        return WalkResult::advance();
+      // Same buffer: only safe if we can prove the accessed slices are
+      // statically disjoint.
+      std::optional<SubviewFootprint> otherFp =
+          resolveSubviewFootprint(otherBase);
+      if (otherFp && footprintsDisjoint(*ourFp, *otherFp))
+        return WalkResult::advance();
+      return WalkResult::interrupt();
+    }
+    // Any other op that may touch memory aliases ours if one of its memref
+    // operands reaches our buffer.
+    if (!isMemoryEffectFree(op) &&
+        llvm::any_of(op->getOperands(), reachesOurBuffer))
+      return WalkResult::interrupt();
+    return WalkResult::advance();
+  });
+  return !res.wasInterrupted();
+}
+
 void mlir::linalg::hoistRedundantVectorTransfers(Operation *root,
                                                  bool verifyNonZeroTrip) {
   bool changed = true;
@@ -314,6 +427,15 @@ void mlir::linalg::hoistRedundantVectorTransfers(Operation *root,
       // Check 2. Note, since both xfer Ops share the source, we only need to
       // look at one of them.
       auto base = transferRead.getBase();
+      // Whether hoisting is safe despite a view base. Computed lazily and
+      // cached since it is only consulted when a view is present.
+      std::optional<bool> viewAliasingIsSafeCache;
+      auto viewAliasingIsSafe = [&]() {
+        if (!viewAliasingIsSafeCache)
+          viewAliasingIsSafeCache =
+              footprintAliasingIsSafe(loop, transferRead, transferWrite);
+        return *viewAliasingIsSafeCache;
+      };
       auto *source = base.getDefiningOp();
       if (source) {
         // NOTE: We treat `memref.assume_alignment` as a special case.
@@ -337,11 +459,13 @@ void mlir::linalg::hoistRedundantVectorTransfers(Operation *root,
           if (numInLoopUses && memPreAlignment.hasOneUse())
             source = memPreAlignment.getDefiningOp();
         }
-        if (isa_and_nonnull<ViewLikeOpInterface>(source))
+        if (isa_and_nonnull<ViewLikeOpInterface>(source) &&
+            !viewAliasingIsSafe())
           return WalkResult::advance();
       }
 
-      if (llvm::any_of(base.getUsers(), llvm::IsaPred<ViewLikeOpInterface>))
+      if (llvm::any_of(base.getUsers(), llvm::IsaPred<ViewLikeOpInterface>) &&
+          !viewAliasingIsSafe())
         return WalkResult::advance();
 
       // Check 3.
diff --git a/mlir/test/Dialect/Linalg/hoisting.mlir b/mlir/test/Dialect/Linalg/hoisting.mlir
index d947c2ac3c466..cd25aca879efb 100644
--- a/mlir/test/Dialect/Linalg/hoisting.mlir
+++ b/mlir/test/Dialect/Linalg/hoisting.mlir
@@ -1171,3 +1171,195 @@ module attributes {transform.with_named_sequence} {
     transform.yield
   }
 }
+
+// -----
+
+///----------------------------------------------------------------------------------------
+/// Tests for hoisting through `memref.subview` views, guarded by a static
+/// footprint disjointness check.
+///----------------------------------------------------------------------------------------
+
+// Disjoint static subviews of one buffer: both transfer pairs are hoisted.
+
+// CHECK-LABEL:   func.func @hoist_disjoint_static_subviews(
+// CHECK-SAME:      %[[MEM:[a-zA-Z0-9]+]]: memref<4xf32>,
+// CHECK:           %[[SV0:.*]] = memref.subview %[[MEM]][0] [2] [1]
+// CHECK-NEXT:      %[[SV1:.*]] = memref.subview %[[MEM]][2] [2] [1]
+// CHECK-NEXT:      %[[R0:.*]] = vector.transfer_read %[[SV0]]
+// CHECK-NEXT:      %[[R1:.*]] = vector.transfer_read %[[SV1]]
+// CHECK-NEXT:      %[[FOR:.*]]:2 = scf.for {{.*}} iter_args(%[[I0:.*]] = %[[R0]], %[[I1:.*]] = %[[R1]]) -> (vector<2xf32>, vector<2xf32>) {
+// CHECK-NEXT:        %[[U0:.*]] = "val_use"(%[[I0]])
+// CHECK-NEXT:        %[[U1:.*]] = "val_use"(%[[I1]])
+// CHECK-NEXT:        scf.yield %[[U0]], %[[U1]]
+// CHECK-NEXT:      }
+// CHECK-NEXT:      vector.transfer_write %[[FOR]]#1, %[[SV1]]
+// CHECK-NEXT:      vector.transfer_write %[[FOR]]#0, %[[SV0]]
+func.func @hoist_disjoint_static_subviews(
+    %mem: memref<4xf32>, %lb : index, %ub : index, %step: index) {
+  %c0 = arith.constant 0 : index
+  %pad = arith.constant 0.0 : f32
+  %sv0 = memref.subview %mem[0][2][1] : memref<4xf32> to memref<2xf32, strided<[1]>>
+  %sv1 = memref.subview %mem[2][2][1] : memref<4xf32> to memref<2xf32, strided<[1], offset: 2>>
+  scf.for %i = %lb to %ub step %step {
+    %r0 = vector.transfer_read %sv0[%c0], %pad : memref<2xf32, strided<[1]>>, vector<2xf32>
+    %u0 = "val_use"(%r0) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u0, %sv0[%c0] : vector<2xf32>, memref<2xf32, strided<[1]>>
+
+    %r1 = vector.transfer_read %sv1[%c0], %pad : memref<2xf32, strided<[1], offset: 2>>, vector<2xf32>
+    %u1 = "val_use"(%r1) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u1, %sv1[%c0] : vector<2xf32>, memref<2xf32, strided<[1], offset: 2>>
+  }
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+    %0 = transform.structured.match ops{["func.func"]} in %arg1
+      : (!transform.any_op) -> !transform.any_op
+    transform.structured.hoist_redundant_vector_transfers %0
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+}
+
+// -----
+
+// Overlapping static subviews are not provably disjoint: hoisting is blocked.
+
+// CHECK-LABEL:   func.func @negative_hoist_overlapping_static_subviews(
+// CHECK:           scf.for {{.*}} step %{{.*}} {
+// CHECK:             vector.transfer_read
+// CHECK:             vector.transfer_write
+// CHECK:             vector.transfer_read
+// CHECK:             vector.transfer_write
+// CHECK:           }
+
+func.func @negative_hoist_overlapping_static_subviews(
+    %mem: memref<4xf32>, %lb : index, %ub : index, %step: index) {
+  %c0 = arith.constant 0 : index
+  %pad = arith.constant 0.0 : f32
+  %sv0 = memref.subview %mem[0][2][1] : memref<4xf32> to memref<2xf32, strided<[1]>>
+  %sv1 = memref.subview %mem[1][2][1] : memref<4xf32> to memref<2xf32, strided<[1], offset: 1>>
+  scf.for %i = %lb to %ub step %step {
+    %r0 = vector.transfer_read %sv0[%c0], %pad : memref<2xf32, strided<[1]>>, vector<2xf32>
+    %u0 = "val_use"(%r0) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u0, %sv0[%c0] : vector<2xf32>, memref<2xf32, strided<[1]>>
+
+    %r1 = vector.transfer_read %sv1[%c0], %pad : memref<2xf32, strided<[1], offset: 1>>, vector<2xf32>
+    %u1 = "val_use"(%r1) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u1, %sv1[%c0] : vector<2xf32>, memref<2xf32, strided<[1], offset: 1>>
+  }
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+    %0 = transform.structured.match ops{["func.func"]} in %arg1
+      : (!transform.any_op) -> !transform.any_op
+    transform.structured.hoist_redundant_vector_transfers %0
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+}
+
+// -----
+
+// A dynamic subview offset yields no static footprint: hoisting is blocked.
+
+// CHECK-LABEL:   func.func @negative_hoist_dynamic_offset_subview(
+// CHECK:           scf.for {{.*}} step %{{.*}} {
+// CHECK:             vector.transfer_read
+// CHECK:             vector.transfer_write
+// CHECK:           }
+
+func.func @negative_hoist_dynamic_offset_subview(
+    %mem: memref<4xf32>, %off: index, %lb : index, %ub : index, %step: index) {
+  %c0 = arith.constant 0 : index
+  %pad = arith.constant 0.0 : f32
+  %sv = memref.subview %mem[%off][2][1] : memref<4xf32> to memref<2xf32, strided<[1], offset: ?>>
+  scf.for %i = %lb to %ub step %step {
+    %r0 = vector.transfer_read %sv[%c0], %pad : memref<2xf32, strided<[1], offset: ?>>, vector<2xf32>
+    %u0 = "val_use"(%r0) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u0, %sv[%c0] : vector<2xf32>, memref<2xf32, strided<[1], offset: ?>>
+  }
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+    %0 = transform.structured.match ops{["func.func"]} in %arg1
+      : (!transform.any_op) -> !transform.any_op
+    transform.structured.hoist_redundant_vector_transfers %0
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+}
+
+// -----
+
+// Subview rooted at a collapse_shape (not a genuine buffer): hoisting blocked.
+
+// CHECK-LABEL:   func.func @negative_hoist_subview_of_collapse_shape(
+// CHECK:           scf.for {{.*}} step %{{.*}} {
+// CHECK:             vector.transfer_read
+// CHECK:             vector.transfer_write
+// CHECK:           }
+
+func.func @negative_hoist_subview_of_collapse_shape(
+    %mem: memref<2x2xf32>, %lb : index, %ub : index, %step: index) {
+  %c0 = arith.constant 0 : index
+  %pad = arith.constant 0.0 : f32
+  %collapse = memref.collapse_shape %mem [[0, 1]] : memref<2x2xf32> into memref<4xf32>
+  %sv = memref.subview %collapse[0][2][1] : memref<4xf32> to memref<2xf32, strided<[1]>>
+  scf.for %i = %lb to %ub step %step {
+    %r0 = vector.transfer_read %sv[%c0], %pad : memref<2xf32, strided<[1]>>, vector<2xf32>
+    %u0 = "val_use"(%r0) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u0, %sv[%c0] : vector<2xf32>, memref<2xf32, strided<[1]>>
+  }
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+    %0 = transform.structured.match ops{["func.func"]} in %arg1
+      : (!transform.any_op) -> !transform.any_op
+    transform.structured.hoist_redundant_vector_transfers %0
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+}
+
+// -----
+
+// A non-transfer op touching the buffer may alias: hoisting is blocked.
+
+// CHECK-LABEL:   func.func @negative_hoist_subview_extra_memory_effect(
+// CHECK:           scf.for {{.*}} step %{{.*}} {
+// CHECK:             vector.transfer_read
+// CHECK:             vector.transfer_write
+// CHECK:             "prevent_hoisting"
+// CHECK:           }
+
+func.func @negative_hoist_subview_extra_memory_effect(
+    %mem: memref<4xf32>, %lb : index, %ub : index, %step: index) {
+  %c0 = arith.constant 0 : index
+  %pad = arith.constant 0.0 : f32
+  %sv0 = memref.subview %mem[0][2][1] : memref<4xf32> to memref<2xf32, strided<[1]>>
+  scf.for %i = %lb to %ub step %step {
+    %r0 = vector.transfer_read %sv0[%c0], %pad : memref<2xf32, strided<[1]>>, vector<2xf32>
+    %u0 = "val_use"(%r0) : (vector<2xf32>) -> vector<2xf32>
+    vector.transfer_write %u0, %sv0[%c0] : vector<2xf32>, memref<2xf32, strided<[1]>>
+    "prevent_hoisting"(%mem) : (memref<4xf32>) -> ()
+  }
+  return
+}
+
+module attributes {transform.with_named_sequence} {
+  transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+    %0 = transform.structured.match ops{["func.func"]} in %arg1
+      : (!transform.any_op) -> !transform.any_op
+    transform.structured.hoist_redundant_vector_transfers %0
+      : (!transform.any_op) -> !transform.any_op
+    transform.yield
+  }
+}



More information about the Mlir-commits mailing list