[Codegen] Masked vectorization of inner_tiled (#23935)

So far, vectorization of `inner_tiled` did not support dynamic tensor
shapes. This PR enables vectorization of `inner_tiled` with dynamic
tensor shapes in the outer dimensions through masking for the case that
vector sizes are provided.

This is part of https://github.com/iree-org/iree/issues/23415.

Assisted-by: Claude Code

---------

Signed-off-by: Lukas Sommer <lukas.sommer@amd.com>
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/generic_vectorization_masked_inferred.mlir b/compiler/src/iree/compiler/Codegen/Common/test/generic_vectorization_masked_inferred.mlir
index 5449920..1badf10 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/generic_vectorization_masked_inferred.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/test/generic_vectorization_masked_inferred.mlir
@@ -873,3 +873,45 @@
 //       CHECK:   %[[MMA:.+]] = iree_codegen.inner_tiled ins(%[[LHS]], %[[RHS]], %[[LHS_SCALE]], %[[RHS_SCALE]]) outs(%[[ACC]])
 //  CHECK-SAME: : vector<3x5x1x32xf4E2M1FN>, vector<5x1x7x32xf8E4M3FN>, vector<3x5x1xf8E8M0FNU>, vector<5x7x1xf8E8M0FNU> into vector<3x7x4xf32>
 //       CHECK:   vector.transfer_write %[[MMA]], %arg4[%c0, %c0, %c0] {{.*}} : vector<3x7x4xf32>, tensor<3x7x4xf32>
+
+// -----
+
+// Masked vectorization of inner_tiled with dynamic outer dimensions.
+// Vector sizes come from the iree_codegen.vector_tile_sizes attribute.
+// The LHS has shape <?x?x4xf16> (outer dims dynamic), iteration space is
+// [i=2, j=5, k=3], so reads should be masked.
+#contraction_accesses = [
+ affine_map<(i, j, k) -> (i, k)>,
+ affine_map<(i, j, k) -> (k, j)>,
+ affine_map<(i, j, k) -> (i, j)>
+]
+func.func @masked_tensor_multi_mma(%lhs: tensor<?x?x4xf16>, %rhs: tensor<?x?x4xf16>, %acc: tensor<?x?x4xf32>) -> tensor<?x?x4xf32> {
+  %0 = iree_codegen.inner_tiled ins(%lhs, %rhs) outs(%acc) {
+    iree_codegen.vector_tile_sizes = array<i64: 2, 5, 3>,
+    indexing_maps = #contraction_accesses,
+    iterator_types = [#linalg.iterator_type<parallel>, #linalg.iterator_type<parallel>, #linalg.iterator_type<reduction>],
+    kind = #iree_gpu.mma_layout<MFMA_F32_16x16x16_F16>,
+    semantics = #iree_gpu.mma_semantics<distributed = true, opaque = false>
+  } : tensor<?x?x4xf16>, tensor<?x?x4xf16> into tensor<?x?x4xf32>
+  return %0 : tensor<?x?x4xf32>
+}
+
+// CHECK-LABEL: func @masked_tensor_multi_mma
+
+// With vectorSizes, reads use create_mask for dynamic dims.
+// LHS: outer (i=2, k=3) + inner (4) → vector<2x3x4xf16>
+// RHS: outer (k=3, j=5) + inner (4) → vector<3x5x4xf16>
+// ACC: outer (i=2, j=5) + inner (4) → vector<2x5x4xf32>
+//   CHECK-DAG:   %[[CSTF16:.+]] = arith.constant 0.000000e+00 : f16
+//   CHECK-DAG:   %[[CSTF32:.+]] = arith.constant 0.000000e+00 : f32
+//       CHECK:   %[[LHS_MASK:.+]] = vector.create_mask {{.*}} : vector<2x3x4xi1>
+//       CHECK:   %[[LHS:.+]] = vector.transfer_read %arg0{{.*}}, %[[CSTF16]], %[[LHS_MASK]]{{.*}} : tensor<?x?x4xf16>, vector<2x3x4xf16>
+//       CHECK:   %[[RHS_MASK:.+]] = vector.create_mask {{.*}} : vector<3x5x4xi1>
+//       CHECK:   %[[RHS:.+]] = vector.transfer_read %arg1{{.*}}, %[[CSTF16]], %[[RHS_MASK]]{{.*}} : tensor<?x?x4xf16>, vector<3x5x4xf16>
+//       CHECK:   %[[ACC_MASK:.+]] = vector.create_mask {{.*}} : vector<2x5x4xi1>
+//       CHECK:   %[[ACC:.+]] = vector.transfer_read %arg2{{.*}}, %[[CSTF32]], %[[ACC_MASK]]{{.*}} : tensor<?x?x4xf32>, vector<2x5x4xf32>
+//       CHECK:   %[[MMA:.+]] = iree_codegen.inner_tiled ins(%[[LHS]], %[[RHS]]) outs(%[[ACC]])
+//  CHECK-SAME:     : vector<2x3x4xf16>, vector<3x5x4xf16> into vector<2x5x4xf32>
+//       CHECK:   %[[WRITE_MASK:.+]] = vector.create_mask {{.*}} : vector<2x5x4xi1>
+//       CHECK:   vector.transfer_write %[[MMA]], %arg2{{.*}}, %[[WRITE_MASK]] {in_bounds = [false, false, true]}
+//  CHECK-SAME:     : vector<2x5x4xf32>, tensor<?x?x4xf32>
diff --git a/compiler/src/iree/compiler/Codegen/Interfaces/BUILD.bazel b/compiler/src/iree/compiler/Codegen/Interfaces/BUILD.bazel
index 7d54aa3..5771116 100644
--- a/compiler/src/iree/compiler/Codegen/Interfaces/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/Interfaces/BUILD.bazel
@@ -247,6 +247,7 @@
         "@llvm-project//mlir:TensorDialect",
         "@llvm-project//mlir:UBDialect",
         "@llvm-project//mlir:VectorDialect",
+        "@llvm-project//mlir:VectorUtils",
     ],
 )
 
diff --git a/compiler/src/iree/compiler/Codegen/Interfaces/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/Interfaces/CMakeLists.txt
index 5afd252..e4e0d8b 100644
--- a/compiler/src/iree/compiler/Codegen/Interfaces/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/Interfaces/CMakeLists.txt
@@ -181,6 +181,7 @@
     MLIRTensorDialect
     MLIRUBDialect
     MLIRVectorDialect
+    MLIRVectorUtils
     iree::compiler::Codegen::Dialect::Codegen::IR::IREECodegenDialect
     iree::compiler::Codegen::Dialect::VectorExt::IR::IREEVectorExtDialect
     iree::compiler::Dialect::LinalgExt::IR
diff --git a/compiler/src/iree/compiler/Codegen/Interfaces/VectorizableOpInterface.cpp b/compiler/src/iree/compiler/Codegen/Interfaces/VectorizableOpInterface.cpp
index a07d3d8..be4e33e 100644
--- a/compiler/src/iree/compiler/Codegen/Interfaces/VectorizableOpInterface.cpp
+++ b/compiler/src/iree/compiler/Codegen/Interfaces/VectorizableOpInterface.cpp
@@ -20,6 +20,7 @@
 #include "mlir/Dialect/Tensor/IR/Tensor.h"
 #include "mlir/Dialect/UB/IR/UBOps.h"
 #include "mlir/Dialect/Vector/IR/VectorOps.h"
+#include "mlir/Dialect/Vector/Utils/VectorUtils.h"
 #include "mlir/IR/AffineMap.h"
 #include "mlir/IR/IRMapping.h"
 
@@ -1055,9 +1056,17 @@
     if (!tiledOp.hasTensorSemantics()) {
       return false;
     }
-    SmallVector<ShapedType> argTypes = tiledOp.getOperandShapedTypes();
-    return llvm::all_of(argTypes,
-                        [](ShapedType st) { return st.hasStaticShape(); });
+    SmallVector<int64_t> loopRanges;
+    tiledOp.getIterationBounds(loopRanges);
+    // If vector sizes are provided (from tile size analysis or config),
+    // dynamic outer shapes are fine - they'll be masked during vectorization.
+    // However, vector sizes must be >= the static outer dimension sizes.
+    if (!vectorSizes.empty()) {
+      return succeeded(
+          vector::isValidMaskedInputVector(loopRanges, vectorSizes));
+    }
+    // Without vector sizes, require static outer shapes.
+    return ShapedType::isStaticShape(loopRanges);
   }
 
   FailureOr<SmallVector<Value>> vectorize(Operation *op, RewriterBase &rewriter,
@@ -1070,6 +1079,19 @@
     Location loc = tiledOp.getLoc();
 
     SmallVector<ShapedType> argTypes = tiledOp.getOperandShapedTypes();
+    SmallVector<AffineMap> indexingMaps = tiledOp.getIndexingMapsArray();
+
+    // If no vector sizes are provided, use static loop ranges and the inBounds
+    // attribute instead of masking.
+    bool needsMasking = true;
+    if (vectorSizes.empty()) {
+      SmallVector<int64_t> loopRanges;
+      tiledOp.getIterationBounds(loopRanges);
+      assert(ShapedType::isStaticShape(loopRanges) &&
+             "unable to infer vector sizes");
+      vectorSizes = loopRanges;
+      needsMasking = false;
+    }
 
     // Construct the zero padding value for each operand. Ideally, we'd need the
     // InnerTile interface to return the padding value to use. If it is not
@@ -1081,13 +1103,35 @@
               rewriter, loc, rewriter.getZeroAttr(argType.getElementType()));
         });
 
-    SmallVector<Value> newOperands = tiledOp.getOperands();
-    for (auto [operand, type, padValue] :
-         llvm::zip_equal(newOperands, argTypes, padValues)) {
-      operand = vector::createReadOrMaskedRead(
-          rewriter, loc, operand, type.getShape(), padValue,
-          /*useInBoundsInsteadOfMasking=*/true);
+    // Compute the read shape for each operand.
+    SmallVector<SmallVector<int64_t>> readShapes;
+    for (auto [i, argType] : llvm::enumerate(argTypes)) {
+      if (!needsMasking) {
+        readShapes.push_back(llvm::to_vector(argType.getShape()));
+        continue;
+      }
+      // Outer dimensions come from vector sizes via the indexing map, inner
+      // dimensions are static.
+      SmallVector<int64_t> readShape;
+      AffineMap map = indexingMaps[i];
+      for (AffineExpr expr : map.getResults()) {
+        auto dimExpr = cast<AffineDimExpr>(expr);
+        readShape.push_back(vectorSizes[dimExpr.getPosition()]);
+      }
+      ArrayRef<int64_t> innerShape = tiledOp.getOperandInnerShape(i);
+      readShape.append(innerShape.begin(), innerShape.end());
+      readShapes.push_back(std::move(readShape));
     }
+
+    // Read each operand into a vector, with masking if needed.
+    SmallVector<Value> newOperands(tiledOp->getOperands());
+    for (auto [operand, readShape, padValue] :
+         llvm::zip_equal(newOperands, readShapes, padValues)) {
+      operand = vector::createReadOrMaskedRead(
+          rewriter, loc, operand, readShape, padValue,
+          /*useInBoundsInsteadOfMasking=*/!needsMasking);
+    }
+
     auto newTiledOp = IREE::Codegen::InnerTiledOp::create(
         rewriter, loc,
         ValueRange{newOperands}.take_front(tiledOp.getNumInputs()),
@@ -1095,16 +1139,40 @@
         tiledOp.getIndexingMaps(), tiledOp.getIteratorTypes(),
         tiledOp.getKind(), tiledOp.getSemantics());
 
+    // Write results back to tensor, with masking if needed.
+    // TODO: Use createWriteOrMaskedWrite once it is promoted to a public
+    // utility in mlir/Dialect/Vector/Utils/VectorUtils.h.
     auto zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
     SmallVector<Value> results;
-    for (auto [result, tensorAcc] :
-         llvm::zip_equal(newTiledOp.getResults(), tiledOp.getOutputs())) {
-      int64_t rank = cast<RankedTensorType>(tensorAcc.getType()).getRank();
-      auto write = vector::TransferWriteOp::create(
-          rewriter, loc, result, tensorAcc,
-          /*indices=*/SmallVector<Value>(rank, zero),
-          /*inBounds=*/SmallVector<bool>(rank, true));
-      results.push_back(write.getResults().front());
+    unsigned numInputs = tiledOp.getNumInputs();
+    for (auto [i, result, dest] :
+         llvm::enumerate(newTiledOp.getResults(), tiledOp.getOutputs())) {
+      auto destType = cast<ShapedType>(dest.getType());
+      int64_t rank = destType.getRank();
+      SmallVector<Value> indices(rank, zero);
+
+      ArrayRef<int64_t> writeShape = readShapes[numInputs + i];
+      SmallVector<bool> inBounds(rank);
+      for (int64_t d = 0; d < rank; ++d) {
+        inBounds[d] =
+            destType.isStaticDim(d) && destType.getDimSize(d) >= writeShape[d];
+      }
+
+      auto write = vector::TransferWriteOp::create(rewriter, loc, result, dest,
+                                                   indices, inBounds);
+      if (!needsMasking) {
+        results.push_back(write.getResults().front());
+        continue;
+      }
+      auto vecType = cast<VectorType>(result.getType());
+      auto maskType =
+          vecType.cloneWith(/*shape=*/std::nullopt, rewriter.getI1Type());
+      SmallVector<OpFoldResult> mixedSizes =
+          tensor::getMixedSizes(rewriter, loc, dest);
+      Value mask =
+          vector::CreateMaskOp::create(rewriter, loc, maskType, mixedSizes);
+      results.push_back(
+          mlir::vector::maskOperation(rewriter, write, mask)->getResult(0));
     }
     return results;
   }