[Mlir-commits] [mlir] [mlir][bufferization] Add best-fit algorithm to static memory planner (PR #207403)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Fri Jul 3 07:31:05 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir-bufferization
Author: Javed Absar (javedabsar1)
<details>
<summary>Changes</summary>
Introduces an algorithm selection option to the static memory planner pass and
adds a best-fit algorithm that reuses memory from expired allocations by finding
the smallest suitable gap.
The planning algorithms are factored into a separate StaticMemoryPlanning.{h,cpp}
to keep them independent of MLIR IR and easily testable.
---
Patch is 22.62 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/207403.diff
7 Files Affected:
- (modified) mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td (+8)
- (modified) mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td (+21-10)
- (added) mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h (+50)
- (modified) mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt (+1)
- (modified) mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp (+35-62)
- (added) mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp (+111)
- (added) mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir (+118)
``````````diff
diff --git a/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td b/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
index bafa84645e57b..a8cad26e9341b 100644
--- a/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
+++ b/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
@@ -24,4 +24,12 @@ def LayoutMapOption : I32EnumAttr<"LayoutMapOption",
let cppNamespace = "::mlir::bufferization";
}
+def MemoryPlannerAlgorithm : I32EnumAttr<"MemoryPlannerAlgorithm",
+ "memory planning algorithm", [
+ I32EnumAttrCase<"Trivial", 0, "trivial">,
+ I32EnumAttrCase<"BestFit", 1, "best-fit">
+]> {
+ let cppNamespace = "::mlir::bufferization";
+}
+
#endif // BUFFERIZATION_ENUMS
diff --git a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
index c7f6cae571e40..8408315dda607 100644
--- a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
+++ b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
@@ -197,13 +197,11 @@ def StaticMemoryPlannerAnalysisPass
- Unique same-block `memref.dealloc`.
- Allocations in nested blocks are ignored for now.
- Eligible allocations are packed into a single arena. Currently, we use a
- trivial sequential allocation strategy with alignment padding. But the
- interface will also allow any packing algorithm to be plugged in as long as
- it respects the interface. The arena is an i8 byte buffer
- (`memref<Nxi8>`) that can hold allocations of different element types.
- Each original allocation is replaced with a `memref.view` operation that
- creates a typed view into the arena at the computed offset.
+ Eligible allocations are packed into a single arena using a configurable
+ planning algorithm (see the `algorithm` option). The arena is an i8 byte
+ buffer (`memref<Nxi8>`) that can hold allocations of different element
+ types. Each original allocation is replaced with a `memref.view` operation
+ that creates a typed view into the arena at the computed offset.
Ineligible allocations are skipped and retain their original
alloc/dealloc operations. Skip reasons are reported via op remarks.
@@ -234,11 +232,24 @@ def StaticMemoryPlannerAnalysisPass
```
}];
- let options = [Option<
- "arenaMode", "arena-mode", "std::string",
+ let options = [
+ Option<"arenaMode", "arena-mode", "std::string",
/*default=*/"\"allocate\"",
"Arena allocation mode: 'allocate' creates arena via AllocOp, "
- "'arg' extracts arena from function arguments">];
+ "'arg' extracts arena from function arguments">,
+ Option<"algorithm", "algorithm",
+ "::mlir::bufferization::MemoryPlannerAlgorithm",
+ /*default=*/"::mlir::bufferization::MemoryPlannerAlgorithm::Trivial",
+ "Memory planning algorithm to use.",
+ [{::llvm::cl::values(
+ clEnumValN(::mlir::bufferization::MemoryPlannerAlgorithm::Trivial,
+ "trivial",
+ "Sequential packing without lifetime overlap"),
+ clEnumValN(::mlir::bufferization::MemoryPlannerAlgorithm::BestFit,
+ "best-fit",
+ "Best-fit packing with lifetime-aware gap reuse")
+ )}]>,
+ ];
let statistics =
[Statistic<"numEligible", "num-eligible",
diff --git a/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h b/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h
new file mode 100644
index 0000000000000..626e384ce4642
--- /dev/null
+++ b/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h
@@ -0,0 +1,50 @@
+//===- StaticMemoryPlanning.h - Memory planning algorithms ------*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Pure memory planning algorithms for static arena allocation. These operate
+// on abstract allocation descriptors (size, alignment, lifetime) and produce
+// byte offsets. They are independent of MLIR IR.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
+#define MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallVector.h"
+#include <cstdint>
+
+namespace mlir {
+namespace bufferization {
+
+/// Descriptor for a single allocation to be placed by the memory planner.
+struct MemoryPlannerAlloc {
+ int64_t sizeInBytes = 0;
+ int64_t alignment = 1;
+ int64_t timeStart = 0; // Operation index when allocation becomes live
+ int64_t timeEnd = 0; // Operation index when allocation is freed
+};
+
+/// Sequential packing without lifetime overlap. Each allocation is placed
+/// immediately after the previous one (with alignment padding). Ignores
+/// lifetimes entirely.
+llvm::SmallVector<int64_t>
+trivialMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs);
+
+/// Best-fit packing with lifetime-aware gap reuse. Processes allocations in
+/// time order and places each one in the smallest gap left by expired
+/// allocations. Falls back to extending the arena if no gap fits.
+llvm::SmallVector<int64_t>
+bestFitMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs);
+
+} // namespace bufferization
+} // namespace mlir
+
+#endif // MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
index 5df9f19a5e30a..006fcd1ce0ec7 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
+++ b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
@@ -15,6 +15,7 @@ add_mlir_dialect_library(MLIRBufferizationTransforms
OwnershipBasedBufferDeallocation.cpp
TensorCopyInsertion.cpp
OptimizeAllocationLiveness.cpp
+ StaticMemoryPlanning.cpp
StaticMemoryPlannerAnalysis.cpp
ADDITIONAL_HEADER_DIRS
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
index 48b6e97c809ef..c695230870bde 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
+++ b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
@@ -7,18 +7,18 @@
//===----------------------------------------------------------------------===//
//
// Transforms memref.alloc/memref.dealloc pairs into a single arena allocation
-// with memref.view. Uses simple sequential offset assignment where each
-// allocation gets its own space without overlap (baseline algorithm).
+// with memref.view. Delegates offset computation to planning algorithms in
+// StaticMemoryPlanning.h.
//
//===----------------------------------------------------------------------===//
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Bufferization/Transforms/Passes.h"
+#include "mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/IR/Builders.h"
#include "mlir/Interfaces/FunctionInterfaces.h"
#include "llvm/Support/Debug.h"
-#include "llvm/Support/MathExtras.h"
#include <numeric>
#define DEBUG_TYPE "static-memory-planner"
@@ -34,19 +34,6 @@ using namespace mlir;
namespace {
-//===----------------------------------------------------------------------===//
-// Data structures
-//===----------------------------------------------------------------------===//
-
-/// Allocation info for memory planning (independent of MLIR).
-/// This can be used with pure planning algorithms.
-struct Alloc {
- int64_t sizeInBytes = 0; // Size in bytes
- int64_t alignment = 1; // Required alignment in bytes
- int64_t timeStart = 0; // Operation index when allocation starts
- int64_t timeEnd = 0; // Operation index when allocation ends (dealloc)
-};
-
/// A candidate allocation with its matching deallocation and assigned offset.
struct AllocationCandidate {
memref::AllocOp alloc;
@@ -81,38 +68,6 @@ static int64_t computeSizeInBytes(MemRefType memrefType) {
return (numElements * elementSizeInBits + 7) / 8; // Round up to bytes
}
-/// Align an offset to the specified alignment.
-/// Returns the smallest value >= offset that is a multiple of alignment.
-static int64_t alignOffset(int64_t offset, int64_t alignment) {
- return llvm::alignTo(offset, alignment);
-}
-
-//===----------------------------------------------------------------------===//
-// Memory Planning Algorithms
-//===----------------------------------------------------------------------===//
-
-/// Simple sequential memory planner (baseline algorithm).
-/// arenaAlignment must be a multiple (LCM) of all alloc.alignment values.
-/// Allocates each buffer one after another with proper alignment padding.
-/// Returns offsets in bytes for each allocation.
-static SmallVector<int64_t> trivialMemoryPlanner(int64_t arenaAlignment,
- ArrayRef<Alloc> allocs) {
- SmallVector<int64_t> offsets;
- int64_t currentOffset = 0;
-
- for (const auto &alloc : allocs) {
- currentOffset = alignOffset(currentOffset, alloc.alignment);
-#ifndef NDEBUG
- assert((arenaAlignment + currentOffset) % alloc.alignment == 0 &&
- "invalid alignment");
-#endif
- offsets.push_back(currentOffset);
- currentOffset += alloc.sizeInBytes;
- }
-
- return offsets;
-}
-
//===----------------------------------------------------------------------===//
// StaticMemoryPlannerAnalysisPass
//===----------------------------------------------------------------------===//
@@ -137,7 +92,7 @@ struct StaticMemoryPlannerAnalysisPass
}
}
- // Step 1: Collect eligible allocation candidates
+ // Step 1: Collect eligible allocation candidates.
SmallVector<AllocationCandidate> candidates;
funcOp->walk([&](memref::AllocOp allocOp) {
@@ -174,22 +129,40 @@ struct StaticMemoryPlannerAnalysisPass
if (candidates.empty())
return;
- // Step 2: Prepare allocation info for planner
- SmallVector<Alloc> allocInfos;
+ // Step 2: Build allocation descriptors with lifetime info.
+ SmallVector<bufferization::MemoryPlannerAlloc> allocInfos;
int64_t arenaAlignment = 1;
- for (const auto &candidate : candidates) {
- Alloc allocInfo;
- allocInfo.sizeInBytes = candidate.sizeInBytes;
- allocInfo.alignment = candidate.alignment;
- allocInfos.push_back(allocInfo);
+ for (auto &candidate : candidates) {
+ bufferization::MemoryPlannerAlloc info;
+ info.sizeInBytes = candidate.sizeInBytes;
+ info.alignment = candidate.alignment;
+
+ Block *block = candidate.alloc->getBlock();
+ int64_t opIdx = 0;
+ for (Operation &op : *block) {
+ if (&op == candidate.alloc.getOperation())
+ info.timeStart = opIdx;
+ if (&op == candidate.dealloc.getOperation())
+ info.timeEnd = opIdx;
+ ++opIdx;
+ }
+
+ allocInfos.push_back(info);
arenaAlignment = std::lcm(arenaAlignment, candidate.alignment);
}
- // Step 3: Run the planning algorithm
- SmallVector<int64_t> offsets =
- trivialMemoryPlanner(arenaAlignment, allocInfos);
+ // Step 3: Run the planning algorithm.
+ SmallVector<int64_t> offsets;
+ switch (algorithm) {
+ case bufferization::MemoryPlannerAlgorithm::Trivial:
+ offsets = bufferization::trivialMemoryPlanner(arenaAlignment, allocInfos);
+ break;
+ case bufferization::MemoryPlannerAlgorithm::BestFit:
+ offsets = bufferization::bestFitMemoryPlanner(arenaAlignment, allocInfos);
+ break;
+ }
- // Assign computed offsets back to candidates
+ // Step 4: Compute total arena size and assign offsets.
int64_t totalSize = 0;
for (size_t i = 0; i < candidates.size(); ++i) {
candidates[i].offset = offsets[i];
@@ -200,7 +173,7 @@ struct StaticMemoryPlannerAnalysisPass
<< " alignment=" << candidates[i].alignment << "\n");
}
- // Step 4: Obtain arena based on arena mode
+ // Step 5: Obtain arena based on arena mode.
Operation *firstAlloc = candidates.front().alloc;
OpBuilder builder(firstAlloc);
Value arenaValue;
@@ -242,7 +215,7 @@ struct StaticMemoryPlannerAnalysisPass
return signalPassFailure();
}
- // Step 5: Replace each alloc with memref.view directly on arena
+ // Step 6: Replace each alloc with memref.view into the arena.
for (auto &candidate : candidates) {
builder.setInsertionPoint(candidate.alloc);
Location loc = candidate.alloc.getLoc();
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp
new file mode 100644
index 0000000000000..e1779cb8e5831
--- /dev/null
+++ b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp
@@ -0,0 +1,111 @@
+//===- StaticMemoryPlanning.cpp - Memory planning algorithms --------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h"
+#include "llvm/Support/MathExtras.h"
+#include <numeric>
+
+using namespace mlir::bufferization;
+
+/// Align an offset to the specified alignment.
+static int64_t alignOffset(int64_t offset, int64_t alignment) {
+ return llvm::alignTo(offset, alignment);
+}
+
+llvm::SmallVector<int64_t>
+mlir::bufferization::trivialMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs) {
+ llvm::SmallVector<int64_t> offsets;
+ int64_t currentOffset = 0;
+
+ for (const auto &alloc : allocs) {
+ currentOffset = alignOffset(currentOffset, alloc.alignment);
+ assert((arenaAlignment + currentOffset) % alloc.alignment == 0 &&
+ "invalid alignment");
+ offsets.push_back(currentOffset);
+ currentOffset += alloc.sizeInBytes;
+ }
+
+ return offsets;
+}
+
+llvm::SmallVector<int64_t>
+mlir::bufferization::bestFitMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs) {
+ struct Placement {
+ int64_t offset;
+ int64_t size;
+ int64_t timeEnd;
+ };
+
+ // Process allocations in order of start time.
+ llvm::SmallVector<unsigned> order(allocs.size());
+ std::iota(order.begin(), order.end(), 0);
+ llvm::sort(order, [&](unsigned a, unsigned b) {
+ return allocs[a].timeStart < allocs[b].timeStart;
+ });
+
+ llvm::SmallVector<Placement> placements;
+ llvm::SmallVector<int64_t> offsets(allocs.size(), 0);
+
+ for (unsigned idx : order) {
+ const MemoryPlannerAlloc &alloc = allocs[idx];
+
+ // Collect intervals that are still live at this allocation's start time.
+ llvm::SmallVector<std::pair<int64_t, int64_t>> occupied;
+ for (const auto &p : placements) {
+ if (p.timeEnd > alloc.timeStart)
+ occupied.push_back({p.offset, p.offset + p.size});
+ }
+ llvm::sort(occupied);
+
+ // Find the best (smallest) gap that fits this allocation.
+ int64_t bestOffset = -1;
+ int64_t bestGapSize = INT64_MAX;
+
+ // Arena high-water mark from all placements so far.
+ int64_t arenaEnd = 0;
+ for (const auto &p : placements)
+ arenaEnd = std::max(arenaEnd, p.offset + p.size);
+
+ int64_t gapStart = 0;
+ for (const auto &[occStart, occEnd] : occupied) {
+ int64_t alignedStart = alignOffset(gapStart, alloc.alignment);
+ int64_t gapEnd = occStart;
+ if (alignedStart + alloc.sizeInBytes <= gapEnd) {
+ int64_t gapSize = gapEnd - alignedStart;
+ if (gapSize < bestGapSize) {
+ bestGapSize = gapSize;
+ bestOffset = alignedStart;
+ }
+ }
+ gapStart = std::max(gapStart, occEnd);
+ }
+
+ // Check the trailing gap (between last occupied and arena end).
+ int64_t alignedTrailing = alignOffset(gapStart, alloc.alignment);
+ if (alignedTrailing + alloc.sizeInBytes <= arenaEnd) {
+ int64_t trailingSize = arenaEnd - alignedTrailing;
+ if (trailingSize < bestGapSize) {
+ bestGapSize = trailingSize;
+ bestOffset = alignedTrailing;
+ }
+ }
+
+ // If no existing gap worked, append at the end.
+ if (bestOffset < 0)
+ bestOffset = alignedTrailing;
+
+ assert((arenaAlignment + bestOffset) % alloc.alignment == 0 &&
+ "invalid alignment");
+ offsets[idx] = bestOffset;
+ placements.push_back({bestOffset, alloc.sizeInBytes, alloc.timeEnd});
+ }
+
+ return offsets;
+}
diff --git a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir
new file mode 100644
index 0000000000000..6c261880140a5
--- /dev/null
+++ b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir
@@ -0,0 +1,118 @@
+// RUN: mlir-opt %s -pass-pipeline="builtin.module(func.func(static-memory-planner-analysis{algorithm=best-fit}))" \
+// RUN: -split-input-file | FileCheck %s
+
+// -----
+
+// Test 1: Non-overlapping lifetimes reuse the same memory.
+// With trivial packing this would be 8192 bytes; best-fit reuses the space.
+// CHECK-LABEL: func @reuse_non_overlapping
+func.func @reuse_non_overlapping() {
+ // Arena should be 4096 bytes (1024 * 4), not 8192.
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // First allocation at offset 0
+ // CHECK-NEXT: %[[C0_0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0_0]]][] : memref<4096xi8> to memref<1024xf32>
+ // Second allocation also at offset 0 (reuses freed space)
+ // CHECK-NEXT: %[[C0_1:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0_1]]][] : memref<4096xi8> to memref<1024xf32>
+ %0 = memref.alloc() : memref<1024xf32>
+ memref.dealloc %0 : memref<1024xf32>
+ %1 = memref.alloc() : memref<1024xf32>
+ memref.dealloc %1 : memref<1024xf32>
+ return
+}
+
+// -----
+
+// Test 2: Overlapping lifetimes cannot reuse memory.
+// CHECK-LABEL: func @no_reuse_overlapping
+func.func @no_reuse_overlapping() {
+ // Both are live at the same time, so arena = 4096 + 2048 = 6144 bytes.
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<6144xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<6144xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C4096]]][] : memref<6144xi8> to memref<512xf32>
+ %0 = memref.alloc() : memref<1024xf32>
+ %1 = memref.alloc() : memref<512xf32>
+ memref.dealloc %0 : memref<1024xf32>
+ memref.dealloc %1 : memref<512xf32>
+ return
+}
+
+// -----
+
+// Test 3: Best-fit picks the smallest suitable gap.
+// Layout: A(4096) at 0, B(1024) at 4096, C(4096) at 5120, D(1024) at 9216.
+// B and D are freed while A and C are still live, creating two gaps:
+// [4096, 5120) = 1024 bytes (B's slot)
+// [9216, 10240) = 1024 bytes (D's slot)
+// Then we free A, creating gap [0, 4096) = 4096 bytes.
+// Now allocate E(512 bytes). Gaps: [0,4096)=4096, [4096,5120)=1024, [9216,10240)=1024.
+// Best-fit should pick one of the 1024-byte gaps (smallest fit for 512).
+// CHECK-LABEL: func @best_fit_smallest_gap
+func.func @best_fit_smallest_gap() {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<10240xi8>
+ // A at offset 0
+ // CHECK-NEXT: %{{.*}} = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ // B at offset 4096
+ // CHECK-NEXT: %{{.*}} = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}}...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/207403
More information about the Mlir-commits
mailing list