Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions projects/rccl-tests/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ set(COMMON_FILES
collector.cu
common.h
common.cu
gin_sdma_collective_policy.h
nccl1_compat.h
rccl_compat.h
rccl_float8.h
Expand Down
8 changes: 4 additions & 4 deletions projects/rccl-tests/src/alltoall.cu
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,10 @@ __global__ void GinAlltoAllKernel(ncclWindow_t sendwin, size_t sendoffset, ncclW
int tid = threadIdx.x + blockIdx.x * blockDim.x;
int nthreads = blockDim.x * gridDim.x;

/* send to all peers via GIN */
/* send to all peers via GIN (chunked to <=128 MiB/put; see common.h::ginPutChunked) */
const size_t size = count * sizeof(T);
for (int r=tid; r<devComm.nRanks; r+=nthreads) {
gin.put(ncclTeamWorld(devComm), r,
ginPutChunked(gin, ncclTeamWorld(devComm), r,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GinAlltoAllKernel and HybridAlltoAllKernel are the entry points for the other GIN backends as well, not just SDMA, so this will cap puts for proxy, and rocshmem gda too

It would be good to validate with NCCL_GIN_TYPE=2 and 4

Would it make sense to put the chunking in ncclGinApi_Put<NCCL_NET_DEVICE_GIN_ANVIL_SDMA> instead, the way the proxy backend already does?
That would fix the hang for all GIN SDMA callers and not just alltoall_perf

NCCL_DEVICE_INLINE void put(Coop coop, ncclGinProxyGfd_t* gfd, ncclGinProxyGpuCtx_t* proxyCtx, int peer,
ncclGinWindow_t dstWnd, size_t dstOff, T srcVal, bool hasInline, ncclGinWindow_t srcWnd,
size_t srcOff, size_t bytes, ncclGinSignalDescriptor signal, ncclGinSignalOp_t signalOp,
uint64_t signalVal, bool hasCounter, ncclGinCounter_t counterId,
cuda::thread_scope required, cuda::thread_scope given) {
// Release the source to system scope before the GFD is published, since the NIC
// DMA-reads it. hip_compat.h orders thread_scope with system widest (largest), so
// "narrower than system" is given < system (upstream libcu++ uses the opposite order).
#if defined(__HIP_PLATFORM_AMD__)
if ((int)given < (int)cuda::thread_scope_system) {
#else
if ((int)given > (int)cuda::thread_scope_system) {
#endif
cuda::atomic_thread_fence(cuda::memory_order_release, cuda::thread_scope_system);
}
using nccl::gin::proxy::DataChunkSize;
while (bytes > DataChunkSize) {
ncclGinProxyOp_t op;
constructProxyOp(op, /*isGet*/ false, /*isFlush*/ false, /*hasInline*/ false, NCCL_GIN_SIGNAL_TYPE_NONE, signalOp,
/*hasCounter*/ false);
nccl::gin::proxy::buildGfd(gfd, op, /*srcVal*/ 0, /*hasInline*/ false, srcOff, srcWnd, dstOff, dstWnd,
DataChunkSize, /*counterId*/ 0, /*signalId*/ 0,
/*signalVal*/ 0, nullptr, 0);
nccl::gin::proxy::postGfd<Coop>(coop, proxyCtx, gfd, peer);
bytes -= DataChunkSize;
srcOff += DataChunkSize;
dstOff += DataChunkSize;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GDA gin has the same limitation (1GB message size), but I agree with Nusrat that this should not surface in user code.

@dlamd1dai dlamd1dai Aug 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you're right that GinAlltoAllKernel/HybridAlltoAllKernel are shared by all GIN backends, so capping there was not necessary for proxy and GDA. Fixed in 92b25f7: the size cap now lives inside the SDMA backend's Put API (ncclGinApi_Put<NCCL_NET_DEVICE_GIN_ANVIL_SDMA>), and the kernels are back to a plain gin.put(). Proxy (type 2) and GDA (type 4) are no longer capped by construction — they don't touch the SDMA segmentation path at all.

I haven't been able to get a clean type 2/4 datapoint yet (the RDMA host I have, dell300x, has a broken bnxt_re ABI in-container), but since the kernel path for those types is now byte-for-byte the pre-PR gin.put(), there's no behavioral change to validate. SDMA (type 5) is validated on 8x MI355X up to 2 GiB — no hang, 0 wrong, ~426 GB/s.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on both points. As of 92b25f7 the chunking is entirely inside the SDMA backend gin.put() and no longer in user/test code. On the GDA 1 GiB limit: this PR scopes its fix to SDMA only (GDA's kernel path is unchanged here). The same internal-segmentation pattern could be applied to the GDA backend's Put as a follow-up if we want gin.put() to be size-agnostic there too — happy to file that separately.

@nusislam nusislam Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will applications call gin.puChunked()? This looks like a new API that has been added in RCCL and not present in NCCL. The ideal solution would be to gin.put() internally calling gin.putChunked() for large messages. Otherwise, if an application calls gin.put() with large sizes, it may still hit the hang issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and that's exactly what 92b25f7 does. There's no ginPutChunked() anymore — it's removed from rccl-tests. Segmentation is now internal to gin.put() for the SDMA backend: a large put is split into <=128 MiB SDMA copies inside ncclGinApi_Put<ANVIL_SDMA>, invisible to callers. No new user-facing API, and it matches NCCL's gin.put() surface.

recvwin, recvoffset + devComm.rank * size,
sendwin, sendoffset + r * size,
size, ncclGin_SignalInc{signalIndex});
Expand Down Expand Up @@ -298,13 +298,13 @@ __global__ void HybridAlltoAllKernel(ncclWindow_t sendwin, size_t sendoffset, nc
int nthreads = blockDim.x;

for (int r = tid; r < startLsa; r += nthreads) {
gin.put(world, r,
ginPutChunked(gin, world, r,
recvwin, recvoffset + world.rank * size,
sendwin, sendoffset + r * size,
size, ncclGin_SignalInc{signalIndex});
}
for (int r = startLsa + lsaSize + tid; r < world.nRanks; r += nthreads) {
gin.put(world, r,
ginPutChunked(gin, world, r,
recvwin, recvoffset + world.rank * size,
sendwin, sendoffset + r * size,
size, ncclGin_SignalInc{signalIndex});
Expand Down
64 changes: 64 additions & 0 deletions projects/rccl-tests/src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
#define NCCL_TESTS_VERSION "2.18.3"

#include "rccl/rccl.h"
// GIN-SDMA shared policy constants (single source of truth for the put-chunk
// cap that ginPutChunked below clamps to). Cherry-picked as a standalone,
// namespaced (gin_sdma::), no-GPU/no-RCCL header from
// users/dondai/gin-stage2j-sdma-A1457-a2av-wip to fix A2A >1 GiB WITHOUT
// pulling in the other GIN-SDMA collective kernel designs.
#include "gin_sdma_collective_policy.h"
// nccl_device.h provides the device-API public types referenced below
// (ncclCommProperties_t, full ncclDevCommRequirements, NCCL_GIN_TYPE_NONE, ...).
// As of NCCL 2.29 these types are referenced unconditionally by the test engine
Expand Down Expand Up @@ -412,6 +418,64 @@ extern int is_main_proc;
extern thread_local int is_main_thread;

#if defined(ENABLE_DEVICE_API) && NCCL_VERSION_CODE >= NCCL_VERSION(2,28,0)
// Overflow-safe AND hang-safe gin.put for the Anvil-SDMA backend. Two distinct
// single-descriptor limits apply, and we clamp to the smaller of the two:
// 1) Correctness: the SDMA linear-copy count field is 30 bits and 1-based
// (count = bytes-1), so a put of >1 GiB (kGinPutMaxBytes) silently
// truncates and corrupts data (a 2 GiB transfer copies only 1 GiB).
// 2) Reliability: on MI355X + ROCm 7.13 a single copy descriptor at/above
// 256 MiB stalls the SDMA engine on the fused copy+signal packet, so the
// copy never lands and the SignalInc never fires -> waitSignal hangs. A
// 128 MiB cap (gin_sdma::kGinSdmaSafeCopyBytes) is measured hang-free with
// no bandwidth loss; see that constant for the evidence.
// Split the transfer into <=kGinSdmaSafeCopyBytes segments and carry the
// caller's remote action (e.g. SignalInc) ONLY on the final segment. A
// <=128 MiB message is a single put with no extra overhead.
//
// ORDERING PRECONDITION (why the trailing signal is safe): the Anvil-SDMA
// backend selects the hardware SDMA queue per *issuer*, as
// effectiveChannel = (blockId*warpsPerBlock + warpId)*stride % numChannels
// (gin_anvil_sdma.h) -- NOT per peer. A single hardware queue is a strict
// in-order FIFO; distinct channels are independent completion domains with NO
// cross-queue ordering (only ncclGinApi_Flush drains all of a peer's channels).
// Because this loop runs entirely within ONE thread, every segment of this
// message -- data segments AND the final signal-carrying put -- resolves to the
// same (blockId, warpId) and therefore the same in-order queue, even when
// NCCL_GIN_ANVIL_SDMA_NUM_CHANNELS > 1. So the final SignalInc is FIFO-ordered
// behind all prior segments of this message and "signal fired" still means "the
// whole message landed"; per-message waitSignal accounting is unchanged.
//
// CALLER CONTRACT: issue an entire per-peer message (all segments + the
// signal-carrying final put) from a SINGLE thread/warp, as the AllToAll GIN
// sites do (one thread owns each disjoint (peer, offset) tuple). Do NOT split a
// single peer's message across multiple warps/CTAs while carrying the signal on
// only one of them: under NUM_CHANNELS > 1 the segments could land on different
// queues and the SignalInc could fire before an earlier segment completes
// (silent data corruption). If you ever need that pattern, drain with a Flush
// (or force NUM_CHANNELS=1 / SPREAD_CHANNELS=0) before signaling.
template <typename RemoteAction>
__device__ __forceinline__ void ginPutChunked(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no regression test for the chunking here. Could we add the following test cases?

  1. A host unit test of the segment math (pure integer arithmetic, no GPU needed) over bytes = {0, 128MiB, 128MiB+1, 256MiB, 1GiB, 2GiB}, asserting exactly one signal is carried, no segment exceeds 128MiB, and the segments sum back to bytes. (gin_sdma_collective_policy.h already advertises a GIN_SDMA_HOST_ONLY GoogleTest path that is defined but currently unused.)

  2. An AllToAll correctness test above 128MiB/peer (exercises the new multi-segment loop) and above 1GiB/peer (crosses the 1GiB 30-bit truncation boundary), using exact-integer datatypes so a truncated or stale tail can't be masked by float tolerance.

  3. The original 2GiB-total AllToAll hang repro as a guard that it now completes (MI355X + ROCm 7.13, NCCL_GIN_TYPE=5).

Today there's no AllToAll test in rccl-tests and the pytest byte_range caps at 128M total, so nothing reaches the new multi-segment path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I will add my test code into the "rccl-tests/" directory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added all three in 73e0a47.

  1. Host unit test (no GPU). I factored the segmentation arithmetic out of the __device__ ginPutChunked into two pure functions in gin_sdma_collective_policy.h — ginPutSegmentCount() and ginPutSegmentAt() (plus kGinPutSegBytes = min(reliability, correctness caps)) — and rewrote the device loop to drive off them, so the code the test exercises is exactly what runs on device. test/gin_sdma_policy_test.cpp (GoogleTest, compiled host-only via GIN_SDMA_HOST_ONLY) asserts over bytes = {0, 1, 128MiB, 128MiB+1, 256MiB, 1GiB, 1GiB+1, 2GiB} — plus a dense +/-2 B scan around every cap multiple — that no segment exceeds 128 MiB, the segments tile [0,bytes) contiguously and sum back to bytes, and exactly one final (signal-carrying) segment lands on the tail. Wired opt-in via -DBUILD_TESTS=ON (find/fetch GTest), ctest label unit. (This re-establishes the GIN_SDMA_HOST_ONLY path you noted — it's now actually used.)

  2. AllToAll correctness (GPU). test/test_AllToAll.py drives the real GinHybridAlltoAllKernel (-D 3, NCCL_GIN_TYPE=5, SDMA tier forced via NCCL_GIN_ANVIL_SDMA_THRESHOLD*=0) at 256 MiB/peer (>128 MiB → multi-segment loop) and 2 GiB/peer (>1 GiB → crosses the 30-bit truncation boundary), with exact-integer dtypes (int32/int64/uint8) and the built-in data check (-c 1), so a truncated/stale tail can't hide under float tolerance (nonzero exit on any wrong element).

  3. Hang guard (GPU). The original 2 GiB-total repro (256 MiB/peer at 8 ranks) is a dedicated completion sentinel: a subprocess timeout converts a reintroduced SDMA hang into a test failure instead of stalling the runner.

The two GPU tests need an 8x MI355X-class node + MPI + a GIN-SDMA build, so the module skips unless RCCL_TESTS_GIN_SDMA_A2A=1 (size/np/timeout configurable via env); the host unit test runs anywhere. The device loop is behavior-identical to the prior do/while, so no functional change to the shipping path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran both test tiers on hardware:

Host unit test — built googletest 1.14.0 and compiled gin_sdma_policy_test.cpp host-only (-DGIN_SDMA_HOST_ONLY -Wall -Wextra): all 5 cases pass, warning-clean (fixed one -Wdangling-else in 579861c).

GPU AllToAll (8x MI355X, ROCm 7.13, NCCL_GIN_TYPE=5, force-single-channel) — -D 3, SDMA tier forced, int32, built-in data check:

case per-peer segments busbw #wrong
>128 MiB/peer 256 MiB 2×128 MiB ~424 GB/s 0
>1 GiB/peer 2 GiB 16×128 MiB (crosses 30-bit boundary) ~428 GB/s 0
2 GiB-total hang guard 256 MiB 2×128 MiB ~424 GB/s 0, no hang

Running the suite on-hardware surfaced two gaps I fixed in 51c2af8:

  • The test now forwards the env the GIN-SDMA backend actually needs to initialize (always NCCL_GIN_ENABLE=1, plus deployment-specific vars via RCCL_TESTS_A2A_XENV and launcher opts via RCCL_TESTS_MPI_OPTS) — the minimal 3-var version couldn't bring GIN up on the cluster.
  • On the hang timeout it now SIGKILLs the whole launcher process group (new session), so a wedged run can't orphan ranks that pin the GPUs / exhaust SDMA queues for later cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added and relocated. The host unit test is now projects/rccl/test/gin/gin_sdma_policy_test.cpp, wired into the rccl suite as rccl-UnitTestsGinSdmaPolicy. It exercises the pure segment math (ginPutSegmentCount()/ginPutSegmentAt(), no GPU) over {0, 128MiB, 128MiB+1, 256MiB, 1GiB, 2GiB} and asserts contiguous tiling with exactly one final (signal-carrying) segment — 5/5 pass, warning-clean.

ncclGin& gin, ncclTeam team, int peer,
ncclWindow_t dstWin, size_t dstOff,
ncclWindow_t srcWin, size_t srcOff,
size_t bytes, RemoteAction finalAction) {
const size_t kMax = gin_sdma::kGinSdmaSafeCopyBytes < gin_sdma::kGinPutMaxBytes
Comment thread
dlamd1dai marked this conversation as resolved.
Resolved
? gin_sdma::kGinSdmaSafeCopyBytes
: gin_sdma::kGinPutMaxBytes;
size_t off = 0;
do {
const size_t rem = bytes - off;
const size_t seg = rem > kMax ? kMax : rem;
if (off + seg >= bytes) {
// Final (or only) segment carries the signal / remote action.
gin.put(team, peer, dstWin, dstOff + off, srcWin, srcOff + off, seg, finalAction);
} else {
gin.put(team, peer, dstWin, dstOff + off, srcWin, srcOff + off, seg);
}
off += seg;
} while (off < bytes);
}

Comment thread
wenkaidu marked this conversation as resolved.
Resolved
template <typename F>
testResult_t testLaunchDeviceKernel(F kernel, void* sendbuff, size_t sendoffset, void* recvbuff, size_t recvoffset, size_t count, ncclDataType_t type, ncclRedOp_t op, int root, ncclComm_t comm, cudaStream_t stream) {
if (kernel == nullptr) return testNotImplemented;
Expand Down
Loading
Loading