[LLVMCPU] Make cpu_data feature field a compile-time constant (#24811)
The processor_data passed to a ukernel call is built with the CPU
feature bitmask set to a compile-time constant taken from the executable
target's cpu_features. The remaining fields of environment's cpu_data
(currently unused) are copied from the runtime processor data.
A constant feature field lets each ukernel's feature check fold at
compile time, so the matching tile is selected and inlined and the
off-target tile variants are DCE'd before codegen.
Assisted-by: Claude Code
---------
Signed-off-by: Ege Beysel <beyselege@gmail.com>
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
index d6e993a..13ccfd7 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/compiler/Codegen/LLVMCPU/DispatchABI.h"
+#include <cstdint>
#include "iree/compiler/Codegen/Utils/Utils.h"
#include "iree/schemas/cpu_data.h"
@@ -12,6 +13,7 @@
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/Path.h"
+#include "llvm/TargetParser/Triple.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Math/IR/Math.h"
#include "mlir/Dialect/Utils/StructuredOpsUtils.h"
@@ -893,38 +895,35 @@
di.getBasicType(resultValue.getType()), builder);
}
-Value HALDispatchABI::updateProcessorDataFromTargetAttr(
- Operation *forOp, Value processorDataPtrValue, OpBuilder &builder) {
- // Get the target attr.
- IREE::HAL::ExecutableTargetAttr targetAttr =
- IREE::HAL::ExecutableTargetAttr::lookup(forOp);
+static uint64_t getFeatureBitPattern(Operation *forOp) {
+ uint64_t specifiedCpuDataField0 = 0;
+ auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(forOp);
if (!targetAttr) {
- return processorDataPtrValue;
+ return specifiedCpuDataField0;
}
DictionaryAttr targetConfig = targetAttr.getConfiguration();
-
- // Lookup CPU features.
std::optional<StringRef> cpuFeatures = getConfigCpuFeatures(targetConfig);
if (!cpuFeatures) {
- return processorDataPtrValue;
+ return specifiedCpuDataField0;
}
-
- // Currently requiring all CPU feature bits to be in field 0. Generalize as
- // needed when other CPU feature fields start to be used.
- uint64_t specifiedCpuDataField0 = 0;
- {
- // Map llvm feature-name to bit used to represent it in IREE_CPUDATA_FIELD0.
- //
- // TODO(ravishankarm): This link to the runtime schemas needs to be broken.
- // Instead we should use a reflection callback to resolve arch guarded
- // features directly in the compiler.
- llvm::StringMap<uint64_t> featureToBitPattern;
- auto targetTriple = getTargetTriple(targetConfig);
- if (!targetTriple) {
- return processorDataPtrValue;
- }
- std::string targetArchUppercase =
- StringRef(getIreeArchNameForTargetTriple(targetTriple.value())).upper();
+ std::optional<llvm::Triple> targetTriple = getTargetTriple(targetConfig);
+ if (!targetTriple) {
+ return specifiedCpuDataField0;
+ }
+ // Currently requiring all CPU feature bits to be in field 0. Generalize
+ // as needed when other CPU feature fields start to be used.
+ // The remaining fields _can_ carry architecture-defined runtime processor
+ // data and are passed through unchanged.
+ //
+ // Map llvm feature-name to bit used to represent it in
+ // IREE_CPUDATA_FIELD0.
+ //
+ // TODO(ravishankarm): This link to the runtime schemas needs to be
+ // broken. Instead we should use a reflection callback to resolve arch
+ // guarded features directly in the compiler.
+ llvm::StringMap<uint64_t> featureToBitPattern;
+ std::string targetArchUppercase =
+ StringRef(getIreeArchNameForTargetTriple(targetTriple.value())).upper();
#define IREE_CPU_FEATURE_BIT(arch, field_index, bit_pos, bit_name, llvm_name) \
if (targetArchUppercase == #arch) { \
assert(field_index == 0); \
@@ -933,36 +932,36 @@
#include "iree/schemas/cpu_feature_bits.inl"
#undef IREE_CPU_FEATURE_BIT
- // Find CPU features in featureToBitPattern
- SmallVector<StringRef> cpuFeatureStrings;
- cpuFeatures.value().split(cpuFeatureStrings, ',', /*MakeSplit=*/-1,
- /*KeepEmpty=*/false);
- for (auto featureString : cpuFeatureStrings) {
- // CPU features are typically prefixed with a +, e.g. +avx,+avx2,+fma.
- featureString.consume_front("+");
- // Silently skip unknown CPU features, more flexible for now. Note that
- // some features occurring here are not standard CPU features but internal
- // things such as the "+reserve-x18" that we add on arm64.
- if (featureToBitPattern.count(featureString)) {
- specifiedCpuDataField0 |= featureToBitPattern.lookup(featureString);
- }
+ // Find CPU features in featureToBitPattern.
+ SmallVector<StringRef> cpuFeatureStrings;
+ cpuFeatures.value().split(cpuFeatureStrings, ',', /*MakeSplit=*/-1,
+ /*KeepEmpty=*/false);
+ for (auto featureString : cpuFeatureStrings) {
+ // CPU features are typically prefixed with a +, e.g. +avx,+avx2,+fma.
+ featureString.consume_front("+");
+ // Silently skip unknown CPU features, more flexible for now. Note that
+ // some features occurring here are not standard CPU features but
+ // internal things such as the "+reserve-x18" that we add on arm64.
+ if (featureToBitPattern.count(featureString)) {
+ specifiedCpuDataField0 |= featureToBitPattern.lookup(featureString);
}
}
- if (specifiedCpuDataField0 == 0) {
- return processorDataPtrValue;
- }
+ return specifiedCpuDataField0;
+}
- // Create a new stack allocation for the bit pattern.
+Value HALDispatchABI::updateProcessorDataFromTargetAttr(
+ Operation *forOp, Value processorDataPtrValue, OpBuilder &builder) {
+ uint64_t specifiedCpuDataField0 = getFeatureBitPattern(forOp);
Location loc = forOp->getLoc();
MLIRContext *context = forOp->getContext();
auto ptrType = LLVM::LLVMPointerType::get(context);
auto i64Ty = builder.getI64Type();
// The stack allocation goes into the entry point of the block.
- // The compile-time cpu features are patched onto `cpu_data` from the target
- // environment. That should happen in the loop body, so that the compile-time
- // cpu features are visible to the post-link LLVM optimizations that would
- // fold the microkernel tile size selection logic that checks these, and the
- // selection happens at compile-time.
+ // The compile-time cpu features overwrite the field 0 of `cpu_data` from the
+ // target environment. That should happen in the loop body, so that the
+ // compile-time cpu features are visible to the post-link LLVM optimizations
+ // that would fold the microkernel tile size selection logic that checks
+ // these, and the selection happens at compile-time.
Value alloca;
{
auto funcOp = forOp->getParentOfType<LLVM::LLVMFuncOp>();
@@ -974,15 +973,11 @@
alloca = LLVM::AllocaOp::create(builder, loc, ptrType, i64Ty, arraySize,
/*alignment=*/sizeof(uint64_t));
}
- // Load the 0-th value.
- Value srcData0 =
- LLVM::LoadOp::create(builder, loc, i64Ty, processorDataPtrValue);
- // Set the specified CPU arch data.
- Value bitPatternVal = LLVM::ConstantOp::create(
+ // Field 0: the compile-time CPU feature bitmask.
+ Value field0 = LLVM::ConstantOp::create(
builder, loc, i64Ty, builder.getI64IntegerAttr(specifiedCpuDataField0));
- srcData0 = LLVM::OrOp::create(builder, loc, srcData0, bitPatternVal);
- LLVM::StoreOp::create(builder, loc, srcData0, alloca);
- // Copy over the rest.
+ LLVM::StoreOp::create(builder, loc, field0, alloca);
+ // Fields 1..N: the runtime processor data, copied through.
for (int64_t i = 1, e = ProcessorDataCapacity; i < e; ++i) {
Value loadPtr = LLVM::GEPOp::create(
builder, loc, processorDataPtrValue.getType(), i64Ty,
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/convert_to_llvm.mlir b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/convert_to_llvm.mlir
index 501ca10..f6b2b48 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/test/convert_to_llvm.mlir
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/test/convert_to_llvm.mlir
@@ -31,15 +31,25 @@
}
}
// CHECK: llvm.func @default_cconv_with_extra_fields(!llvm.ptr, i32, f64, !llvm.ptr, i32) -> f32
-// CHECK: llvm.func @bar
+// CHECK: llvm.func @bar(%[[ENV:[^:]+]]:
+// CHECK-DAG: %[[C0:.+]] = llvm.mlir.constant(0 : i64) : i64
// CHECK-DAG: %[[Ci32:.+]] = llvm.mlir.constant(42 : i32) : i32
// CHECK-DAG: %[[Cf64:.+]] = llvm.mlir.constant(4.200000e+01 : f64) : f64
-// CHECK-DAG: %[[ALLOCA:.+]] = llvm.alloca
-// CHECK-DAG: %[[DATA:.+]] = llvm.getelementptr inbounds %arg0[4]
+// CHECK: %[[CPU_DATA:.+]] = llvm.alloca %{{.+}} x i64
+// CHECK: %[[ALLOCA:.+]] = llvm.alloca %{{.+}} x f32
+// CHECK: %[[ENV_DATA:.+]] = llvm.getelementptr inbounds %[[ENV]][4]
+// CHECK: llvm.store %[[C0]], %[[CPU_DATA]]
+// Remaining fields are copied from the runtime processor data.
+// CHECK: %[[SRC1:.+]] = llvm.getelementptr inbounds %[[ENV_DATA]][1]
+// CHECK: %[[VAL1:.+]] = llvm.load %[[SRC1]]
+// CHECK: %[[DST1:.+]] = llvm.getelementptr inbounds %[[CPU_DATA]][1]
+// CHECK: llvm.store %[[VAL1]], %[[DST1]]
// CHECK-DAG: %[[PROCESSOR_INFO:.+]] = llvm.load %arg2
// CHECK: %[[PROCESSOR_ID:.+]] = llvm.extractvalue %[[PROCESSOR_INFO]][4]
+// With no executable target the feature bitmask is zero; the ukernel receives a
+// cpu_data stack buffer (field 0 constant, fields 1..N from the runtime data).
// CHECK: %[[VAL:.+]] = llvm.call @default_cconv_with_extra_fields
-// CHECK-SAME: (%[[ALLOCA]], %[[Ci32]], %[[Cf64]], %[[DATA]], %[[PROCESSOR_ID]])
+// CHECK-SAME: (%[[ALLOCA]], %[[Ci32]], %[[Cf64]], %[[CPU_DATA]], %[[PROCESSOR_ID]])
// -----
@@ -49,8 +59,8 @@
// site instead of the function entry block, it would re-execute on every
// loop iteration without ever popping the stack back off, overflowing it for
// large iteration counts. So the alloca must be in the entry block and must not
-// reappear in the loop body, the cpu feature patching itself should be in the loop body
-// next to the call.
+// reappear in the loop body, the cpu feature override on the field0 itself should
+// be in the loop body next to the call.
#executable_target = #hal.executable.target<"llvm-cpu", "embedded-elf-arm_64", {cpu_features = "+dotprod", target_triple = "aarch64-none-elf"}>
module {
func.func private @default_cconv_with_extra_fields_in_loop(memref<f32>, i32, f64) -> (f32) attributes {
@@ -72,26 +82,31 @@
return
}
}
-// CHECK: llvm.func @loop_caller(%[[ARG0:.+]]: {{.*}}llvm.ptr{{.*}}, %[[ARG1:.+]]: {{.*}}llvm.ptr{{.*}}, %[[ARG2:.+]]: {{.*}}llvm.ptr{{.*}})
-// Entry block: only the patch buffer's stack slot is reserved here, once.
+// CHECK: llvm.func @loop_caller(%[[ENV:[^:]+]]:
+// Entry block: field 0 is a compile-time constant (`+dotprod` == 4096) and the
+// cpu_data buffer's stack slot is reserved here, once.
// CHECK-NOT: ^{{.+}}:
-// CHECK: %[[PATCHED_DATA:.+]] = llvm.alloca %{{.+}} x i64
+// CHECK: %[[FIELD0:.+]] = llvm.mlir.constant(4096 : i64)
+// CHECK-NOT: ^{{.+}}:
+// CHECK: %[[CPU_DATA:.+]] = llvm.alloca %{{.+}} x i64
// CHECK-NOT: ^{{.+}}:
// CHECK: llvm.br ^[[HEADER:.+]](
// Loop header: just the trip-count check.
// CHECK: ^[[HEADER]]
// CHECK: llvm.cond_br %{{.+}}, ^[[BODY:.+]], ^[[EXIT:.+]]
-// Loop body: no fresh alloca; the compile-time cpu features are patched onto the target
-// environment here in the loop body, storing into the entry-block buffer right before the call.
+// Loop body: no fresh alloca. Field 0 stores the constant cpu features; remaining fields
+// are copied from the runtime processor data.
// CHECK: ^[[BODY]]
// CHECK-NOT: llvm.alloca
-// CHECK: %[[ENV_DATA:.+]] = llvm.getelementptr inbounds %[[ARG0]]
-// CHECK: %[[SRC0:.+]] = llvm.load %[[ENV_DATA]]
-// CHECK: %[[PATCHED0:.+]] = llvm.or %[[SRC0]], %{{.+}}
-// CHECK: llvm.store %[[PATCHED0]], %[[PATCHED_DATA]]
+// CHECK: %[[ENV_DATA:.+]] = llvm.getelementptr inbounds %[[ENV]][4]
+// CHECK: llvm.store %[[FIELD0]], %[[CPU_DATA]]
+// CHECK: %[[SRC1:.+]] = llvm.getelementptr inbounds %[[ENV_DATA]][1]
+// CHECK: %[[VAL1:.+]] = llvm.load %[[SRC1]]
+// CHECK: %[[DST1:.+]] = llvm.getelementptr inbounds %[[CPU_DATA]][1]
+// CHECK: llvm.store %[[VAL1]], %[[DST1]]
// CHECK-NOT: llvm.alloca
// CHECK: llvm.call @default_cconv_with_extra_fields_in_loop
-// CHECK-SAME: %[[PATCHED_DATA]]
+// CHECK-SAME: %[[CPU_DATA]]
// CHECK: llvm.br ^[[HEADER]]
// Loop exit.
// CHECK: ^[[EXIT]]