[Codegen] KernelDispatch recognition and tiling for data-tiled conv (#24716)
Teaches LLVMCPU KernelDispatch to recognize data-tiled convolution
dispatches produced by encoding materialization and assigns them a
dedicated root lowering config that vectorizes the output width (ow) and
the inner data-tiling channel dims (oc_inner, ic_inner, sizes k0/c0),
and distributes batch (unit tiles), output channel blocks (oc_outer),
and output height, with peeling enabled.
Co-authored-by: Jelle Schuhmacher <schuehmacher@roofline.ai>
Assisted-by: Claude code
Signed-off-by: Pooja Hemashekar <hemashekar@roofline.ai>
---------
Signed-off-by: Pooja Hemashekar <hemashekar@roofline.ai>
Co-authored-by: Jelle Schuhmacher <schuehmacher@roofline.ai>
diff --git a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.cpp b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.cpp
index d49aad4..12704af 100644
--- a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.cpp
+++ b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.cpp
@@ -10,6 +10,8 @@
#include "iree/compiler/Dialect/LinalgExt/Utils/MatchUtils.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/InterleavedRange.h"
+#include "mlir/Dialect/Arith/IR/Arith.h"
+#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/Linalg/IR/LinalgInterfaces.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/Dialect/Utils/StructuredOpsUtils.h"
@@ -690,4 +692,63 @@
return space;
}
+/// Recognize the data-tiled convolution generic emitted by encoding
+/// materialization, identified by its iterator types [P,P,P,P, R,R,R, P,R],
+/// operand ranks (input 5, filter 6, output 5), indexing maps, and
+/// multiply-accumulate body.
+bool isDataTiledConvGeneric(Operation *op) {
+ auto genericOp = dyn_cast<linalg::GenericOp>(op);
+ if (!genericOp) {
+ return false;
+ }
+ using IT = utils::IteratorType;
+ SmallVector<IT> iterTypes = genericOp.getIteratorTypesArray();
+ SmallVector<IT> expected = {IT::parallel, IT::parallel, IT::parallel,
+ IT::parallel, IT::reduction, IT::reduction,
+ IT::reduction, IT::parallel, IT::reduction};
+ if (iterTypes.size() != 9 || !llvm::equal(iterTypes, expected)) {
+ return false;
+ }
+
+ if (genericOp.getNumDpsInputs() != 2 || genericOp.getNumDpsInits() != 1) {
+ return false;
+ }
+ auto inputType = dyn_cast<RankedTensorType>(
+ genericOp.getDpsInputOperand(0)->get().getType());
+ auto filterType = dyn_cast<RankedTensorType>(
+ genericOp.getDpsInputOperand(1)->get().getType());
+ auto outputType = dyn_cast<RankedTensorType>(
+ genericOp.getDpsInitOperand(0)->get().getType());
+ if (!inputType || !filterType || !outputType) {
+ return false;
+ }
+ if (inputType.getRank() != 5 || filterType.getRank() != 6 ||
+ outputType.getRank() != 5) {
+ return false;
+ }
+
+ FailureOr<linalg::ConvolutionDimensions> cDims =
+ linalg::inferConvolutionDims(genericOp);
+ if (failed(cDims) || cDims->strides.size() != 2 ||
+ cDims->dilations.size() != 2) {
+ return false;
+ }
+
+ // Require the maps and iterator types to be exactly what the encoding
+ // materialization emits for this op's strides/dilations.
+ DataTiledConvIterationSpace space = getDataTiledConvIterationSpace(
+ genericOp.getContext(), cDims->strides, cDims->dilations);
+ if (genericOp.getIndexingMapsArray() != space.indexingMaps ||
+ genericOp.getIteratorTypesArray() != space.iteratorTypes) {
+ return false;
+ }
+
+ return linalg::detail::isContractionBody(
+ *genericOp.getBlock(),
+ [](Operation *mul, Operation *add) {
+ return isa<arith::MulFOp>(mul) && isa<arith::AddFOp>(add);
+ },
+ llvm::nulls());
+}
+
} // namespace mlir::iree_compiler::IREE::Codegen
diff --git a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.h b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.h
index b9c509d..5bfafb6 100644
--- a/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.h
+++ b/compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.h
@@ -165,6 +165,11 @@
getDataTiledConvIterationSpace(MLIRContext *ctx, ArrayRef<int64_t> strides,
ArrayRef<int64_t> dilations);
+/// Returns true if `op` is a 9D data-tiled convolution generic emitted by
+/// encoding materialization (matched by iterator types, operand ranks,
+/// indexing maps, and multiply-accumulate body).
+bool isDataTiledConvGeneric(Operation *op);
+
} // namespace mlir::iree_compiler::IREE::Codegen
#endif // IREE_COMPILER_CODEGEN_DIALECT_CODEGEN_UTILS_H_
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/BUILD.bazel b/compiler/src/iree/compiler/Codegen/LLVMCPU/BUILD.bazel
index c1e522d..4e81511 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/BUILD.bazel
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/BUILD.bazel
@@ -102,6 +102,7 @@
"//compiler/src/iree/compiler/Codegen/Dialect/CPU/IR:IREECPUDialect",
"//compiler/src/iree/compiler/Codegen/Dialect/Codegen/IR:IREECodegenDialect",
"//compiler/src/iree/compiler/Codegen/Dialect/Codegen/Transforms:IREECodegenTransforms",
+ "//compiler/src/iree/compiler/Codegen/Dialect/Codegen/Utils",
"//compiler/src/iree/compiler/Codegen/Dialect/VectorExt/IR:IREEVectorExtDialect",
"//compiler/src/iree/compiler/Codegen/Dialect/VectorExt/Transforms:VectorExtTransforms",
"//compiler/src/iree/compiler/Codegen/Interfaces:PartitionableLoopsInterface",
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/CMakeLists.txt b/compiler/src/iree/compiler/Codegen/LLVMCPU/CMakeLists.txt
index 1a5de09..d20f744 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/CMakeLists.txt
@@ -160,6 +160,7 @@
iree::compiler::Codegen::Dialect::CPU::IR::IREECPUDialect
iree::compiler::Codegen::Dialect::Codegen::IR::IREECodegenDialect
iree::compiler::Codegen::Dialect::Codegen::Transforms::IREECodegenTransforms
+ iree::compiler::Codegen::Dialect::Codegen::Utils
iree::compiler::Codegen::Dialect::VectorExt::IR::IREEVectorExtDialect
iree::compiler::Codegen::Dialect::VectorExt::Transforms::VectorExtTransforms
iree::compiler::Codegen::Interfaces::PartitionableLoopsInterface
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
index ede7559..83c8444 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/KernelDispatch.cpp
@@ -12,6 +12,7 @@
#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenInterfaces.h"
#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenOps.h"
#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenTypes.h"
+#include "iree/compiler/Codegen/Dialect/Codegen/Utils/Utils.h"
#include "iree/compiler/Codegen/Interfaces/PartitionableLoopsInterface.h"
#include "iree/compiler/Codegen/LLVMCPU/LLVMCPUSelectUKernels.h"
#include "iree/compiler/Codegen/LLVMCPU/TargetMLTransformInfo.h"
@@ -3114,6 +3115,65 @@
getCPUTranslationInfo(op.getContext(), CPUPipeline::Mmt4dTilingExpert));
}
+/// Assigns CPUDoubleTilingExpert pipeline config to the data-tiled
+/// convolution generic.
+static LogicalResult
+setConvDataTiledGenericRootConfig(mlir::FunctionOpInterface entryPointFn,
+ linalg::GenericOp convOp) {
+ // Loop ranges:
+ // (d0, d1, d2, d3, d4, d5, d6, d7, d8)
+ // (n, OC/k0, OH, OW, IC/c0, FH, FW, k0, c0)
+ SmallVector<int64_t, 9> loopRanges =
+ cast<linalg::LinalgOp>(convOp.getOperation()).getStaticLoopRanges();
+ int64_t k0 = loopRanges[7];
+ int64_t c0 = loopRanges[8];
+
+ // Vectorization strategy:
+ // - Vectorize across OW, the register-blocking dimension: the kernel
+ // carries one accumulator vector per OW element to hide FMA latency.
+ // Use k0 as the OW tile size for now; it matches the desired
+ // accumulator count on current targets (16 for AVX-512, 8 for NEON).
+ // TODO(phemashekar): Derive this directly from the target register budget
+ // instead.
+ // - Vectorize across the data-tiling inner dims oc_inner (k0) and
+ // ic_inner (c0).
+ SmallVector<int64_t> vecTileSizes(9, 1);
+ vecTileSizes[3] = k0;
+ vecTileSizes[7] = k0;
+ vecTileSizes[8] = c0;
+ setAlwaysVectorizeSizes(convOp, vecTileSizes);
+
+ // Distribute over N, OC/k0, and OH. N is limited to unit tiles: batch
+ // tile sizes are constrained by the generated temporary buffer sizes,
+ // and larger batch tiles can lead to stack allocation errors.
+ DistributionHeuristicConfig distConfig;
+ distConfig.maxTileSizes.assign(9, 0);
+ distConfig.maxTileSizes[0] = 1;
+ distConfig.maxTileSizes[1] = clDefaultDistTileSize / 2;
+ distConfig.maxTileSizes[2] = clDefaultDistTileSize / 2;
+ SmallVector<int64_t> distTileSizes =
+ getDefaultDistributedLevelTileSizes(convOp, distConfig);
+
+ LDBG() << "Data tiled convolution:";
+ LDBG() << " Dist tile sizes: " << distTileSizes;
+ LDBG() << " Vector tile sizes: " << vecTileSizes;
+
+ LoweringConfigGenerator generator(convOp);
+ generator.setDistributionTileSizes(distTileSizes);
+ generator.setVectorTileSizes(vecTileSizes);
+ IREE::CPU::LoweringConfigAttr loweringConfig =
+ generator.generateCPULoweringConfig();
+
+ // Enable loop peeling so the OW vectorization tile produces static-sized
+ // main loop iterations (vectorizable) with a scalar peeled tail.
+ DictionaryAttr pipelineConfig =
+ getPipelineConfWithPeelingAttr(convOp.getContext());
+ return setOpConfigAndEntryPointFnTranslation(
+ entryPointFn, convOp, loweringConfig,
+ getCPUTranslationInfo(convOp.getContext(),
+ CPUPipeline::DoubleTilingExpert, pipelineConfig));
+}
+
/// Redirects to methods that set the configuration based on operation type.
static LogicalResult
setRootConfigImpl(mlir::FunctionOpInterface entryPointFn, Operation *op,
@@ -3147,6 +3207,10 @@
is2DPoolingOp(linalgOp)) {
return setConvInterfaceRootConfig(entryPointFn, linalgOp);
}
+ if (IREE::Codegen::isDataTiledConvGeneric(op)) {
+ return setConvDataTiledGenericRootConfig(entryPointFn,
+ cast<linalg::GenericOp>(op));
+ }
if (linalg::isaContractionOpInterface(linalgOp) &&
meetLegacyContractionOpInterface(linalgOp)) {
return setContractionRootConfig(entryPointFn, linalgOp);
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_lowering_strategy.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_lowering_strategy.mlir
index 56f07be..23dccd6 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_lowering_strategy.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_aarch64_lowering_strategy.mlir
@@ -104,6 +104,28 @@
// -----
+#executable_target_embedded_elf_arm_64_ = #hal.executable.target<"llvm-cpu", "embedded-elf-arm_64", {data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", native_vector_size = 16 : index, target_triple = "aarch64-none-elf"}>
+#map = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d0, d4, d2 + d5, d3 + d6, d8)>
+#map1 = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d1, d4, d5, d6, d8, d7)>
+#map2 = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d0, d1, d2, d3, d7)>
+func.func @conv_2d_nchwc_data_tiled(%arg0: tensor<1x1x16x16x16xf32>, %arg1: tensor<1x1x3x3x16x16xf32>, %arg2: tensor<1x1x14x14x16xf32>) -> tensor<1x1x14x14x16xf32> attributes {hal.executable.target = #executable_target_embedded_elf_arm_64_} {
+ %0 = linalg.generic {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "parallel", "parallel", "reduction", "reduction", "reduction", "parallel", "reduction"]} ins(%arg0, %arg1 : tensor<1x1x16x16x16xf32>, tensor<1x1x3x3x16x16xf32>) outs(%arg2 : tensor<1x1x14x14x16xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %out: f32):
+ %1 = arith.mulf %in, %in_0 : f32
+ %2 = arith.addf %1, %out : f32
+ linalg.yield %2 : f32
+ } -> tensor<1x1x14x14x16xf32>
+ return %0 : tensor<1x1x14x14x16xf32>
+}
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_cpu.lowering_config<distribution = [0, 0, 1, 0, 0, 0, 0, 0, 0], vector_common_parallel = [1, 1, 1, 16, 0, 0, 0, 16, 0], vector_reduction = [0, 0, 0, 0, 1, 1, 1, 0, 16]>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_cpu.pipeline<DoubleTilingExpert>, {enable_loop_peeling}>
+// CHECK: func.func @conv_2d_nchwc_data_tiled(
+// CHECK-SAME: translation_info = #[[TRANSLATION]]
+// CHECK: linalg.generic
+// CHECK-SAME: lowering_config = #[[CONFIG]]
+
+// -----
+
#executable_target_system_elf_arm_64_ = #hal.executable.target<"llvm-cpu", "system-elf-arm_64", {data_layout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128", native_vector_size = 16 : index, target_triple = "aarch64-none-linux-android30"}>
func.func @restrict_num_workgroups(%3: tensor<1x11x11x576xf32>, %4: tensor<5x5x576xf32>) -> tensor<1x7x7x576xf32> attributes {hal.executable.target = #executable_target_system_elf_arm_64_} {
%cst = arith.constant 0.000000e+00 : f32
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_x86_64_lowering_strategy.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_x86_64_lowering_strategy.mlir
index 3317074..8fc1c54 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_x86_64_lowering_strategy.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/select_x86_64_lowering_strategy.mlir
@@ -455,6 +455,30 @@
// -----
+// The materialized data-tiled convolution generic is recognized via the
+// convolution interface and routed to the double-tiling pipeline.
+#executable_target_embedded_elf_x86_64_ = #hal.executable.target<"llvm-cpu", "embedded-elf-x86_64", {cpu_features = "+avx512f", data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", native_vector_size = 16 : index, target_triple = "x86_64-unknown-linux-gnu"}>
+#map = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d0, d4, d2 + d5, d3 + d6, d8)>
+#map1 = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d1, d4, d5, d6, d8, d7)>
+#map2 = affine_map<(d0, d1, d2, d3, d4, d5, d6, d7, d8) -> (d0, d1, d2, d3, d7)>
+func.func @conv_2d_nchwc_data_tiled(%arg0: tensor<1x1x16x16x16xf32>, %arg1: tensor<1x1x3x3x16x16xf32>, %arg2: tensor<1x1x14x14x16xf32>) -> tensor<1x1x14x14x16xf32> attributes {hal.executable.target = #executable_target_embedded_elf_x86_64_} {
+ %0 = linalg.generic {indexing_maps = [#map, #map1, #map2], iterator_types = ["parallel", "parallel", "parallel", "parallel", "reduction", "reduction", "reduction", "parallel", "reduction"]} ins(%arg0, %arg1 : tensor<1x1x16x16x16xf32>, tensor<1x1x3x3x16x16xf32>) outs(%arg2 : tensor<1x1x14x14x16xf32>) {
+ ^bb0(%in: f32, %in_0: f32, %out: f32):
+ %1 = arith.mulf %in, %in_0 : f32
+ %2 = arith.addf %1, %out : f32
+ linalg.yield %2 : f32
+ } -> tensor<1x1x14x14x16xf32>
+ return %0 : tensor<1x1x14x14x16xf32>
+}
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_cpu.lowering_config<distribution = [0, 0, 1, 0, 0, 0, 0, 0, 0], vector_common_parallel = [1, 1, 1, 16, 0, 0, 0, 16, 0], vector_reduction = [0, 0, 0, 0, 1, 1, 1, 0, 16]>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_cpu.pipeline<DoubleTilingExpert>, {enable_loop_peeling}>
+// CHECK: func.func @conv_2d_nchwc_data_tiled(
+// CHECK-SAME: translation_info = #[[TRANSLATION]]
+// CHECK: linalg.generic
+// CHECK-SAME: lowering_config = #[[CONFIG]]
+
+// -----
+
#executable_target_embedded_elf_x86_64_ = #hal.executable.target<"llvm-cpu", "embedded-elf-x86_64", {cpu = "cascadelake", cpu_features = "+mmx,+popcnt,+sse,+sse2,+sse3,+ssse3,+sse4.1,+sse4.2,+avx,+avx2,+fma,+avx512f,+bmi,+bmi2,+aes,+pclmul,+avx512vl,+avx512bw,+avx512dq,+avx512cd,+avx512vnni,+adx,+clflushopt,+clwb,+cx16,+cx8,+crc32,+f16c,+fsgsbase,+fxsr,+invpcid,+lzcnt,+movbe,+pku,+prfchw,+rdrnd,+rdseed,+sahf,+x87,+xsave,+xsavec,+xsaveopt,+xsaves", data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", native_vector_size = 32 : index, target_triple = "x86_64-none-elf", ukernels = false}>
func.func @pooling_nchw_max(%2: tensor<1x64x114x114xf32>) -> tensor<1x64x56x56xf32> attributes {hal.executable.target = #executable_target_embedded_elf_x86_64_} {
%cst = arith.constant -3.40282347E+38 : f32