[Mlir-commits] [mlir] [mlir][linalg] Add maps-based inferConvolutionDims overload (PR #203323)

Ege Beysel llvmlistbot at llvm.org
Fri Jun 12 08:16:59 PDT 2026


================
@@ -176,4 +176,139 @@ TEST_F(InferConvolutionDimsTest, Conv2DPairing) {
       << "outputImage[1]=1 should pair with filterLoop[1]=2 (ow <-> kw)";
 }
 
+/// Asserts that two ConvolutionDimensions are equal across every populated
+/// field.
+static void expectConvDimsEq(const ConvolutionDimensions &lhs,
+                             const ConvolutionDimensions &rhs) {
+  EXPECT_EQ(lhs.batch, rhs.batch);
+  EXPECT_EQ(lhs.outputImage, rhs.outputImage);
+  EXPECT_EQ(lhs.outputChannel, rhs.outputChannel);
+  EXPECT_EQ(lhs.filterLoop, rhs.filterLoop);
+  EXPECT_EQ(lhs.inputChannel, rhs.inputChannel);
+  EXPECT_EQ(lhs.depth, rhs.depth);
+  EXPECT_EQ(lhs.strides, rhs.strides);
+  EXPECT_EQ(lhs.dilations, rhs.dilations);
+}
+
+/// Verify that inferring convolution dimensions from indexing maps produces
+/// same result as inferring thrm directly from the convolution op.
+TEST_F(InferConvolutionDimsTest, MapsOverloadMatchesOpOverload) {
+  OpBuilder builder(ctx.get());
+  OwningOpRef<ModuleOp> module = ModuleOp::create(builder.getUnknownLoc());
+  builder.setInsertionPointToStart(module->getBody());
+  Location loc = builder.getUnknownLoc();
+  Type f32 = builder.getF32Type();
+  auto empty = [&](ArrayRef<int64_t> shape) -> Value {
+    return tensor::EmptyOp::create(builder, loc, shape, f32);
+  };
+  auto ones = builder.getI64TensorAttr({1, 1});
+
+  SmallVector<Operation *> convs;
+  {
+    Value out = empty({1, 4, 4, 4});
+    convs.push_back(linalg::Conv2DNhwcHwcfOp::create(
+                        builder, loc, out.getType(),
+                        ValueRange{empty({1, 10, 5, 3}), empty({2, 2, 3, 4})},
+                        ValueRange{out},
+                        /*strides=*/builder.getI64TensorAttr({2, 1}),
+                        /*dilations=*/builder.getI64TensorAttr({3, 1}))
+                        .getOperation());
+  }
+  {
+    Value out = empty({1, 4, 4, 3});
+    convs.push_back(linalg::DepthwiseConv2DNhwcHwcOp::create(
+                        builder, loc, out.getType(),
+                        ValueRange{empty({1, 5, 5, 3}), empty({2, 2, 3})},
+                        ValueRange{out}, ones, ones)
+                        .getOperation());
+  }
+
+  for (Operation *op : convs) {
+    LinalgOp linalgOp = cast<LinalgOp>(op);
+    FailureOr<ConvolutionDimensions> fromOp = inferConvolutionDims(linalgOp);
+    ASSERT_TRUE(succeeded(fromOp))
+        << "op overload failed for " << op->getName().getStringRef().str();
+    FailureOr<ConvolutionDimensions> fromMaps =
+        inferConvolutionDims(linalgOp.getIndexingMapsArray());
+    ASSERT_TRUE(succeeded(fromMaps))
+        << "maps overload failed for " << op->getName().getStringRef().str();
+    expectConvDimsEq(*fromOp, *fromMaps);
+  }
+
+  // Ensure the depthwise conv populates the depth field.
+  FailureOr<ConvolutionDimensions> depthwiseDims =
+      inferConvolutionDims(cast<LinalgOp>(convs.back()).getIndexingMapsArray());
+  ASSERT_TRUE(succeeded(depthwiseDims));
+  EXPECT_FALSE(depthwiseDims->depth.empty());
+}
+
+/// The maps overload must infer the correct dimensions for a strided and
+/// dilated NHWC/HWCF convolution.
+TEST_F(InferConvolutionDimsTest, InferNhwcConvDimensions) {
+  MLIRContext *c = ctx.get();
+  AffineExpr d0, d1, d2, d3, d4, d5, d6;
+  bindDims(c, d0, d1, d2, d3, d4, d5, d6);
+  // Loop order: (n=d0, oh=d1, ow=d2, f=d3, kh=d4, kw=d5, c=d6),
+  // strides {2,1}, dilations {3,1}.
+  SmallVector<AffineMap> maps = {
+      AffineMap::get(7, 0, {d0, d1 * 2 + d4 * 3, d2 + d5, d6}, c),
+      AffineMap::get(7, 0, {d4, d5, d6, d3}, c),
+      AffineMap::get(7, 0, {d0, d1, d2, d3}, c)};
+  FailureOr<ConvolutionDimensions> dims = inferConvolutionDims(maps);
+  ASSERT_TRUE(succeeded(dims));
+  ConvolutionDimensions expected{/*batch=*/{0},
+                                 /*outputImage=*/{1, 2},
+                                 /*outputChannel=*/{3},
+                                 /*filterLoop=*/{4, 5},
+                                 /*inputChannel=*/{6},
+                                 /*depth=*/{},
+                                 /*strides=*/{2, 1},
+                                 /*dilations=*/{3, 1}};
+  expectConvDimsEq(expected, *dims);
+}
+
+/// Maps-only check that filterLoop pairing is reconstructed from the input map:
+/// the swapped layout (d0 + d3, d1 + d2) must yield filterLoop [3, 2].
+TEST_F(InferConvolutionDimsTest, MapsOverloadConv2DPairing) {
+  MLIRContext *c = ctx.get();
+  AffineExpr d0, d1, d2, d3;
+  bindDims(c, d0, d1, d2, d3);
+  SmallVector<AffineMap> maps = {AffineMap::get(4, 0, {d0 + d3, d1 + d2}, c),
+                                 AffineMap::get(4, 0, {d2, d3}, c),
+                                 AffineMap::get(4, 0, {d0, d1}, c)};
+  FailureOr<ConvolutionDimensions> dims = inferConvolutionDims(maps);
+  ASSERT_TRUE(succeeded(dims));
+  EXPECT_EQ(dims->outputImage, (SmallVector<unsigned, 2>{0, 1}));
+  EXPECT_EQ(dims->filterLoop, (SmallVector<unsigned, 2>{3, 2}));
+}
+
+/// Inputs that cannot describe a convolution must fail.
+TEST_F(InferConvolutionDimsTest, MapsOverloadRejectsInvalidInput) {
+  MLIRContext *c = ctx.get();
+  AffineExpr d0, d1, d2, d3;
+  bindDims(c, d0, d1, d2, d3);
+
+  // Wrong number of maps (must be exactly 3: input, filter, output).
+  {
+    auto m = AffineMap::get(2, 0, {d0, d1}, c);
+    EXPECT_TRUE(failed(inferConvolutionDims(SmallVector<AffineMap>{m, m})));
+  }
+
+  // Output map is not a projected permutation: iterator types are unknowable.
----------------
egebeysel wrote:

```suggestion
  // Output map is not a projected permutation: iterator types are unidentifiable.
```

https://github.com/llvm/llvm-project/pull/203323


More information about the Mlir-commits mailing list