[Codegen][CPU] Wire LowerBitcodeUKernels into the Mmt4dTilingExpert pipeline. (#24570)
Completes the new-style C-ukernel pipeline integration:
* `LLVMCPUTarget.cpp`: install `iree_codegen.ukernel_provider =
#iree_cpu.ukernel_provider` on every LLVMCPU executable target config
whenever `--iree-llvmcpu-enable-llvm-ukernels` is non-empty. This
mirrors what ROCM does and is what lets the generic
`LowerBitcodeUKernelsPass` find the CPU provider for the
bitcode-lookup-and-attach.
* `KernelDispatch.cpp::setRootConfig(InnerTiledOp)`: when
`selectUKernel` returns a descriptor, set the reduction-dim vector tile
size to 0 (untiled) instead of 1 (unrolled). The ukernel's C function
takes the outer K extent as a runtime argument (`k_outer`) and runs the
K loop itself; pre-unrolling here would both defeat that and bloat the
IR. Matches the equivalent split in
`setDataTiledMmaInnerTiledLoweringConfig` on the GPU side.
* `Passes.cpp::addMmt4dTilingExpertPassPipeline`: insert
`createLowerBitcodeUKernelsPass()` between the parallel-tile and
reduction-tile passes. A nop when no op carries a `iree_codegen.ukernel`
descriptor, so it stays in the default pipeline. The companion
`LowerUKernelOpsToCalls` already runs after bufferization (in
`addLowerToLLVMPasses`); together they implement the user's "early
`inner_tiled` -> `ukernel.generic` on tensors; late `ukernel.generic` ->
`func.call` on memrefs" split.
Lit test (`select_ukernel.mlir`) gains a second RUN line that chains
`iree-llvmcpu-select-lowering-strategy` with
`iree-codegen-lower-bitcode-ukernels` and asserts:
- the `inner_tiled` is rewritten to `iree_codegen.ukernel.generic`
carrying the right `iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16` name;
- the matching bitcode appears as `hal.executable.objects` (resolved
from the global `EmbeddedDataDirectory` populated at LLVMCPU plugin
init);
- and the original `inner_tiled` is gone, guarding against the rewrite
silently no-op'ing on a regression.
An end-to-end `iree-compile` test would also be desirable, but the
current pipeline state needs more wiring (workgroup-distribution
verifier complains about the bufferization-introduced copy ops around
the workgroup loop, on both the ukernel and non-ukernel paths) — that
investigation belongs with the follow-up commit that fills in real
ukernel bodies and an actual numerical matmul test.
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.7 <noreply@anthropic.com>
diff --git a/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp b/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
index ad04206..fda3035 100644
--- a/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
+++ b/compiler/plugins/target/LLVMCPU/LLVMCPUTarget.cpp
@@ -180,6 +180,14 @@
configItems.emplace_back(
b.getStringAttr(IREE::Encoding::kEncodingResolverAttrName),
IREE::CPU::CPUEncodingResolverAttr::get(context, {}));
+ // When at least one LLVM-bitcode ukernel category is enabled, install
+ // the CPU ukernel provider attribute so that the subsequent
+ // `LowerBitcodeUKernelsPass` can find and attach the matching
+ // bitcode.
+ if (!target.llvmUkernels.empty()) {
+ configItems.emplace_back(b.getStringAttr(kUKernelProviderName),
+ IREE::CPU::UKernelProviderAttr::get(context));
+ }
// Compute the format used at runtime to select the executable loader.
std::string format;
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 6fc0834..2952f41 100644
--- a/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir
+++ b/compiler/plugins/target/LLVMCPU/builtins/ukernel/test/select_ukernel.mlir
@@ -1,5 +1,7 @@
// RUN: iree-opt --pass-pipeline='builtin.module(iree-llvmcpu-select-lowering-strategy)' \
// RUN: --split-input-file %s | FileCheck %s
+// RUN: iree-opt --pass-pipeline='builtin.module(iree-llvmcpu-select-lowering-strategy,func.func(iree-codegen-lower-bitcode-ukernels))' \
+// RUN: --split-input-file %s | FileCheck %s --check-prefix=CHAIN
// Drives the LLVMCPU `LLVMCPUSelectLoweringStrategy` pass on
// `inner_tiled` rooted dispatches carrying a CPU `DataTiledMMAAttr`. The
@@ -10,10 +12,12 @@
// doesn't run target-option serialization).
// Enabled case: `selectCPUUKernel` matches the BF16 1x16x2 intrinsic and
-// annotates the op with `iree_codegen.ukernel = …`.
+// annotates the op with `iree_codegen.ukernel = …`. The chained CHAIN run
+// additionally lowers that descriptor to a `ukernel.generic`.
#executable_target_enabled = #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"
@@ -36,10 +40,22 @@
} : tensor<2x4x1x2xbf16>, tensor<2x4x16x2xbf16> into tensor<2x2x1x16xf32>
return %0 : tensor<2x2x1x16xf32>
}
+// The reduction (K) dim is left untiled: the ukernel owns the K loop, so the
+// lowering_config carries no `vector_reduction` entry (contrast the disabled
+// case below, which tiles it to `vector_reduction = [0, 0, 1]`).
+// CHECK: #iree_cpu.lowering_config<distribution = [1, 1, 0], vector_common_parallel = [1, 1, 0]>
// CHECK-LABEL: func.func @bf16_inner_tiled_ukernel_enabled
// CHECK: iree_codegen.inner_tiled
// CHECK-SAME: iree_codegen.ukernel = #iree_codegen.ukernel_descriptor<"iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16", bitcode>
+// The next pass lowers the descriptor to a `ukernel.generic` carrying the
+// resolved bitcode, and the original `inner_tiled` is gone.
+// CHAIN-LABEL: func.func @bf16_inner_tiled_ukernel_enabled
+// CHAIN: iree_codegen.ukernel.generic
+// CHAIN-SAME: hal.executable.objects = [{{.*}}"iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16.x86_64_avx512bf16.bc"
+// CHAIN-SAME: "iree_uk_mma_x86_avx512bf16_1x16x2_f32_bf16"
+// CHAIN-NOT: iree_codegen.inner_tiled
+
// -----
// Disabled case: target config lacks `llvm_ukernels`, so `selectCPUUKernel`
@@ -69,6 +85,16 @@
} : tensor<2x4x1x2xbf16>, tensor<2x4x16x2xbf16> into tensor<2x2x1x16xf32>
return %0 : tensor<2x2x1x16xf32>
}
+// Without a ukernel, the reduction (K) dim is tiled normally.
+// CHECK: #iree_cpu.lowering_config<distribution = [1, 1, 0], vector_common_parallel = [1, 1, 0], vector_reduction = [0, 0, 1]>
// CHECK-LABEL: func.func @bf16_inner_tiled_ukernel_disabled
// CHECK: iree_codegen.inner_tiled
// CHECK-NOT: iree_codegen.ukernel = #iree_codegen.ukernel_descriptor
+
+// Same with the chained pipeline: SelectLoweringStrategy doesn't attach a
+// descriptor, so LowerBitcodeUKernels has nothing to rewrite and the
+// `inner_tiled` survives untouched.
+// CHAIN-LABEL: func.func @bf16_inner_tiled_ukernel_disabled
+// CHAIN: iree_codegen.inner_tiled
+// CHAIN-NOT: iree_codegen.ukernel.generic
+// CHAIN-NOT: hal.executable.objects
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
index 8c4791b..c075923 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
@@ -3051,13 +3051,11 @@
assert(!getLoweringConfig(op) && "expected lowering_config is not set");
// Annotate the op with a ukernel descriptor if `selectUKernel` matches one.
// This is gated by `--iree-llvmcpu-enable-llvm-ukernels=inner_tiled` on the
- // target config; off by default. Tile sizes and translation_info below stay
- // the same — the ukernel handles only the innermost intrinsic execution,
- // and the surrounding tiling continues to do its job. The bitcode is
- // resolved and attached as `hal.executable.objects` later by the
+ // target config; off by default. When a ukernel is selected, the bitcode
+ // is resolved and attached as `hal.executable.objects` later by the
// `#iree_cpu.ukernel_provider` during `LowerBitcodeUKernelsPass`.
- if (IREE::Codegen::UKernelDescriptorAttr ukernelDescriptor =
- selectCPUUKernel(op)) {
+ IREE::Codegen::UKernelDescriptorAttr ukernelDescriptor = selectCPUUKernel(op);
+ if (ukernelDescriptor) {
setUKernelDescriptor(op, ukernelDescriptor);
}
SmallVector<int64_t> bounds;
@@ -3085,11 +3083,21 @@
}
}
- // Vector tiling: every iter dim down to a single inner tile. The
- // generator's `splitParallelAndReductionTiles` lifts the parallel entries
- // into `vector_common_parallel` and the reduction entries into
- // `vector_reduction`.
+ // Vector tiling: every parallel iter dim down to a single inner tile.
+ // Reduction dims tile to 1 by default — codegen unrolls the K loop. But
+ // when a ukernel is in play, leave reduction dims untiled (size 0): the
+ // ukernel's C function takes the outer K count as a runtime argument
+ // (`k_outer`) and runs the K loop itself; pre-unrolling it here would
+ // both defeat that and bloat the IR. This mirrors the equivalent split
+ // in `setDataTiledMmaInnerTiledLoweringConfig` on the GPU side.
SmallVector<int64_t> vecTileSizes(numLoops, 1);
+ if (ukernelDescriptor) {
+ for (auto [i, kind] : llvm::enumerate(iteratorTypes)) {
+ if (kind == utils::IteratorType::reduction) {
+ vecTileSizes[i] = 0;
+ }
+ }
+ }
LoweringConfigGenerator generator(op);
generator.setDistributionTileSizes(distTileSizes);
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
index e9f4fb6..16020e8 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/Passes.cpp
@@ -348,6 +348,15 @@
funcPassManager.addPass(createCPUPrepareUkernelsPass());
funcPassManager.addPass(createCPULowerToUKernelsPass(
pipelineOpt.cpuOpts.skipIntermediateRoundings));
+ // Rewrite `inner_tiled` ops carrying an `iree_codegen.ukernel` descriptor
+ // to `iree_codegen.ukernel.generic` calls (set earlier by `selectUKernel`
+ // in `KernelDispatch`, gated by `--iree-llvmcpu-enable-llvm-ukernels`).
+ // Must run after the parallel-tile pass above so the inner_tiled has its
+ // M/N outer extents tiled to single tiles, and before bufferization since
+ // the rewrite happens on tensors (the eventual `ukernel.generic` -> func
+ // call lowering happens after bufferize). A nop when no op carries the
+ // descriptor, so safe to leave in the default pipeline.
+ funcPassManager.addPass(createLowerBitcodeUKernelsPass());
funcPassManager.addPass(createLLVMCPUTileRootAndFuseInputOperandsPass(
IREE::CPU::TilingLevel::VectorReductionTiles));
// `VectorInnerParallelTiles` level models the tiling and fusion for the