// 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 <cstring>

#include "iree/async/frontier_tracker.h"
#include "iree/base/api.h"
#include "iree/hal/api.h"
#include "iree/hal/testing/mock_device.h"
#include "iree/testing/gtest.h"
#include "iree/testing/status_matchers.h"

namespace iree::hal {
namespace {

using ::iree::testing::status::StatusIs;

// Helper: creates a mock device with the given identifier and default
// (zeroed) capabilities.
static iree_hal_device_t* CreateMockDevice(const char* identifier) {
  iree_hal_mock_device_options_t options;
  iree_hal_mock_device_options_initialize(&options);
  options.identifier = iree_make_cstring_view(identifier);
  iree_hal_device_t* device = NULL;
  IREE_CHECK_OK(
      iree_hal_mock_device_create(&options, iree_allocator_system(), &device));
  return device;
}

// Helper: creates a mock device with configurable capabilities.
static iree_hal_device_t* CreateMockDeviceWithCapabilities(
    const char* identifier,
    const iree_hal_device_capabilities_t* capabilities) {
  iree_hal_mock_device_options_t options;
  iree_hal_mock_device_options_initialize(&options);
  options.identifier = iree_make_cstring_view(identifier);
  options.capabilities = *capabilities;
  iree_hal_device_t* device = NULL;
  IREE_CHECK_OK(
      iree_hal_mock_device_create(&options, iree_allocator_system(), &device));
  return device;
}

static iree_hal_device_t* CreateMockDeviceThatFailsTopologyAssignment(
    const char* identifier) {
  iree_hal_mock_device_options_t options;
  iree_hal_mock_device_options_initialize(&options);
  options.identifier = iree_make_cstring_view(identifier);
  options.assign_topology_info_status_code = IREE_STATUS_UNAVAILABLE;
  iree_hal_device_t* device = NULL;
  IREE_CHECK_OK(
      iree_hal_mock_device_create(&options, iree_allocator_system(), &device));
  return device;
}

static iree_async_frontier_tracker_t* CreateFrontierTracker() {
  iree_async_frontier_tracker_options_t options =
      iree_async_frontier_tracker_options_default();
  iree_async_frontier_tracker_t* frontier_tracker = NULL;
  IREE_CHECK_OK(iree_async_frontier_tracker_create(
      options, iree_allocator_system(), &frontier_tracker));
  return frontier_tracker;
}

static void InitializeDeviceGroupBuilder(
    iree_hal_device_group_builder_t* builder) {
  iree_async_frontier_tracker_t* frontier_tracker = CreateFrontierTracker();
  iree_hal_device_group_builder_initialize(builder, frontier_tracker);
  iree_async_frontier_tracker_release(frontier_tracker);
}

//===----------------------------------------------------------------------===//
// Builder validation tests
//===----------------------------------------------------------------------===//

// An empty builder (no devices added) must fail to finalize.
TEST(DeviceGroupBuilder, EmptyBuilderFails) {
  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);

  iree_hal_device_group_t* group = NULL;
  IREE_EXPECT_STATUS_IS(IREE_STATUS_INVALID_ARGUMENT,
                        iree_hal_device_group_builder_finalize(
                            &builder, iree_allocator_system(), &group));
  EXPECT_EQ(group, nullptr);

  iree_hal_device_group_builder_deinitialize(&builder);
}

// Adding more than IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT devices must fail.
TEST(DeviceGroupBuilder, ExceedsMaxCapacity) {
  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);

  iree_hal_device_t* device = CreateMockDevice("mock");
  for (iree_host_size_t i = 0; i < IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT; ++i) {
    IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device));
  }

  // The next add must fail.
  IREE_EXPECT_STATUS_IS(
      IREE_STATUS_RESOURCE_EXHAUSTED,
      iree_hal_device_group_builder_add_device(&builder, device));

  iree_hal_device_group_builder_deinitialize(&builder);
  iree_hal_device_release(device);
}

// After finalize the builder is zeroed and must not be reused.
TEST(DeviceGroupBuilder, FinalizeInvalidatesBuilder) {
  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);

  iree_hal_device_t* device = CreateMockDevice("mock");
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  // Builder should be zeroed.
  EXPECT_EQ(builder.count, 0u);
  for (iree_host_size_t i = 0; i < IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT; ++i) {
    EXPECT_EQ(builder.devices[i], nullptr);
  }

  iree_hal_device_group_release(group);
  iree_hal_device_release(device);
}

// A failed finalize (empty builder) also zeroes the builder.
TEST(DeviceGroupBuilder, FailedFinalizeInvalidatesBuilder) {
  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);

  iree_hal_device_group_t* group = NULL;
  iree_status_t status = iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group);
  EXPECT_THAT(status, StatusIs(iree::StatusCode::kInvalidArgument));
  iree_status_ignore(status);

  EXPECT_EQ(builder.count, 0u);
}

// If a device rejects topology assignment, finalize must abort assignments that
// were already published to earlier devices before releasing the group
// topology.
TEST(DeviceGroupBuilder, FailedTopologyAssignmentClearsAssignedDevices) {
  iree_hal_device_t* device_a = CreateMockDevice("mock");
  iree_hal_device_t* device_b =
      CreateMockDeviceThatFailsTopologyAssignment("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* group = NULL;
  iree_status_t status = iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group);
  EXPECT_THAT(status, StatusIs(iree::StatusCode::kUnavailable));
  iree_status_ignore(status);

  EXPECT_EQ(group, nullptr);
  EXPECT_EQ(iree_hal_device_topology_info(device_a)->topology, nullptr);
  EXPECT_EQ(iree_hal_device_topology_info(device_b)->topology, nullptr);

  iree_hal_device_release(device_b);
  iree_hal_device_release(device_a);
}

// Queue pool backends expose driver internals that rely on group-assigned
// frontier/topology state.
TEST(Device, QueryQueuePoolBackendRequiresDeviceGroup) {
  iree_hal_device_t* device = CreateMockDevice("mock");

  iree_hal_queue_pool_backend_t backend;
  iree_status_t status = iree_hal_device_query_queue_pool_backend(
      device, IREE_HAL_QUEUE_AFFINITY_ANY, &backend);
  EXPECT_THAT(status, StatusIs(iree::StatusCode::kFailedPrecondition));
  iree_status_ignore(status);

  iree_hal_device_release(device);
}

//===----------------------------------------------------------------------===//
// Single-device group tests
//===----------------------------------------------------------------------===//

// A group with one device should have a self-edge topology.
TEST(DeviceGroup, SingleDevice) {
  iree_hal_device_t* device = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  // Verify accessors.
  EXPECT_EQ(iree_hal_device_group_device_count(group), 1u);
  EXPECT_EQ(iree_hal_device_group_device_at(group, 0), device);
  EXPECT_EQ(iree_hal_device_group_device_at(group, 1), nullptr);

  // Topology should have 1 device with a valid self-edge.
  const iree_hal_topology_t* topology = iree_hal_device_group_topology(group);
  EXPECT_EQ(topology->device_count, 1u);
  iree_hal_topology_edge_t self_edge =
      iree_hal_topology_query_edge(topology, 0, 0);
  EXPECT_EQ(iree_hal_topology_edge_wait_mode(self_edge.lo),
            IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE);
  EXPECT_EQ(iree_hal_topology_edge_signal_mode(self_edge.lo),
            IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE);

  // Device should have topology info assigned.
  const iree_hal_device_topology_info_t* info =
      iree_hal_device_topology_info(device);
  EXPECT_EQ(info->topology_index, 0u);
  EXPECT_EQ(info->topology, topology);

  // Single-device group: all bitmaps should be 0 (no peers).
  EXPECT_EQ(info->can_wait_from, 0u);
  EXPECT_EQ(info->can_signal_to, 0u);
  EXPECT_EQ(info->can_import_from, 0u);
  EXPECT_EQ(info->can_p2p_with, 0u);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device);
}

//===----------------------------------------------------------------------===//
// Multi-device group tests
//===----------------------------------------------------------------------===//

// Two devices with the same driver identifier.
TEST(DeviceGroup, TwoDevicesSameDriver) {
  iree_hal_device_t* device_a = CreateMockDevice("mock");
  iree_hal_device_t* device_b = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  EXPECT_EQ(iree_hal_device_group_device_count(group), 2u);
  EXPECT_EQ(iree_hal_device_group_device_at(group, 0), device_a);
  EXPECT_EQ(iree_hal_device_group_device_at(group, 1), device_b);

  const iree_hal_topology_t* topology = iree_hal_device_group_topology(group);
  EXPECT_EQ(topology->device_count, 2u);

  // Cross-device edge should exist (computed from zeroed capabilities).
  iree_hal_topology_edge_t edge_ab =
      iree_hal_topology_query_edge(topology, 0, 1);
  EXPECT_FALSE(iree_hal_topology_edge_is_empty(edge_ab));

  // Both devices should have topology info with correct indices.
  const iree_hal_device_topology_info_t* info_a =
      iree_hal_device_topology_info(device_a);
  const iree_hal_device_topology_info_t* info_b =
      iree_hal_device_topology_info(device_b);
  EXPECT_EQ(info_a->topology_index, 0u);
  EXPECT_EQ(info_b->topology_index, 1u);
  EXPECT_EQ(info_a->topology, topology);
  EXPECT_EQ(info_b->topology, topology);

  // Cross-device query through topology_info should return the same edge.
  iree_hal_topology_edge_t queried =
      iree_hal_device_topology_query_edge(info_a, info_b);
  EXPECT_EQ(queried.lo, edge_ab.lo);
  EXPECT_EQ(queried.hi, edge_ab.hi);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
}

// Two devices with different driver identifiers (cross-driver pair).
TEST(DeviceGroup, TwoDevicesDifferentDrivers) {
  iree_hal_device_t* device_a = CreateMockDevice("driver_a");
  iree_hal_device_t* device_b = CreateMockDevice("driver_b");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  const iree_hal_topology_t* topology = iree_hal_device_group_topology(group);

  // Cross-driver edge from edge_from_capabilities with zeroed capabilities
  // (no import/export handle types) produces COPY mode for everything — there
  // are no external handles to import, so the only option is host-staged copy.
  iree_hal_topology_edge_t edge_ab =
      iree_hal_topology_query_edge(topology, 0, 1);
  EXPECT_EQ(iree_hal_topology_edge_wait_mode(edge_ab.lo),
            IREE_HAL_TOPOLOGY_INTEROP_MODE_COPY);
  EXPECT_EQ(iree_hal_topology_edge_buffer_read_mode_noncoherent(edge_ab.lo),
            IREE_HAL_TOPOLOGY_INTEROP_MODE_COPY);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
}

// Three devices: verifies bitmap computation with more than two devices.
TEST(DeviceGroup, ThreeDevicesBitmaps) {
  iree_hal_device_t* device_a = CreateMockDevice("mock");
  iree_hal_device_t* device_b = CreateMockDevice("mock");
  iree_hal_device_t* device_c = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_c));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  EXPECT_EQ(iree_hal_device_group_device_count(group), 3u);

  const iree_hal_device_topology_info_t* info_a =
      iree_hal_device_topology_info(device_a);
  const iree_hal_device_topology_info_t* info_b =
      iree_hal_device_topology_info(device_b);
  const iree_hal_device_topology_info_t* info_c =
      iree_hal_device_topology_info(device_c);

  // All three devices should have topology indices 0, 1, 2.
  EXPECT_EQ(info_a->topology_index, 0u);
  EXPECT_EQ(info_b->topology_index, 1u);
  EXPECT_EQ(info_c->topology_index, 2u);

  // With zeroed capabilities, edge_from_capabilities produces edges with
  // IMPORT wait mode (non-NONE) and IMPORT signal mode, so can_wait_from
  // and can_signal_to should have all peer bits set.
  //
  // Device 0 peers are 1 and 2: bitmap = (1<<1) | (1<<2) = 0x6.
  // Device 1 peers are 0 and 2: bitmap = (1<<0) | (1<<2) = 0x5.
  // Device 2 peers are 0 and 1: bitmap = (1<<0) | (1<<1) = 0x3.
  EXPECT_EQ(info_a->can_wait_from, 0x6u);
  EXPECT_EQ(info_b->can_wait_from, 0x5u);
  EXPECT_EQ(info_c->can_wait_from, 0x3u);

  EXPECT_EQ(info_a->can_signal_to, 0x6u);
  EXPECT_EQ(info_b->can_signal_to, 0x5u);
  EXPECT_EQ(info_c->can_signal_to, 0x3u);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
  iree_hal_device_release(device_c);
}

//===----------------------------------------------------------------------===//
// Configurable capabilities tests
//===----------------------------------------------------------------------===//

// Devices with specific NUMA node assignments should produce a topology with
// those NUMA nodes set.
TEST(DeviceGroup, NumaNodeAssignment) {
  iree_hal_device_capabilities_t caps_a;
  memset(&caps_a, 0, sizeof(caps_a));
  caps_a.numa_node = 0;

  iree_hal_device_capabilities_t caps_b;
  memset(&caps_b, 0, sizeof(caps_b));
  caps_b.numa_node = 1;

  iree_hal_device_t* device_a =
      CreateMockDeviceWithCapabilities("mock", &caps_a);
  iree_hal_device_t* device_b =
      CreateMockDeviceWithCapabilities("mock", &caps_b);

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  const iree_hal_topology_t* topology = iree_hal_device_group_topology(group);
  EXPECT_EQ(topology->numa_nodes[0], 0u);
  EXPECT_EQ(topology->numa_nodes[1], 1u);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
}

//===----------------------------------------------------------------------===//
// Lifetime and reference counting tests
//===----------------------------------------------------------------------===//

// The group retains devices, so the device stays alive after the caller
// releases it.
TEST(DeviceGroup, GroupRetainsDevices) {
  iree_hal_device_t* device = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  // Release our reference — the group still holds one.
  iree_hal_device_release(device);

  // Device should still be accessible through the group.
  EXPECT_NE(iree_hal_device_group_device_at(group, 0), nullptr);

  // Releasing the group releases the last device reference.
  iree_hal_device_group_release(group);
}

// Retain/release on the group itself.
TEST(DeviceGroup, RetainRelease) {
  iree_hal_device_t* device = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  // Extra retain + release cycle.
  iree_hal_device_group_retain(group);
  iree_hal_device_group_release(group);

  // Group is still alive, topology pointer should be valid.
  const iree_hal_topology_t* topology = iree_hal_device_group_topology(group);
  EXPECT_EQ(topology->device_count, 1u);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device);
}

// Null is safe for retain/release.
TEST(DeviceGroup, NullRetainRelease) {
  iree_hal_device_group_retain(NULL);
  iree_hal_device_group_release(NULL);
}

//===----------------------------------------------------------------------===//
// Replacement group tests
//===----------------------------------------------------------------------===//

typedef struct DeviceGroupReplacementContext {
  iree_hal_device_t* replacements[2];
  iree_host_size_t call_count;
} DeviceGroupReplacementContext;

static iree_status_t ReplaceDeviceWithRetainedMock(
    iree_hal_device_group_t* source_group, iree_host_size_t device_index,
    iree_hal_device_t* source_device, void* user_data,
    iree_hal_device_t** out_replacement_device) {
  DeviceGroupReplacementContext* context =
      (DeviceGroupReplacementContext*)user_data;
  EXPECT_EQ(source_device,
            iree_hal_device_group_device_at(source_group, device_index));
  iree_hal_device_retain(context->replacements[device_index]);
  *out_replacement_device = context->replacements[device_index];
  ++context->call_count;
  return iree_ok_status();
}

TEST(DeviceGroup, CreateWithReplacementsPreservesTopology) {
  iree_hal_device_t* device_a = CreateMockDevice("mock");
  iree_hal_device_t* device_b = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* source_group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &source_group));

  iree_hal_device_t* replacement_a = CreateMockDevice("mock");
  iree_hal_device_t* replacement_b = CreateMockDevice("mock");
  DeviceGroupReplacementContext context = {};
  context.replacements[0] = replacement_a;
  context.replacements[1] = replacement_b;
  iree_hal_device_group_replacement_callback_t replacement_callback = {};
  replacement_callback.fn = ReplaceDeviceWithRetainedMock;
  replacement_callback.user_data = &context;

  iree_hal_device_group_t* replacement_group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_create_with_replacements(
      source_group, replacement_callback, iree_allocator_system(),
      &replacement_group));

  EXPECT_EQ(2u, context.call_count);
  EXPECT_EQ(2u, iree_hal_device_group_device_count(replacement_group));
  EXPECT_EQ(replacement_a,
            iree_hal_device_group_device_at(replacement_group, 0));
  EXPECT_EQ(replacement_b,
            iree_hal_device_group_device_at(replacement_group, 1));
  EXPECT_EQ(iree_hal_device_group_topology(source_group)->device_count,
            iree_hal_device_group_topology(replacement_group)->device_count);
  EXPECT_EQ(iree_hal_device_topology_info(replacement_a)->topology,
            iree_hal_device_group_topology(replacement_group));
  EXPECT_EQ(iree_hal_device_topology_info(replacement_a)->topology_index, 0u);
  EXPECT_EQ(iree_hal_device_topology_info(replacement_b)->topology,
            iree_hal_device_group_topology(replacement_group));
  EXPECT_EQ(iree_hal_device_topology_info(replacement_b)->topology_index, 1u);

  iree_hal_device_group_release(replacement_group);
  iree_hal_device_release(replacement_a);
  iree_hal_device_release(replacement_b);
  iree_hal_device_group_release(source_group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
}

//===----------------------------------------------------------------------===//
// Topology pointer stability tests
//===----------------------------------------------------------------------===//

// The topology pointer returned by the group must remain stable and match
// what was assigned to devices.
TEST(DeviceGroup, TopologyPointerStability) {
  iree_hal_device_t* device_a = CreateMockDevice("mock");
  iree_hal_device_t* device_b = CreateMockDevice("mock");

  iree_hal_device_group_builder_t builder;
  InitializeDeviceGroupBuilder(&builder);
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_a));
  IREE_ASSERT_OK(iree_hal_device_group_builder_add_device(&builder, device_b));

  iree_hal_device_group_t* group = NULL;
  IREE_ASSERT_OK(iree_hal_device_group_builder_finalize(
      &builder, iree_allocator_system(), &group));

  const iree_hal_topology_t* group_topology =
      iree_hal_device_group_topology(group);
  const iree_hal_device_topology_info_t* info_a =
      iree_hal_device_topology_info(device_a);
  const iree_hal_device_topology_info_t* info_b =
      iree_hal_device_topology_info(device_b);

  // Both devices should point at the same topology that the group owns.
  EXPECT_EQ(info_a->topology, group_topology);
  EXPECT_EQ(info_b->topology, group_topology);

  iree_hal_device_group_release(group);
  iree_hal_device_release(device_a);
  iree_hal_device_release(device_b);
}

}  // namespace
}  // namespace iree::hal
