Skip to content

cuda: test RCCL with a small all-reduce after startup and fall back cleanly (#7) - #9

Open
JCraigWasTaken wants to merge 2 commits into
mxxm-t:masterfrom
JCraigWasTaken:rccl-probe-fallback
Open

cuda: test RCCL with a small all-reduce after startup and fall back cleanly (#7)#9
JCraigWasTaken wants to merge 2 commits into
mxxm-t:masterfrom
JCraigWasTaken:rccl-probe-fallback

Conversation

@JCraigWasTaken

Copy link
Copy Markdown

On two MI50s with PCIe peer access enabled, RCCL starts up fine, connects both cards, and then fails on the first real all-reduce. The test tool that ships with RCCL shows the same failure without llama.cpp, on both the 7.14 and 6.3.3 builds, and none of RCCL's transport switches change it, so this is RCCL itself, not the fork. Today the fork only falls back when RCCL fails to start, so on a peer-access kernel it crashes on the first token.

This change runs a tiny all-reduce right after RCCL starts: 64 numbers per card, each card contributing its own number, checked against the expected total. It checks the return codes, the stream, and RCCL's asynchronous error state, because on these cards the failure only shows up after the calls return. If anything is wrong it shuts the RCCL connections down, prints one warning saying which path it is using instead, and carries on with the same fallback the startup-failure case already uses. If GGML_CUDA_ALLREDUCE=nccl was set on purpose, a failed test stops with a message pointing at that setting. GGML_CUDA_RCCL_PROBE=0 skips the test.

Found on the way: the "internal" all-reduce is not compiled into AMD builds at all, so GGML_CUDA_ALLREDUCE=internal has always meant the fork's butterfly exchange on AMD. The messages now say that instead of reporting a failure that could never have succeeded. Nothing about the behaviour changes.

Measured on two MI50s with Qwen3.8-27B at 8-bit split across both cards, production settings: with nothing set, the server starts, prints the warning once per model load, and runs at 46.4 tokens per second decode and 315 prefill on an 8,148-token prompt, the same as with the setting forced by hand. With GGML_CUDA_ALLREDUCE=nccl set, it stops at startup with the clear message.

Fixes #7

🤖 Generated with Claude Code

https://claude.ai/code/session_018WHiJ3bdMZsRmkWqMnK1pj

JCraigWasTaken and others added 2 commits September 3, 2026 21:20
…nly (issue mxxm-t#7)

On a 2x MI50 (gfx906) box running a kernel that supports PCIe peer access,
ncclCommInitAll() returns ncclSuccess and RCCL reports its P2P channels as
connected, but the very first real all-reduce dies with "unhandled cuda
error" and the aborting NCCL_CHECK macro takes the process down. The fault
is in RCCL 2.30.4 itself, not in llama.cpp: rccl-tests all_reduce_perf
reproduces it standalone on the same machine. On kernels without peer
access the init call fails instead, so the existing init-failure path fires
and the fork falls back to its own exchange - which is why this was never
seen before. The only workaround so far was setting
GGML_CUDA_ALLREDUCE=internal by hand.

Run a tiny all-reduce right after ncclCommInitAll and take the same
fallback the init-failure path takes if it does not work. The probe checks
the ncclResult_t values directly instead of using NCCL_CHECK, and because
the MI50 failure is asynchronous it also checks cudaStreamSynchronize and
ncclCommGetAsyncError on every rank, then verifies the sum numerically so a
silently wrong result also triggers the fallback. On failure the comms are
aborted and cleared, one GGML_LOG_WARN line explains what happened and
names GGML_CUDA_ALLREDUCE, and init continues into the internal path.

If GGML_CUDA_ALLREDUCE=nccl was set explicitly the probe still runs but a
failure aborts with the same explanation, since the user asked for NCCL.
GGML_CUDA_RCCL_PROBE=0 skips the probe; it is on by default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHiJ3bdMZsRmkWqMnK1pj
…e same way GGML_CUDA_ALLREDUCE=internal does

It already did.  The post-probe fallback calls
ggml_backend_cuda_comm_init_internal(), which is byte-for-byte the same
function GGML_CUDA_ALLREDUCE=internal calls, with the same context, the
same dev_ids and no state consumed by the NCCL branch in between.  What
made it look otherwise is that on this box (2x MI50, HIP) *both* paths
end up on the meta-backend butterfly, because the internal AllReduce is
compiled out: allreduce.cu guards the whole implementation with
"#if !defined(GGML_USE_HIP) && !defined(GGML_USE_MUSA)" and HIP builds
get a stub ggml_cuda_ar_pipeline_init() that returns nullptr always.
init_internal reads that nullptr as an init failure and logs
"internal AllReduce init failed (n_devices != 2?)", which is wrong on
two counts: nothing was tried, and n_devices was fine.  The production
GGML_CUDA_ALLREDUCE=internal runs log the same line and have all along.

So there is no fallback to repair, only messages that misdescribe what
happened.  Give the file a compile-time pair -- whether the internal
AllReduce exists in this build, and the name of whatever "internal"
resolves to -- and use it in both places: on HIP/MUSA the init step now
says at debug level that there is no internal AllReduce in this build
rather than warning about a failure, and the probe-failure warning names
the path actually being taken.  On CUDA every message is unchanged.

Behaviour is unchanged everywhere; only log text moves.  On this box the
no-env fallback run already reached 46.29 t/s decode and 317 t/s prefill,
matching the GGML_CUDA_ALLREDUCE=internal champion, which is the same
thing measured twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WHiJ3bdMZsRmkWqMnK1pj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eval bug: 2x MI50 tensor split aborts in RCCL on the first AllReduce once peer access works, no fallback to butterfly

1 participant