[Mlir-commits] [mlir] [mlir][mem2reg] Promote whole-buffer memref to a vector SSA value (PR #211880)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Fri Jul 24 11:52:47 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir
Author: Jianhui Li (Jianhui-Li)
<details>
<summary>Changes</summary>
When a vectorized scf.for carries a reduction or accumulation tensor — the C accumulator of a contraction, or a reduction buffer — bufferization materializes that loop-carried tensor into a scratch memref, and promote-buffers-to-stack turns it into a small memref.alloca. The buffer's only accesses are a whole-buffer vector.transfer_write that stores the accumulator and a whole-buffer vector.transfer_read that reads it back, both at index 0, in-bounds, with an identity permutation map and a vector type that exactly matches the memref shape. Such a buffer is effectively an SSA vector spilled to the stack, and it blocks downstream vectorization and register allocation. MLIR's Mem2Reg can already promote single-use memory into SSA values, but today only for scalar slots — memref::AllocaOp::getPromotableSlots offers a slot only for a single-element memref, and memref.load/memref.store are the only load/store ops wired into the pass — so these whole-buffer allocas are left untouched.
This PR teaches Mem2Reg to promote such a buffer into a single vector SSA value. The machinery is already type-agnostic — MemorySlot carries an arbitrary elemType, and scf.for already threads a slot of any type as an iter_arg/result — so no changes to the pass or to SCF are needed. memref::AllocaOp::getPromotableSlots now also offers a vector-typed slot for a static, multi-element memref, and vector.transfer_read/transfer_write implement PromotableMemOpInterface as external models, gated by an isWholeBufferTransfer predicate (single blocking use on the slot pointer, exact type match, all-zero indices, identity map, in-bounds, no mask). Any access that is not a whole-buffer transfer fails its own canUsesBeRemoved check and aborts promotion of that slot with zero IR mutation. A new lit test covers straight-line and scf.for-threaded promotion, the 2-D case, and six negative cases.
assisted-by-claude
---
Full diff: https://github.com/llvm/llvm-project/pull/211880.diff
6 Files Affected:
- (added) mlir/include/mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h (+21)
- (modified) mlir/lib/Dialect/MemRef/IR/MemRefMemorySlot.cpp (+12-4)
- (modified) mlir/lib/Dialect/Vector/Transforms/CMakeLists.txt (+2)
- (added) mlir/lib/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.cpp (+159)
- (modified) mlir/lib/RegisterAllDialects.cpp (+2)
- (added) mlir/test/Dialect/Vector/mem2reg.mlir (+172)
``````````diff
diff --git a/mlir/include/mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h b/mlir/include/mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h
new file mode 100644
index 0000000000000..4c2e386728419
--- /dev/null
+++ b/mlir/include/mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h
@@ -0,0 +1,21 @@
+//===- MemorySlotOpInterfaceImpl.h - Mem2Reg for vector ops -----*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef MLIR_DIALECT_VECTOR_TRANSFORMS_MEMORYSLOTOPINTERFACEIMPL_H
+#define MLIR_DIALECT_VECTOR_TRANSFORMS_MEMORYSLOTOPINTERFACEIMPL_H
+
+namespace mlir {
+
+class DialectRegistry;
+
+namespace vector {
+void registerMemorySlotOpInterfaceExternalModels(DialectRegistry ®istry);
+} // namespace vector
+} // namespace mlir
+
+#endif // MLIR_DIALECT_VECTOR_TRANSFORMS_MEMORYSLOTOPINTERFACEIMPL_H
diff --git a/mlir/lib/Dialect/MemRef/IR/MemRefMemorySlot.cpp b/mlir/lib/Dialect/MemRef/IR/MemRefMemorySlot.cpp
index 6748e2cf71804..17b1417550524 100644
--- a/mlir/lib/Dialect/MemRef/IR/MemRefMemorySlot.cpp
+++ b/mlir/lib/Dialect/MemRef/IR/MemRefMemorySlot.cpp
@@ -66,11 +66,19 @@ SmallVector<MemorySlot> memref::AllocaOp::getPromotableSlots() {
MemRefType type = getType();
if (!type.hasStaticShape())
return {};
- // Make sure the memref contains only a single element.
- if (type.getNumElements() != 1)
- return {};
- return {MemorySlot{getResult(), type.getElementType()}};
+ // A single-element memref is promoted to a scalar SSA value.
+ if (type.getNumElements() == 1)
+ return {MemorySlot{getResult(), type.getElementType()}};
+
+ // A multi-element memref can be promoted to a single vector SSA value when it
+ // is only ever accessed as a whole buffer (e.g. through whole-buffer
+ // `vector.transfer_read`/`vector.transfer_write`).
+ if (VectorType::isValidElementType(type.getElementType()))
+ return {MemorySlot{
+ getResult(), VectorType::get(type.getShape(), type.getElementType())}};
+
+ return {};
}
Value memref::AllocaOp::getDefaultValue(const MemorySlot &slot,
diff --git a/mlir/lib/Dialect/Vector/Transforms/CMakeLists.txt b/mlir/lib/Dialect/Vector/Transforms/CMakeLists.txt
index 112a1db6fe93b..dfe873f1a1b8d 100644
--- a/mlir/lib/Dialect/Vector/Transforms/CMakeLists.txt
+++ b/mlir/lib/Dialect/Vector/Transforms/CMakeLists.txt
@@ -15,6 +15,7 @@ add_mlir_dialect_library(MLIRVectorTransforms
LowerVectorToFromElementsToShuffleTree.cpp
LowerVectorTransfer.cpp
LowerVectorTranspose.cpp
+ MemorySlotOpInterfaceImpl.cpp
SubsetOpInterfaceImpl.cpp
VectorDistribute.cpp
VectorDropLeadUnitDim.cpp
@@ -44,6 +45,7 @@ add_mlir_dialect_library(MLIRVectorTransforms
MLIRGPUUtils
MLIRIR
MLIRLinalgDialect
+ MLIRMemorySlotInterfaces
MLIRMemRefDialect
MLIRMemRefTransforms
MLIRMemRefUtils
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.cpp
new file mode 100644
index 0000000000000..352983ba153fa
--- /dev/null
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.cpp
@@ -0,0 +1,159 @@
+//===- MemorySlotOpInterfaceImpl.cpp - Mem2Reg for vector ops -------------===//
+//
+// 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 Mem2Reg-related interfaces for Vector dialect
+// operations. It allows a memref that is only ever accessed as a whole buffer
+// through `vector.transfer_read`/`vector.transfer_write` to be promoted into a
+// single vector SSA value.
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h"
+
+#include "mlir/Dialect/Utils/StaticValueUtils.h"
+#include "mlir/Dialect/Vector/IR/VectorOps.h"
+#include "mlir/IR/BuiltinTypes.h"
+#include "mlir/Interfaces/MemorySlotInterfaces.h"
+
+using namespace mlir;
+using namespace mlir::vector;
+
+//===----------------------------------------------------------------------===//
+// Utilities
+//===----------------------------------------------------------------------===//
+
+/// Returns whether `xferOp` accesses exactly the whole contents of `slot`, so
+/// it can act as a plain whole-buffer load/store during Mem2Reg.
+template <typename TransferOpTy>
+static bool
+isWholeBufferTransfer(TransferOpTy xferOp, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses) {
+ // The sole blocking use must be the slot pointer as the transfer's base.
+ if (blockingUses.size() != 1)
+ return false;
+ Value blockingUse = (*blockingUses.begin())->get();
+ if (blockingUse != slot.ptr || xferOp.getBase() != slot.ptr)
+ return false;
+
+ // Reject the tensor form (already implied, since slot pointers are memrefs).
+ if (!isa<MemRefType>(xferOp.getBase().getType()))
+ return false;
+
+ // Exact type match pins rank/extents/element type and rejects scalable
+ // vectors.
+ if (xferOp.getVectorType() != slot.elemType)
+ return false;
+
+ // Access must start at the buffer origin in every dimension.
+ for (Value index : xferOp.getIndices()) {
+ std::optional<int64_t> constIndex = getConstantIntValue(index);
+ if (!constIndex || *constIndex != 0)
+ return false;
+ }
+
+ // Identity map: no broadcast or transpose.
+ if (!xferOp.getPermutationMap().isIdentity())
+ return false;
+
+ // All dimensions in bounds: no out-of-buffer element, no padding.
+ if (xferOp.hasOutOfBoundsDim())
+ return false;
+
+ // A mask would make the access partial.
+ if (xferOp.getMask())
+ return false;
+
+ return true;
+}
+
+//===----------------------------------------------------------------------===//
+// Interface models
+//===----------------------------------------------------------------------===//
+
+namespace {
+
+struct TransferReadOpMemOpModel
+ : public PromotableMemOpInterface::ExternalModel<TransferReadOpMemOpModel,
+ vector::TransferReadOp> {
+ bool loadsFrom(Operation *op, const MemorySlot &slot) const {
+ return cast<vector::TransferReadOp>(op).getBase() == slot.ptr;
+ }
+
+ bool storesTo(Operation *op, const MemorySlot &slot) const { return false; }
+
+ Value getStored(Operation *op, const MemorySlot &slot, OpBuilder &builder,
+ Value reachingDef, const DataLayout &dataLayout) const {
+ llvm_unreachable("getStored should not be called on TransferReadOp");
+ }
+
+ bool canUsesBeRemoved(Operation *op, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses,
+ SmallVectorImpl<OpOperand *> &newBlockingUses,
+ const DataLayout &dataLayout) const {
+ return isWholeBufferTransfer(cast<vector::TransferReadOp>(op), slot,
+ blockingUses);
+ }
+
+ DeletionKind
+ removeBlockingUses(Operation *op, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses,
+ OpBuilder &builder, Value reachingDefinition,
+ const DataLayout &dataLayout) const {
+ // Whole-buffer read: replace the loaded vector with the reaching
+ // definition.
+ cast<vector::TransferReadOp>(op).getVector().replaceAllUsesWith(
+ reachingDefinition);
+ return DeletionKind::Delete;
+ }
+};
+
+struct TransferWriteOpMemOpModel
+ : public PromotableMemOpInterface::ExternalModel<TransferWriteOpMemOpModel,
+ vector::TransferWriteOp> {
+ bool loadsFrom(Operation *op, const MemorySlot &slot) const { return false; }
+
+ bool storesTo(Operation *op, const MemorySlot &slot) const {
+ return cast<vector::TransferWriteOp>(op).getBase() == slot.ptr;
+ }
+
+ Value getStored(Operation *op, const MemorySlot &slot, OpBuilder &builder,
+ Value reachingDef, const DataLayout &dataLayout) const {
+ return cast<vector::TransferWriteOp>(op).getValueToStore();
+ }
+
+ bool canUsesBeRemoved(Operation *op, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses,
+ SmallVectorImpl<OpOperand *> &newBlockingUses,
+ const DataLayout &dataLayout) const {
+ // No self-store guard needed: a vector value can never equal a memref slot.
+ return isWholeBufferTransfer(cast<vector::TransferWriteOp>(op), slot,
+ blockingUses);
+ }
+
+ DeletionKind
+ removeBlockingUses(Operation *op, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses,
+ OpBuilder &builder, Value reachingDefinition,
+ const DataLayout &dataLayout) const {
+ return DeletionKind::Delete;
+ }
+};
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Register external models
+//===----------------------------------------------------------------------===//
+
+void mlir::vector::registerMemorySlotOpInterfaceExternalModels(
+ DialectRegistry ®istry) {
+ registry.addExtension(+[](MLIRContext *ctx, vector::VectorDialect *dialect) {
+ TransferReadOp::attachInterface<TransferReadOpMemOpModel>(*ctx);
+ TransferWriteOp::attachInterface<TransferWriteOpMemOpModel>(*ctx);
+ });
+}
diff --git a/mlir/lib/RegisterAllDialects.cpp b/mlir/lib/RegisterAllDialects.cpp
index 974b5f533860a..62304ae88b43c 100644
--- a/mlir/lib/RegisterAllDialects.cpp
+++ b/mlir/lib/RegisterAllDialects.cpp
@@ -97,6 +97,7 @@
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/Dialect/Vector/Transforms/BufferizableOpInterfaceImpl.h"
#include "mlir/Dialect/Vector/Transforms/IndexedAccessOpInterfaceImpl.h"
+#include "mlir/Dialect/Vector/Transforms/MemorySlotOpInterfaceImpl.h"
#include "mlir/Dialect/Vector/Transforms/SubsetOpInterfaceImpl.h"
#include "mlir/Dialect/WasmSSA/IR/WasmSSA.h"
#include "mlir/Dialect/X86/X86Dialect.h"
@@ -200,6 +201,7 @@ void mlir::registerAllDialects(DialectRegistry ®istry) {
tosa::registerShardingInterfaceExternalModels(registry);
vector::registerBufferizableOpInterfaceExternalModels(registry);
vector::registerIndexedAccessOpInterfaceExternalModels(registry);
+ vector::registerMemorySlotOpInterfaceExternalModels(registry);
vector::registerSubsetOpInterfaceExternalModels(registry);
vector::registerValueBoundsOpInterfaceExternalModels(registry);
NVVM::registerNVVMTargetInterfaceExternalModels(registry);
diff --git a/mlir/test/Dialect/Vector/mem2reg.mlir b/mlir/test/Dialect/Vector/mem2reg.mlir
new file mode 100644
index 0000000000000..ec6e19d0a5c19
--- /dev/null
+++ b/mlir/test/Dialect/Vector/mem2reg.mlir
@@ -0,0 +1,172 @@
+// RUN: mlir-opt %s --pass-pipeline='builtin.module(func.func(mem2reg))' --split-input-file | FileCheck %s
+
+// A memref that is only ever accessed as a whole buffer through
+// vector.transfer_read / vector.transfer_write is promoted to a single vector
+// SSA value.
+
+// CHECK-LABEL: func.func @whole_buffer_write_read
+// CHECK-SAME: (%[[PAD:.*]]: f32)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[CST:.*]] = arith.constant dense<1.000000e+00> : vector<4xf32>
+// CHECK: return %[[CST]] : vector<4xf32>
+func.func @whole_buffer_write_read(%pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A whole-buffer slot carried across scf.for is threaded as an iter_arg/result.
+
+// CHECK-LABEL: func.func @whole_buffer_in_loop
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[RES:.*]] = scf.for {{.*}} iter_args(%[[IT:.*]] = %{{.*}}) -> (vector<4xf32>)
+// CHECK: %[[NEXT:.*]] = arith.addf %[[IT]], %[[IT]] : vector<4xf32>
+// CHECK: scf.yield %[[NEXT]] : vector<4xf32>
+// CHECK: return %[[RES]] : vector<4xf32>
+func.func @whole_buffer_in_loop(%pad: f32, %lb: index, %ub: index, %step: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ scf.for %i = %lb to %ub step %step {
+ %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ %n = arith.addf %v, %v : vector<4xf32>
+ vector.transfer_write %n, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ }
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A multi-dimensional whole-buffer memref is promoted to a matching vector.
+
+// CHECK-LABEL: func.func @whole_buffer_2d
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: return %{{.*}} : vector<2x4xf32>
+func.func @whole_buffer_2d(%pad: f32) -> vector<2x4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<2x4xf32>
+ %a = memref.alloca() : memref<2x4xf32>
+ vector.transfer_write %cst, %a[%c0, %c0] {in_bounds = [true, true]} : vector<2x4xf32>, memref<2x4xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {in_bounds = [true, true]} : memref<2x4xf32>, vector<2x4xf32>
+ return %r : vector<2x4xf32>
+}
+
+// -----
+
+// Non-zero access offset: the transfer does not cover the whole buffer, so the
+// slot must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_nonzero_index
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_nonzero_index(%pad: f32) -> vector<4xf32> {
+ %c1 = arith.constant 1 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %cst, %a[%c1] {in_bounds = [true]} : vector<4xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c1], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A masked transfer only touches part of the buffer: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_masked
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_masked(%pad: f32, %m: vector<4xi1>) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0], %m {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A partial (out-of-bounds) transfer must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_out_of_bounds
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_out_of_bounds(%pad: f32) -> vector<8xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<8xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] : vector<8xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad : memref<4xf32>, vector<8xf32>
+ return %r : vector<8xf32>
+}
+
+// -----
+
+// A non-identity (transposing) permutation map is not a whole-buffer identity
+// access: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_transpose_map
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_transpose_map(%pad: f32) -> vector<4x2xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<2x4xf32>
+ %a = memref.alloca() : memref<2x4xf32>
+ vector.transfer_write %cst, %a[%c0, %c0] {in_bounds = [true, true]} : vector<2x4xf32>, memref<2x4xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {in_bounds = [true, true], permutation_map = affine_map<(d0, d1) -> (d1, d0)>} : memref<2x4xf32>, vector<4x2xf32>
+ return %r : vector<4x2xf32>
+}
+
+// -----
+
+// An alloca also accessed through a scalar memref.load cannot be promoted to a
+// vector: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_mixed_scalar_access
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+// CHECK: memref.load
+func.func @negative_mixed_scalar_access(%pad: f32) -> (vector<4xf32>, f32) {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ %s = memref.load %a[%c0] : memref<4xf32>
+ return %r, %s : vector<4xf32>, f32
+}
+
+// -----
+
+// A scalable vector never equals the fixed-size slot type: must NOT be
+// promoted.
+
+// CHECK-LABEL: func.func @negative_scalable
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_scalable(%pad: f32) -> vector<[4]xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<[4]xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<[4]xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/211880
More information about the Mlir-commits
mailing list