[Stream] Fold transferred stream clones into minimal slices before loads (#24740)

This PR Extends clone folding for `stream.async.load` and
`stream.tensor.load` to
handle the staging transfer introduced before loads.

Both stream.async.load and stream.tensor.load require their source to be
a staging resource, while stream.async.clone and stream.tensor.clone
always produce non-staging resources. As a result, loads from cloned
resources are always preceded by a stream.async.transfer.

This change matches the pattern:
`
  clone -> transfer -> load
`

and rewrites it to:
`
  slice -> transfer -> load
`

by creating a minimal slice of the original source, transferring only
that slice to staging, and loading from it. This avoids cloning and
transferring the entire resource when only a single value is accessed,
while preserving the required staging semantics.

---------

Signed-off-by: LekkalaSravya3 <lekkala.sravya@multicorewareinc.com>
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp
index 3dca743..42f7041 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOpFolders.cpp
@@ -1410,14 +1410,90 @@
 //===----------------------------------------------------------------------===//
 // stream.tensor.load
 //===----------------------------------------------------------------------===//
+namespace {
+
+// Replaces a load from a transferred clone with a load from a
+// transferred single-element slice of the original source.
+struct FoldTensorCloneIntoLoad : OpRewritePattern<TensorLoadOp> {
+  using Base::Base;
+
+  LogicalResult matchAndRewrite(TensorLoadOp loadOp,
+                                PatternRewriter &rewriter) const override {
+    // Match: stream.tensor.load <- stream.async.transfer
+    auto transferOp = loadOp.getSource().getDefiningOp<AsyncTransferOp>();
+    if (!transferOp) {
+      return failure();
+    }
+
+    if (!transferOp.getResult().hasOneUse()) {
+      return failure();
+    }
+
+    // Match: stream.async.transfer <- stream.tensor.clone
+    auto cloneOp = transferOp.getSource().getDefiningOp<TensorCloneOp>();
+    if (!cloneOp) {
+      return failure();
+    }
+
+    // Clones are allowed to change shape and element type ,Only fold when the
+    // clone is a pure copy so slicing the original source is equivalent to
+    // slicing the clone result.
+    if (cloneOp.getSourceEncoding() != cloneOp.getResultEncoding() ||
+        cloneOp.getResultEncoding() != loadOp.getSourceEncoding()) {
+      return failure();
+    }
+    auto sourceType = dyn_cast<RankedTensorType>(loadOp.getSourceEncoding());
+    if (!sourceType) {
+      return failure();
+    }
+    int64_t rank = sourceType.getRank();
+
+    auto loc = loadOp.getLoc();
+
+    // Create a single-element slice (length 1 in every dimension).
+    auto one = arith::ConstantIndexOp::create(rewriter, loc, 1);
+    auto zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
+    SmallVector<Value> lengths(rank, one);
+    SmallVector<Value> zeroIndices(rank, zero);
+
+    SmallVector<int64_t> unitShape(rank, 1);
+    auto sliceType = RankedTensorType::get(
+        unitShape, sourceType.getElementType(), sourceType.getEncoding());
+
+    Value sliceSize = TensorSizeOfOp::create(
+        rewriter, loc, rewriter.getIndexType(), TypeAttr::get(sliceType),
+        ValueRange{}, cloneOp.getAffinityAttr());
+
+    // Slice the original tensor instead of the cloned tensor.
+    auto sliceOp = TensorSliceOp::create(
+        rewriter, loc, cloneOp.getSource().getType(), cloneOp.getSource(),
+        cloneOp.getSourceEncoding(), cloneOp.getSourceEncodingDims(),
+        cloneOp.getSourceSize(), loadOp.getIndices(), lengths, sliceType,
+        ValueRange{}, sliceSize, cloneOp.getAffinityAttr());
+
+    // Transfer the sliced tensor to staging.
+    auto stagedSliceOp = AsyncTransferOp::create(
+        rewriter, loc, transferOp.getResult().getType(), sliceOp.getResult(),
+        sliceSize, sliceSize, transferOp.getSourceAffinityAttr(),
+        transferOp.getResultAffinityAttr());
+
+    // Load from the staged slice at zero indices.
+    rewriter.replaceOpWithNewOp<TensorLoadOp>(
+        loadOp, loadOp.getResult().getType(), stagedSliceOp.getResult(),
+        sliceType, ValueRange{}, sliceSize, zeroIndices);
+
+    return success();
+  }
+};
+
+} // namespace
 
 void TensorLoadOp::getCanonicalizationPatterns(RewritePatternSet &results,
                                                MLIRContext *context) {
   // TODO(benvanik): splat + load -> splat value.
-  // TODO(benvanik): clone + ex load -> slice (ranged) + load.
-  // TODO(benvanik): slice + ex load -> slice (ranged) + load.
   // TODO(benvanik): value->transfer->load -> value->slice->transfer->load?
   // TODO(benvanik): combine multiple loads from the same target if contiguous.
+  results.insert<FoldTensorCloneIntoLoad>(context);
 }
 
 //===----------------------------------------------------------------------===//
@@ -2250,16 +2326,74 @@
   }
 };
 
+// Replaces a load from a transferred clone with a load from a
+// transferred byte-ranged slice of the original source.
+struct FoldAsyncCloneIntoLoad : OpRewritePattern<AsyncLoadOp> {
+  using Base::Base;
+
+  LogicalResult matchAndRewrite(AsyncLoadOp loadOp,
+                                PatternRewriter &rewriter) const override {
+    // Match: stream.async.load <- stream.async.transfer
+    auto transferOp = loadOp.getSource().getDefiningOp<AsyncTransferOp>();
+    if (!transferOp) {
+      return failure();
+    }
+    // Only fold if the transfer is not used by anything else: otherwise the
+    // original clone+transfer will still be required and we'd just be adding
+    // an additional slice+transfer alongside it.
+    if (!transferOp.getResult().hasOneUse()) {
+      return failure();
+    }
+
+    // Match: stream.async.transfer <- stream.async.clone
+    auto cloneOp = transferOp.getSource().getDefiningOp<AsyncCloneOp>();
+    if (!cloneOp) {
+      return failure();
+    }
+
+    auto loc = loadOp.getLoc();
+    int64_t byteSize =
+        IREE::Util::getRoundedElementByteWidth(loadOp.getResult().getType());
+
+    auto byteSizeValue =
+        arith::ConstantIndexOp::create(rewriter, loc, byteSize);
+    auto zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
+
+    // Compute the end of the byte range to slice.
+    auto sourceEnd = rewriter.createOrFold<arith::AddIOp>(
+        loc, loadOp.getSourceOffset(), byteSizeValue);
+
+    // Slice the original resource instead of the cloned resource.
+    auto sliceOp = AsyncSliceOp::create(
+        rewriter, loc, cloneOp.getSource().getType(), cloneOp.getSource(),
+        cloneOp.getSourceSize(), loadOp.getSourceOffset(), sourceEnd,
+        byteSizeValue, cloneOp.getAffinityAttr());
+
+    // Transfer the sliced resource to staging.
+    auto stagedSliceOp = AsyncTransferOp::create(
+        rewriter, loc, transferOp.getResult().getType(), sliceOp.getResult(),
+        byteSizeValue, byteSizeValue, transferOp.getSourceAffinityAttr(),
+        transferOp.getResultAffinityAttr());
+
+    // Load from the staged slice at offset 0.
+    rewriter.replaceOpWithNewOp<AsyncLoadOp>(
+        loadOp, loadOp.getResult().getType(), stagedSliceOp.getResult(),
+        byteSizeValue, zero);
+
+    return success();
+  }
+};
+
 } // namespace
 
 void AsyncLoadOp::getCanonicalizationPatterns(RewritePatternSet &results,
                                               MLIRContext *context) {
   // TODO(benvanik): splat + load -> splat value.
-  // TODO(benvanik): clone + ex load -> slice (ranged) + load.
   // TODO(benvanik): slice + ex load -> slice (ranged) + load.
   // TODO(benvanik): value->transfer->load -> value->slice->transfer->load?
   // TODO(benvanik): combine multiple loads from the same target if contiguous.
   results.insert<FoldAsyncLoadBitcast>(context);
+  results.insert<FoldAsyncCloneIntoLoad>(context);
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/test/async_folding.mlir b/compiler/src/iree/compiler/Dialect/Stream/IR/test/async_folding.mlir
index b8977e4..49a3a32 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/test/async_folding.mlir
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/test/async_folding.mlir
@@ -501,6 +501,22 @@
 
 // -----
 
+// CHECK-LABEL: @FoldAsyncCloneIntoLoad
+util.func private @FoldAsyncCloneIntoLoad(%arg0: !stream.resource<external>, %arg1: index, %arg2: index) -> f32 {
+  // CHECK-NOT: stream.async.clone
+  %0 = stream.async.clone %arg0 : !stream.resource<external>{%arg1} -> !stream.resource<*>{%arg1}
+  %1 = stream.async.transfer %0 : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+  // CHECK: %[[END:.+]] = arith.addi %arg2, %c4 : index
+  // CHECK: %[[SLICE:.+]] = stream.async.slice %arg0[%arg2 to %[[END]]] : !stream.resource<external>{%arg1} -> !stream.resource<external>{%c4}
+  // CHECK: %[[STAGED:.+]] = stream.async.transfer %[[SLICE]] : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+  // CHECK: %[[VALUE:.+]] = stream.async.load %[[STAGED]][%c0] : !stream.resource<staging>{%c4} -> f32
+  %2 = stream.async.load %1[%arg2] : !stream.resource<staging>{%arg1} -> f32
+  // CHECK: util.return %[[VALUE]]
+  util.return %2 : f32
+}
+
+// -----
+
 // CHECK-LABEL: @FoldAsyncStoreBitcast
 util.func private @FoldAsyncStoreBitcast(%arg0: !stream.resource<staging>, %arg1: index, %arg2: f32) -> !stream.resource<staging> {
   %c0 = arith.constant 0 : index
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/test/tensor_folding.mlir b/compiler/src/iree/compiler/Dialect/Stream/IR/test/tensor_folding.mlir
index cb6088c..1271328 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/test/tensor_folding.mlir
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/test/tensor_folding.mlir
@@ -206,6 +206,25 @@
 
 // -----
 
+// CHECK-LABEL: @FoldTensorCloneIntoLoad
+util.func private @FoldTensorCloneIntoLoad(%arg0: !stream.resource<external>, %arg1: index) -> f32 {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  // CHECK-NOT: stream.tensor.clone
+  %0 = stream.tensor.clone %arg0 : tensor<4x4xf32> in !stream.resource<external>{%arg1} -> tensor<4x4xf32> in !stream.resource<*>{%arg1}
+  %1 = stream.async.transfer %0 : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+  // CHECK: %[[SIZE:.+]] = stream.tensor.sizeof tensor<1x1xf32> : index
+  // CHECK: %[[SLICE:.+]] = stream.tensor.slice %arg0[%c0, %c1 for %c1, %c1] : tensor<4x4xf32> in !stream.resource<external>{%arg1} -> tensor<1x1xf32> in !stream.resource<external>{%[[SIZE]]}
+  // CHECK: %[[STAGED:.+]] = stream.async.transfer %[[SLICE]] : !stream.resource<external>{%[[SIZE]]} -> !stream.resource<staging>{%[[SIZE]]}
+  // CHECK: %[[VALUE:.+]] = stream.tensor.load %[[STAGED]][%c0, %c0] : tensor<1x1xf32> in !stream.resource<staging>{%[[SIZE]]} -> f32
+  %2 = stream.tensor.load %1[%c0, %c1] : tensor<4x4xf32> in !stream.resource<staging>{%arg1} -> f32
+  // CHECK: util.return %[[VALUE]]
+  util.return %2 : f32
+}
+
+
+// -----
+
 // CHECK-LABEL: @ElideUnneededTensorClones
 util.func private @ElideUnneededTensorClones(%arg0: !stream.resource<*>, %arg1: index) -> f32 {
   %c0 = arith.constant 0 : index