Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/collectivex-sweep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
description: "EP library to sweep — 'all' runs every EP backend in one matrix"
type: choice
default: all
options: [all, deepep-v2, mori, uccl-ep]
options: [all, deepep-v2, mori, uccl-ep, nccl-ep]
only_sku:
description: Restrict to one GHA runner pool; blank = all
type: string
Expand Down
470 changes: 470 additions & 0 deletions experimental/CollectiveX/bench/ep_nccl.py

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions experimental/CollectiveX/bench/run_ep.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def _runtime_info(torch, *, vendor: str) -> dict:

def main() -> int:
ap = argparse.ArgumentParser(description="CollectiveX EP dispatch/combine sweep")
ap.add_argument("--backend", required=True, choices=["deepep-v2", "mori", "uccl-ep"])
ap.add_argument("--backend", required=True,
choices=["deepep-v2", "mori", "uccl-ep", "nccl-ep"])
ep_harness.add_common_args(ap)
args = ap.parse_args()

Expand Down Expand Up @@ -92,18 +93,21 @@ def main() -> int:
from ep_mori import MoRIBackend as Backend
elif args.backend == "uccl-ep":
from ep_uccl import UCCLEPBackend as Backend
elif args.backend == "nccl-ep":
from ep_nccl import NCCLEPBackend as Backend
else:
from ep_deepep_v2 import DeepEPV2Backend as Backend

# MoRI registers the default GPU process group with its SHMEM runtime. Keep that
# group device-only so scale-out does not also depend on a host Gloo fabric.
if not dist.is_initialized():
if args.backend in ("mori", "uccl-ep"):
if args.backend in ("mori", "uccl-ep", "nccl-ep"):
# MoRI registers this group with its SHMEM runtime; UCCL-EP is portable across
# NVIDIA (NCCL) and AMD (RCCL) and bootstraps its Buffer + CPU-proxy ranks from
# it. Both take the explicit rank/world_size form and keep the group device-only
# so scale-out does not also depend on a host Gloo fabric. (uccl-ep's own default-
# PG registration, if any, happens in the adapter's create_buffer, mirroring MoRI.)
# it. NCCL EP forms its OWN NCCL communicator and uses this group only to broadcast
# that communicator's unique id (no MPI in CollectiveX) plus the harness's timing
# collectives. All take the explicit rank/world_size form and keep the group
# device-only so scale-out does not also depend on a host Gloo fabric.
dist.init_process_group(
backend="nccl",
rank=rank,
Expand Down
19 changes: 10 additions & 9 deletions experimental/CollectiveX/configs/platform_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"scale_up_domain": 8,
"scale_up_transport": "nvlink",
"launcher": "single-slurm",
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8]},
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8], "nccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8], "nccl-ep": [8]},
"fabric": {"nic": "ConnectX-7 2x200GbE", "switch": "Arista 7060DX5-64S (Tomahawk4, 25.6T)"},
"operator": {
"partition": "hpc-gpu-1",
Expand All @@ -33,8 +33,8 @@
"scale_up_domain": 8,
"scale_up_transport": "nvlink",
"launcher": "single-slurm",
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8]},
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8], "nccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8], "nccl-ep": [8]},
"fabric": {"nic": "ConnectX-7 400G", "switch": "NVIDIA Quantum-2 QM9790 (25.6T, InfiniBand)"},
"operator": {
"partition": "main",
Expand All @@ -53,8 +53,8 @@
"scale_up_domain": 8,
"scale_up_transport": "nvlink",
"launcher": "single-slurm",
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8]},
"backends": {"deepep-v2": [8, 16], "uccl-ep": [8], "nccl-ep": [8]},
"ll_backends": {"deepep-v2": [8], "uccl-ep": [8], "nccl-ep": [8]},
"fabric": {"nic": "ConnectX-7 400GbE", "switch": "Whitebox Tomahawk3 leaf + Tomahawk4 (RoCE)"},
"operator": {
"partition": "gpu-2",
Expand All @@ -76,7 +76,8 @@
"scale_up_domain": 8,
"scale_up_transport": "nvlink",
"launcher": "single-slurm",
"backends": {"deepep-v2": [8, 16]},
"backends": {"deepep-v2": [8, 16], "nccl-ep": [8]},
"ll_backends": {"nccl-ep": [8]},

Check failure on line 80 in experimental/CollectiveX/configs/platform_config.json

View check run for this annotation

Claude / Claude Code Review

nccl-ep low-latency rows enable a reproducibly-wedging decode path

PR description confirms every low-latency nccl-ep attempt on h100-dgxc, h200-dgxc, b200-dgxc, and b300 wedges (a lagging rank times out its dispatch receive against the ~100s EP timeout and the kernel traps with cudaErrorLaunchFailure/719), yet platform_config.json still enables `ll_backends: {"nccl-ep": [8]}` on all four SKUs. Since sweep_matrix.py's `_ll_runnable` reads this map directly and `test_nccl_ep_rollout_shape` locks the rows in as `runnable`, merging as-is schedules guaranteed-failin

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.

🔴 PR description confirms every low-latency nccl-ep attempt on h100-dgxc, h200-dgxc, b200-dgxc, and b300 wedges (a lagging rank times out its dispatch receive against the ~100s EP timeout and the kernel traps with cudaErrorLaunchFailure/719), yet platform_config.json still enables ll_backends: {"nccl-ep": [8]} on all four SKUs. Since sweep_matrix.py's _ll_runnable reads this map directly and test_nccl_ep_rollout_shape locks the rows in as runnable, merging as-is schedules guaranteed-failing low-latency EP8 legs on real GPU clusters every time nccl-ep or all is swept. The author explicitly asks for a reviewer call on whether to drop or gate these four config rows; recommend gating them out (or dropping the nccl-ep low-latency rows plus the matching test assertion) in this PR rather than merging a coverage row that is known-broken.

Extended reasoning...

What the bug is. configs/platform_config.json adds "ll_backends": {..., "nccl-ep": [8]} to h100-dgxc (diff line ~13), h200-dgxc (~line 37), b200-dgxc (~line 57), and adds a brand-new ll_backends block containing nccl-ep: [8] to b300, which previously had no ll_backends entry at all. These four rows tell the sweep generator that the low-latency (decode) NCCL EP path is runnable at EP8 on every x86 SKU NCCL EP is offered on.

How it is dispatched. sweep_matrix.py::_ll_runnable is a one-line lookup: return ep in platform.get('ll_backends', {}).get(backend, []). resolve_matrix calls this to decide whether to fan a cell out into a low-latency case, and any cell where it returns True is recorded with disposition: "runnable" — meaning it is picked up by collectivex-sweep.yml and actually dispatched to the GPU cluster whenever the workflow is run with backend: nccl-ep or backend: all. The new test test_nccl_ep_rollout_shape in tests/test_matrix.py goes further and asserts ll == {(sku, 8) for sku in {h100-dgxc, h200-dgxc, b200-dgxc, b300}}, which locks this broken low-latency scheduling in as expected/passing behavior rather than flagging it as unsupported.

Why nothing prevents the failure. The PR's own description, in the 'Known issue — the low-latency path wedges on x86' section, states plainly: 'every low-latency attempt on h100-dgxc, b300 and b200-dgxc wedges: one rank falls behind, its peers time out their dispatch receives against the compile-time ~100s EP timeout, and the kernel traps (cudaErrorLaunchFailure, 719)'. The root cause is identified as the stale-signal/pipeline-desync race in NCCL EP's LL count/flag protocol, filed upstream as NVIDIA/nccl#2303 and fixed by NVIDIA/nccl#2306 — a fix that is NOT present in the pinned nccl4py[cu13]==0.3.1 wheel this tree installs (COLLX_NCCL4PY_SPEC in runtime/common.sh). Nothing in ep_nccl.py, the launcher backend-selection logic, or the sweep generator gates this LL path off — the config rows are the only thing standing between the sweep and a guaranteed crash, and they currently say 'go'.

Why this is actionable within this tree. Unlike the upstream wheel bug itself (which cannot be fixed here), dropping or gating the four ll_backends: {"nccl-ep": [8]} rows (and the corresponding assertion in test_nccl_ep_rollout_shape) is a purely local, one-PR change. The author is explicit about this being an open decision: 'Reviewer decision requested: drop or gate those ll_backends rows in a follow-up, or leave them pending a fixed wheel.' Leaving the rows in place as submitted answers that question implicitly (and wrongly) in favor of dispatching the broken path. This also breaks with the precedent already established elsewhere in this same config for known-broken cells — e.g. cross-node EP16 on the same SKUs is recorded as an unsupported coverage row (never dispatched) precisely because it faults reproducibly at nccl_ep.cc:2884; the low-latency EP8 legs deserve the same treatment instead of being marked runnable.

Step-by-step proof. (1) An operator triggers collectivex-sweep.yml with backend: nccl-ep (or all) and default modes (blank = all modes). (2) The setup job runs sweep_matrix.py, which iterates SKUs and for h100-dgxc/h200-dgxc/b200-dgxc/b300 finds ll_backends["nccl-ep"] == [8], so _ll_runnable(platform, "nccl-ep", 8) returns True and a low-latency EP8 case is added to requested_cases with disposition: "runnable" and folded into an executable shard. (3) The sweep job matrix picks up that shard, allocates real GPU nodes via Slurm, builds/activates the nccl4py==0.3.1 wheel, and runs bench/run_ep.py --backend nccl-ep with mode=low-latency. (4) Per the PR's own validation notes, one rank in that run falls behind due to the stale-signal race in the wheel's LL protocol; its peers block on their dispatch receive until the ~100s EP timeout fires, and the kernel traps with cudaErrorLaunchFailure (error 719). (5) The shard leg fails, burning a real multi-minute GPU allocation and a red CI leg — reproducibly, every time this cell is swept, because the wheel's fix (NVIDIA/nccl#2306) is not present.

Suggested fix. Drop the four "nccl-ep": [8] entries from ll_backends (leaving backends for normal/HT mode intact, since HT validated green), and drop or adjust test_nccl_ep_rollout_shape's low-latency assertion accordingly — the same pattern the config already uses to scope out the EP16 cross-node cells as unsupported rather than dispatching known-broken work.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already fixed — d58d56566 (pushed after this review ran) drops all four "nccl-ep": [8] rows from ll_backends, so _ll_runnable no longer schedules those legs, and test_nccl_ep_rollout_shape now asserts self.assertEqual(ll, set()). On the current head the only ll_backends entries are deepep-v2/uccl-ep on the three x86 SKUs and mori on the three CDNA ones.

Root cause for the record: the wedge is not an x86-only quirk. The LL parity-buffer signals carry no generation tag, so at a fixed workload a lapped rank consumes a two-calls-old value and the pipeline desyncs — filed as NVIDIA/nccl#2303, fix in #2306. The rows come back when a fixed wheel ships; 78e595c92 documents the gap in the meantime.

"fabric": {"nic": "ConnectX-8 2x400GbE", "switch": "NVIDIA Spectrum-X SN5600 (51.2T)"},
"operator": {
"partition": "batch_1",
Expand All @@ -100,7 +101,7 @@
"scale_up_domain": 72,
"scale_up_transport": "mnnvl",
"launcher": "gb-nv",
"backends": {"deepep-v2": [8, 16]},
"backends": {"deepep-v2": [8, 16], "nccl-ep": [8, 16]},
"fabric": {"nic": "MNNVL (scale-out not used)", "switch": "NVLink NVL72"},
"operator": {
"partition": "batch",
Expand All @@ -119,7 +120,7 @@
"scale_up_domain": 72,
"scale_up_transport": "mnnvl",
"launcher": "gb-nv",
"backends": {"deepep-v2": [8, 16]},
"backends": {"deepep-v2": [8, 16], "nccl-ep": [8, 16]},
"fabric": {"nic": "MNNVL (scale-out not used)", "switch": "NVLink NVL72"},
"operator": {
"partition": "batch_1",
Expand Down
12 changes: 7 additions & 5 deletions experimental/CollectiveX/launchers/launch_gb-nv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export COLLX_TRANSPORT=mnnvl
export COLLX_NODES="$NODES" COLLX_GPUS_PER_NODE="$GPN" COLLX_SCALE_UP_DOMAIN="$SCALE_UP_DOMAIN"
export COLLX_NGPUS="$NGPUS"
case "$COLLX_BENCH" in
deepep-v2) ;;
deepep-v2 | nccl-ep) ;;
*) collx_die "unsupported $PRODUCT EP backend: $COLLX_BENCH" ;;
esac
collx_require_vars COLLX_IMAGE COLLX_IMAGE_PLATFORM COLLX_PARTITION COLLX_ACCOUNT COLLX_SQUASH_DIR COLLX_STAGE_DIR
Expand All @@ -53,10 +53,12 @@ collx_select_image "$IMAGE"
MOUNT_SRC="$(collx_stage_path "$REPO_ROOT" "$COLLX_STAGE_DIR")"
collx_stage_repo "$REPO_ROOT" "$MOUNT_SRC"
CONTAINER_MOUNTS="$MOUNT_SRC:/ix"
# ---- backend-setup: pinned DeepEP source + isolated build cache -------------
# The backend case above admits only deepep-v2, so its staging is unconditional.
collx_prepare_deepep_source "$MOUNT_SRC" \
|| collx_die "cannot stage the pinned backend source"
# ---- backend-setup: pinned source (deepep-v2 only) + isolated build cache ----
# nccl-ep is pip-only (nccl4py wheel; no source stage); deepep-v2 needs its pinned tree.
if [ "$COLLX_BENCH" = deepep-v2 ]; then
collx_prepare_deepep_source "$MOUNT_SRC" \
|| collx_die "cannot stage the pinned backend source"
fi
export COLLX_BACKEND_SOURCE_ROOT=/ix/experimental/CollectiveX/.collx_sources
collx_prepare_backend_cache "$COLLX_SQUASH_DIR" \
|| collx_die "cannot prepare the isolated backend cache"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ TIME_MIN="${COLLX_TIME:-$DEFAULT_TIME}"
IMAGE="$COLLX_IMAGE"
TS="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
case "$COLLX_BENCH" in
deepep-v2 | uccl-ep) ;;
deepep-v2 | uccl-ep | nccl-ep) ;;
*) collx_die "unsupported $RUNNER EP backend: $COLLX_BENCH" ;;
esac

Expand Down
10 changes: 10 additions & 0 deletions experimental/CollectiveX/runtime/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ COLLX_DEEPEP_V2_COMMIT="fa8a9b16898204afd347c663b89e65ef87dc6ce6"
COLLX_UCCL_REPO="https://github.com/uccl-project/uccl"
COLLX_UCCL_COMMIT="fc1b582031221645ea9fce58aeb57187713145e3"

# NCCL EP (NVIDIA's native MoE dispatch/combine on the NCCL Device API). Primary path is the
# published nccl4py wheel — it bundles libnccl_ep.so's JIT runtime and pulls the matching
# nvidia-nccl-cu13 (>= 2.30, carrying the Device API + GIN nccl.ep needs). The from-source pins
# below are the fallback, deferred until on-metal bring-up shows the wheel is insufficient:
# contrib/nccl_ep is absent from the v2.29.x / v2.30.4 release tags, so any such build must use
# this post-merge master commit (which contains contrib/nccl_ep), NOT a release tag.
COLLX_NCCL4PY_SPEC="nccl4py[cu13]==0.3.1"
COLLX_NCCL_EP_REPO="https://github.com/NVIDIA/nccl"
COLLX_NCCL_EP_COMMIT="9d22d5dfec8391ee65b56df139d471f8e08e921e"

# Print bounded command output without maintaining a parallel failure taxonomy.
collx_log_tail() {
local log_path="$1"
Expand Down
117 changes: 117 additions & 0 deletions experimental/CollectiveX/runtime/prepare_backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,122 @@ uccl_prepare() {
collx_log "UCCL-EP ready ($COLLX_UCCL_COMMIT, deep_ep wrapper over uccl.ep CPU-proxy runtime)"
}

# ---- NCCL EP lifecycle ------------------------------------------------------

# Slug of the pinned pip spec, safe as a cache-dir path component.
nccl_ep_spec_slug() {
printf '%s' "$COLLX_NCCL4PY_SPEC" | tr -cs 'A-Za-z0-9_.-' '-'
}

# Cache root keyed by cpu + build arch + image + pinned wheel spec, under the shared /cx-cache
# mount ($COLLX_BACKEND_CACHE_ROOT). Returns non-zero when no shared cache is mounted (manual
# runs), so the caller falls back to a node-local install. Mirrors uccl_cache_root.
nccl_ep_cache_root() {
local arch="$1" cpu base image slug
cpu="$(uname -m)"
[[ "$cpu" =~ ^[A-Za-z0-9._-]+$ ]] || return 1
base="${COLLX_BACKEND_CACHE_ROOT:-}"
[[ "$base" = /* ]] || return 1
image="$(printf '%s' "${COLLECTIVEX_IMAGE:-manual}" | tr -cs 'A-Za-z0-9_.-' '-')"
arch="$(printf '%s' "$arch" | tr -cs 'A-Za-z0-9_.-' '-')"
slug="$(nccl_ep_spec_slug)"
printf '%s/nccl-ep-%s-%s-%s-%s' \
"$base" "$cpu" "${arch#-}" "${image#-}" "${slug#-}"
}

# Put the installed wheel ($root/site) on PYTHONPATH for the probe and rank tasks, and the
# wheel-bundled NCCL runtime lib dir ahead of the image torch's older NCCL on the loader path
# (nccl.ep needs NCCL >= 2.29.3's Device API + GIN; the image torch bundles an older NCCL). Both
# PYTHONPATH and LD_LIBRARY_PATH are already carried to the ranks by write_rank_env.
nccl_ep_activate() {
local root="$1" site="$1/site" nccl_lib
[ -d "$site" ] || { collx_log "ERROR: NCCL EP cache site is unavailable"; return 1; }
export PYTHONPATH="$site${PYTHONPATH:+:$PYTHONPATH}"
for nccl_lib in "$site"/nvidia/nccl*/lib; do
if [ -d "$nccl_lib" ]; then
export LD_LIBRARY_PATH="$nccl_lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
break
fi
done
# NCCL EP group creation gates on the NCCL Device API (LSA symmetric memory), which NCCL only
# advertises when cuMem allocation is enabled; without it ncclEpCreateGroup returns
# ncclInvalidUsage. Persisted here (already in RANK_ENV_VARS) so every rank has it, mirroring
# the launcher's process-wide export. Verified on h100 EP8 (2026-07-21).
export NCCL_CUMEM_ENABLE=1
}

nccl_ep_probe() {
# import torch FIRST so libc10/libnccl are resident before the nccl.ep extension dlopens; then
# nccl.core (libnccl.so) and nccl.ep (libnccl_ep.so JIT runtime). nccl.ep.__init__ runs its own
# libnccl/libnccl_ep CUDA-major consistency check on import and raises ImportError on mismatch.
python3 - <<'PY'
import torch # noqa: F401
import nccl.core # noqa: F401
import nccl.ep # noqa: F401
PY
}

# Primary install: the published nccl4py[cu13] wheel + deps into $root/site via pip --target
# (self-contained; the runtime imports it through PYTHONPATH, so cache-hit and cache-miss paths
# import identically — mirrors uccl_install's copy-to-cache scheme). The from-source fallback
# (OpenMPI + build NCCL + contrib/nccl_ep from COLLX_NCCL_EP_COMMIT, with a matching launcher
# source-staging arm) is deferred until bring-up shows the wheel does not ship libnccl_ep.so.
nccl_ep_install() {
local root="$1" site="$1/site"
if [ -e "$root" ] || [ -L "$root" ]; then
rm -rf "$root" || { collx_log "ERROR: incomplete NCCL EP cache-reset failed"; return 1; }
fi
mkdir -m 700 "$root" || { collx_log "ERROR: NCCL EP cache-create failed"; return 1; }
mkdir -p "$site" || { collx_log "ERROR: NCCL EP cache-site-create failed"; return 1; }
collx_log "NCCL EP: installing $COLLX_NCCL4PY_SPEC (pip --target)"
# --target installs into an isolated tree and does not touch the system env, so PEP 668 does
# not apply; torch is imported from the image at runtime (nccl.ep's torch interop resolver).
python3 -m pip install -q --disable-pip-version-check --no-input \
--target "$site" "$COLLX_NCCL4PY_SPEC" >&2 2>&1 \
|| { collx_log "ERROR: NCCL EP nccl4py install failed"; return 1; }
nccl_ep_activate "$root" \
|| { collx_log "ERROR: NCCL EP environment activation failed"; return 1; }
nccl_ep_probe || { collx_log "ERROR: NCCL EP import probe failed"; return 1; }
: > "$root/.ready"
}

# NCCL EP lifecycle: install once per (arch, image, wheel-spec) into the shared /cx-cache behind
# an flock + .ready marker, reused on every later allocation (mirrors uccl_prepare); fall back to
# a node-local install when no shared cache is mounted (e.g. a manual run).
nccl_ep_prepare() {
local arch root ready lock_path
command -v python3 >/dev/null || { collx_log "ERROR: python3 unavailable for NCCL EP"; return 1; }
arch="$(cuda_arch)" || return 1
if root="$(nccl_ep_cache_root "$arch")"; then
ready="$root/.ready"; lock_path="${root}.lock"
command -v flock >/dev/null \
|| { collx_log "ERROR: flock is required for NCCL EP caching"; return 1; }
mkdir -p "${root%/*}" || return 1
collx_log "NCCL EP: preparing $COLLX_NCCL4PY_SPEC (shared cache $root)"
if ! (
[ ! -L "$lock_path" ] || { collx_log "ERROR: NCCL EP cache lock is unsafe"; exit 1; }
(umask 077; : >> "$lock_path") && chmod 600 "$lock_path" \
|| { collx_log "ERROR: NCCL EP cache-lock-create failed"; exit 1; }
exec 9<>"$lock_path" || { collx_log "ERROR: NCCL EP cache-lock-open failed"; exit 1; }
flock 9 || { collx_log "ERROR: NCCL EP cache-lock-acquire failed"; exit 1; }
if [ ! -f "$ready" ] || [ ! -d "$root/site" ]; then
nccl_ep_install "$root" || exit 1
fi
); then
collx_log "ERROR: shared NCCL EP environment is incomplete"; return 1
fi
else
root="/tmp/collectivex-nccl-ep-cache-$(nccl_ep_spec_slug)"
collx_log "NCCL EP: preparing $COLLX_NCCL4PY_SPEC (node-local $root; no shared cache mounted)"
if [ ! -f "$root/.ready" ] || [ ! -d "$root/site" ]; then
nccl_ep_install "$root" || return 1
fi
fi
nccl_ep_activate "$root" || return 1
nccl_ep_probe || { collx_log "ERROR: NCCL EP import probe failed"; return 1; }
collx_log "NCCL EP ready ($COLLX_NCCL4PY_SPEC; libnccl_ep.so JIT runtime, NCCL Device API LSA/GIN)"
}

# ---- container boundary ----------------------------------------------------

write_rank_env() {
Expand Down Expand Up @@ -462,6 +578,7 @@ main() {
|| { collx_log "ERROR: MoRI backend import failed"; return 1; }
;;
uccl-ep) uccl_prepare || return 1 ;;
nccl-ep) nccl_ep_prepare || return 1 ;;
*)
collx_log "ERROR: unknown backend preparation request"
return 1
Expand Down
3 changes: 3 additions & 0 deletions experimental/CollectiveX/sweep_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@
# caller-prequantized dispatch (DeepEP blockwise e4m3fn; MoRI per-SKU e4m3 tensor cast).
# A precision the backend does not list is gated out at generation (no case emitted).
BACKEND_PRECISIONS = {
"deepep-v2": ("bf16", "fp8"),
"mori": ("bf16", "fp8"),
"uccl-ep": ("bf16", "fp8"),
# NCCL EP is BF16-only this release: its FP8 machinery exists upstream but RELEASE.md
# lists it unsupported/untested, so no FP8 case is emitted (see bench/ep_nccl.py).
"nccl-ep": ("bf16",),
}
# Short shard-ID slug per non-normal mode. Normal-mode shard IDs carry no mode

Check warning on line 43 in experimental/CollectiveX/sweep_matrix.py

View check run for this annotation

Claude / Claude Code Review

README.md / docs/methodology.md not updated for nccl-ep (stale + now-false claims)

This PR adds nccl-ep as a BF16-only backend (sweep_matrix.py:41) with new backend/ll_backends rows in platform_config.json, but doesn't refresh README.md or docs/methodology.md the way the UCCL-EP PR (#2288) did. As a result, README.md:16 / methodology.md:24 now falsely claim FP8 dispatch happens 'on every backend', and README.md's backend scope table (lines 66-70) plus the ll_backends prose (lines 28-30) omit nccl-ep entirely. This is a pre-existing-pattern gap the author already flagged and of
Comment on lines 36 to 43

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.

🟡 This PR adds nccl-ep as a BF16-only backend (sweep_matrix.py:41) with new backend/ll_backends rows in platform_config.json, but doesn't refresh README.md or docs/methodology.md the way the UCCL-EP PR (#2288) did. As a result, README.md:16 / methodology.md:24 now falsely claim FP8 dispatch happens 'on every backend', and README.md's backend scope table (lines 66-70) plus the ll_backends prose (lines 28-30) omit nccl-ep entirely. This is a pre-existing-pattern gap the author already flagged and offered to fix here or in a follow-up.

Extended reasoning...

What's stale/false: Two related documentation problems land with this PR because it changes backend capability without touching README.md or docs/methodology.md:

  1. Active factual inaccuracy. README.md:16 states dispatch precision is "a BF16 control plus an FP8 dispatch on every backend, caller-prequantized in normal mode," and docs/methodology.md:24 says similarly "a BF16 control and an FP8 dispatch (bf16, fp8)." This PR adds nccl-ep as a fourth backend that is BF16-only: sweep_matrix.py sets BACKEND_PRECISIONS["nccl-ep"] = ("bf16",) (diff line 41), and the new test_nccl_ep_rollout_shape test asserts every nccl-ep case emits precision == "bf16" with no FP8 case ever generated. So "an FP8 dispatch on every backend" is no longer true — it's false for one of the four backends CollectiveX now ships.

  2. Missing-row staleness. README.md's "Backend | Current scope" table (lines 66-70) and its ll_backends summary prose (lines 28-30, "currently DeepEP V2 EP8 on H100/H200/B200, MoRI EP8 on ..., and UCCL-EP EP8 on H100/H200/B200 only") still enumerate exactly DeepEP V2 / MoRI / UCCL-EP. This PR's configs/platform_config.json changes add nccl-ep to backends on h100/h200/b200-dgxc/b300 (EP8) and gb200/gb300 (EP8+16), and to ll_backends on the four x86 SKUs (EP8) — none of which are reflected in the docs. docs/methodology.md lines 75-79 repeat the same enumeration gap.

Why nothing catches this today: These are prose/table facts in Markdown, not code assertions — there is no test that cross-checks README/methodology text against platform_config.json or sweep_matrix.BACKEND_PRECISIONS, so nothing fails CI when the docs drift from the code they describe.

Precedent: The prior UCCL-EP PR (#2288), which added a new backend in exactly the same shape (new adapter + platform_config rows + precision entry), refreshed both README.md and docs/methodology.md in the same PR. This PR's own description explicitly acknowledges the gap: "the UCCL-EP precedent (#2288) refreshed README.md and docs/methodology.md; this PR does not... I'm happy to add the backend's documentation rows in a follow-up or here."

Step-by-step proof of the false claim:

  1. Read README.md:16 — verbatim: "dispatch precision is a swept dimension — a BF16 control plus an FP8 dispatch on every backend, caller-prequantized in normal mode."
  2. Read the diff's sweep_matrix.py lines 36-43 — BACKEND_PRECISIONS["nccl-ep"] = ("bf16",), with a comment noting NCCL EP's FP8 machinery is unsupported/untested upstream this release.
  3. Read sweep_matrix.py's case-generation loop — for a runnable cell, emit_precisions is filtered to supported = [p for p in SWEEP["precisions"] if p in backend_precisions], so for nccl-ep this is just ["bf16"]; no fp8 case is ever emitted for this backend.
  4. Read the new test_nccl_ep_rollout_shape test — it asserts {item["case"]["precision"] for ... if backend == "nccl-ep"} == {"bf16"}, confirming zero FP8 cases exist for nccl-ep across the whole matrix.
  5. Therefore "an FP8 dispatch on every backend" is contradicted by the codebase itself post-merge: nccl-ep is a real, sweep-registered backend that never dispatches FP8.

Fix: Update README.md:16 and docs/methodology.md:24 to scope the FP8 claim to the backends that support it (deepep-v2/mori/uccl-ep) rather than "every backend," and add nccl-ep rows to the "Backend | Current scope" table and the ll_backends prose in both files, mirroring what #2288 did for uccl-ep. Given the author already offered to do this in-PR or as a follow-up, either is acceptable — this is a documentation-only gap with no effect on sweep execution or correctness, so it shouldn't block merge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 78e595c92.

  • README.md:16 and docs/methodology.md:24 no longer claim FP8 on every backend — the claim is scoped to DeepEP V2 / MoRI / UCCL-EP, with NCCL EP called out as BF16-only this release. methodology.md also names BACKEND_PRECISIONS as the place that decides it.
  • Added a NCCL EP row to the README backend scope table and the matching paragraph in methodology.md: NCCL Device API (LSA intra-node, GIN inter-node) via nccl4py, HIGH_THROUGHPUT in normal mode, NVIDIA-only + CUDA 13 only, EP8 on H100/H200/B200/B300 plus EP8+EP16 on GB200/GB300 inside MNNVL, x86 EP16 an unsupported coverage row.
  • Both ll_backends passages now say NCCL EP has no low-latency row on any SKU and why ([Issue]: OW_LATENCY dispatch/combine receives time out on GB200/GB300 NVL72 NVIDIA/nccl#2303) — reflecting d58d56566 rather than the config state this review saw.

# segment so existing references stay valid; a low-latency shard adds "-ll".
_MODE_SLUG = {"low-latency": "ll"}

Expand Down
58 changes: 57 additions & 1 deletion experimental/CollectiveX/tests/test_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def test_runnable_cases_fan_out_over_backend_precisions(self):
if precision in sweep_matrix.BACKEND_PRECISIONS[cell[1]]
}
self.assertEqual(precisions, expected, cell)
# Every backend (deepep-v2, mori, uccl-ep) realizes BF16 and FP8.
# Every backend that lists FP8 (deepep-v2, mori, uccl-ep) realizes BF16 and FP8;
# nccl-ep is BF16-only, so the cross-cell union of realized precisions stays {bf16, fp8}.
self.assertEqual(
{precision for precisions in by_cell.values() for precision in precisions},
{"bf16", "fp8"},
Expand Down Expand Up @@ -202,6 +203,61 @@ def test_uccl_ep_rollout_shape(self):
}
self.assertEqual(ll_skus, {"h100-dgxc", "h200-dgxc", "b200-dgxc"})

def test_nccl_ep_rollout_shape(self):
# NCCL-EP's rollout, locked to the on-metal verdict (2026-07-22, all via the real launcher):
# * RDMA scale-out SKUs (h100/h200/b200/b300): EP8 runnable, EP16 an UNSUPPORTED coverage
# row. EP16 cross-node rides NCCL's kernel-initiated GDAKI GIN, which faults identically
# on RoCE (h100/b200/b300) and InfiniBand (h200) — a reproducible NCCL-EP v0.1.0 internode
# limitation, so EP16 is scoped out like uccl-ep's.
# * GB NVL72 SKUs (gb200/gb300, MNNVL, gb-nv launcher, 4 GPU/node): EP8 AND EP16 runnable —
# both stay inside the 72-GPU scale-up domain (world <= scale_up_domain => LSA, no GIN),
# so EP16 works over MNNVL where the RDMA-GIN path walls.
# * AMD SKUs: no rows (NCCL EP is NVIDIA-only; AMD runs mori).
# LL (decode) EP8 on the four RDMA SKUs only (Device-API LSA path; b300 works despite deepep-v2's
# IBGDA blocker) — NOT on GB (LL over MNNVL crashes in comm teardown, gb ll_backends omitted).
# BF16 only — no FP8 case (NCCL EP FP8 unsupported this release).
document = matrix(backend="all")
runnable = {
(item["sku"], item["case"]["ep"])
for item in document["requested_cases"]
if item["case"]["backend"] == "nccl-ep" and item["disposition"] == "runnable"
}
unsupported = {
(item["sku"], item["case"]["ep"])
for item in document["requested_cases"]
if item["case"]["backend"] == "nccl-ep" and item["disposition"] == "unsupported"
}
rdma_skus = {"h100-dgxc", "h200-dgxc", "b200-dgxc", "b300"}
gb_skus = {"gb200", "gb300"}
# RDMA SKUs: EP8 runnable + EP16 unsupported. GB SKUs: EP8 and EP16 both runnable.
self.assertEqual(
runnable,
{(sku, 8) for sku in rdma_skus} | {(sku, ep) for sku in gb_skus for ep in (8, 16)},
)
self.assertEqual(unsupported, {(sku, 16) for sku in rdma_skus})
# Offered on the six NVIDIA SKUs; never on AMD.
offered = {sku for sku, _ in runnable | unsupported}
self.assertEqual(offered, rdma_skus | gb_skus)
for absent in ("mi355x", "mi325x-tw", "mi300x-tw"):
self.assertNotIn(absent, offered)
# Every nccl-ep case is BF16 (FP8 unsupported this release).
self.assertEqual(
{
item["case"]["precision"]
for item in document["requested_cases"]
if item["case"]["backend"] == "nccl-ep"
},
{"bf16"},
)
# Low-latency (decode) EP8 on the four RDMA SKUs only — not GB.
ll = {
(item["sku"], item["case"]["ep"])
for item in document["requested_cases"]
if item["case"]["backend"] == "nccl-ep"
and item["case"]["mode"] == "low-latency"
}
self.assertEqual(ll, {(sku, 8) for sku in rdma_skus})

def test_invalid_filters_fail_closed(self):
for options in (
{"exclude_skus": "unknown"},
Expand Down
Loading
Loading