[Codegen][CPU] Fill in the bf16 and i8 ukernel bodies + e2e tests. (#24572)
Replaces the bf16 and i8-VNNI seeds' stub bodies with real SIMD
implementations, both generic over the `intrinsics_{m,n,k}` unrolling
factors and structured like the AMDGPU C ukernel they were adapted from
(`iree_uk_amdgpu_multi_mma_mfma_i32_16x16x32_i8`): accumulators in
registers, an outer loop over the K tiles (`k_outer`), and inside it the
`(intrinsics_m, intrinsics_n, intrinsics_k)` unroll. The `intrinsics_*`
arrive as constants at the inlined call site, so the loops fully unroll
and the `acc_regs` arrays become fixed register files -- the bitcode-LTO
equivalent of a C++ template, as the README describes.
- bf16 (`MMA_X86_AVX512BF16_1x16x2_F32_BF16`): one `_mm512_dpbf16_ps`
per (m, n, k), with the LHS K-pair broadcast via `set1_ps`.
- i8 (`MMA_X86_AVX512VNNI_16x16x2_I32_I8_CASTI16`): the 16x16x2 tile is
bit-compatible with the codegen path `lowerX86Avx512Vnni16x16x2I8` --
one `vpmovsxbw` widen of each i8 panel to i16, the `vpshufd` /
`vbroadcasti32x4` fan-out, and 16 `vpdpwssd` over the block-interleaved
(rlo, chi, rhi, clo) ACC layout. The i8 ukernel needs `-mavx512bw` for
the widen, so it is added to the VNNI copts.
`LLVMCPUSelectUKernels` now only selects a ukernel when its bitcode
actually exists (via `attachUKernelBitcodeOnOp`'s bool return), so an
`MMAIntrinsic` the cost model picks but for which no seed exists -- e.g.
the M<->N-swapped `MMA_X86_AVX512BF16_16x1x2_F32_BF16` -- falls back to
codegen instead of dangling an undefined symbol.
Adds two execution/numerical tests, the first of the new C-bitcode
ukernel path: `e2e_matmul_cpu_dt_inner_tiled_llvm_ukernel_bf16_f32`
(avx512bf16) and `..._i8_i32` (avx512vnni). Each compiles a data-tiled
matmul with `--iree-llvmcpu-enable-llvm-ukernels=inner_tiled`, links the
ukernel bitcode, runs on host and checks results against a reference --
exercising the operand threading and generic `intrinsics_{m,n,k}`
unrolling that the IR-level lit tests cannot. Both were confirmed to
actually select their ukernel (not silently fall back to codegen).
Progress towards https://github.com/iree-org/iree/issues/24574.
Signed-off-by: Benoit Jacob <jacob.benoit.1@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>diff --git a/compiler/plugins/target/LLVMCPU/builtins/ukernel/BUILD.bazel b/compiler/plugins/target/LLVMCPU/builtins/ukernel/BUILD.bazel
index d99ef18..7b9a8e1 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/BUILD.bazel
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/BUILD.bazel
@@ -42,6 +42,7 @@
X86_64_AVX512_VNNI_COPTS = [
"-mavx512f",
+ "-mavx512bw",
"-mavx512vnni",
]
diff --git a/compiler/plugins/target/LLVMCPU/builtins/ukernel/CMakeLists.txt b/compiler/plugins/target/LLVMCPU/builtins/ukernel/CMakeLists.txt
index c7b3069..3e33a47 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/CMakeLists.txt
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/CMakeLists.txt
@@ -43,6 +43,7 @@
"iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16.c"
COPTS
"-mavx512f"
+ "-mavx512bw"
"-mavx512vnni"
)
diff --git a/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16.c b/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16.c
index c7a5728..65d1ad7 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16.c
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16.c
@@ -13,39 +13,97 @@
// intrinsic name verbatim (lowercased, with the `iree_uk_` prefix), in line
// with the AMDGPU C ukernel convention.
//
-// The "inner K loop" the ukernel owns is the loop over the K *tiles* that
-// sits *inside* the outer M/N loops; those outer M/N loops are tiled away by
-// ordinary IREE tiling before this ukernel runs. The ukernel handles
-// arbitrary positive `intrinsics_{m,n,k}` (passed as arguments and looped
-// over); the loops fully unroll after the ukernel is inlined into its
-// constant-`intrinsics_*` caller -- the bitcode-LTO equivalent of a C++
-// template.
+// Adapted from the AMDGPU C ukernel
+// `iree_uk_amdgpu_multi_mma_mfma_i32_16x16x32_i8`: same shape — accumulators
+// held in registers, an outer loop over the K dimension, and inside it the
+// `(intrinsics_m, intrinsics_n, intrinsics_k)` unrolling — minus the
+// GPU-specific shared-memory / subgroup machinery.
+//
+// The "inner K loop" the ukernel owns is the loop over the K *tiles*
+// (`k_outer` below) that sits *inside* the outer M/N loops; those outer M/N
+// loops are tiled away by ordinary IREE tiling before this ukernel runs.
+// This is NOT a restriction to `intrinsics_{m,n,k} = 1`: the ukernel handles
+// arbitrary positive `intrinsics_{m,n,k}` via the `for` loops below.
+//
+// `intrinsics_{m,n,k}` are passed as function arguments and so look like
+// runtime values inside this translation unit, but the ukernel is always
+// inlined into its caller (a bug otherwise) and the caller always passes the
+// matching `DataTiledMMAAttr` constants. Together with post-inline IR
+// optimization on the linked bitcode, the `for` loops fully unroll and the
+// `acc_regs` VLA becomes a fixed register array specialized to each call
+// site's `intrinsics_{m,n,k}` — the bitcode-LTO equivalent of C++ templates.
//
// ABI: each shaped operand is passed as (base pointer, element offset) so the
-// caller doesn't need a GEP before the call. No strides are passed: the ACC
-// tile is contiguous, so the ukernel addresses each intrinsic's fragment from
-// `intrinsics_{m,n}` and its own fixed fragment size.
+// caller doesn't need a GEP before the call (the offset is added here). No
+// strides are passed: the ACC tile is contiguous, so the ukernel addresses
+// each fragment from `intrinsics_{m,n}` and its fixed fragment size. Offsets
+// are in units of the operand element type (bf16 for LHS/RHS, f32 for ACC).
//
-// NOTE (seed scaffolding): this initial seed has a stub body. It exists so
-// that the surrounding *framework* -- bitcode build, embedding,
-// `hal.executable_object` injection, IR rewrite to `ukernel.generic` -- can
-// be landed and lit-tested. A follow-up commit replaces the body with the
-// `_mm512_dpbf16_ps`-based inner loop and adds an e2e matmul test for it.
+// Data-tiled operand layout (matching the `DataTiledMMAAttr` swizzle, same as
+// the AMDGPU ukernel):
+// - ACC: one `__m512` (= M0=1 x N0=16 f32) per (m, n) intrinsic, tightly
+// packed row-major over the (m, n) grid, so fragment (m, n) is at
+// `acc + (m * intrinsics_n + n) * 16`.
+// - LHS: per outer-K step, `intrinsics_m * intrinsics_k` units of 2 bf16
+// (= one M0=1 x K0=2 fragment = a 4-byte `vdpbf16ps` m_bcst unit),
+// ordered [m][k]; consecutive outer-K steps are contiguous.
+// - RHS: per outer-K step, `intrinsics_n * intrinsics_k` panels of 32 bf16
+// (= one N0=16 x K0=2 fragment = one `__m512`), ordered [n][k];
+// consecutive outer-K steps are contiguous.
IREE_UK_ALWAYS_INLINE
void iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16(
const uint16_t *lhs_base, int64_t lhs_offset, const uint16_t *rhs_base,
int64_t rhs_offset, float *acc_base, int64_t acc_offset, int32_t k_outer,
int32_t intrinsics_m, int32_t intrinsics_n, int32_t intrinsics_k) {
- (void)lhs_base;
- (void)lhs_offset;
- (void)rhs_base;
- (void)rhs_offset;
- (void)acc_base;
- (void)acc_offset;
- (void)k_outer;
- (void)intrinsics_m;
- (void)intrinsics_n;
- (void)intrinsics_k;
- // TODO(ukernels): real inner K loop using `_mm512_dpbf16_ps`, looping over
- // intrinsics_{m,n,k}.
+ const uint16_t *lhs = lhs_base + lhs_offset;
+ const float *rhs = (const float *)(rhs_base + rhs_offset);
+ float *acc = acc_base + acc_offset;
+
+ // The ACC tile is contiguous: each intrinsic's fragment is one __m512
+ // (M0*N0 = 1*16 f32), so fragment (m, n) sits
+ // `(m * intrinsics_n + n) * kAccFragElems` elements into `acc`.
+ enum { kAccFragElems = 16 };
+
+ // One accumulator register (1x16 f32) per (m, n) intrinsic. The VLA
+ // dimensions are compile-time constants at the inlined call site, so this
+ // lowers to a fixed register array.
+ __m512 acc_regs[intrinsics_m][intrinsics_n];
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ acc_regs[m][n] =
+ _mm512_loadu_ps(acc + (m * intrinsics_n + n) * kAccFragElems);
+ }
+ }
+
+ for (int32_t ko = 0; ko < k_outer; ++ko) {
+ const uint16_t *lhs_block =
+ lhs + (int64_t)ko * intrinsics_m * intrinsics_k * 2;
+ const float *rhs_block =
+ rhs + (int64_t)ko * intrinsics_n * intrinsics_k * 16;
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ for (int32_t k = 0; k < intrinsics_k; ++k) {
+ // LHS fragment: 2 bf16 (one M-row's K-pair) broadcast across the
+ // 16 SIMD lanes via `set1_ps` (the splat shape `vdpbf16ps`'s
+ // m_bcst variant pattern-matches). The bitcast to `__m512bh` is a
+ // width-preserving no-op LLVM elides.
+ __m512 lhs_bcast = _mm512_set1_ps(
+ *(const float *)(lhs_block + (m * intrinsics_k + k) * 2));
+ // RHS fragment: one (N=16 x K=2) bf16 panel = 16 f32.
+ __m512 rhs_panel =
+ _mm512_loadu_ps(rhs_block + (n * intrinsics_k + k) * 16);
+ acc_regs[m][n] =
+ _mm512_dpbf16_ps(acc_regs[m][n], *(const __m512bh *)&lhs_bcast,
+ *(const __m512bh *)&rhs_panel);
+ }
+ }
+ }
+ }
+
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ _mm512_storeu_ps(acc + (m * intrinsics_n + n) * kAccFragElems,
+ acc_regs[m][n]);
+ }
+ }
}
diff --git a/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16.c b/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16.c
index 9e27639..c13da43 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16.c
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16.c
@@ -13,47 +13,118 @@
// intrinsic name verbatim (lowercased, with the `iree_uk_` prefix), in line
// with the AMDGPU C ukernel convention.
//
-// Implements the inner K-loop for the unrolled (intrinsics_m, intrinsics_n,
-// intrinsics_k) tile built from the 16x16x2 i8 VNNI intrinsic via AVX-512
-// VNNI `vpdpwssd`. The "CASTI16" in the MMA intrinsic name reflects that
-// the s8 inputs are zero/sign-extended into i16 lanes before being fed to
-// the 16-bit VNNI instruction; that cast is handled in the inner loop.
+// Structurally identical to `iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16`:
+// accumulators held in registers, an outer loop over the K *tiles*
+// (`k_outer`), and inside it the `(intrinsics_m, intrinsics_n, intrinsics_k)`
+// unroll. This is NOT a restriction to `intrinsics_{m,n,k} = 1`: the ukernel
+// handles arbitrary positive `intrinsics_{m,n,k}` via the `for` loops below,
+// which fully unroll once inlined into the constant-`intrinsics_*` caller.
//
-// `intrinsics_{m,n,k}` are passed as function arguments and so look like
-// runtime values inside this translation unit, but the ukernel is always
-// inlined into its caller (a bug otherwise) and the caller always passes
-// the matching `DataTiledMMAAttr` constants. Together with post-inline IR
-// optimization on the linked bitcode, the body specializes to specific
-// compile-time `intrinsics_{m,n,k}` values at each call site.
+// The "CASTI16" in the intrinsic name reflects that the s8 inputs are
+// sign-extended to i16 lanes before being fed to the 16-bit VNNI instruction
+// `vpdpwssd` (the 8-bit `vpdpbusd` would mishandle the s8 x s8 signedness);
+// that widen happens once per panel in the inner loop.
//
-// NOTE (seed scaffolding): this initial seed has a stub body. It exists so
-// that the surrounding *framework* -- bitcode build, embedding,
-// `hal.executable_object` injection, IR rewrite to `ukernel.generic` -- can
-// be exercised end-to-end. A follow-up commit replaces the body with the
-// `_mm512_dpwssd_epi32`-based inner loop and adds an e2e matmul test for
-// it. This is the "practically useful" seed: i8x i8->i32 via VNNI is a
-// workhorse for quantized inference, and codegen has a residual perf gap
-// on this case.
-// ABI matches the inner_tiled -> ukernel.generic lowering (see
-// `iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16`): each shaped operand passed
-// as (base, element offset) only (no strides; the ACC tile is contiguous),
-// then the scalar `k_outer` / `intrinsics_{m,n,k}`.
+// ABI: each shaped operand is passed as (base pointer, element offset) so the
+// caller doesn't need a GEP before the call (the offset is added here). No
+// strides are passed: the ACC tile is contiguous, so the ukernel addresses
+// each fragment from `intrinsics_{m,n}` and its fixed fragment size. Offsets
+// are in units of the operand element type (i8 for LHS/RHS, i32 for ACC).
+//
+// Per-intrinsic (16x16x2) tile, matching `lowerX86Avx512Vnni16x16x2I8` in
+// IREECPUAttrs.cpp (the codegen path this ukernel must be bit-compatible
+// with) and the `getIntrinsicSwizzle` data layout:
+// - LHS: one row-major 16x2 i8 panel (= 32 i8 = <32xi8>). Dword `x` of its
+// i16-widened form holds the (k0, k1) pair of LHS row `x`.
+// - RHS: one row-major 16x2 i8 panel (= 32 i8), same shape; dword `x` holds
+// column `x`'s (k0, k1) pair.
+// - ACC: one 16x16 i32 tile (= 256 i32) in the block-interleaved
+// (rlo, chi, rhi, clo) order, with row r = 4*rhi + rlo and column
+// c = 4*chi + clo. The 16 i32 at flat offset (4*rlo + chi)*16 are one
+// `vpdpwssd` accumulator, whose dword `4*rhi + clo` holds ACC element
+// (r, c). The (m, n) grid of 16x16 tiles is tightly packed, so fragment
+// (m, n) is at `acc + (m * intrinsics_n + n) * 256`.
IREE_UK_ALWAYS_INLINE
void iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16(
const void *lhs_base, int64_t lhs_offset, const void *rhs_base,
int64_t rhs_offset, void *acc_base, int64_t acc_offset, int32_t k_outer,
int32_t intrinsics_m, int32_t intrinsics_n, int32_t intrinsics_k) {
- (void)lhs_base;
- (void)lhs_offset;
- (void)rhs_base;
- (void)rhs_offset;
- (void)acc_base;
- (void)acc_offset;
- (void)k_outer;
- (void)intrinsics_m;
- (void)intrinsics_n;
- (void)intrinsics_k;
- // TODO(ukernels): real inner K loop using `_mm512_dpwssd_epi32` after
- // widening the s8 LHS/RHS halves to i16 lanes (loop over
- // intrinsics_{m,n,k} like the bf16 ukernel).
+ const int8_t *lhs = (const int8_t *)lhs_base + lhs_offset;
+ const int8_t *rhs = (const int8_t *)rhs_base + rhs_offset;
+ int32_t *acc = (int32_t *)acc_base + acc_offset;
+
+ // The ACC tile is contiguous: each intrinsic's fragment is one 16x16 i32
+ // tile (256 i32), so fragment (m, n) sits
+ // `(m * intrinsics_n + n) * kAccFragElems` elements into `acc`.
+ enum { kAccFragElems = 256 };
+
+ // 256 i32 (= 16 __m512i `vpdpwssd` accumulators) per (m, n) intrinsic. The
+ // VLA dimensions are compile-time constants at the inlined call site, so
+ // this lowers to a fixed register array.
+ __m512i acc_regs[intrinsics_m][intrinsics_n][16];
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ const int32_t *frag = acc + (m * intrinsics_n + n) * kAccFragElems;
+ for (int c = 0; c < 16; ++c) {
+ acc_regs[m][n][c] = _mm512_loadu_si512(frag + c * 16);
+ }
+ }
+ }
+
+ for (int32_t ko = 0; ko < k_outer; ++ko) {
+ // Each (m, k) / (n, k) fragment is one 16x2 i8 panel = 32 i8.
+ const int8_t *lhs_block =
+ lhs + (int64_t)ko * intrinsics_m * intrinsics_k * 32;
+ const int8_t *rhs_block =
+ rhs + (int64_t)ko * intrinsics_n * intrinsics_k * 32;
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ __m512i(*regs)[16] = &acc_regs[m][n];
+ for (int32_t k = 0; k < intrinsics_k; ++k) {
+ // Widen each i8 panel to i16 once (one `vpmovsxbw`); dword `x` then
+ // holds the (k0, k1) pair of LHS row / RHS column `x`.
+ __m512i lhs_i16 = _mm512_cvtepi8_epi16(_mm256_loadu_si256(
+ (const __m256i *)(lhs_block + (m * intrinsics_k + k) * 32)));
+ __m512i rhs_i16 = _mm512_cvtepi8_epi16(_mm256_loadu_si256(
+ (const __m256i *)(rhs_block + (n * intrinsics_k + k) * 32)));
+ // lhs_dup[rlo]: `vpshufd` broadcasting dword `4*lane + rlo` across
+ // each 128-bit lane (lane L then holds LHS row 4*L + rlo).
+ // rhs_bcast[chi]: `vbroadcasti32x4` of the 128-bit block of columns
+ // [4*chi, 4*chi+4) to all 4 lanes. The shuffle immediates must be
+ // compile-time constants (`s * 0x55` for s = 0..3), so the 4 cases
+ // are spelled out rather than looped.
+ __m512i lhs_dup[4] = {
+ _mm512_shuffle_epi32(lhs_i16, (_MM_PERM_ENUM)0x00),
+ _mm512_shuffle_epi32(lhs_i16, (_MM_PERM_ENUM)0x55),
+ _mm512_shuffle_epi32(lhs_i16, (_MM_PERM_ENUM)0xAA),
+ _mm512_shuffle_epi32(lhs_i16, (_MM_PERM_ENUM)0xFF),
+ };
+ __m512i rhs_bcast[4] = {
+ _mm512_shuffle_i32x4(rhs_i16, rhs_i16, 0x00),
+ _mm512_shuffle_i32x4(rhs_i16, rhs_i16, 0x55),
+ _mm512_shuffle_i32x4(rhs_i16, rhs_i16, 0xAA),
+ _mm512_shuffle_i32x4(rhs_i16, rhs_i16, 0xFF),
+ };
+ // 16 `vpdpwssd` over the 4x4 (rlo, chi) grid; accumulator (rlo, chi)
+ // lives at flat offset (4*rlo + chi)*16.
+ for (int rlo = 0; rlo < 4; ++rlo) {
+ for (int chi = 0; chi < 4; ++chi) {
+ int idx = 4 * rlo + chi;
+ (*regs)[idx] = _mm512_dpwssd_epi32((*regs)[idx], lhs_dup[rlo],
+ rhs_bcast[chi]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (int32_t m = 0; m < intrinsics_m; ++m) {
+ for (int32_t n = 0; n < intrinsics_n; ++n) {
+ int32_t *frag = acc + (m * intrinsics_n + n) * kAccFragElems;
+ for (int c = 0; c < 16; ++c) {
+ _mm512_storeu_si512(frag + c * 16, acc_regs[m][n][c]);
+ }
+ }
+ }
}
diff --git a/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir b/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir
index 2952f41..5365ba7 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir
@@ -98,3 +98,89 @@
// CHAIN: iree_codegen.inner_tiled
// CHAIN-NOT: iree_codegen.ukernel.generic
// CHAIN-NOT: hal.executable.objects
+
+// -----
+
+// Same configuration as `bf16_inner_tiled_ukernel_enabled` but with
+// `intrinsics_m = 2`. Arbitrary positive `intrinsics_{m,n,k}` are
+// supported — the ukernel loops over them — so `selectCPUUKernel` matches
+// this case just like the unit one, and the chained pipeline rewrites it
+// to a `ukernel.generic`.
+#executable_target_enabled_unrolled = #hal.executable.target<"llvm-cpu", "embedded-elf-x86_64", {
+ cpu_features = "+avx512f,+avx512bf16",
+ data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
+ iree_codegen.ukernel_provider = #iree_cpu.ukernel_provider,
+ llvm_ukernels = "inner_tiled",
+ native_vector_size = 64 : index,
+ target_triple = "x86_64-unknown-unknown-eabi-elf"
+}>
+
+func.func @bf16_inner_tiled_ukernel_unrolled_accepted(
+ %lhs: tensor<2x4x2x1x2xbf16>, %rhs: tensor<2x4x1x16x2xbf16>, %acc: tensor<2x2x2x1x1x16xf32>
+ ) -> tensor<2x2x2x1x1x16xf32>
+ attributes {hal.executable.target = #executable_target_enabled_unrolled} {
+ %0 = iree_codegen.inner_tiled ins(%lhs, %rhs) outs(%acc) {
+ indexing_maps = [
+ affine_map<(d0, d1, d2) -> (d0, d2)>,
+ affine_map<(d0, d1, d2) -> (d1, d2)>,
+ affine_map<(d0, d1, d2) -> (d0, d1)>
+ ],
+ iterator_types = [#linalg.iterator_type<parallel>,
+ #linalg.iterator_type<parallel>,
+ #linalg.iterator_type<reduction>],
+ kind = #iree_cpu.data_tiled_mma_layout<intrinsic = MMA_X86_AVX512BF16_1x16x2_F32_BF16, intrinsics_m = 2>,
+ semantics = #iree_cpu.mma_semantics<>
+ } : tensor<2x4x2x1x2xbf16>, tensor<2x4x1x16x2xbf16> into tensor<2x2x2x1x1x16xf32>
+ return %0 : tensor<2x2x2x1x1x16xf32>
+}
+// CHECK-LABEL: func.func @bf16_inner_tiled_ukernel_unrolled_accepted
+// CHECK: iree_codegen.inner_tiled
+// CHECK-SAME: iree_codegen.ukernel = #iree_codegen.ukernel_descriptor<"iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16", bitcode>
+
+// CHAIN-LABEL: func.func @bf16_inner_tiled_ukernel_unrolled_accepted
+// CHAIN: iree_codegen.ukernel.generic
+// CHAIN-SAME: "iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16"
+// CHAIN-NOT: iree_codegen.inner_tiled
+
+// -----
+
+// No-bitcode fallback: `selectCPUUKernel` maps every intrinsic name (so this
+// M<->N-swapped 16x1x2 orientation yields the name
+// `iree_uk_mma_x86_avx512bf16_16x1x2_f32_bf16`), but only intrinsics with a
+// built-in bitcode seed are actually selected. 16x1x2 has no seed, so
+// `attachUKernelBitcodeOnOp` finds no matching bitcode and the op falls back
+// to codegen: no descriptor is attached, and the chained pipeline leaves the
+// `inner_tiled` untouched.
+#executable_target_no_bitcode = #hal.executable.target<"llvm-cpu", "embedded-elf-x86_64", {
+ cpu_features = "+avx512f,+avx512bf16",
+ data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128",
+ iree_codegen.ukernel_provider = #iree_cpu.ukernel_provider,
+ llvm_ukernels = "inner_tiled",
+ native_vector_size = 64 : index,
+ target_triple = "x86_64-unknown-unknown-eabi-elf"
+}>
+
+func.func @bf16_inner_tiled_ukernel_no_bitcode(
+ %lhs: tensor<2x4x16x2xbf16>, %rhs: tensor<2x4x1x2xbf16>, %acc: tensor<2x2x16x1xf32>
+ ) -> tensor<2x2x16x1xf32> attributes {hal.executable.target = #executable_target_no_bitcode} {
+ %0 = iree_codegen.inner_tiled ins(%lhs, %rhs) outs(%acc) {
+ indexing_maps = [
+ affine_map<(d0, d1, d2) -> (d0, d2)>,
+ affine_map<(d0, d1, d2) -> (d1, d2)>,
+ affine_map<(d0, d1, d2) -> (d0, d1)>
+ ],
+ iterator_types = [#linalg.iterator_type<parallel>,
+ #linalg.iterator_type<parallel>,
+ #linalg.iterator_type<reduction>],
+ kind = #iree_cpu.data_tiled_mma_layout<intrinsic = MMA_X86_AVX512BF16_16x1x2_F32_BF16>,
+ semantics = #iree_cpu.mma_semantics<>
+ } : tensor<2x4x16x2xbf16>, tensor<2x4x1x2xbf16> into tensor<2x2x16x1xf32>
+ return %0 : tensor<2x2x16x1xf32>
+}
+// CHECK-LABEL: func.func @bf16_inner_tiled_ukernel_no_bitcode
+// CHECK: iree_codegen.inner_tiled
+// CHECK-NOT: iree_codegen.ukernel = #iree_codegen.ukernel_descriptor
+
+// CHAIN-LABEL: func.func @bf16_inner_tiled_ukernel_no_bitcode
+// CHAIN: iree_codegen.inner_tiled
+// CHAIN-NOT: iree_codegen.ukernel.generic
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUSelectUKernels.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUSelectUKernels.cpp
index bb97786..c52b714 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUSelectUKernels.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/LLVMCPUSelectUKernels.cpp
@@ -54,6 +54,10 @@
return {};
}
+ // Any positive `intrinsics_{m,n,k}` is supported: the ukernel takes them as
+ // arguments and loops over them, and those loops fully unroll after the
+ // ukernel is inlined into its (constant-`intrinsics_*`) caller.
+
auto execTarget = IREE::HAL::ExecutableTargetAttr::lookup(op);
if (!execTarget) {
return {};
@@ -74,7 +78,16 @@
// (`ensureUKernelBitcodeAndFinalizeConfig` in
// `compiler/src/iree/compiler/Codegen/LLVMGPU/Utils/LLVMGPUSelectUKernels.cpp`)
// and makes the configuration-pass output self-contained for lit tests.
- IREE::CPU::attachUKernelBitcodeOnOp(op, name);
+ //
+ // Only select the ukernel if its bitcode actually exists. Not every
+ // `MMAIntrinsic` the cost model picks has a built-in ukernel — e.g. the
+ // M<->N-swapped `MMA_X86_AVX512BF16_16x1x2_F32_BF16` orientation has no
+ // seed even though its natural sibling does — and matching one without
+ // bitcode would dangle an undefined symbol at link time. When absent, fall
+ // back to codegen by returning {}.
+ if (!IREE::CPU::attachUKernelBitcodeOnOp(op, name)) {
+ return {};
+ }
MLIRContext *context = op->getContext();
return IREE::Codegen::UKernelDescriptorAttr::get(
diff --git a/tests/e2e/matmul/BUILD.bazel b/tests/e2e/matmul/BUILD.bazel
index 0ae01d9..872298a 100644
--- a/tests/e2e/matmul/BUILD.bazel
+++ b/tests/e2e/matmul/BUILD.bazel
@@ -270,6 +270,72 @@
("bf16", "f32"),
]]
+# LLVMCPU, data-tiling lowering to iree_codegen.inner_tiled AND routing the
+# inner_tiled op through a built-in C-bitcode microkernel via
+# `--iree-llvmcpu-enable-llvm-ukernels=inner_tiled`. Restricted to the
+# avx512bf16 variant, which is the one for which a bf16 ukernel
+# (`iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16`) exists; this is the
+# numerical end-to-end check that the ukernel (and the generic
+# `intrinsics_{m,n,k}` unrolling + operand threading) computes correct
+# results, complementing the IR-level lit tests under
+# compiler/plugins/target/LLVMCPU/builtins/ukernel/test/.
+iree_generated_e2e_runner_test(
+ name = "e2e_matmul_cpu_dt_inner_tiled_llvm_ukernel_bf16_f32",
+ compiler_flags = [
+ "--iree-opt-data-tiling",
+ "--iree-llvmcpu-enable-inner-tiled",
+ "--iree-llvmcpu-enable-llvm-ukernels=inner_tiled",
+ ],
+ generator = ":generate_e2e_matmul_tests",
+ generator_args = [
+ "--lhs_rhs_type=bf16",
+ "--acc_type=f32",
+ ],
+ tags = [
+ # bf16 triggers internal LLVM assertion errors on riscv and wasm.
+ "noriscv",
+ "nowasm",
+ ],
+ target_backends_and_drivers = [
+ ("llvm-cpu", "local-task"),
+ ],
+ target_cpu_features_variants = [
+ "x86_64:avx512bf16:" + ",".join(X86_64_AVX512_BF16),
+ ],
+ test_runner = "//tools/testing/e2e:iree-e2e-matmul-test",
+ test_type = "matmul",
+)
+
+# Companion of the bf16 test above for the symmetric signed-i8 -> i32 path:
+# routes the data-tiled inner_tiled op through the C-bitcode microkernel
+# `iree_uk_mma_x86_avx512vnni_16x16x2_i32_i8_casti16` (x86 AVX-512 VNNI
+# `vpdpwssd`, with the s8 inputs widened to i16). Restricted to the
+# avx512vnni variant, which is the one for which that ukernel exists; this
+# is the numerical end-to-end check that the ukernel computes correct
+# results, complementing the IR-level lit tests under
+# compiler/plugins/target/LLVMCPU/builtins/ukernel/test/.
+iree_generated_e2e_runner_test(
+ name = "e2e_matmul_cpu_dt_inner_tiled_llvm_ukernel_i8_i32",
+ compiler_flags = [
+ "--iree-opt-data-tiling",
+ "--iree-llvmcpu-enable-inner-tiled",
+ "--iree-llvmcpu-enable-llvm-ukernels=inner_tiled",
+ ],
+ generator = ":generate_e2e_matmul_tests",
+ generator_args = [
+ "--lhs_rhs_type=i8",
+ "--acc_type=i32",
+ ],
+ target_backends_and_drivers = [
+ ("llvm-cpu", "local-task"),
+ ],
+ target_cpu_features_variants = [
+ "x86_64:avx512vnni:" + ",".join(X86_64_AVX512_VNNI),
+ ],
+ test_runner = "//tools/testing/e2e:iree-e2e-matmul-test",
+ test_type = "matmul",
+)
+
# Mixed unsigned-LHS / signed-RHS i8 matmul: exercises x86 AVX-512 VNNI
# `vpdpbusd`, our first asymmetric MMA. The `generic` variant exercises
# the standard codegen fallback (no matching MMA intrinsic).
diff --git a/tests/e2e/matmul/CMakeLists.txt b/tests/e2e/matmul/CMakeLists.txt
index 58b4c09..e31ef11 100644
--- a/tests/e2e/matmul/CMakeLists.txt
+++ b/tests/e2e/matmul/CMakeLists.txt
@@ -716,6 +716,57 @@
iree_generated_e2e_runner_test(
NAME
+ e2e_matmul_cpu_dt_inner_tiled_llvm_ukernel_bf16_f32
+ TEST_TYPE
+ matmul
+ GENERATOR
+ "generate_e2e_matmul_tests.py"
+ GENERATOR_ARGS
+ "--lhs_rhs_type=bf16"
+ "--acc_type=f32"
+ TEST_RUNNER
+ iree_tools_testing_e2e_iree-e2e-matmul-test
+ TARGET_BACKENDS
+ "llvm-cpu"
+ DRIVERS
+ "local-task"
+ COMPILER_FLAGS
+ "--iree-opt-data-tiling"
+ "--iree-llvmcpu-enable-inner-tiled"
+ "--iree-llvmcpu-enable-llvm-ukernels=inner_tiled"
+ LABELS
+ "noriscv"
+ "nowasm"
+ TARGET_CPU_FEATURES_VARIANTS
+ "x86_64:avx512bf16:+avx,+avx2,+fma,+f16c,+avx512f,+avx512vl,+avx512cd,+avx512bw,+avx512dq,+avx512bf16"
+)
+
+iree_generated_e2e_runner_test(
+ NAME
+ e2e_matmul_cpu_dt_inner_tiled_llvm_ukernel_i8_i32
+ TEST_TYPE
+ matmul
+ GENERATOR
+ "generate_e2e_matmul_tests.py"
+ GENERATOR_ARGS
+ "--lhs_rhs_type=i8"
+ "--acc_type=i32"
+ TEST_RUNNER
+ iree_tools_testing_e2e_iree-e2e-matmul-test
+ TARGET_BACKENDS
+ "llvm-cpu"
+ DRIVERS
+ "local-task"
+ COMPILER_FLAGS
+ "--iree-opt-data-tiling"
+ "--iree-llvmcpu-enable-inner-tiled"
+ "--iree-llvmcpu-enable-llvm-ukernels=inner_tiled"
+ TARGET_CPU_FEATURES_VARIANTS
+ "x86_64:avx512vnni:+avx,+avx2,+fma,+f16c,+avx512f,+avx512vl,+avx512cd,+avx512bw,+avx512dq,+avx512vnni"
+)
+
+iree_generated_e2e_runner_test(
+ NAME
e2e_matmul_cpu_dt_inner_tiled_ui8_i8_i32
TEST_TYPE
matmul