[Codegen][LLVMCPU] Split loadProcessorData: hoist only the alloca (#24786)
35b2070 (#24749) fixed unbounded stack growth by moving the whole
loadProcessorData CPU-feature patch buffer construction to the function
entry block. But that also hoisted the feature-bit or/store away from
the ukernel call, and once the ukernel inlines, LLVM can no longer
store-to-load forward the forced feature bits into the ukernel's runtime
feature check across the enclosing loop, so the mmt4d/pack tile-func
select never devirtualizes and the microkernel is left out-of-line.
Split the two concerns: reserve only the alloca (the stack slot, the
actual cause of #24744) in the entry block, but keep the feature-bit
patch at the call site. The stores re-run per iteration but allocate no
stack, so the stack stays bounded while the forced bits remain adjacent
to the (inlined) load and fold as before, restoring devirtualization.
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 4c04a5d..d6e993a 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
@@ -957,10 +957,23 @@
MLIRContext *context = forOp->getContext();
auto ptrType = LLVM::LLVMPointerType::get(context);
auto i64Ty = builder.getI64Type();
- Value arraySize = LLVM::ConstantOp::create(
- builder, loc, i64Ty, builder.getI64IntegerAttr(ProcessorDataCapacity));
- Value alloca = LLVM::AllocaOp::create(builder, loc, ptrType, i64Ty, arraySize,
- /*alignment=*/sizeof(uint64_t));
+ // 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.
+ Value alloca;
+ {
+ auto funcOp = forOp->getParentOfType<LLVM::LLVMFuncOp>();
+ assert(funcOp && "usage requires an enclosing LLVMFuncOp");
+ OpBuilder::InsertionGuard guard(builder);
+ builder.setInsertionPointToStart(&funcOp.getFunctionBody().front());
+ Value arraySize = LLVM::ConstantOp::create(
+ builder, loc, i64Ty, builder.getI64IntegerAttr(ProcessorDataCapacity));
+ 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);
@@ -993,19 +1006,8 @@
// way from the environment argument. This is redundant with loadFieldValue
// but that returns values instead.
//
- // `forOp` here is the call itself, which may sit inside a loop, so if we
- // built these ops at its insertion point we'd re-run the stack allocation
- // on every loop iteration with nothing to ever pop it back off, overflowing
- // the stack for large iteration counts (#24744). Build them in the
- // enclosing function's entry block instead, where they only run once.
- // The computation only depends on the function's `environment`
- // argument and static target attributes, so it can safely move to the
- // enclosing function's entry block instead, where it only runs once.
- auto funcOp = forOp->getParentOfType<LLVM::LLVMFuncOp>();
- assert(funcOp && "usage requires an enclosing LLVMFuncOp");
- OpBuilder::InsertionGuard guard(builder);
- builder.setInsertionPointToStart(&funcOp.getFunctionBody().front());
-
+ // The feature-bit patching stays at the call-site to enable proper ukernel
+ // inlining.
auto loc = forOp->getLoc();
auto environmentPtrValue =
buildArgDI(forOp, /*argNum=*/0, getLocalArgument(forOp, 0), "environment",
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 e3bfe93..501ca10 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
@@ -48,8 +48,9 @@
// to patch in statically-known CPU-feature bits. If that alloca were built at the call
// 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. Checks each of the 4 blocks below: the alloca must
-// be in the entry block and must not reappear in the loop body.
+// 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.
#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 {
@@ -71,22 +72,24 @@
return
}
}
-// CHECK: llvm.func @loop_caller
-// Entry block: the processor-data patch buffer is built once, here.
-// CHECK-NOT: ^{{.+}}:
-// CHECK: %[[ENV_DATA:.+]] = llvm.getelementptr inbounds %arg0[{{[0-9]+}}]
+// 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-NOT: ^{{.+}}:
// CHECK: %[[PATCHED_DATA:.+]] = llvm.alloca %{{.+}} x i64
// CHECK-NOT: ^{{.+}}:
-// CHECK: llvm.load %[[ENV_DATA]]
-// CHECK-NOT: ^{{.+}}:
// CHECK: llvm.br ^[[HEADER:.+]](
// Loop header: just the trip-count check.
// CHECK: ^[[HEADER]]
// CHECK: llvm.cond_br %{{.+}}, ^[[BODY:.+]], ^[[EXIT:.+]]
-// Loop body: reuses the entry-block buffer; no fresh alloca here.
+// 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.
// 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-NOT: llvm.alloca
// CHECK: llvm.call @default_cconv_with_extra_fields_in_loop
// CHECK-SAME: %[[PATCHED_DATA]]
// CHECK: llvm.br ^[[HEADER]]