[DT][CPU]: RVV scalable encoding materialization (#24600)

This PR introduces scalable data-tiled layout selection and
materialization for RVV. Though since there exist microkernels for the
RV64 backend with the V extension, previous mechanism to select these
data-tiled layouts _statically_ based on the `+zvl*b` flags is also
preserved and the scalable data-tiled layout materialization is hidden
behind the flag scalable vectorization flag.

The exactl layout selection (the tile sizes) though is based on the same
principles as the static one - fully utilize all 32 of the V registers.
F16 tile sizes that underutilize the V registers in the static case (see
[this discord
thread](https://discord.com/channels/689900678990135345/1456679894812590326/1461764521403224209))).

**NOTE**: the `+zvl*b` flags DO NOT give us the _exact_ values of VLEN
that the hardware implements but rather the minimum one, meaning that
the previous static mechanism can underutilize the vector registers and
is not vector-length agnostic.

---------

Signed-off-by: Ege Beysel <beyselege@gmail.com>
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_aarch64.mlir b/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_aarch64.mlir
index 9c422c4..3ddee79 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_aarch64.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_aarch64.mlir
@@ -153,7 +153,7 @@
     %arg1: tensor<16x1xf32, #encoding_rhs>,
     %arg2: tensor<16x1xf32, #encoding_result>
 ) -> tensor<16x1xf32, #encoding_result> attributes {
-  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="aarch64-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {cpu_features="+sve", target_triple="aarch64-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
 } {
   %0 = linalg.matmul ins(%arg0, %arg1 : tensor<16x16xf32, #encoding_lhs>, tensor<16x1xf32, #encoding_rhs>)
                     outs(%arg2 : tensor<16x1xf32, #encoding_result>) -> tensor<16x1xf32, #encoding_result>
@@ -161,11 +161,17 @@
 }
 
 // CHECK-LABEL: func @matvec_shaped_matmul_lowering_f32f32f32_aarch64
-// CHECK-SAME:    %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
-// CHECK-SAME:    %[[RHS:[a-zA-Z0-9]+]]: tensor<1x16x1x1xf32>
-// CHECK-SAME:    %[[OUT:[a-zA-Z0-9]+]]: tensor<1x2x1x8xf32>
+// NO-SVE-SAME:    %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
+// NO-SVE-SAME:    %[[RHS:[a-zA-Z0-9]+]]: tensor<1x16x1x1xf32>
+// NO-SVE-SAME:    %[[OUT:[a-zA-Z0-9]+]]: tensor<1x2x1x8xf32>
+
+// WITH-SVE-SAME:  %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
+// WITH-SVE-SAME:  %[[RHS:[a-zA-Z0-9]+]]: tensor<?x16x?x1xf32>
+// WITH-SVE-SAME:  %[[OUT:[a-zA-Z0-9]+]]: tensor<2x?x8x?xf32>
+
 // CHECK:         %[[MMT4D:.+]] = linalg.mmt4d
-// CHECK-SAME:       ins(%[[RHS]], %[[LHS]]
+// NO-SVE-SAME:       ins(%[[RHS]], %[[LHS]]
+// WITH-SVE-SAME:       ins(%[[LHS]], %[[RHS]]
 // CHECK-SAME:       outs(%[[OUT]]
 // CHECK:         return %[[MMT4D]]
 
@@ -221,7 +227,7 @@
 }
 //     CHECK-DAG: #[[$MAP0:.+]] = affine_map<()[s0] -> (s0 ceildiv 8)>
 //  WITH-SVE-DAG: #[[$MAP1:.+]] = affine_map<()[s0, s1] -> (s0 ceildiv s1)>
-//   CHECK-LABEL: func @matmul_lowering_f32f32f32_aarch64()
+//         CHECK: func @matmul_lowering_f32f32f32_aarch64()
 //  WITH-SVE-DAG:   %[[C8:.+]] = arith.constant 8 : index
 //     CHECK-DAG:   %[[M:.+]] = hal.interface.constant.load layout({{.+}}) ordinal(0)
 //     CHECK-DAG:   %[[N:.+]] = hal.interface.constant.load layout({{.+}}) ordinal(1)
@@ -310,7 +316,7 @@
     %rhs: tensor<16x1xf32, #encoding_rhs>,
     %result: tensor<16x1xf32, #encoding_result>
 ) -> tensor<16x1xf32, #encoding_result> attributes {
-  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="aarch64-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {cpu_features="+sve", target_triple="aarch64-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
 } {
   %matmul = linalg.matmul
     ins(%lhs, %rhs : tensor<16x16xf32, #encoding_lhs>, tensor<16x1xf32, #encoding_rhs>)
@@ -319,13 +325,20 @@
   return %matmul : tensor<16x1xf32, #encoding_result>
 }
 // CHECK-LABEL: func @matvec_lowering_f32f32f32_aarch64(
-//  CHECK-SAME:     %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
-//  CHECK-SAME:     %[[RHS:[a-zA-Z0-9]+]]: tensor<1x16x1x1xf32>
-//  CHECK-SAME:     %[[OUTS:[a-zA-Z0-9]+]]: tensor<1x2x1x8xf32>
-//       CHECK:   %[[MMT4D:.+]] = linalg.mmt4d
-//  CHECK-SAME:       ins(%[[RHS]], %[[LHS]] :
-//  CHECK-SAME:       outs(%[[OUTS]] :
-//       CHECK:   return %[[MMT4D]]
+//   NO-SVE-SAME:     %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
+//   NO-SVE-SAME:     %[[RHS:[a-zA-Z0-9]+]]: tensor<1x16x1x1xf32>
+//   NO-SVE-SAME:     %[[OUT:[a-zA-Z0-9]+]]: tensor<1x2x1x8xf32>
+
+
+// WITH-SVE-SAME:     %[[LHS:[a-zA-Z0-9]+]]: tensor<2x16x8x1xf32>
+// WITH-SVE-SAME:     %[[RHS:[a-zA-Z0-9]+]]: tensor<?x16x?x1xf32>
+// WITH-SVE-SAME:     %[[OUT:[a-zA-Z0-9]+]]: tensor<2x?x8x?xf32>
+
+//          CHECK:   %[[MMT4D:.+]] = linalg.mmt4d
+//    NO-SVE-SAME:       ins(%[[RHS]], %[[LHS]] :
+//  WITH-SVE-SAME:       ins(%[[LHS]], %[[RHS]] :
+//     CHECK-SAME:       outs(%[[OUT]] :
+//          CHECK:   return %[[MMT4D]]
 
 // -----
 
diff --git a/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_riscv.mlir b/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_riscv.mlir
index b1a54e0..a7ed1af 100644
--- a/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_riscv.mlir
+++ b/compiler/src/iree/compiler/Codegen/Common/test/materialize_encoding_riscv.mlir
@@ -1,12 +1,46 @@
-// RUN: iree-opt --pass-pipeline="builtin.module(func.func(iree-codegen-materialize-device-encoding))" --split-input-file %s | FileCheck %s
+// Tests for RISC-V targets with static VLEN-based tiles and scalable vectorization.
+//
+// STATIC mode (default): Expected to use VLEN-based tile sizes computed from zvl* features.
+// SCALABLE mode (opt-in): Expected to use smaller base tile sizes that scale with vscale at runtime.
+//
+// RUN: iree-opt --pass-pipeline="builtin.module(func.func(iree-codegen-materialize-device-encoding))" --split-input-file %s | FileCheck %s --check-prefixes=CHECK,STATIC
+// RUN: iree-opt --pass-pipeline="builtin.module(func.func(iree-codegen-materialize-device-encoding))" --iree-llvmcpu-enable-scalable-vectorization=true --split-input-file %s | FileCheck %s --check-prefixes=CHECK,SCALABLE
+
+// RISC-V f32 matmul encoding tests.
 
 #map = affine_map<(d0, d1, d2) -> (d0, d2)>
 #map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
 #map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
-#encoding_lhs = #iree_encoding.encoding<operand_index = 0, op_type = matmul, element_types = [i8, i8, i32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
-#encoding_rhs = #iree_encoding.encoding<operand_index = 1, op_type = matmul, element_types = [i8, i8, i32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
-#encoding_result = #iree_encoding.encoding<operand_index = 2, op_type = matmul, element_types = [i8, i8, i32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
-func.func @matmul_lowering_f32f32f32_riscv(%lhs: tensor<?x?xf32>, %rhs: tensor<?x?xf32>, %acc: tensor<?x?xf32>) -> tensor<?x?xf32> attributes {
+#encoding_lhs = #iree_encoding.encoding<operand_index = 0, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+#encoding_rhs = #iree_encoding.encoding<operand_index = 1, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+#encoding_res = #iree_encoding.encoding<operand_index = 2, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+
+// RISC-V64 without V extension - no data tiling (fallback)
+func.func @negative_matmul_lowering_f32f32f32_riscv64_no_v_ext(%lhs: tensor<?x?xf32>, %rhs: tensor<?x?xf32>, %acc: tensor<?x?xf32>, %m : index, %n : index, %k : index) -> tensor<?x?xf32> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %0 = iree_encoding.set_encoding %lhs encoding_dims{%m, %n, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_lhs>
+  %1 = iree_encoding.set_encoding %rhs encoding_dims{%m, %n, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_rhs>
+  %2 = iree_encoding.set_encoding %acc encoding_dims{%m, %n, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_res>
+  %3 = linalg.matmul
+      ins(%0, %1 : tensor<?x?xf32, #encoding_lhs>,
+                   tensor<?x?xf32, #encoding_rhs>)
+      outs(%2 : tensor<?x?xf32, #encoding_res>)
+      -> tensor<?x?xf32, #encoding_res>
+  %4 = iree_encoding.unset_encoding %3 encoding_dims{%m, %n, %k} : tensor<?x?xf32, #encoding_res> -> tensor<?x?xf32>{%m, %n}
+  return %4 : tensor<?x?xf32>
+}
+
+// RISC-V64 without V extension does not implement data-tiling.
+// CHECK-LABEL: func @negative_matmul_lowering_f32f32f32_riscv64_no_v_ext
+//   CHECK-NOT:   linalg.pack
+//   CHECK-NOT:   linalg.mmt4d
+//       CHECK:   %[[RES:.+]] = linalg.matmul
+//   CHECK-NOT:   linalg.unpack
+//       CHECK:   return %[[RES]]
+
+// RISC-V32 without V extension - no data tiling
+func.func @negative_matmul_lowering_f32f32f32_riscv32_no_v_ext(%lhs: tensor<?x?xf32>, %rhs: tensor<?x?xf32>, %acc: tensor<?x?xf32>) -> tensor<?x?xf32> attributes {
   hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv32-xyz-xyz", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
 } {
   %c0 = arith.constant 0 : index
@@ -16,22 +50,167 @@
   %K = tensor.dim %lhs, %c1 : tensor<?x?xf32>
   %0 = iree_encoding.set_encoding %lhs encoding_dims{%M, %N, %K} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_lhs>
   %1 = iree_encoding.set_encoding %rhs encoding_dims{%M, %N, %K} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_rhs>
-  %2 = iree_encoding.set_encoding %acc encoding_dims{%M, %N, %K} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_result>
+  %2 = iree_encoding.set_encoding %acc encoding_dims{%M, %N, %K} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_res>
   %3 = linalg.matmul
       ins(%0, %1 : tensor<?x?xf32, #encoding_lhs>,
                    tensor<?x?xf32, #encoding_rhs>)
-      outs(%2 : tensor<?x?xf32, #encoding_result>)
-      -> tensor<?x?xf32, #encoding_result>
-  %4 = iree_encoding.unset_encoding %3 encoding_dims{%M, %N, %K} : tensor<?x?xf32, #encoding_result> -> tensor<?x?xf32>{%M, %N}
+      outs(%2 : tensor<?x?xf32, #encoding_res>)
+      -> tensor<?x?xf32, #encoding_res>
+  %4 = iree_encoding.unset_encoding %3 encoding_dims{%M, %N, %K} : tensor<?x?xf32, #encoding_res> -> tensor<?x?xf32>{%M, %N}
   return %4 : tensor<?x?xf32>
 }
-// RISC-V targets does not implement data-tiling yet.
-// CHECK-LABEL: func @matmul_lowering_f32f32f32_riscv
+// RISC-V32 without V extension does not implement data-tiling.
+// CHECK-LABEL: func @negative_matmul_lowering_f32f32f32_riscv32_no_v_ext
+//   CHECK-NOT:   linalg.pack
+//   CHECK-NOT:   linalg.mmt4d
 //       CHECK:   %[[RES:.+]] = linalg.matmul
+//   CHECK-NOT:   linalg.unpack
 //       CHECK:   return %[[RES]]
 
+// RISC-V64 with V extension - set_encoding for LHS (f32)
+// The LHS operand corresponds to the M dimension, which is NOT scalable.
+// Therefore, STATIC and SCALABLE produce the same result for LHS.
+func.func @matmul_set_encoding_LHS_f32_riscv64(%arg0: tensor<?x?xf32>, %m: index, %n: index, %k: index) -> tensor<?x?xf32, #encoding_lhs> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %0 = iree_encoding.set_encoding %arg0 encoding_dims{%m, %n, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_lhs>
+  return %0 : tensor<?x?xf32, #encoding_lhs>
+}
+// CHECK-LABEL: func.func @matmul_set_encoding_LHS_f32_riscv64
+// CHECK-SAME:    %[[ARG0:[a-zA-Z0-9]+]]: tensor<?x?xf32>
+// CHECK:         %[[PACK:.+]] = linalg.pack %[[ARG0]]
+// CHECK-SAME:      outer_dims_perm = [0, 1]
+// CHECK-SAME:      inner_dims_pos = [0, 1]
+// CHECK-SAME:      inner_tiles = [7, 1]
+// CHECK-SAME:      -> tensor<?x?x7x1xf32>
+// CHECK:         return %[[PACK]]
+
+// RISC-V64 with V extension - set_encoding for RHS (f32)
+// The RHS operand corresponds to the N dimension, which IS scalable in when scalable vectorization is enabled.
+// In static mode: N tile is fixed based on VLEN (16 for VLEN=128)
+// In scalable mode: N tile is scalable (8 * vscale)
+func.func @matmul_set_encoding_RHS_f32_riscv64(%arg0: tensor<?x?xf32>, %m: index, %n: index, %k: index) -> tensor<?x?xf32, #encoding_rhs> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %0 = iree_encoding.set_encoding %arg0 encoding_dims{%m, %n, %k} : tensor<?x?xf32> -> tensor<?x?xf32, #encoding_rhs>
+  return %0 : tensor<?x?xf32, #encoding_rhs>
+}
+// CHECK-LABEL: func.func @matmul_set_encoding_RHS_f32_riscv64
+// CHECK-SAME:    %[[SRC:[a-zA-Z0-9]+]]: tensor<?x?xf32>
+// CHECK-DAG:     %[[PAD:.+]] = arith.constant 0.000000e+00 : f32
+
+// SCALABLE-DAG:  %[[C8:.*]] = arith.constant 8 : index
+// SCALABLE-DAG:  %[[VSCALE:.*]] = vector.vscale
+// SCALABLE:      %[[C8_VSCALE:.*]] = arith.muli %[[VSCALE]], %[[C8]] : index
+
+/// With dynamic dimensions the N dimension is padded up to a multiple of the
+/// (possibly scalable) inner tile, so both modes emit a padding value.
+// CHECK:         %[[PACK:.+]] = linalg.pack %[[SRC]]
+// CHECK-SAME:      padding_value(%[[PAD]] : f32)
+// CHECK-SAME:      outer_dims_perm = [1, 0]
+// CHECK-SAME:      inner_dims_pos = [1, 0]
+
+// STATIC-SAME:     inner_tiles = [16, 1]
+// STATIC-SAME:     -> tensor<?x?x16x1xf32>
+
+// SCALABLE-SAME:   inner_tiles = [%[[C8_VSCALE]], 1]
+// SCALABLE-SAME:   -> tensor<?x?x?x1xf32>
+
+// CHECK:         return %[[PACK]]
+
+// RISC-V64 with V extension - unset_encoding for RESULT (f32)
+// The RESULT operand has both M and N dimensions. M is fixed, N differs by mode.
+func.func @matmul_unset_encoding_RESULT_f32_riscv64(%arg0: tensor<?x?xf32, #encoding_res>, %m: index, %n: index, %k: index) -> tensor<?x?xf32> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %0 = iree_encoding.unset_encoding %arg0 encoding_dims{%m, %n, %k} : tensor<?x?xf32, #encoding_res> -> tensor<?x?xf32>{%m, %n}
+  return %0 : tensor<?x?xf32>
+}
+// CHECK-LABEL: func.func @matmul_unset_encoding_RESULT_f32_riscv64
+// STATIC-SAME:    %[[INPUT:[a-zA-Z0-9]+]]: tensor<?x?x7x16xf32>
+// SCALABLE-SAME:  %[[INPUT:[a-zA-Z0-9]+]]: tensor<?x?x7x?xf32>
+
+// CHECK-DAG:     %[[EMPTY:.+]] = tensor.empty{{.*}} : tensor<?x?xf32>
+
+// SCALABLE-DAG:  %[[C8:.+]] = arith.constant 8 : index
+// SCALABLE:      %[[VSCALE:.+]] = vector.vscale
+// SCALABLE:      %[[C8_VSCALE:.+]] = arith.muli %[[VSCALE]], %[[C8]] : index
+
+// CHECK:         %[[UNPACK:.+]] = linalg.unpack %[[INPUT]]
+// CHECK-SAME:        outer_dims_perm = [0, 1] inner_dims_pos = [0, 1]
+// STATIC-SAME:       inner_tiles = [7, 16]
+// SCALABLE-SAME:     inner_tiles = [7, %[[C8_VSCALE]]]
+
+// CHECK:         return %[[UNPACK]] : tensor<?x?xf32>
+
+// RISC-V64 with V extension - matmul lowering (f32)
+func.func @matmul_lowering_f32f32f32_riscv64(
+    %lhs: tensor<?x?xf32, #encoding_lhs>,
+    %rhs: tensor<?x?xf32, #encoding_rhs>,
+    %result: tensor<?x?xf32, #encoding_res>
+) -> tensor<?x?xf32, #encoding_res> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %matmul = linalg.matmul
+    ins(%lhs, %rhs : tensor<?x?xf32, #encoding_lhs>, tensor<?x?xf32, #encoding_rhs>)
+    outs(%result : tensor<?x?xf32, #encoding_res>)
+    -> tensor<?x?xf32, #encoding_res>
+  return %matmul : tensor<?x?xf32, #encoding_res>
+}
+
+// CHECK-LABEL: func @matmul_lowering_f32f32f32_riscv64(
+// STATIC-SAME:   %[[LHS:[a-zA-Z0-9]+]]: tensor<?x?x7x1xf32>
+// STATIC-SAME:   %[[RHS:[a-zA-Z0-9]+]]: tensor<?x?x16x1xf32>
+// STATIC-SAME:   %[[OUTS:[a-zA-Z0-9]+]]: tensor<?x?x7x16xf32>
+
+// SCALABLE-SAME: %[[LHS:[a-zA-Z0-9]+]]: tensor<?x?x7x1xf32>
+// SCALABLE-SAME: %[[RHS:[a-zA-Z0-9]+]]: tensor<?x?x?x1xf32>
+// SCALABLE-SAME: %[[OUTS:[a-zA-Z0-9]+]]: tensor<?x?x7x?xf32>
+
+// CHECK:         %[[MMT4D:.+]] = linalg.mmt4d
+// CHECK-SAME:       ins(%[[LHS]], %[[RHS]] :
+// CHECK-SAME:       outs(%[[OUTS]] :
+// CHECK:         return %[[MMT4D]]
+
+// Checks that the +zvl512b flag indeed reflects on the data-tiled layout for the static case
+// and does not make a difference on the scalable case.
+
+func.func @matmul_lowering_f32f32f32_riscv64_zvl512b(
+    %lhs: tensor<?x?xf32, #encoding_lhs>,
+    %rhs: tensor<?x?xf32, #encoding_rhs>,
+    %result: tensor<?x?xf32, #encoding_res>
+) -> tensor<?x?xf32, #encoding_res> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v,+zvl512b", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %matmul = linalg.matmul
+    ins(%lhs, %rhs : tensor<?x?xf32, #encoding_lhs>, tensor<?x?xf32, #encoding_rhs>)
+    outs(%result : tensor<?x?xf32, #encoding_res>)
+    -> tensor<?x?xf32, #encoding_res>
+  return %matmul : tensor<?x?xf32, #encoding_res>
+}
+
+// CHECK-LABEL: func @matmul_lowering_f32f32f32_riscv64_zvl512b(
+// STATIC-SAME:   %[[LHS:[a-zA-Z0-9]+]]: tensor<?x?x7x1xf32>
+
+// +v implies +zvl128b, so the data-tiled layout without the flag had 16 for the N dim.
+// +zvl512b therefore has 16 * (512/128) = 64
+
+// STATIC-SAME:   %[[RHS:[a-zA-Z0-9]+]]: tensor<?x?x64x1xf32>
+// STATIC-SAME:   %[[OUTS:[a-zA-Z0-9]+]]: tensor<?x?x7x64xf32>
+
+// SCALABLE-SAME: %[[LHS:[a-zA-Z0-9]+]]: tensor<?x?x7x1xf32>
+// SCALABLE-SAME: %[[RHS:[a-zA-Z0-9]+]]: tensor<?x?x?x1xf32>
+// SCALABLE-SAME: %[[OUTS:[a-zA-Z0-9]+]]: tensor<?x?x7x?xf32>
+
+// CHECK:         %[[MMT4D:.+]] = linalg.mmt4d
+// CHECK-SAME:       ins(%[[LHS]], %[[RHS]] :
+// CHECK-SAME:       outs(%[[OUTS]] :
+// CHECK:         return %[[MMT4D]]
+
 // -----
 
+// RISC-V32 with ukernels - uses mmt4d with default tiles
+
 #map = affine_map<(d0, d1, d2) -> (d0, d2)>
 #map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
 #map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
@@ -60,3 +239,125 @@
 // CHECK-SAME:      ins(%[[LHS]], %[[RHS]]
 // CHECK-SAME:      outs(%[[ACC]]
 // CHECK:         return %[[MMT4D]]
+
+// -----
+
+// Checks that we don't transpose M/N for the narrow-N case when scalable inner tiles are present.
+// For scalable vectors, we want to keep the N dimension scalable.
+
+#map = affine_map<(d0, d1, d2) -> (d0, d2)>
+#map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
+#map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
+#encoding_lhs = #iree_encoding.encoding<operand_index = 0, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [100, 4, 500]>
+#encoding_rhs = #iree_encoding.encoding<operand_index = 1, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [100, 4, 500]>
+#encoding_result = #iree_encoding.encoding<operand_index = 2, op_type = matmul, element_types = [f32, f32, f32], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [100, 4, 500]>
+func.func @matmul_lowering_narrow_n_f32f32f32_riscv64(
+    %lhs: tensor<100x500xf32, #encoding_lhs>,
+    %rhs: tensor<500x4xf32, #encoding_rhs>,
+    %result: tensor<100x4xf32, #encoding_result>
+) -> tensor<100x4xf32, #encoding_result> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %matmul = linalg.matmul
+    ins(%lhs, %rhs : tensor<100x500xf32, #encoding_lhs>, tensor<500x4xf32, #encoding_rhs>)
+    outs(%result : tensor<100x4xf32, #encoding_result>)
+    -> tensor<100x4xf32, #encoding_result>
+  return %matmul : tensor<100x4xf32, #encoding_result>
+}
+
+// CHECK-LABEL: func @matmul_lowering_narrow_n_f32f32f32_riscv64(
+// STATIC-SAME:   %[[ARG0:[a-zA-Z0-9]+]]: tensor<7x500x16x1xf32>
+// STATIC-SAME:   %[[ARG1:[a-zA-Z0-9]+]]: tensor<1x500x4x1xf32>
+// STATIC-SAME:   %[[ARG2:[a-zA-Z0-9]+]]: tensor<1x7x4x16xf32>
+
+// SCALABLE-SAME: %[[ARG0:[a-zA-Z0-9]+]]: tensor<15x500x7x1xf32>
+// SCALABLE-SAME: %[[ARG1:[a-zA-Z0-9]+]]: tensor<?x500x?x1xf32>
+// SCALABLE-SAME: %[[ARG2:[a-zA-Z0-9]+]]: tensor<15x?x7x?xf32>
+
+// STATIC:         %[[MMT4D:.+]] = linalg.mmt4d
+// STATIC-SAME:       ins(%[[ARG1]], %[[ARG0]] :
+// STATIC-SAME:       outs(%[[ARG2]] :
+
+// SCALABLE:       %[[MMT4D:.+]] = linalg.mmt4d
+// SCALABLE-SAME:     ins(%[[ARG0]], %[[ARG1]] :
+// SCALABLE-SAME:     outs(%[[ARG2]] :
+
+// CHECK:         return %[[MMT4D]]
+
+// -----
+
+// RISC-V64 with V extension and zvfh - full matmul lowering (f16)
+
+#map = affine_map<(d0, d1, d2) -> (d0, d2)>
+#map1 = affine_map<(d0, d1, d2) -> (d2, d1)>
+#map2 = affine_map<(d0, d1, d2) -> (d0, d1)>
+#encoding_lhs = #iree_encoding.encoding<operand_index = 0, op_type = matmul, element_types = [f16, f16, f16], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+#encoding_rhs = #iree_encoding.encoding<operand_index = 1, op_type = matmul, element_types = [f16, f16, f16], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+#encoding_result = #iree_encoding.encoding<operand_index = 2, op_type = matmul, element_types = [f16, f16, f16], user_indexing_maps = [#map, #map1, #map2], iteration_sizes = [?, ?, ?]>
+func.func @full_matmul_lowering_f16f16f16_riscv64(
+    %lhs: tensor<?x?xf16>,
+    %rhs: tensor<?x?xf16>,
+    %acc: tensor<?x?xf16>,
+    %m : index, %n : index, %k : index
+) -> tensor<?x?xf16> attributes {
+  hal.executable.target = #hal.executable.target<"llvm-cpu", "xyz", {target_triple="riscv64-xyz-xyz", cpu_features="+v,+zvfh", iree.encoding.resolver = #iree_cpu.cpu_encoding_resolver<>}>
+} {
+  %0 = iree_encoding.set_encoding %lhs encoding_dims{%m, %n, %k} : tensor<?x?xf16> -> tensor<?x?xf16, #encoding_lhs>
+  %1 = iree_encoding.set_encoding %rhs encoding_dims{%m, %n, %k} : tensor<?x?xf16> -> tensor<?x?xf16, #encoding_rhs>
+  %2 = iree_encoding.set_encoding %acc encoding_dims{%m, %n, %k} : tensor<?x?xf16> -> tensor<?x?xf16, #encoding_result>
+  %3 = linalg.matmul
+      ins(%0, %1 : tensor<?x?xf16, #encoding_lhs>,
+                   tensor<?x?xf16, #encoding_rhs>)
+      outs(%2 : tensor<?x?xf16, #encoding_result>)
+      -> tensor<?x?xf16, #encoding_result>
+  %4 = iree_encoding.unset_encoding %3 encoding_dims{%m, %n, %k} : tensor<?x?xf16, #encoding_result> -> tensor<?x?xf16>{%m, %n}
+  return %4 : tensor<?x?xf16>
+}
+
+/// F16 on RISC-V64 with V+zvfh extension:
+/// For the static case: M=7, N=16 (vlen=128), K=1
+/// For the scalable case: M=7, N=8*vscale (vlen=64*vscale), K=1
+
+// CHECK-LABEL: func @full_matmul_lowering_f16f16f16_riscv64(
+// CHECK-SAME:    %[[LHS:[a-zA-Z0-9]+]]: tensor<?x?xf16>
+// CHECK-SAME:    %[[RHS:[a-zA-Z0-9]+]]: tensor<?x?xf16>
+// CHECK-SAME:    %[[ACC:[a-zA-Z0-9]+]]: tensor<?x?xf16>
+
+// SCALABLE:      %[[C8:.+]] = arith.constant 8
+
+/// Pack LHS: [M, K] -> [?, ?, 7, 1] (this happens before vscale computation)
+// CHECK:         %[[PACK_LHS:.+]] = linalg.pack %[[LHS]]
+// CHECK-SAME:      outer_dims_perm = [0, 1]
+// CHECK-SAME:      inner_dims_pos = [0, 1]
+// CHECK-SAME:      inner_tiles = [7, 1]
+
+// SCALABLE:      %[[VSCALE:.+]] = vector.vscale
+// SCALABLE:      %[[C8_VSCALE:.+]] = arith.muli %[[VSCALE]], %[[C8]]
+
+/// Pack RHS: [K, N] -> [?, ?, N_tile, 1] with outer_dims_perm = [1, 0]
+// CHECK:         %[[PACK_RHS:.+]] = linalg.pack %[[RHS]]
+// CHECK-SAME:      outer_dims_perm = [1, 0]
+// CHECK-SAME:      inner_dims_pos = [1, 0]
+// STATIC-SAME:     inner_tiles = [16, 1]
+// SCALABLE-SAME:   inner_tiles = [%[[C8_VSCALE]], 1]
+
+/// Pack ACC: [M, N] -> [?, ?, 7, N_tile]
+// CHECK:         %[[PACK_ACC:.+]] = linalg.pack %[[ACC]]
+// CHECK-SAME:      outer_dims_perm = [0, 1]
+// CHECK-SAME:      inner_dims_pos = [0, 1]
+// STATIC-SAME:     inner_tiles = [7, 16]
+// SCALABLE-SAME:   inner_tiles = [7, %[[C8_VSCALE]]]
+
+/// The mmt4d operation
+// CHECK:         %[[MMT4D:.+]] = linalg.mmt4d
+// CHECK-SAME:      ins(%[[PACK_LHS]], %[[PACK_RHS]] :
+// CHECK-SAME:      outs(%[[PACK_ACC]] :
+
+/// Unpack result: [?, ?, 7, N_tile] -> [M, N]
+// CHECK:         %[[UNPACK:.+]] = linalg.unpack %[[MMT4D]]
+// CHECK-SAME:      outer_dims_perm = [0, 1]
+// CHECK-SAME:      inner_dims_pos = [0, 1]
+// STATIC-SAME:     inner_tiles = [7, 16]
+// SCALABLE-SAME:   inner_tiles = [7, %[[C8_VSCALE]]]
+
+// CHECK:         return %[[UNPACK]]
diff --git a/compiler/src/iree/compiler/Codegen/ExternalInterfaces/CPUEncodingExternalModels.cpp b/compiler/src/iree/compiler/Codegen/ExternalInterfaces/CPUEncodingExternalModels.cpp
index a9e0670..cd14497 100644
--- a/compiler/src/iree/compiler/Codegen/ExternalInterfaces/CPUEncodingExternalModels.cpp
+++ b/compiler/src/iree/compiler/Codegen/ExternalInterfaces/CPUEncodingExternalModels.cpp
@@ -71,14 +71,30 @@
 // Utilities.
 //===----------------------------------------------------------------------===//
 
+/// Determines which tile dimensions should be scalable for a given target.
+/// For AArch64 SVE/SVE2 and RISC-V with the V extension (when not using static
+/// vectors), the N dimension is made scalable to take advantage of scalable
+/// vector registers.
+/// Returns failure if scalable tiling is not applicable for the target.
 static FailureOr<IREE::Codegen::ScalableTileFlags>
 getScalableTileFlags(linalg::ContractionDimensions cDims,
                      IREE::Encoding::EncodingAttr encoding,
                      DictionaryAttr config) {
   // TODO(egebeysel): I think this isScalable*Enabled flag should be temporary
   // and the temporary SME flag should probably come next to it.
-  if (!isAArch64(config) || !isScalableVectorizationEnabled()) {
-    LDBG() << "Pre-conditions to enable scalable tiling are not met!";
+  if (!isScalableVectorizationEnabled()) {
+    LDBG() << "Scalable vectorization is not enabled!";
+    return failure();
+  }
+
+  // Check if target supports scalable vectors.
+  bool hasAArch64ScalableSupport =
+      isAArch64(config) &&
+      (hasFeature(config, "+sve") || hasFeature(config, "+sve2"));
+  bool hasRISCVScalableSupport = isRISCV64(config) && hasFeature(config, "+v");
+
+  if (!hasAArch64ScalableSupport && !hasRISCVScalableSupport) {
+    LDBG() << "Target does not support scalable vectors!";
     return failure();
   }
 
@@ -90,7 +106,9 @@
                       : encoding.mapDimToOperandIndex(cDims.n[0]);
   std::optional<unsigned> kDim = encoding.mapDimToOperandIndex(cDims.k[0]);
   IREE::Codegen::ScalableTileFlags scalableTiles;
-  // TODO(egebeysel): Add logic for SME.
+
+  // M dimension: fixed for both AArch64 SVE and RISC-V V extension.
+  // TODO(egebeysel): Add logic for SME which may have scalable M.
   if (mDim.has_value()) {
     if (hasFeature(config, "+sme")) {
       LDBG() << "SME with data-tiling is not supported yet!";
@@ -98,10 +116,17 @@
     }
     scalableTiles.push_back(false);
   }
+
+  // N dimension: scalable for both AArch64 SVE/SVE2 and RISC-V V extension
+  // (when using scalable vectors). This allows the tile to grow with the
+  // vector register length.
   if (nDim.has_value()) {
-    scalableTiles.push_back(hasFeature(config, "+sve") ||
-                            hasFeature(config, "+sve2"));
+    scalableTiles.push_back(hasAArch64ScalableSupport ||
+                            hasRISCVScalableSupport);
   }
+
+  // K dimension: fixed for both targets. The reduction dimension does not
+  // benefit from scalability in the same way as the N dimension.
   if (kDim.has_value()) {
     scalableTiles.push_back(false);
   }
@@ -219,7 +244,7 @@
                            IREE::Encoding::MatmulNarrowDim narrowDim) {
   // Handle narrow-N by transposing to reduce to narrow-M. Note: the
   // enumeratedTiles currently only enumerate narrow-M cases.
-  if (narrowDim.isN()) {
+  if (narrowDim.isN() && !isScalableVectorizationEnabled()) {
     narrowDim.dim = IREE::Encoding::MatmulNarrowDim::Dim::M;
     TileMxNxK tile = chooseMatmulTile(enumeratedTiles, narrowDim);
     std::swap(tile.M, tile.N);
@@ -994,18 +1019,42 @@
 // Enumerate tile sizes to choose from on riscv64.
 // For narrow-{M,N} cases, this only enumerates on narrow M. The narrow-N cases
 // are handled by transposition in chooseMatmulTile.
+//
+// When using static VLEN-based tile sizes, the N dimension is computed from the
+// VLEN using zvl* features.
+//
+// When using scalable vectors
+// (iree-llvmcpu-enable-scalable-vectorization=true), we use smaller base tile
+// sizes that scale with vscale (VLEN/64). The base tile sizes assume VLEN=64
+// bits, meaning vscale=1 at the minimum. When scalable vectorization is
+// enabled, these base sizes will be multiplied by vscale at runtime to utilize
+// the full vector register width.
+//
+// Base tile calculation for VLEN=64:
+//   - f32 (32 bits): 64/32 = 2 elements per vscale
+//   - f16 (16 bits): 64/16 = 4 elements per vscale
 static SmallVector<TileMxNxK>
 enumerateMatmulTileRiscv64(TypeRange elementTypes, DictionaryAttr config) {
-
-  // Data-Tiling is only implemented for the V extension
+  // Data-tiling is only implemented for the V extension.
   if (!hasFeature(config, "+v")) {
     return {};
   }
-  size_t vlen = getRISCVVVlenFromCPUFeatures(config);
+
   assert(elementTypes.size() == 3);
   Type lhs = elementTypes[0];
   Type rhs = elementTypes[1];
   Type out = elementTypes[2];
+
+  // RVV has both static and scalable vector modes.
+  // Static mode: use VLEN-based tile sizes from zvl* features.
+  // Scalable mode: use base VLEN=64 bits as the base size, the vscale
+  // multiplier and the correct scalable data-tiled layout will be emitted
+  // accordingly in `getScalableTileFlags`.
+  constexpr size_t kBaseVlen = 64;
+  size_t vlen = isScalableVectorizationEnabled()
+                    ? kBaseVlen
+                    : getRISCVVVlenFromCPUFeatures(config);
+
   if (lhs.isF32() && rhs.isF32() && out.isF32()) {
     // VLEN-aware Tile size selection
     // One concern that needs to be addressed here is that
@@ -1020,10 +1069,13 @@
     };
   }
   if (lhs.isF16() && rhs.isF16()) {
+    // TODO(egebeysel): currently, the F16 path uses LMUL=2 and therefore 14
+    // registers for accumulators. This has to be investigated and the
+    // corresponding microkernels have to be adjusted if need be.
     int N0 = vlen / 8;
     if (hasFeature(config, "+zvfh")) {
       return {
-          TileMxNxK{7, N0, 1},
+          TileMxNxK{7, N0, 1}, // Aim to use vfmacc.
           TileMxNxK{4, N0, 1}, // Truncation of the above.
           TileMxNxK{2, N0, 1}, // Truncation of the above.
           TileMxNxK{1, N0, 1}, // Truncation of the above.
@@ -1031,7 +1083,7 @@
     }
     if (hasFeature(config, "+zvfhmin")) {
       return {
-          TileMxNxK{6, N0, 1},
+          TileMxNxK{6, N0, 1}, // Aim to use vfmacc.
           TileMxNxK{4, N0, 1}, // Truncation of the above.
           TileMxNxK{2, N0, 1}, // Truncation of the above.
           TileMxNxK{1, N0, 1}, // Truncation of the above.
@@ -1326,6 +1378,27 @@
       return info;
     }
     auto narrowDim = IREE::Encoding::getPo2MatmulNarrowDim(encoding);
+    // In case of scalable vectors, we explicitly use the narrow M dimension
+    // config to use the M dimension for calculating padding penalties while we
+    // choose the layout.
+    // TODO(egebeysel): reconcile this with the static case. Currently,
+    // this results in vector types with non-trailing scalable dimensions, e.g.
+    // vector<[8]x8xf32>, which cannot be legalized or lowered to LLVM properly.
+    // LLVM only handles vector types with only-trailing scalable dimensions
+    // properly, e.g. vector<8x[8]xf32> is lowered to !llvm.array<8 x
+    // vector<[8]xf32>>. The former have to be legalized, so that they can be
+    // lowered to an array of scalable vectors, but the patterns are missing.
+    if (isScalableVectorizationEnabled() && narrowDim.isN() &&
+        !cDims.m.empty()) {
+      int64_t m = getEncodingContractionLikeSizes(encoding).value().M;
+      if (ShapedType::isDynamic(m)) {
+        narrowDim = {};
+      } else {
+        narrowDim = {IREE::Encoding::MatmulNarrowDim::Dim::M, m};
+      }
+    }
+    // Choose a final matmul TileMxNxK from the above-enumerated tile shapes,
+    // taking narrow dimensions into account.
     TileMxNxK chosenTileMxNxK =
         chooseMatmulTile(enumeratedTileMxNxK, narrowDim);
     FailureOr<MaterializeEncodingInfo> maybeEncodingInfo =