CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
869 lines
35 KiB
Python
869 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# See LICENSE.txt for more license information.
|
|
|
|
"""Python EP test — replicates ep_test.cu using the nccl_wrapper.py API.
|
|
|
|
Usage:
|
|
mpirun -np <N> python ep_test.py [OPTIONS]
|
|
|
|
Options (identical to ep_test.cu):
|
|
-a {ll,ht} Algorithm mode (default: ll)
|
|
-m Disable max_tokens_per_rank (HT only, not yet supported)
|
|
-s {none,dispatch,combine,both} Send-only mode (default: none)
|
|
-c Enable cached mode (HT only)
|
|
-r Enable random mode
|
|
-t NUM Number of tokens (default: 50)
|
|
-d NUM Hidden dimension size (default: 7168)
|
|
"""
|
|
|
|
import argparse
|
|
import ctypes
|
|
import os
|
|
import random
|
|
import struct
|
|
import sys
|
|
|
|
from mpi4py import MPI
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "python"))
|
|
|
|
from nccl_ep.nccl_wrapper import (
|
|
NCCLLibrary,
|
|
ncclDataTypeEnum,
|
|
ncclEpAlgorithm_t,
|
|
ncclEpAllocFn_t,
|
|
ncclEpDispatchConfig_t,
|
|
ncclEpFreeFn_t,
|
|
ncclEpGroupConfig_t,
|
|
ncclEpTensorTag_t,
|
|
ncclNDTensor_t,
|
|
ncclUniqueId,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CUDA runtime helpers (ctypes bindings for the small subset we need)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_cuda_rt = ctypes.CDLL("libcudart.so")
|
|
|
|
# Declare argtypes/restype for every CUDA runtime function we use.
|
|
# Without these, ctypes defaults to passing Python ints as 32-bit C ints,
|
|
# which silently truncates 64-bit device pointers — especially fatal
|
|
# inside CFUNCTYPE callbacks (the allocator/free callbacks).
|
|
_cuda_rt.cudaSetDevice.argtypes = [ctypes.c_int]
|
|
_cuda_rt.cudaSetDevice.restype = ctypes.c_int
|
|
_cuda_rt.cudaStreamCreate.argtypes = [ctypes.POINTER(ctypes.c_void_p)]
|
|
_cuda_rt.cudaStreamCreate.restype = ctypes.c_int
|
|
_cuda_rt.cudaMalloc.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_size_t]
|
|
_cuda_rt.cudaMalloc.restype = ctypes.c_int
|
|
_cuda_rt.cudaFree.argtypes = [ctypes.c_void_p]
|
|
_cuda_rt.cudaFree.restype = ctypes.c_int
|
|
_cuda_rt.cudaMemcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int]
|
|
_cuda_rt.cudaMemcpy.restype = ctypes.c_int
|
|
_cuda_rt.cudaStreamSynchronize.argtypes = [ctypes.c_void_p]
|
|
_cuda_rt.cudaStreamSynchronize.restype = ctypes.c_int
|
|
_cuda_rt.cudaHostAlloc.argtypes = [ctypes.POINTER(ctypes.c_void_p), ctypes.c_size_t, ctypes.c_uint]
|
|
_cuda_rt.cudaHostAlloc.restype = ctypes.c_int
|
|
_cuda_rt.cudaFreeHost.argtypes = [ctypes.c_void_p]
|
|
_cuda_rt.cudaFreeHost.restype = ctypes.c_int
|
|
_cuda_rt.cudaDeviceReset.argtypes = []
|
|
_cuda_rt.cudaDeviceReset.restype = ctypes.c_int
|
|
|
|
CUDA_MEMCPY_H2D = 1
|
|
CUDA_MEMCPY_D2H = 2
|
|
CUDA_MEMCPY_D2D = 3
|
|
CUDA_HOST_ALLOC_MAPPED = 2
|
|
NCCL_EP_AUTO = 0
|
|
|
|
|
|
def _cuda_check(ret, name="CUDA"):
|
|
if ret != 0:
|
|
raise RuntimeError(f"{name} failed with error code {ret}")
|
|
|
|
|
|
def cuda_set_device(dev):
|
|
_cuda_check(_cuda_rt.cudaSetDevice(dev), "cudaSetDevice")
|
|
|
|
|
|
def cuda_stream_create():
|
|
s = ctypes.c_void_p()
|
|
_cuda_check(_cuda_rt.cudaStreamCreate(ctypes.byref(s)), "cudaStreamCreate")
|
|
return s
|
|
|
|
|
|
def cuda_malloc(size):
|
|
ptr = ctypes.c_void_p()
|
|
_cuda_check(_cuda_rt.cudaMalloc(ctypes.byref(ptr), size), "cudaMalloc")
|
|
return ptr
|
|
|
|
|
|
def cuda_free(ptr):
|
|
_cuda_check(_cuda_rt.cudaFree(ptr), "cudaFree")
|
|
|
|
|
|
def cuda_memcpy(dst, src, size, kind):
|
|
_cuda_check(_cuda_rt.cudaMemcpy(dst, src, size, kind), "cudaMemcpy")
|
|
|
|
|
|
def cuda_stream_synchronize(stream):
|
|
_cuda_check(_cuda_rt.cudaStreamSynchronize(stream), "cudaStreamSynchronize")
|
|
|
|
|
|
def cuda_host_alloc(size):
|
|
ptr = ctypes.c_void_p()
|
|
_cuda_check(_cuda_rt.cudaHostAlloc(ctypes.byref(ptr), size, CUDA_HOST_ALLOC_MAPPED), "cudaHostAlloc")
|
|
return ptr
|
|
|
|
|
|
def cuda_free_host(ptr):
|
|
_cuda_check(_cuda_rt.cudaFreeHost(ptr), "cudaFreeHost")
|
|
|
|
|
|
def cuda_device_reset():
|
|
_cuda_check(_cuda_rt.cudaDeviceReset(), "cudaDeviceReset")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tensor helpers (ncclEpTensor* not exposed as high-level wrapper methods)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def tensor_create(nccl, ep_group, ndim, datatype, tag, data, *sizes):
|
|
tensor = ncclNDTensor_t()
|
|
padded = list(sizes) + [1] * (5 - len(sizes))
|
|
nccl.NCCL_CHECK(nccl._funcs["ncclEpTensorCreate"](
|
|
ep_group, ctypes.byref(tensor),
|
|
ctypes.c_uint(ndim), ctypes.c_int(datatype), ctypes.c_int(tag),
|
|
data,
|
|
ctypes.c_uint(padded[0]), ctypes.c_uint(padded[1]),
|
|
ctypes.c_uint(padded[2]), ctypes.c_uint(padded[3]),
|
|
ctypes.c_uint(padded[4]),
|
|
))
|
|
return tensor
|
|
|
|
|
|
def tensor_destroy(nccl, ep_group, tensor):
|
|
nccl.NCCL_CHECK(nccl._funcs["ncclEpTensorDestroy"](ep_group, tensor))
|
|
|
|
|
|
def tensor_get_data(nccl, tensor):
|
|
data = ctypes.c_void_p()
|
|
nccl.NCCL_CHECK(nccl._funcs["ncclEpTensorGetData"](tensor, ctypes.byref(data)))
|
|
return data
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# bfloat16 conversion (matches the C++ float_to_bf16 exactly)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def float_to_bf16(f):
|
|
x = struct.unpack("I", struct.pack("f", f))[0]
|
|
rounding_bias = 0x00007FFF + ((x >> 16) & 1)
|
|
return ((x + rounding_bias) >> 16) & 0xFFFF
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Custom allocator callbacks (matching torchMalloc / torchFree in ep_test.cu)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@ncclEpAllocFn_t
|
|
def _alloc_fn(ptr, size):
|
|
return _cuda_rt.cudaMalloc(ptr, size)
|
|
|
|
|
|
@ncclEpFreeFn_t
|
|
def _free_fn(ptr):
|
|
# ptr arrives as a Python int from the CFUNCTYPE c_void_p parameter.
|
|
# With argtypes set on cudaFree, ctypes auto-converts to c_void_p.
|
|
return _cuda_rt.cudaFree(ptr)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DJB2 hostname hash (matches getHostHash in ep_test.cu)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _host_hash(name):
|
|
h = 5381
|
|
for c in name:
|
|
h = ((h << 5) + h + ord(c)) & 0xFFFFFFFFFFFFFFFF
|
|
return h
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main test
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main(): # noqa: C901 — intentionally kept as a single function to mirror ep_test.cu
|
|
mpi_comm = MPI.COMM_WORLD
|
|
my_rank = mpi_comm.Get_rank()
|
|
n_ranks = mpi_comm.Get_size()
|
|
|
|
# -- argument parsing (same flags as the C++ test) ----------------------
|
|
parser = argparse.ArgumentParser(description="EP Test (Python)")
|
|
parser.add_argument("-a", choices=["ll", "ht"], default="ll", help="Algorithm mode")
|
|
parser.add_argument("-m", action="store_true", help="Disable max_tokens_per_rank (HT only)")
|
|
parser.add_argument("-s", choices=["none", "dispatch", "combine", "both"], default="none",
|
|
help="Send-only mode")
|
|
parser.add_argument("-c", action="store_true", help="Enable cached mode (HT only)")
|
|
parser.add_argument("-r", action="store_true", help="Enable random mode")
|
|
parser.add_argument("-t", type=int, default=50, help="Number of tokens")
|
|
parser.add_argument("-d", type=int, default=7168, help="Hidden dimension size")
|
|
args = parser.parse_args()
|
|
|
|
algorithm = (ncclEpAlgorithm_t.NCCL_EP_ALGO_LOW_LATENCY
|
|
if args.a == "ll" else ncclEpAlgorithm_t.NCCL_EP_ALGO_HIGH_THROUGHPUT)
|
|
disable_max_tokens = args.m
|
|
dispatch_send_only = args.s in ("dispatch", "both")
|
|
combine_send_only = args.s in ("combine", "both")
|
|
cached_mode = args.c
|
|
random_mode = args.r
|
|
num_tokens = args.t
|
|
hidden = args.d
|
|
|
|
if n_ranks not in (2, 4) and n_ranks % 8 != 0:
|
|
if my_rank == 0:
|
|
print("Error: nRanks must be 2, 4 or multiple of 8 for this test")
|
|
sys.exit(1)
|
|
|
|
if disable_max_tokens:
|
|
if my_rank == 0:
|
|
if algorithm != ncclEpAlgorithm_t.NCCL_EP_ALGO_HIGH_THROUGHPUT:
|
|
print("Error: -m is only applicable to HT mode (-a ht)")
|
|
else:
|
|
print("Error: -m (NCCL_EP_AUTO for max_tokens_per_rank) is not yet supported.\n"
|
|
" This feature will be available in a future release for HT mode.")
|
|
sys.exit(1)
|
|
|
|
# -- derived parameters -------------------------------------------------
|
|
ELEMENTS_TESTED_PER_TOKEN = 10
|
|
top_k = min(8, n_ranks)
|
|
num_experts = min(256, top_k * n_ranks)
|
|
num_local_experts = num_experts // n_ranks
|
|
local_experts_start = num_local_experts * my_rank
|
|
local_experts_end = local_experts_start + num_local_experts
|
|
|
|
if num_experts % n_ranks != 0:
|
|
if my_rank == 0:
|
|
print(f"Error: num_experts ({num_experts}) must be divisible by nRanks ({n_ranks})")
|
|
sys.exit(1)
|
|
if top_k > num_local_experts:
|
|
if my_rank == 0:
|
|
print(f"Error: top_k ({top_k}) must be <= num_local_experts ({num_local_experts})")
|
|
sys.exit(1)
|
|
|
|
# -- calculate localRank via hostname hash (same as C++ test) -----------
|
|
hostname = MPI.Get_processor_name().split(".")[0]
|
|
my_hash = _host_hash(hostname)
|
|
all_hashes = mpi_comm.allgather(my_hash)
|
|
local_rank = sum(1 for p in range(my_rank) if all_hashes[p] == my_hash)
|
|
|
|
# -- CUDA setup ---------------------------------------------------------
|
|
cuda_set_device(local_rank)
|
|
stream = cuda_stream_create()
|
|
|
|
# -- NCCL setup ---------------------------------------------------------
|
|
nccl = NCCLLibrary()
|
|
|
|
if my_rank == 0:
|
|
unique_id = nccl.ncclGetUniqueId()
|
|
else:
|
|
unique_id = ncclUniqueId()
|
|
|
|
id_bytes = bytearray(unique_id.internal)
|
|
id_bytes = mpi_comm.bcast(id_bytes, root=0)
|
|
for i in range(128):
|
|
unique_id.internal[i] = id_bytes[i]
|
|
|
|
comm = nccl.ncclCommInitRank(n_ranks, unique_id, my_rank)
|
|
|
|
# -- EP group -----------------------------------------------------------
|
|
config = ncclEpGroupConfig_t()
|
|
config.version = 1
|
|
config.algorithm = algorithm
|
|
config.num_experts = num_experts
|
|
config.max_tokens_per_rank = NCCL_EP_AUTO if disable_max_tokens else num_tokens
|
|
config.token_size_bytes = hidden * 2 # bfloat16
|
|
config.rdma_buffer_size = NCCL_EP_AUTO
|
|
config.num_qp_per_rank = NCCL_EP_AUTO
|
|
config.num_channels = NCCL_EP_AUTO
|
|
|
|
algorithm_name = "LOW_LATENCY" if algorithm == ncclEpAlgorithm_t.NCCL_EP_ALGO_LOW_LATENCY else "HIGH_THROUGHPUT"
|
|
extra = " (no max_tokens_per_rank)" if disable_max_tokens else ""
|
|
print(f"Rank {my_rank}: Testing ncclEpCreateGroup with algorithm: {algorithm_name}{extra}")
|
|
|
|
ep_group = nccl.ncclEpCreateGroup(comm, config, stream, _alloc_fn, _free_fn)
|
|
|
|
# -- topk_idx tensor [num_tokens, top_k] int64 --------------------------
|
|
topk_idx = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclInt64,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_IDX,
|
|
None, num_tokens, top_k,
|
|
)
|
|
|
|
topk_idx_host = (ctypes.c_int64 * (num_tokens * top_k))()
|
|
if random_mode:
|
|
random.seed(my_rank + 42)
|
|
for i in range(num_tokens):
|
|
first_expert = random.randint(0, num_experts - 1)
|
|
topk_idx_host[i * top_k] = first_expert
|
|
for j in range(1, top_k):
|
|
topk_idx_host[i * top_k + j] = (first_expert + j) % num_experts
|
|
if my_rank == 0:
|
|
print("Random mode enabled: first expert random, rest deterministic (no repetitions)")
|
|
else:
|
|
for i in range(num_tokens):
|
|
for j in range(top_k):
|
|
topk_idx_host[i * top_k + j] = (local_experts_end + j) % num_experts
|
|
|
|
topk_idx_data = tensor_get_data(nccl, topk_idx)
|
|
cuda_memcpy(topk_idx_data, topk_idx_host, num_tokens * top_k * 8, CUDA_MEMCPY_H2D)
|
|
|
|
# -- recv_expert_counter for handle (only when disable_max_tokens) ------
|
|
handle_local_tensors = []
|
|
handle_recv_expert_counter = None
|
|
recv_counter_host_ptr = None
|
|
if disable_max_tokens:
|
|
recv_counter_host_ptr = cuda_host_alloc(num_local_experts * 4)
|
|
handle_recv_expert_counter = tensor_create(
|
|
nccl, ep_group, 1, ncclDataTypeEnum.ncclInt32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_HOST,
|
|
recv_counter_host_ptr, num_local_experts,
|
|
)
|
|
handle_local_tensors.append(handle_recv_expert_counter)
|
|
|
|
# -- EP handle ----------------------------------------------------------
|
|
print(f"Rank {my_rank}: Testing ncclEpCreateHandle")
|
|
ep_handle = nccl.ncclEpCreateHandle(
|
|
ep_group, topk_idx, None, stream,
|
|
local_tensors=handle_local_tensors if handle_local_tensors else None,
|
|
)
|
|
cuda_stream_synchronize(stream)
|
|
|
|
if disable_max_tokens:
|
|
num_recv_tokens = nccl.ncclEpHandleGetNumRecvTokens(ep_handle)
|
|
else:
|
|
num_recv_tokens = config.max_tokens_per_rank * num_local_experts
|
|
assert num_recv_tokens > 0
|
|
|
|
# -- dispatch config ----------------------------------------------------
|
|
dispatch_config = ncclEpDispatchConfig_t()
|
|
dispatch_config.round_scales = 0
|
|
|
|
is_ll = algorithm == ncclEpAlgorithm_t.NCCL_EP_ALGO_LOW_LATENCY
|
|
num_inputs = 1 if is_ll else 3
|
|
num_outputs = 1 if is_ll else 3
|
|
num_local = 1 if is_ll else 0
|
|
|
|
# -- input/output tensors for dispatch ----------------------------------
|
|
input_tokens = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_tokens, hidden,
|
|
)
|
|
|
|
topk_weights = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclFloat32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS,
|
|
None, num_tokens, top_k,
|
|
)
|
|
|
|
if is_ll:
|
|
output_tokens = tensor_create(
|
|
nccl, ep_group, 3, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_local_experts, config.max_tokens_per_rank * n_ranks, hidden,
|
|
)
|
|
else:
|
|
output_tokens = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_recv_tokens, hidden,
|
|
)
|
|
|
|
local_tensor_recv_count = None
|
|
if is_ll:
|
|
local_tensor_recv_count = tensor_create(
|
|
nccl, ep_group, 1, ncclDataTypeEnum.ncclInt32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_RECV_EXPERT_COUNTER_DEVICE,
|
|
None, num_local_experts,
|
|
)
|
|
|
|
output_topk_weights = None
|
|
output_topk_idx = None
|
|
if not is_ll:
|
|
output_topk_weights = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclFloat32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS,
|
|
None, num_recv_tokens, top_k,
|
|
)
|
|
output_topk_idx = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclInt64,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_IDX,
|
|
None, num_recv_tokens, top_k,
|
|
)
|
|
|
|
# -- fill input tokens: first ELEMENTS_TESTED_PER_TOKEN elems = 0x1000 + rank
|
|
input_host = (ctypes.c_uint16 * (num_tokens * hidden))()
|
|
for i in range(num_tokens):
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
input_host[i * hidden + j] = 0x1000 + my_rank
|
|
input0_data = tensor_get_data(nccl, input_tokens)
|
|
cuda_memcpy(input0_data, input_host, num_tokens * hidden * 2, CUDA_MEMCPY_H2D)
|
|
|
|
# -- fill topk_weights: 1.0 / top_k
|
|
tw_host = (ctypes.c_float * (num_tokens * top_k))()
|
|
for i in range(num_tokens * top_k):
|
|
tw_host[i] = 1.0 / top_k
|
|
tw_data = tensor_get_data(nccl, topk_weights)
|
|
cuda_memcpy(tw_data, tw_host, num_tokens * top_k * 4, CUDA_MEMCPY_H2D)
|
|
|
|
# -- build dispatch tensor arrays ----------------------------------------
|
|
inputs_arr = (ncclNDTensor_t * num_inputs)()
|
|
inputs_arr[0] = input_tokens
|
|
if not is_ll:
|
|
inputs_arr[1] = topk_weights
|
|
inputs_arr[2] = topk_idx
|
|
|
|
outputs_arr = (ncclNDTensor_t * num_outputs)()
|
|
outputs_arr[0] = output_tokens
|
|
if not is_ll:
|
|
outputs_arr[1] = output_topk_weights
|
|
outputs_arr[2] = output_topk_idx
|
|
|
|
local_arr = (ncclNDTensor_t * max(num_local, 1))()
|
|
if is_ll:
|
|
local_arr[0] = local_tensor_recv_count
|
|
|
|
# -- dispatch -----------------------------------------------------------
|
|
print(f"Rank {my_rank}: Testing ncclEpDispatch (send_only={'true' if dispatch_send_only else 'false'})")
|
|
nccl.ncclEpDispatch(
|
|
ep_handle,
|
|
ctypes.cast(inputs_arr, ctypes.POINTER(ncclNDTensor_t)), num_inputs,
|
|
ctypes.cast(outputs_arr, ctypes.POINTER(ncclNDTensor_t)), num_outputs,
|
|
ctypes.cast(local_arr, ctypes.POINTER(ncclNDTensor_t)) if num_local > 0 else None,
|
|
num_local,
|
|
int(dispatch_send_only),
|
|
dispatch_config,
|
|
stream,
|
|
)
|
|
|
|
print(f"Rank {my_rank}: Testing ncclEpComplete")
|
|
nccl.ncclEpComplete(ep_handle, None, stream)
|
|
cuda_stream_synchronize(stream)
|
|
|
|
# -- read recv_count ----------------------------------------------------
|
|
recv_count_host = None
|
|
should_free_recv_count = False
|
|
if is_ll:
|
|
recv_count_host = (ctypes.c_int * num_local_experts)()
|
|
lt0_data = tensor_get_data(nccl, local_tensor_recv_count)
|
|
cuda_memcpy(recv_count_host, lt0_data, num_local_experts * 4, CUDA_MEMCPY_D2H)
|
|
should_free_recv_count = True
|
|
elif disable_max_tokens and handle_recv_expert_counter is not None:
|
|
hlt0_data = tensor_get_data(nccl, handle_recv_expert_counter)
|
|
recv_count_host = ctypes.cast(hlt0_data, ctypes.POINTER(ctypes.c_int))
|
|
|
|
recv_from_expert_start = (local_experts_start + num_experts - num_local_experts) % num_experts
|
|
recv_rank = recv_from_expert_start // num_local_experts
|
|
|
|
# -- verify dispatch output ---------------------------------------------
|
|
dispatch_check_passed = True
|
|
first_dispatch_output0_host = None
|
|
|
|
if not random_mode and is_ll and recv_count_host is not None:
|
|
total_elems = num_local_experts * config.max_tokens_per_rank * n_ranks * hidden
|
|
output_host = (ctypes.c_uint16 * total_elems)()
|
|
out0_data = tensor_get_data(nccl, output_tokens)
|
|
cuda_memcpy(output_host, out0_data, total_elems * 2, CUDA_MEMCPY_D2H)
|
|
|
|
for e in range(num_local_experts):
|
|
expected_count = num_tokens
|
|
if recv_count_host[e] != expected_count:
|
|
print(f"Recv_count check failed! Rank {my_rank}, expert {e}: "
|
|
f"expected {expected_count}, got {recv_count_host[e]}")
|
|
dispatch_check_passed = False
|
|
break
|
|
max_t = config.max_tokens_per_rank * n_ranks
|
|
for t in range(min(recv_count_host[e], max_t)):
|
|
token_off = (e * max_t + t) * hidden
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
expected = 0x1000 + recv_rank
|
|
actual = output_host[token_off + j]
|
|
if actual != expected:
|
|
print(f"Dispatch data check failed! Rank {my_rank}, expert {e}, "
|
|
f"token {t}, element {j}: expected {expected}, got {actual}")
|
|
dispatch_check_passed = False
|
|
break
|
|
if not dispatch_check_passed:
|
|
break
|
|
if not dispatch_check_passed:
|
|
break
|
|
|
|
elif not random_mode:
|
|
if recv_count_host is not None:
|
|
for e in range(num_local_experts):
|
|
expected_count = num_tokens
|
|
if recv_count_host[e] != expected_count:
|
|
print(f"Recv_count check failed! Rank {my_rank}, expert {e}: "
|
|
f"expected {expected_count}, got {recv_count_host[e]}")
|
|
dispatch_check_passed = False
|
|
break
|
|
|
|
output_host = (ctypes.c_uint16 * (num_recv_tokens * hidden))()
|
|
out0_data = tensor_get_data(nccl, output_tokens)
|
|
cuda_memcpy(output_host, out0_data, num_recv_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
check_count = num_recv_tokens if disable_max_tokens else num_tokens
|
|
for i in range(check_count):
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
expected = 0x1000 + recv_rank
|
|
actual = output_host[i * hidden + j]
|
|
if actual != expected:
|
|
print(f"Dispatch check failed! Rank {my_rank}, token {i}, "
|
|
f"element {j}: expected {expected}, got {actual}")
|
|
dispatch_check_passed = False
|
|
break
|
|
if not dispatch_check_passed:
|
|
break
|
|
first_dispatch_output0_host = output_host
|
|
|
|
# recv_count cleanup (matches C++ should_free_recv_count logic)
|
|
del should_free_recv_count
|
|
|
|
# -- verify HT recv_topk_weights / recv_topk_idx -----------------------
|
|
if not random_mode and not is_ll:
|
|
recv_tw = (ctypes.c_float * (num_recv_tokens * top_k))()
|
|
out1_data = tensor_get_data(nccl, output_topk_weights)
|
|
cuda_memcpy(recv_tw, out1_data, num_recv_tokens * top_k * 4, CUDA_MEMCPY_D2H)
|
|
|
|
recv_ti = (ctypes.c_int64 * (num_recv_tokens * top_k))()
|
|
out2_data = tensor_get_data(nccl, output_topk_idx)
|
|
cuda_memcpy(recv_ti, out2_data, num_recv_tokens * top_k * 8, CUDA_MEMCPY_D2H)
|
|
|
|
ht_outputs_valid = True
|
|
print(f"Rank {my_rank}: Verifying recv_topk_weights and recv_topk_idx")
|
|
expected_weight = 1.0 / top_k
|
|
weight_errors = 0
|
|
idx_errors = 0
|
|
|
|
for i in range(num_tokens):
|
|
for j in range(top_k):
|
|
off = i * top_k + j
|
|
if recv_tw[off] != expected_weight:
|
|
if weight_errors < 5:
|
|
print(f"Rank {my_rank}: recv_topk_weights[{i}][{j}] = {recv_tw[off]}, "
|
|
f"expected {expected_weight}")
|
|
weight_errors += 1
|
|
ht_outputs_valid = False
|
|
idx_val = recv_ti[off]
|
|
if idx_val < 0 or idx_val >= num_experts:
|
|
if idx_errors < 5:
|
|
print(f"Rank {my_rank}: recv_topk_idx[{i}][{j}] = {idx_val}, "
|
|
f"expected range [0, {num_experts})")
|
|
idx_errors += 1
|
|
ht_outputs_valid = False
|
|
|
|
if weight_errors > 0:
|
|
print(f"Rank {my_rank}: recv_topk_weights verification failed with {weight_errors} errors")
|
|
if idx_errors > 0:
|
|
print(f"Rank {my_rank}: recv_topk_idx verification failed with {idx_errors} errors")
|
|
if ht_outputs_valid:
|
|
print(f"Rank {my_rank}: {algorithm_name} mode recv_topk_weights and recv_topk_idx verification passed")
|
|
else:
|
|
dispatch_check_passed = False
|
|
|
|
if random_mode:
|
|
print(f"Rank {my_rank}: {algorithm_name} Dispatch flow completed (random mode, checks skipped)")
|
|
elif dispatch_check_passed:
|
|
print(f"Rank {my_rank}: {algorithm_name} Dispatch flow passed successfully")
|
|
else:
|
|
print(f"Rank {my_rank}: Exiting test due to dispatch failure")
|
|
sys.exit(1)
|
|
|
|
# ===================================================================
|
|
# Combine phase
|
|
# ===================================================================
|
|
print(f"Rank {my_rank}: Testing {algorithm_name} Combine flow")
|
|
|
|
if is_ll:
|
|
expert_outputs = tensor_create(
|
|
nccl, ep_group, 3, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_local_experts, config.max_tokens_per_rank * n_ranks, hidden,
|
|
)
|
|
eo_host = (ctypes.c_uint16 * (config.max_tokens_per_rank * hidden))()
|
|
for t in range(config.max_tokens_per_rank):
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
eo_host[t * hidden + j] = float_to_bf16(float((j + 1) * 2))
|
|
eo_data = tensor_get_data(nccl, expert_outputs)
|
|
for e in range(num_local_experts):
|
|
offset_bytes = e * config.max_tokens_per_rank * hidden * n_ranks * 2
|
|
dst = ctypes.c_void_p(eo_data.value + offset_bytes)
|
|
cuda_memcpy(dst, eo_host, config.max_tokens_per_rank * hidden * 2, CUDA_MEMCPY_H2D)
|
|
else:
|
|
expert_outputs = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_recv_tokens, hidden,
|
|
)
|
|
eo_host = (ctypes.c_uint16 * (num_recv_tokens * hidden))()
|
|
for t in range(num_recv_tokens):
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
eo_host[t * hidden + j] = float_to_bf16(float((j + 1) * 2))
|
|
eo_data = tensor_get_data(nccl, expert_outputs)
|
|
cuda_memcpy(eo_data, eo_host, num_recv_tokens * hidden * 2, CUDA_MEMCPY_H2D)
|
|
|
|
combined_output = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_tokens, hidden,
|
|
)
|
|
|
|
combine_in = (ncclNDTensor_t * 1)()
|
|
combine_in[0] = expert_outputs
|
|
|
|
combine_out = (ncclNDTensor_t * 1)()
|
|
combine_out[0] = combined_output
|
|
|
|
combine_local = (ncclNDTensor_t * 1)()
|
|
combine_num_local = 0
|
|
if is_ll:
|
|
combine_local[0] = topk_weights
|
|
combine_num_local = 1
|
|
|
|
print(f"Rank {my_rank}: Testing ncclEpCombine (send_only={'true' if combine_send_only else 'false'})")
|
|
nccl.ncclEpCombine(
|
|
ep_handle,
|
|
ctypes.cast(combine_in, ctypes.POINTER(ncclNDTensor_t)), 1,
|
|
ctypes.cast(combine_out, ctypes.POINTER(ncclNDTensor_t)), 1,
|
|
ctypes.cast(combine_local, ctypes.POINTER(ncclNDTensor_t)) if combine_num_local > 0 else None,
|
|
combine_num_local,
|
|
int(combine_send_only),
|
|
None,
|
|
stream,
|
|
)
|
|
|
|
nccl.ncclEpComplete(ep_handle, None, stream)
|
|
cuda_stream_synchronize(stream)
|
|
|
|
# -- verify combine output ---------------------------------------------
|
|
combine_errors = 0
|
|
if not random_mode:
|
|
co_host = (ctypes.c_uint16 * (num_tokens * hidden))()
|
|
co_data = tensor_get_data(nccl, combined_output)
|
|
cuda_memcpy(co_host, co_data, num_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
|
|
for i in range(num_tokens):
|
|
for j in range(ELEMENTS_TESTED_PER_TOKEN):
|
|
expected = float_to_bf16(float((j + 1) * 2))
|
|
actual = co_host[i * hidden + j]
|
|
if actual != expected:
|
|
print(f"Combine check failed! Rank {my_rank}, token {i}, "
|
|
f"element {j}: expected {expected}, got {actual}")
|
|
combine_errors += 1
|
|
if combine_errors >= 5:
|
|
break
|
|
if combine_errors >= 5:
|
|
break
|
|
|
|
if random_mode:
|
|
print(f"Rank {my_rank}: Combine flow completed (random mode, checks skipped)")
|
|
elif combine_errors == 0:
|
|
print(f"Rank {my_rank}: Combine verification PASSED! "
|
|
f"All {num_tokens} tokens with {hidden} elements each correctly combined")
|
|
else:
|
|
print(f"Rank {my_rank}: Combine verification FAILED with {combine_errors} errors")
|
|
print(f"Rank {my_rank}: Exiting test due to combine failure")
|
|
sys.exit(1)
|
|
|
|
# ===================================================================
|
|
# Cached mode test (HT only — repeat dispatch + combine, compare)
|
|
# ===================================================================
|
|
if cached_mode:
|
|
if is_ll:
|
|
print(f"Rank {my_rank}: Error - cached mode is only supported in HT modes (not LL)")
|
|
sys.exit(1)
|
|
|
|
print(f"Rank {my_rank}: Testing cached mode ({algorithm_name})")
|
|
|
|
# save first-phase outputs
|
|
first_d0 = first_d1 = first_d2 = first_co = None
|
|
if not random_mode:
|
|
first_d0 = (ctypes.c_uint16 * (num_recv_tokens * hidden))()
|
|
d0 = tensor_get_data(nccl, output_tokens)
|
|
cuda_memcpy(first_d0, d0, num_recv_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
|
|
first_d1 = (ctypes.c_float * (num_recv_tokens * top_k))()
|
|
d1 = tensor_get_data(nccl, output_topk_weights)
|
|
cuda_memcpy(first_d1, d1, num_recv_tokens * top_k * 4, CUDA_MEMCPY_D2H)
|
|
|
|
first_d2 = (ctypes.c_int64 * (num_recv_tokens * top_k))()
|
|
d2 = tensor_get_data(nccl, output_topk_idx)
|
|
cuda_memcpy(first_d2, d2, num_recv_tokens * top_k * 8, CUDA_MEMCPY_D2H)
|
|
|
|
first_co = (ctypes.c_uint16 * (num_tokens * hidden))()
|
|
co = tensor_get_data(nccl, combined_output)
|
|
cuda_memcpy(first_co, co, num_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
|
|
# new output tensors for second dispatch
|
|
cached_out_tokens = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_recv_tokens, hidden,
|
|
)
|
|
cached_combined_output = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclBfloat16,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOKENS,
|
|
None, num_tokens, hidden,
|
|
)
|
|
cached_combined_tw = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclFloat32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS,
|
|
None, num_tokens, top_k,
|
|
)
|
|
|
|
cached_comb_outs = (ncclNDTensor_t * 2)()
|
|
cached_comb_outs[0] = cached_combined_output
|
|
cached_comb_outs[1] = cached_combined_tw
|
|
|
|
# topk_weights input for combine (copy from dispatch output)
|
|
cached_ctw_in = tensor_create(
|
|
nccl, ep_group, 2, ncclDataTypeEnum.ncclFloat32,
|
|
ncclEpTensorTag_t.NCCL_EP_TENSOR_TAG_TOPK_WEIGHTS,
|
|
None, num_recv_tokens, top_k,
|
|
)
|
|
ctw_dst = tensor_get_data(nccl, cached_ctw_in)
|
|
ctw_src = tensor_get_data(nccl, output_topk_weights)
|
|
cuda_memcpy(ctw_dst, ctw_src, num_recv_tokens * top_k * 4, CUDA_MEMCPY_D2D)
|
|
|
|
cached_comb_ins = (ncclNDTensor_t * 2)()
|
|
cached_comb_ins[0] = expert_outputs
|
|
cached_comb_ins[1] = cached_ctw_in
|
|
|
|
# second dispatch (cached — only tokens, 1 input / 1 output)
|
|
cached_disp_outs = (ncclNDTensor_t * 1)()
|
|
cached_disp_outs[0] = cached_out_tokens
|
|
|
|
print(f"Rank {my_rank}: Testing cached mode - second ncclEpDispatch call "
|
|
f"(send_only={'true' if dispatch_send_only else 'false'})")
|
|
nccl.ncclEpDispatch(
|
|
ep_handle,
|
|
ctypes.cast(inputs_arr, ctypes.POINTER(ncclNDTensor_t)), 1,
|
|
ctypes.cast(cached_disp_outs, ctypes.POINTER(ncclNDTensor_t)), 1,
|
|
None, 0,
|
|
int(dispatch_send_only),
|
|
dispatch_config,
|
|
stream,
|
|
)
|
|
|
|
print(f"Rank {my_rank}: Testing cached mode - second ncclEpComplete (dispatch)")
|
|
nccl.ncclEpComplete(ep_handle, None, stream)
|
|
cuda_stream_synchronize(stream)
|
|
|
|
print(f"Rank {my_rank}: Testing cached mode - second ncclEpCombine call "
|
|
f"(send_only={'true' if combine_send_only else 'false'})")
|
|
nccl.ncclEpCombine(
|
|
ep_handle,
|
|
ctypes.cast(cached_comb_ins, ctypes.POINTER(ncclNDTensor_t)), 2,
|
|
ctypes.cast(cached_comb_outs, ctypes.POINTER(ncclNDTensor_t)), 2,
|
|
None, 0,
|
|
int(combine_send_only),
|
|
None,
|
|
stream,
|
|
)
|
|
|
|
print(f"Rank {my_rank}: Testing cached mode - second ncclEpComplete (combine)")
|
|
nccl.ncclEpComplete(ep_handle, None, stream)
|
|
cuda_stream_synchronize(stream)
|
|
|
|
# compare first vs second phase
|
|
cached_dispatch_errors = 0
|
|
cached_combine_errors = 0
|
|
|
|
if not random_mode:
|
|
sec_d0 = (ctypes.c_uint16 * (num_recv_tokens * hidden))()
|
|
sd0 = tensor_get_data(nccl, cached_out_tokens)
|
|
cuda_memcpy(sec_d0, sd0, num_recv_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
|
|
sec_co = (ctypes.c_uint16 * (num_tokens * hidden))()
|
|
sco = tensor_get_data(nccl, cached_combined_output)
|
|
cuda_memcpy(sec_co, sco, num_tokens * hidden * 2, CUDA_MEMCPY_D2H)
|
|
|
|
sec_tw = (ctypes.c_float * (num_tokens * top_k))()
|
|
stw = tensor_get_data(nccl, cached_combined_tw)
|
|
cuda_memcpy(sec_tw, stw, num_tokens * top_k * 4, CUDA_MEMCPY_D2H)
|
|
|
|
for i in range(num_recv_tokens * hidden):
|
|
if first_d0[i] != sec_d0[i]:
|
|
if cached_dispatch_errors < 5:
|
|
print(f"Rank {my_rank}: Cached dispatch output0 mismatch at {i}: "
|
|
f"first={first_d0[i]}, second={sec_d0[i]}")
|
|
cached_dispatch_errors += 1
|
|
|
|
for i in range(num_tokens * hidden):
|
|
if first_co[i] != sec_co[i]:
|
|
if cached_combine_errors < 5:
|
|
print(f"Rank {my_rank}: Cached combine output mismatch at {i}: "
|
|
f"first={first_co[i]}, second={sec_co[i]}")
|
|
cached_combine_errors += 1
|
|
|
|
expected_w = 1.0 / top_k
|
|
for i in range(num_tokens * top_k):
|
|
if sec_tw[i] != expected_w:
|
|
if cached_combine_errors < 5:
|
|
print(f"Rank {my_rank}: Cached combine topk_weights mismatch at {i}: "
|
|
f"expected={expected_w}, got={sec_tw[i]}")
|
|
cached_combine_errors += 1
|
|
|
|
if random_mode:
|
|
print(f"Rank {my_rank}: Cached mode completed (random mode, checks skipped)")
|
|
elif cached_dispatch_errors == 0 and cached_combine_errors == 0:
|
|
print(f"Rank {my_rank}: Cached mode verification PASSED - dispatch and combine outputs match")
|
|
else:
|
|
print(f"Rank {my_rank}: Cached mode verification FAILED - "
|
|
f"dispatch errors: {cached_dispatch_errors}, combine errors: {cached_combine_errors}")
|
|
sys.exit(1)
|
|
|
|
# cleanup cached tensors
|
|
tensor_destroy(nccl, ep_group, cached_out_tokens)
|
|
tensor_destroy(nccl, ep_group, cached_combined_output)
|
|
tensor_destroy(nccl, ep_group, cached_combined_tw)
|
|
tensor_destroy(nccl, ep_group, cached_ctw_in)
|
|
|
|
print(f"Rank {my_rank}: Cached mode - second dispatch and combine calls completed successfully")
|
|
|
|
# ===================================================================
|
|
# Cleanup — all tensor destroys must happen before ncclEpGroupDestroy
|
|
# because GroupDestroy free()s the ep_group struct whose free_fn is
|
|
# needed by TensorDestroy to release owned device memory.
|
|
# ===================================================================
|
|
tensor_destroy(nccl, ep_group, expert_outputs)
|
|
tensor_destroy(nccl, ep_group, topk_weights)
|
|
tensor_destroy(nccl, ep_group, combined_output)
|
|
tensor_destroy(nccl, ep_group, topk_idx)
|
|
if disable_max_tokens and handle_recv_expert_counter is not None:
|
|
rc_data = tensor_get_data(nccl, handle_recv_expert_counter)
|
|
cuda_free_host(rc_data)
|
|
tensor_destroy(nccl, ep_group, handle_recv_expert_counter)
|
|
tensor_destroy(nccl, ep_group, input_tokens)
|
|
tensor_destroy(nccl, ep_group, output_tokens)
|
|
if not is_ll:
|
|
tensor_destroy(nccl, ep_group, output_topk_weights)
|
|
tensor_destroy(nccl, ep_group, output_topk_idx)
|
|
if is_ll:
|
|
tensor_destroy(nccl, ep_group, local_tensor_recv_count)
|
|
|
|
nccl.ncclEpHandleDestroy(ep_handle)
|
|
nccl.ncclEpGroupDestroy(ep_group, stream)
|
|
|
|
# ncclCommDestroy (not exposed in wrapper — call underlying library)
|
|
_nccl_base = NCCLLibrary._nccl_base_lib or nccl.lib
|
|
_nccl_base.ncclCommDestroy(comm)
|
|
|
|
cuda_device_reset()
|
|
print(f"[MPI Rank {my_rank}] Success ")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|