[Mlir-commits] [mlir] [mlir][affine] Skip nested duplicate loop matches in the super-vectorizer strategy (PR #224850)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Sat Sep 19 11:13:34 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-mlir-affine

Author: Stanislav Bardyuk (kodlan)

<details>
<summary>Changes</summary>

`-affine-super-vectorize=virtual-vector-size=32,256` asserts on this input:

```mlir
func.func @<!-- -->m(%A: memref<8x64xf32>) {
  affine.for %i = 0 to 8 {
    affine.for %j = 0 to 64 {
      affine.for %k = 0 to 1 {
        %0 = affine.load %A[%i, %j] : memref<8x64xf32>
        %1 = arith.addf %0, %0 : f32
        affine.store %1, %A[%i, %j] : memref<8x64xf32>
      }
    }
  }
  return
}
```

```
VectorUtils.cpp:469: Assertion `(!permutationMap || permutationMap.getNumResults() == static_cast<size_t>(vecToReadRank)) && "expected permutation map results to match vector rank."' failed.
```

The nested pattern matcher walks the whole subtree of a matched loop, so for the 2-D pattern the match rooted at `%i` reports both `%j` and `%k` as inner loops (`%k` is parallel because it runs once, and it is not used in any index). `analyzeProfitability` then maps both of them to vector dimension 1. When the load inside `%k` is vectorized, `makePermutationMap` sees three enclosing vectorized loops and builds a map with three results for a `vector<32x256xf32>`.

This only shows up when the innermost match (`%j` with `%k`) fails first, which it does since #<!-- -->184618 bails out on the store whose permutation map would have a broadcast dimension (the store does not use `%k`). The driver then tries the match rooted at `%i` and hits the bad strategy.

The fix skips, at each pattern level, a matched loop that is nested inside another matched loop of the same level, so only the outermost candidates get a vector dimension. The input above now vectorizes `%i` by 32 and `%j` by 256 and leaves `%k` as a scalar loop, the same output shape `vectorize_matmul` in `vectorize_2d.mlir` has for its 3-deep nest (there the third loop is a reduction and never matches, so it did not hit this). Sibling matches are unaffected. The public `vectorizeAffineLoopNest` entry already rejects such a nesting through `verifyLoopNesting`; the pass driver never checked it, and only the strategy map matters for `vectorizeLoopNest`, so the fix goes there.

The same assertion is also reachable in the other direction, when a load or store is enclosed by fewer vectorized loops than the vector rank (memory ops directly under the outer loop of a 2-D match). `vectorizeAffineLoad` and `vectorizeAffineStore` now fail the op when the permutation map's result count does not match the vector rank, so the match fails instead of the pass aborting. Both shapes have a test.

Fixes #<!-- -->220566

---
Full diff: https://github.com/llvm/llvm-project/pull/224850.diff


2 Files Affected:

- (modified) mlir/lib/Dialect/Affine/Transforms/SuperVectorize.cpp (+22-2) 
- (modified) mlir/test/Dialect/Affine/SuperVectorize/vectorize_2d.mlir (+41) 


``````````diff
diff --git a/mlir/lib/Dialect/Affine/Transforms/SuperVectorize.cpp b/mlir/lib/Dialect/Affine/Transforms/SuperVectorize.cpp
index 6c6e8ed979d235..f417615a0e5f9b 100644
--- a/mlir/lib/Dialect/Affine/Transforms/SuperVectorize.cpp
+++ b/mlir/lib/Dialect/Affine/Transforms/SuperVectorize.cpp
@@ -659,12 +659,17 @@ static LogicalResult analyzeProfitability(ArrayRef<NestedMatch> matches,
                                           unsigned patternDepth,
                                           VectorizationStrategy *strategy) {
   for (auto m : matches) {
+    Operation *loop = m.getMatchedOperation();
+    // Skip loops nested in another match at this level (same vector dim).
+    if (llvm::any_of(matches, [loop](const NestedMatch &other) {
+          return other.getMatchedOperation()->isProperAncestor(loop);
+        }))
+      continue;
     if (failed(analyzeProfitability(m.getMatchedChildren(), depthInPattern + 1,
                                     patternDepth, strategy))) {
       return failure();
     }
-    vectorizeLoopIfProfitable(m.getMatchedOperation(), depthInPattern,
-                              patternDepth, strategy);
+    vectorizeLoopIfProfitable(loop, depthInPattern, patternDepth, strategy);
   }
   return success();
 }
@@ -1265,6 +1270,13 @@ static Operation *vectorizeAffineLoad(AffineLoadOp loadOp,
   LLVM_DEBUG(dbgs() << "\n[early-vect]+++++ permutationMap: ");
   LLVM_DEBUG(permutationMap.print(dbgs()));
 
+  // One map result per enclosing vectorized loop; must match the vector rank.
+  if (permutationMap.getNumResults() != vectorType.getRank()) {
+    LLVM_DEBUG(dbgs() << "\n[early-vect]+++++ permutationMap rank does not "
+                         "match the vector rank\n");
+    return nullptr;
+  }
+
   Value transferVal = createReadOrMaskedRead(
       state.builder, loadOp.getLoc(), loadOp.getMemRef(), vectorType,
       /*padValue=*/std::nullopt, /*useInBoundsInsteadOfMasking=*/true, indices,
@@ -1314,6 +1326,14 @@ static Operation *vectorizeAffineStore(AffineStoreOp storeOp,
   LLVM_DEBUG(dbgs() << "\n[early-vect]+++++ permutationMap: ");
   LLVM_DEBUG(permutationMap.print(dbgs()));
 
+  // Same rank check as for loads.
+  if (permutationMap.getNumResults() !=
+      cast<VectorType>(vectorValue.getType()).getRank()) {
+    LLVM_DEBUG(dbgs() << "\n[early-vect]+++++ permutationMap rank does not "
+                         "match the vector rank\n");
+    return nullptr;
+  }
+
   // A transfer_write with a broadcast dimension (constant expr in the
   // permutation map) is invalid. Bail out to avoid producing invalid IR.
   if (llvm::any_of(permutationMap.getResults(),
diff --git a/mlir/test/Dialect/Affine/SuperVectorize/vectorize_2d.mlir b/mlir/test/Dialect/Affine/SuperVectorize/vectorize_2d.mlir
index eb5120a49e3d4b..f3782d9ca9a053 100644
--- a/mlir/test/Dialect/Affine/SuperVectorize/vectorize_2d.mlir
+++ b/mlir/test/Dialect/Affine/SuperVectorize/vectorize_2d.mlir
@@ -143,3 +143,44 @@ func.func @vectorize_matmul(%arg0: memref<?x?xf32>, %arg1: memref<?x?xf32>, %arg
   }
   return
 }
+
+// The innermost loop runs once and no index uses it, so it is parallel and is
+// matched as an inner loop just like %i1. The match rooted at %i0 used to map
+// %i1 and %i2 to the same vector dimension and crash on the load.
+// VECT-LABEL: func @vectorize_nested_parallel_loops
+//      VECT:   affine.for %[[I0:.*]] = 0 to 8 step 4 {
+// VECT-NEXT:     affine.for %[[I1:.*]] = 0 to 64 step 8 {
+// VECT-NEXT:       affine.for %{{.*}} = 0 to 1 {
+//      VECT:         %[[LD:.*]] = vector.transfer_read %{{.*}}[%[[I0]], %[[I1]]], %{{.*}} {{.*}}: memref<8x64xf32>, vector<4x8xf32>
+// VECT-NEXT:         %[[ADD:.*]] = arith.addf %[[LD]], %[[LD]] : vector<4x8xf32>
+// VECT-NEXT:         vector.transfer_write %[[ADD]], %{{.*}}[%[[I0]], %[[I1]]] {{.*}}: vector<4x8xf32>, memref<8x64xf32>
+func.func @vectorize_nested_parallel_loops(%A : memref<8x64xf32>) {
+  affine.for %i0 = 0 to 8 {
+    affine.for %i1 = 0 to 64 {
+      affine.for %i2 = 0 to 1 {
+        %0 = affine.load %A[%i0, %i1] : memref<8x64xf32>
+        %1 = arith.addf %0, %0 : f32
+        affine.store %1, %A[%i0, %i1] : memref<8x64xf32>
+      }
+    }
+  }
+  return
+}
+
+// The memory ops are only nested in %i0, but the match rooted at %i0 assigns
+// the second vector dimension to %i1, so their permutation maps have a single
+// result for a 2-D vector. Vectorization must fail instead of asserting.
+// VECT-LABEL: func @vectorize_memory_ops_outside_inner_loop
+//  VECT-NOT:   vector.transfer_read
+//  VECT-NOT:   vector.transfer_write
+func.func @vectorize_memory_ops_outside_inner_loop(%A : memref<8xf32>, %B : memref<8xf32>) {
+  affine.for %i0 = 0 to 8 {
+    %0 = affine.load %A[%i0] : memref<8xf32>
+    affine.store %0, %B[%i0] : memref<8xf32>
+    affine.for %i1 = 0 to 64 {
+      %c1 = arith.constant 1 : index
+      %1 = arith.addi %i1, %c1 : index
+    }
+  }
+  return
+}

``````````

</details>


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


More information about the Mlir-commits mailing list