[Mlir-commits] [mlir] [mlir][linalg] Constrain tiling semi-affine maps (PR #212240)
Adam Siemieniuk
llvmlistbot at llvm.org
Tue Aug 4 05:54:41 PDT 2026
https://github.com/adam-smnk updated https://github.com/llvm/llvm-project/pull/212240
>From 0f5919f0ba540b279f9e36515957199075f06bb0 Mon Sep 17 00:00:00 2001
From: Adam Siemieniuk <adam.siemieniuk at intel.com>
Date: Mon, 27 Jul 2026 14:20:39 +0200
Subject: [PATCH 1/3] [mlir][linalg] Constrain tiling semi-affine maps
Expands checks in Linalg's tiling implementation in presence of
semi-affine indexing maps to reject unsafe tiling configurations.
Current tiling can produce incorrect results when tiling occurs on
a dimension accessed via semi-affine map. This is due to lack of tile
offset tracking as shift in tiled slices cannot be represented today
using symbol-free indexing maps.
Assisted-by: Claude
---
.../Linalg/Transforms/TilingInterfaceImpl.cpp | 81 +++++
.../Dialect/Linalg/tile-semi-affine-maps.mlir | 328 ++++++++++++++++++
.../Dialect/Linalg/transform-op-fuse.mlir | 59 +++-
3 files changed, 467 insertions(+), 1 deletion(-)
create mode 100644 mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
diff --git a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
index 13b959fc7b0cc..cea308771b911 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
@@ -82,6 +82,79 @@ static LogicalResult inlinePayload(OpBuilder &b, LinalgOp linalgOp,
return success();
}
+/// Verify that tiling can be applied in presence of semi-affine maps.
+static LogicalResult
+validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
+ ArrayRef<OpFoldResult> sizes) {
+ auto isTiledDim = [&](unsigned pos) {
+ return pos < offsets.size() && !isZeroInteger(offsets[pos]);
+ };
+
+ for (AffineMap map : linalgOp.getIndexingMapsArray()) {
+ for (AffineExpr result : map.getResults()) {
+ WalkResult status = result.walk([&](AffineExpr expr) -> WalkResult {
+ auto binExpr = dyn_cast<AffineBinaryOpExpr>(expr);
+ if (!binExpr)
+ return WalkResult::advance();
+ AffineExprKind kind = binExpr.getKind();
+ if (kind != AffineExprKind::Mod && kind != AffineExprKind::FloorDiv &&
+ kind != AffineExprKind::CeilDiv)
+ return WalkResult::advance();
+
+ // Skip if the semi-affine expression does not involve any of the tiled
+ // dimensions.
+ bool involvesTiledDim = false;
+ for (unsigned pos = 0, e = offsets.size(); pos < e; ++pos) {
+ if (isTiledDim(pos) && expr.isFunctionOfDim(pos)) {
+ involvesTiledDim = true;
+ break;
+ }
+ }
+ if (!involvesTiledDim)
+ return WalkResult::advance();
+
+ // Allow only `d OP C` map where `d` is a dimension and `C` is a
+ // constant. A compound LHS (e.g. `(d0 + d1)`, `(d0 * 2)`, a nested
+ // semi-affine expression) or a non-constant step is not provably safe,
+ // so reject it.
+ auto dimExpr = dyn_cast<AffineDimExpr>(binExpr.getLHS());
+ auto stepExpr = dyn_cast<AffineConstantExpr>(binExpr.getRHS());
+ if (!dimExpr || !stepExpr || stepExpr.getValue() <= 0) {
+ linalgOp.emitOpError()
+ << "tiling is not supported for the semi-affine indexing map: "
+ "only a single iteration dimension divided by a positive "
+ "constant step can be tiled over a tiled dimension";
+ return WalkResult::interrupt();
+ }
+ unsigned dimPos = dimExpr.getPosition();
+ int64_t step = stepExpr.getValue();
+
+ // Tile boundaries stay aligned to the step only when the tile size and
+ // step divide one another.
+ // Dynamic tile sizes are assumed to be valid.
+ FailureOr<int64_t> tileSize =
+ ValueBoundsConstraintSet::computeConstantBound(
+ presburger::BoundType::UB, sizes[dimPos],
+ /*stopCondition=*/nullptr,
+ ValueBoundsOptions{/*closedUB=*/true});
+ if (succeeded(tileSize) &&
+ !(*tileSize % step == 0 || step % *tileSize == 0)) {
+ linalgOp.emitOpError()
+ << "tiling is not supported for the semi-affine indexing map: "
+ "tile size "
+ << *tileSize << " for dimension d" << dimPos
+ << " must divide or be divisible by the step " << step;
+ return WalkResult::interrupt();
+ }
+ return WalkResult::advance();
+ });
+ if (status.wasInterrupted())
+ return failure();
+ }
+ }
+ return success();
+}
+
//===----------------------------------------------------------------------===//
// External Model for implementing `TilingInterface` for `LinalgOp`s.
//===----------------------------------------------------------------------===//
@@ -138,6 +211,14 @@ struct LinalgOpTilingInterface
// specified could lead to out of bounds accesses.
Location loc = op->getLoc();
LinalgOp linalgOp = cast<LinalgOp>(op);
+ // In case of a semi-affine expression, generalized tracking of tiles would
+ // require a per-tile-position shift that cannot be expressed by the
+ // symbol-free indexing maps.
+ // Thus, tiling is allowed only when the semi-affine maps can be proven safe
+ // for the current tiling configuration. Otherwise, tiling can end up
+ // producing incorrect results.
+ if (failed(validateTilingSemiAffineMaps(linalgOp, offsets, sizes)))
+ return failure();
SmallVector<Value> valuesToTile = linalgOp->getOperands();
SmallVector<Value> tiledOperands = makeTiledShapes(
b, loc, linalgOp, valuesToTile, offsets, sizes, {}, true);
diff --git a/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
new file mode 100644
index 0000000000000..8e1ad594f31f2
--- /dev/null
+++ b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
@@ -0,0 +1,328 @@
+// RUN: mlir-opt %s -transform-interpreter -canonicalize -split-input-file -verify-diagnostics | FileCheck %s
+
+#map = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+// CHECK-LABEL: func @tile_floordiv_tile_multiple_of_step
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: tensor.extract_slice %{{.*}}[%[[IV]]] [6] [1] : tensor<12xf32> to tensor<6xf32>
+// CHECK: tensor.extract_slice %{{.*}} [2] [1] : tensor<4xf32> to tensor<2xf32>
+// CHECK: linalg.generic
+func.func @tile_floordiv_tile_multiple_of_step(%arg0: tensor<12xf32>, %arg1: tensor<4xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %0 = linalg.generic {indexing_maps = [#map, #floordiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<4xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// The step being a multiple of the tile size is also aligned: every tile falls
+// within a single `floordiv` bucket.
+
+#map = affine_map<(d0) -> (d0)>
+#floordiv4 = affine_map<(d0) -> (d0 floordiv 4)>
+
+// CHECK-LABEL: func @tile_floordiv_step_multiple_of_tile
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: tensor.extract_slice %{{.*}}[%[[IV]]] [2] [1] : tensor<12xf32> to tensor<2xf32>
+// CHECK: tensor.extract_slice %{{.*}} [1] [1] : tensor<3xf32> to tensor<1xf32>
+// CHECK: linalg.generic
+func.func @tile_floordiv_step_multiple_of_tile(%arg0: tensor<12xf32>, %arg1: tensor<3xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %0 = linalg.generic {indexing_maps = [#map, #floordiv4, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<3xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [2] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A `floordiv` on a dimension that is not tiled is always safe.
+
+#ident = affine_map<(d0, d1) -> (d0, d1)>
+#floordiv3 = affine_map<(d0, d1) -> (d0, d1 floordiv 3)>
+
+// CHECK-LABEL: func @tile_floordiv_untiled_dim
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: linalg.generic
+func.func @tile_floordiv_untiled_dim(%arg0: tensor<8x12xf32>, %arg1: tensor<8x4xf32>, %out: tensor<8x12xf32>) -> tensor<8x12xf32> {
+ %0 = linalg.generic {indexing_maps = [#ident, #floordiv3, #ident], iterator_types = ["parallel", "parallel"]}
+ ins(%arg0, %arg1 : tensor<8x12xf32>, tensor<8x4xf32>) outs(%out : tensor<8x12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<8x12xf32>
+ return %0 : tensor<8x12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4, 0] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// `mod` obeys the same alignment rule as `floordiv`/`ceildiv`: with a tile size
+// that is a multiple of the modulus the full `[0, modulus)` slice is taken and
+// the tiled op re-applies the `mod` on local indices, so tiling is correct.
+
+#map = affine_map<(d0) -> (d0)>
+#mod3 = affine_map<(d0) -> (d0 mod 3)>
+
+// CHECK-LABEL: func @tile_mod_aligned
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: tensor.extract_slice %{{.*}}[%[[IV]]] [6] [1] : tensor<12xf32> to tensor<6xf32>
+// CHECK: tensor.extract_slice %{{.*}} [3] [1] : tensor<3xf32> to tensor<3xf32>
+// CHECK: linalg.generic
+func.func @tile_mod_aligned(%arg0: tensor<12xf32>, %arg1: tensor<3xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %0 = linalg.generic {indexing_maps = [#map, #mod3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<3xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A high-dimensional op with several distinct maps: the whole op is validated,
+// so every semi-affine access must be aligned.
+// Here `d0 floordiv 4` (tile 4) and `d2 mod 3` (tile 6) are both aligned.
+
+#id = affine_map<(d0, d1, d2) -> (d0, d1, d2)>
+#fdiv = affine_map<(d0, d1, d2) -> (d0 floordiv 4, d1)>
+#mod = affine_map<(d0, d1, d2) -> (d2 mod 3)>
+
+// CHECK-LABEL: func @tile_3d_generic_multi_map_aligned_floordiv_and_mod
+// CHECK: scf.for %[[IV0:.*]] = %{{.*}} step %{{.*}}
+// CHECK: scf.for %[[IV1:.*]] = %{{.*}} step %{{.*}}
+// CHECK: scf.for %[[IV2:.*]] = %{{.*}} step %{{.*}}
+// CHECK: tensor.extract_slice %{{.*}} [4, 3, 6] [1, 1, 1] : tensor<8x6x12xf32> to tensor<4x3x6xf32>
+// CHECK: tensor.extract_slice %{{.*}} [1, 3] [1, 1] : tensor<2x6xf32> to tensor<1x3xf32>
+// CHECK: tensor.extract_slice %{{.*}} [3] [1] : tensor<3xf32> to tensor<3xf32>
+// CHECK: linalg.generic
+func.func @tile_3d_generic_multi_map_aligned_floordiv_and_mod(%a0: tensor<8x6x12xf32>, %a1: tensor<2x6xf32>, %a2: tensor<3xf32>, %out: tensor<8x6x12xf32>) -> tensor<8x6x12xf32> {
+ %0 = linalg.generic {indexing_maps = [#id, #fdiv, #mod, #id], iterator_types = ["parallel", "parallel", "parallel"]}
+ ins(%a0, %a1, %a2 : tensor<8x6x12xf32>, tensor<2x6xf32>, tensor<3xf32>) outs(%out : tensor<8x6x12xf32>) {
+ ^bb0(%in0: f32, %in1: f32, %in2: f32, %o: f32):
+ %s = arith.addf %in0, %in1 : f32
+ %r = arith.addf %s, %in2 : f32
+ linalg.yield %r : f32
+ } -> tensor<8x6x12xf32>
+ return %0 : tensor<8x6x12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:4 = transform.structured.tile_using_for %0 tile_sizes [4, 3, 6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">, !transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// Provably misaligned `floordiv`: tile size 4 neither divides nor is divisible
+// by the step 3. Tiling must be rejected.
+
+#map = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+func.func @negative_tile_floordiv_misaligned(%arg0: tensor<12xf32>, %arg1: tensor<4xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must divide or be divisible by the step 3}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#map, #floordiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<4xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+#map = affine_map<(d0) -> (d0)>
+#ceildiv3 = affine_map<(d0) -> (d0 ceildiv 3)>
+
+func.func @negative_tile_ceildiv_misaligned(%arg0: tensor<12xf32>, %arg1: tensor<5xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must divide or be divisible by the step 3}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#map, #ceildiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<5xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+#map = affine_map<(d0) -> (d0)>
+#mod3 = affine_map<(d0) -> (d0 mod 3)>
+
+func.func @negative_tile_mod_misaligned(%arg0: tensor<12xf32>, %arg1: tensor<3xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must divide or be divisible by the step 3}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#map, #mod3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<3xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A semi-affine expression with a compound LHS (`d0 + d1`) over a tiled
+// dimension cannot be proven tiling-safe and is conservatively rejected, even
+// though the tile size would be aligned to the step for a bare dimension.
+
+#ident = affine_map<(d0, d1) -> (d0, d1)>
+#sum = affine_map<(d0, d1) -> ((d0 + d1) floordiv 4)>
+
+func.func @negative_tile_floordiv_sum_lhs(%arg0: tensor<8x8xf32>, %arg1: tensor<4xf32>, %out: tensor<8x8xf32>) -> tensor<8x8xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: only a single iteration dimension divided by a positive constant step can be tiled over a tiled dimension}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#ident, #sum, #ident], iterator_types = ["parallel", "parallel"]}
+ ins(%arg0, %arg1 : tensor<8x8xf32>, tensor<4xf32>) outs(%out : tensor<8x8xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<8x8xf32>
+ return %0 : tensor<8x8xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4, 0] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A single tiled dimension with a constant offset in the LHS (`d0 + 1`) is also
+// a compound LHS and is conservatively rejected, even for an otherwise aligned
+// tile size.
+
+#map = affine_map<(d0) -> (d0)>
+#offset = affine_map<(d0) -> ((d0 + 1) floordiv 3)>
+
+func.func @negative_tile_floordiv_offset_lhs(%arg0: tensor<12xf32>, %arg1: tensor<5xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: only a single iteration dimension divided by a positive constant step can be tiled over a tiled dimension}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#map, #offset, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<5xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// High-dimensional op with several distinct maps where an aligned `floordiv`
+// (`d0 floordiv 4`, tile 4) precedes a misaligned `mod` (`d2 mod 3`, tile 5).
+// Validation must scan the whole op and reject on the later, offending map.
+
+#id = affine_map<(d0, d1, d2) -> (d0, d1, d2)>
+#fdiv = affine_map<(d0, d1, d2) -> (d0 floordiv 4, d1)>
+#mod = affine_map<(d0, d1, d2) -> (d2 mod 3)>
+
+func.func @negative_tile_3d_generic_misaligned_mod_in_later_map(%a0: tensor<8x6x12xf32>, %a1: tensor<2x6xf32>, %a2: tensor<3xf32>, %out: tensor<8x6x12xf32>) -> tensor<8x6x12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 5 for dimension d2 must divide or be divisible by the step 3}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#id, #fdiv, #mod, #id], iterator_types = ["parallel", "parallel", "parallel"]}
+ ins(%a0, %a1, %a2 : tensor<8x6x12xf32>, tensor<2x6xf32>, tensor<3xf32>) outs(%out : tensor<8x6x12xf32>) {
+ ^bb0(%in0: f32, %in1: f32, %in2: f32, %o: f32):
+ %s = arith.addf %in0, %in1 : f32
+ %r = arith.addf %s, %in2 : f32
+ linalg.yield %r : f32
+ } -> tensor<8x6x12xf32>
+ return %0 : tensor<8x6x12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:4 = transform.structured.tile_using_for %0 tile_sizes [4, 3, 5] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">, !transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
diff --git a/mlir/test/Dialect/Linalg/transform-op-fuse.mlir b/mlir/test/Dialect/Linalg/transform-op-fuse.mlir
index dab8491708104..a9e3f06736174 100644
--- a/mlir/test/Dialect/Linalg/transform-op-fuse.mlir
+++ b/mlir/test/Dialect/Linalg/transform-op-fuse.mlir
@@ -1,4 +1,4 @@
-// RUN: mlir-opt %s --transform-interpreter --split-input-file -canonicalize | FileCheck %s
+// RUN: mlir-opt %s --transform-interpreter --split-input-file -canonicalize -verify-diagnostics | FileCheck %s
// CHECK-LABEL: func.func @fuse_unary
func.func @fuse_unary(%arg0: tensor<?x?xf32>, %arg1: tensor<?x?xf32>) -> tensor<?x?xf32> {
@@ -667,3 +667,60 @@ module attributes {transform.with_named_sequence} {
transform.yield
}
}
+
+// -----
+
+#id = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+// CHECK-LABEL: func.func @fuse_producer_semi_affine_aligned
+// CHECK: scf.for
+// CHECK: tensor.extract_slice %{{.*}} [2] [1] : tensor<4xf32> to tensor<2xf32>
+// CHECK: linalg.generic
+// CHECK: linalg.exp
+func.func @fuse_producer_semi_affine_aligned(%arg0: tensor<12xf32>, %scale: tensor<4xf32>, %init: tensor<12xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %0 = linalg.generic {indexing_maps = [#id, #floordiv3, #id], iterator_types = ["parallel"]}
+ ins(%arg0, %scale : tensor<12xf32>, tensor<4xf32>) outs(%init : tensor<12xf32>) {
+ ^bb0(%a: f32, %s: f32, %o: f32):
+ %m = arith.addf %a, %s : f32
+ linalg.yield %m : f32
+ } -> tensor<12xf32>
+ %1 = linalg.exp ins(%0 : tensor<12xf32>) outs(%out : tensor<12xf32>) -> tensor<12xf32>
+ return %1 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.exp"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1, %loops = transform.structured.fuse %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.any_op)
+ transform.yield
+ }
+}
+
+// -----
+
+// The semi-affine map tiling check is applied during fusion: a producer with a
+// misaligned `floordiv` map (step 3, tile 4) cannot be fused.
+
+#id = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+func.func @negative_fuse_producer_semi_affine_misaligned(%arg0: tensor<12xf32>, %scale: tensor<4xf32>, %init: tensor<12xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+1 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must divide or be divisible by the step 3}}
+ %0 = linalg.generic {indexing_maps = [#id, #floordiv3, #id], iterator_types = ["parallel"]}
+ ins(%arg0, %scale : tensor<12xf32>, tensor<4xf32>) outs(%init : tensor<12xf32>) {
+ ^bb0(%a: f32, %s: f32, %o: f32):
+ %m = arith.addf %a, %s : f32
+ linalg.yield %m : f32
+ } -> tensor<12xf32>
+ %1 = linalg.exp ins(%0 : tensor<12xf32>) outs(%out : tensor<12xf32>) -> tensor<12xf32>
+ return %1 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.exp"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1, %loops = transform.structured.fuse %0 tile_sizes [4] : (!transform.any_op) -> (!transform.any_op, !transform.any_op)
+ transform.yield
+ }
+}
>From de463f3a071b561ffa7295b47aaf968906e6c691 Mon Sep 17 00:00:00 2001
From: Adam Siemieniuk <adam.siemieniuk at intel.com>
Date: Fri, 31 Jul 2026 13:42:55 +0200
Subject: [PATCH 2/3] Constrain ceildiv
---
.../Linalg/Transforms/TilingInterfaceImpl.cpp | 39 +++++++++---
.../Dialect/Linalg/tile-semi-affine-maps.mlir | 62 ++++++++++++++++++-
2 files changed, 93 insertions(+), 8 deletions(-)
diff --git a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
index cea308771b911..bb24ab1bc9621 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
@@ -127,23 +127,48 @@ validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
return WalkResult::interrupt();
}
unsigned dimPos = dimExpr.getPosition();
- int64_t step = stepExpr.getValue();
- // Tile boundaries stay aligned to the step only when the tile size and
- // step divide one another.
- // Dynamic tile sizes are assumed to be valid.
+ // Tiles are spaced by the full tile size, so tile origins are its
+ // multiples (0, tileSize, 2*tileSize, ...).
+ // A tile's indices are `origin + d'`, with `origin` the tile's start
+ // and `0 <= d' < tileSize`. A trailing partial tile is a full tile
+ // truncated at the same origin, spanning a subset of the same `d'`, so
+ // full-tile validity implies partial-tile validity and validating the
+ // upper-bound tile size suffices.
FailureOr<int64_t> tileSize =
ValueBoundsConstraintSet::computeConstantBound(
presburger::BoundType::UB, sizes[dimPos],
/*stopCondition=*/nullptr,
ValueBoundsOptions{/*closedUB=*/true});
- if (succeeded(tileSize) &&
- !(*tileSize % step == 0 || step % *tileSize == 0)) {
+
+ // Dynamic tile sizes are assumed to be valid.
+ // Unit tile is always valid.
+ if (failed(tileSize) || *tileSize == 1)
+ return WalkResult::advance();
+
+ // Tiled op reuses the same map on a slice whose base offset is
+ // `m(origin) - m(0)`, so it is correct only when
+ // `m(origin + d') == (m(origin) - m(0)) + m(d')` for every `d'`.
+ // Slice origins are tile-size multiples, so this reduces to a relation
+ // between the tile size and the step `C`:
+ // - `floordiv`/`mod` are locally affine within a step window (floordiv
+ // is constant, mod is linear), so they compose when the origin is
+ // step-aligned (`C | tileSize`) or the whole tile fits in one window
+ // (`tileSize | C`);
+ // - `ceildiv` jumps at `k * C + 1` instead of `k * C`, so a
+ // non-step-aligned origin already straddles the jump. It composes
+ // only from a step-aligned origin, i.e. `C | tileSize`.
+ int64_t step = stepExpr.getValue();
+ bool isCeil = kind == AffineExprKind::CeilDiv;
+ bool safe = *tileSize % step == 0 || (!isCeil && step % *tileSize == 0);
+ if (!safe) {
linalgOp.emitOpError()
<< "tiling is not supported for the semi-affine indexing map: "
"tile size "
<< *tileSize << " for dimension d" << dimPos
- << " must divide or be divisible by the step " << step;
+ << (isCeil ? " must be a multiple of the step "
+ : " must divide or be divisible by the step ")
+ << step;
return WalkResult::interrupt();
}
return WalkResult::advance();
diff --git a/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
index 8e1ad594f31f2..0671640af2fb8 100644
--- a/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
+++ b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
@@ -119,6 +119,37 @@ module attributes {transform.with_named_sequence} {
// -----
+// `ceildiv` composes only from a step-aligned origin, so it is safe only when
+// the step divides the tile size (unlike `floordiv`/`mod`).
+
+#map = affine_map<(d0) -> (d0)>
+#ceildiv3 = affine_map<(d0) -> (d0 ceildiv 3)>
+
+// CHECK-LABEL: func @tile_ceildiv_tile_multiple_of_step
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: tensor.extract_slice %{{.*}}[%[[IV]]] [6] [1] : tensor<12xf32> to tensor<6xf32>
+// CHECK: tensor.extract_slice %{{.*}} [3] [1] : tensor<5xf32> to tensor<3xf32>
+// CHECK: linalg.generic
+func.func @tile_ceildiv_tile_multiple_of_step(%arg0: tensor<12xf32>, %arg1: tensor<5xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %0 = linalg.generic {indexing_maps = [#map, #ceildiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<5xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
// A high-dimensional op with several distinct maps: the whole op is validated,
// so every semi-affine access must be aligned.
// Here `d0 floordiv 4` (tile 4) and `d2 mod 3` (tile 6) are both aligned.
@@ -189,7 +220,7 @@ module attributes {transform.with_named_sequence} {
#ceildiv3 = affine_map<(d0) -> (d0 ceildiv 3)>
func.func @negative_tile_ceildiv_misaligned(%arg0: tensor<12xf32>, %arg1: tensor<5xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
- // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must divide or be divisible by the step 3}}
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d0 must be a multiple of the step 3}}
// expected-error @+2 {{'linalg.generic' op failed to tile operation}}
// expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
%0 = linalg.generic {indexing_maps = [#map, #ceildiv3, #map], iterator_types = ["parallel"]}
@@ -211,6 +242,35 @@ module attributes {transform.with_named_sequence} {
// -----
+// Unlike `floordiv`/`mod`, `ceildiv` is not safe when the step is a multiple of
+// the tile size: the tile origin is not step-aligned.
+
+#map = affine_map<(d0) -> (d0)>
+#ceildiv4 = affine_map<(d0) -> (d0 ceildiv 4)>
+
+func.func @negative_tile_ceildiv_step_multiple_of_tile(%arg0: tensor<12xf32>, %arg1: tensor<4xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 2 for dimension d0 must be a multiple of the step 4}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#map, #ceildiv4, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<4xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [2] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
#map = affine_map<(d0) -> (d0)>
#mod3 = affine_map<(d0) -> (d0 mod 3)>
>From 63049fa7611560a08d4fe92216563fcf752edbcf Mon Sep 17 00:00:00 2001
From: Adam Siemieniuk <adam.siemieniuk at intel.com>
Date: Tue, 4 Aug 2026 14:47:09 +0200
Subject: [PATCH 3/3] Validate tiling using given sizes
---
.../Linalg/Transforms/TilingInterfaceImpl.cpp | 57 ++++---
.../Dialect/Linalg/tile-semi-affine-maps.mlir | 153 ++++++++++++++++++
2 files changed, 189 insertions(+), 21 deletions(-)
diff --git a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
index bb24ab1bc9621..b00ab8a7d6ee7 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/TilingInterfaceImpl.cpp
@@ -84,11 +84,29 @@ static LogicalResult inlinePayload(OpBuilder &b, LinalgOp linalgOp,
/// Verify that tiling can be applied in presence of semi-affine maps.
static LogicalResult
-validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
- ArrayRef<OpFoldResult> sizes) {
- auto isTiledDim = [&](unsigned pos) {
- return pos < offsets.size() && !isZeroInteger(offsets[pos]);
- };
+validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> sizes) {
+ // Precompute each dimension's constant tile-size upper bound once.
+ // A failed entry marks a dynamic tile with no static bound.
+ SmallVector<FailureOr<int64_t>> tileSizeBounds =
+ llvm::map_to_vector(sizes, [](OpFoldResult size) {
+ return ValueBoundsConstraintSet::computeConstantBound(
+ presburger::BoundType::UB, size,
+ /*stopCondition=*/nullptr, ValueBoundsOptions{/*closedUB=*/true});
+ });
+ SmallVector<int64_t> loopRanges = linalgOp.getStaticLoopRanges();
+
+ // Dynamic tiles or dynamic loop ranges are conservatively treated as tiled.
+ SmallVector<bool> tiledDims(loopRanges.size(), false);
+ for (auto [pos, tileSize] : llvm::enumerate(tileSizeBounds)) {
+ if (failed(tileSize)) {
+ tiledDims[pos] = true;
+ continue;
+ }
+ if (*tileSize == 0)
+ continue;
+ tiledDims[pos] =
+ ShapedType::isDynamic(loopRanges[pos]) || *tileSize < loopRanges[pos];
+ }
for (AffineMap map : linalgOp.getIndexingMapsArray()) {
for (AffineExpr result : map.getResults()) {
@@ -101,15 +119,16 @@ validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
kind != AffineExprKind::CeilDiv)
return WalkResult::advance();
- // Skip if the semi-affine expression does not involve any of the tiled
- // dimensions.
- bool involvesTiledDim = false;
- for (unsigned pos = 0, e = offsets.size(); pos < e; ++pos) {
- if (isTiledDim(pos) && expr.isFunctionOfDim(pos)) {
- involvesTiledDim = true;
- break;
- }
- }
+ // Skip if the semi-affine expression does not involve any tiled
+ // dimension: an untiled dimension keeps its full extent in every tile,
+ // so re-applying the map on the slice is exact.
+ bool involvesTiledDim = expr.walk([&](AffineExpr e) -> WalkResult {
+ auto dim = dyn_cast<AffineDimExpr>(e);
+ if (dim && tiledDims[dim.getPosition()])
+ return WalkResult::interrupt();
+ return WalkResult::advance();
+ })
+ .wasInterrupted();
if (!involvesTiledDim)
return WalkResult::advance();
@@ -126,7 +145,6 @@ validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
"constant step can be tiled over a tiled dimension";
return WalkResult::interrupt();
}
- unsigned dimPos = dimExpr.getPosition();
// Tiles are spaced by the full tile size, so tile origins are its
// multiples (0, tileSize, 2*tileSize, ...).
@@ -135,11 +153,8 @@ validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> offsets,
// truncated at the same origin, spanning a subset of the same `d'`, so
// full-tile validity implies partial-tile validity and validating the
// upper-bound tile size suffices.
- FailureOr<int64_t> tileSize =
- ValueBoundsConstraintSet::computeConstantBound(
- presburger::BoundType::UB, sizes[dimPos],
- /*stopCondition=*/nullptr,
- ValueBoundsOptions{/*closedUB=*/true});
+ unsigned dimPos = dimExpr.getPosition();
+ FailureOr<int64_t> tileSize = tileSizeBounds[dimPos];
// Dynamic tile sizes are assumed to be valid.
// Unit tile is always valid.
@@ -242,7 +257,7 @@ struct LinalgOpTilingInterface
// Thus, tiling is allowed only when the semi-affine maps can be proven safe
// for the current tiling configuration. Otherwise, tiling can end up
// producing incorrect results.
- if (failed(validateTilingSemiAffineMaps(linalgOp, offsets, sizes)))
+ if (failed(validateTilingSemiAffineMaps(linalgOp, sizes)))
return failure();
SmallVector<Value> valuesToTile = linalgOp->getOperands();
SmallVector<Value> tiledOperands = makeTiledShapes(
diff --git a/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
index 0671640af2fb8..555fc9206194d 100644
--- a/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
+++ b/mlir/test/Dialect/Linalg/tile-semi-affine-maps.mlir
@@ -386,3 +386,156 @@ module attributes {transform.with_named_sequence} {
transform.yield
}
}
+
+// -----
+
+// A `floordiv` on a statically shaped, tiled dimension (`d1`, tile 6, step 3)
+// is validated even when a separate, dynamically shaped dimension (`d0`) is
+// tiled but not part of the semi-affine map. The dynamic extent does not block
+// checking the static access.
+
+#id = affine_map<(d0, d1) -> (d0, d1)>
+#floordiv3 = affine_map<(d0, d1) -> (d0, d1 floordiv 3)>
+
+// CHECK-LABEL: func @tile_dynamic_dim_with_aligned_static_floordiv
+// CHECK: scf.for
+// CHECK: scf.for
+// CHECK: linalg.generic
+func.func @tile_dynamic_dim_with_aligned_static_floordiv(%arg0: tensor<?x12xf32>, %arg1: tensor<?x4xf32>, %out: tensor<?x12xf32>) -> tensor<?x12xf32> {
+ %0 = linalg.generic {indexing_maps = [#id, #floordiv3, #id], iterator_types = ["parallel", "parallel"]}
+ ins(%arg0, %arg1 : tensor<?x12xf32>, tensor<?x4xf32>) outs(%out : tensor<?x12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<?x12xf32>
+ return %0 : tensor<?x12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:3 = transform.structured.tile_using_for %0 tile_sizes [8, 6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// Validate per dim check coverage. No blanket assumption in presence of dynamic dimensions.
+// The static semi-affine dimension is still validated and rejected when misaligned.
+
+#id = affine_map<(d0, d1) -> (d0, d1)>
+#floordiv3 = affine_map<(d0, d1) -> (d0, d1 floordiv 3)>
+
+func.func @negative_tile_dynamic_dim_with_misaligned_static_floordiv(%arg0: tensor<?x12xf32>, %arg1: tensor<?x4xf32>, %out: tensor<?x12xf32>) -> tensor<?x12xf32> {
+ // expected-error @+3 {{'linalg.generic' op tiling is not supported for the semi-affine indexing map: tile size 4 for dimension d1 must divide or be divisible by the step 3}}
+ // expected-error @+2 {{'linalg.generic' op failed to tile operation}}
+ // expected-error @+1 {{'linalg.generic' op failed to generate tiling loops}}
+ %0 = linalg.generic {indexing_maps = [#id, #floordiv3, #id], iterator_types = ["parallel", "parallel"]}
+ ins(%arg0, %arg1 : tensor<?x12xf32>, tensor<?x4xf32>) outs(%out : tensor<?x12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<?x12xf32>
+ return %0 : tensor<?x12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:3 = transform.structured.tile_using_for %0 tile_sizes [8, 4] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// Fully dynamic shapes with static tile sizes: the loop range is dynamic, so
+// the dimension is conservatively treated as tiled and the aligned `floordiv`
+// (tile 6, step 3) is validated.
+
+#map = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+// CHECK-LABEL: func @tile_dynamic_shape_static_tile
+// CHECK: scf.for
+// CHECK: linalg.generic
+func.func @tile_dynamic_shape_static_tile(%arg0: tensor<?xf32>, %arg1: tensor<?xf32>, %out: tensor<?xf32>) -> tensor<?xf32> {
+ %0 = linalg.generic {indexing_maps = [#map, #floordiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<?xf32>, tensor<?xf32>) outs(%out : tensor<?xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<?xf32>
+ return %0 : tensor<?xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [6] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A `floordiv` on a dynamically shaped dimension that is left untiled:
+// its full extent has no static upper bound, so the tile size is dynamic
+// (full dim size here) and the access is assumed valid regardless of the step.
+
+#id = affine_map<(d0, d1) -> (d0, d1)>
+#floordiv3 = affine_map<(d0, d1) -> (d0, d1 floordiv 3)>
+
+// CHECK-LABEL: func @tile_untiled_dynamic_semi_affine_dim
+// CHECK: scf.for %[[IV:.*]] = %{{.*}} to %{{.*}} step %{{.*}}
+// CHECK: linalg.generic
+func.func @tile_untiled_dynamic_semi_affine_dim(%arg0: tensor<8x?xf32>, %arg1: tensor<8x?xf32>, %out: tensor<8x?xf32>) -> tensor<8x?xf32> {
+ %0 = linalg.generic {indexing_maps = [#id, #floordiv3, #id], iterator_types = ["parallel", "parallel"]}
+ ins(%arg0, %arg1 : tensor<8x?xf32>, tensor<8x?xf32>) outs(%out : tensor<8x?xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<8x?xf32>
+ return %0 : tensor<8x?xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [4] : (!transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
+
+// -----
+
+// A user-provided dynamic tile size (an SSA value) is assumed valid.
+
+#map = affine_map<(d0) -> (d0)>
+#floordiv3 = affine_map<(d0) -> (d0 floordiv 3)>
+
+func.func private @get_tile_size() -> index
+
+// CHECK-LABEL: func @tile_dynamic_tile_size_semi_affine
+// CHECK: scf.for
+// CHECK: linalg.generic
+func.func @tile_dynamic_tile_size_semi_affine(%arg0: tensor<12xf32>, %arg1: tensor<4xf32>, %out: tensor<12xf32>) -> tensor<12xf32> {
+ %sz = func.call @get_tile_size() : () -> index
+ %0 = linalg.generic {indexing_maps = [#map, #floordiv3, #map], iterator_types = ["parallel"]}
+ ins(%arg0, %arg1 : tensor<12xf32>, tensor<4xf32>) outs(%out : tensor<12xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %o: f32):
+ %1 = arith.addf %in, %in_0 : f32
+ linalg.yield %1 : f32
+ } -> tensor<12xf32>
+ return %0 : tensor<12xf32>
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%arg1: !transform.any_op {transform.readonly}) {
+ %0 = transform.structured.match ops{["linalg.generic"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %sz = transform.structured.match ops{["func.call"]} in %arg1 : (!transform.any_op) -> !transform.any_op
+ %1:2 = transform.structured.tile_using_for %0 tile_sizes [%sz] : (!transform.any_op, !transform.any_op) -> (!transform.any_op, !transform.op<"scf.for">)
+ transform.yield
+ }
+}
More information about the Mlir-commits
mailing list