[Mlir-commits] [mlir] Added FoldSelfCopy pattern to gpu memcpy (PR #221216)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Fri Sep 4 05:51:44 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir
Author: Edward-EJ
<details>
<summary>Changes</summary>
Hi,
This is my first attempt at a PR for MLIR so I've decided to try implement something simple that I encountered while working on lowering my dialect to GPU targets, namely a canonicalization pattern for the gpu.memcpy operation. The purpose of the pattern is to fold self copies, i.e
```
gpu.memcpy(%a, %a) : memref<2xf16>, memref<2xf16>
```
While I don't think IR like the above is going to be generated often, I also think it can be canonicalized away at this stage. Currently lowering the following IR:
```
func.func @<!-- -->main(%arg0: memref<100xf16, 1>)
{
%1 = gpu.wait async
%2 = gpu.memcpy async [%1] %arg0, %arg0: memref<100xf16, 1>, memref<100xf16, 1>
gpu.wait[%2]
return
}
```
using "--canonicalize --gpu-to-llvm --reconcile-unrealized-casts --canonicalize" yields:
```
llvm.func @<!-- -->main(%arg0: !llvm.ptr<1>, %arg1: !llvm.ptr<1>, %arg2: i64, %arg3: i64, %arg4: i64) {
%0 = llvm.mlir.zero : !llvm.ptr<1>
%1 = llvm.call @<!-- -->mgpuStreamCreate() : () -> !llvm.ptr
%2 = llvm.getelementptr %0[100] : (!llvm.ptr<1>) -> !llvm.ptr<1>, f16
%3 = llvm.ptrtoint %2 : !llvm.ptr<1> to i64
%4 = llvm.addrspacecast %arg1 : !llvm.ptr<1> to !llvm.ptr
%5 = llvm.addrspacecast %arg1 : !llvm.ptr<1> to !llvm.ptr
llvm.call @<!-- -->mgpuMemcpy(%5, %4, %3, %1) : (!llvm.ptr, !llvm.ptr, i64, !llvm.ptr) -> ()
llvm.call @<!-- -->mgpuStreamSynchronize(%1) : (!llvm.ptr) -> ()
llvm.call @<!-- -->mgpuStreamDestroy(%1) : (!llvm.ptr) -> ()
llvm.return
}
llvm.func @<!-- -->mgpuStreamCreate() -> !llvm.ptr
llvm.func @<!-- -->mgpuMemcpy(!llvm.ptr, !llvm.ptr, i64, !llvm.ptr)
llvm.func @<!-- -->mgpuStreamSynchronize(!llvm.ptr)
llvm.func @<!-- -->mgpuStreamDestroy(!llvm.ptr)
}
```
but I think this should just be a no-op.
The only complexity with this pattern is how to deal with the asynchronous case when the ```memcpy``` operation produces an asyncToken. In the case where the ```memcpy``` has just one async dependency, the pattern will simply replace the users of the result of the ```memcpy``` with its async dependency. When there are several async dependencies, the ```memcpy``` can instead be replaced by a ```wait``` which takes in the ```memcpy```'s dependencies.
I look forward to hearing your feedback and thank you in advance for your time.
--Edward
---
Full diff: https://github.com/llvm/llvm-project/pull/221216.diff
2 Files Affected:
- (modified) mlir/lib/Dialect/GPU/IR/GPUDialect.cpp (+35-6)
- (modified) mlir/test/Dialect/GPU/canonicalize.mlir (+55)
``````````diff
diff --git a/mlir/lib/Dialect/GPU/IR/GPUDialect.cpp b/mlir/lib/Dialect/GPU/IR/GPUDialect.cpp
index a39b43c9803eb..4792cad60e715 100644
--- a/mlir/lib/Dialect/GPU/IR/GPUDialect.cpp
+++ b/mlir/lib/Dialect/GPU/IR/GPUDialect.cpp
@@ -2089,6 +2089,35 @@ LogicalResult MemcpyOp::verify() {
namespace {
+/// Fold gpu.memcpy(%x, %x).
+struct FoldSelfCopy : public OpRewritePattern<MemcpyOp> {
+ using OpRewritePattern<MemcpyOp>::OpRewritePattern;
+
+ LogicalResult matchAndRewrite(MemcpyOp memcpyOp,
+ PatternRewriter &rewriter) const override {
+ if (memcpyOp.getSrc() != memcpyOp.getDst())
+ return failure();
+
+ // Synchronous self-copy is a no-op.
+ if (!memcpyOp.getAsyncToken()) {
+ rewriter.eraseOp(memcpyOp);
+ return success();
+ }
+
+ ValueRange asyncDependencies = memcpyOp.getAsyncDependencies();
+
+ // If only 1 dependency, replace `memcpyOp` with the dependency.
+ if (asyncDependencies.size() == 1)
+ rewriter.replaceOp(memcpyOp, asyncDependencies.front());
+
+ // If several dependencies, replace `memcpyOp` with a `gpu.wait`.
+ rewriter.replaceOpWithNewOp<WaitOp>(
+ memcpyOp, memcpyOp.getAsyncToken().getType(), asyncDependencies);
+
+ return success();
+ }
+};
+
/// Erases a common case of copy ops where a destination value is used only by
/// the copy op, alloc and dealloc ops.
struct EraseTrivialCopyOp : public OpRewritePattern<MemcpyOp> {
@@ -2126,7 +2155,12 @@ struct EraseTrivialCopyOp : public OpRewritePattern<MemcpyOp> {
void MemcpyOp::getCanonicalizationPatterns(RewritePatternSet &results,
MLIRContext *context) {
- results.add<EraseTrivialCopyOp>(context);
+ results.add<EraseTrivialCopyOp, FoldSelfCopy>(context);
+}
+
+LogicalResult MemcpyOp::fold(FoldAdaptor adaptor,
+ SmallVectorImpl<::mlir::OpFoldResult> &results) {
+ return memref::foldMemRefCast(*this);
}
//===----------------------------------------------------------------------===//
@@ -2198,11 +2232,6 @@ LogicalResult SubgroupMmaComputeOp::verify() {
return success();
}
-LogicalResult MemcpyOp::fold(FoldAdaptor adaptor,
- SmallVectorImpl<::mlir::OpFoldResult> &results) {
- return memref::foldMemRefCast(*this);
-}
-
LogicalResult MemsetOp::fold(FoldAdaptor adaptor,
SmallVectorImpl<::mlir::OpFoldResult> &results) {
return memref::foldMemRefCast(*this);
diff --git a/mlir/test/Dialect/GPU/canonicalize.mlir b/mlir/test/Dialect/GPU/canonicalize.mlir
index 8f2803b267490..e3811f3b87dee 100644
--- a/mlir/test/Dialect/GPU/canonicalize.mlir
+++ b/mlir/test/Dialect/GPU/canonicalize.mlir
@@ -157,6 +157,61 @@ func.func @fold_memcpy_op(%arg0: i1) {
// -----
+// CHECK-LABEL: func @synhcronous_self_memcpy
+func.func @synhcronous_self_memcpy(%arg0: memref<2xf16>)
+{
+ gpu.memcpy %arg0, %arg0 : memref<2xf16>, memref<2xf16>
+ return
+}
+
+// CHECK-NOT: gpu.memcpy
+
+// -----
+
+// CHECK-LABEL: func @asynchronous_self_memcpy1
+func.func @asynchronous_self_memcpy1(%arg0: memref<2xf16>) {
+ %1 = gpu.wait async
+ %memref, %asyncToken = gpu.alloc async [%1] () : memref<2xf16>
+ gpu.wait [%1]
+ %2 = gpu.wait async
+ %3 = gpu.memcpy async [%2] %memref, %memref : memref<2xf16>, memref<2xf16>
+ gpu.wait [%2]
+ return
+}
+
+// CHECK-NOT: gpu.memcpy
+
+// -----
+
+// CHECK-LABEL: func @asynchronous_self_memcpy2(
+// CHECK-SAME: %[[ARG0:.*]]: memref<2xf16>
+func.func @asynchronous_self_memcpy2(%arg0: memref<2xf16>) {
+ // CHECK: %[[T0:.*]] = gpu.wait async
+ %1 = gpu.wait async
+
+ // CHECK: %{{.*}}, %[[T1:.*]] = gpu.alloc async [%[[T0]]] () : memref<2xf16>
+ // CHECK: %{{.*}}, %[[T2:.*]] = gpu.alloc async [%[[T0]]] () : memref<2xf16>
+ %memref, %asyncToken = gpu.alloc async [%1] () : memref<2xf16>
+ %memref1, %asyncToken1 = gpu.alloc async [%1] () : memref<2xf16>
+
+ // CHECK-NOT: gpu.memcpy {{.*}} %[[ARG0]], %[[ARG0]]
+ // CHECK: %[[T3:.*]] = gpu.wait async [%[[T1]], %[[T2]]]
+ %3 = gpu.memcpy async [%asyncToken, %asyncToken1] %arg0, %arg0 : memref<2xf16>, memref<2xf16>
+
+ // CHECK: gpu.wait [%[[T3]]]
+ gpu.wait [%3]
+
+ // CHECK: gpu.memcpy async [%[[T3]]] %[[ARG0]], %{{.*}}
+ // CHECK: gpu.memcpy async [%[[T3]]] %[[ARG0]], %{{.*}}
+ %4 = gpu.memcpy async [%3] %arg0, %memref : memref<2xf16>, memref<2xf16>
+ %5 = gpu.memcpy async [%3] %arg0, %memref1 : memref<2xf16>, memref<2xf16>
+
+ gpu.wait [%4, %5]
+ return
+}
+
+// -----
+
// We cannot fold memcpy here as dest is a block argument.
// CHECK-LABEL: func @do_not_fold_memcpy_op1
func.func @do_not_fold_memcpy_op1(%arg0: i1, %arg1: memref<2xf16>) {
``````````
</details>
https://github.com/llvm/llvm-project/pull/221216
More information about the Mlir-commits
mailing list