[LLVMGPU] Add pass to distribute undistributed copies to threads (#19715)
This pass walks a function and distributes any memref copies not present
within an scf.forall distributed to threads/warps/lanes. This pass
assumes that implicit distribution (a la gpu.thread_id) is not present.
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
index c35b511..a2b98a5 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/BUILD.bazel
@@ -56,6 +56,7 @@
"GPUCombineValueBarriers.cpp",
"GPUCreateFastSlowPath.cpp",
"GPUDistribute.cpp",
+ "GPUDistributeCopyUsingForall.cpp",
"GPUDistributeForall.cpp",
"GPUDistributeScfFor.cpp",
"GPUDistributeSharedMemoryCopy.cpp",
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
index 51576eb..df7015c 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/CMakeLists.txt
@@ -54,6 +54,7 @@
"GPUCombineValueBarriers.cpp"
"GPUCreateFastSlowPath.cpp"
"GPUDistribute.cpp"
+ "GPUDistributeCopyUsingForall.cpp"
"GPUDistributeForall.cpp"
"GPUDistributeScfFor.cpp"
"GPUDistributeSharedMemoryCopy.cpp"
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/GPUDistributeCopyUsingForall.cpp b/compiler/src/iree/compiler/Codegen/Common/GPU/GPUDistributeCopyUsingForall.cpp
new file mode 100644
index 0000000..85099df
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/GPUDistributeCopyUsingForall.cpp
@@ -0,0 +1,171 @@
+// Copyright 2025 The IREE Authors
+//
+// Licensed 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
+
+#include "iree/compiler/Codegen/Common/GPU/Passes.h"
+#include "iree/compiler/Codegen/Dialect/GPU/IR/IREEGPUAttrs.h"
+#include "iree/compiler/Codegen/Utils/GPUUtils.h"
+#include "mlir/Dialect/Affine/IR/AffineOps.h"
+#include "mlir/Dialect/Arith/Utils/Utils.h"
+#include "mlir/Dialect/GPU/IR/GPUDialect.h"
+#include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/SCF/Utils/Utils.h"
+#include "mlir/Dialect/Utils/StaticValueUtils.h"
+#include "mlir/Dialect/Vector/Transforms/VectorTransforms.h"
+
+#define DEBUG_TYPE "iree-codegen-gpu-distribute-shared-memory-copy"
+
+namespace mlir::iree_compiler {
+
+#define GEN_PASS_DEF_GPUDISTRIBUTECOPYUSINGFORALLPASS
+#include "iree/compiler/Codegen/Common/GPU/Passes.h.inc"
+
+namespace {
+//====---------------------------------------------------------------------===//
+// Pass to lower workgroup memory copy to distibuted
+// transfer_read/transfer_write ops.
+//====---------------------------------------------------------------------===//
+
+// For optimal performance we always want to copy 128 bits
+static constexpr int kPreferredCopyNumBits = 128;
+
+// Moves the copy into a single threaded forall.
+static void distributeCopyToSingleThread(RewriterBase &rewriter,
+ memref::CopyOp copy) {
+ SmallVector<Attribute> mapping = {gpu::GPUThreadMappingAttr::get(
+ rewriter.getContext(), gpu::MappingId::LinearDim0)};
+ scf::ForallOp newForallOp = rewriter.create<scf::ForallOp>(
+ copy.getLoc(), ArrayRef<OpFoldResult>{rewriter.getIndexAttr(0)},
+ ArrayRef<OpFoldResult>{rewriter.getIndexAttr(1)},
+ ArrayRef<OpFoldResult>{rewriter.getIndexAttr(1)},
+ /*outputs=*/ValueRange(), /*mapping=*/rewriter.getArrayAttr(mapping));
+ rewriter.moveOpBefore(copy, newForallOp.getBody(),
+ newForallOp.getBody()->begin());
+}
+
+/// Distributes a copy with a thread mapping.
+static void distributeCopyToThreads(RewriterBase &rewriter, memref::CopyOp copy,
+ ArrayRef<OpFoldResult> tileSizes) {
+ int64_t rank = tileSizes.size();
+ assert(rank == copy.getTarget().getType().getRank() &&
+ "tile size and copy rank mismatch");
+ if (rank == 0) {
+ distributeCopyToSingleThread(rewriter, copy);
+ return;
+ }
+
+ Location loc = copy.getLoc();
+ MLIRContext *context = rewriter.getContext();
+
+ SmallVector<OpFoldResult> lowerBounds(rank, rewriter.getIndexAttr(0));
+ SmallVector<OpFoldResult> upperBounds =
+ memref::getMixedSizes(rewriter, loc, copy.getSource());
+
+ SmallVector<Attribute> mapping;
+ int idx = 0;
+ for (int64_t i = 0, e = rank; i < e; ++i) {
+ unsigned mappingId =
+ static_cast<unsigned>(gpu::MappingId::LinearDim0) + idx++;
+ mapping.push_back(gpu::GPUThreadMappingAttr::get(
+ context, static_cast<gpu::MappingId>(mappingId)));
+ }
+ mapping = llvm::to_vector(llvm::reverse(mapping));
+
+ scf::ForallOp newForallOp = rewriter.create<scf::ForallOp>(
+ copy.getLoc(), lowerBounds, upperBounds, tileSizes,
+ /*outputs=*/ValueRange(), /*mapping=*/rewriter.getArrayAttr(mapping));
+
+ rewriter.setInsertionPointToStart(newForallOp.getBody());
+
+ AffineExpr d0, d1, d2;
+ bindDims(context, d0, d1, d2);
+ SmallVector<OpFoldResult> sizes;
+ AffineMap minMap =
+ AffineMap::get(/*dimCount=*/3, /*symbolCount=*/0, {d0, d1 - d2}, context);
+ for (auto [ub, tileSize, iterator] : llvm::zip_equal(
+ upperBounds, tileSizes, newForallOp.getInductionVars())) {
+ std::optional<int64_t> staticUb = getConstantIntValue(ub);
+ std::optional<int64_t> staticTileSize = getConstantIntValue(tileSize);
+ if ((staticUb && staticTileSize &&
+ staticUb.value() % staticTileSize.value() == 0) ||
+ (staticTileSize.value_or(0) == 1)) {
+ sizes.push_back(tileSize);
+ } else {
+ sizes.push_back(
+ rewriter
+ .create<affine::AffineMinOp>(
+ loc, rewriter.getIndexType(), minMap,
+ ValueRange{
+ getValueOrCreateConstantIndexOp(rewriter, loc, tileSize),
+ getValueOrCreateConstantIndexOp(rewriter, loc, ub),
+ iterator})
+ .getResult());
+ }
+ }
+
+ SmallVector<OpFoldResult> offsets =
+ getAsOpFoldResult(newForallOp.getInductionVars());
+ SmallVector<OpFoldResult> strides(rank, rewriter.getIndexAttr(1));
+ Value sourceTile = rewriter.create<memref::SubViewOp>(
+ loc, copy.getSource(), offsets, sizes, strides);
+ Value targetTile = rewriter.create<memref::SubViewOp>(
+ loc, copy.getTarget(), offsets, sizes, strides);
+ rewriter.replaceOpWithNewOp<memref::CopyOp>(copy, sourceTile, targetTile);
+}
+
+static SmallVector<OpFoldResult> getCopyTileSizes(Builder &b,
+ memref::CopyOp copy) {
+ int64_t rank = copy.getTarget().getType().getRank();
+ if (rank == 0) {
+ return {};
+ }
+
+ SmallVector<OpFoldResult> tileSizes(rank - 1, b.getIndexAttr(1));
+ int64_t elementBitWidth = llvm::cast<MemRefType>(copy.getTarget().getType())
+ .getElementTypeBitWidth();
+ tileSizes.push_back(b.getIndexAttr(kPreferredCopyNumBits / elementBitWidth));
+ return tileSizes;
+}
+
+} // namespace
+
+namespace {
+struct GPUDistributeCopyUsingForallPass final
+ : impl::GPUDistributeCopyUsingForallPassBase<
+ GPUDistributeCopyUsingForallPass> {
+ void runOnOperation() override {
+ MLIRContext *context = &getContext();
+ auto funcOp = getOperation();
+
+ SmallVector<memref::CopyOp> copies;
+
+ // Walk in PreOrder so that parent operations are visited before children,
+ // thus allowing all operations contained within thread/warp/lane foralls
+ // to be skipped.
+ funcOp.walk<WalkOrder::PreOrder>([&](Operation *op) {
+ if (auto forallOp = dyn_cast<scf::ForallOp>(op)) {
+ // Skip ops contained within forall ops with thread/warp/lane mappings.
+ if (forallOpHasMappingType<IREE::GPU::LaneIdAttr,
+ gpu::GPUWarpMappingAttr,
+ gpu::GPUThreadMappingAttr>(forallOp)) {
+ return WalkResult::skip();
+ }
+ }
+ if (auto copy = dyn_cast<memref::CopyOp>(op)) {
+ copies.push_back(copy);
+ }
+ return WalkResult::advance();
+ });
+
+ IRRewriter rewriter(context);
+ for (auto copy : copies) {
+ rewriter.setInsertionPoint(copy);
+ SmallVector<OpFoldResult> tileSizes = getCopyTileSizes(rewriter, copy);
+ distributeCopyToThreads(rewriter, copy, tileSizes);
+ }
+ }
+};
+} // namespace
+} // namespace mlir::iree_compiler
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
index 7b9f96f..340fa65 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/Passes.td
@@ -31,6 +31,14 @@
let dependentDialects = ["::mlir::scf::SCFDialect"];
}
+def GPUDistributeCopyUsingForallPass :
+ InterfacePass<"iree-codegen-gpu-distribute-copy-using-forall", "mlir::FunctionOpInterface"> {
+ let summary = "Pass to distribute copies to threads.";
+ let dependentDialects = [
+ "::mlir::affine::AffineDialect", "::mlir::gpu::GPUDialect", "::mlir::scf::SCFDialect"
+ ];
+}
+
def GPUDistributeForallPass :
InterfacePass<"iree-codegen-gpu-distribute-forall", "mlir::FunctionOpInterface"> {
let summary = "Pass to distribute scf.forall ops.";
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
index df2e5fe..4775dde 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/BUILD.bazel
@@ -23,6 +23,7 @@
"gpu_check_resource_usage.mlir",
"gpu_create_fast_slow_path.mlir",
"gpu_distribute.mlir",
+ "gpu_distribute_copy_using_forall.mlir",
"gpu_distribute_forall.mlir",
"gpu_distribute_scf_for.mlir",
"gpu_distribute_shared_memory.mlir",
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
index 2fa5ba2..b43d2ef 100644
--- a/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/CMakeLists.txt
@@ -20,6 +20,7 @@
"gpu_combine_value_barriers.mlir"
"gpu_create_fast_slow_path.mlir"
"gpu_distribute.mlir"
+ "gpu_distribute_copy_using_forall.mlir"
"gpu_distribute_forall.mlir"
"gpu_distribute_scf_for.mlir"
"gpu_distribute_shared_memory.mlir"
diff --git a/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_distribute_copy_using_forall.mlir b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_distribute_copy_using_forall.mlir
new file mode 100644
index 0000000..5666f1f
--- /dev/null
+++ b/compiler/src/iree/compiler/Codegen/Common/GPU/test/gpu_distribute_copy_using_forall.mlir
@@ -0,0 +1,99 @@
+// RUN: iree-opt --split-input-file --pass-pipeline='builtin.module(func.func(iree-codegen-gpu-distribute-copy-using-forall))' %s | FileCheck %s
+
+func.func @static_copy(%src : memref<56x32xf32>, %target : memref<56x32xf32>) {
+ memref.copy %src, %target : memref<56x32xf32> to memref<56x32xf32>
+ return
+}
+
+// CHECK-LABEL: func.func @static_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<56x32xf32>, %[[TARGET:.+]]: memref<56x32xf32>)
+
+// CHECK: scf.forall (%[[IV0:[A-Za-z0-9]+]], %[[IV1:[A-Za-z0-9]+]]) = (0, 0) to (56, 32) step (1, 4) {
+// CHECK-DAG: %[[SRC_SUBVIEW:.+]] = memref.subview %[[SRC]][%[[IV0]], %[[IV1]]] [1, 4] [1, 1]
+// CHECK-DAG: %[[TARGET_SUBVIEW:.+]] = memref.subview %[[TARGET]][%[[IV0]], %[[IV1]]] [1, 4] [1, 1]
+// CHECK: memref.copy %[[SRC_SUBVIEW]], %[[TARGET_SUBVIEW]]
+// CHECK: mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+func.func @unaligned_copy(%src : memref<56x31xf32>, %target : memref<56x31xf32>) {
+ memref.copy %src, %target : memref<56x31xf32> to memref<56x31xf32>
+ return
+}
+
+// CHECK: #[[$MAP:.+]] = affine_map<(d0, d1, d2) -> (d0, d1 - d2)>
+// CHECK-LABEL: func.func @unaligned_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<56x31xf32>, %[[TARGET:.+]]: memref<56x31xf32>)
+
+// CHECK: scf.forall (%[[IV0:[A-Za-z0-9]+]], %[[IV1:[A-Za-z0-9]+]]) = (0, 0) to (56, 31) step (1, 4) {
+// CHECK: %[[MIN:.+]] = affine.min #[[$MAP]](%c4, %c31, %[[IV1]])
+// CHECK-DAG: %[[SRC_SUBVIEW:.+]] = memref.subview %[[SRC]][%[[IV0]], %[[IV1]]] [1, %[[MIN]]]
+// CHECK-DAG: %[[TARGET_SUBVIEW:.+]] = memref.subview %[[TARGET]][%[[IV0]], %[[IV1]]] [1, %[[MIN]]]
+// CHECK: memref.copy %[[SRC_SUBVIEW]], %[[TARGET_SUBVIEW]]
+// CHECK: mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+func.func @dynamic_copy(%src : memref<?x?xf32>, %target : memref<?x?xf32>) {
+ memref.copy %src, %target : memref<?x?xf32> to memref<?x?xf32>
+ return
+}
+
+// CHECK: #[[$MAP:.+]] = affine_map<(d0, d1, d2) -> (d0, d1 - d2)>
+// CHECK-LABEL: func.func @dynamic_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<?x?xf32>, %[[TARGET:.+]]: memref<?x?xf32>)
+
+// CHECK-DAG: %[[D0:.+]] = memref.dim %[[SRC]], %c0 : memref<?x?xf32>
+// CHECK-DAG: %[[D1:.+]] = memref.dim %[[SRC]], %c1 : memref<?x?xf32>
+// CHECK: scf.forall (%[[IV0:[A-Za-z0-9]+]], %[[IV1:[A-Za-z0-9]+]]) = (0, 0) to (%[[D0]], %[[D1]]) step (1, 4) {
+// CHECK: %[[MIN:.+]] = affine.min #[[$MAP]](%c4, %[[D1]], %[[IV1]])
+// CHECK-DAG: %[[SRC_SUBVIEW:.+]] = memref.subview %[[SRC]][%[[IV0]], %[[IV1]]] [1, %[[MIN]]]
+// CHECK-DAG: %[[TARGET_SUBVIEW:.+]] = memref.subview %[[TARGET]][%[[IV0]], %[[IV1]]] [1, %[[MIN]]]
+// CHECK: memref.copy %[[SRC_SUBVIEW]], %[[TARGET_SUBVIEW]]
+// CHECK: mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+func.func @f16_copy(%src : memref<56x32xf16>, %target : memref<56x32xf16>) {
+ memref.copy %src, %target : memref<56x32xf16> to memref<56x32xf16>
+ return
+}
+
+// CHECK-LABEL: func.func @f16_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<56x32xf16>, %[[TARGET:.+]]: memref<56x32xf16>)
+
+// CHECK: scf.forall (%[[IV0:[A-Za-z0-9]+]], %[[IV1:[A-Za-z0-9]+]]) = (0, 0) to (56, 32) step (1, 8) {
+// CHECK-DAG: %[[SRC_SUBVIEW:.+]] = memref.subview %[[SRC]][%[[IV0]], %[[IV1]]] [1, 8]
+// CHECK-DAG: %[[TARGET_SUBVIEW:.+]] = memref.subview %[[TARGET]][%[[IV0]], %[[IV1]]] [1, 8]
+// CHECK: memref.copy %[[SRC_SUBVIEW]], %[[TARGET_SUBVIEW]]
+// CHECK: mapping = [#gpu.thread<linear_dim_1>, #gpu.thread<linear_dim_0>]
+
+// -----
+
+func.func @rank_0_copy(%src : memref<f32>, %target : memref<f32>) {
+ memref.copy %src, %target : memref<f32> to memref<f32>
+ return
+}
+
+// CHECK-LABEL: func.func @rank_0_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<f32>, %[[TARGET:.+]]: memref<f32>)
+
+// CHECK: scf.forall (%{{.*}}) in (1) {
+// CHECK: memref.copy %[[SRC]], %[[TARGET]]
+// CHECK: mapping = [#gpu.thread<linear_dim_0>]
+
+// -----
+
+func.func @already_distributed_copy(%src : memref<56x32xf32>, %target : memref<56x32xf32>) {
+ scf.forall (%arg2) in (1) {
+ memref.copy %src, %target : memref<56x32xf32> to memref<56x32xf32>
+ } {mapping = [#gpu.thread<linear_dim_0>]}
+ return
+}
+
+// CHECK-LABEL: func.func @already_distributed_copy
+// CHECK-SAME: (%[[SRC:.+]]: memref<56x32xf32>, %[[TARGET:.+]]: memref<56x32xf32>)
+
+// CHECK: scf.forall (%{{.*}}) in (1) {
+// CHECK: memref.copy %[[SRC]], %[[TARGET]]
+// CHECK: mapping = [#gpu.thread<linear_dim_0>]
diff --git a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
index c1ea822..8e112af 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMGPU/Passes.cpp
@@ -452,6 +452,7 @@
addGPUBufferizePasses(funcPassManager);
// Step 8. Resolve remaining parallel loops.
+ funcPassManager.addPass(createGPUDistributeCopyUsingForallPass());
funcPassManager.addPass(iree_compiler::createNormalizeLoopBoundsPass(
NormalizeLoopBoundsPassOptions{/*normalizeFor=*/false,
/*normalizeForall=*/true}));