[cuda] Port over channel implementation via NCCL (#14059)
This commit ports over channel implementation via NCCL.
Modifications over the existing code is mostly around removing
the context wrapper and making status handling better.
Progress towards https://github.com/openxla/iree/issues/13245
diff --git a/experimental/cuda2/CMakeLists.txt b/experimental/cuda2/CMakeLists.txt
index d8f3df0..3f3fdef 100644
--- a/experimental/cuda2/CMakeLists.txt
+++ b/experimental/cuda2/CMakeLists.txt
@@ -26,6 +26,8 @@
"cuda_driver.c"
"memory_pools.c"
"memory_pools.h"
+ "nccl_channel.c"
+ "nccl_channel.h"
"pipeline_layout.c"
"pipeline_layout.h"
DEPS
@@ -61,6 +63,7 @@
iree::base
iree::base::internal::dynamic_library
iree_cuda::headers
+ nccl::headers
PUBLIC
)
diff --git a/experimental/cuda2/api.h b/experimental/cuda2/api.h
index 777d6ef..20951b0 100644
--- a/experimental/cuda2/api.h
+++ b/experimental/cuda2/api.h
@@ -20,6 +20,11 @@
// iree_hal_cuda2_device_t
//===----------------------------------------------------------------------===//
+// ncclUniqueId exposed without exporting the NCCL headers.
+typedef struct {
+ char data[128];
+} iree_hal_cuda2_nccl_id_t;
+
// Parameters defining a CUmemoryPool.
typedef struct iree_hal_cuda2_memory_pool_params_t {
// Minimum number of bytes to keep in the pool when trimming with
diff --git a/experimental/cuda2/cuda_device.c b/experimental/cuda2/cuda_device.c
index 5ed2dc3..7bbfdd5 100644
--- a/experimental/cuda2/cuda_device.c
+++ b/experimental/cuda2/cuda_device.c
@@ -6,7 +6,6 @@
#include "experimental/cuda2/cuda_device.h"
-#include <iree/hal/device.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
@@ -16,6 +15,8 @@
#include "experimental/cuda2/cuda_dynamic_symbols.h"
#include "experimental/cuda2/cuda_status_util.h"
#include "experimental/cuda2/memory_pools.h"
+#include "experimental/cuda2/nccl_channel.h"
+#include "experimental/cuda2/nccl_dynamic_symbols.h"
#include "experimental/cuda2/pipeline_layout.h"
#include "iree/base/internal/arena.h"
#include "iree/base/internal/math.h"
@@ -41,6 +42,7 @@
iree_hal_driver_t* driver;
const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols;
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols;
// Parameters used to control device behavior.
iree_hal_cuda2_device_params_t params;
@@ -56,6 +58,9 @@
bool supports_memory_pools;
iree_hal_cuda2_memory_pools_t memory_pools;
iree_hal_allocator_t* device_allocator;
+
+ // Optional provider used for creating/configuring collective channels.
+ iree_hal_channel_provider_t* channel_provider;
} iree_hal_cuda2_device_t;
static const iree_hal_device_vtable_t iree_hal_cuda2_device_vtable;
@@ -96,7 +101,8 @@
iree_hal_driver_t* driver, iree_string_view_t identifier,
const iree_hal_cuda2_device_params_t* params, CUdevice cu_device,
CUstream stream, CUcontext context,
- const iree_hal_cuda2_dynamic_symbols_t* symbols,
+ const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols,
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols,
iree_allocator_t host_allocator, iree_hal_device_t** out_device) {
iree_hal_cuda2_device_t* device = NULL;
iree_host_size_t total_size = iree_sizeof_struct(*device) + identifier.size;
@@ -113,7 +119,8 @@
&device->block_pool);
device->driver = driver;
iree_hal_driver_retain(device->driver);
- device->cuda_symbols = symbols;
+ device->cuda_symbols = cuda_symbols;
+ device->nccl_symbols = nccl_symbols;
device->params = *params;
device->cu_context = context;
device->cu_device = cu_device;
@@ -126,7 +133,7 @@
if (iree_status_is_ok(status) && params->async_allocations) {
int supports_memory_pools = 0;
status = IREE_CURESULT_TO_STATUS(
- symbols,
+ cuda_symbols,
cuDeviceGetAttribute(&supports_memory_pools,
CU_DEVICE_ATTRIBUTE_MEMORY_POOLS_SUPPORTED,
cu_device),
@@ -137,13 +144,13 @@
// Create memory pools first so that we can share them with the allocator.
if (iree_status_is_ok(status) && device->supports_memory_pools) {
status = iree_hal_cuda2_memory_pools_initialize(
- symbols, cu_device, ¶ms->memory_pools, host_allocator,
+ cuda_symbols, cu_device, ¶ms->memory_pools, host_allocator,
&device->memory_pools);
}
if (iree_status_is_ok(status)) {
status = iree_hal_cuda2_allocator_create(
- (iree_hal_device_t*)device, symbols, cu_device, stream,
+ (iree_hal_device_t*)device, cuda_symbols, cu_device, stream,
device->supports_memory_pools ? &device->memory_pools : NULL,
host_allocator, &device->device_allocator);
}
@@ -159,11 +166,12 @@
iree_status_t iree_hal_cuda2_device_create(
iree_hal_driver_t* driver, iree_string_view_t identifier,
const iree_hal_cuda2_device_params_t* params,
- const iree_hal_cuda2_dynamic_symbols_t* symbols, CUdevice device,
+ const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols,
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols, CUdevice device,
iree_allocator_t host_allocator, iree_hal_device_t** out_device) {
IREE_ASSERT_ARGUMENT(driver);
IREE_ASSERT_ARGUMENT(params);
- IREE_ASSERT_ARGUMENT(symbols);
+ IREE_ASSERT_ARGUMENT(cuda_symbols);
IREE_ASSERT_ARGUMENT(out_device);
IREE_TRACE_ZONE_BEGIN(z0);
@@ -173,27 +181,27 @@
CUcontext context = NULL;
if (iree_status_is_ok(status)) {
status = IREE_CURESULT_TO_STATUS(
- symbols, cuDevicePrimaryCtxRetain(&context, device));
+ cuda_symbols, cuDevicePrimaryCtxRetain(&context, device));
}
if (iree_status_is_ok(status)) {
- status = IREE_CURESULT_TO_STATUS(symbols, cuCtxSetCurrent(context));
+ status = IREE_CURESULT_TO_STATUS(cuda_symbols, cuCtxSetCurrent(context));
}
// Create the default stream for the device.
CUstream stream = NULL;
if (iree_status_is_ok(status)) {
status = IREE_CURESULT_TO_STATUS(
- symbols, cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING));
+ cuda_symbols, cuStreamCreate(&stream, CU_STREAM_NON_BLOCKING));
}
if (iree_status_is_ok(status)) {
status = iree_hal_cuda2_device_create_internal(
- driver, identifier, params, device, stream, context, symbols,
- host_allocator, out_device);
+ driver, identifier, params, device, stream, context, cuda_symbols,
+ nccl_symbols, host_allocator, out_device);
}
if (!iree_status_is_ok(status)) {
- if (stream) symbols->cuStreamDestroy(stream);
- if (context) symbols->cuDevicePrimaryCtxRelease(device);
+ if (stream) cuda_symbols->cuStreamDestroy(stream);
+ if (context) cuda_symbols->cuDevicePrimaryCtxRelease(device);
}
IREE_TRACE_ZONE_END(z0);
@@ -221,6 +229,9 @@
// There should be no more buffers live that use the allocator.
iree_hal_allocator_release(device->device_allocator);
+ // Buffers may have been retaining collective resources.
+ iree_hal_channel_provider_release(device->channel_provider);
+
// Destroy memory pools that hold on to reserved memory.
iree_hal_cuda2_memory_pools_deinitialize(&device->memory_pools);
@@ -269,7 +280,10 @@
static void iree_hal_cuda2_replace_channel_provider(
iree_hal_device_t* base_device, iree_hal_channel_provider_t* new_provider) {
- // TODO: implement this together with channel support.
+ iree_hal_cuda2_device_t* device = iree_hal_cuda2_device_cast(base_device);
+ iree_hal_channel_provider_retain(new_provider);
+ iree_hal_channel_provider_release(device->channel_provider);
+ device->channel_provider = new_provider;
}
static iree_status_t iree_hal_cuda2_device_trim(
@@ -327,8 +341,87 @@
static iree_status_t iree_hal_cuda2_device_create_channel(
iree_hal_device_t* base_device, iree_hal_queue_affinity_t queue_affinity,
iree_hal_channel_params_t params, iree_hal_channel_t** out_channel) {
- return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
- "channel not yet implmeneted");
+ iree_hal_cuda2_device_t* device = iree_hal_cuda2_device_cast(base_device);
+ if (!device->nccl_symbols || !device->nccl_symbols->dylib) {
+ return iree_make_status(
+ IREE_STATUS_UNAVAILABLE,
+ "NCCL runtime library (%d.%d.%d) not available; ensure installed and "
+ "the shared library is on your PATH/LD_LIBRARY_PATH "
+ "(nccl.dll/libnccl.so)",
+ NCCL_MAJOR, NCCL_MINOR, NCCL_PATCH);
+ }
+
+ // Today we only allow a single logical device per channel.
+ // We could multiplex channels but it'd be better to surface that to the
+ // compiler so that it can emit the right rank math.
+ int requested_count = iree_math_count_ones_u64(queue_affinity);
+ // TODO(#12206): properly assign affinity in the compiler.
+ if (requested_count != 64 && requested_count != 1) {
+ return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+ "exactly one participant is allowed in a "
+ "channel but %d were specified",
+ requested_count);
+ }
+
+ // Ask the channel provider (if configured) for the default rank and count
+ // if the user did not set them.
+ if (device->channel_provider &&
+ (params.rank == IREE_HAL_CHANNEL_RANK_DEFAULT ||
+ params.count == IREE_HAL_CHANNEL_COUNT_DEFAULT)) {
+ IREE_RETURN_IF_ERROR(
+ iree_hal_channel_provider_query_default_rank_and_count(
+ device->channel_provider, ¶ms.rank, ¶ms.count),
+ "querying default collective group rank and count");
+ }
+
+ // An ID is required to initialize NCCL. On the root it'll be the local ID and
+ // on all other participants it'll be the root ID.
+ iree_hal_cuda2_nccl_id_t id;
+ memset(&id, 0, sizeof(id));
+ if (iree_const_byte_span_is_empty(params.id)) {
+ // User wants the default ID.
+ if (!device->channel_provider) {
+ return iree_make_status(
+ IREE_STATUS_INVALID_ARGUMENT,
+ "default collective channel ID requested but no channel provider has "
+ "been set on the device to provide it");
+ }
+ if (params.rank == 0) {
+ // Bootstrap NCCL to get the root ID.
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_nccl_get_unique_id(device->nccl_symbols, &id),
+ "bootstrapping NCCL root");
+ }
+ // Exchange NCCL ID with all participants.
+ IREE_RETURN_IF_ERROR(iree_hal_channel_provider_exchange_default_id(
+ device->channel_provider,
+ iree_make_byte_span((void*)&id, sizeof(id))),
+ "exchanging NCCL ID with other participants");
+ } else if (params.id.data_length != IREE_ARRAYSIZE(id.data)) {
+ // User provided something but it's not what we expect.
+ return iree_make_status(
+ IREE_STATUS_INVALID_ARGUMENT,
+ "NCCL ID must be %" PRIhsz
+ " bytes matching the ncclUniqueId struct but caller provided %" PRIhsz
+ " bytes",
+ IREE_ARRAYSIZE(id.data), sizeof(id));
+ } else {
+ // User provided the ID - we treat it as opaque here and let NCCL validate.
+ memcpy(id.data, params.id.data, IREE_ARRAYSIZE(id.data));
+ }
+
+ if (iree_hal_cuda2_nccl_id_is_empty(&id)) {
+ // TODO: maybe this is ok? a localhost alias or something?
+ return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+ "no default NCCL ID specified (all zeros)");
+ }
+
+ // TODO: when we support multiple logical devices we'll want to pass in the
+ // context of the device mapped to the queue_affinity. For now since this
+ // implementation only supports one device we pass in the only one we have.
+ return iree_hal_cuda2_nccl_channel_create(
+ device->cuda_symbols, device->nccl_symbols, &id, params.rank,
+ params.count, device->host_allocator, out_channel);
}
static iree_status_t iree_hal_cuda2_device_create_command_buffer(
diff --git a/experimental/cuda2/cuda_device.h b/experimental/cuda2/cuda_device.h
index 70dbd36..5def0de 100644
--- a/experimental/cuda2/cuda_device.h
+++ b/experimental/cuda2/cuda_device.h
@@ -9,6 +9,7 @@
#include "experimental/cuda2/api.h"
#include "experimental/cuda2/cuda_dynamic_symbols.h"
+#include "experimental/cuda2/nccl_dynamic_symbols.h"
#include "iree/base/api.h"
#include "iree/hal/api.h"
@@ -20,7 +21,8 @@
iree_status_t iree_hal_cuda2_device_create(
iree_hal_driver_t* driver, iree_string_view_t identifier,
const iree_hal_cuda2_device_params_t* params,
- const iree_hal_cuda2_dynamic_symbols_t* symbols, CUdevice device,
+ const iree_hal_cuda2_dynamic_symbols_t* symbols,
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols, CUdevice device,
iree_allocator_t host_allocator, iree_hal_device_t** out_device);
// Returns the CUDA context bound to the given |device| if it is a CUDA device
diff --git a/experimental/cuda2/cuda_driver.c b/experimental/cuda2/cuda_driver.c
index 40c09a4..b72dab2 100644
--- a/experimental/cuda2/cuda_driver.c
+++ b/experimental/cuda2/cuda_driver.c
@@ -489,7 +489,7 @@
// Attempt to create the device now.
iree_status_t status = iree_hal_cuda2_device_create(
base_driver, device_name, &driver->device_params, &driver->cuda_symbols,
- device, host_allocator, out_device);
+ &driver->nccl_symbols, device, host_allocator, out_device);
IREE_TRACE_ZONE_END(z0);
return status;
diff --git a/experimental/cuda2/nccl_channel.c b/experimental/cuda2/nccl_channel.c
new file mode 100644
index 0000000..6d3cbb1
--- /dev/null
+++ b/experimental/cuda2/nccl_channel.c
@@ -0,0 +1,568 @@
+// Copyright 2023 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 "experimental/cuda2/nccl_channel.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "experimental/cuda2/cuda_buffer.h"
+#include "experimental/cuda2/cuda_status_util.h"
+#include "experimental/cuda2/nccl_headers.h"
+#include "experimental/cuda2/nccl_status_util.h"
+#include "iree/base/api.h"
+#include "iree/base/tracing.h"
+
+typedef struct iree_hal_cuda2_nccl_channel_t {
+ iree_hal_resource_t resource;
+
+ const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols;
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols;
+
+ iree_allocator_t host_allocator;
+
+ // Parent channel this was split from, if any.
+ // This is only used to keep the parent channel live for as long as there are
+ // any split channels live (including transitive splits).
+ iree_hal_channel_t* parent_channel;
+
+ // This participant's rank in the communicator.
+ // Equivalent to ncclCommUserRank.
+ int rank;
+ // Total number of participants in the communicator.
+ // Equivalent to ncclCommCount.
+ int count;
+
+ // Communicator handle.
+ ncclComm_t comm;
+
+ // Hash of the unique ID used to create the communicator.
+ // This is consistent with the hashes NCCL itself uses for logging but is not
+ // guaranteed to be unique - only use for informational purposes.
+ IREE_TRACE(uint64_t id_hash;)
+} iree_hal_cuda2_nccl_channel_t;
+
+static const iree_hal_channel_vtable_t iree_hal_cuda2_nccl_channel_vtable;
+
+static iree_hal_cuda2_nccl_channel_t* iree_hal_cuda2_nccl_channel_cast(
+ iree_hal_channel_t* base_value) {
+ IREE_HAL_ASSERT_TYPE(base_value, &iree_hal_cuda2_nccl_channel_vtable);
+ return (iree_hal_cuda2_nccl_channel_t*)base_value;
+}
+
+static const iree_hal_cuda2_nccl_channel_t*
+iree_hal_cuda2_nccl_channel_const_cast(const iree_hal_channel_t* base_value) {
+ IREE_HAL_ASSERT_TYPE(base_value, &iree_hal_cuda2_nccl_channel_vtable);
+ return (const iree_hal_cuda2_nccl_channel_t*)base_value;
+}
+
+// Returns the same value as NCCL's init.cc hashUniqueId.
+// These magic constants were chosen by their implementation and unlikely to
+// be stable as it's not part of their public API. So they are only meant to be
+// used for correlating debug logging/traces. We keep it internal here too so
+// that we aren't tempted to use it in other places.
+static uint64_t iree_hal_cuda2_nccl_hash_id(
+ const iree_hal_cuda2_nccl_id_t* id) {
+ uint64_t hash = 0xDEADBEEF;
+ for (iree_host_size_t i = 0; i < sizeof(*id); i++) {
+ hash ^= hash >> 32;
+ hash *= 0x8DB3DB47FA2994ADull;
+ hash += id->data[i];
+ }
+ return hash;
+}
+
+iree_status_t iree_hal_cuda2_nccl_get_unique_id(
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* symbols,
+ iree_hal_cuda2_nccl_id_t* out_id) {
+ static_assert(sizeof(*out_id) == sizeof(ncclUniqueId),
+ "NCCL ID size mismatch");
+
+ IREE_ASSERT_ARGUMENT(symbols);
+ IREE_ASSERT_ARGUMENT(out_id);
+ IREE_TRACE_ZONE_BEGIN(z0);
+
+ memset(out_id, 0, sizeof(*out_id));
+ iree_status_t status = IREE_NCCL_RESULT_TO_STATUS(
+ symbols, ncclGetUniqueId((ncclUniqueId*)out_id), "ncclGetUniqueId");
+
+ IREE_TRACE_ZONE_END(z0);
+ return status;
+}
+
+iree_status_t iree_hal_cuda2_nccl_channel_create(
+ const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols,
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols,
+ const iree_hal_cuda2_nccl_id_t* id, int rank, int count,
+ iree_allocator_t host_allocator, iree_hal_channel_t** out_channel) {
+ IREE_ASSERT_ARGUMENT(cuda_symbols);
+ IREE_ASSERT_ARGUMENT(nccl_symbols);
+ IREE_ASSERT_ARGUMENT(id);
+ IREE_ASSERT_ARGUMENT(out_channel);
+ IREE_TRACE_ZONE_BEGIN(z0);
+
+ *out_channel = NULL;
+ IREE_TRACE(const uint64_t id_hash = iree_hal_cuda2_nccl_hash_id(id));
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, id_hash);
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, rank);
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, count);
+
+ ncclComm_t comm = NULL;
+ ncclConfig_t config = NCCL_CONFIG_INITIALIZER;
+ // TODO: use async to check a timeout.
+ config.blocking = 1;
+ IREE_NCCL_RETURN_AND_END_ZONE_IF_ERROR(
+ z0, nccl_symbols,
+ ncclCommInitRankConfig(&comm, count, *((const ncclUniqueId*)id), rank,
+ &config),
+ "ncclCommInitRankConfig");
+
+ iree_hal_cuda2_nccl_channel_t* channel = NULL;
+ iree_status_t status =
+ iree_allocator_malloc(host_allocator, sizeof(*channel), (void**)&channel);
+
+ if (iree_status_is_ok(status)) {
+ iree_hal_resource_initialize(&iree_hal_cuda2_nccl_channel_vtable,
+ &channel->resource);
+ channel->cuda_symbols = cuda_symbols;
+ channel->nccl_symbols = nccl_symbols;
+ channel->host_allocator = host_allocator;
+ channel->parent_channel = NULL;
+ channel->rank = rank;
+ channel->count = count;
+ channel->comm = comm;
+ IREE_TRACE(channel->id_hash = id_hash);
+ *out_channel = (iree_hal_channel_t*)channel;
+ }
+
+ IREE_TRACE_ZONE_END(z0);
+ return status;
+}
+
+static void iree_hal_cuda2_nccl_channel_destroy(
+ iree_hal_channel_t* base_channel) {
+ iree_hal_cuda2_nccl_channel_t* channel =
+ iree_hal_cuda2_nccl_channel_cast(base_channel);
+ IREE_TRACE_ZONE_BEGIN(z0);
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, channel->id_hash);
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, channel->rank);
+ IREE_TRACE_ZONE_APPEND_VALUE(z0, channel->count);
+
+ iree_allocator_t host_allocator = channel->host_allocator;
+
+ // TODO(#9580): support async tear down
+ // We could be smarter about starting finalization of all channels async and
+ // then waiting for them to complete but we aren't currently optimizing for
+ // lifetime performance. To do that we'd probably want to track each open
+ // channel on the device that created them and manage teardown there.
+ //
+ // Recommended:
+ // ncclCommFinalize(channel->comm); // non-blocking!
+ // while (ncclCommGetAsyncError == ncclInProgress) sleep(1);
+ // ncclCommDestroy(channel->comm)
+ // Should work the same (as we are doing a blocking teardown):
+ // ncclCommDestroy(channel->comm)
+ IREE_NCCL_IGNORE_ERROR(channel->nccl_symbols,
+ ncclCommFinalize(channel->comm));
+
+ IREE_NCCL_IGNORE_ERROR(channel->nccl_symbols, ncclCommDestroy(channel->comm));
+
+ iree_hal_channel_release(channel->parent_channel);
+ iree_allocator_free(host_allocator, channel);
+
+ IREE_TRACE_ZONE_END(z0);
+}
+
+static iree_status_t iree_hal_cuda2_nccl_channel_split(
+ iree_hal_channel_t* base_channel, int32_t color, int32_t key,
+ iree_hal_channel_flags_t flags, iree_hal_channel_t** out_split_channel) {
+ iree_hal_cuda2_nccl_channel_t* channel =
+ iree_hal_cuda2_nccl_channel_cast(base_channel);
+
+ // TODO: see if we need to set the sharing config - we may always want to.
+ ncclConfig_t config = NCCL_CONFIG_INITIALIZER;
+ // TODO: use async to check a timeout.
+ config.blocking = 1;
+
+ // Split the communicator.
+ ncclComm_t split_comm = NULL;
+ IREE_NCCL_RETURN_IF_ERROR(
+ channel->nccl_symbols,
+ ncclCommSplit(channel->comm, color, key, &split_comm, &config),
+ "ncclCommSplit");
+
+ // Query the local rank/count from the split communicator.
+ int split_rank = 0;
+ int split_count = 0;
+ iree_status_t status = IREE_NCCL_RESULT_TO_STATUS(
+ channel->nccl_symbols, ncclCommUserRank(split_comm, &split_rank),
+ "ncclCommUserRank");
+ if (iree_status_is_ok(status)) {
+ status = IREE_NCCL_RESULT_TO_STATUS(channel->nccl_symbols,
+ ncclCommCount(split_comm, &split_count),
+ "ncclCommCount");
+ }
+
+ // Wrap the split communicator in a new channel.
+ iree_hal_cuda2_nccl_channel_t* split_channel = NULL;
+ if (iree_status_is_ok(status)) {
+ status =
+ iree_allocator_malloc(channel->host_allocator, sizeof(*split_channel),
+ (void**)&split_channel);
+ }
+ if (iree_status_is_ok(status)) {
+ iree_hal_resource_initialize(&iree_hal_cuda2_nccl_channel_vtable,
+ &split_channel->resource);
+ split_channel->cuda_symbols = channel->cuda_symbols;
+ split_channel->nccl_symbols = channel->nccl_symbols;
+ split_channel->host_allocator = channel->host_allocator;
+ split_channel->parent_channel = base_channel;
+ iree_hal_channel_retain(base_channel);
+ split_channel->rank = split_rank;
+ split_channel->count = split_count;
+ split_channel->comm = split_comm;
+ *out_split_channel = (iree_hal_channel_t*)split_channel;
+ }
+
+ if (!iree_status_is_ok(status)) {
+ IREE_NCCL_IGNORE_ERROR(channel->nccl_symbols, ncclCommDestroy(split_comm));
+ }
+ return status;
+}
+
+static void iree_hal_cuda2_nccl_channel_query_rank_and_count(
+ const iree_hal_channel_t* base_channel, int32_t* out_rank,
+ int32_t* out_count) {
+ IREE_ASSERT_ARGUMENT(base_channel);
+ IREE_ASSERT_ARGUMENT(out_count);
+ const iree_hal_cuda2_nccl_channel_t* channel =
+ iree_hal_cuda2_nccl_channel_const_cast(base_channel);
+ // NOTE: since it's cheap we keep rank/count local - this lets us trace them
+ // out without needing to call into NCCL each time.
+ *out_rank = channel->rank;
+ *out_count = channel->count;
+}
+
+// Returns the NCCL communicator for the given |channel|, if available.
+static ncclComm_t iree_hal_cuda2_nccl_channel_comm(
+ iree_hal_channel_t* base_channel) {
+ IREE_ASSERT_ARGUMENT(base_channel);
+ iree_hal_cuda2_nccl_channel_t* channel =
+ iree_hal_cuda2_nccl_channel_cast(base_channel);
+ return channel->comm;
+}
+
+static iree_status_t iree_hal_cuda2_get_nccl_data_type(
+ iree_hal_collective_element_type_t in, ncclDataType_t* out) {
+ switch (in) {
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_SINT_8:
+ *out = ncclInt8;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_UINT_8:
+ *out = ncclUint8;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_SINT_16:
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "SINT16 is not supported for collective op");
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_UINT_16:
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "UINT16 is not supported for collective op");
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_SINT_32:
+ *out = ncclInt32;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_UINT_32:
+ *out = ncclUint32;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_SINT_64:
+ *out = ncclInt64;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_UINT_64:
+ *out = ncclUint64;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_FLOAT_16:
+ *out = ncclFloat16;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_FLOAT_32:
+ *out = ncclFloat32;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_FLOAT_64:
+ *out = ncclFloat64;
+ break;
+ case IREE_HAL_COLLECTIVE_ELEMENT_TYPE_BFLOAT_16:
+ *out = ncclFloat64;
+ break;
+ default:
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "unhandled element type for collective op");
+ }
+ return iree_ok_status();
+}
+
+static iree_status_t iree_hal_cuda2_get_nccl_reduction_type(
+ iree_hal_collective_reduction_t in, ncclRedOp_t* out) {
+ switch (in) {
+ case IREE_HAL_COLLECTIVE_REDUCTION_SUM:
+ *out = ncclSum;
+ break;
+ case IREE_HAL_COLLECTIVE_REDUCTION_PRODUCT:
+ *out = ncclProd;
+ break;
+ case IREE_HAL_COLLECTIVE_REDUCTION_MINIMUM:
+ *out = ncclMin;
+ break;
+ case IREE_HAL_COLLECTIVE_REDUCTION_MAXIMUM:
+ *out = ncclMax;
+ break;
+ case IREE_HAL_COLLECTIVE_REDUCTION_AVERAGE:
+ *out = ncclAvg;
+ break;
+ default:
+ return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+ "unhandled reduction type for collective op");
+ }
+
+ return iree_ok_status();
+}
+
+static iree_status_t iree_hal_cuda2_nccl_submit_batch_entry(
+ const iree_hal_collective_batch_entry_t* entry, CUstream stream) {
+ IREE_ASSERT_ARGUMENT(entry);
+ IREE_ASSERT_ARGUMENT(stream);
+
+ iree_hal_cuda2_nccl_channel_t* channel =
+ iree_hal_cuda2_nccl_channel_cast(entry->channel);
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* symbols = channel->nccl_symbols;
+ ncclComm_t comm = iree_hal_cuda2_nccl_channel_comm(entry->channel);
+
+ ncclDataType_t datatype;
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_get_nccl_data_type(entry->op.element_type, &datatype));
+
+ switch (entry->op.kind) {
+ case IREE_HAL_COLLECTIVE_KIND_ALL_GATHER: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclAllGather((const void*)sendbuff, (void*)recvbuff,
+ entry->element_count, datatype, comm, stream),
+ "ncclAllGather");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_ALL_REDUCE: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ ncclRedOp_t redop;
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_get_nccl_reduction_type(entry->op.reduction, &redop));
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclAllReduce((const void*)sendbuff, (void*)recvbuff,
+ entry->element_count, datatype, redop, comm, stream),
+ "ncclAllReduce");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_ALL_TO_ALL: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ iree_device_size_t send_count = entry->element_count / channel->count;
+ iree_device_size_t element_size_bytes =
+ iree_hal_collective_element_byte_count(entry->op.element_type);
+ iree_device_size_t rank_offset = send_count * element_size_bytes;
+ // These calls are already grouped by iree_hal_cuda2_nccl_submit_batch.
+ for (iree_host_size_t r = 0; r < channel->count; ++r) {
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclSend((const void*)(sendbuff + r * rank_offset), send_count,
+ datatype, r, comm, stream),
+ "ncclSend");
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclRecv((void*)(recvbuff + r * rank_offset), send_count, datatype,
+ r, comm, stream),
+ "ncclRecv");
+ }
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_BROADCAST: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclBroadcast((const void*)sendbuff, (void*)recvbuff,
+ entry->element_count, datatype, entry->param, comm,
+ stream),
+ "ncclBroadcast");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_REDUCE: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ ncclRedOp_t redop;
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_get_nccl_reduction_type(entry->op.reduction, &redop));
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclReduce((const void*)sendbuff, (void*)recvbuff,
+ entry->element_count, datatype, redop, entry->param, comm,
+ stream),
+ "ncclReduce");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_REDUCE_SCATTER: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ ncclRedOp_t redop;
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_get_nccl_reduction_type(entry->op.reduction, &redop));
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclReduceScatter((const void*)sendbuff, (void*)recvbuff,
+ entry->element_count, datatype, redop, comm,
+ stream),
+ "ncclReduceScatter");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_SEND: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclSend((const void*)sendbuff, entry->element_count, datatype,
+ entry->param, comm, stream),
+ "ncclSend");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_RECV: {
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ IREE_NCCL_RETURN_IF_ERROR(symbols,
+ ncclRecv((void*)recvbuff, entry->element_count,
+ datatype, entry->param, comm, stream),
+ "ncclRecv");
+ break;
+ }
+ case IREE_HAL_COLLECTIVE_KIND_SEND_RECV: {
+ CUdeviceptr sendbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->send_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->send_binding.buffer) +
+ entry->send_binding.offset;
+ CUdeviceptr recvbuff =
+ iree_hal_cuda2_buffer_device_pointer(
+ iree_hal_buffer_allocated_buffer(entry->recv_binding.buffer)) +
+ iree_hal_buffer_byte_offset(entry->recv_binding.buffer) +
+ entry->recv_binding.offset;
+ int16_t sendid;
+ int16_t recvid;
+ memcpy(&sendid, &entry->param, 2);
+ memcpy(&recvid, (char*)&entry->param + 2, 2);
+ if (sendid != -1) {
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclSend((const void*)sendbuff, entry->element_count, datatype,
+ sendid, comm, stream),
+ "ncclSend");
+ }
+ if (recvid != -1) {
+ IREE_NCCL_RETURN_IF_ERROR(
+ symbols,
+ ncclRecv((void*)recvbuff, entry->element_count, datatype, recvid,
+ comm, stream),
+ "ncclRecv");
+ } else {
+ // Zero out recvbuff if this rank is not receiving any data.
+ iree_device_size_t num_bytes =
+ entry->element_count *
+ iree_hal_collective_element_byte_count(entry->op.element_type);
+ IREE_CUDA_RETURN_IF_ERROR(
+ channel->cuda_symbols,
+ cuMemsetD8Async(recvbuff, 0, num_bytes, stream), "cuMemsetD8Async");
+ }
+ break;
+ }
+ } // switch
+ return iree_ok_status();
+}
+
+iree_status_t iree_hal_cuda2_nccl_submit_batch(
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* symbols,
+ const iree_hal_collective_batch_t* batch, CUstream stream) {
+ IREE_ASSERT_ARGUMENT(symbols);
+ IREE_ASSERT_ARGUMENT(batch);
+ IREE_ASSERT_ARGUMENT(stream);
+
+ // Issue all collective operations in the batch as part of a group.
+ // NCCL may be able to fuse or reduce overheads by issuing like this.
+ IREE_NCCL_RETURN_IF_ERROR(symbols, ncclGroupStart(), "ncclGroupStart");
+ for (iree_host_size_t i = 0; i < batch->count; ++i) {
+ IREE_RETURN_IF_ERROR(
+ iree_hal_cuda2_nccl_submit_batch_entry(&batch->entries[i], stream));
+ }
+ IREE_NCCL_RETURN_IF_ERROR(symbols, ncclGroupEnd(), "ncclGroupEnd");
+
+ return iree_ok_status();
+}
+
+static const iree_hal_channel_vtable_t iree_hal_cuda2_nccl_channel_vtable = {
+ .destroy = iree_hal_cuda2_nccl_channel_destroy,
+ .split = iree_hal_cuda2_nccl_channel_split,
+ .query_rank_and_count = iree_hal_cuda2_nccl_channel_query_rank_and_count,
+};
diff --git a/experimental/cuda2/nccl_channel.h b/experimental/cuda2/nccl_channel.h
new file mode 100644
index 0000000..a6da61f
--- /dev/null
+++ b/experimental/cuda2/nccl_channel.h
@@ -0,0 +1,57 @@
+// Copyright 2023 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
+
+#ifndef EXPERIMENTAL_CUDA2_NCCL_CHANNEL_H_
+#define EXPERIMENTAL_CUDA2_NCCL_CHANNEL_H_
+
+#include "experimental/cuda2/api.h"
+#include "experimental/cuda2/cuda_dynamic_symbols.h"
+#include "experimental/cuda2/cuda_headers.h"
+#include "experimental/cuda2/nccl_dynamic_symbols.h"
+#include "iree/base/api.h"
+#include "iree/hal/api.h"
+#include "iree/hal/utils/collective_batch.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+// Returns true if |id| is all zeros indicating an empty ID.
+static inline bool iree_hal_cuda2_nccl_id_is_empty(
+ const iree_hal_cuda2_nccl_id_t* id) {
+ for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(id->data); ++i) {
+ if (id->data[i] != 0) return false;
+ }
+ return true;
+}
+
+// Gets a unique ID to bootstrap a new communicator. It calls ncclGetUniqueId
+// under the hood.
+iree_status_t iree_hal_cuda2_nccl_get_unique_id(
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* symbols,
+ iree_hal_cuda2_nccl_id_t* out_id);
+
+// Creates a IREE HAL channel using the given NCCL |id|, |rank|, and |count|.
+// It calls ncclCommInitRankConfig under the hood.
+iree_status_t iree_hal_cuda2_nccl_channel_create(
+ const iree_hal_cuda2_dynamic_symbols_t* cuda_symbols,
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols,
+ const iree_hal_cuda2_nccl_id_t* id, int rank, int count,
+ iree_allocator_t host_allocator, iree_hal_channel_t** out_channel);
+
+// Performs a non-blocking submission of |batch| to |stream|.
+// The backing storage of |batch| is dropped immediately but all resources
+// referenced will be retained by the parent command buffer for its lifetime.
+// Note that operations in the batch may apply to different channels.
+iree_status_t iree_hal_cuda2_nccl_submit_batch(
+ const iree_hal_cuda2_nccl_dynamic_symbols_t* nccl_symbols,
+ const iree_hal_collective_batch_t* batch, CUstream stream);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
+
+#endif // EXPERIMENTAL_CUDA2_NCCL_CHANNEL_H_
diff --git a/experimental/cuda2/nccl_status_util.c b/experimental/cuda2/nccl_status_util.c
index c16e9f9..81ca097 100644
--- a/experimental/cuda2/nccl_status_util.c
+++ b/experimental/cuda2/nccl_status_util.c
@@ -11,7 +11,7 @@
#include "experimental/cuda2/nccl_dynamic_symbols.h"
#include "iree/base/status.h"
-iree_status_t iree_hal_nccl2_result_to_status(
+iree_status_t iree_hal_cuda2_nccl_result_to_status(
const iree_hal_cuda2_nccl_dynamic_symbols_t* syms, ncclResult_t result,
const char* file, uint32_t line) {
iree_status_code_t code;
diff --git a/experimental/cuda2/nccl_status_util.h b/experimental/cuda2/nccl_status_util.h
index 2826b1d..65db9a9 100644
--- a/experimental/cuda2/nccl_status_util.h
+++ b/experimental/cuda2/nccl_status_util.h
@@ -21,30 +21,44 @@
// Usage:
// iree_status_t status = IREE_NCCL_RESULT_TO_STATUS(nccl_symbols,
// ncclDoThing(...));
-#define IREE_NCCL_RESULT_TO_STATUS(syms, expr, ...) \
- iree_hal_nccl2_result_to_status((syms), ((syms)->expr), __FILE__, __LINE__)
+#define IREE_NCCL_RESULT_TO_STATUS(syms, expr, ...) \
+ iree_hal_cuda2_nccl_result_to_status((syms), ((syms)->expr), __FILE__, \
+ __LINE__)
// IREE_RETURN_IF_ERROR but implicitly converts the ncclResult_t return value to
// an iree_status_t.
//
// Usage:
// IREE_NCCL_RETURN_IF_ERROR(nccl_symbols, ncclDoThing(...), "message");
-#define IREE_NCCL_RETURN_IF_ERROR(syms, expr, ...) \
- IREE_RETURN_IF_ERROR(iree_hal_nccl2_result_to_status((syms), ((syms)->expr), \
- __FILE__, __LINE__), \
+#define IREE_NCCL_RETURN_IF_ERROR(syms, expr, ...) \
+ IREE_RETURN_IF_ERROR(iree_hal_cuda2_nccl_result_to_status( \
+ (syms), ((syms)->expr), __FILE__, __LINE__), \
__VA_ARGS__)
+// IREE_RETURN_IF_ERROR but ends the current zone and implicitly converts the
+// ncclResult_t return value to an iree_status_t.
+//
+// Usage:
+// IREE_NCCL_RETURN_AND_END_ZONE_IF_ERROR(zone_id, cuda_symbols,
+// ncclDoThing(...), "message");
+#define IREE_NCCL_RETURN_AND_END_ZONE_IF_ERROR(zone_id, syms, expr, ...) \
+ IREE_RETURN_AND_END_ZONE_IF_ERROR( \
+ zone_id, \
+ iree_hal_cuda2_nccl_result_to_status((syms), ((syms)->expr), __FILE__, \
+ __LINE__), \
+ __VA_ARGS__)
+
// IREE_IGNORE_ERROR but implicitly converts the ncclResult_t return value to
// an iree_status_t.
//
// Usage:
// IREE_NCCL_IGNORE_ERROR(nccl_symbols, ncclDoThing(...));
-#define IREE_NCCL_IGNORE_ERROR(syms, expr) \
- IREE_IGNORE_ERROR(iree_hal_nccl2_result_to_status((syms), ((syms)->expr), \
- __FILE__, __LINE__))
+#define IREE_NCCL_IGNORE_ERROR(syms, expr) \
+ IREE_IGNORE_ERROR(iree_hal_cuda2_nccl_result_to_status( \
+ (syms), ((syms)->expr), __FILE__, __LINE__))
// Converts a ncclResult_t to an iree_status_t object.
-iree_status_t iree_hal_nccl2_result_to_status(
+iree_status_t iree_hal_cuda2_nccl_result_to_status(
const iree_hal_cuda2_nccl_dynamic_symbols_t* syms, ncclResult_t result,
const char* file, uint32_t line);