CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
2511 lines
118 KiB
C++
2511 lines
118 KiB
C++
/*
|
|
* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
* See LICENSE.txt for more license information.
|
|
*/
|
|
|
|
#include <unistd.h>
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <new>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <nccl.h>
|
|
#include <nccl_device.h>
|
|
#include "include/common.hpp"
|
|
#include "include/nccl_ep.h"
|
|
|
|
// HT (High Throughput) includes
|
|
#include "device/hybridep_adapter.cuh"
|
|
#include "device/hybridep_configs.cuh"
|
|
|
|
// Internal definition of the opaque ncclNDTensor type
|
|
struct ncclNDTensor {
|
|
unsigned int version;
|
|
unsigned int ndim;
|
|
unsigned int* sizes;
|
|
unsigned int* strides;
|
|
ncclDataType_t datatype;
|
|
void* data;
|
|
unsigned int tag;
|
|
ncclEpTensorFlags_t flags;
|
|
bool owns_data;
|
|
};
|
|
|
|
// Forward declarations for HT functions
|
|
static ncclResult_t init_hybridep_intranode(ncclEpGroup_t ep_group, const ncclEpGroupConfig_t* config, cudaStream_t stream);
|
|
static ncclResult_t destroy_hybridep_intranode(ncclEpGroup_t ep_group);
|
|
static ncclResult_t init_hybridep_internode(ncclEpGroup_t ep_group, const ncclEpGroupConfig_t* config, cudaStream_t stream);
|
|
static ncclResult_t destroy_hybridep_internode(ncclEpGroup_t ep_group);
|
|
static void tensor_free(ncclEpGroup_t group, ncclNDTensor_t t);
|
|
|
|
// Define NCCL_CHECK_RESULT macro for NCCL error checking
|
|
#ifndef NCCL_CHECK_RESULT
|
|
#define NCCL_CHECK_RESULT(cmd) do { \
|
|
ncclResult_t res = cmd; \
|
|
if (res != ncclSuccess) { \
|
|
fprintf(stderr, "NCCL error %d at %s:%d\n", \
|
|
res, __FILE__, __LINE__); \
|
|
exit(EXIT_FAILURE); \
|
|
} \
|
|
} while(0)
|
|
#endif
|
|
|
|
// Helper function to convert ncclDataType_t to cudaDataType_t
|
|
static cudaDataType_t ncclDataTypeToCudaDataType(ncclDataType_t nccl_type) {
|
|
switch (nccl_type) {
|
|
case ncclFloat16: return CUDA_R_16F;
|
|
case ncclFloat32: return CUDA_R_32F;
|
|
case ncclFloat64: return CUDA_R_64F;
|
|
case ncclBfloat16: return CUDA_R_16BF;
|
|
case ncclInt8: return CUDA_R_8I;
|
|
case ncclInt32: return CUDA_R_32I;
|
|
case ncclInt64: return CUDA_R_64I;
|
|
case ncclUint8: return CUDA_R_8U;
|
|
case ncclUint32: return CUDA_R_32U;
|
|
case ncclUint64: return CUDA_R_64U;
|
|
default:
|
|
assert(false && "Unsupported ncclDataType_t for conversion to cudaDataType_t");
|
|
return CUDA_R_16BF; // Default fallback
|
|
}
|
|
}
|
|
|
|
static size_t ncclTypeSize(ncclDataType_t nccl_type) {
|
|
switch (nccl_type) {
|
|
case ncclInt8:
|
|
case ncclUint8:
|
|
case ncclFloat8e4m3:
|
|
case ncclFloat8e5m2:
|
|
return 1;
|
|
case ncclFloat16:
|
|
case ncclBfloat16:
|
|
return 2;
|
|
case ncclInt32:
|
|
case ncclUint32:
|
|
case ncclFloat32:
|
|
return 4;
|
|
case ncclInt64:
|
|
case ncclUint64:
|
|
case ncclFloat64:
|
|
return 8;
|
|
default:
|
|
assert(false && "Unsupported ncclDataType_t for size query");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Allgather on host memory using NCCL (used once for hostname exchange).
|
|
// This operates on a single in-place host buffer, unlike batchAllGatherIpcHandles
|
|
// which batches multiple IPC handles via a packed device buffer.
|
|
// Each rank contributes element_size bytes at offset rank * element_size.
|
|
static void ncclAllGatherHost(
|
|
void* host_buffer,
|
|
size_t element_size,
|
|
int rank,
|
|
int nRanks,
|
|
ncclComm_t comm,
|
|
cudaStream_t stream
|
|
) {
|
|
const size_t total_size = element_size * nRanks;
|
|
void* d_buffer;
|
|
CUDA_CHECK(cudaMalloc(&d_buffer, total_size));
|
|
CUDA_CHECK(cudaMemcpy(
|
|
static_cast<uint8_t*>(d_buffer) + rank * element_size,
|
|
static_cast<uint8_t*>(host_buffer) + rank * element_size,
|
|
element_size, cudaMemcpyHostToDevice));
|
|
NCCL_CHECK_RESULT(ncclAllGather(
|
|
static_cast<uint8_t*>(d_buffer) + rank * element_size,
|
|
d_buffer, element_size, ncclUint8, comm, stream));
|
|
CUDA_CHECK(cudaStreamSynchronize(stream));
|
|
CUDA_CHECK(cudaMemcpy(host_buffer, d_buffer, total_size, cudaMemcpyDeviceToHost));
|
|
CUDA_CHECK(cudaFree(d_buffer));
|
|
}
|
|
|
|
// NCCL barrier using AllReduce.
|
|
// If workspace is provided, it is used directly (must be at least sizeof(int) device bytes).
|
|
// Otherwise a temporary cudaMalloc/cudaFree pair is used as fallback.
|
|
static ncclResult_t ncclBarrier(ncclComm_t comm, cudaStream_t stream, void* workspace = nullptr) {
|
|
int *nccl_barrier_var = nullptr;
|
|
bool owns_memory = false;
|
|
if (workspace) {
|
|
nccl_barrier_var = static_cast<int*>(workspace);
|
|
} else {
|
|
CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&nccl_barrier_var), sizeof(int)));
|
|
owns_memory = true;
|
|
}
|
|
CUDA_CHECK(cudaMemset(nccl_barrier_var, 0, sizeof(int)));
|
|
NCCL_CHECK_RESULT(ncclAllReduce(nccl_barrier_var, nccl_barrier_var, 1, ncclInt, ncclSum, comm, stream));
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
if (owns_memory) {
|
|
CUDA_CHECK(cudaFree(nccl_barrier_var));
|
|
}
|
|
return ncclSuccess;
|
|
}
|
|
|
|
// Opaque struct definitions
|
|
struct ncclEpGroup {
|
|
ncclComm_t comm;
|
|
|
|
int nRanks; // Total number of ranks (from ncclCommCount)
|
|
int rank; // This rank's ID
|
|
int nNodes; // Number of nodes
|
|
|
|
void* ep_workspace; // Device workspace for EP operations
|
|
int cuda_device_id; // CUDA device ID
|
|
int lsa_team_size; // LSA team size: ncclTeamLsa(comm).nRanks
|
|
int rdma_ranks; // RDMA ranks (nRanks / lsa_team_size)
|
|
int rdma_rank; // RDMA rank (rank / lsa_team_size)
|
|
void* rdma_buffer;
|
|
ncclEpGroupConfig_t config; // Stored configuration
|
|
|
|
struct {
|
|
// Split communicator: groups ranks with same local_rank across nodes (nNodes ranks each)
|
|
ncclComm_t split_comm = nullptr; // Sub-communicator for GIN (nNodes ranks)
|
|
// Device communicator (single comm, multiple contexts)
|
|
ncclDevComm_t* dcomms = nullptr; // Host array of device communicators
|
|
ncclDevComm_t* d_dcomms = nullptr; // Device array of device communicators
|
|
int num_comms = 0; // Number of communicators (always 1)
|
|
int num_dcomms = 0; // Number of device comms
|
|
int qps_per_rank = 0; // Total QPs (connections) per rank
|
|
int num_ctx_per_comm = 0; // Number of contexts per communicator
|
|
|
|
// GIN memory base pointer and window
|
|
void* gin_base_ptr = nullptr; // Base pointer for all GIN memory
|
|
ncclWindow_t nccl_window = {}; // Single registered window handle (pointer-sized)
|
|
unsigned signals_base = 0; // Base signal ID for dispatch
|
|
unsigned combine_signal_offset = 0; // Signal offset for combine operations
|
|
int num_total_signals = 0; // Total number of signals
|
|
|
|
// Used by kernels to calculate actual addresses for RDMA puts
|
|
size_t rdma_intra_node_red_token_offset = 0;
|
|
size_t combine_rdma_inter_node_group_token_offset = 0;
|
|
size_t rdma_intra_node_red_prob_offset = 0;
|
|
size_t combine_rdma_inter_node_group_prob_offset = 0;
|
|
size_t token_staging_offset = 0;
|
|
size_t dense_prob_offset = 0;
|
|
size_t scaling_factor_staging_offset = 0;
|
|
|
|
// Layout: [NUM_LSA_TEAMS-1][BATCH_SIZE * bytes_per_entry]
|
|
// bytes_per_entry = hidden * sizeof(TOKEN_DATA_TYPE) + prob_size + sf_size
|
|
size_t rdma_send_staging_offset = 0;
|
|
size_t rdma_inter_node_group_packed_offset = 0; // Packed receive buffer (token+prob+sf)
|
|
|
|
unsigned signals_tail_base = 0; // Base signal ID for tail tracking (sender -> receiver)
|
|
int num_max_rdma_chunked_send_tokens = HYBRIDEP_DISPATCH_RDMA_BATCH_SIZE;
|
|
|
|
} gin_config;
|
|
|
|
int num_local_experts; // Number of local experts (num_experts / comm->nRanks)
|
|
int max_recv_tokens; // Worst-case receive capacity: nRanks * max_tokens_per_rank
|
|
int hidden; // Hidden size (token_size_bytes / ncclTypeSize(ncclBfloat16))
|
|
unsigned int device_sm_count; // Number of SMs on the device
|
|
unsigned int num_sms_ht; // Number of SMs to use for HT kernels
|
|
|
|
// Custom allocator function pointers
|
|
ncclEpAllocFn_t alloc_fn;
|
|
ncclEpFreeFn_t free_fn;
|
|
|
|
// Physical node properties (CUDA device assignment, IPC between co-located GPUs)
|
|
int gpus_per_node; // Physical GPUs per node (nRanks / nNodes)
|
|
int rank_in_node; // Per-node CUDA device ordinal (= cuda_device_id)
|
|
int node_id; // Physical node index (rank / gpus_per_node)
|
|
|
|
// Kernel params (node-based for HT; lsa_rank_count == gpus_per_node)
|
|
int lsa_rank_count; // Ranks per node passed to HT kernels as num_ranks_per_node
|
|
|
|
// NCCL device API
|
|
size_t num_nccl_comms;
|
|
std::vector<ncclComm_t> nccl_comms;
|
|
ncclDevComm_t* nccl_dev_comms;
|
|
ncclWindow_t* nccl_wins;
|
|
int num_dispatch_signals;
|
|
|
|
// HT buffers for intranode communication
|
|
struct {
|
|
// IPC-mapped buffer pointer arrays (fixed-size, indexed by local NVL rank)
|
|
// Host arrays for population and cleanup
|
|
void **dispatch_expert_output_token_buffer_ptrs;
|
|
float **dispatch_expert_output_prob_buffer_ptrs;
|
|
float **dispatch_expert_output_scaling_factor_buffer_ptrs;
|
|
uint16_t **combine_expert_input_token_buffer_ptrs;
|
|
float **combine_expert_input_prob_buffer_ptrs;
|
|
|
|
// Local buffers (owned by this rank)
|
|
void *expert_output_token;
|
|
float *expert_output_prob;
|
|
float *expert_output_scaling_factor;
|
|
uint16_t *expert_input_token;
|
|
float *expert_input_prob;
|
|
|
|
// Sync flags (rank 0 allocates, others IPC-map)
|
|
uint32_t *intra_node_write_completion_flags;
|
|
uint32_t *combine_intra_node_write_completion_flags;
|
|
// Grid barrier counter for fused device_sync in dispatch tail (per-rank, not IPC-shared)
|
|
uint32_t *dispatch_grid_barrier_counter;
|
|
// Host-side expected flag counters (replaces device-side update_expected_value_kernel)
|
|
uint64_t host_dispatch_expected_rdma = 0;
|
|
uint32_t host_dispatch_expected_intra = 0;
|
|
uint64_t host_combine_expected_rdma = 0;
|
|
uint32_t host_combine_expected_intra = 0;
|
|
|
|
// RDMA buffers (multi-node only)
|
|
uint64_t *rdma_inter_node_group_flags;
|
|
uint16_t *rdma_intra_node_red_token;
|
|
float *rdma_intra_node_red_prob;
|
|
uint16_t *combine_rdma_inter_node_group_token;
|
|
float *combine_rdma_inter_node_group_prob;
|
|
uint64_t *combine_rdma_inter_node_group_flags;
|
|
|
|
// Pre-registered dispatch buffers (group-level, allocated during Group Create)
|
|
// These are pre-registered with GIN to avoid ~60ms registration overhead during dispatch
|
|
void *token_staging_buffer; // Pre-registered staging buffer for user tokens
|
|
float *dense_prob_buffer; // Pre-registered buffer for sparse→dense prob conversion
|
|
float *scaling_factor_staging_buffer; // Pre-registered staging buffer for FP8 scaling factors
|
|
|
|
// Merged IPC buffer (single cudaMalloc for all IPC-shared buffers)
|
|
void* ipc_mega_buffer = nullptr;
|
|
size_t ipc_mega_buffer_size = 0;
|
|
size_t ipc_dispatch_token_offset = 0;
|
|
size_t ipc_dispatch_prob_offset = 0;
|
|
size_t ipc_combine_token_offset = 0;
|
|
size_t ipc_combine_prob_offset = 0;
|
|
|
|
void** peer_ipc_base_ptrs = nullptr; // Opened IPC base pointers per peer (for cleanup)
|
|
void* host_ptr_block = nullptr; // Single cudaHostAlloc for all pointer arrays
|
|
|
|
// Config
|
|
bool initialized;
|
|
bool internode_initialized;
|
|
} ht_buffers;
|
|
|
|
// Constructor to properly initialize all members
|
|
ncclEpGroup() :
|
|
comm(nullptr),
|
|
nRanks(0),
|
|
rank(0),
|
|
nNodes(0),
|
|
ep_workspace(nullptr),
|
|
cuda_device_id(0),
|
|
rdma_ranks(0),
|
|
rdma_rank(0),
|
|
rdma_buffer(nullptr),
|
|
config{},
|
|
num_local_experts(0),
|
|
max_recv_tokens(0),
|
|
hidden(0),
|
|
device_sm_count(0),
|
|
num_sms_ht(0),
|
|
alloc_fn(nullptr),
|
|
free_fn(nullptr),
|
|
gpus_per_node(0),
|
|
rank_in_node(0),
|
|
node_id(0),
|
|
lsa_rank_count(0),
|
|
num_nccl_comms(0),
|
|
nccl_comms{},
|
|
nccl_dev_comms(nullptr),
|
|
nccl_wins(nullptr),
|
|
num_dispatch_signals(0),
|
|
ht_buffers{} {}
|
|
};
|
|
|
|
// Batch allgather of multiple IPC handles in a single NCCL collective.
|
|
// Reduces N separate cudaMalloc/ncclAllGather/cudaFree cycles to just 1.
|
|
// local_handles: array of num_handles local IPC handle pointers (each CUDA_IPC_HANDLE_SIZE bytes)
|
|
// all_handles: array of num_handles output buffers (each CUDA_IPC_HANDLE_SIZE * nRanks bytes)
|
|
static void batchAllGatherIpcHandles(
|
|
const void* const* local_handles,
|
|
void** all_handles,
|
|
int num_handles,
|
|
int rank,
|
|
int nRanks,
|
|
ncclComm_t comm,
|
|
cudaStream_t stream
|
|
) {
|
|
const size_t per_rank_size = static_cast<size_t>(num_handles) * CUDA_IPC_HANDLE_SIZE;
|
|
const size_t total_size = per_rank_size * nRanks;
|
|
void* d_buffer;
|
|
CUDA_CHECK(cudaMalloc(&d_buffer, total_size));
|
|
|
|
// Pack all local handles into a contiguous host buffer, then copy to device
|
|
std::unique_ptr<uint8_t[]> pack_buf(new uint8_t[per_rank_size]);
|
|
for (int i = 0; i < num_handles; i++) {
|
|
memcpy(pack_buf.get() + i * CUDA_IPC_HANDLE_SIZE,
|
|
local_handles[i], CUDA_IPC_HANDLE_SIZE);
|
|
}
|
|
CUDA_CHECK(cudaMemcpy(
|
|
static_cast<uint8_t*>(d_buffer) + rank * per_rank_size,
|
|
pack_buf.get(), per_rank_size, cudaMemcpyHostToDevice));
|
|
|
|
// Single NCCL AllGather for all handles
|
|
NCCL_CHECK_RESULT(ncclAllGather(
|
|
static_cast<uint8_t*>(d_buffer) + rank * per_rank_size,
|
|
d_buffer, per_rank_size, ncclUint8, comm, stream));
|
|
CUDA_CHECK(cudaStreamSynchronize(stream));
|
|
|
|
// Copy all results back to host, then unpack per-handle
|
|
std::unique_ptr<uint8_t[]> host_buf(new uint8_t[total_size]);
|
|
CUDA_CHECK(cudaMemcpy(host_buf.get(), d_buffer, total_size, cudaMemcpyDeviceToHost));
|
|
CUDA_CHECK(cudaFree(d_buffer));
|
|
|
|
// Deinterleave: for each handle i, extract rank r's copy from the packed layout
|
|
for (int i = 0; i < num_handles; i++) {
|
|
uint8_t* out = static_cast<uint8_t*>(all_handles[i]);
|
|
for (int r = 0; r < nRanks; r++) {
|
|
memcpy(out + r * CUDA_IPC_HANDLE_SIZE,
|
|
host_buf.get() + r * per_rank_size + i * CUDA_IPC_HANDLE_SIZE,
|
|
CUDA_IPC_HANDLE_SIZE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// HT Intranode Initialization (adapted for public NCCL APIs)
|
|
static ncclResult_t init_hybridep_intranode(ncclEpGroup_t ep_group,
|
|
const ncclEpGroupConfig_t* in_config,
|
|
cudaStream_t stream)
|
|
{
|
|
ncclComm_t comm = ep_group->comm;
|
|
int nRanks = ep_group->nRanks;
|
|
int rank = ep_group->rank;
|
|
// Node semantics: IPC, buffer sizing, and indexing use per-node device ordinals.
|
|
// Note: for MNNVL this only covers same-node peers; cross-node MNNVL HT is not supported.
|
|
int gpus_per_node = ep_group->gpus_per_node;
|
|
int rank_in_node = ep_group->rank_in_node;
|
|
int node_id = ep_group->node_id;
|
|
int hidden = ep_group->hidden;
|
|
int num_local_experts = ep_group->num_local_experts;
|
|
int max_recv_tokens = ep_group->max_recv_tokens;
|
|
|
|
ep_group->ht_buffers.initialized = false;
|
|
|
|
// Enable P2P access between GPUs on the same physical node.
|
|
// cudaDeviceCanAccessPeer/cudaDeviceEnablePeerAccess operate on per-node device ordinals
|
|
// (rank_in_node, gpus_per_node) — these are node concepts, not LSA concepts.
|
|
// Cross-node MNNVL P2P is handled by the NVLink fabric through IPC handles; it does not
|
|
// need cudaDeviceEnablePeerAccess and cannot be probed with cudaDeviceCanAccessPeer since
|
|
// remote node device ordinals are not valid on the local node.
|
|
// TODO: replace this loop with a NCCL API that queries P2P capability (e.g. ncclCommQueryProperties).
|
|
for (int i = 0; i < gpus_per_node; i++) {
|
|
if (i == rank_in_node) continue;
|
|
int can_p2p = 0;
|
|
CUDA_CHECK(cudaDeviceCanAccessPeer(&can_p2p, rank_in_node, i));
|
|
if (can_p2p) {
|
|
cudaError_t err = cudaDeviceEnablePeerAccess(i, 0);
|
|
if (err != cudaSuccess && err != cudaErrorPeerAccessAlreadyEnabled) {
|
|
fprintf(stderr, "HT: Failed to enable P2P from GPU %d to GPU %d\n", rank_in_node, i);
|
|
}
|
|
}
|
|
}
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
|
|
// =========================================================================
|
|
// Phase 1: Allocate all buffers upfront (before batch IPC exchange)
|
|
// =========================================================================
|
|
|
|
// Consolidated IPC mega-buffer: single cudaMalloc for all 4 IPC-shared buffers.
|
|
// Expert-prob buffers sized by gpus_per_node (node semantics).
|
|
auto align_ipc = [](size_t s) -> size_t { return (s + 255) & ~size_t(255); };
|
|
|
|
size_t expert_output_token_sz = static_cast<size_t>(max_recv_tokens) * hidden * sizeof(uint16_t);
|
|
size_t expert_output_prob_sz = static_cast<size_t>(max_recv_tokens) * num_local_experts * gpus_per_node * sizeof(float);
|
|
size_t expert_input_token_sz = static_cast<size_t>(max_recv_tokens) * hidden * sizeof(uint16_t);
|
|
size_t expert_input_prob_sz = static_cast<size_t>(max_recv_tokens) * num_local_experts * gpus_per_node * sizeof(float);
|
|
|
|
size_t dispatch_token_aligned = align_ipc(expert_output_token_sz);
|
|
size_t dispatch_prob_aligned = align_ipc(expert_output_prob_sz);
|
|
size_t combine_token_aligned = align_ipc(expert_input_token_sz);
|
|
size_t combine_prob_aligned = align_ipc(expert_input_prob_sz);
|
|
|
|
size_t mega_sz = dispatch_token_aligned + dispatch_prob_aligned
|
|
+ combine_token_aligned + combine_prob_aligned;
|
|
CUDA_CHECK(cudaMalloc(&ep_group->ht_buffers.ipc_mega_buffer, mega_sz));
|
|
ep_group->ht_buffers.ipc_mega_buffer_size = mega_sz;
|
|
|
|
uint8_t* mega_base = static_cast<uint8_t*>(ep_group->ht_buffers.ipc_mega_buffer);
|
|
ep_group->ht_buffers.ipc_dispatch_token_offset = 0;
|
|
ep_group->ht_buffers.expert_output_token = mega_base;
|
|
|
|
ep_group->ht_buffers.ipc_dispatch_prob_offset = dispatch_token_aligned;
|
|
ep_group->ht_buffers.expert_output_prob = reinterpret_cast<float*>(mega_base + dispatch_token_aligned);
|
|
|
|
ep_group->ht_buffers.ipc_combine_token_offset = dispatch_token_aligned + dispatch_prob_aligned;
|
|
ep_group->ht_buffers.expert_input_token = reinterpret_cast<uint16_t*>(
|
|
mega_base + dispatch_token_aligned + dispatch_prob_aligned);
|
|
|
|
ep_group->ht_buffers.ipc_combine_prob_offset = dispatch_token_aligned + dispatch_prob_aligned + combine_token_aligned;
|
|
ep_group->ht_buffers.expert_input_prob = reinterpret_cast<float*>(
|
|
mega_base + dispatch_token_aligned + dispatch_prob_aligned + combine_token_aligned);
|
|
|
|
// Host pointer arrays indexed by per-node position (one entry per node peer).
|
|
size_t host_block_sz = sizeof(void*) * gpus_per_node
|
|
+ sizeof(float*) * gpus_per_node
|
|
+ sizeof(uint16_t*) * gpus_per_node
|
|
+ sizeof(float*) * gpus_per_node;
|
|
CUDA_CHECK(cudaHostAlloc(&ep_group->ht_buffers.host_ptr_block, host_block_sz, cudaHostAllocMapped));
|
|
|
|
uint8_t* hptr = static_cast<uint8_t*>(ep_group->ht_buffers.host_ptr_block);
|
|
ep_group->ht_buffers.dispatch_expert_output_token_buffer_ptrs = reinterpret_cast<void**>(hptr);
|
|
hptr += sizeof(void*) * gpus_per_node;
|
|
ep_group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs = reinterpret_cast<float**>(hptr);
|
|
hptr += sizeof(float*) * gpus_per_node;
|
|
ep_group->ht_buffers.combine_expert_input_token_buffer_ptrs = reinterpret_cast<uint16_t**>(hptr);
|
|
hptr += sizeof(uint16_t*) * gpus_per_node;
|
|
ep_group->ht_buffers.combine_expert_input_prob_buffer_ptrs = reinterpret_cast<float**>(hptr);
|
|
|
|
// Merged completion flags: rank_in_node 0 is the primary within the node.
|
|
if (rank_in_node == 0) {
|
|
uint32_t* completion_flags_base;
|
|
CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&completion_flags_base), 2 * sizeof(uint32_t)));
|
|
CUDA_CHECK(cudaMemsetAsync(completion_flags_base, 0, 2 * sizeof(uint32_t), stream));
|
|
ep_group->ht_buffers.intra_node_write_completion_flags = completion_flags_base;
|
|
ep_group->ht_buffers.combine_intra_node_write_completion_flags = completion_flags_base + 1;
|
|
}
|
|
|
|
// Dispatch grid barrier counter (local to each rank, NOT IPC-shared)
|
|
{
|
|
uint32_t* grid_barrier_base;
|
|
CUDA_CHECK(ep_group->alloc_fn(reinterpret_cast<void**>(&grid_barrier_base), sizeof(uint32_t)));
|
|
CUDA_CHECK(cudaMemsetAsync(grid_barrier_base, 0, sizeof(uint32_t), stream));
|
|
ep_group->ht_buffers.dispatch_grid_barrier_counter = grid_barrier_base;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Phase 2: Get IPC handles and batch-allgather in a single NCCL collective
|
|
// Consolidated: [0] = mega buffer (token+prob+combine), [1] = completion flags
|
|
// =========================================================================
|
|
constexpr int NUM_IPC_HANDLES = 2;
|
|
cudaIpcMemHandle_t local_handles[NUM_IPC_HANDLES];
|
|
memset(local_handles, 0, sizeof(local_handles));
|
|
|
|
CUDA_CHECK(cudaIpcGetMemHandle(&local_handles[0], ep_group->ht_buffers.ipc_mega_buffer));
|
|
if (rank_in_node == 0) {
|
|
CUDA_CHECK(cudaIpcGetMemHandle(&local_handles[1], ep_group->ht_buffers.intra_node_write_completion_flags));
|
|
}
|
|
|
|
std::unique_ptr<uint8_t[]> all_handles_buf[NUM_IPC_HANDLES];
|
|
for (int i = 0; i < NUM_IPC_HANDLES; i++) {
|
|
all_handles_buf[i].reset(new uint8_t[CUDA_IPC_HANDLE_SIZE * nRanks]);
|
|
}
|
|
|
|
const void* local_handle_ptrs[NUM_IPC_HANDLES];
|
|
void* all_handle_ptrs[NUM_IPC_HANDLES];
|
|
for (int i = 0; i < NUM_IPC_HANDLES; i++) {
|
|
local_handle_ptrs[i] = &local_handles[i];
|
|
all_handle_ptrs[i] = all_handles_buf[i].get();
|
|
}
|
|
|
|
batchAllGatherIpcHandles(local_handle_ptrs, all_handle_ptrs, NUM_IPC_HANDLES,
|
|
rank, nRanks, comm, stream);
|
|
|
|
// =========================================================================
|
|
// Phase 3: Open IPC handles for same-node peers (node semantics).
|
|
// Indexed by rank_in_node; node_id identifies which physical node this rank belongs to.
|
|
// =========================================================================
|
|
|
|
// Track opened IPC base pointers for cleanup (one per node peer)
|
|
CUDA_CHECK(cudaHostAlloc(&ep_group->ht_buffers.peer_ipc_base_ptrs,
|
|
sizeof(void*) * gpus_per_node, cudaHostAllocMapped));
|
|
memset(ep_group->ht_buffers.peer_ipc_base_ptrs, 0, sizeof(void*) * gpus_per_node);
|
|
|
|
// Open 1 mega-buffer IPC handle per node peer, derive 4 pointers via offsets
|
|
for (int i = 0; i < gpus_per_node; i++) {
|
|
if (i == rank_in_node) {
|
|
ep_group->ht_buffers.dispatch_expert_output_token_buffer_ptrs[i] =
|
|
ep_group->ht_buffers.expert_output_token;
|
|
ep_group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs[i] =
|
|
ep_group->ht_buffers.expert_output_prob;
|
|
ep_group->ht_buffers.combine_expert_input_token_buffer_ptrs[i] =
|
|
ep_group->ht_buffers.expert_input_token;
|
|
ep_group->ht_buffers.combine_expert_input_prob_buffer_ptrs[i] =
|
|
ep_group->ht_buffers.expert_input_prob;
|
|
} else {
|
|
int peer_global = node_id * gpus_per_node + i;
|
|
cudaIpcMemHandle_t peer_handle;
|
|
memcpy(&peer_handle, all_handles_buf[0].get() + peer_global * CUDA_IPC_HANDLE_SIZE,
|
|
CUDA_IPC_HANDLE_SIZE);
|
|
void* peer_base;
|
|
CUDA_CHECK(cudaIpcOpenMemHandle(&peer_base, peer_handle, cudaIpcMemLazyEnablePeerAccess));
|
|
ep_group->ht_buffers.peer_ipc_base_ptrs[i] = peer_base;
|
|
|
|
uint8_t* pb = static_cast<uint8_t*>(peer_base);
|
|
ep_group->ht_buffers.dispatch_expert_output_token_buffer_ptrs[i] =
|
|
pb + ep_group->ht_buffers.ipc_dispatch_token_offset;
|
|
ep_group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs[i] =
|
|
reinterpret_cast<float*>(pb + ep_group->ht_buffers.ipc_dispatch_prob_offset);
|
|
ep_group->ht_buffers.combine_expert_input_token_buffer_ptrs[i] =
|
|
reinterpret_cast<uint16_t*>(pb + ep_group->ht_buffers.ipc_combine_token_offset);
|
|
ep_group->ht_buffers.combine_expert_input_prob_buffer_ptrs[i] =
|
|
reinterpret_cast<float*>(pb + ep_group->ht_buffers.ipc_combine_prob_offset);
|
|
}
|
|
}
|
|
|
|
// Merged completion flags: non-rank_in_node-0 ranks IPC-map from node-local rank 0.
|
|
if (rank_in_node != 0) {
|
|
int node_rank0_global = node_id * gpus_per_node;
|
|
cudaIpcMemHandle_t rank0_handle;
|
|
memcpy(&rank0_handle, all_handles_buf[1].get() + node_rank0_global * CUDA_IPC_HANDLE_SIZE, CUDA_IPC_HANDLE_SIZE);
|
|
void* ptr;
|
|
CUDA_CHECK(cudaIpcOpenMemHandle(&ptr, rank0_handle, cudaIpcMemLazyEnablePeerAccess));
|
|
ep_group->ht_buffers.intra_node_write_completion_flags = static_cast<uint32_t*>(ptr);
|
|
ep_group->ht_buffers.combine_intra_node_write_completion_flags = static_cast<uint32_t*>(ptr) + 1;
|
|
}
|
|
|
|
ep_group->ht_buffers.initialized = true;
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
// HT Intranode Cleanup
|
|
static ncclResult_t destroy_hybridep_intranode(ncclEpGroup_t ep_group) {
|
|
if (!ep_group->ht_buffers.initialized) return ncclSuccess;
|
|
|
|
// Node semantics: IPC cleanup mirrors Phase 3 — indexed by per-node position.
|
|
int rank_in_node = ep_group->rank_in_node;
|
|
int gpus_per_node = ep_group->gpus_per_node;
|
|
|
|
// Close IPC handles: 1 mega-buffer handle per node peer + 1 completion flag handle
|
|
for (int i = 0; i < gpus_per_node; i++) {
|
|
if (i != rank_in_node && ep_group->ht_buffers.peer_ipc_base_ptrs &&
|
|
ep_group->ht_buffers.peer_ipc_base_ptrs[i]) {
|
|
cudaIpcCloseMemHandle(ep_group->ht_buffers.peer_ipc_base_ptrs[i]);
|
|
}
|
|
}
|
|
|
|
// Close completion flag IPC handle (non-rank_in_node-0)
|
|
if (rank_in_node != 0) {
|
|
if (ep_group->ht_buffers.intra_node_write_completion_flags) {
|
|
cudaIpcCloseMemHandle(ep_group->ht_buffers.intra_node_write_completion_flags);
|
|
}
|
|
}
|
|
|
|
// Free consolidated IPC mega-buffer (replaces 4 individual cudaFree calls)
|
|
if (ep_group->ht_buffers.ipc_mega_buffer) {
|
|
cudaFree(ep_group->ht_buffers.ipc_mega_buffer);
|
|
ep_group->ht_buffers.ipc_mega_buffer = nullptr;
|
|
ep_group->ht_buffers.expert_output_token = nullptr;
|
|
ep_group->ht_buffers.expert_output_prob = nullptr;
|
|
ep_group->ht_buffers.expert_input_token = nullptr;
|
|
ep_group->ht_buffers.expert_input_prob = nullptr;
|
|
}
|
|
if (ep_group->ht_buffers.expert_output_scaling_factor) {
|
|
ep_group->free_fn(ep_group->ht_buffers.expert_output_scaling_factor);
|
|
}
|
|
// Free dispatch grid barrier counter
|
|
if (ep_group->ht_buffers.dispatch_grid_barrier_counter) {
|
|
ep_group->free_fn(ep_group->ht_buffers.dispatch_grid_barrier_counter);
|
|
}
|
|
|
|
// Free merged completion flags (rank_in_node 0 only; base pointer covers both dispatch and combine)
|
|
if (rank_in_node == 0) {
|
|
if (ep_group->ht_buffers.intra_node_write_completion_flags) {
|
|
cudaFree(ep_group->ht_buffers.intra_node_write_completion_flags);
|
|
}
|
|
}
|
|
|
|
// Free consolidated host pointer block and peer IPC tracking array
|
|
if (ep_group->ht_buffers.dispatch_expert_output_scaling_factor_buffer_ptrs) {
|
|
cudaFreeHost(ep_group->ht_buffers.dispatch_expert_output_scaling_factor_buffer_ptrs);
|
|
}
|
|
if (ep_group->ht_buffers.host_ptr_block) {
|
|
cudaFreeHost(ep_group->ht_buffers.host_ptr_block);
|
|
ep_group->ht_buffers.host_ptr_block = nullptr;
|
|
}
|
|
if (ep_group->ht_buffers.peer_ipc_base_ptrs) {
|
|
cudaFreeHost(ep_group->ht_buffers.peer_ipc_base_ptrs);
|
|
ep_group->ht_buffers.peer_ipc_base_ptrs = nullptr;
|
|
}
|
|
|
|
ep_group->ht_buffers.initialized = false;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
// NCCLCHECK macro for public API error checking
|
|
// Undef any existing definition from internal NCCL headers to avoid using ncclDebugLog
|
|
#ifdef NCCLCHECK
|
|
#undef NCCLCHECK
|
|
#endif
|
|
#define NCCLCHECK(cmd) do { \
|
|
ncclResult_t res = cmd; \
|
|
if (res != ncclSuccess) { \
|
|
fprintf(stderr, "NCCL error %s:%d '%s'\n", \
|
|
__FILE__, __LINE__, ncclGetErrorString(res));\
|
|
return res; \
|
|
} \
|
|
} while(0)
|
|
|
|
// CUDACHECK_RET macro for CUDA calls in functions returning ncclResult_t
|
|
#ifndef CUDACHECK_RET
|
|
#define CUDACHECK_RET(cmd) do { \
|
|
cudaError_t err = cmd; \
|
|
if (err != cudaSuccess) { \
|
|
fprintf(stderr, "CUDA error %s:%d '%s'\n", \
|
|
__FILE__, __LINE__, cudaGetErrorString(err));\
|
|
return ncclInternalError; \
|
|
} \
|
|
} while(0)
|
|
#endif
|
|
|
|
// Constants for GIN configuration
|
|
static constexpr int HYBRIDEP_GIN_MAX_CONTEXTS = 32;
|
|
static constexpr int HYBRIDEP_GIN_CTXS_PER_COMM = 4;
|
|
static constexpr int MAX_BARRIER_SESSIONS = 32;
|
|
|
|
static ncclResult_t init_hybridep_internode(ncclEpGroup_t ep_group,
|
|
const ncclEpGroupConfig_t* in_config,
|
|
cudaStream_t stream)
|
|
{
|
|
// Initialize using public NCCL APIs
|
|
if (!in_config || !ep_group) {
|
|
fprintf(stderr, "init_hybridep_internode: null config or ep_group\n");
|
|
return ncclInvalidArgument;
|
|
}
|
|
|
|
int nNodes = ep_group->nNodes;
|
|
// HT internode always uses physical node semantics: RDMA groups = nNodes, IPC = gpus_per_node.
|
|
// MNNVL (multiple nodes with NVLink fabric) is treated like regular multi-node RDMA for HT.
|
|
int gpus_per_node = ep_group->gpus_per_node;
|
|
int rank_in_node = ep_group->rank_in_node;
|
|
int node_id = ep_group->node_id;
|
|
int n_ranks_per_node = gpus_per_node;
|
|
ep_group->ht_buffers.internode_initialized = false;
|
|
|
|
if (nNodes <= 1) {
|
|
// Single physical node — no internode RDMA needed
|
|
return ncclSuccess;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Phase 1: All local allocations (no collectives)
|
|
// ncclMemAlloc + buffer partitioning moved here from after ncclDevCommCreate
|
|
// to remove them from the collective critical path.
|
|
// =========================================================================
|
|
|
|
constexpr size_t GIN_ALIGNMENT = 4096;
|
|
auto align_size = [](size_t sz, size_t alignment) {
|
|
return (sz + alignment - 1) & ~(alignment - 1);
|
|
};
|
|
|
|
// These buffers are accessed with stride MAX_SUPPORTED_TOKENS_PER_RANK (compile-time constant
|
|
// used as rdma_remote_node_id * MAX_SUPPORTED_TOKENS_PER_RANK + token_offset in the kernel).
|
|
// They must be sized for that stride regardless of the runtime max_tokens_per_rank.
|
|
size_t rdma_intra_node_red_token_sz = align_size(static_cast<size_t>(MAX_SUPPORTED_TOKENS_PER_RANK * (nNodes - 1)) * ep_group->hidden * sizeof(uint16_t), GIN_ALIGNMENT);
|
|
size_t combine_rdma_inter_node_group_token_sz = rdma_intra_node_red_token_sz;
|
|
size_t rdma_intra_node_red_prob_sz = align_size(static_cast<size_t>(MAX_SUPPORTED_TOKENS_PER_RANK * (nNodes - 1)) * (ep_group->num_local_experts * n_ranks_per_node) * sizeof(float), GIN_ALIGNMENT);
|
|
size_t combine_rdma_inter_node_group_prob_sz = rdma_intra_node_red_prob_sz;
|
|
size_t flags_sz = align_size(static_cast<size_t>(nNodes) * sizeof(uint64_t), GIN_ALIGNMENT);
|
|
size_t token_staging_sz = align_size(static_cast<size_t>(ep_group->config.max_tokens_per_rank) * ep_group->hidden * sizeof(uint16_t), GIN_ALIGNMENT);
|
|
size_t dense_prob_sz = align_size(static_cast<size_t>(ep_group->config.max_tokens_per_rank) * ep_group->config.num_experts * sizeof(float), GIN_ALIGNMENT);
|
|
size_t scaling_factor_staging_sz = align_size(static_cast<size_t>(ep_group->config.max_tokens_per_rank) * sizeof(float), GIN_ALIGNMENT);
|
|
|
|
size_t bytes_per_token_entry = ep_group->hidden * sizeof(uint16_t);
|
|
size_t bytes_per_prob_entry = (ep_group->num_local_experts * n_ranks_per_node) * sizeof(float);
|
|
size_t bytes_per_sf_entry = (ep_group->hidden / 128) * sizeof(float);
|
|
size_t bytes_per_entry = bytes_per_token_entry + bytes_per_prob_entry + bytes_per_sf_entry;
|
|
size_t rdma_send_staging_sz = align_size(static_cast<size_t>(nNodes - 1) * ep_group->config.max_tokens_per_rank * bytes_per_entry, GIN_ALIGNMENT);
|
|
size_t rdma_recv_packed_sz = align_size(static_cast<size_t>(nNodes - 1) * ep_group->config.max_tokens_per_rank * bytes_per_entry, GIN_ALIGNMENT);
|
|
|
|
size_t total_gin_buffer_size = 0;
|
|
total_gin_buffer_size += rdma_intra_node_red_token_sz;
|
|
total_gin_buffer_size += combine_rdma_inter_node_group_token_sz;
|
|
total_gin_buffer_size += rdma_intra_node_red_prob_sz;
|
|
total_gin_buffer_size += combine_rdma_inter_node_group_prob_sz;
|
|
total_gin_buffer_size += flags_sz * 2;
|
|
total_gin_buffer_size += token_staging_sz;
|
|
total_gin_buffer_size += dense_prob_sz;
|
|
total_gin_buffer_size += scaling_factor_staging_sz;
|
|
total_gin_buffer_size += rdma_send_staging_sz;
|
|
total_gin_buffer_size += rdma_recv_packed_sz;
|
|
|
|
NCCLCHECK(ncclMemAlloc(&ep_group->gin_config.gin_base_ptr, total_gin_buffer_size));
|
|
|
|
// Partition the buffer into individual regions
|
|
uint8_t* ptr = reinterpret_cast<uint8_t*>(ep_group->gin_config.gin_base_ptr);
|
|
size_t offset = 0;
|
|
|
|
ep_group->ht_buffers.rdma_intra_node_red_token = reinterpret_cast<uint16_t*>(ptr + offset);
|
|
offset += rdma_intra_node_red_token_sz;
|
|
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_token = reinterpret_cast<uint16_t*>(ptr + offset);
|
|
offset += combine_rdma_inter_node_group_token_sz;
|
|
|
|
ep_group->ht_buffers.rdma_intra_node_red_prob = reinterpret_cast<float*>(ptr + offset);
|
|
offset += rdma_intra_node_red_prob_sz;
|
|
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_prob = reinterpret_cast<float*>(ptr + offset);
|
|
offset += combine_rdma_inter_node_group_prob_sz;
|
|
|
|
ep_group->ht_buffers.rdma_inter_node_group_flags = reinterpret_cast<uint64_t*>(ptr + offset);
|
|
CUDACHECK_RET(cudaMemset(ep_group->ht_buffers.rdma_inter_node_group_flags, 0, flags_sz));
|
|
offset += flags_sz;
|
|
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_flags = reinterpret_cast<uint64_t*>(ptr + offset);
|
|
CUDACHECK_RET(cudaMemset(ep_group->ht_buffers.combine_rdma_inter_node_group_flags, 0, flags_sz));
|
|
offset += flags_sz;
|
|
|
|
ep_group->ht_buffers.token_staging_buffer = reinterpret_cast<void*>(ptr + offset);
|
|
offset += token_staging_sz;
|
|
|
|
ep_group->ht_buffers.dense_prob_buffer = reinterpret_cast<float*>(ptr + offset);
|
|
offset += dense_prob_sz;
|
|
|
|
ep_group->ht_buffers.scaling_factor_staging_buffer = reinterpret_cast<float*>(ptr + offset);
|
|
offset += scaling_factor_staging_sz;
|
|
|
|
offset += rdma_send_staging_sz;
|
|
|
|
// Calculate offsets for kernel mr_info
|
|
size_t cur_offset = 0;
|
|
ep_group->gin_config.rdma_intra_node_red_token_offset = cur_offset;
|
|
cur_offset += rdma_intra_node_red_token_sz;
|
|
|
|
ep_group->gin_config.combine_rdma_inter_node_group_token_offset = cur_offset;
|
|
cur_offset += combine_rdma_inter_node_group_token_sz;
|
|
|
|
ep_group->gin_config.rdma_intra_node_red_prob_offset = cur_offset;
|
|
cur_offset += rdma_intra_node_red_prob_sz;
|
|
|
|
ep_group->gin_config.combine_rdma_inter_node_group_prob_offset = cur_offset;
|
|
cur_offset += combine_rdma_inter_node_group_prob_sz;
|
|
|
|
cur_offset += flags_sz * 2;
|
|
|
|
ep_group->gin_config.token_staging_offset = cur_offset;
|
|
cur_offset += token_staging_sz;
|
|
|
|
ep_group->gin_config.dense_prob_offset = cur_offset;
|
|
cur_offset += dense_prob_sz;
|
|
|
|
ep_group->gin_config.scaling_factor_staging_offset = cur_offset;
|
|
cur_offset += scaling_factor_staging_sz;
|
|
|
|
ep_group->gin_config.rdma_send_staging_offset = cur_offset;
|
|
cur_offset += rdma_send_staging_sz;
|
|
|
|
ep_group->gin_config.rdma_inter_node_group_packed_offset = cur_offset;
|
|
cur_offset += rdma_recv_packed_sz;
|
|
|
|
// =========================================================================
|
|
// Phase 2: main_comm collective -- create split communicator
|
|
// =========================================================================
|
|
// Node concept: color = rank's position within its physical node (rank_in_node).
|
|
// Groups same-node-position ranks across nodes into one split_comm for GIN/RDMA ops.
|
|
// key = node_id orders ranks within each split_comm by their node index.
|
|
int color = rank_in_node;
|
|
int key = node_id;
|
|
NCCLCHECK(ncclCommSplit(ep_group->comm, color, key, &ep_group->gin_config.split_comm, nullptr));
|
|
|
|
int qps_per_rank = ep_group->config.num_qp_per_rank;
|
|
int min_required_ctx = HYBRIDEP_DISPATCH_NUM_OF_BLOCKS * HYBRIDEP_DISPATCH_N2N_WARPS;
|
|
if (qps_per_rank == 0) qps_per_rank = min_required_ctx;
|
|
if (qps_per_rank < min_required_ctx) {
|
|
fprintf(stderr, "[HT GIN] Error: num_qp_per_rank(%d) must be >= %d for dedicated N2N warp contexts\n",
|
|
qps_per_rank, min_required_ctx);
|
|
return ncclInvalidUsage;
|
|
}
|
|
ep_group->gin_config.qps_per_rank = qps_per_rank;
|
|
ep_group->gin_config.num_comms = 1;
|
|
ep_group->gin_config.num_ctx_per_comm = qps_per_rank;
|
|
|
|
int max_chunks_per_rank = (MAX_SUPPORTED_TOKENS_PER_RANK + HT_OF_NUM_TOKENS_PER_CHUNK - 1) / HT_OF_NUM_TOKENS_PER_CHUNK;
|
|
int dispatch_signals = n_ranks_per_node * nNodes * max_chunks_per_rank;
|
|
int combine_signals = n_ranks_per_node * nNodes * max_chunks_per_rank;
|
|
int streaming_tail_signals = nNodes * nNodes * n_ranks_per_node * max_chunks_per_rank;
|
|
int streaming_head_signals = nNodes * nNodes * n_ranks_per_node;
|
|
ep_group->gin_config.num_total_signals = dispatch_signals + combine_signals +
|
|
streaming_tail_signals + streaming_head_signals + MAX_BARRIER_SESSIONS;
|
|
ep_group->gin_config.signals_base = 0;
|
|
ep_group->gin_config.combine_signal_offset = dispatch_signals;
|
|
ep_group->gin_config.signals_tail_base = dispatch_signals + combine_signals;
|
|
|
|
// =========================================================================
|
|
// Phase 3: split_comm setup (DevCommCreate + WindowRegister)
|
|
// =========================================================================
|
|
ep_group->gin_config.num_dcomms = 1;
|
|
ep_group->gin_config.dcomms = new ncclDevComm_t[1];
|
|
|
|
{
|
|
ncclCommProperties_t props = NCCL_COMM_PROPERTIES_INITIALIZER;
|
|
NCCL_CHECK_RESULT(ncclCommQueryProperties(ep_group->gin_config.split_comm, &props));
|
|
if (props.ginType == NCCL_GIN_TYPE_NONE) {
|
|
fprintf(stderr, "[HT GIN] Error: NCCL EP internode requires GIN, but GIN is not supported\n");
|
|
return ncclInvalidUsage;
|
|
}
|
|
}
|
|
|
|
{
|
|
ncclDevCommRequirements reqs = NCCL_DEV_COMM_REQUIREMENTS_INITIALIZER;
|
|
reqs.ginSignalCount = ep_group->gin_config.num_total_signals;
|
|
reqs.ginConnectionType = NCCL_GIN_CONNECTION_FULL;
|
|
reqs.ginContextCount = ep_group->gin_config.num_ctx_per_comm;
|
|
reqs.ginQueueDepth = 3 * HT_OF_NUM_TOKENS_PER_CHUNK + 1;
|
|
NCCLCHECK(ncclDevCommCreate(ep_group->gin_config.split_comm, &reqs, &ep_group->gin_config.dcomms[0]));
|
|
}
|
|
|
|
CUDACHECK_RET(cudaMalloc(reinterpret_cast<void**>(&ep_group->gin_config.d_dcomms),
|
|
sizeof(ncclDevComm_t) * ep_group->gin_config.num_dcomms));
|
|
CUDACHECK_RET(cudaMemcpy(ep_group->gin_config.d_dcomms, ep_group->gin_config.dcomms,
|
|
sizeof(ncclDevComm_t) * ep_group->gin_config.num_dcomms, cudaMemcpyHostToDevice));
|
|
|
|
// WindowRegister: depends on split_comm (Phase 2) and gin_base_ptr (Phase 1)
|
|
NCCLCHECK(ncclCommWindowRegister(ep_group->gin_config.split_comm,
|
|
ep_group->gin_config.gin_base_ptr,
|
|
total_gin_buffer_size,
|
|
&ep_group->gin_config.nccl_window, 0));
|
|
|
|
ep_group->ht_buffers.internode_initialized = true;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
static ncclResult_t destroy_hybridep_internode(ncclEpGroup_t ep_group){
|
|
if (!ep_group->ht_buffers.internode_initialized) return ncclSuccess;
|
|
|
|
// =========================================================================
|
|
// Cleanup using public NCCL APIs
|
|
// =========================================================================
|
|
|
|
// Destroy device communicator (created on split_comm)
|
|
if (ep_group->gin_config.dcomms != nullptr) {
|
|
if (ep_group->gin_config.split_comm) {
|
|
ncclResult_t res = ncclDevCommDestroy(ep_group->gin_config.split_comm, &ep_group->gin_config.dcomms[0]);
|
|
if (res != ncclSuccess) {
|
|
fprintf(stderr, "[HT GIN] Warning: Failed to destroy device comm: %s\n",
|
|
ncclGetErrorString(res));
|
|
}
|
|
}
|
|
delete[] ep_group->gin_config.dcomms;
|
|
ep_group->gin_config.dcomms = nullptr;
|
|
}
|
|
// Free device memory for dcomms
|
|
if (ep_group->gin_config.d_dcomms != nullptr) {
|
|
cudaFree(ep_group->gin_config.d_dcomms);
|
|
ep_group->gin_config.d_dcomms = nullptr;
|
|
}
|
|
|
|
// Deregister the single window on split_comm
|
|
if (ep_group->gin_config.gin_base_ptr != nullptr) {
|
|
ncclCommWindowDeregister(ep_group->gin_config.split_comm, ep_group->gin_config.nccl_window);
|
|
ep_group->gin_config.nccl_window = {};
|
|
}
|
|
|
|
// Free the single GIN buffer (contains all RDMA regions)
|
|
if (ep_group->gin_config.gin_base_ptr != nullptr) {
|
|
ncclResult_t res = ncclMemFree(ep_group->gin_config.gin_base_ptr);
|
|
if (res != ncclSuccess) {
|
|
fprintf(stderr, "[HT GIN] Warning: Failed to free GIN memory: %s\n",
|
|
ncclGetErrorString(res));
|
|
}
|
|
ep_group->gin_config.gin_base_ptr = nullptr;
|
|
|
|
// Clear buffer pointers (they pointed into gin_base_ptr)
|
|
ep_group->ht_buffers.rdma_intra_node_red_token = nullptr;
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_token = nullptr;
|
|
ep_group->ht_buffers.rdma_intra_node_red_prob = nullptr;
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_prob = nullptr;
|
|
ep_group->ht_buffers.rdma_inter_node_group_flags = nullptr;
|
|
ep_group->ht_buffers.combine_rdma_inter_node_group_flags = nullptr;
|
|
ep_group->ht_buffers.token_staging_buffer = nullptr;
|
|
ep_group->ht_buffers.dense_prob_buffer = nullptr;
|
|
ep_group->ht_buffers.scaling_factor_staging_buffer = nullptr;
|
|
}
|
|
|
|
// Finalize and destroy the split communicator
|
|
if (ep_group->gin_config.split_comm != nullptr) {
|
|
ncclCommFinalize(ep_group->gin_config.split_comm);
|
|
ncclCommDestroy(ep_group->gin_config.split_comm);
|
|
ep_group->gin_config.split_comm = nullptr;
|
|
}
|
|
ep_group->gin_config.num_comms = 0;
|
|
|
|
ep_group->ht_buffers.internode_initialized = false;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpCreateGroup(
|
|
ncclEpGroup_t* out_ep_group,
|
|
ncclComm_t comm,
|
|
const ncclEpGroupConfig_t* in_config,
|
|
cudaStream_t stream,
|
|
ncclEpAllocFn_t alloc_fn,
|
|
ncclEpFreeFn_t free_fn
|
|
) {
|
|
// Parameter validation
|
|
assert(out_ep_group != nullptr);
|
|
assert(in_config != nullptr);
|
|
int nRanks;
|
|
assert(comm != nullptr && ncclCommCount(comm, &nRanks) == ncclSuccess && nRanks > 0);
|
|
assert(in_config->version == 1 && "ncclEpCreateGroup: invalid config version (expected 1)");
|
|
assert((in_config->algorithm == NCCL_EP_ALGO_LOW_LATENCY ||
|
|
in_config->algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT) &&
|
|
"ncclEpCreateGroup: invalid algorithm, supported: low_latency, high_throughput");
|
|
|
|
bool low_latency_mode = (in_config->algorithm == NCCL_EP_ALGO_LOW_LATENCY);
|
|
bool hybridep_mode = (in_config->algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT);
|
|
assert(in_config->num_experts > 0 && "ncclEpCreateGroup: num_experts must be greater than 0");
|
|
assert(in_config->token_size_bytes > 0 && "ncclEpCreateGroup: token_size_bytes must be greater than 0");
|
|
assert(!(in_config->algorithm == NCCL_EP_ALGO_LOW_LATENCY && in_config->max_tokens_per_rank == 0) &&
|
|
"ncclEpCreateGroup: max_tokens_per_rank must be greater than 0 for low latency mode");
|
|
assert(!(in_config->algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT && in_config->max_tokens_per_rank == 0) &&
|
|
"ncclEpCreateGroup: max_tokens_per_rank must be set for HT backend");
|
|
assert(!(in_config->algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT &&
|
|
in_config->max_tokens_per_rank > MAX_SUPPORTED_TOKENS_PER_RANK) &&
|
|
"ncclEpCreateGroup: HT max_tokens_per_rank exceeds build-time MAX_SUPPORTED_TOKENS_PER_RANK");
|
|
// Query LSA team size: number of ranks reachable via NVLink/LSA from this rank.
|
|
ncclTeam lsa_team = ncclTeamLsa(comm);
|
|
const int lsa_team_size = lsa_team.nRanks;
|
|
|
|
if (hybridep_mode) {
|
|
assert((nRanks % lsa_team_size) == 0 &&
|
|
"ncclEpCreateGroup: HT requires nRanks divisible by lsa_team_size");
|
|
}
|
|
|
|
// Allocate EP group structure
|
|
void* raw_memory = malloc(sizeof(ncclEpGroup));
|
|
assert(raw_memory != nullptr && "Failed to malloc for ncclEpGroup");
|
|
*out_ep_group = new (raw_memory) ncclEpGroup();
|
|
ncclEpGroup_t ep_group = *out_ep_group;
|
|
|
|
// Store configuration
|
|
ep_group->comm = comm;
|
|
ep_group->config = *in_config;
|
|
// C-style cast required: cudaMalloc/cudaFree are overloaded and reinterpret_cast
|
|
// cannot disambiguate overloaded functions.
|
|
ep_group->alloc_fn = alloc_fn ? alloc_fn : (ncclEpAllocFn_t)cudaMalloc;
|
|
ep_group->free_fn = free_fn ? free_fn : (ncclEpFreeFn_t)cudaFree;
|
|
|
|
NCCL_CHECK_RESULT(ncclCommCount(comm, &ep_group->nRanks));
|
|
NCCL_CHECK_RESULT(ncclCommUserRank(comm, &ep_group->rank));
|
|
NCCL_CHECK_RESULT(ncclCommCuDevice(comm, &ep_group->cuda_device_id));
|
|
|
|
// Determine number of nodes by gathering hostnames and counting unique ones
|
|
constexpr size_t HOSTNAME_LEN = 256;
|
|
std::vector<char> all_hostnames(HOSTNAME_LEN * ep_group->nRanks, 0);
|
|
gethostname(all_hostnames.data() + ep_group->rank * HOSTNAME_LEN, HOSTNAME_LEN);
|
|
ncclAllGatherHost(all_hostnames.data(), HOSTNAME_LEN, ep_group->rank, ep_group->nRanks, comm, stream);
|
|
std::set<std::string> unique_hosts;
|
|
for (int i = 0; i < ep_group->nRanks; ++i) {
|
|
unique_hosts.insert(std::string(all_hostnames.data() + i * HOSTNAME_LEN));
|
|
}
|
|
ep_group->nNodes = static_cast<int>(unique_hosts.size());
|
|
|
|
ep_group->num_local_experts = ep_group->config.num_experts / ep_group->nRanks;
|
|
ep_group->max_recv_tokens = ep_group->nRanks *
|
|
static_cast<int>(ep_group->config.max_tokens_per_rank);
|
|
ep_group->hidden = ep_group->config.token_size_bytes / ncclTypeSize(ncclBfloat16);
|
|
|
|
// Apply default values for auto-configured fields (when set to NCCL_EP_AUTO)
|
|
if (ep_group->config.num_channels == NCCL_EP_AUTO) {
|
|
ep_group->config.num_channels = 10;
|
|
}
|
|
|
|
if (ep_group->config.rdma_buffer_size == NCCL_EP_AUTO && ep_group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
ep_group->config.rdma_buffer_size = nccl_ep::get_low_latency_rdma_size_hint(ep_group->config.max_tokens_per_rank, ep_group->hidden, ep_group->nRanks, ep_group->config.num_experts);
|
|
}
|
|
|
|
if (ep_group->config.num_qp_per_rank == NCCL_EP_AUTO) {
|
|
ep_group->config.num_qp_per_rank = HYBRIDEP_DISPATCH_NUM_OF_BLOCKS * HYBRIDEP_DISPATCH_N2N_WARPS;
|
|
}
|
|
|
|
// Physical node properties
|
|
ep_group->gpus_per_node = ep_group->nRanks / ep_group->nNodes;
|
|
ep_group->rank_in_node = ep_group->cuda_device_id;
|
|
ep_group->node_id = ep_group->rank / ep_group->gpus_per_node;
|
|
ep_group->lsa_rank_count = ep_group->gpus_per_node;
|
|
|
|
// LSA domain properties (used by LL path)
|
|
ep_group->lsa_team_size = lsa_team_size;
|
|
ep_group->rdma_ranks = std::max(1, ep_group->nRanks / lsa_team_size);
|
|
ep_group->rdma_rank = ep_group->rank / lsa_team_size;
|
|
ep_group->rdma_buffer = nullptr;
|
|
|
|
CUDA_CHECK(cudaSetDevice(ep_group->cuda_device_id));
|
|
cudaDeviceProp device_prop = {};
|
|
CUDA_CHECK(cudaGetDeviceProperties(&device_prop, ep_group->cuda_device_id));
|
|
ep_group->device_sm_count = device_prop.multiProcessorCount;
|
|
|
|
CUDA_CHECK(ep_group->alloc_fn(&ep_group->ep_workspace, NUM_WORKSPACE_BYTES));
|
|
CUDA_CHECK(cudaMemsetAsync(ep_group->ep_workspace, 0, NUM_WORKSPACE_BYTES, stream));
|
|
|
|
ncclCommProperties_t props = NCCL_COMM_PROPERTIES_INITIALIZER;
|
|
NCCL_CHECK_RESULT(ncclCommQueryProperties(ep_group->comm, &props));
|
|
if (!props.deviceApiSupport) {
|
|
fprintf(stderr, "Error: NCCL EP requires NCCL Device API support, but Device API is not supported\n");
|
|
return ncclInvalidUsage;
|
|
}
|
|
|
|
// Initialize HT intranode buffers (IPC handles, completion flags, etc.)
|
|
if (hybridep_mode) {
|
|
NCCL_CHECK_RESULT(init_hybridep_intranode(ep_group, in_config, stream));
|
|
NCCL_CHECK_RESULT(init_hybridep_internode(ep_group, in_config, stream));
|
|
}
|
|
|
|
if (ep_group->config.rdma_buffer_size > 0 && low_latency_mode) {
|
|
// Allocate RDMA buffer
|
|
ncclBarrier(ep_group->comm, stream, ep_group->ep_workspace);
|
|
NCCL_CHECK_RESULT(ncclMemAlloc(&ep_group->rdma_buffer, ep_group->config.rdma_buffer_size));
|
|
|
|
// Clean buffer (mainly for low-latency mode)
|
|
CUDA_CHECK(cudaMemset(ep_group->rdma_buffer, 0, ep_group->config.rdma_buffer_size));
|
|
// NCCL related setup - use ep_group->comm directly with all GIN contexts
|
|
// (like DeepEP: 1 comm, N contexts, no split needed)
|
|
ep_group->num_nccl_comms = 0; // no split comms created
|
|
|
|
// Cleaning up any pending CUDA error
|
|
CUDA_CHECK(cudaGetLastError());
|
|
|
|
// Create device communicator on ep_group->comm with all GIN contexts
|
|
ncclDevComm_t* nccl_dev_comms_host = new ncclDevComm_t[1];
|
|
nccl_dev_comms_host[0] = ncclDevComm_t{};
|
|
ep_group->num_dispatch_signals = ep_group->num_local_experts * ep_group->nRanks;
|
|
int num_total_signals = ep_group->num_dispatch_signals;
|
|
|
|
ncclCommProperties_t props = NCCL_COMM_PROPERTIES_INITIALIZER;
|
|
NCCLCHECK(ncclCommQueryProperties(ep_group->comm, &props));
|
|
if (props.nLsaTeams > 1 && props.ginType == NCCL_GIN_TYPE_NONE) {
|
|
fprintf(stderr, "[LL] Error: NCCL EP requires GIN, but GIN is not supported\n");
|
|
return ncclInvalidUsage;
|
|
}
|
|
|
|
ncclDevCommRequirements reqs = NCCL_DEV_COMM_REQUIREMENTS_INITIALIZER;
|
|
if (props.nLsaTeams > 1) {
|
|
reqs.ginContextCount = ep_group->config.num_qp_per_rank; // all contexts in single comm
|
|
// Signal layout: combine uses [0, num_total_signals), dispatch uses [num_total_signals, 2*num_total_signals)
|
|
reqs.ginSignalCount = 2 * num_total_signals;
|
|
reqs.ginForceEnable = true;
|
|
reqs.ginConnectionType = NCCL_GIN_CONNECTION_FULL;
|
|
}
|
|
NCCL_CHECK_RESULT(ncclDevCommCreate(ep_group->comm, &reqs, &nccl_dev_comms_host[0]));
|
|
|
|
CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&ep_group->nccl_dev_comms),
|
|
sizeof(ncclDevComm_t)));
|
|
CUDA_CHECK(cudaMemcpy(ep_group->nccl_dev_comms, nccl_dev_comms_host,
|
|
sizeof(ncclDevComm_t), cudaMemcpyHostToDevice));
|
|
|
|
// Register RDMA buffer with single NCCL window
|
|
ncclWindow_t* nccl_wins_host = new ncclWindow_t[1];
|
|
NCCL_CHECK_RESULT(ncclCommWindowRegister(ep_group->comm, ep_group->rdma_buffer,
|
|
ep_group->config.rdma_buffer_size, &nccl_wins_host[0], 0));
|
|
|
|
CUDA_CHECK(cudaMalloc(reinterpret_cast<void**>(&ep_group->nccl_wins),
|
|
sizeof(ncclWindow_t)));
|
|
CUDA_CHECK(cudaMemcpy(ep_group->nccl_wins, nccl_wins_host,
|
|
sizeof(ncclWindow_t), cudaMemcpyHostToDevice));
|
|
|
|
// Cleanup host memory for NCCL windows and devcomms
|
|
delete[] nccl_wins_host;
|
|
nccl_wins_host = nullptr;
|
|
|
|
delete[] nccl_dev_comms_host;
|
|
nccl_dev_comms_host = nullptr;
|
|
|
|
ncclBarrier(ep_group->comm, stream, ep_group->ep_workspace);
|
|
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
}
|
|
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpGroupDestroy(
|
|
ncclEpGroup_t ep_group,
|
|
cudaStream_t stream
|
|
) {
|
|
if (ep_group == nullptr) {
|
|
return ncclSuccess;
|
|
}
|
|
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
|
|
// Clean up HT intranode resources
|
|
if (ep_group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT &&
|
|
ep_group->ht_buffers.initialized) {
|
|
destroy_hybridep_intranode(ep_group);
|
|
}
|
|
// Clean up HT internode resources (GIN deregistration must happen before ncclCommDestroy)
|
|
if (ep_group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT &&
|
|
ep_group->ht_buffers.internode_initialized) {
|
|
destroy_hybridep_internode(ep_group);
|
|
}
|
|
// Clean up workspace memory
|
|
if (ep_group->ep_workspace != nullptr) {
|
|
CUDA_CHECK(ep_group->free_fn(ep_group->ep_workspace));
|
|
}
|
|
|
|
// Clean up RDMA resources (single-comm path: 1 window, 1 devcomm on ep_group->comm)
|
|
if (ep_group->config.rdma_buffer_size > 0 && NCCL_EP_ALGO_LOW_LATENCY == ep_group->config.algorithm) {
|
|
CUDA_CHECK(cudaDeviceSynchronize());
|
|
NCCL_CHECK_RESULT(ncclBarrier(ep_group->comm, stream));
|
|
|
|
// Deregister single NCCL window (copy back from device, deregister on ep_group->comm)
|
|
ncclWindow_t win_host;
|
|
CUDA_CHECK(cudaMemcpy(&win_host, ep_group->nccl_wins,
|
|
sizeof(ncclWindow_t), cudaMemcpyDeviceToHost));
|
|
NCCL_CHECK_RESULT(ncclCommWindowDeregister(ep_group->comm, win_host));
|
|
CUDA_CHECK(cudaFree(ep_group->nccl_wins));
|
|
ep_group->nccl_wins = nullptr;
|
|
|
|
// Free RDMA buffer (after window deregistered)
|
|
if (ep_group->rdma_buffer) {
|
|
NCCL_CHECK_RESULT(ncclMemFree(ep_group->rdma_buffer));
|
|
ep_group->rdma_buffer = nullptr;
|
|
}
|
|
|
|
// Destroy single NCCL device communicator (copy back from device, destroy on ep_group->comm)
|
|
ncclDevComm_t dc_host;
|
|
CUDA_CHECK(cudaMemcpy(&dc_host, ep_group->nccl_dev_comms,
|
|
sizeof(ncclDevComm_t), cudaMemcpyDeviceToHost));
|
|
NCCL_CHECK_RESULT(ncclDevCommDestroy(ep_group->comm, &dc_host));
|
|
CUDA_CHECK(cudaFree(ep_group->nccl_dev_comms));
|
|
ep_group->nccl_dev_comms = nullptr;
|
|
|
|
// No split comms to destroy (using ep_group->comm directly)
|
|
}
|
|
// Invoke destructor explicitly (placement new was used)
|
|
ep_group->~ncclEpGroup();
|
|
|
|
// Free the group structure
|
|
free(ep_group);
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpTensorCreate(
|
|
ncclEpGroup_t ep_group,
|
|
ncclNDTensor_t* tensor,
|
|
unsigned int ndim,
|
|
ncclDataType_t datatype,
|
|
ncclEpTensorTag_t tag,
|
|
void* data,
|
|
unsigned int size0,
|
|
unsigned int size1,
|
|
unsigned int size2,
|
|
unsigned int size3,
|
|
unsigned int size4
|
|
) {
|
|
assert(ep_group != nullptr);
|
|
assert(tensor != nullptr);
|
|
assert(ndim > 0 && ndim <= 5);
|
|
|
|
unsigned int dim_sizes[] = {size0, size1, size2, size3, size4};
|
|
|
|
struct ncclNDTensor* t = new struct ncclNDTensor();
|
|
t->version = 1;
|
|
t->ndim = ndim;
|
|
t->datatype = datatype;
|
|
t->tag = tag;
|
|
t->flags = NCCL_EP_TENSOR_FLAG_NONE;
|
|
|
|
t->sizes = new unsigned int[ndim];
|
|
t->strides = new unsigned int[ndim];
|
|
|
|
unsigned int total_size = 1;
|
|
for (unsigned int i = 0; i < ndim; i++) {
|
|
t->sizes[i] = dim_sizes[i];
|
|
t->strides[i] = 1;
|
|
total_size *= dim_sizes[i];
|
|
}
|
|
|
|
if (data != nullptr) {
|
|
t->data = data;
|
|
t->owns_data = false;
|
|
} else {
|
|
CUDA_CHECK(ep_group->alloc_fn(&t->data, total_size * ncclTypeSize(datatype)));
|
|
t->owns_data = true;
|
|
}
|
|
*tensor = t;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpTensorDestroy(
|
|
ncclEpGroup_t ep_group,
|
|
ncclNDTensor_t tensor
|
|
) {
|
|
if (tensor == nullptr) return ncclSuccess;
|
|
|
|
if (tensor->owns_data && tensor->data && ep_group) {
|
|
CUDA_CHECK(ep_group->free_fn(tensor->data));
|
|
}
|
|
delete[] tensor->sizes;
|
|
delete[] tensor->strides;
|
|
delete tensor;
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpTensorGetData(
|
|
ncclNDTensor_t tensor,
|
|
void** data
|
|
) {
|
|
assert(tensor != nullptr);
|
|
assert(data != nullptr);
|
|
*data = tensor->data;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpTensorGetSizes(
|
|
ncclNDTensor_t tensor,
|
|
const unsigned int** sizes,
|
|
unsigned int* ndim
|
|
) {
|
|
assert(tensor != nullptr);
|
|
if (sizes != nullptr) *sizes = tensor->sizes;
|
|
if (ndim != nullptr) *ndim = tensor->ndim;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
struct ncclEpHandle {
|
|
ncclEpGroup_t group;
|
|
|
|
bool use_fp8;
|
|
|
|
// tensor that is owned by the user, do not free this tensor!
|
|
ncclNDTensor_t topk_idx;
|
|
int num_tokens, num_topk;
|
|
|
|
bool cached_mode;
|
|
int num_scales;
|
|
int hidden_int4;
|
|
|
|
union {
|
|
struct {
|
|
// packed tensors for LL
|
|
ncclNDTensor_t expert_recv_source_indices;
|
|
ncclNDTensor_t expert_dispatch_layout;
|
|
|
|
int buffer_idx = 0;
|
|
|
|
std::function<void(unsigned int)> continue_fn;
|
|
nccl_ep::LowLatencyLayout layout;
|
|
} ll;
|
|
struct {
|
|
|
|
// Global routing map: allgathered routing decisions from ALL ranks (bitmap format).
|
|
// Contains complete routing information needed for preprocessing.
|
|
// dtype: uint8_t (bitmap: 1 bit per expert, 8 experts per byte)
|
|
// layout: [total_tokens, ceil(num_experts / 8)]
|
|
// lifetime: valid after ncclAllGather in ncclEpCreateHandle
|
|
uint8_t* global_routing_map;
|
|
|
|
// =================================================================================
|
|
// PREPROCESSING OUTPUTS - Computed once per iteration, used by dispatch & combine
|
|
// =================================================================================
|
|
|
|
// Sparse-to-dense map: maps each (token, source_rank) pair to its position in
|
|
// the destination rank's expert buffer. Used by NVLink (intra-node) warps.
|
|
// Value of -1 indicates token is not routed to that rank.
|
|
// dtype: int32_t
|
|
// layout: [num_nodes * max_tokens_per_rank, num_ranks_per_node]
|
|
// usage: dispatch S2G warp group, combine G2S warp group
|
|
// lifetime: valid after metadata_preprocessing, constant within iteration
|
|
// vs NCCL HT: no direct equivalent.
|
|
// - HT: uses is_token_in_rank (local tokens only) + atomics to compute positions
|
|
// - HT: precomputes positions for ALL nodes' tokens, -1 sentinel for not routed
|
|
int32_t* sparse_to_dense_map;
|
|
|
|
// RDMA-to-attention map: boolean mask indicating which tokens this node needs
|
|
// to RECEIVE from RDMA (inter-node). Indexed by [node_id, token_id].
|
|
// Primarily used during combine to know which remote tokens to wait for.
|
|
// dtype: bool
|
|
// layout: [num_nodes, max_tokens_per_rank_padded_to_16]
|
|
// (padding to 16 required for TMA alignment)
|
|
// usage: dispatch G2S warp (polling), combine inter-node G2S/reduction warps
|
|
// lifetime: valid after metadata_preprocessing, constant within iteration
|
|
// vs NCCL HT: inverse perspective of is_token_in_rank.
|
|
// - is_token_in_rank: outbound - "where do MY tokens go?" [my_token, dest_rank]
|
|
// - rdma_to_attn_map: inbound - "which remote tokens do I receive?" [src_node, token]
|
|
bool* rdma_to_attn_map;
|
|
|
|
// Attention-to-RDMA map: boolean mask indicating which local tokens need to be
|
|
// SENT via RDMA (inter-node) to each remote node.
|
|
// Only allocated when num_nodes > 1.
|
|
// dtype: bool
|
|
// layout: [max_tokens_per_rank, num_nodes - 1]
|
|
// usage: dispatch N2N (RDMA) warp group
|
|
// lifetime: valid after metadata_preprocessing, constant within iteration
|
|
// vs NCCL HT: closest equivalent to is_token_in_rank for inter-node RDMA.
|
|
// - is_token_in_rank: per-rank granularity [num_tokens, num_ranks]
|
|
// - attn_to_rdma_map: per-node granularity [num_tokens, num_nodes-1] (RDMA only)
|
|
bool* attn_to_rdma_map;
|
|
|
|
// Per-token per-rank bitmask cache produced during preprocessing.
|
|
// dtype: uint8_t
|
|
// layout: [num_nodes * max_tokens_per_rank * ranks_per_node]
|
|
uint8_t* token_rank_mask;
|
|
|
|
// Local expert routing map: per-expert routing for tokens in this rank's buffer.
|
|
// Used by subsequent expert MLP layers to route tokens to correct experts.
|
|
// dtype: bool
|
|
// layout: [max_recv_tokens, experts_per_rank]
|
|
// where max_recv_tokens = num_ranks * max_tokens_per_rank
|
|
// usage: passed to expert computation layers (not used by dispatch/combine directly)
|
|
// lifetime: valid after metadata_preprocessing, constant within iteration
|
|
// vs NCCL HT: similar purpose to num_tokens_per_expert but more detailed
|
|
bool* local_expert_routing_map;
|
|
|
|
// Number of tokens routed to local experts (total across all local experts).
|
|
// Each token counted once even if routed to multiple local experts.
|
|
// dtype: int32_t
|
|
// layout: [1]
|
|
// usage: buffer sizing, iteration control
|
|
// lifetime: valid after metadata_preprocessing
|
|
int32_t* num_tokens_for_experts;
|
|
|
|
// Host-pinned mirror of num_tokens_for_experts (single scalar).
|
|
// Filled once during handle creation on the same stream as preprocessing.
|
|
int32_t* num_tokens_for_experts_host;
|
|
|
|
// =================================================================================
|
|
// CONVERSION BUFFERS - Pre-allocated to avoid dispatch/combine-time malloc
|
|
// =================================================================================
|
|
|
|
// Dense prob buffer: shared scratch for sparse↔dense conversions
|
|
// dtype: float
|
|
// layout: [max_tokens_per_rank, num_experts]
|
|
// usage:
|
|
// - dispatch forward: sparse→dense input topk_weights conversion
|
|
// - combine backward: dense→sparse output prob conversion
|
|
// lifetime: allocated at handle creation, freed at handle destroy
|
|
// note: dispatch and combine are sequential, so one buffer suffices
|
|
// For multi-node: points to group-level pre-registered buffer
|
|
// For single-node: handle-owned buffer
|
|
float* dense_prob_buffer;
|
|
|
|
// Token staging buffer: pre-registered buffer to avoid GIN registration during dispatch
|
|
// User tokens are copied here during dispatch, then this buffer is used for RDMA
|
|
// dtype: uint16_t (bf16) or uint8_t (fp8)
|
|
// layout: [max_tokens_per_rank, hidden]
|
|
// usage: copy user tokens → use for inter-node RDMA
|
|
// lifetime: group-owned (allocated in Group Create, freed in Group Destroy)
|
|
void* token_staging_buffer; // Pointer to group-level buffer (not handle-owned)
|
|
|
|
// Scaling factor staging buffer: pre-registered buffer for FP8 scaling factors
|
|
// User scaling factors are copied here during dispatch, then this buffer is used for RDMA
|
|
// dtype: float
|
|
// layout: [max_tokens_per_rank]
|
|
// usage: copy user scaling factors → use for inter-node RDMA
|
|
// lifetime: group-owned (allocated in Group Create, freed in Group Destroy)
|
|
float* scaling_factor_staging_buffer; // Pointer to group-level buffer (not handle-owned)
|
|
|
|
// RDMA inter-node group flags: atomic completion flags for each remote node.
|
|
// Remote ranks increment via RDMA atomic fetch-add to signal chunk completion.
|
|
// Only allocated when num_nodes > 1.
|
|
// dtype: uint64_t
|
|
// layout: [num_nodes - 1]
|
|
// usage: dispatch N2N warp (signaling), dispatch G2S warp (polling)
|
|
// lifetime: reset to 0 at init, incremented by remote RDMA atomics
|
|
//uint64_t* rdma_inter_node_group_flags;
|
|
|
|
// Per-handle preprocessing block (single allocation for all preprocessing buffers)
|
|
void* preprocessing_block;
|
|
size_t preprocessing_zero_region_size;
|
|
size_t preprocessing_s2d_size;
|
|
void* preprocessing_scan_tmp;
|
|
int32_t* per_expert_counts_tmp;
|
|
} hybridep;
|
|
};
|
|
|
|
ncclEpHandle()
|
|
: group(nullptr),
|
|
topk_idx(nullptr),
|
|
num_tokens(0),
|
|
num_topk(0),
|
|
cached_mode(false),
|
|
num_scales(0),
|
|
hidden_int4(0) {
|
|
constexpr size_t union_size = std::max(sizeof(ll), sizeof(hybridep));
|
|
memset(static_cast<void*>(&ll), 0, union_size);
|
|
}
|
|
|
|
~ncclEpHandle() {
|
|
}
|
|
};
|
|
|
|
static bool tensor_is_contiguous(ncclNDTensor_t tensor) {
|
|
for (unsigned int i = 0; i < tensor->ndim; i++)
|
|
if (tensor->strides[i] != 1)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
// static const ncclNDTensor_t* find_tensor_by_tag(const ncclNDTensor_t* const* tensors, int num_tensors, ncclEpTensorTag_t tag) {
|
|
// for (int i = 0; i < num_tensors; i++) {
|
|
// if (tensors[i]->tag == tag)
|
|
// return tensors[i];
|
|
// }
|
|
// return nullptr;
|
|
// }
|
|
|
|
static ncclNDTensor_t find_tensor_by_tag(const ncclNDTensor_t* tensors, int num_tensors, ncclEpTensorTag_t tag) {
|
|
for (int i = 0; i < num_tensors; i++) {
|
|
if (tensors[i]->tag == tag)
|
|
return tensors[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static bool is_internode_available(ncclEpGroup_t ep_group) {
|
|
// True when there are multiple physical nodes. HT always uses node-based RDMA across nodes,
|
|
// including MNNVL (which has NVLink fabric but is treated as regular multi-node for HT).
|
|
return ep_group->nNodes > 1;
|
|
}
|
|
|
|
|
|
static void tensor_free(ncclEpGroup_t group, ncclNDTensor_t t) {
|
|
if (t == nullptr) return;
|
|
if (t->owns_data && t->data)
|
|
group->free_fn(t->data);
|
|
if (t->strides)
|
|
delete[] t->strides;
|
|
if (t->sizes)
|
|
delete[] t->sizes;
|
|
delete t;
|
|
}
|
|
|
|
ncclResult_t ncclEpCreateHandle(
|
|
ncclEpHandle_t* out_handle,
|
|
ncclEpGroup_t ep_group,
|
|
ncclNDTensor_t topk_idx,
|
|
const ncclNDTensor_t* local_tensors,
|
|
unsigned int num_local_tensors,
|
|
const ncclEpHandleConfig_t* config,
|
|
cudaStream_t stream,
|
|
bool use_fp8
|
|
) {
|
|
assert(topk_idx != nullptr);
|
|
assert(ep_group != nullptr);
|
|
assert(out_handle != nullptr);
|
|
assert(config == nullptr);
|
|
|
|
// Validate communicator
|
|
int nRanks = 0;
|
|
assert(ep_group->comm != nullptr);
|
|
assert(ncclCommCount(ep_group->comm, &nRanks) == ncclSuccess);
|
|
assert(nRanks > 0);
|
|
|
|
// Validate tensor properties
|
|
assert(topk_idx->ndim == 2);
|
|
assert(topk_idx->datatype == ncclInt64);
|
|
assert(topk_idx->tag == NCCL_EP_TENSOR_TAG_TOPK_IDX);
|
|
assert(tensor_is_contiguous(topk_idx));
|
|
|
|
// Validate expert configuration
|
|
assert(ep_group->config.num_experts > 0);
|
|
assert(ep_group->config.num_experts % ep_group->nRanks == 0);
|
|
|
|
// Create and initialize handle
|
|
*out_handle = new ncclEpHandle();
|
|
ncclEpHandle_t handle = *out_handle;
|
|
|
|
handle->group = ep_group;
|
|
handle->use_fp8 = use_fp8;
|
|
handle->topk_idx = topk_idx;
|
|
|
|
// Extract tensor dimensions
|
|
const int num_tokens = static_cast<int>(topk_idx->sizes[0]);
|
|
const int num_topk = static_cast<int>(topk_idx->sizes[1]);
|
|
handle->num_tokens = num_tokens;
|
|
handle->num_topk = num_topk;
|
|
|
|
if (ep_group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
// LL mode does not accept local tensors
|
|
assert(num_local_tensors == 0 && "LL mode does not accept local tensors in ncclEpCreateHandle");
|
|
|
|
// Allocate packed tensors
|
|
// packed_recv_x is the input tensor in the dispatch
|
|
ncclEpTensorCreate(ep_group, &handle->ll.expert_recv_source_indices, 2, ncclInt32, NCCL_EP_TENSOR_TAG_NONE, nullptr, static_cast<unsigned int>(handle->group->num_local_experts), static_cast<unsigned int>(ep_group->nRanks * ep_group->config.max_tokens_per_rank));
|
|
ncclEpTensorCreate(ep_group, &handle->ll.expert_dispatch_layout, 2, ncclInt64, NCCL_EP_TENSOR_TAG_NONE, nullptr, static_cast<unsigned int>(handle->group->num_local_experts), static_cast<unsigned int>(ep_group->nRanks));
|
|
|
|
assert((ep_group->config.max_tokens_per_rank * handle->group->num_local_experts) % 4 == 0 and "TMA requires the number of tokens to be multiple of 4");
|
|
|
|
handle->ll.layout = nccl_ep::LowLatencyLayout(handle->group->rdma_buffer, handle->group->config.max_tokens_per_rank, handle->group->hidden, handle->group->nRanks, handle->group->config.num_experts);
|
|
|
|
assert(handle->ll.layout.total_bytes <= handle->group->config.rdma_buffer_size);
|
|
} else { // HT
|
|
assert(ep_group->config.max_tokens_per_rank > 0 && "HT requires max_tokens_per_rank > 0");
|
|
assert(handle->num_tokens <= static_cast<int>(ep_group->config.max_tokens_per_rank) && "Token count exceeds HT buffer capacity");
|
|
|
|
// Optional: per-expert token counts output
|
|
ncclNDTensor_t recv_expert_counter = nullptr;
|
|
if (num_local_tensors > 0) {
|
|
recv_expert_counter = find_tensor_by_tag(local_tensors, num_local_tensors, NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_HOST);
|
|
if (recv_expert_counter == nullptr) {
|
|
recv_expert_counter = find_tensor_by_tag(local_tensors, num_local_tensors, NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_DEVICE);
|
|
}
|
|
}
|
|
|
|
const int nRanks = ep_group->nRanks;
|
|
const int num_experts = ep_group->config.num_experts;
|
|
const int max_tokens = ep_group->config.max_tokens_per_rank;
|
|
const int n_ranks_per_node = ep_group->lsa_rank_count;
|
|
const int nNodes = ep_group->nNodes;
|
|
const int experts_per_rank = ep_group->num_local_experts;
|
|
|
|
// Allocate preprocessing buffers per-handle (single block allocation)
|
|
{
|
|
auto align256 = [](size_t s) -> size_t { return (s + 255) & ~size_t(255); };
|
|
|
|
const int total_send_tokens = nRanks * max_tokens;
|
|
const int padded_max_tokens = ((max_tokens + 15) / 16) * 16;
|
|
const int num_experts_packed = (num_experts + 7) / 8;
|
|
|
|
size_t sz_routing = align256(static_cast<size_t>(total_send_tokens) * num_experts_packed);
|
|
size_t sz_r2a = align256(static_cast<size_t>(nNodes) * padded_max_tokens * sizeof(bool));
|
|
size_t sz_a2r = (nNodes > 1) ? align256(static_cast<size_t>(max_tokens) * (nNodes - 1) * sizeof(bool)) : 0;
|
|
size_t sz_ler = align256(static_cast<size_t>(ep_group->max_recv_tokens) * experts_per_rank * sizeof(bool));
|
|
size_t sz_ntfe = align256(sizeof(int32_t));
|
|
size_t zero_region = sz_routing + sz_r2a + sz_a2r + sz_ler + sz_ntfe;
|
|
|
|
size_t sz_s2d = align256(static_cast<size_t>(nNodes) * max_tokens * n_ranks_per_node * sizeof(int32_t));
|
|
size_t sz_rank_mask = align256(static_cast<size_t>(nNodes) * max_tokens * n_ranks_per_node * sizeof(uint8_t));
|
|
|
|
size_t sz_scan_tmp = align256(nccl_ep::hybridep::get_preprocessing_scan_tmp_size(n_ranks_per_node));
|
|
size_t sz_prob = !is_internode_available(ep_group) ? align256(static_cast<size_t>(max_tokens) * num_experts * sizeof(float)) : 0;
|
|
size_t sz_per_expert_counts_tmp = align256(static_cast<size_t>(experts_per_rank) * sizeof(int32_t));
|
|
size_t no_memset_region = sz_rank_mask + sz_scan_tmp + sz_prob + sz_per_expert_counts_tmp;
|
|
|
|
size_t total_size = zero_region + sz_s2d + no_memset_region;
|
|
|
|
CUDA_CHECK(ep_group->alloc_fn(&handle->hybridep.preprocessing_block, total_size));
|
|
handle->hybridep.preprocessing_zero_region_size = zero_region;
|
|
handle->hybridep.preprocessing_s2d_size = sz_s2d;
|
|
|
|
char* ptr = static_cast<char*>(handle->hybridep.preprocessing_block);
|
|
size_t offset = 0;
|
|
|
|
handle->hybridep.global_routing_map = reinterpret_cast<uint8_t*>(ptr + offset);
|
|
offset += sz_routing;
|
|
|
|
handle->hybridep.rdma_to_attn_map = reinterpret_cast<bool*>(ptr + offset);
|
|
offset += sz_r2a;
|
|
|
|
handle->hybridep.attn_to_rdma_map = (nNodes > 1) ? reinterpret_cast<bool*>(ptr + offset) : nullptr;
|
|
offset += sz_a2r;
|
|
|
|
handle->hybridep.local_expert_routing_map = reinterpret_cast<bool*>(ptr + offset);
|
|
offset += sz_ler;
|
|
|
|
handle->hybridep.num_tokens_for_experts = reinterpret_cast<int32_t*>(ptr + offset);
|
|
offset += sz_ntfe;
|
|
|
|
handle->hybridep.sparse_to_dense_map = reinterpret_cast<int32_t*>(ptr + offset);
|
|
offset += sz_s2d;
|
|
|
|
handle->hybridep.token_rank_mask = reinterpret_cast<uint8_t*>(ptr + offset);
|
|
offset += sz_rank_mask;
|
|
|
|
handle->hybridep.preprocessing_scan_tmp = reinterpret_cast<void*>(ptr + offset);
|
|
offset += sz_scan_tmp;
|
|
|
|
if (!is_internode_available(ep_group)) {
|
|
// Single physical node: allocate handle-local buffer
|
|
handle->hybridep.dense_prob_buffer = reinterpret_cast<float*>(ptr + offset);
|
|
offset += sz_prob;
|
|
} else {
|
|
handle->hybridep.dense_prob_buffer = nullptr;
|
|
}
|
|
|
|
handle->hybridep.per_expert_counts_tmp = reinterpret_cast<int32_t*>(ptr + offset);
|
|
offset += sz_per_expert_counts_tmp;
|
|
}
|
|
|
|
// For multi-node: dense_prob_buffer is the group-level GIN-registered buffer
|
|
if (is_internode_available(ep_group)) {
|
|
handle->hybridep.dense_prob_buffer = ep_group->ht_buffers.dense_prob_buffer;
|
|
}
|
|
|
|
// Staging buffers (group-level, GIN-registered)
|
|
if (is_internode_available(ep_group)) {
|
|
handle->hybridep.token_staging_buffer = ep_group->ht_buffers.token_staging_buffer;
|
|
handle->hybridep.scaling_factor_staging_buffer = ep_group->ht_buffers.scaling_factor_staging_buffer;
|
|
} else {
|
|
handle->hybridep.token_staging_buffer = nullptr;
|
|
handle->hybridep.scaling_factor_staging_buffer = nullptr;
|
|
}
|
|
|
|
// Cache actual received token count on host once per handle.
|
|
CUDA_CHECK(cudaHostAlloc(
|
|
reinterpret_cast<void**>(&handle->hybridep.num_tokens_for_experts_host),
|
|
sizeof(int32_t),
|
|
cudaHostAllocDefault));
|
|
*handle->hybridep.num_tokens_for_experts_host = -1;
|
|
|
|
// Initialize only the regions touched by this handle's num_tokens.
|
|
// Keep allocation capacities unchanged to preserve buffer layout/contracts.
|
|
const int num_experts_packed = (num_experts + 7) / 8;
|
|
const int padded_num_tokens = ((handle->num_tokens + 15) / 16) * 16; // rdma_to_attn_map uint4 loads
|
|
const size_t routing_send_bytes = static_cast<size_t>(handle->num_tokens) * num_experts_packed;
|
|
const size_t rdma_to_attn_bytes = static_cast<size_t>(nNodes) * padded_num_tokens * sizeof(bool);
|
|
const size_t attn_to_rdma_bytes = (nNodes > 1) ?
|
|
static_cast<size_t>(handle->num_tokens) * (nNodes - 1) * sizeof(bool) : 0;
|
|
const size_t local_expert_routing_bytes =
|
|
static_cast<size_t>(ep_group->max_recv_tokens) * experts_per_rank * sizeof(bool);
|
|
const size_t sparse_to_dense_bytes =
|
|
static_cast<size_t>(nNodes) * handle->num_tokens * n_ranks_per_node * sizeof(int32_t);
|
|
|
|
// convert_topk_to_routing_map uses bitwise-OR into this buffer, so local send rows must be pre-zeroed.
|
|
uint8_t* local_routing_send_ptr =
|
|
handle->hybridep.global_routing_map + (max_tokens * num_experts_packed) * ep_group->rank;
|
|
if (routing_send_bytes > 0) {
|
|
CUDA_CHECK(cudaMemsetAsync(local_routing_send_ptr, 0, routing_send_bytes, stream));
|
|
}
|
|
if (rdma_to_attn_bytes > 0) {
|
|
CUDA_CHECK(cudaMemsetAsync(handle->hybridep.rdma_to_attn_map, 0, rdma_to_attn_bytes, stream));
|
|
}
|
|
if (attn_to_rdma_bytes > 0) {
|
|
CUDA_CHECK(cudaMemsetAsync(handle->hybridep.attn_to_rdma_map, 0, attn_to_rdma_bytes, stream));
|
|
}
|
|
// Keep full clear for local_expert_routing_map to preserve current dispatch output behavior
|
|
// when callers allocate outputs at max receive capacity.
|
|
CUDA_CHECK(cudaMemsetAsync(
|
|
handle->hybridep.local_expert_routing_map, 0, local_expert_routing_bytes, stream));
|
|
CUDA_CHECK(cudaMemsetAsync(
|
|
handle->hybridep.num_tokens_for_experts, 0, sizeof(*handle->hybridep.num_tokens_for_experts), stream));
|
|
if (sparse_to_dense_bytes > 0) {
|
|
CUDA_CHECK(cudaMemsetAsync(
|
|
handle->hybridep.sparse_to_dense_map, 0xFF, sparse_to_dense_bytes, stream));
|
|
}
|
|
|
|
// ===== Step 1: Convert sparse topk_idx to bitmap routing map =====
|
|
nccl_ep::hybridep::convert_topk_to_routing_map(
|
|
static_cast<const int64_t*>(topk_idx->data),
|
|
local_routing_send_ptr,
|
|
handle->num_tokens,
|
|
handle->num_topk,
|
|
num_experts_packed,
|
|
stream);
|
|
|
|
// ===== Step 2: Allgather bitmap routing maps =====
|
|
NCCL_CHECK_RESULT(ncclAllGather(
|
|
local_routing_send_ptr,
|
|
handle->hybridep.global_routing_map,
|
|
static_cast<size_t>(handle->num_tokens) * num_experts_packed,
|
|
ncclUint8,
|
|
ep_group->comm,
|
|
stream));
|
|
|
|
// ===== Step 3: Run metadata_preprocessing =====
|
|
int32_t* per_expert_counts_device = nullptr;
|
|
bool copy_per_expert_counts_to_host = false;
|
|
if (recv_expert_counter != nullptr) {
|
|
if (recv_expert_counter != nullptr) {
|
|
assert(recv_expert_counter->ndim == 1 && "recv_expert_counter must be 1D");
|
|
assert(recv_expert_counter->datatype == ncclInt32 && "recv_expert_counter must be ncclInt32");
|
|
assert(recv_expert_counter->sizes[0] >= static_cast<unsigned int>(ep_group->num_local_experts) &&
|
|
"recv_expert_counter size must be >= num_local_experts");
|
|
assert(recv_expert_counter->data != nullptr && "recv_expert_counter data must not be null");
|
|
}
|
|
|
|
if (recv_expert_counter->tag == NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_HOST) {
|
|
// Fused preprocessing uses atomics; avoid atomics directly into mapped host memory.
|
|
per_expert_counts_device = handle->hybridep.per_expert_counts_tmp;
|
|
copy_per_expert_counts_to_host = true;
|
|
} else {
|
|
per_expert_counts_device = static_cast<int32_t*>(recv_expert_counter->data);
|
|
}
|
|
}
|
|
|
|
nccl_ep::hybridep::call_metadata_preprocessing(
|
|
handle->hybridep.global_routing_map,
|
|
handle->hybridep.sparse_to_dense_map,
|
|
handle->hybridep.rdma_to_attn_map,
|
|
handle->hybridep.attn_to_rdma_map,
|
|
handle->hybridep.token_rank_mask,
|
|
handle->hybridep.num_tokens_for_experts,
|
|
handle->hybridep.local_expert_routing_map,
|
|
per_expert_counts_device,
|
|
handle->hybridep.preprocessing_scan_tmp,
|
|
ep_group->node_id,
|
|
ep_group->rank_in_node,
|
|
handle->num_tokens,
|
|
ep_group->hidden,
|
|
nNodes,
|
|
n_ranks_per_node,
|
|
experts_per_rank,
|
|
stream);
|
|
|
|
if (copy_per_expert_counts_to_host) {
|
|
CUDA_CHECK(cudaMemcpyAsync(
|
|
recv_expert_counter->data,
|
|
per_expert_counts_device,
|
|
static_cast<size_t>(experts_per_rank) * sizeof(int32_t),
|
|
cudaMemcpyDeviceToHost,
|
|
stream));
|
|
}
|
|
|
|
// Mirror the actual recv-token count to host once; getters can read this cached value.
|
|
CUDA_CHECK(cudaMemcpyAsync(
|
|
handle->hybridep.num_tokens_for_experts_host,
|
|
handle->hybridep.num_tokens_for_experts,
|
|
sizeof(*handle->hybridep.num_tokens_for_experts_host),
|
|
cudaMemcpyDeviceToHost,
|
|
stream));
|
|
}
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpHandleDestroy(
|
|
ncclEpHandle_t handle
|
|
) {
|
|
if (!handle)
|
|
return ncclSuccess;
|
|
|
|
if (handle->group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
tensor_free(handle->group, handle->ll.expert_recv_source_indices);
|
|
tensor_free(handle->group, handle->ll.expert_dispatch_layout);
|
|
} else if (handle->group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT) {
|
|
if (handle->hybridep.num_tokens_for_experts_host) {
|
|
CUDA_CHECK(cudaFreeHost(handle->hybridep.num_tokens_for_experts_host));
|
|
handle->hybridep.num_tokens_for_experts_host = nullptr;
|
|
}
|
|
if (handle->hybridep.preprocessing_block) {
|
|
handle->group->free_fn(handle->hybridep.preprocessing_block);
|
|
handle->hybridep.preprocessing_block = nullptr;
|
|
}
|
|
}
|
|
|
|
delete handle;
|
|
return ncclSuccess;
|
|
}
|
|
|
|
// EP Operations
|
|
|
|
ncclResult_t ncclEpDispatch(
|
|
ncclEpHandle_t handle,
|
|
const ncclNDTensor_t* inputs,
|
|
unsigned int num_inputs,
|
|
const ncclNDTensor_t* outputs,
|
|
unsigned int num_outputs,
|
|
const ncclNDTensor_t* local_tensors,
|
|
unsigned int num_local_tensors,
|
|
unsigned int send_only,
|
|
const ncclEpDispatchConfig_t* config,
|
|
cudaStream_t stream
|
|
) {
|
|
ncclEpGroup_t group = handle->group;
|
|
if (group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
ncclNDTensor_t x = find_tensor_by_tag(
|
|
inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOKENS
|
|
);
|
|
assert(x != nullptr);
|
|
assert(x->ndim == 2);
|
|
assert(tensor_is_contiguous(x));
|
|
assert(x->datatype == ncclBfloat16);
|
|
assert(x->sizes[0] == handle->num_tokens);
|
|
assert(x->sizes[0] <= group->config.max_tokens_per_rank);
|
|
assert(x->sizes[1] % sizeof(int4) == 0);
|
|
assert(x->sizes[1] % 128 == 0);
|
|
assert(x->sizes[1] * ncclTypeSize(x->datatype) == group->config.token_size_bytes);
|
|
|
|
// Find and validate output tensors
|
|
ncclNDTensor_t recv_x = find_tensor_by_tag(
|
|
outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOKENS
|
|
);
|
|
ncclNDTensor_t scales = find_tensor_by_tag(
|
|
outputs, num_outputs, NCCL_EP_TENSOR_TAG_SCALES
|
|
);
|
|
assert(recv_x != nullptr);
|
|
assert(recv_x->ndim == 3);
|
|
assert(tensor_is_contiguous(recv_x));
|
|
assert(recv_x->sizes[0] == group->num_local_experts);
|
|
assert(recv_x->sizes[1] == group->nRanks * group->config.max_tokens_per_rank);
|
|
assert(recv_x->sizes[2] == group->hidden);
|
|
|
|
if (scales != nullptr) {
|
|
constexpr int scale_block_size = 128;
|
|
assert(group->hidden % 512 == 0);
|
|
assert(scales->ndim == 3);
|
|
assert(tensor_is_contiguous(scales));
|
|
assert(scales->datatype == ncclFloat32);
|
|
assert(scales->sizes[0] == group->num_local_experts);
|
|
assert(scales->sizes[1] == group->config.max_tokens_per_rank * group->nRanks);
|
|
assert(scales->sizes[2] == group->hidden / scale_block_size);
|
|
}
|
|
|
|
ncclNDTensor_t recv_count = find_tensor_by_tag(
|
|
local_tensors, num_local_tensors, NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_DEVICE
|
|
);
|
|
|
|
assert(recv_count != nullptr);
|
|
assert(recv_count->ndim == 1);
|
|
assert(tensor_is_contiguous(recv_count));
|
|
assert(recv_count->datatype == ncclInt32);
|
|
assert(recv_count->sizes[0] == group->num_local_experts);
|
|
|
|
const auto& buffer = handle->ll.layout.buffers[handle->ll.buffer_idx];
|
|
auto& next_buffer = handle->ll.layout.buffers[handle->ll.buffer_idx ^= 1];
|
|
const auto next_clean_meta = next_buffer.clean_meta();
|
|
|
|
unsigned signal_base = group->num_dispatch_signals;
|
|
auto dispatch_fn = [=](int phases) {
|
|
// Prepare data pointers
|
|
auto* recv_x_data = recv_x->data;
|
|
auto* scales_data = scales ? scales->data : nullptr;
|
|
auto* expert_recv_source_indices_data = static_cast<int*>(handle->ll.expert_recv_source_indices->data);
|
|
auto* expert_dispatch_layout_data = static_cast<int64_t*>(handle->ll.expert_dispatch_layout->data);
|
|
auto* recv_count_data = static_cast<int*>(recv_count->data);
|
|
auto* x_data = x->data;
|
|
auto* topk_idx_data = static_cast<int64_t*>(handle->topk_idx->data);
|
|
|
|
const bool use_fp8 = (scales != nullptr);
|
|
const bool round_scale = config->round_scales;
|
|
const bool use_ue8m0 = false;
|
|
|
|
nccl_ep::internode_ll::dispatch(
|
|
x_data,
|
|
topk_idx_data,
|
|
recv_x_data,
|
|
scales_data,
|
|
expert_recv_source_indices_data,
|
|
expert_dispatch_layout_data,
|
|
recv_count_data,
|
|
buffer.dispatch_rdma_send_buffer,
|
|
buffer.dispatch_rdma_recv_data_buffer,
|
|
buffer.dispatch_rdma_recv_count_buffer,
|
|
(reinterpret_cast<uint64_t>(buffer.dispatch_rdma_send_buffer) - reinterpret_cast<uint64_t>(group->rdma_buffer)),
|
|
(reinterpret_cast<uint64_t>(buffer.dispatch_rdma_recv_data_buffer) - reinterpret_cast<uint64_t>(group->rdma_buffer)),
|
|
(reinterpret_cast<uint64_t>(buffer.dispatch_rdma_recv_count_buffer) - reinterpret_cast<uint64_t>(group->rdma_buffer)),
|
|
next_clean_meta.first,
|
|
next_clean_meta.second,
|
|
nullptr, /*recv_send_sizes=*/
|
|
nullptr, /*recv_send_offsets=*/
|
|
handle->num_tokens,
|
|
group->hidden,
|
|
group->config.max_tokens_per_rank,
|
|
handle->num_topk,
|
|
group->config.num_experts,
|
|
group->rank,
|
|
group->nRanks,
|
|
use_fp8,
|
|
round_scale,
|
|
use_ue8m0,
|
|
phases,
|
|
group->num_nccl_comms,
|
|
group->nccl_dev_comms,
|
|
group->nccl_wins,
|
|
signal_base,
|
|
group->ep_workspace,
|
|
group->device_sm_count,
|
|
stream
|
|
);
|
|
};
|
|
|
|
// Execute dispatch with appropriate phase flags
|
|
const int dispatch_phases = send_only ?
|
|
LOW_LATENCY_SEND_PHASE :
|
|
(LOW_LATENCY_SEND_PHASE | LOW_LATENCY_RECV_PHASE);
|
|
dispatch_fn(dispatch_phases);
|
|
|
|
if (send_only) {
|
|
handle->ll.continue_fn = dispatch_fn;
|
|
}
|
|
} else { // HT
|
|
|
|
bool is_single_node = !is_internode_available(group);
|
|
|
|
assert(num_local_tensors == 0 && "HT dispatch does not accept local_tensors");
|
|
|
|
ncclNDTensor_t x = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOKENS);
|
|
ncclNDTensor_t topk_idx = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOPK_IDX);
|
|
ncclNDTensor_t topk_weights = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS);
|
|
ncclNDTensor_t scales = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_SCALES);
|
|
|
|
assert(x != nullptr);
|
|
assert(x->ndim == 2 && tensor_is_contiguous(x));
|
|
assert(x->sizes[0] == handle->num_tokens);
|
|
assert(x->sizes[0] <= group->config.max_tokens_per_rank);
|
|
assert(x->sizes[1] == group->hidden &&
|
|
"HT dispatch token hidden size must match group configuration");
|
|
|
|
// For multi-node: copy user buffers to pre-registered staging buffers
|
|
// The staging buffers were allocated and GIN-registered during Group Create
|
|
// This avoids ~60ms GIN registration overhead on the dispatch hot path
|
|
void* token_ptr = x->data; // Default: use user buffer directly
|
|
if (!is_single_node && handle->hybridep.token_staging_buffer != nullptr) {
|
|
// Copy user tokens to pre-registered staging buffer (D2D copy is ~0.1ms vs ~30ms GIN registration)
|
|
size_t token_size = x->sizes[0] * x->sizes[1] * ncclTypeSize(x->datatype);
|
|
CUDA_CHECK(cudaMemcpyAsync(handle->hybridep.token_staging_buffer, x->data, token_size, cudaMemcpyDeviceToDevice, stream));
|
|
token_ptr = handle->hybridep.token_staging_buffer;
|
|
}
|
|
// Detect FP8 mode based on datatype
|
|
bool use_fp8 = (x->datatype == ncclFloat8e4m3 || x->datatype == ncclFloat8e5m2);
|
|
|
|
// For FP8: copy user scaling factors to pre-registered staging buffer
|
|
float* scales_ptr = use_fp8 ? static_cast<float*>(scales->data) : nullptr; // Default: use user buffer directly
|
|
if (use_fp8 && !is_single_node && handle->hybridep.scaling_factor_staging_buffer != nullptr) {
|
|
// Copy user scaling factors to pre-registered staging buffer (D2D copy is ~0.1ms vs ~30ms GIN registration)
|
|
size_t scales_size = x->sizes[0] * sizeof(float); // One scale per token
|
|
CUDA_CHECK(cudaMemcpyAsync(handle->hybridep.scaling_factor_staging_buffer, scales->data, scales_size, cudaMemcpyDeviceToDevice, stream));
|
|
scales_ptr = handle->hybridep.scaling_factor_staging_buffer;
|
|
}
|
|
if (!use_fp8) {
|
|
assert(x->datatype == ncclBfloat16);
|
|
} else {
|
|
assert(scales != nullptr && "FP8 tokens require scales input");
|
|
assert(scales->ndim == 2 && tensor_is_contiguous(scales));
|
|
assert(scales->datatype == ncclFloat32);
|
|
}
|
|
|
|
// HT dispatch kernel uses TMA for token/prob/scaling-factor payloads.
|
|
// Keep these constraints at API-entry to fail fast on unsupported shapes.
|
|
const int experts_per_node = group->num_local_experts * group->lsa_rank_count;
|
|
assert((experts_per_node * static_cast<int>(sizeof(float))) % 16 == 0 &&
|
|
"HT dispatch requires experts_per_node to be multiple of 4 (16B prob TMA alignment)");
|
|
|
|
const size_t token_bytes_per_token =
|
|
static_cast<size_t>(group->hidden) * ncclTypeSize(x->datatype);
|
|
assert((token_bytes_per_token % 16) == 0 &&
|
|
"HT dispatch requires token bytes per token to be 16B aligned for TMA");
|
|
|
|
if (use_fp8) {
|
|
assert((group->hidden % 128) == 0 &&
|
|
"HT dispatch FP8 requires hidden_dim multiple of 128");
|
|
assert((((group->hidden / 128) * static_cast<int>(sizeof(float))) % 16) == 0 &&
|
|
"HT dispatch FP8 requires scaling-factor bytes per token to be 16B aligned");
|
|
}
|
|
|
|
// Output tensors
|
|
ncclNDTensor_t recv_topk_weights = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS);
|
|
ncclNDTensor_t recv_topk_idx = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOPK_IDX);
|
|
|
|
// Detect forward/backward mode
|
|
bool forward_dispatch = (topk_idx != nullptr);
|
|
|
|
// Validate topk inputs
|
|
if (forward_dispatch) {
|
|
assert(topk_weights != nullptr);
|
|
assert(topk_idx->ndim == 2 && tensor_is_contiguous(topk_idx) && topk_idx->datatype == ncclInt64);
|
|
assert(topk_weights->ndim == 2 && tensor_is_contiguous(topk_weights) && topk_weights->datatype == ncclFloat32);
|
|
assert(topk_weights->sizes[0] == handle->num_tokens);
|
|
assert(topk_weights->sizes[1] == handle->num_topk);
|
|
} else {
|
|
assert(topk_weights == nullptr);
|
|
assert(recv_topk_weights == nullptr);
|
|
assert(recv_topk_idx == nullptr);
|
|
}
|
|
|
|
/* ===== Convert sparse topk_weights → dense prob (sparse→dense format) ===== */
|
|
// NCCL HT uses sparse format: topk_idx[token][k] = expert_id, topk_weights[token][k] = weight
|
|
// HT uses dense format: prob[token][expert] = weight (0 if not routed)
|
|
float* dense_prob = handle->hybridep.dense_prob_buffer;
|
|
if (forward_dispatch) {
|
|
size_t dense_prob_size = static_cast<size_t>(handle->num_tokens) * group->config.num_experts * sizeof(float);
|
|
CUDA_CHECK(cudaMemsetAsync(dense_prob, 0, dense_prob_size, stream));
|
|
|
|
nccl_ep::hybridep::sparse_to_dense_prob(
|
|
static_cast<const int64_t*>(topk_idx->data),
|
|
static_cast<const float*>(topk_weights->data),
|
|
dense_prob,
|
|
handle->num_tokens,
|
|
handle->num_topk,
|
|
group->config.num_experts,
|
|
stream);
|
|
}
|
|
|
|
/* ===== Build DispatchParams ===== */
|
|
// DispatchParams encapsulates all buffers and metadata needed by HT dispatch kernel:
|
|
// - Input buffers: attn_input_token, attn_input_prob, attn_input_scaling_factor
|
|
// - IPC output buffers: expert_output_token_ptrs, expert_output_prob_ptrs (per-rank pointers)
|
|
// - RDMA staging buffers: rdma_inter_node_group_* (for multi-node only)
|
|
// - Metadata: sparse_to_dense_map, rdma_to_attn_map, attn_to_rdma_map
|
|
// - Sync flags: expected_*_flag_value, intra_node_write_completion_flags
|
|
nccl_ep::hybridep::DispatchParams params;
|
|
params.hidden_dim = group->hidden;
|
|
params.experts_per_rank = group->num_local_experts;
|
|
params.num_ranks_per_node = group->lsa_rank_count;
|
|
params.attn_input_token = token_ptr;
|
|
params.attn_input_prob = forward_dispatch ? dense_prob : nullptr;
|
|
params.attn_input_scaling_factor = use_fp8 ? static_cast<const float*>(scales_ptr) : nullptr;
|
|
// Use HOST pointer arrays - these get copied into the kernel param struct for fast __grid_constant__ access
|
|
params.expert_output_token_ptrs = group->ht_buffers.dispatch_expert_output_token_buffer_ptrs;
|
|
params.expert_output_prob_ptrs = group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs;
|
|
params.expert_output_scaling_factor_ptrs = use_fp8 ? group->ht_buffers.dispatch_expert_output_scaling_factor_buffer_ptrs : nullptr;
|
|
params.rdma_to_attn_map = handle->hybridep.rdma_to_attn_map;
|
|
params.attn_to_rdma_map = handle->hybridep.attn_to_rdma_map;
|
|
params.sparse_to_dense_map = handle->hybridep.sparse_to_dense_map;
|
|
group->ht_buffers.host_dispatch_expected_rdma += 1;
|
|
group->ht_buffers.host_dispatch_expected_intra += group->lsa_rank_count;
|
|
params.expected_rdma_flag_value = is_single_node ? 0 : group->ht_buffers.host_dispatch_expected_rdma;
|
|
params.rdma_inter_node_group_flags = is_single_node ? nullptr : group->ht_buffers.rdma_inter_node_group_flags;
|
|
params.expected_intra_node_flag_value = group->ht_buffers.host_dispatch_expected_intra;
|
|
params.intra_node_write_completion_flags = group->ht_buffers.intra_node_write_completion_flags;
|
|
params.dispatch_grid_barrier_counter = group->ht_buffers.dispatch_grid_barrier_counter;
|
|
// Pass device communicators and windows
|
|
params.dcomms = is_single_node ? nullptr : group->gin_config.d_dcomms;
|
|
params.nccl_window = is_single_node ? ncclWindow_t{} : group->gin_config.nccl_window;
|
|
params.num_gin_comms = is_single_node ? 0 : group->gin_config.num_comms;
|
|
params.num_ctx_per_comm = is_single_node ? 0 : group->gin_config.num_ctx_per_comm;
|
|
params.gin_base_ptr = is_single_node ? nullptr : group->gin_config.gin_base_ptr;
|
|
params.signals_base = group->gin_config.signals_base;
|
|
// Use offsets relative to gin_base_ptr
|
|
// All buffers are part of one large registered window
|
|
// Calculate bytes_per_entry for batched staging
|
|
size_t bytes_per_token_entry = group->hidden * sizeof(uint16_t); // token data
|
|
size_t bytes_per_prob_entry = (group->num_local_experts * group->lsa_rank_count) * sizeof(float); // prob data
|
|
size_t bytes_per_sf_entry = (group->hidden / 128) * sizeof(float); // scaling factor (FP8)
|
|
size_t bytes_per_entry = bytes_per_token_entry + bytes_per_prob_entry + bytes_per_sf_entry;
|
|
|
|
params.mr_info = {
|
|
.attn_input_token_offset = is_single_node ? 0 : group->gin_config.token_staging_offset,
|
|
.attn_input_prob_offset = is_single_node ? 0 : group->gin_config.dense_prob_offset,
|
|
.attn_input_scaling_factor_offset = is_single_node ? 0 : group->gin_config.scaling_factor_staging_offset,
|
|
// Batched staging parameters (packed layout)
|
|
.rdma_send_staging_offset = is_single_node ? 0 : group->gin_config.rdma_send_staging_offset,
|
|
.rdma_inter_node_group_packed_offset = is_single_node ? 0 : group->gin_config.rdma_inter_node_group_packed_offset,
|
|
.bytes_per_entry = bytes_per_entry,
|
|
.max_tokens_per_dest = static_cast<size_t>(group->config.max_tokens_per_rank),
|
|
// Streaming signal parameters
|
|
.signals_tail_base = is_single_node ? 0 : static_cast<unsigned>(group->gin_config.signals_tail_base),
|
|
.num_max_rdma_chunked_send_tokens = is_single_node ? 0 : group->gin_config.num_max_rdma_chunked_send_tokens,
|
|
};
|
|
params.local_rank = group->rank_in_node;
|
|
params.node_rank = group->node_id;
|
|
params.num_tokens_per_rank = handle->num_tokens;
|
|
|
|
// Call dispatch kernel
|
|
nccl_ep::hybridep::call_dispatch(
|
|
params,
|
|
group->config.max_tokens_per_rank,
|
|
group->nNodes,
|
|
use_fp8,
|
|
forward_dispatch,
|
|
stream
|
|
);
|
|
|
|
/* ===== Copy IPC staging → caller outputs ===== */
|
|
// HT kernel writes to IPC-mapped buffers (dispatch_expert_output_*_buffer_ptrs)
|
|
// Copy results to user-provided output tensors
|
|
ncclNDTensor_t recv_x = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOKENS);
|
|
if (recv_x != nullptr) {
|
|
assert(recv_x->ndim == 2 && tensor_is_contiguous(recv_x));
|
|
size_t copy_size = static_cast<size_t>(recv_x->sizes[0]) * recv_x->sizes[1] * ncclTypeSize(recv_x->datatype);
|
|
|
|
CUDA_CHECK(cudaMemcpyAsync(recv_x->data,
|
|
group->ht_buffers.dispatch_expert_output_token_buffer_ptrs[group->rank_in_node],
|
|
copy_size,
|
|
cudaMemcpyDeviceToDevice,
|
|
stream));
|
|
}
|
|
|
|
/* ===== Convert dense output → sparse format ===== */
|
|
if (forward_dispatch) {
|
|
// Convert outputs - both recv_topk_weights and recv_topk_idx must be provided together
|
|
if (recv_topk_weights != nullptr && recv_topk_idx != nullptr) {
|
|
// Validate formats for both outputs
|
|
assert(recv_topk_weights->ndim == 2 && tensor_is_contiguous(recv_topk_weights));
|
|
assert(recv_topk_weights->datatype == ncclFloat32);
|
|
assert(recv_topk_idx->ndim == 2 && tensor_is_contiguous(recv_topk_idx));
|
|
assert(recv_topk_idx->datatype == ncclInt64);
|
|
assert(recv_topk_weights->sizes[0] == recv_topk_idx->sizes[0]);
|
|
|
|
int num_recv_tokens = static_cast<int>(recv_topk_weights->sizes[0]);
|
|
int experts_per_node = group->num_local_experts * group->lsa_rank_count;
|
|
|
|
nccl_ep::hybridep::dense_to_sparse_prob(
|
|
group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs[group->rank_in_node],
|
|
handle->hybridep.local_expert_routing_map,
|
|
static_cast<float*>(recv_topk_weights->data),
|
|
static_cast<int64_t*>(recv_topk_idx->data),
|
|
num_recv_tokens,
|
|
handle->num_topk,
|
|
group->num_local_experts,
|
|
experts_per_node,
|
|
group->rank_in_node,
|
|
stream);
|
|
} else {
|
|
// Both outputs must be provided together or neither
|
|
assert(recv_topk_weights == nullptr && recv_topk_idx == nullptr);
|
|
}
|
|
}
|
|
|
|
// Copy FP8 scales output
|
|
if (use_fp8) {
|
|
ncclNDTensor_t recv_scales = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_SCALES);
|
|
if (recv_scales != nullptr) {
|
|
assert(recv_scales->ndim == 2 && tensor_is_contiguous(recv_scales));
|
|
size_t copy_size = static_cast<size_t>(recv_scales->sizes[0]) * recv_scales->sizes[1] * ncclTypeSize(recv_scales->datatype);
|
|
|
|
CUDA_CHECK(cudaMemcpyAsync(recv_scales->data,
|
|
group->ht_buffers.dispatch_expert_output_scaling_factor_buffer_ptrs[group->rank_in_node],
|
|
copy_size,
|
|
cudaMemcpyDeviceToDevice,
|
|
stream));
|
|
}
|
|
}
|
|
}
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpCombine(
|
|
ncclEpHandle_t handle,
|
|
const ncclNDTensor_t* inputs,
|
|
unsigned int num_inputs,
|
|
const ncclNDTensor_t* outputs,
|
|
unsigned int num_outputs,
|
|
const ncclNDTensor_t* local_tensors,
|
|
unsigned int num_local_tensors,
|
|
unsigned int send_only,
|
|
const ncclEpCombineConfig_t* config,
|
|
cudaStream_t stream
|
|
) {
|
|
if (handle->group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
// Find and validate input tensors
|
|
ncclNDTensor_t x = find_tensor_by_tag(
|
|
inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOKENS
|
|
);
|
|
assert(x != nullptr);
|
|
|
|
ncclNDTensor_t topk_idx = handle->topk_idx;
|
|
ncclNDTensor_t src_info = handle->ll.expert_recv_source_indices;
|
|
ncclNDTensor_t layout_range = handle->ll.expert_dispatch_layout;
|
|
|
|
// Find and validate local tensors
|
|
ncclNDTensor_t topk_weights = find_tensor_by_tag(
|
|
local_tensors, num_local_tensors, NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS
|
|
);
|
|
assert(topk_weights != nullptr);
|
|
|
|
// Extract configuration values
|
|
const int num_experts = handle->group->config.num_experts;
|
|
const int num_ranks = handle->group->nRanks;
|
|
const int num_max_dispatch_tokens_per_rank = handle->group->config.max_tokens_per_rank;
|
|
|
|
// Validate input tensor x
|
|
assert(x->ndim == 3);
|
|
assert(tensor_is_contiguous(x));
|
|
assert(x->datatype == ncclBfloat16);
|
|
assert(x->sizes[0] == num_experts / num_ranks);
|
|
assert(x->sizes[1] == num_ranks * num_max_dispatch_tokens_per_rank);
|
|
assert(x->sizes[2] % sizeof(int4) == 0);
|
|
assert(x->sizes[2] % 128 == 0);
|
|
|
|
// Validate topk_idx tensor
|
|
assert(topk_idx->ndim == 2);
|
|
assert(tensor_is_contiguous(topk_idx));
|
|
assert(topk_idx->datatype == ncclInt64);
|
|
|
|
// Validate src_info tensor
|
|
assert(src_info->ndim == 2);
|
|
assert(tensor_is_contiguous(src_info));
|
|
assert(src_info->datatype == ncclInt32);
|
|
assert(x->sizes[0] == src_info->sizes[0]);
|
|
|
|
// Validate topk_weights tensor
|
|
assert(topk_weights->ndim == 2);
|
|
assert(tensor_is_contiguous(topk_weights));
|
|
assert(topk_weights->sizes[0] == topk_idx->sizes[0]);
|
|
assert(topk_weights->sizes[1] == topk_idx->sizes[1]);
|
|
assert(topk_weights->datatype == ncclFloat32);
|
|
assert(topk_weights->sizes[0] <= num_max_dispatch_tokens_per_rank);
|
|
|
|
// Extract dimensions
|
|
const int hidden = static_cast<int>(x->sizes[2]);
|
|
const int num_topk = static_cast<int>(topk_weights->sizes[1]);
|
|
const int num_combined_tokens = static_cast<int>(topk_weights->sizes[0]);
|
|
|
|
// Manage double-buffering
|
|
const auto& buffer = handle->ll.layout.buffers[handle->ll.buffer_idx];
|
|
auto& next_buffer = handle->ll.layout.buffers[handle->ll.buffer_idx ^= 1];
|
|
const auto next_clean_meta = next_buffer.clean_meta();
|
|
|
|
// Validate buffer layout
|
|
assert(handle->ll.layout.total_bytes <= handle->group->config.rdma_buffer_size);
|
|
|
|
// Find and validate output tensor
|
|
ncclNDTensor_t out = find_tensor_by_tag(
|
|
outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOKENS
|
|
);
|
|
|
|
assert(out != nullptr);
|
|
assert(out->ndim == 2);
|
|
assert(tensor_is_contiguous(out));
|
|
assert(out->sizes[0] == num_combined_tokens);
|
|
assert(out->sizes[1] == hidden);
|
|
assert(out->datatype == x->datatype);
|
|
|
|
// Define combine lambda
|
|
unsigned signal_base = handle->ll.buffer_idx * (handle->group->num_dispatch_signals / 2);
|
|
auto combine_fn = [=](int phases) {
|
|
// Prepare data pointers
|
|
auto* out_data = out->data;
|
|
auto* x_data = x->data;
|
|
auto* topk_idx_data = static_cast<int64_t*>(topk_idx->data);
|
|
auto* topk_weights_data = static_cast<float*>(topk_weights->data);
|
|
auto* src_info_data = static_cast<int*>(src_info->data);
|
|
auto* layout_range_data = static_cast<int64_t*>(layout_range->data);
|
|
|
|
const bool use_fp8 = false;
|
|
const bool zero_copy = false;
|
|
|
|
nccl_ep::internode_ll::combine(
|
|
x_data,
|
|
src_info_data,
|
|
layout_range_data,
|
|
topk_idx_data,
|
|
topk_weights_data,
|
|
out_data,
|
|
buffer.combine_rdma_send_buffer,
|
|
buffer.combine_rdma_recv_data_buffer,
|
|
buffer.combine_rdma_recv_flag_buffer,
|
|
(reinterpret_cast<uint64_t>(buffer.combine_rdma_send_buffer) - reinterpret_cast<uint64_t>(handle->group->rdma_buffer)),
|
|
(reinterpret_cast<uint64_t>(buffer.combine_rdma_recv_data_buffer) - reinterpret_cast<uint64_t>(handle->group->rdma_buffer)),
|
|
(reinterpret_cast<uint64_t>(buffer.combine_rdma_recv_flag_buffer) - reinterpret_cast<uint64_t>(handle->group->rdma_buffer)),
|
|
next_clean_meta.first,
|
|
next_clean_meta.second,
|
|
nullptr, /*recv_topk_idx=*/
|
|
num_combined_tokens,
|
|
hidden,
|
|
num_max_dispatch_tokens_per_rank,
|
|
num_topk,
|
|
num_experts,
|
|
handle->group->rank,
|
|
handle->group->nRanks,
|
|
use_fp8,
|
|
phases,
|
|
zero_copy,
|
|
handle->group->num_nccl_comms,
|
|
handle->group->nccl_dev_comms,
|
|
handle->group->nccl_wins,
|
|
signal_base,
|
|
handle->group->ep_workspace,
|
|
handle->group->device_sm_count,
|
|
stream
|
|
);
|
|
};
|
|
|
|
// Execute combine with appropriate phase flags
|
|
const int combine_phases = send_only ?
|
|
LOW_LATENCY_SEND_PHASE : (LOW_LATENCY_SEND_PHASE | LOW_LATENCY_RECV_PHASE);
|
|
combine_fn(combine_phases);
|
|
|
|
if (send_only) {
|
|
handle->ll.continue_fn = combine_fn;
|
|
}
|
|
} else if (handle->group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT) {
|
|
// ===== HT mode =====
|
|
// Combine: gather expert outputs back to original token positions
|
|
// Forward combine: just tokens (inference)
|
|
// Backward combine: tokens + gradients (training, topk_weights provided)
|
|
ncclEpGroup_t group = handle->group;
|
|
bool is_single_node = !is_internode_available(group);
|
|
//assert(is_single_node && "HT mode only supports single-node");
|
|
|
|
/* ===== Inputs validation ===== */
|
|
ncclNDTensor_t x = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOKENS);
|
|
assert(x != nullptr);
|
|
assert(x->ndim == 2 && tensor_is_contiguous(x));
|
|
assert(x->datatype == ncclBfloat16); // HT combine only supports BF16
|
|
|
|
// Get dimensions from input tensor
|
|
auto num_tokens = static_cast<int>(x->sizes[0]);
|
|
auto hidden = static_cast<int>(x->sizes[1]);
|
|
|
|
// Validate int4 alignment for TMA
|
|
assert((hidden * ncclTypeSize(x->datatype)) % sizeof(int4) == 0);
|
|
|
|
// Number of tokens to combine back (original token count from this rank)
|
|
auto num_combined_tokens = handle->num_tokens;
|
|
|
|
// Top-k checks (for backward mode)
|
|
int num_topk = 0;
|
|
ncclNDTensor_t topk_weights = find_tensor_by_tag(inputs, num_inputs, NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS);
|
|
ncclNDTensor_t combined_topk_weights = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS);
|
|
|
|
// Determine if this is backward mode (topk_weights provided = backward combine)
|
|
bool backward_combine = (topk_weights != nullptr);
|
|
|
|
if (backward_combine) {
|
|
num_topk = static_cast<int>(topk_weights->sizes[1]);
|
|
|
|
assert(combined_topk_weights != nullptr);
|
|
assert(combined_topk_weights->ndim == 2 && tensor_is_contiguous(combined_topk_weights));
|
|
assert(combined_topk_weights->sizes[0] == num_combined_tokens);
|
|
assert(combined_topk_weights->sizes[1] == num_topk);
|
|
assert(combined_topk_weights->datatype == ncclFloat32);
|
|
}
|
|
|
|
// HT combine doesn't use local tensors, so hybridep should not use local tensors either
|
|
assert(num_local_tensors == 0);
|
|
|
|
/* ===== Output tensors ===== */
|
|
ncclNDTensor_t combined_x = find_tensor_by_tag(outputs, num_outputs, NCCL_EP_TENSOR_TAG_TOKENS);
|
|
assert(combined_x != nullptr);
|
|
assert(combined_x->ndim == 2 && tensor_is_contiguous(combined_x));
|
|
assert(combined_x->sizes[0] == num_combined_tokens); // Output should match original token count
|
|
assert(combined_x->sizes[1] == hidden); // Should match input hidden dimension
|
|
|
|
/* ===== Copy input to IPC staging buffers ===== */
|
|
// Expert MLP output needs to be in IPC buffer so other ranks can read it
|
|
size_t token_copy_size = static_cast<size_t>(num_tokens) * hidden * sizeof(uint16_t); // BF16 = uint16_t
|
|
CUDA_CHECK(cudaMemcpyAsync(
|
|
group->ht_buffers.expert_input_token,
|
|
x->data,
|
|
token_copy_size,
|
|
cudaMemcpyDeviceToDevice,
|
|
stream));
|
|
|
|
/* ===== Convert sparse topk_weights to dense prob for backward combine ===== */
|
|
// For backward combine, convert sparse input weights to dense format for HT kernel
|
|
if (backward_combine) {
|
|
int experts_per_node = group->num_local_experts * group->lsa_rank_count;
|
|
size_t dense_prob_size = static_cast<size_t>(num_tokens) * experts_per_node * sizeof(float);
|
|
|
|
// Zero-initialize the dense prob buffer before scattering
|
|
CUDA_CHECK(cudaMemsetAsync(
|
|
group->ht_buffers.combine_expert_input_prob_buffer_ptrs[group->rank_in_node],
|
|
0, dense_prob_size, stream));
|
|
|
|
// Convert sparse [num_tokens, topk] to dense [num_tokens, experts_per_node]
|
|
// Uses local_expert_routing_map to determine expert positions (matches dispatch output order)
|
|
nccl_ep::hybridep::sparse_to_dense_prob_combine(
|
|
static_cast<const float*>(topk_weights->data),
|
|
handle->hybridep.local_expert_routing_map,
|
|
group->ht_buffers.combine_expert_input_prob_buffer_ptrs[group->rank_in_node],
|
|
num_tokens,
|
|
num_topk,
|
|
group->num_local_experts, // experts_per_rank
|
|
experts_per_node,
|
|
group->rank_in_node,
|
|
stream);
|
|
}
|
|
|
|
// Use pre-allocated dense prob buffer for backward combine
|
|
float* dense_output_prob = handle->hybridep.dense_prob_buffer;
|
|
if (backward_combine) {
|
|
size_t dense_output_prob_size = static_cast<size_t>(num_combined_tokens) * group->config.num_experts * sizeof(float);
|
|
CUDA_CHECK(cudaMemsetAsync(dense_output_prob, 0, dense_output_prob_size, stream));
|
|
}
|
|
|
|
/* ===== Build CombineParams ===== */
|
|
// CombineParams encapsulates all buffers and metadata needed by HT combine kernel:
|
|
// - IPC input buffers: expert_input_token_ptrs, expert_input_prob_ptrs (per-rank pointers)
|
|
// - Output buffers: attn_output_token, attn_output_prob (user-provided)
|
|
// - RDMA buffers: rdma_intra_node_red_*, combine_rdma_inter_node_group_* (for multi-node)
|
|
// - Metadata: sparse_to_dense_map, rdma_to_attn_map, attn_to_rdma_map, local_expert_routing_map
|
|
// - Sync flags: combine_expected_*_flag_value, combine_intra_node_write_completion_flags
|
|
nccl_ep::hybridep::CombineParams params;
|
|
params.hidden_dim = group->hidden;
|
|
params.experts_per_rank = group->num_local_experts;
|
|
params.num_ranks_per_node = group->lsa_rank_count;
|
|
// Use HOST pointer arrays - these get copied into the kernel param struct for fast __grid_constant__ access
|
|
params.expert_input_token_ptrs = group->ht_buffers.combine_expert_input_token_buffer_ptrs;
|
|
params.expert_input_prob_ptrs = backward_combine ? group->ht_buffers.combine_expert_input_prob_buffer_ptrs : nullptr;
|
|
params.attn_output_token = combined_x->data;
|
|
params.attn_output_prob = backward_combine ? dense_output_prob : nullptr;
|
|
params.rdma_intra_node_red_token = is_single_node ? nullptr : group->ht_buffers.rdma_intra_node_red_token;
|
|
params.rdma_intra_node_red_prob = (!is_single_node && backward_combine) ? group->ht_buffers.rdma_intra_node_red_prob : nullptr;
|
|
params.combine_rdma_inter_node_group_token = is_single_node ? nullptr : group->ht_buffers.combine_rdma_inter_node_group_token;
|
|
params.combine_rdma_inter_node_group_prob = (!is_single_node && backward_combine) ? group->ht_buffers.combine_rdma_inter_node_group_prob : nullptr;
|
|
params.sparse_to_dense_map = handle->hybridep.sparse_to_dense_map;
|
|
params.rdma_to_attn_map = handle->hybridep.rdma_to_attn_map;
|
|
params.attn_to_rdma_map = handle->hybridep.attn_to_rdma_map;
|
|
params.local_expert_routing_map = handle->hybridep.local_expert_routing_map;
|
|
group->ht_buffers.host_combine_expected_rdma += 1;
|
|
group->ht_buffers.host_combine_expected_intra += group->lsa_rank_count;
|
|
params.combine_expected_rdma_flag_value = is_single_node ? 0 : group->ht_buffers.host_combine_expected_rdma;
|
|
params.combine_rdma_inter_node_group_flags = is_single_node ? nullptr : group->ht_buffers.combine_rdma_inter_node_group_flags;
|
|
params.combine_expected_intra_node_flag_value = group->ht_buffers.host_combine_expected_intra;
|
|
params.combine_intra_node_write_completion_flags = group->ht_buffers.combine_intra_node_write_completion_flags;
|
|
// Pass device communicators and windows
|
|
params.dcomms = is_single_node ? nullptr : group->gin_config.d_dcomms;
|
|
params.nccl_window = is_single_node ? ncclWindow_t{} : group->gin_config.nccl_window;
|
|
params.num_gin_comms = is_single_node ? 0 : group->gin_config.num_comms;
|
|
params.num_ctx_per_comm = is_single_node ? 0 : group->gin_config.num_ctx_per_comm;
|
|
params.gin_base_ptr = is_single_node ? nullptr : group->gin_config.gin_base_ptr;
|
|
params.signals_base = group->gin_config.signals_base;
|
|
params.combine_signal_offset = group->gin_config.combine_signal_offset;
|
|
// Use offsets relative to gin_base_ptr
|
|
params.mr_info = {
|
|
.rdma_intra_node_red_token_offset = is_single_node ? 0 : group->gin_config.rdma_intra_node_red_token_offset,
|
|
.combine_rdma_inter_node_group_token_offset = is_single_node ? 0 : group->gin_config.combine_rdma_inter_node_group_token_offset,
|
|
.rdma_intra_node_red_prob_offset = is_single_node ? 0 : group->gin_config.rdma_intra_node_red_prob_offset,
|
|
.combine_rdma_inter_node_group_prob_offset = is_single_node ? 0 : group->gin_config.combine_rdma_inter_node_group_prob_offset,
|
|
};
|
|
params.local_rank = group->rank_in_node;
|
|
params.node_rank = group->node_id;
|
|
params.num_tokens_per_rank = num_combined_tokens;
|
|
params.num_recv_tokens = num_tokens;
|
|
|
|
/* ===== Call combine kernel ===== */
|
|
nccl_ep::hybridep::call_combine(
|
|
params,
|
|
group->config.max_tokens_per_rank, // max_tokens_per_rank
|
|
group->nNodes, // num_nodes (RDMA domain size)
|
|
backward_combine, // backward mode flag
|
|
stream
|
|
);
|
|
|
|
/* ===== Convert dense output prob to sparse format ===== */
|
|
// For backward combine, convert kernel's dense output to sparse format
|
|
// HT outputs dense [num_tokens, num_experts], NCCL expects sparse [num_tokens, topk]
|
|
if (backward_combine && combined_topk_weights != nullptr) {
|
|
nccl_ep::hybridep::dense_to_sparse_prob_combine(
|
|
dense_output_prob,
|
|
handle->hybridep.global_routing_map + (num_combined_tokens * ((group->config.num_experts + 7) / 8))*group->rank,
|
|
static_cast<float*>(combined_topk_weights->data),
|
|
nullptr, // No need to output topk_idx for combine
|
|
num_combined_tokens,
|
|
num_topk,
|
|
group->config.num_experts,
|
|
stream);
|
|
}
|
|
|
|
handle->cached_mode = true;
|
|
}
|
|
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpComplete(
|
|
ncclEpHandle_t handle,
|
|
const ncclEpCompleteConfig_t* config,
|
|
cudaStream_t stream
|
|
) {
|
|
if (handle->group->config.algorithm == NCCL_EP_ALGO_LOW_LATENCY) {
|
|
if (handle->ll.continue_fn) {
|
|
handle->ll.continue_fn(LOW_LATENCY_RECV_PHASE);
|
|
handle->ll.continue_fn = nullptr;
|
|
}
|
|
} else if (handle->group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT) {
|
|
// HT mode - no continue needed (synchronous)
|
|
}
|
|
return ncclSuccess;
|
|
}
|
|
|
|
ncclResult_t ncclEpHandleGetNumRecvTokens(
|
|
ncclEpHandle_t handle,
|
|
unsigned int* num_recv_tokens
|
|
) {
|
|
if (handle->group->config.algorithm == NCCL_EP_ALGO_HIGH_THROUGHPUT) {
|
|
int32_t actual_recv_tokens =
|
|
(handle->hybridep.num_tokens_for_experts_host != nullptr) ?
|
|
*handle->hybridep.num_tokens_for_experts_host : -1;
|
|
|
|
// Fallback for callers that query before stream synchronization.
|
|
if (actual_recv_tokens < 0) {
|
|
CUDA_CHECK(cudaMemcpy(
|
|
&actual_recv_tokens,
|
|
handle->hybridep.num_tokens_for_experts,
|
|
sizeof(actual_recv_tokens),
|
|
cudaMemcpyDeviceToHost));
|
|
if (handle->hybridep.num_tokens_for_experts_host != nullptr) {
|
|
*handle->hybridep.num_tokens_for_experts_host = actual_recv_tokens;
|
|
}
|
|
}
|
|
|
|
assert(actual_recv_tokens >= 0);
|
|
assert(actual_recv_tokens <= handle->group->max_recv_tokens);
|
|
*num_recv_tokens = static_cast<unsigned int>(actual_recv_tokens);
|
|
} else { // LL
|
|
return ncclInvalidUsage;
|
|
}
|
|
return ncclSuccess;
|
|
}
|