/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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(d_buffer) + rank * element_size, static_cast(host_buffer) + rank * element_size, element_size, cudaMemcpyHostToDevice)); NCCL_CHECK_RESULT(ncclAllGather( static_cast(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(workspace); } else { CUDA_CHECK(cudaMalloc(reinterpret_cast(&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 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(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 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(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(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 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(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(max_recv_tokens) * hidden * sizeof(uint16_t); size_t expert_output_prob_sz = static_cast(max_recv_tokens) * num_local_experts * gpus_per_node * sizeof(float); size_t expert_input_token_sz = static_cast(max_recv_tokens) * hidden * sizeof(uint16_t); size_t expert_input_prob_sz = static_cast(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(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(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( 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( 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(ep_group->ht_buffers.host_ptr_block); ep_group->ht_buffers.dispatch_expert_output_token_buffer_ptrs = reinterpret_cast(hptr); hptr += sizeof(void*) * gpus_per_node; ep_group->ht_buffers.dispatch_expert_output_prob_buffer_ptrs = reinterpret_cast(hptr); hptr += sizeof(float*) * gpus_per_node; ep_group->ht_buffers.combine_expert_input_token_buffer_ptrs = reinterpret_cast(hptr); hptr += sizeof(uint16_t*) * gpus_per_node; ep_group->ht_buffers.combine_expert_input_prob_buffer_ptrs = reinterpret_cast(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(&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(&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 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(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(pb + ep_group->ht_buffers.ipc_dispatch_prob_offset); ep_group->ht_buffers.combine_expert_input_token_buffer_ptrs[i] = reinterpret_cast(pb + ep_group->ht_buffers.ipc_combine_token_offset); ep_group->ht_buffers.combine_expert_input_prob_buffer_ptrs[i] = reinterpret_cast(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(ptr); ep_group->ht_buffers.combine_intra_node_write_completion_flags = static_cast(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(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(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(nNodes) * sizeof(uint64_t), GIN_ALIGNMENT); size_t token_staging_sz = align_size(static_cast(ep_group->config.max_tokens_per_rank) * ep_group->hidden * sizeof(uint16_t), GIN_ALIGNMENT); size_t dense_prob_sz = align_size(static_cast(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(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(nNodes - 1) * ep_group->config.max_tokens_per_rank * bytes_per_entry, GIN_ALIGNMENT); size_t rdma_recv_packed_sz = align_size(static_cast(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(ep_group->gin_config.gin_base_ptr); size_t offset = 0; ep_group->ht_buffers.rdma_intra_node_red_token = reinterpret_cast(ptr + offset); offset += rdma_intra_node_red_token_sz; ep_group->ht_buffers.combine_rdma_inter_node_group_token = reinterpret_cast(ptr + offset); offset += combine_rdma_inter_node_group_token_sz; ep_group->ht_buffers.rdma_intra_node_red_prob = reinterpret_cast(ptr + offset); offset += rdma_intra_node_red_prob_sz; ep_group->ht_buffers.combine_rdma_inter_node_group_prob = reinterpret_cast(ptr + offset); offset += combine_rdma_inter_node_group_prob_sz; ep_group->ht_buffers.rdma_inter_node_group_flags = reinterpret_cast(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(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(ptr + offset); offset += token_staging_sz; ep_group->ht_buffers.dense_prob_buffer = reinterpret_cast(ptr + offset); offset += dense_prob_sz; ep_group->ht_buffers.scaling_factor_staging_buffer = reinterpret_cast(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(&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 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 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(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(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(&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(&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 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(&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(topk_idx->sizes[0]); const int num_topk = static_cast(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(handle->group->num_local_experts), static_cast(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(handle->group->num_local_experts), static_cast(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(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(total_send_tokens) * num_experts_packed); size_t sz_r2a = align256(static_cast(nNodes) * padded_max_tokens * sizeof(bool)); size_t sz_a2r = (nNodes > 1) ? align256(static_cast(max_tokens) * (nNodes - 1) * sizeof(bool)) : 0; size_t sz_ler = align256(static_cast(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(nNodes) * max_tokens * n_ranks_per_node * sizeof(int32_t)); size_t sz_rank_mask = align256(static_cast(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(max_tokens) * num_experts * sizeof(float)) : 0; size_t sz_per_expert_counts_tmp = align256(static_cast(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(handle->hybridep.preprocessing_block); size_t offset = 0; handle->hybridep.global_routing_map = reinterpret_cast(ptr + offset); offset += sz_routing; handle->hybridep.rdma_to_attn_map = reinterpret_cast(ptr + offset); offset += sz_r2a; handle->hybridep.attn_to_rdma_map = (nNodes > 1) ? reinterpret_cast(ptr + offset) : nullptr; offset += sz_a2r; handle->hybridep.local_expert_routing_map = reinterpret_cast(ptr + offset); offset += sz_ler; handle->hybridep.num_tokens_for_experts = reinterpret_cast(ptr + offset); offset += sz_ntfe; handle->hybridep.sparse_to_dense_map = reinterpret_cast(ptr + offset); offset += sz_s2d; handle->hybridep.token_rank_mask = reinterpret_cast(ptr + offset); offset += sz_rank_mask; handle->hybridep.preprocessing_scan_tmp = reinterpret_cast(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(ptr + offset); offset += sz_prob; } else { handle->hybridep.dense_prob_buffer = nullptr; } handle->hybridep.per_expert_counts_tmp = reinterpret_cast(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(&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(handle->num_tokens) * num_experts_packed; const size_t rdma_to_attn_bytes = static_cast(nNodes) * padded_num_tokens * sizeof(bool); const size_t attn_to_rdma_bytes = (nNodes > 1) ? static_cast(handle->num_tokens) * (nNodes - 1) * sizeof(bool) : 0; const size_t local_expert_routing_bytes = static_cast(ep_group->max_recv_tokens) * experts_per_rank * sizeof(bool); const size_t sparse_to_dense_bytes = static_cast(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(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(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(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(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(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(handle->ll.expert_recv_source_indices->data); auto* expert_dispatch_layout_data = static_cast(handle->ll.expert_dispatch_layout->data); auto* recv_count_data = static_cast(recv_count->data); auto* x_data = x->data; auto* topk_idx_data = static_cast(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(buffer.dispatch_rdma_send_buffer) - reinterpret_cast(group->rdma_buffer)), (reinterpret_cast(buffer.dispatch_rdma_recv_data_buffer) - reinterpret_cast(group->rdma_buffer)), (reinterpret_cast(buffer.dispatch_rdma_recv_count_buffer) - reinterpret_cast(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(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(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(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(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(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(topk_idx->data), static_cast(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(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(group->config.max_tokens_per_rank), // Streaming signal parameters .signals_tail_base = is_single_node ? 0 : static_cast(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(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(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(recv_topk_weights->data), static_cast(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(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(x->sizes[2]); const int num_topk = static_cast(topk_weights->sizes[1]); const int num_combined_tokens = static_cast(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(topk_idx->data); auto* topk_weights_data = static_cast(topk_weights->data); auto* src_info_data = static_cast(src_info->data); auto* layout_range_data = static_cast(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(buffer.combine_rdma_send_buffer) - reinterpret_cast(handle->group->rdma_buffer)), (reinterpret_cast(buffer.combine_rdma_recv_data_buffer) - reinterpret_cast(handle->group->rdma_buffer)), (reinterpret_cast(buffer.combine_rdma_recv_flag_buffer) - reinterpret_cast(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(x->sizes[0]); auto hidden = static_cast(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(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(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(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(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(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(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(actual_recv_tokens); } else { // LL return ncclInvalidUsage; } return ncclSuccess; }