[Codegen][LLVMCPU] Hoist in-loop stack alloca (#24749)

`HALDispatchABI::loadProcessorData` built its CPU-feature patch buffer
with an `alloca` at the ukernel call site instead of the function entry
block. `RewriteCallOpABI` materializes this after all MLIR->LLVM
conversions are done, so no later pass can hoist it out of a loop if the
call happens to sit inside one, causing the stack to grow unboundedly on
large matmuls.

The computation only depends on the function's `environment` argument
and static target attributes, so it can safely move to the entry block.

Fixes #24744

---------

Signed-off-by: Federico Bruzzone <federico.bruzzone.i@gmail.com>
diff --git a/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp b/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
index 6e49965..4c04a5d 100644
--- a/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
+++ b/compiler/src/iree/compiler/Codegen/LLVMCPU/DispatchABI.cpp
@@ -992,6 +992,20 @@
   // To get a pointer to the processor data we need to track pointers all the
   // 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());
+
   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 1049e32..e3bfe93 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
@@ -43,6 +43,59 @@
 
 // -----
 
+// Regression test for #24744: a `hal.import.bitcode` call requesting
+// "processor_data" (e.g. a ukernel call) gets a local `i64` buffer allocated
+// 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.
+#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 {
+      hal.import.bitcode = true,
+      hal.import.cconv = 0 : i32,
+      hal.import.fields = ["processor_data", "processor_id"],
+      llvm.bareptr = true
+  }
+  func.func @loop_caller() attributes {hal.executable.target = #executable_target} {
+    %lb = arith.constant 0 : index
+    %ub = arith.constant 1024 : index
+    %step = arith.constant 1 : index
+    %c0 = arith.constant 42 : i32
+    %c1 = arith.constant 42.0 : f64
+    %0 = memref.alloca() : memref<f32>
+    scf.for %i = %lb to %ub step %step {
+      %1 = func.call @default_cconv_with_extra_fields_in_loop(%0, %c0, %c1) : (memref<f32>, i32, f64) -> (f32)
+    }
+    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-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.
+//       CHECK: ^[[BODY]]
+//   CHECK-NOT:   llvm.alloca
+//       CHECK:   llvm.call @default_cconv_with_extra_fields_in_loop
+//  CHECK-SAME:       %[[PATCHED_DATA]]
+//       CHECK:   llvm.br ^[[HEADER]]
+// Loop exit.
+//       CHECK: ^[[EXIT]]
+//       CHECK:   llvm.return
+
+// -----
+
 #pipeline_layout = #hal.pipeline.layout<bindings = [
   #hal.pipeline.binding<storage_buffer>,
   #hal.pipeline.binding<storage_buffer>