blob: ddf1241c5cbcccf4e6cabccf17eb12f857ef3810 [file]
// 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/hal/device_group.h"
#include <string.h>
#include "iree/async/frontier_tracker.h"
#include "iree/base/internal/atomics.h"
#include "iree/hal/topology_builder.h"
//===----------------------------------------------------------------------===//
// iree_hal_device_group_t
//===----------------------------------------------------------------------===//
struct iree_hal_device_group_t {
// Retain/release counter for the group object.
iree_atomic_ref_count_t ref_count;
// Host allocator used to free the group object.
iree_allocator_t host_allocator;
// Number of devices in the group.
iree_host_size_t device_count;
// Retained frontier tracker shared by all devices in the group.
iree_async_frontier_tracker_t* frontier_tracker;
// Retained devices. Order defines topology indices (device i = devices[i]).
iree_hal_device_t* devices[IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT];
// Cached driver name for each device (backed by the device's own storage,
// valid for the device's lifetime).
iree_string_view_t driver_names[IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT];
// Immutable topology matrix built during creation.
// Embedded (not heap-allocated) so devices can hold a stable pointer to it
// for the group's lifetime.
iree_hal_topology_t topology;
};
IREE_API_EXPORT iree_status_t iree_hal_device_group_create_from_device(
iree_hal_device_t* device, iree_async_frontier_tracker_t* frontier_tracker,
iree_allocator_t host_allocator, iree_hal_device_group_t** out_group) {
IREE_ASSERT_ARGUMENT(device);
IREE_ASSERT_ARGUMENT(frontier_tracker);
IREE_ASSERT_ARGUMENT(out_group);
*out_group = NULL;
iree_hal_device_group_builder_t builder;
iree_hal_device_group_builder_initialize(&builder, frontier_tracker);
iree_status_t status =
iree_hal_device_group_builder_add_device(&builder, device);
if (iree_status_is_ok(status)) {
status = iree_hal_device_group_builder_finalize(&builder, host_allocator,
out_group);
} else {
iree_hal_device_group_builder_deinitialize(&builder);
}
return status;
}
static void iree_hal_device_group_destroy(iree_hal_device_group_t* group) {
IREE_ASSERT_ARGUMENT(group);
IREE_TRACE_ZONE_BEGIN(z0);
for (iree_host_size_t i = 0; i < group->device_count; ++i) {
iree_hal_device_release(group->devices[i]);
}
iree_async_frontier_tracker_release(group->frontier_tracker);
iree_allocator_t host_allocator = group->host_allocator;
iree_allocator_free(host_allocator, group);
IREE_TRACE_ZONE_END(z0);
}
IREE_API_EXPORT void iree_hal_device_group_retain(
iree_hal_device_group_t* group) {
if (group) {
iree_atomic_ref_count_inc(&group->ref_count);
}
}
IREE_API_EXPORT void iree_hal_device_group_release(
iree_hal_device_group_t* group) {
if (group && iree_atomic_ref_count_dec(&group->ref_count) == 1) {
iree_hal_device_group_destroy(group);
}
}
IREE_API_EXPORT iree_host_size_t
iree_hal_device_group_device_count(const iree_hal_device_group_t* group) {
IREE_ASSERT_ARGUMENT(group);
return group->device_count;
}
IREE_API_EXPORT iree_hal_device_t* iree_hal_device_group_device_at(
const iree_hal_device_group_t* group, iree_host_size_t index) {
IREE_ASSERT_ARGUMENT(group);
if (index >= group->device_count) return NULL;
return group->devices[index];
}
IREE_API_EXPORT const iree_hal_topology_t* iree_hal_device_group_topology(
const iree_hal_device_group_t* group) {
IREE_ASSERT_ARGUMENT(group);
return &group->topology;
}
//===----------------------------------------------------------------------===//
// iree_hal_device_group_builder_t
//===----------------------------------------------------------------------===//
IREE_API_EXPORT void iree_hal_device_group_builder_initialize(
iree_hal_device_group_builder_t* builder,
iree_async_frontier_tracker_t* frontier_tracker) {
IREE_ASSERT_ARGUMENT(builder);
IREE_ASSERT_ARGUMENT(frontier_tracker);
memset(builder, 0, sizeof(*builder));
builder->frontier_tracker = frontier_tracker;
iree_async_frontier_tracker_retain(frontier_tracker);
}
IREE_API_EXPORT void iree_hal_device_group_builder_deinitialize(
iree_hal_device_group_builder_t* builder) {
IREE_ASSERT_ARGUMENT(builder);
iree_async_frontier_tracker_release(builder->frontier_tracker);
memset(builder, 0, sizeof(*builder));
}
IREE_API_EXPORT iree_status_t iree_hal_device_group_builder_add_device(
iree_hal_device_group_builder_t* builder, iree_hal_device_t* device) {
IREE_ASSERT_ARGUMENT(builder);
IREE_ASSERT_ARGUMENT(device);
if (builder->count >= IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT) {
return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED,
"device group builder at max capacity (%d)",
IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT);
}
builder->devices[builder->count++] = device;
return iree_ok_status();
}
// Computes pre-populated bitmaps for a device's topology info by scanning
// its edges in the topology matrix.
static void iree_hal_device_group_compute_bitmaps(
const iree_hal_topology_t* topology, uint32_t device_index,
iree_hal_device_topology_info_t* out_info) {
uint32_t device_count = topology->device_count;
out_info->can_wait_from = 0;
out_info->can_signal_to = 0;
out_info->can_import_from = 0;
out_info->can_p2p_with = 0;
for (uint32_t j = 0; j < device_count; ++j) {
if (j == device_index) continue;
// can_wait_from: can device_index wait on device j's semaphores?
// Edge[j][device_index] describes how device_index interacts with j's
// resources.
iree_hal_topology_edge_t edge_from_j =
iree_hal_topology_query_edge(topology, j, device_index);
if (iree_hal_topology_edge_wait_mode(edge_from_j.lo) !=
IREE_HAL_TOPOLOGY_INTEROP_MODE_NONE) {
out_info->can_wait_from |= (iree_hal_topology_device_bitmap_t)1 << j;
}
// can_import_from: can device_index import buffers from device j?
// Set if either non-coherent or coherent buffers can be shared without
// a host-staged copy.
iree_hal_topology_interop_mode_t nc_read =
iree_hal_topology_edge_buffer_read_mode_noncoherent(edge_from_j.lo);
iree_hal_topology_interop_mode_t c_read =
iree_hal_topology_edge_buffer_read_mode_coherent(edge_from_j.lo);
if (nc_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE ||
nc_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_IMPORT ||
c_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE ||
c_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_IMPORT) {
out_info->can_import_from |= (iree_hal_topology_device_bitmap_t)1 << j;
}
// can_p2p_with: P2P access between device_index and device j?
if (nc_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE ||
c_read == IREE_HAL_TOPOLOGY_INTEROP_MODE_NATIVE ||
(iree_hal_topology_edge_capability_flags(edge_from_j.lo) &
IREE_HAL_TOPOLOGY_CAPABILITY_P2P_COPY)) {
out_info->can_p2p_with |= (iree_hal_topology_device_bitmap_t)1 << j;
}
// can_signal_to: can device_index signal to device j?
// Edge[device_index][j] describes how j interacts with device_index's
// resources — the signal_mode tells us if j can observe our signals.
iree_hal_topology_edge_t edge_to_j =
iree_hal_topology_query_edge(topology, device_index, j);
if (iree_hal_topology_edge_signal_mode(edge_to_j.lo) !=
IREE_HAL_TOPOLOGY_INTEROP_MODE_NONE) {
out_info->can_signal_to |= (iree_hal_topology_device_bitmap_t)1 << j;
}
}
}
IREE_API_EXPORT iree_status_t iree_hal_device_group_create_with_replacements(
iree_hal_device_group_t* source_group,
iree_hal_device_group_replacement_callback_t replacement_callback,
iree_allocator_t host_allocator, iree_hal_device_group_t** out_group) {
IREE_ASSERT_ARGUMENT(source_group);
IREE_ASSERT_ARGUMENT(out_group);
*out_group = NULL;
IREE_TRACE_ZONE_BEGIN(z0);
if (IREE_UNLIKELY(!replacement_callback.fn)) {
IREE_TRACE_ZONE_END(z0);
return iree_make_status(
IREE_STATUS_INVALID_ARGUMENT,
"device group replacement callback function is required");
}
const iree_host_size_t device_count = source_group->device_count;
iree_hal_device_group_t* group = NULL;
iree_status_t status =
iree_allocator_malloc(host_allocator, sizeof(*group), (void**)&group);
if (!iree_status_is_ok(status)) {
IREE_TRACE_ZONE_END(z0);
return status;
}
memset(group, 0, sizeof(*group));
iree_atomic_ref_count_init(&group->ref_count);
group->host_allocator = host_allocator;
group->device_count = device_count;
group->frontier_tracker = source_group->frontier_tracker;
iree_async_frontier_tracker_retain(group->frontier_tracker);
group->topology = source_group->topology;
for (iree_host_size_t i = 0; i < device_count && iree_status_is_ok(status);
++i) {
iree_hal_device_t* replacement_device = NULL;
status = replacement_callback.fn(source_group, i, source_group->devices[i],
replacement_callback.user_data,
&replacement_device);
if (iree_status_is_ok(status) && IREE_UNLIKELY(!replacement_device)) {
status = iree_make_status(
IREE_STATUS_INVALID_ARGUMENT,
"replacement device %" PRIhsz " must not be NULL", i);
}
if (iree_status_is_ok(status)) {
group->devices[i] = replacement_device;
} else {
iree_hal_device_release(replacement_device);
}
group->driver_names[i] = source_group->driver_names[i];
}
iree_host_size_t assigned_device_count = 0;
for (iree_host_size_t i = 0; i < device_count && iree_status_is_ok(status);
++i) {
iree_hal_device_topology_info_t topology_info;
memset(&topology_info, 0, sizeof(topology_info));
topology_info.topology_index = (uint32_t)i;
topology_info.topology = &group->topology;
topology_info.self_edge =
iree_hal_topology_query_edge(&group->topology, (uint32_t)i, (uint32_t)i)
.lo;
topology_info.frontier.tracker = group->frontier_tracker;
topology_info.frontier.base_axis = iree_async_axis_make_queue(
iree_async_frontier_tracker_session_epoch(group->frontier_tracker),
iree_async_frontier_tracker_machine_index(group->frontier_tracker),
(uint8_t)i, /*queue_index=*/0);
iree_hal_device_group_compute_bitmaps(&group->topology, (uint32_t)i,
&topology_info);
status =
iree_hal_device_assign_topology_info(group->devices[i], &topology_info);
if (iree_status_is_ok(status)) {
assigned_device_count = i + 1;
}
}
if (iree_status_is_ok(status)) {
*out_group = group;
} else {
for (iree_host_size_t i = 0; i < assigned_device_count; ++i) {
status = iree_status_join(status, iree_hal_device_assign_topology_info(
group->devices[i], NULL));
}
iree_hal_device_group_release(group);
}
IREE_TRACE_ZONE_END(z0);
return status;
}
IREE_API_EXPORT iree_status_t iree_hal_device_group_builder_finalize(
iree_hal_device_group_builder_t* builder, iree_allocator_t host_allocator,
iree_hal_device_group_t** out_group) {
IREE_ASSERT_ARGUMENT(builder);
IREE_ASSERT_ARGUMENT(out_group);
*out_group = NULL;
IREE_TRACE_ZONE_BEGIN(z0);
iree_host_size_t device_count = builder->count;
if (device_count == 0) {
iree_hal_device_group_builder_deinitialize(builder);
IREE_TRACE_ZONE_END(z0);
return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
"device group builder has no devices");
}
// Allocate the group.
iree_hal_device_group_t* group = NULL;
iree_status_t status =
iree_allocator_malloc(host_allocator, sizeof(*group), (void**)&group);
if (!iree_status_is_ok(status)) {
iree_hal_device_group_builder_deinitialize(builder);
IREE_TRACE_ZONE_END(z0);
return status;
}
memset(group, 0, sizeof(*group));
iree_atomic_ref_count_init(&group->ref_count);
group->host_allocator = host_allocator;
group->device_count = device_count;
group->frontier_tracker = builder->frontier_tracker;
builder->frontier_tracker = NULL;
// Retain all devices and cache driver names.
for (iree_host_size_t i = 0; i < device_count; ++i) {
group->devices[i] = builder->devices[i];
iree_hal_device_retain(group->devices[i]);
group->driver_names[i] = iree_hal_device_id(group->devices[i]);
}
// Invalidate the builder now — we've taken everything we need.
memset(builder, 0, sizeof(*builder));
// Query capabilities from all devices.
iree_hal_device_capabilities_t
capabilities[IREE_HAL_TOPOLOGY_MAX_DEVICE_COUNT];
memset(capabilities, 0, sizeof(capabilities));
for (iree_host_size_t i = 0; i < device_count && iree_status_is_ok(status);
++i) {
status =
iree_hal_device_query_capabilities(group->devices[i], &capabilities[i]);
}
// Build topology.
iree_hal_topology_builder_t topology_builder;
iree_hal_topology_builder_initialize(&topology_builder,
(uint32_t)device_count);
// Compute and set edges for all device pairs.
for (uint32_t i = 0; i < (uint32_t)device_count && iree_status_is_ok(status);
++i) {
for (uint32_t j = 0;
j < (uint32_t)device_count && iree_status_is_ok(status); ++j) {
if (i == j) continue; // Self-edges are pre-initialized.
// Compute base edge from capabilities.
iree_hal_topology_edge_t edge = iree_hal_topology_edge_from_capabilities(
&capabilities[i], &capabilities[j], group->driver_names[i],
group->driver_names[j]);
// Allow same-driver devices to refine the edge with hardware-specific
// knowledge (e.g., NVLink topology, Infinity Fabric link widths).
if (iree_string_view_equal(group->driver_names[i],
group->driver_names[j])) {
status = iree_hal_device_refine_topology_edge(group->devices[i],
group->devices[j], &edge);
if (!iree_status_is_ok(status)) break;
}
status =
iree_hal_topology_builder_set_edge(&topology_builder, i, j, edge);
}
}
// Set NUMA nodes from queried capabilities.
for (uint32_t i = 0; i < (uint32_t)device_count && iree_status_is_ok(status);
++i) {
status = iree_hal_topology_builder_set_numa_node(&topology_builder, i,
capabilities[i].numa_node);
}
// Finalize topology.
if (iree_status_is_ok(status)) {
status =
iree_hal_topology_builder_finalize(&topology_builder, &group->topology);
}
// Assign topology info to each device.
iree_host_size_t assigned_device_count = 0;
for (iree_host_size_t i = 0; i < device_count && iree_status_is_ok(status);
++i) {
iree_hal_device_topology_info_t topology_info;
memset(&topology_info, 0, sizeof(topology_info));
topology_info.topology_index = (uint32_t)i;
topology_info.topology = &group->topology;
topology_info.self_edge =
iree_hal_topology_query_edge(&group->topology, (uint32_t)i, (uint32_t)i)
.lo;
topology_info.frontier.tracker = group->frontier_tracker;
topology_info.frontier.base_axis = iree_async_axis_make_queue(
iree_async_frontier_tracker_session_epoch(group->frontier_tracker),
iree_async_frontier_tracker_machine_index(group->frontier_tracker),
(uint8_t)i, /*queue_index=*/0);
iree_hal_device_group_compute_bitmaps(&group->topology, (uint32_t)i,
&topology_info);
status =
iree_hal_device_assign_topology_info(group->devices[i], &topology_info);
if (iree_status_is_ok(status)) {
assigned_device_count = i + 1;
}
}
if (iree_status_is_ok(status)) {
*out_group = group;
} else {
for (iree_host_size_t i = 0; i < assigned_device_count; ++i) {
status = iree_status_join(status, iree_hal_device_assign_topology_info(
group->devices[i], NULL));
}
iree_hal_device_group_release(group);
}
IREE_TRACE_ZONE_END(z0);
return status;
}