[Codegen] Support more operations in tile size analysis (#23971)

Add support for `inner_tiled`, `linalg.pack` and `linalg.unpack`
operations to the vector tile size analysis.

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/MaterializeVectorTileSizes.cpp b/compiler/src/iree/compiler/Codegen/Common/MaterializeVectorTileSizes.cpp
index a03d1e5..dc99cbe 100644
--- a/compiler/src/iree/compiler/Codegen/Common/MaterializeVectorTileSizes.cpp
+++ b/compiler/src/iree/compiler/Codegen/Common/MaterializeVectorTileSizes.cpp
@@ -6,6 +6,7 @@
 
 #include "iree/compiler/Codegen/Common/Passes.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h"
+#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenOps.h"
 #include "iree/compiler/Codegen/Dialect/VectorExt/IR/VectorExtDialect.h"
 
 #include "llvm/Support/DebugLog.h"
@@ -157,6 +158,73 @@
     return result;
   }
 
+  /// Map tile sizes from pack source space (rank N) to pack dest space
+  /// (rank N+K). Divides packed dims by inner tile sizes, applies
+  /// outer_dims_perm, and appends inner tile sizes as new dimensions.
+  /// The static inner tiles are assumed to not be dynamic values.
+  TileSizes mapPackSourceToDest(ArrayRef<int64_t> innerDimsPos,
+                                ArrayRef<int64_t> staticInnerTiles,
+                                ArrayRef<int64_t> outerDimsPerm) const {
+    if (empty()) {
+      return {};
+    }
+    TileSizes result = *this;
+    for (auto [dimPos, tileSize] :
+         llvm::zip_equal(innerDimsPos, staticInnerTiles)) {
+      assert(ShapedType::isStatic(tileSize) &&
+             "expecting static inner tile size");
+      if (result.dims[dimPos] == kUninitialized ||
+          result.dims[dimPos] == kOverdefined) {
+        continue;
+      }
+      result.dims[dimPos] =
+          llvm::divideCeilSigned(result.dims[dimPos], tileSize);
+    }
+    if (!outerDimsPerm.empty()) {
+      applyPermutationToVector(result.dims, outerDimsPerm);
+    }
+    llvm::append_range(result.dims, staticInnerTiles);
+    return result;
+  }
+
+  /// Map tile sizes from pack dest space (rank N+K) back to source space
+  /// (rank N). Truncates to outer dims, applies inverse permutation, and
+  /// multiplies packed dims by inner tile sizes.
+  TileSizes mapPackDestToSource(unsigned sourceRank,
+                                ArrayRef<int64_t> innerDimsPos,
+                                ArrayRef<int64_t> staticInnerTiles,
+                                ArrayRef<int64_t> outerDimsPerm) const {
+    if (empty()) {
+      return {};
+    }
+    assert(sourceRank <= rank() && "sourceRank exceeds dest tile size rank");
+    TileSizes result(sourceRank);
+    for (unsigned i = 0; i < sourceRank; ++i) {
+      result.dims[i] = dims[i];
+    }
+    if (!outerDimsPerm.empty()) {
+      applyPermutationToVector(result.dims,
+                               invertPermutationVector(outerDimsPerm));
+    }
+    for (auto [dimPos, tileSize] :
+         llvm::zip_equal(innerDimsPos, staticInnerTiles)) {
+      if (result.dims[dimPos] == kUninitialized ||
+          result.dims[dimPos] == kOverdefined) {
+        // Uninitialized and overdefined are preserved.
+        continue;
+      }
+      result.dims[dimPos] *= tileSize;
+    }
+    return result;
+  }
+
+  /// Append dimensions from `suffix` to produce a higher-rank TileSizes.
+  TileSizes append(ArrayRef<int64_t> suffix) const {
+    SmallVector<int64_t> fullDims(dims);
+    fullDims.append(suffix.begin(), suffix.end());
+    return TileSizes(fullDims);
+  }
+
   /// Lattice join: per-dimension merge. Uninitialized is identity.
   static TileSizes join(const TileSizes &lhs, const TileSizes &rhs) {
     TileSizes result = lhs;
@@ -304,6 +372,67 @@
       return success();
     }
 
+    // InnerTiledOp: Propagate the outer dimensions through the indexing maps
+    // and then append the static inner dimensions.
+    if (auto innerTiledOp = dyn_cast<IREE::Codegen::InnerTiledOp>(op)) {
+      SmallVector<AffineMap> indexingMaps = innerTiledOp.getIndexingMapsArray();
+      unsigned numLoops = indexingMaps[0].getNumDims();
+      TileSizes iterTileSizes(numLoops);
+
+      // Merge tile sizes from all operands into iteration space.
+      // mapToIterationSpace reads only the first getNumResults() elements
+      // from the operand TileSizes, naturally skipping inner dims.
+      for (OpOperand &operand : op->getOpOperands()) {
+        TileSizes opTileSizes = getTileSizesFor(
+            operand.get(), operands[operand.getOperandNumber()]);
+        AffineMap map = indexingMaps[operand.getOperandNumber()];
+        iterTileSizes.merge(opTileSizes.mapToIterationSpace(map));
+      }
+
+      // Propagate to results. Results correspond to outputs.
+      unsigned numInputs = innerTiledOp.getNumInputs();
+      for (unsigned i = 0; i < innerTiledOp.getNumOutputs(); ++i) {
+        AffineMap map = indexingMaps[numInputs + i];
+        TileSizes outerTileSizes = iterTileSizes.mapFromIterationSpace(map);
+        if (outerTileSizes.empty()) {
+          continue;
+        }
+        // Extend with static inner dims to match full operand rank.
+        ArrayRef<int64_t> innerShape =
+            innerTiledOp.getOperandInnerShape(numInputs + i);
+        TileSizes fullTileSizes = outerTileSizes.append(innerShape);
+        propagateIfChanged(results[i], results[i]->join(fullTileSizes));
+      }
+      return success();
+    }
+
+    // Pack ops: map source tile sizes to dest space.
+    if (auto packOp = dyn_cast<linalg::PackOp>(op)) {
+      if (ShapedType::isDynamicShape(packOp.getStaticInnerTiles())) {
+        return success();
+      }
+      TileSizes srcTileSizes = getTileSizesFor(packOp.getSource(), operands[0]);
+      TileSizes destTileSizes = srcTileSizes.mapPackSourceToDest(
+          packOp.getInnerDimsPos(), packOp.getStaticInnerTiles(),
+          packOp.getOuterDimsPerm());
+      propagateIfChanged(results[0], results[0]->join(destTileSizes));
+      return success();
+    }
+
+    // Unpack ops: map source (packed) tile sizes to dest (unpacked) space.
+    if (auto unpackOp = dyn_cast<linalg::UnPackOp>(op)) {
+      if (ShapedType::isDynamicShape(unpackOp.getStaticInnerTiles())) {
+        return success();
+      }
+      TileSizes srcTileSizes =
+          getTileSizesFor(unpackOp.getSource(), operands[0]);
+      TileSizes destTileSizes = srcTileSizes.mapPackDestToSource(
+          unpackOp.getDestRank(), unpackOp.getInnerDimsPos(),
+          unpackOp.getStaticInnerTiles(), unpackOp.getOuterDimsPerm());
+      propagateIfChanged(results[0], results[0]->join(destTileSizes));
+      return success();
+    }
+
     return success();
   }
 };
@@ -365,6 +494,83 @@
       return success();
     }
 
+    // InnerTiledOp: propagate backward through indexing maps.
+    if (auto innerTiledOp = dyn_cast<IREE::Codegen::InnerTiledOp>(op)) {
+      SmallVector<AffineMap> indexingMaps = innerTiledOp.getIndexingMapsArray();
+      unsigned numLoops = indexingMaps[0].getNumDims();
+      unsigned numInputs = innerTiledOp.getNumInputs();
+      TileSizes iterTileSizes(numLoops);
+
+      // Gather from results (full-rank lattice, mapToIterationSpace skips
+      // inner dims naturally).
+      for (auto [result, resultLattice] :
+           llvm::zip_equal(op->getResults(), results)) {
+        unsigned resultIdx = cast<OpResult>(result).getResultNumber();
+        AffineMap map = indexingMaps[numInputs + resultIdx];
+        TileSizes tileSizes = getTileSizesFor(result, resultLattice);
+        iterTileSizes.merge(tileSizes.mapToIterationSpace(map));
+      }
+
+      // Gather from operands.
+      for (OpOperand &operand : op->getOpOperands()) {
+        TileSizes opTileSizes = getTileSizesFor(
+            operand.get(), operands[operand.getOperandNumber()]);
+        AffineMap map = indexingMaps[operand.getOperandNumber()];
+        iterTileSizes.merge(opTileSizes.mapToIterationSpace(map));
+      }
+
+      // Propagate back to each operand. Extend outer-rank result with inner
+      // dims to match full operand rank.
+      for (OpOperand &operand : op->getOpOperands()) {
+        unsigned idx = operand.getOperandNumber();
+        AffineMap map = indexingMaps[idx];
+        TileSizes outerTileSizes = iterTileSizes.mapFromIterationSpace(map);
+        if (outerTileSizes.empty()) {
+          continue;
+        }
+        ArrayRef<int64_t> innerShape = innerTiledOp.getOperandInnerShape(idx);
+        TileSizes fullTileSizes = outerTileSizes.append(innerShape);
+        TileSizeLattice *opLattice = operands[idx];
+        propagateIfChanged(opLattice, opLattice->meet(fullTileSizes));
+      }
+      return success();
+    }
+
+    // Pack ops: result tile sizes → source tile sizes (backward).
+    if (auto packOp = dyn_cast<linalg::PackOp>(op)) {
+      if (ShapedType::isDynamicShape(packOp.getStaticInnerTiles())) {
+        return success();
+      }
+      if (packOp.getPaddingValue()) {
+        // We do not backward propagate for pack with padding, as it would
+        // potentially propagate too large tile sizes.
+        return success();
+      }
+      TileSizes resultTileSizes =
+          getTileSizesFor(packOp->getResult(0), results[0]);
+      TileSizes srcTileSizes = resultTileSizes.mapPackDestToSource(
+          packOp.getSourceRank(), packOp.getInnerDimsPos(),
+          packOp.getStaticInnerTiles(), packOp.getOuterDimsPerm());
+      TileSizeLattice *srcLattice = operands[0];
+      propagateIfChanged(srcLattice, srcLattice->meet(srcTileSizes));
+      return success();
+    }
+
+    // Unpack ops: result tile sizes → source tile sizes (backward).
+    if (auto unpackOp = dyn_cast<linalg::UnPackOp>(op)) {
+      if (ShapedType::isDynamicShape(unpackOp.getStaticInnerTiles())) {
+        return success();
+      }
+      TileSizes resultTileSizes =
+          getTileSizesFor(unpackOp->getResult(0), results[0]);
+      TileSizes srcTileSizes = resultTileSizes.mapPackSourceToDest(
+          unpackOp.getInnerDimsPos(), unpackOp.getStaticInnerTiles(),
+          unpackOp.getOuterDimsPerm());
+      TileSizeLattice *srcLattice = operands[0];
+      propagateIfChanged(srcLattice, srcLattice->meet(srcTileSizes));
+      return success();
+    }
+
     return success();
   }
 
@@ -383,20 +589,20 @@
 // Result querying
 //===----------------------------------------------------------------------===//
 
-/// Gather tile sizes into the iteration space of a linalg op by looking up each
+/// Gather tile sizes into the iteration space of an op by looking up each
 /// operand's lattice state in the solver.
-static TileSizes getIterationSpaceTileSizes(linalg::LinalgOp linalgOp,
+static TileSizes getIterationSpaceTileSizes(Operation *op, unsigned numLoops,
+                                            ArrayRef<AffineMap> indexingMaps,
                                             const DataFlowSolver &solver) {
-  unsigned numLoops = linalgOp.getNumLoops();
   TileSizes iterTileSizes(numLoops);
-  for (OpOperand &operand : linalgOp->getOpOperands()) {
+  for (OpOperand &operand : op->getOpOperands()) {
     Value val = operand.get();
     const TileSizeLattice *lattice = solver.lookupState<TileSizeLattice>(val);
     TileSizes tileSize = getTileSizesFor(val, lattice);
     if (tileSize.empty()) {
       continue;
     }
-    AffineMap map = linalgOp.getMatchingIndexingMap(&operand);
+    AffineMap map = indexingMaps[operand.getOperandNumber()];
     assert(map.getNumDims() == numLoops);
     iterTileSizes.merge(tileSize.mapToIterationSpace(map));
   }
@@ -426,23 +632,63 @@
       return signalPassFailure();
     }
 
-    funcOp->walk([&](linalg::LinalgOp linalgOp) {
-      TileSizes tileSizes = getIterationSpaceTileSizes(linalgOp, solver);
+    auto materialize = [](Operation *op, TileSizes tileSizes) -> LogicalResult {
       if (tileSizes.isOverdefined()) {
-        linalgOp.emitOpError()
+        op->emitOpError()
             << "tile size analysis did not determine a valid tile size";
-        return;
+        return failure();
       }
       if (!tileSizes.isDefined()) {
-        LDBG() << "Analysis did not determine tile size for " << *linalgOp;
-        return;
+        LDBG() << "Analysis did not determine tile size for " << *op;
+        return success();
       }
-      assert(tileSizes.rank() == linalgOp.getNumLoops());
-
-      linalgOp->setAttr(
+      op->setAttr(
           kVectorTileSizesAttrName,
-          DenseI64ArrayAttr::get(linalgOp->getContext(), tileSizes.getDims()));
+          DenseI64ArrayAttr::get(op->getContext(), tileSizes.getDims()));
+      return success();
+    };
+
+    auto result = funcOp->walk([&](Operation *op) -> WalkResult {
+      if (isa<linalg::PackOp>(op) || isa<linalg::UnPackOp>(op)) {
+        // linalg.pack and linalg.unpack have an unpacked (rank N) and a packed
+        // (rank N + K) domain. linalg.pack converts from the unpacked domain to
+        // the packed domain, linalg.unpack works the other way round.
+        // Vectorization of the operations expects vector sizes in the packed
+        // domain. After analysis, these are available on operand of linalg.pack
+        // and the result of linalg.unpack, respectively.
+        Value packedVal;
+        if (auto packOp = dyn_cast<linalg::PackOp>(op)) {
+          packedVal = packOp.getResult();
+        } else if (auto unpackOp = dyn_cast<linalg::UnPackOp>(op)) {
+          packedVal = unpackOp.getSource();
+        }
+        const TileSizeLattice *lattice =
+            solver.lookupState<TileSizeLattice>(packedVal);
+        TileSizes tileSizes = getTileSizesFor(packedVal, lattice);
+        return WalkResult(materialize(op, tileSizes));
+      }
+      if (auto linalgOp = dyn_cast<linalg::LinalgOp>(op)) {
+        SmallVector<AffineMap> indexingMaps = linalgOp.getIndexingMapsArray();
+        TileSizes tileSizes = getIterationSpaceTileSizes(
+            op, linalgOp.getNumLoops(), indexingMaps, solver);
+        assert(!tileSizes.isDefined() ||
+               tileSizes.rank() == linalgOp.getNumLoops());
+        return WalkResult(materialize(op, tileSizes));
+      }
+
+      if (auto innerTiledOp = dyn_cast<IREE::Codegen::InnerTiledOp>(op)) {
+        SmallVector<AffineMap> indexingMaps =
+            innerTiledOp.getIndexingMapsArray();
+        unsigned numLoops = indexingMaps[0].getNumDims();
+        TileSizes tileSizes =
+            getIterationSpaceTileSizes(op, numLoops, indexingMaps, solver);
+        return WalkResult(materialize(op, tileSizes));
+      }
+      return WalkResult::advance();
     });
+    if (result.wasInterrupted()) {
+      signalPassFailure();
+    }
   }
 };
 
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/materialize_vector_tile_sizes.mlir b/compiler/src/iree/compiler/Codegen/Common/test/materialize_vector_tile_sizes.mlir
index 514546b..98f3d33 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/materialize_vector_tile_sizes.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/test/materialize_vector_tile_sizes.mlir
@@ -247,36 +247,288 @@
 
 // -----
 
-// Conflicting tile sizes: two to_layout ops with different tile sizes for the
-// same dimension feed into one generic. The dimension becomes overdefined, so
-// no tile size attribute should be materialized.
+#layout_pack = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1], batch_tile = [1, 32], outer_tile = [1, 1],
+  thread_tile = [1, 1], element_tile = [8, 8],
+  subgroup_strides = [0, 0], thread_strides = [0, 0]>
 
-#layout_32 = #iree_vector_ext.nested_layout<
-  subgroup_tile = [1], batch_tile = [4], outer_tile = [1],
-  thread_tile = [1], element_tile = [8],
-  subgroup_strides = [0], thread_strides = [0]>
+// CHECK-LABEL: @pack_forward_propagation
+func.func @pack_forward_propagation(%arg0: tensor<8x256xf16>) -> tensor<8x8x32xf16> {
+  %laid_out = iree_vector_ext.to_layout %arg0 to layout(#layout_pack) : tensor<8x256xf16>
+  %empty_gen = tensor.empty() : tensor<8x256xf16>
+  // CHECK: linalg.generic
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 256>
+  %gen = linalg.generic {
+    indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>,
+                     affine_map<(d0, d1) -> (d0, d1)>],
+    iterator_types = ["parallel", "parallel"]
+  } ins(%laid_out : tensor<8x256xf16>) outs(%empty_gen : tensor<8x256xf16>) {
+  ^bb0(%in: f16, %out: f16):
+    %neg = arith.negf %in : f16
+    linalg.yield %neg : f16
+  } -> tensor<8x256xf16>
+  %empty_pack = tensor.empty() : tensor<8x8x32xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %pack = linalg.pack %gen inner_dims_pos = [1] inner_tiles = [32]
+    into %empty_pack : tensor<8x256xf16> -> tensor<8x8x32xf16>
+  return %pack : tensor<8x8x32xf16>
+}
 
-#layout_64 = #iree_vector_ext.nested_layout<
-  subgroup_tile = [1], batch_tile = [8], outer_tile = [1],
-  thread_tile = [1], element_tile = [8],
-  subgroup_strides = [0], thread_strides = [0]>
+// -----
 
-// CHECK-LABEL: @conflicting_tile_sizes
-func.func @conflicting_tile_sizes(%arg0: tensor<64xf16>, %arg1: tensor<64xf16>) -> tensor<64xf16> {
-  %a = iree_vector_ext.to_layout %arg0 to layout(#layout_32) : tensor<64xf16>
-  %b = iree_vector_ext.to_layout %arg1 to layout(#layout_64) : tensor<64xf16>
-  %empty = tensor.empty() : tensor<64xf16>
+#layout_pack_perm = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 32], outer_tile = [1, 1, 1],
+  thread_tile = [1, 1, 1], element_tile = [4, 16, 8],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 0, 0]>
+
+// CHECK-LABEL: @pack_forward_propagation_outer_perm
+func.func @pack_forward_propagation_outer_perm(%arg0: tensor<4x16x256xf16>) -> tensor<8x4x16x32xf16> {
+  %laid_out = iree_vector_ext.to_layout %arg0 to layout(#layout_pack_perm) : tensor<4x16x256xf16>
+  %empty_gen = tensor.empty() : tensor<4x16x256xf16>
+  // CHECK: linalg.generic
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 4, 16, 256>
+  %gen = linalg.generic {
+    indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>,
+                     affine_map<(d0, d1, d2) -> (d0, d1, d2)>],
+    iterator_types = ["parallel", "parallel", "parallel"]
+  } ins(%laid_out : tensor<4x16x256xf16>) outs(%empty_gen : tensor<4x16x256xf16>) {
+  ^bb0(%in: f16, %out: f16):
+    %neg = arith.negf %in : f16
+    linalg.yield %neg : f16
+  } -> tensor<4x16x256xf16>
+  %empty_pack = tensor.empty() : tensor<8x4x16x32xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 4, 16, 32>
+  %pack = linalg.pack %gen outer_dims_perm = [2, 0, 1] inner_dims_pos = [2] inner_tiles = [32]
+    into %empty_pack : tensor<4x16x256xf16> -> tensor<8x4x16x32xf16>
+  return %pack : tensor<8x4x16x32xf16>
+}
+
+// -----
+
+// Pack with padding value: backward propagation through the pack should be
+// blocked, so the generic producing the pack source should not get tile sizes.
+
+#layout_pack_pad = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 1], outer_tile = [1, 1, 1],
+  thread_tile = [1, 1, 1], element_tile = [8, 8, 32],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 0, 0]>
+
+// CHECK-LABEL: @pack_no_backward_propagation_with_padding
+func.func @pack_no_backward_propagation_with_padding(
+    %arg0: tensor<8x256xf16>) -> tensor<8x8x32xf16> {
+  %cst = arith.constant 0.0 : f16
+  %empty_gen = tensor.empty() : tensor<8x256xf16>
   // CHECK: linalg.generic
   // CHECK-NOT: iree_codegen.vector_tile_sizes
-  %result = linalg.generic {
-    indexing_maps = [affine_map<(d0) -> (d0)>,
-                     affine_map<(d0) -> (d0)>,
-                     affine_map<(d0) -> (d0)>],
-    iterator_types = ["parallel"]
-  } ins(%a, %b : tensor<64xf16>, tensor<64xf16>) outs(%empty : tensor<64xf16>) {
-  ^bb0(%in0: f16, %in1: f16, %out: f16):
-    %add = arith.addf %in0, %in1 : f16
-    linalg.yield %add : f16
-  } -> tensor<64xf16>
-  return %result : tensor<64xf16>
+  %gen = linalg.generic {
+    indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>,
+                     affine_map<(d0, d1) -> (d0, d1)>],
+    iterator_types = ["parallel", "parallel"]
+  } ins(%arg0 : tensor<8x256xf16>) outs(%empty_gen : tensor<8x256xf16>) {
+  ^bb0(%in: f16, %out: f16):
+    %neg = arith.negf %in : f16
+    linalg.yield %neg : f16
+  } -> tensor<8x256xf16>
+  %empty_pack = tensor.empty() : tensor<8x8x32xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %pack = linalg.pack %gen padding_value(%cst : f16)
+    inner_dims_pos = [1] inner_tiles = [32]
+    into %empty_pack : tensor<8x256xf16> -> tensor<8x8x32xf16>
+  %result = iree_vector_ext.to_layout %pack to layout(#layout_pack_pad) : tensor<8x8x32xf16>
+  return %result : tensor<8x8x32xf16>
+}
+
+// -----
+
+#layout_unpack = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1], batch_tile = [1, 32], outer_tile = [1, 1],
+  thread_tile = [1, 1], element_tile = [8, 8],
+  subgroup_strides = [0, 0], thread_strides = [0, 0]>
+
+// CHECK-LABEL: @unpack_backward_propagation
+func.func @unpack_backward_propagation(%arg0: tensor<8x8x32xf16>) -> tensor<8x256xf16> {
+  %empty_unpack = tensor.empty() : tensor<8x256xf16>
+  // CHECK: linalg.unpack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %unpack = linalg.unpack %arg0 inner_dims_pos = [1] inner_tiles = [32]
+    into %empty_unpack : tensor<8x8x32xf16> -> tensor<8x256xf16>
+  %empty_gen = tensor.empty() : tensor<8x256xf16>
+  // CHECK: linalg.generic
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 256>
+  %gen = linalg.generic {
+    indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>,
+                     affine_map<(d0, d1) -> (d0, d1)>],
+    iterator_types = ["parallel", "parallel"]
+  } ins(%unpack : tensor<8x256xf16>) outs(%empty_gen : tensor<8x256xf16>) {
+  ^bb0(%in: f16, %out: f16):
+    %neg = arith.negf %in : f16
+    linalg.yield %neg : f16
+  } -> tensor<8x256xf16>
+  %result = iree_vector_ext.to_layout %gen to layout(#layout_unpack) : tensor<8x256xf16>
+  return %result : tensor<8x256xf16>
+}
+
+// -----
+
+#layout_chain = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1], batch_tile = [1, 32], outer_tile = [1, 1],
+  thread_tile = [1, 1], element_tile = [8, 8],
+  subgroup_strides = [0, 0], thread_strides = [0, 0]>
+
+// CHECK-LABEL: @pack_unpack_chain
+func.func @pack_unpack_chain(%arg0: tensor<8x256xf16>) -> tensor<8x256xf16> {
+  %laid_out = iree_vector_ext.to_layout %arg0 to layout(#layout_chain) : tensor<8x256xf16>
+  %empty_pack = tensor.empty() : tensor<8x8x32xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %pack = linalg.pack %laid_out inner_dims_pos = [1] inner_tiles = [32]
+    into %empty_pack : tensor<8x256xf16> -> tensor<8x8x32xf16>
+  %empty_gen = tensor.empty() : tensor<8x8x32xf16>
+  // CHECK: linalg.generic
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %gen = linalg.generic {
+    indexing_maps = [affine_map<(d0, d1, d2) -> (d0, d1, d2)>,
+                     affine_map<(d0, d1, d2) -> (d0, d1, d2)>],
+    iterator_types = ["parallel", "parallel", "parallel"]
+  } ins(%pack : tensor<8x8x32xf16>) outs(%empty_gen : tensor<8x8x32xf16>) {
+  ^bb0(%in: f16, %out: f16):
+    %neg = arith.negf %in : f16
+    linalg.yield %neg : f16
+  } -> tensor<8x8x32xf16>
+  %empty_unpack = tensor.empty() : tensor<8x256xf16>
+  // CHECK: linalg.unpack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 8, 8, 32>
+  %unpack = linalg.unpack %gen inner_dims_pos = [1] inner_tiles = [32]
+    into %empty_unpack : tensor<8x8x32xf16> -> tensor<8x256xf16>
+  return %unpack : tensor<8x256xf16>
+}
+
+// -----
+
+#layout_pv_lhs = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 16, 4], element_tile = [1, 1, 4],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 1, 16]>
+
+#layout_pv_rhs = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 4, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 4, 16], element_tile = [1, 4, 1],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 16, 1]>
+
+#layout_pv_acc = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 4, 16], element_tile = [1, 4, 1],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 16, 1]>
+
+// CHECK-LABEL: @inner_tiled_attention_pv
+func.func @inner_tiled_attention_pv(
+    %lhs: tensor<1x16x64xf16>,
+    %rhs: tensor<1x64x64xf16>,
+    %acc: tensor<1x16x64xf32>) -> tensor<1x16x64xf32> {
+  %lhs_l = iree_vector_ext.to_layout %lhs to layout(#layout_pv_lhs) : tensor<1x16x64xf16>
+  %rhs_l = iree_vector_ext.to_layout %rhs to layout(#layout_pv_rhs) : tensor<1x64x64xf16>
+  %acc_l = iree_vector_ext.to_layout %acc to layout(#layout_pv_acc) : tensor<1x16x64xf32>
+  %empty_lhs = tensor.empty() : tensor<1x1x4x16x16xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %pack_lhs = linalg.pack %lhs_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_lhs : tensor<1x16x64xf16> -> tensor<1x1x4x16x16xf16>
+  %empty_rhs = tensor.empty() : tensor<1x4x4x16x16xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 4, 4, 16, 16>
+  %pack_rhs = linalg.pack %rhs_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_rhs : tensor<1x64x64xf16> -> tensor<1x4x4x16x16xf16>
+  %empty_acc = tensor.empty() : tensor<1x1x4x16x16xf32>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %pack_acc = linalg.pack %acc_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_acc : tensor<1x16x64xf32> -> tensor<1x1x4x16x16xf32>
+  // CHECK: iree_codegen.inner_tiled
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 4>
+  %result_packed = iree_codegen.inner_tiled ins(%pack_lhs, %pack_rhs) outs(%pack_acc) {
+    indexing_maps = [affine_map<(d0, d1, d2, d3) -> (d0, d1, d2)>,
+                     affine_map<(d0, d1, d2, d3) -> (d0, d2, d3)>,
+                     affine_map<(d0, d1, d2, d3) -> (d0, d1, d3)>],
+    iterator_types = [#linalg.iterator_type<parallel>,
+                      #linalg.iterator_type<parallel>,
+                      #linalg.iterator_type<reduction>,
+                      #linalg.iterator_type<parallel>],
+    kind = #iree_gpu.mma_layout<MFMA_F32_16x16x16_F16>,
+    semantics = #iree_gpu.mma_semantics<distributed = false, opaque = true>
+  } : tensor<1x1x4x16x16xf16>, tensor<1x4x4x16x16xf16> into tensor<1x1x4x16x16xf32>
+  %empty_result = tensor.empty() : tensor<1x16x64xf32>
+  // CHECK: linalg.unpack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %result = linalg.unpack %result_packed inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_result : tensor<1x1x4x16x16xf32> -> tensor<1x16x64xf32>
+  return %result : tensor<1x16x64xf32>
+}
+
+// -----
+
+#layout_pv_lhs = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 16, 4], element_tile = [1, 1, 4],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 1, 16]>
+
+#layout_pv_rhs = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 4, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 4, 16], element_tile = [1, 4, 1],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 16, 1]>
+
+#layout_pv_acc = #iree_vector_ext.nested_layout<
+  subgroup_tile = [1, 1, 1], batch_tile = [1, 1, 4], outer_tile = [1, 1, 1],
+  thread_tile = [1, 4, 16], element_tile = [1, 4, 1],
+  subgroup_strides = [0, 0, 0], thread_strides = [0, 16, 1]>
+
+// CHECK-LABEL: @inner_tiled_dynamic
+func.func @inner_tiled_dynamic(
+    %lhs: tensor<1x?x?xf16>,
+    %rhs: tensor<1x?x?xf16>,
+    %acc: tensor<1x?x?xf32>) -> tensor<1x?x?xf32> {
+  %lhs_l = iree_vector_ext.to_layout %lhs to layout(#layout_pv_lhs) : tensor<1x?x?xf16>
+  %rhs_l = iree_vector_ext.to_layout %rhs to layout(#layout_pv_rhs) : tensor<1x?x?xf16>
+  %acc_l = iree_vector_ext.to_layout %acc to layout(#layout_pv_acc) : tensor<1x?x?xf32>
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %m = tensor.dim %lhs, %c0 : tensor<1x?x?xf16>
+  %n = tensor.dim %rhs, %c0 : tensor<1x?x?xf16>
+  %k = tensor.dim %lhs, %c1 : tensor<1x?x?xf16>
+  %empty_lhs = tensor.empty(%m, %k) : tensor<1x?x?x16x16xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %pack_lhs = linalg.pack %lhs_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_lhs : tensor<1x?x?xf16> -> tensor<1x?x?x16x16xf16>
+  %empty_rhs = tensor.empty(%n, %k) : tensor<1x?x?x16x16xf16>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 4, 4, 16, 16>
+  %pack_rhs = linalg.pack %rhs_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_rhs : tensor<1x?x?xf16> -> tensor<1x?x?x16x16xf16>
+  %empty_acc = tensor.empty(%m, %n) : tensor<1x?x?x16x16xf32>
+  // CHECK: linalg.pack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %pack_acc = linalg.pack %acc_l inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_acc : tensor<1x?x?xf32> -> tensor<1x?x?x16x16xf32>
+  // CHECK: iree_codegen.inner_tiled
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 4>
+  %result_packed = iree_codegen.inner_tiled ins(%pack_lhs, %pack_rhs) outs(%pack_acc) {
+    indexing_maps = [affine_map<(d0, d1, d2, d3) -> (d0, d1, d2)>,
+                     affine_map<(d0, d1, d2, d3) -> (d0, d2, d3)>,
+                     affine_map<(d0, d1, d2, d3) -> (d0, d1, d3)>],
+    iterator_types = [#linalg.iterator_type<parallel>,
+                      #linalg.iterator_type<parallel>,
+                      #linalg.iterator_type<reduction>,
+                      #linalg.iterator_type<parallel>],
+    kind = #iree_gpu.mma_layout<MFMA_F32_16x16x16_F16>,
+    semantics = #iree_gpu.mma_semantics<distributed = false, opaque = true>
+  } : tensor<1x?x?x16x16xf16>, tensor<1x?x?x16x16xf16> into tensor<1x?x?x16x16xf32>
+  %empty_result = tensor.empty(%m, %n) : tensor<1x?x?xf32>
+  // CHECK: linalg.unpack
+  // CHECK-SAME: iree_codegen.vector_tile_sizes = array<i64: 1, 1, 4, 16, 16>
+  %result = linalg.unpack %result_packed inner_dims_pos = [1, 2] inner_tiles = [16, 16]
+    into %empty_result : tensor<1x?x?x16x16xf32> -> tensor<1x?x?xf32>
+  return %result : tensor<1x?x?xf32>
 }