[Metal] Support queue operations on standalone devices (#24840)
The device-group topology changes in #24248 leave frontier_tracker null
until a device joins a group. PR #24474 added guards for standalone CUDA
and HIP devices, but Metal was omitted. This PR simply adds the
equivalent Metal guard (and a regression test).
Fixes #24835.
AI assistance disclosure: I used OpenAI Codex substantially to inspect
the implementation and history, implement the change, and prepare the
regression test. I reviewed and understand the resulting code and
analysis, and wrote this description.
Signed-off-by: Jeremy Howard <github@jhoward.fastmail.fm>
diff --git a/runtime/src/iree/hal/drivers/metal/CMakeLists.txt b/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
index bb2d61b..11e9914 100644
--- a/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
+++ b/runtime/src/iree/hal/drivers/metal/CMakeLists.txt
@@ -53,3 +53,20 @@
"-framework Metal"
PUBLIC
)
+
+iree_cc_test(
+ NAME
+ metal_device_test
+ SRCS
+ "metal_device_test.cc"
+ DEPS
+ ::metal
+ iree::async::util::proactor_pool
+ iree::base
+ iree::base::threading
+ iree::hal
+ iree::testing::gtest
+ iree::testing::gtest_main
+ LABELS
+ "driver=metal"
+)
diff --git a/runtime/src/iree/hal/drivers/metal/metal_device.m b/runtime/src/iree/hal/drivers/metal/metal_device.m
index 1de65b8..9671655 100644
--- a/runtime/src/iree/hal/drivers/metal/metal_device.m
+++ b/runtime/src/iree/hal/drivers/metal/metal_device.m
@@ -98,7 +98,10 @@
// Advances the frontier tracker epoch for the device.
// Called at submit time ([commandBuffer commit]) because the Metal command
// queue is FIFO-ordered: submission order = causal ordering.
+// No-op for standalone devices not assigned to a topology group
+// (frontier_tracker is NULL until iree_hal_device_assign_topology_info).
static void iree_hal_metal_device_advance_frontier(iree_hal_metal_device_t* device) {
+ if (!device->frontier_tracker) return;
uint64_t epoch =
(uint64_t)iree_atomic_fetch_add(&device->epoch, 1, iree_memory_order_acq_rel) + 1;
iree_async_frontier_tracker_advance(device->frontier_tracker, device->axis, epoch);
diff --git a/runtime/src/iree/hal/drivers/metal/metal_device_test.cc b/runtime/src/iree/hal/drivers/metal/metal_device_test.cc
new file mode 100644
index 0000000..0c28f52
--- /dev/null
+++ b/runtime/src/iree/hal/drivers/metal/metal_device_test.cc
@@ -0,0 +1,63 @@
+// Copyright 2026 The IREE Authors
+//
+// Licensed under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "iree/async/util/proactor_pool.h"
+#include "iree/base/api.h"
+#include "iree/base/threading/numa.h"
+#include "iree/hal/api.h"
+#include "iree/hal/drivers/metal/api.h"
+#include "iree/testing/gtest.h"
+#include "iree/testing/status_matchers.h"
+
+namespace iree::hal::metal {
+namespace {
+
+TEST(MetalDeviceTest, StandaloneDeviceQueueAllocaDoesNotCrash) {
+ iree_hal_metal_device_params_t device_params;
+ iree_hal_metal_device_params_initialize(&device_params);
+
+ iree_hal_driver_t* driver = nullptr;
+ IREE_ASSERT_OK(iree_hal_metal_driver_create(
+ IREE_SV("metal"), &device_params, iree_allocator_system(), &driver));
+
+ iree_async_proactor_pool_t* proactor_pool = nullptr;
+ IREE_ASSERT_OK(iree_async_proactor_pool_create(
+ iree_numa_node_count(), /*node_ids=*/nullptr,
+ iree_async_proactor_pool_options_default(), iree_allocator_system(),
+ &proactor_pool));
+
+ iree_hal_device_create_params_t create_params =
+ iree_hal_device_create_params_default();
+ create_params.proactor_pool = proactor_pool;
+ iree_hal_device_t* device = nullptr;
+ iree_status_t status = iree_hal_driver_create_default_device(
+ driver, &create_params, iree_allocator_system(), &device);
+ if (!iree_status_is_ok(status)) {
+ iree_status_ignore(status);
+ iree_async_proactor_pool_release(proactor_pool);
+ iree_hal_driver_release(driver);
+ GTEST_SKIP() << "No Metal device available";
+ }
+
+ iree_hal_buffer_params_t buffer_params = {0};
+ buffer_params.type = IREE_HAL_MEMORY_TYPE_OPTIMAL_FOR_DEVICE;
+ buffer_params.access = IREE_HAL_MEMORY_ACCESS_ALL;
+ buffer_params.usage = IREE_HAL_BUFFER_USAGE_TRANSFER;
+ iree_hal_buffer_t* buffer = nullptr;
+ IREE_ASSERT_OK(iree_hal_device_queue_alloca(
+ device, IREE_HAL_QUEUE_AFFINITY_ANY, iree_hal_semaphore_list_empty(),
+ iree_hal_semaphore_list_empty(), /*pool=*/nullptr, buffer_params,
+ /*allocation_size=*/1024, IREE_HAL_ALLOCA_FLAG_NONE, &buffer));
+ ASSERT_NE(buffer, nullptr);
+
+ iree_hal_buffer_release(buffer);
+ iree_hal_device_release(device);
+ iree_async_proactor_pool_release(proactor_pool);
+ iree_hal_driver_release(driver);
+}
+
+} // namespace
+} // namespace iree::hal::metal