[HAL] Fix inline execution models (initializer inlining and dispatch ABI) (#24827)
The inline execution models were broken end-to-end in both the compiler
and runtime.
On the compiler side, the `HAL_Loader` and `HAL_Inline` dialects did not
support inlining, preventing `CombineInitializers` pass from combining
initializers containing their ops. This broke `inline-dynamic`
compilation generally, and stateful programs using mutable globals under
both `inline-dynamic` and `inline-static`.
On the runtime side, the `hal_loader` dispatch shim incorrectly
accounted for padding in the fixed portion of the VM cconv arguments by
using the C struct size, causing `inline-dynamic` dispatches to fail
with an argument/result signature mismatch.
The runtime also leaked the inline HAL storage buffer wrapper on
release. VM buffer destruction also assumed that every buffer was
created by `iree_vm_buffer_create/clone` as a single aligned allocation
containing both the `iree_vm_buffer_t` handle and its data. Inline HAL
instead embeds the handle inside a wrapper struct that maps an
`iree_hal_buffer_t`, initializing it in place with
`iree_vm_buffer_initialize`. Such buffers must invoke the wrapper’s
custom allocator to release the underlying HAL buffer and free the
wrapper, rather than calling `iree_allocator_free_aligned` on the
embedded handle. Since nothing else in the handle distinguishes the two
cases at destroy time, create/clone now tag their buffers with
`IREE_VM_BUFFER_ACCESS_COALLOCATED` (set internally), and destroy takes
the free-aligned path only for tagged buffers; all others pass the
handle to their allocator.
This PR adds the required inliner interfaces, fixes the dispatch
argument handling, distinguishes co-allocated and in-place-initialized
VM buffers during destruction, and adds compiler/runtime coverage for
the affected execution models and buffer lifetimes.
Assisted-by: Claude Code
---------
Signed-off-by: Pooja Hemashekar <hemashekar@roofline.ai>
diff --git a/compiler/src/iree/compiler/Modules/HAL/Inline/IR/BUILD.bazel b/compiler/src/iree/compiler/Modules/HAL/Inline/IR/BUILD.bazel
index 52bdf36..86e7d74 100644
--- a/compiler/src/iree/compiler/Modules/HAL/Inline/IR/BUILD.bazel
+++ b/compiler/src/iree/compiler/Modules/HAL/Inline/IR/BUILD.bazel
@@ -75,6 +75,7 @@
"@llvm-project//mlir:FuncDialect",
"@llvm-project//mlir:FunctionInterfaces",
"@llvm-project//mlir:IR",
+ "@llvm-project//mlir:InliningUtils",
"@llvm-project//mlir:Parser",
"@llvm-project//mlir:Support",
"@llvm-project//mlir:TransformUtils",
diff --git a/compiler/src/iree/compiler/Modules/HAL/Inline/IR/HALInlineDialect.cpp b/compiler/src/iree/compiler/Modules/HAL/Inline/IR/HALInlineDialect.cpp
index 461a557..98deb01 100644
--- a/compiler/src/iree/compiler/Modules/HAL/Inline/IR/HALInlineDialect.cpp
+++ b/compiler/src/iree/compiler/Modules/HAL/Inline/IR/HALInlineDialect.cpp
@@ -14,6 +14,7 @@
#include "mlir/IR/DialectImplementation.h"
#include "mlir/IR/OpImplementation.h"
#include "mlir/Parser/Parser.h"
+#include "mlir/Transforms/InliningUtils.h"
namespace mlir::iree_compiler::IREE::HAL::Inline {
@@ -41,11 +42,30 @@
}
};
+struct HALInlineInlinerInterface : DialectInlinerInterface {
+ using DialectInlinerInterface::DialectInlinerInterface;
+
+ bool isLegalToInline(Operation *call, Operation *callable,
+ bool wouldBeCloned) const final {
+ return true;
+ }
+
+ bool isLegalToInline(Region *dest, Region *src, bool wouldBeCloned,
+ IRMapping &valueMapping) const final {
+ return true;
+ }
+
+ bool isLegalToInline(Operation *op, Region *dest, bool wouldBeCloned,
+ IRMapping &valueMapping) const final {
+ return true;
+ }
+};
+
} // namespace
HALInlineDialect::HALInlineDialect(MLIRContext *context)
: Dialect(getDialectNamespace(), context, TypeID::get<HALInlineDialect>()) {
- addInterfaces<HALInlineToVMConversionInterface>();
+ addInterfaces<HALInlineToVMConversionInterface, HALInlineInlinerInterface>();
#define GET_OP_LIST
addOperations<
diff --git a/compiler/src/iree/compiler/Modules/HAL/Loader/IR/BUILD.bazel b/compiler/src/iree/compiler/Modules/HAL/Loader/IR/BUILD.bazel
index 0f575de..2135610 100644
--- a/compiler/src/iree/compiler/Modules/HAL/Loader/IR/BUILD.bazel
+++ b/compiler/src/iree/compiler/Modules/HAL/Loader/IR/BUILD.bazel
@@ -75,6 +75,7 @@
"@llvm-project//mlir:FuncDialect",
"@llvm-project//mlir:FunctionInterfaces",
"@llvm-project//mlir:IR",
+ "@llvm-project//mlir:InliningUtils",
"@llvm-project//mlir:Parser",
"@llvm-project//mlir:Support",
"@llvm-project//mlir:TransformUtils",
diff --git a/compiler/src/iree/compiler/Modules/HAL/Loader/IR/HALLoaderDialect.cpp b/compiler/src/iree/compiler/Modules/HAL/Loader/IR/HALLoaderDialect.cpp
index e34c1e8..067930e 100644
--- a/compiler/src/iree/compiler/Modules/HAL/Loader/IR/HALLoaderDialect.cpp
+++ b/compiler/src/iree/compiler/Modules/HAL/Loader/IR/HALLoaderDialect.cpp
@@ -14,6 +14,7 @@
#include "mlir/IR/DialectImplementation.h"
#include "mlir/IR/OpImplementation.h"
#include "mlir/Parser/Parser.h"
+#include "mlir/Transforms/InliningUtils.h"
namespace mlir::iree_compiler::IREE::HAL::Loader {
@@ -41,11 +42,30 @@
}
};
+struct HALLoaderInlinerInterface : DialectInlinerInterface {
+ using DialectInlinerInterface::DialectInlinerInterface;
+
+ bool isLegalToInline(Operation *call, Operation *callable,
+ bool wouldBeCloned) const final {
+ return true;
+ }
+
+ bool isLegalToInline(Region *dest, Region *src, bool wouldBeCloned,
+ IRMapping &valueMapping) const final {
+ return true;
+ }
+
+ bool isLegalToInline(Operation *op, Region *dest, bool wouldBeCloned,
+ IRMapping &valueMapping) const final {
+ return true;
+ }
+};
+
} // namespace
HALLoaderDialect::HALLoaderDialect(MLIRContext *context)
: Dialect(getDialectNamespace(), context, TypeID::get<HALLoaderDialect>()) {
- addInterfaces<HALLoaderToVMConversionInterface>();
+ addInterfaces<HALLoaderToVMConversionInterface, HALLoaderInlinerInterface>();
#define GET_OP_LIST
addOperations<
diff --git a/runtime/src/iree/modules/hal/inline/module.c b/runtime/src/iree/modules/hal/inline/module.c
index 350cf32..eb3aaf0 100644
--- a/runtime/src/iree/modules/hal/inline/module.c
+++ b/runtime/src/iree/modules/hal/inline/module.c
@@ -87,6 +87,9 @@
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, iree_allocator_malloc(host_allocator, sizeof(*storage),
(void**)&storage));
+ storage->host_allocator = host_allocator;
+ storage->hal_buffer = hal_buffer;
+ iree_hal_buffer_retain(hal_buffer);
// Map the HAL buffer into host-accessible memory. It almost always is but
// it's possible the buffer we were passed was allocated on a real device that
diff --git a/runtime/src/iree/modules/hal/loader/module.c b/runtime/src/iree/modules/hal/loader/module.c
index 063190d..7b53bdc 100644
--- a/runtime/src/iree/modules/hal/loader/module.c
+++ b/runtime/src/iree/modules/hal/loader/module.c
@@ -341,18 +341,24 @@
void* IREE_RESTRICT module_state) {
// TODO(benvanik): support multiple variadic segments in one call.
// For now we inline what it would do in a very painful way.
+ //
+ // NOTE: sizeof(iree_vm_abi_rIiii_t) may include trailing struct padding
+ // that is not present before the following variadic segment. Size the
+ // fixed prefix through its final field instead.
+ const iree_host_size_t params_size =
+ offsetof(iree_vm_abi_rIiii_t, i4) +
+ IREE_VM_ABI_FIELD_SIZE(iree_vm_abi_rIiii_t, i4);
bool args_ok = true;
if (args_storage.data_length <
- (sizeof(iree_vm_abi_rIiii_t) + sizeof(iree_vm_size_t) +
- sizeof(iree_vm_size_t))) {
+ (params_size + sizeof(iree_vm_size_t) + sizeof(iree_vm_size_t))) {
// Can't fit even with zero lengths.
args_ok = false;
}
- iree_hal_loader_dispatch_args_t args = {
- .params = *(const iree_vm_abi_rIiii_t*)args_storage.data,
- };
+ iree_hal_loader_dispatch_args_t args;
+ memset(&args, 0, sizeof(args));
if (args_ok) {
- const uint8_t* constants_ptr = args_storage.data + sizeof(args.params);
+ memcpy(&args.params, args_storage.data, params_size);
+ const uint8_t* constants_ptr = args_storage.data + params_size;
args.constant_count = *(const iree_vm_size_t*)constants_ptr;
args.constants = (const uint32_t*)(constants_ptr + sizeof(iree_vm_size_t));
const uint8_t* bindings_ptr =
diff --git a/runtime/src/iree/modules/hal/module.c b/runtime/src/iree/modules/hal/module.c
index 1b86137..6c51b13 100644
--- a/runtime/src/iree/modules/hal/module.c
+++ b/runtime/src/iree/modules/hal/module.c
@@ -1100,6 +1100,14 @@
args->constant_count * sizeof(uint32_t)),
bindings, (iree_hal_dispatch_flags_t)args->flags);
}
+// The shim below uses sizeof(iree_vm_abi_rrIiiiI_t) as the offset of the
+// following variadic segment; that is only correct while the struct has no
+// trailing padding as the VM packs cconv arguments unpadded.
+static_assert(sizeof(iree_vm_abi_rrIiiiI_t) ==
+ offsetof(iree_vm_abi_rrIiiiI_t, i6) +
+ IREE_VM_ABI_FIELD_SIZE(iree_vm_abi_rrIiiiI_t, i6),
+ "iree_vm_abi_rrIiiiI_t must have no trailing padding; size "
+ "through the final field instead of using sizeof");
static iree_status_t iree_hal_module_command_buffer_dispatch_shim(
iree_vm_stack_t* IREE_RESTRICT stack, iree_vm_native_function_flags_t flags,
iree_byte_span_t args_storage, iree_byte_span_t rets_storage,
@@ -1222,6 +1230,14 @@
args->constant_count * sizeof(uint32_t)),
bindings, flags);
}
+// The shim below uses sizeof(iree_vm_abi_rrIirII_t) as the offset of the
+// following variadic segment; that is only correct while the struct has no
+// trailing padding as the VM packs cconv arguments unpadded.
+static_assert(sizeof(iree_vm_abi_rrIirII_t) ==
+ offsetof(iree_vm_abi_rrIirII_t, i6) +
+ IREE_VM_ABI_FIELD_SIZE(iree_vm_abi_rrIirII_t, i6),
+ "iree_vm_abi_rrIirII_t must have no trailing padding; size "
+ "through the final field instead of using sizeof");
static iree_status_t iree_hal_module_command_buffer_dispatch_indirect_shim(
iree_vm_stack_t* IREE_RESTRICT stack, iree_vm_native_function_flags_t flags,
iree_byte_span_t args_storage, iree_byte_span_t rets_storage,
diff --git a/runtime/src/iree/vm/buffer.c b/runtime/src/iree/vm/buffer.c
index f903a16..d4e8b6e 100644
--- a/runtime/src/iree/vm/buffer.c
+++ b/runtime/src/iree/vm/buffer.c
@@ -125,7 +125,8 @@
memset(data_ptr, 0, prefix_size - sizeof(*buffer)); // padding
iree_byte_span_t target_span =
iree_make_byte_span(data_ptr + prefix_size, length);
- iree_vm_buffer_initialize(access, target_span, allocator, buffer);
+ iree_vm_buffer_initialize(access | IREE_VM_BUFFER_ACCESS_COALLOCATED,
+ target_span, allocator, buffer);
*out_buffer = buffer;
IREE_TRACE_ZONE_END(z0);
@@ -140,10 +141,16 @@
// Module-owned buffers are embedded in the bytecode module. Reaching zero
// external references releases the owner retain taken by the first retain.
iree_vm_module_release(buffer->storage_module);
- } else {
+ } else if (iree_all_bits_set(buffer->access,
+ IREE_VM_BUFFER_ACCESS_COALLOCATED)) {
// Buffers are stored as [prefix | data]; freeing the prefix is all we need
// to do to free it all.
iree_allocator_free_aligned(buffer->allocator, buffer);
+ } else {
+ // Buffer handle was initialized in-place over storage owned elsewhere;
+ // notify the allocator so any wrapping storage can be released. Note that
+ // it must not be treated as an aligned allocation.
+ iree_allocator_free(buffer->allocator, buffer);
}
IREE_TRACE_ZONE_END(z0);
@@ -263,7 +270,8 @@
memset(data_ptr, 0, prefix_size - sizeof(*buffer)); // padding
iree_byte_span_t target_span =
iree_make_byte_span(data_ptr + prefix_size, length);
- iree_vm_buffer_initialize(access, target_span, allocator, buffer);
+ iree_vm_buffer_initialize(access | IREE_VM_BUFFER_ACCESS_COALLOCATED,
+ target_span, allocator, buffer);
// Copy the data from the source buffer.
memcpy(target_span.data, source_span.data, target_span.data_length);
diff --git a/runtime/src/iree/vm/buffer.h b/runtime/src/iree/vm/buffer.h
index 18fc8d6..88084fa 100644
--- a/runtime/src/iree/vm/buffer.h
+++ b/runtime/src/iree/vm/buffer.h
@@ -34,6 +34,12 @@
IREE_VM_BUFFER_ACCESS_ORIGIN_GUEST = 1u << 2,
// Buffer references external host memory with an unknown lifetime.
IREE_VM_BUFFER_ACCESS_ORIGIN_HOST = 1u << 3,
+
+ // Buffer handle and data are co-allocated in a single aligned allocation
+ // and the handle pointer must be freed with iree_allocator_free_aligned.
+ // Set internally by iree_vm_buffer_create/iree_vm_buffer_clone; buffers
+ // initialized in-place over existing storage must not set this.
+ IREE_VM_BUFFER_ACCESS_COALLOCATED = 1u << 4,
};
typedef uint32_t iree_vm_buffer_access_t;
diff --git a/runtime/src/iree/vm/buffer_test.cc b/runtime/src/iree/vm/buffer_test.cc
index a3801b2..94b7145 100644
--- a/runtime/src/iree/vm/buffer_test.cc
+++ b/runtime/src/iree/vm/buffer_test.cc
@@ -52,6 +52,42 @@
ASSERT_TRUE(did_free);
}
+// Buffers initialized in-place may use their allocator as a destruction
+// callback for wrapping storage and must not take the aligned free path.
+TEST_F(VMBufferTest, InitializedBufferDestroyNotifiesAllocator) {
+ void* freed_ptr = nullptr;
+ iree_allocator_t test_allocator = {
+ /*.self=*/&freed_ptr,
+ /*.ctl=*/
+ +[](void* self, iree_allocator_command_t command, const void* params,
+ void** inout_ptr) {
+ if (command == IREE_ALLOCATOR_COMMAND_FREE) {
+ *(void**)self = *inout_ptr;
+ }
+ return iree_ok_status();
+ },
+ };
+
+ uint32_t data[] = {0, 1, 2, 3};
+ // Mimic the inline HAL wrapper. The zeroed prefix makes an incorrect
+ // iree_allocator_free_aligned call read a null base pointer and skip the
+ // free.
+ struct {
+ void* prefix[2];
+ iree_vm_buffer_t buffer;
+ } storage;
+ memset(&storage, 0, sizeof(storage));
+ iree_vm_buffer_initialize(
+ IREE_VM_BUFFER_ACCESS_MUTABLE | IREE_VM_BUFFER_ACCESS_ORIGIN_HOST,
+ iree_make_byte_span(data, sizeof(data)), test_allocator, &storage.buffer);
+
+ ASSERT_EQ(freed_ptr, nullptr);
+ iree_vm_buffer_release(&storage.buffer);
+ // The destroy path passes the non-NULL buffer handle so wrapper allocators
+ // are notified even when the buffer data is empty.
+ ASSERT_EQ(freed_ptr, &storage.buffer);
+}
+
typedef struct test_module_t {
// VM module interface used for retain/release testing.
iree_vm_module_t interface;
diff --git a/tests/compiler_driver/BUILD.bazel b/tests/compiler_driver/BUILD.bazel
index fafafa8..a463f5d 100644
--- a/tests/compiler_driver/BUILD.bazel
+++ b/tests/compiler_driver/BUILD.bazel
@@ -22,6 +22,7 @@
"executable_benchmarks.mlir",
"hal_executable.mlir",
"inline_dynamic_hal_executable.mlir",
+ "inline_hal_mutable_global.mlir",
"inline_static_hal_executable.mlir",
"precompile.mlir",
"preprocessing_flags.mlir",
diff --git a/tests/compiler_driver/CMakeLists.txt b/tests/compiler_driver/CMakeLists.txt
index 5ebc0fc..df0a623 100644
--- a/tests/compiler_driver/CMakeLists.txt
+++ b/tests/compiler_driver/CMakeLists.txt
@@ -17,6 +17,7 @@
"executable_benchmarks.mlir"
"hal_executable.mlir"
"inline_dynamic_hal_executable.mlir"
+ "inline_hal_mutable_global.mlir"
"inline_static_hal_executable.mlir"
"precompile.mlir"
"preprocessing_flags.mlir"
diff --git a/tests/compiler_driver/inline_dynamic_hal_executable.mlir b/tests/compiler_driver/inline_dynamic_hal_executable.mlir
index c2e7cd0..536ac43 100644
--- a/tests/compiler_driver/inline_dynamic_hal_executable.mlir
+++ b/tests/compiler_driver/inline_dynamic_hal_executable.mlir
@@ -18,3 +18,16 @@
// CHECK-NOT: hal.pipeline_layout
// CHECK-NOT: hal.semaphore
// CHECK-NOT: hal.executable private
+
+// Compiling through the VM lowering requires inlining the executable-loading
+// initializer into the combined initializer.
+// RUN: iree-compile \
+// RUN: --compile-to=vm \
+// RUN: --iree-execution-model=inline-dynamic \
+// RUN: --iree-hal-target-device=local \
+// RUN: --iree-hal-local-target-device-backends=vmvx %s | FileCheck %s --check-prefix=CHECK-VM
+
+// CHECK-VM-LABEL: vm.func private @__init()
+// CHECK-VM: vm.call @hal_loader.executable.query_support
+// CHECK-VM: vm.call @hal_loader.executable.load
+// CHECK-VM: vm.global.store.ref %{{.+}}, @simple_mul_dispatch_0
diff --git a/tests/compiler_driver/inline_hal_mutable_global.mlir b/tests/compiler_driver/inline_hal_mutable_global.mlir
new file mode 100644
index 0000000..40bd4f0
--- /dev/null
+++ b/tests/compiler_driver/inline_hal_mutable_global.mlir
@@ -0,0 +1,36 @@
+// Mutable globals require an initializer that allocates their backing buffer
+// with hal_inline ops; compiling through the VM lowering requires inlining
+// that initializer into the combined initializer.
+
+// RUN: iree-compile \
+// RUN: --compile-to=vm \
+// RUN: --iree-execution-model=inline-dynamic \
+// RUN: --iree-hal-target-device=local \
+// RUN: --iree-hal-local-target-device-backends=vmvx %s | \
+// RUN: FileCheck %s --check-prefix=CHECK-DYNAMIC
+
+// CHECK-DYNAMIC-LABEL: vm.func private @__init()
+// CHECK-DYNAMIC-DAG: vm.call @hal_inline.buffer.allocate
+// CHECK-DYNAMIC-DAG: vm.global.store.ref %{{.+}}, @state
+// CHECK-DYNAMIC-DAG: vm.call @hal_loader.executable.load
+// CHECK-DYNAMIC: vm.global.store.ref %{{.+}}, @update_dispatch_0
+
+// RUN: iree-compile \
+// RUN: --compile-to=vm \
+// RUN: --iree-execution-model=inline-static \
+// RUN: --iree-hal-target-device=local \
+// RUN: --iree-hal-local-target-device-backends=vmvx-inline %s | \
+// RUN: FileCheck %s --check-prefix=CHECK-STATIC
+
+// CHECK-STATIC-LABEL: vm.func private @__init()
+// CHECK-STATIC: vm.call @hal_inline.buffer.allocate
+// CHECK-STATIC: vm.global.store.ref %{{.+}}, @state
+
+util.global private mutable @state = dense<1.0> : tensor<4xf32>
+
+func.func @update(%arg0: tensor<4xf32>) -> tensor<4xf32> {
+ %state = util.global.load @state : tensor<4xf32>
+ %sum = arith.addf %state, %arg0 : tensor<4xf32>
+ util.global.store %sum, @state : tensor<4xf32>
+ return %sum : tensor<4xf32>
+}
diff --git a/tools/test/BUILD.bazel b/tools/test/BUILD.bazel
index d71a773..ed42d7b 100644
--- a/tools/test/BUILD.bazel
+++ b/tools/test/BUILD.bazel
@@ -49,6 +49,7 @@
"iree-replay-tools.txt",
"iree-run-mlir.mlir",
"iree-run-module-expected.mlir",
+ "iree-run-module-inline-dynamic.mlir",
"iree-run-module-inputs.mlir",
"iree-run-module-multi.mlir",
"iree-run-module-outputs.mlir",
diff --git a/tools/test/CMakeLists.txt b/tools/test/CMakeLists.txt
index 04e11b4..effeaf4 100644
--- a/tools/test/CMakeLists.txt
+++ b/tools/test/CMakeLists.txt
@@ -38,6 +38,7 @@
"iree-run-mlir.mlir"
"iree-run-module-expected.mlir"
"iree-run-module-in-place.mlir"
+ "iree-run-module-inline-dynamic.mlir"
"iree-run-module-inputs.mlir"
"iree-run-module-multi.mlir"
"iree-run-module-outputs.mlir"
diff --git a/tools/test/iree-run-module-inline-dynamic.mlir b/tools/test/iree-run-module-inline-dynamic.mlir
new file mode 100644
index 0000000..08b1fa9
--- /dev/null
+++ b/tools/test/iree-run-module-inline-dynamic.mlir
@@ -0,0 +1,13 @@
+// Executes an inline HAL (no device) module via the hal_loader module.
+// RUN: (iree-compile --iree-execution-model=inline-dynamic --iree-hal-target-device=local --iree-hal-local-target-device-backends=vmvx %s | \
+// RUN: iree-run-module --module=- --function=abs --input="2xf32=-2 3") | FileCheck %s
+// RUN: (iree-compile --iree-execution-model=inline-dynamic --iree-hal-target-device=local --iree-hal-local-target-device-backends=llvm-cpu %s | \
+// RUN: iree-run-module --module=- --function=abs --input="2xf32=-2 3") | FileCheck %s
+
+// CHECK-LABEL: EXEC @abs
+func.func @abs(%input : tensor<2xf32>) -> (tensor<2xf32>) {
+ %result = math.absf %input : tensor<2xf32>
+ return %result : tensor<2xf32>
+}
+// CHECK: result[0]: hal.buffer_view
+// CHECK-NEXT: 2xf32=2 3