[LLVMCPU] set inner-tile alignment hints on scalable pack ops (#24807)

During tile size selection, global tile sizes and mappings to each
operation are available. Here, each scalable `linalg.pack` inner tile is
compared against the loop tile size chosen for that dimension and
corresponding inner tile alignment hints are set. This is done before
the loop tile sizes for the pack op are translated into the packed outer
domain.

On the other hand, a `linalg.pack` whose producer is a `linalg.unpack`
is pruned by MultiLoweringConfigGenerator and never receives a lowering
config, so it is handled separately by reading the producer unpack's
per-level tile sizes and comparing them against the pack's scalable
inner tiles.

Assisted-by: Claude Code

---

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>

---------

Signed-off-by: Ege Beysel <beyselege@gmail.com>
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
index ad3f892..bc93f7b 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
@@ -6,6 +6,7 @@
 
 #include "iree/compiler/Codegen/LLVMCPU/KernelDispatch.h"
 
+#include "iree/compiler/Codegen/Common/TileAndFuseUtils.h"
 #include "iree/compiler/Codegen/Dialect/CPU/IR/IREECPUTypes.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h"
 #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenEnums.h"
@@ -28,6 +29,7 @@
 #include "iree/compiler/Dialect/TensorExt/IR/TensorExtOps.h"
 #include "llvm/ADT/EquivalenceClasses.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/STLForwardCompat.h"
 #include "llvm/ADT/SmallVectorExtras.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/Support/CommandLine.h"
@@ -3247,6 +3249,23 @@
   return failure();
 }
 
+/// Returns the `InnerTileAlignment` implied by a loop tile size relative to a
+/// scalable pack/unpack inner tile whose vscale multiplier is `innerBase`.
+static mlir::InnerTileAlignment
+getScalableInnerTileAlignment(int64_t loopTile, bool loopScalable,
+                              int64_t innerBase) {
+  if (innerBase <= 0 || loopTile <= 0 || !loopScalable) {
+    return mlir::InnerTileAlignment::Unknown;
+  }
+  if (loopTile == innerBase) {
+    return mlir::InnerTileAlignment::Equal;
+  }
+  if (loopTile % innerBase == 0) {
+    return mlir::InnerTileAlignment::Multiple;
+  }
+  return mlir::InnerTileAlignment::Unknown;
+}
+
 /// Transforms tiling sizes from the unpacked domain to the packed domain
 /// for a `PackOp` by undoing the scaling for inner dimensions and applying
 /// outer dimension permutations.
@@ -3785,6 +3804,40 @@
   }
 }
 
+/// Captures the inner-tile alignment of a scalable `linalg.pack` at tiling
+/// `level` from its (unpacked-domain) loop tile sizes.
+static void capturePackInnerTileAlignment(
+    linalg::PackOp packOp, const SizesAndScalableFlags &packScalableTilesFlags,
+    IREE::CPU::TilingLevel level, ArrayRef<int64_t> tileSizes,
+    ArrayRef<bool> scalableFlags,
+    SmallVectorImpl<std::pair<IREE::CPU::TilingLevel, SmallVector<int64_t>>>
+        &perLevel) {
+  // TODO(egebeysel): wire in distribution tile size alignment logic.
+  if (level == IREE::CPU::TilingLevel::DistributionTiles) {
+    return;
+  }
+  SmallVector<int64_t> alignments(
+      packOp.getSourceRank(),
+      llvm::to_underlying(mlir::InnerTileAlignment::Unknown));
+  bool any = false;
+  for (auto [i, pos] : llvm::enumerate(packOp.getInnerDimsPos())) {
+    // Only scalable inner tiles need a hint; static ones are resolved by static
+    // shape inference downstream.
+    if (!packScalableTilesFlags.second[i] || pos >= tileSizes.size()) {
+      continue;
+    }
+    mlir::InnerTileAlignment kind = getScalableInnerTileAlignment(
+        tileSizes[pos], scalableFlags[pos], packScalableTilesFlags.first[i]);
+    alignments[pos] = llvm::to_underlying(kind);
+    if (kind != mlir::InnerTileAlignment::Unknown) {
+      any = true;
+    }
+  }
+  if (any) {
+    perLevel.emplace_back(level, std::move(alignments));
+  }
+}
+
 void MultiLoweringConfigGenerator::setNewTilingConfigs() {
   SmallVector<IREE::CPU::TilingLevel> tilingLevels;
   tilingLevels.reserve(globalTileSizes.size());
@@ -3798,6 +3851,8 @@
         cast<TilingInterface>(op).getLoopIteratorTypes();
     int numLoops = iterTypes.size();
     SmallVector<IREE::CPU::LoweringConfigLevelInfo> newTilingInfo;
+    SmallVector<std::pair<IREE::CPU::TilingLevel, SmallVector<int64_t>>>
+        perLevelAlignments;
     // Collect new tiling info.
     for (IREE::CPU::TilingLevel level : tilingLevels) {
       SmallVector<int64_t> tileSizes(numLoops, 0);
@@ -3826,6 +3881,15 @@
       }
 
       if (auto packOp = dyn_cast<linalg::PackOp>(op)) {
+        // Capture this level's inner tile alignment for the pack's scalable
+        // inner tiles from the loop tile sizes while they are still in the
+        // unpacked domain.
+        if (auto packScalableTilesFlags =
+                getScalableTileSizesAndFlags(packOp.getMixedTiles())) {
+          capturePackInnerTileAlignment(packOp, *packScalableTilesFlags, level,
+                                        tileSizes, scalableFlags,
+                                        perLevelAlignments);
+        }
         // `MultiLoweringConfigGenerator` propagates tiling on the
         // unpacked dimensions, while for a pack operation, `LoweringConfig`
         // defines tiling on the packed inner dimensions. Therefore, use
@@ -3853,6 +3917,9 @@
         getNewLoweringConfig(rootOperation->getContext(), newTilingInfo,
                              /*setDistributionConfig=*/op == rootOperation);
     setLoweringConfig(op, config);
+    if (!perLevelAlignments.empty()) {
+      IREE::CPU::InnerTileAlignmentsAttr::setOnOp(op, perLevelAlignments);
+    }
   }
 }
 
@@ -4121,6 +4188,53 @@
   return setTranslationInfo(entryPointFn, translationInfo);
 }
 
+/// For a `linalg.pack` whose producer is a `linalg.unpack`, no lowering config
+/// is assigned (see `shouldSetLoweringConfig`), so we cannot derive alignment
+/// hints from that. The pack shares its (unpacked) iteration domain with the
+/// producer unpack, which does carry a config, so for each tiling level present
+/// on the unpack we compare that level's tile sizes against the pack's scalable
+/// inner tiles and infer the inner tile alignment hints.
+static void annotateScalablePackConsumerOfUnpack(linalg::PackOp packOp) {
+  auto unpackOp = packOp.getSource().getDefiningOp<linalg::UnPackOp>();
+  if (!unpackOp) {
+    return;
+  }
+  auto unpackConfig =
+      getLoweringConfig<IREE::CPU::LoweringConfigAttr>(unpackOp);
+  if (!unpackConfig) {
+    return;
+  }
+  std::optional<SizesAndScalableFlags> packScalableTilesFlags =
+      getScalableTileSizesAndFlags(packOp.getMixedTiles());
+  if (!packScalableTilesFlags) {
+    return;
+  }
+
+  SmallVector<std::pair<IREE::CPU::TilingLevel, SmallVector<int64_t>>> perLevel;
+  for (int levelIdx = 0;
+       levelIdx < llvm::to_underlying(IREE::CPU::TilingLevel::MaxNumTileLevels);
+       ++levelIdx) {
+    auto level = static_cast<IREE::CPU::TilingLevel>(levelIdx);
+    // TODO(egebeysel): distribution tile sizes are only set on the root
+    // operation. They need special logic here.
+    if (!unpackConfig.hasTilingLevel(llvm::to_underlying(level))) {
+      continue;
+    }
+    auto levelAttr = dyn_cast<IREE::Codegen::LoweringConfigTilingLevelAttr>(
+        unpackConfig.getTilingLevelAttr(llvm::to_underlying(level)));
+    if (!levelAttr) {
+      continue;
+    }
+    capturePackInnerTileAlignment(packOp, *packScalableTilesFlags, level,
+                                  levelAttr.getSizes(),
+                                  levelAttr.getScalableFlags(), perLevel);
+  }
+
+  if (!perLevel.empty()) {
+    IREE::CPU::InnerTileAlignmentsAttr::setOnOp(packOp, perLevel);
+  }
+}
+
 /// Returns true if the given operation should have a lowering config set.
 ///
 /// This predicate excludes:
@@ -4209,6 +4323,16 @@
                                               rootOperation))) {
       return failure();
     }
+
+    // Packs fed by an unpack are pruned above and never receive a lowering
+    // config, yet they are still tiled as fused consumers downstream.
+    // Precompute the alignment of their scalable inner tiles relative to the
+    // producer unpack's per-level tile sizes.
+    for (Operation *op : computeOps) {
+      if (auto packOp = dyn_cast<linalg::PackOp>(op)) {
+        annotateScalablePackConsumerOfUnpack(packOp);
+      }
+    }
   }
 
   return success();
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_sve_lowering_strategy.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_sve_lowering_strategy.mlir
index 9a00d51..f7b1628 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_sve_lowering_strategy.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_sve_lowering_strategy.mlir
@@ -278,6 +278,7 @@
 //      CHECK: func.func @pack(
 // CHECK-SAME:     translation_info = #[[TRANSLATION]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG]]
 
 // -----
@@ -308,6 +309,7 @@
 //      CHECK:   linalg.generic
 // CHECK-SAME:       lowering_config = #[[CONFIG1]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG2]]
 
 // -----
@@ -354,6 +356,7 @@
 //      CHECK:   linalg.generic
 // CHECK-SAME:       lowering_config = #[[CONFIG3]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_inner_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG4]]
 
 // -----
@@ -385,6 +388,7 @@
 //      CHECK:   linalg.generic
 // CHECK-SAME:       lowering_config = #[[CONFIG1]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG2]]
 
 // -----
@@ -423,6 +427,7 @@
 //      CHECK:   linalg.generic
 // CHECK-SAME:       lowering_config = #[[CONFIG2]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG3]]
 
 // -----
@@ -430,7 +435,7 @@
 #executable_target_system_elf_arm_64_ = #hal.executable.target<"llvm-cpu", "system-elf-arm_64", {cpu = "", cpu_features = "+v9a,+sve", data_layout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128", link_embedded = false, native_vector_size = 16 : index, target_triple = "aarch64-none-linux-android34"}>
 #map = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>
 #map2 = affine_map<()[s0] -> (10240 ceildiv s0)>
-func.func @mmt4d_generic_unpack_pack(%arg0: tensor<5x4096x16x1xf16>, %arg1: tensor<?x4096x?x1xf16>) -> tensor<5x10240x16x1xf16> attributes {hal.executable.target = #executable_target_system_elf_arm_64_} {
+func.func @mmt4d_generic_unpack_pack(%arg0: tensor<5x4096x16x1xf16>, %arg1: tensor<?x4096x?x1xf16>) -> tensor<5x?x16x?xf16> attributes {hal.executable.target = #executable_target_system_elf_arm_64_} {
   %cst = arith.constant 0.000000e+00 : f16
   %cst_0 = arith.constant 0.000000e+00 : f32
 
@@ -450,9 +455,9 @@
   } -> tensor<5x?x16x?xf16>
   %5 = tensor.empty() : tensor<77x10240xf16>
   %unpack = linalg.unpack %4 outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [16, %c16_vscale] into %5 : tensor<5x?x16x?xf16> -> tensor<77x10240xf16>
-  %6 = tensor.empty() : tensor<5x10240x16x1xf16>
-  %pack = linalg.pack %unpack padding_value(%cst : f16) outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [16, 1] into %6 : tensor<77x10240xf16> -> tensor<5x10240x16x1xf16>
-  return %pack : tensor<5x10240x16x1xf16>
+  %6 = tensor.empty(%n0, %c16_vscale) : tensor<5x?x16x?xf16>
+  %pack = linalg.pack %unpack padding_value(%cst : f16) outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [16, %c16_vscale] into %6 : tensor<77x10240xf16> -> tensor<5x?x16x?xf16>
+  return %pack : tensor<5x?x16x?xf16>
 }
 // CHECK-DAG:   #[[$CONFIG0:.+]] = #iree_cpu.lowering_config<vector_common_parallel = [1, 1, 16, [16]]>
 // CHECK-DAG:   #[[$CONFIG1:.+]] = #iree_cpu.lowering_config<distribution = [1, 1, 0, 0, 0, 0], vector_common_parallel = [1, 1, 0, 16, [16], 0], vector_reduction = [0, 0, 1, 0, 0, 1]>
@@ -466,8 +471,61 @@
 // CHECK-SAME:      {lowering_config = #[[$CONFIG0]]}
 // CHECK:         linalg.unpack
 // CHECK-SAME:      {lowering_config = #[[$CONFIG2]]}
+// The consumer pack has no lowering config of its own; it carries the
+// precomputed inner-tile alignment hint instead.
 // CHECK:         linalg.pack
-// CHECK-NOT:      lowering_config
+// CHECK-SAME:      inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
+// CHECK-NOT:       lowering_config
+
+// -----
+
+// Same as the above, but the pack operations scalable inner tile does not match it's unpack producer's.
+// The alignment hint is not set.
+
+#executable_target_system_elf_arm_64_ = #hal.executable.target<"llvm-cpu", "system-elf-arm_64", {cpu = "", cpu_features = "+v9a,+sve", data_layout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128", link_embedded = false, native_vector_size = 16 : index, target_triple = "aarch64-none-linux-android34"}>
+#map = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>
+#map2 = affine_map<()[s0] -> (77 ceildiv s0)>
+func.func @negative_hint_mmt4d_generic_unpack_pack(%arg0: tensor<5x4096x16x1xf16>, %arg1: tensor<?x4096x?x1xf16>) -> tensor<?x640x16x?xf16> attributes {hal.executable.target = #executable_target_system_elf_arm_64_} {
+  %cst = arith.constant 0.000000e+00 : f16
+  %cst_0 = arith.constant 0.000000e+00 : f32
+
+  %c16 = arith.constant 16 : index
+  %vscale = vector.vscale
+  %c16_vscale = arith.muli %vscale, %c16 : index
+  %n0 = affine.apply #map2()[%c16_vscale]
+
+  %0 = tensor.empty(%n0, %c16_vscale) : tensor<5x?x16x?xf16>
+  %1 = tensor.empty(%n0, %c16_vscale) : tensor<5x?x16x?xf32>
+  %2 = linalg.fill ins(%cst_0 : f32) outs(%1 : tensor<5x?x16x?xf32>) -> tensor<5x?x16x?xf32>
+  %3 = linalg.mmt4d ins(%arg0, %arg1 : tensor<5x4096x16x1xf16>, tensor<?x4096x?x1xf16>) outs(%2 : tensor<5x?x16x?xf32>) -> tensor<5x?x16x?xf32>
+  %4 = linalg.generic {indexing_maps = [#map, #map], iterator_types = ["parallel", "parallel", "parallel", "parallel"]} ins(%3 : tensor<5x?x16x?xf32>) outs(%0 : tensor<5x?x16x?xf16>) {
+  ^bb0(%in: f32, %out: f16):
+    %7 = arith.truncf %in : f32 to f16
+    linalg.yield %7 : f16
+  } -> tensor<5x?x16x?xf16>
+  %5 = tensor.empty() : tensor<77x10240xf16>
+  %unpack = linalg.unpack %4 outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [16, %c16_vscale] into %5 : tensor<5x?x16x?xf16> -> tensor<77x10240xf16>
+  %6 = tensor.empty(%n0, %c16_vscale) : tensor<?x640x16x?xf16>
+  %pack = linalg.pack %unpack padding_value(%cst : f16) outer_dims_perm = [0, 1] inner_dims_pos = [1, 0] inner_tiles = [16, %c16_vscale] into %6 : tensor<77x10240xf16> -> tensor<?x640x16x?xf16>
+  return %pack : tensor<?x640x16x?xf16>
+}
+// CHECK-DAG:   #[[$CONFIG0:.+]] = #iree_cpu.lowering_config<vector_common_parallel = [1, 1, 16, [16]]>
+// CHECK-DAG:   #[[$CONFIG1:.+]] = #iree_cpu.lowering_config<distribution = [1, 1, 0, 0, 0, 0], vector_common_parallel = [1, 1, 0, 16, [16], 0], vector_reduction = [0, 0, 1, 0, 0, 1]>
+// CHECK-DAG:   #[[$CONFIG2:.+]] = #iree_cpu.lowering_config<vector_common_parallel = [16, [16]]>
+// CHECK-LABEL: func.func @negative_hint_mmt4d_generic_unpack_pack(
+// CHECK:         linalg.fill
+// CHECK-SAME:      {lowering_config = #[[$CONFIG0]]}
+// CHECK:         linalg.mmt4d
+// CHECK-SAME:      {lowering_config = #[[$CONFIG1]]}
+// CHECK:         linalg.generic
+// CHECK-SAME:      {lowering_config = #[[$CONFIG0]]}
+// CHECK:         linalg.unpack
+// CHECK-SAME:      {lowering_config = #[[$CONFIG2]]}
+// The consumer pack has no lowering config nor carries an inner tile alignment,
+// since the unpack and pack inner dimensions are not aligned.
+// CHECK:         linalg.pack
+// CHECK-NOT:      inner_tile_alignments
+// CHECK-NOT:       lowering_config
 
 // -----
 
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_riscv_v_lowering_strategy.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_riscv_v_lowering_strategy.mlir
index f63738f..5fbb57d 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_riscv_v_lowering_strategy.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_riscv_v_lowering_strategy.mlir
@@ -118,6 +118,7 @@
 //      CHECK: func.func @pack(
 // CHECK-SAME:     translation_info = #[[TRANSLATION]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG]]
 
 // -----
@@ -148,6 +149,7 @@
 //      CHECK:   linalg.generic
 // CHECK-SAME:       lowering_config = #[[CONFIG1]]
 //      CHECK:   linalg.pack
+// CHECK-SAME:       inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
 // CHECK-SAME:       lowering_config = #[[CONFIG2]]
 
 // -----
@@ -155,7 +157,7 @@
 #executable_target_riscv64 = #hal.executable.target<"llvm-cpu", "embedded-elf-riscv_64", {cpu_features = "+zvfh,+v", data_layout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128", native_vector_size = 16 : index, target_triple = "riscv64-unknown-unknown-eabi-elf"}>
 #map = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>
 #map2 = affine_map<()[s0] -> (10240 ceildiv s0)>
-func.func @mmt4d_generic_unpack_pack(%arg0: tensor<5x4096x7x1xf16>, %arg1: tensor<?x4096x?x1xf16>) -> tensor<5x10240x7x1xf16> attributes {hal.executable.target = #executable_target_riscv64} {
+func.func @mmt4d_generic_unpack_pack(%arg0: tensor<5x4096x7x1xf16>, %arg1: tensor<?x4096x?x1xf16>) -> tensor<5x?x7x?xf16> attributes {hal.executable.target = #executable_target_riscv64} {
   %cst = arith.constant 0.000000e+00 : f16
   %cst_0 = arith.constant 0.000000e+00 : f32
 
@@ -175,9 +177,9 @@
   } -> tensor<5x?x7x?xf16>
   %5 = tensor.empty() : tensor<33x10240xf16>
   %unpack = linalg.unpack %4 outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [7, %c8_vscale] into %5 : tensor<5x?x7x?xf16> -> tensor<33x10240xf16>
-  %6 = tensor.empty() : tensor<5x10240x7x1xf16>
-  %pack = linalg.pack %unpack padding_value(%cst : f16) outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [7, 1] into %6 : tensor<33x10240xf16> -> tensor<5x10240x7x1xf16>
-  return %pack : tensor<5x10240x7x1xf16>
+  %6 = tensor.empty(%n0, %c8_vscale) : tensor<5x?x7x?xf16>
+  %pack = linalg.pack %unpack padding_value(%cst : f16) outer_dims_perm = [0, 1] inner_dims_pos = [0, 1] inner_tiles = [7, %c8_vscale] into %6 : tensor<33x10240xf16> -> tensor<5x?x7x?xf16>
+  return %pack : tensor<5x?x7x?xf16>
 }
 // CHECK-DAG:   #[[$CONFIG0:.+]] = #iree_cpu.lowering_config<vector_common_parallel = [1, 1, 7, [8]]>
 // CHECK-DAG:   #[[$CONFIG1:.+]] = #iree_cpu.lowering_config<distribution = [5, 1, 0, 0, 0, 0], vector_common_parallel = [1, 1, 0, 7, [8], 0], vector_reduction = [0, 0, 1, 0, 0, 1]>
@@ -191,5 +193,8 @@
 // CHECK-SAME:      {lowering_config = #[[$CONFIG0]]}
 // CHECK:         linalg.unpack
 // CHECK-SAME:      {lowering_config = #[[$CONFIG2]]}
+// The consumer pack has no lowering config of its own; it carries the
+// precomputed inner-tile alignment hint instead.
 // CHECK:         linalg.pack
-// CHECK-NOT:      lowering_config
+// CHECK-SAME:      inner_tile_alignments = #iree_cpu.inner_tile_alignments<vector_common_parallel = [Unknown, Equal]>
+// CHECK-NOT:       lowering_config