Skip to content

CollectiveX: add UCCL-EP as a third pluggable EP backend#2288

Merged
Oseltamivir merged 10 commits into
mainfrom
collectivex-uccl-ep
Jul 21, 2026
Merged

CollectiveX: add UCCL-EP as a third pluggable EP backend#2288
Oseltamivir merged 10 commits into
mainfrom
collectivex-uccl-ep

Conversation

@Oseltamivir

@Oseltamivir Oseltamivir commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What

Adds UCCL-EP (github.com/uccl-project/uccl) as a third pluggable expert-parallel backend in CollectiveX, alongside deepep-v2 (NVIDIA) and mori (AMD).

UCCL-EP is a drop-in, DeepEP-API-identical backend over a GPU→CPU-proxy GPUDirect RDMA transport (plain libibverbs; no NVSHMEM/IBGDA), with cudaIpc scale-up. It runs on both NVIDIA and AMD, exercising a completely different transport than the two existing backends on the same matrix.

Changes

  • Adapter bench/ep_uccl.py over the legacy Buffer surface — normal (unweighted-rank-sum combine) and low-latency (weighted-kernel-sum). Caller-prequantized blockwise FP8 (e4m3fn / e4m3fnuz per arch) with BF16 combine, so the existing fp8-dispatch/bf16-combine correctness gate (COMBINE_REL_TOL) is preserved unchanged.
  • Capability map (configs/platform_config.json): uccl-ep EP8 on h100/h200/b200 + mi355x + mi300x-tw/mi325x-tw. LL is NVIDIA-only EP8 (AMD LL trips a warp-group assertion). b300/gb200/gb300 and EP16 emit honest unsupported rows.
  • Build seam (from-source, in-image) across all three launchers: prepare_backend.sh uccl_prepare (Slurm/enroot + mi-amds) and the launch_mi-tw.sh Docker path (build-once-to-host + PYTHONPATH reuse). common.sh pins the source and emits UCCL_* network env. The AMD build swaps cudaMallocManagedcudaMallocHost (CDNA lacks usable managed memory).
  • CI: uccl-ep added as a dispatchable CollectiveX Sweep backend choice.
  • Tests + docs updated; .collx_sources/ and .collx_uccl_pfx/ gitignored.

Adding a backend extends the matrix under the existing schema (new series / case_ids) — no sweep-version bump.

Validation (on-metal + CI)

  • Unit suite green (49 tests).
  • CI sweep canary (h200): EP8 all four legs green (normal + LL × bf16/fp8) — first run of the codified build seam end to end.
  • On-metal bring-up: h100/h200 all EP8 paths; mi300x-tw/mi325x-tw/mi355x normal bf16+fp8; b200 bf16 normal.

EP16 (cross-node): functional but wall-clock-limited → scoped to EP8

The internode combine kernel asserts num_max_rdma_chunked_send_tokens >= num_warps_per_forwarder. A follow-up commit fixes the adapter's Config (mirror UCCL's own test_internode bench: size buffers from a generous config, num_qps_per_rank = num_sms, drive dispatch/combine with the recommended per-world-size configs). With that fix the CPU proxy connects cross-node to all 16 peers and the light EP16 case passes correctness — but its CPU-proxy throughput overruns the standardized 900s per-case wall-clock budget on heavy token counts. Rather than weaken the guard for one backend, uccl-ep is scoped to EP8 and EP16 is an honest unsupported coverage row (mirroring the mori EP16 re-wall); the internode Config fix stays in the adapter so EP16 is one budget change away if pursued.

UCCL-EP (github.com/uccl-project/uccl) is a drop-in, DeepEP-API-identical
expert-parallel backend over a GPU->CPU-proxy GPUDirect RDMA transport
(libibverbs; no NVSHMEM/IBGDA) with cudaIpc scale-up. It runs on both NVIDIA
and AMD, slotting in beside deepep-v2 (NVIDIA) and mori (AMD).

- adapter bench/ep_uccl.py over the legacy Buffer surface: normal
  (unweighted-rank-sum combine) and low-latency (weighted-kernel-sum);
  caller-prequantized blockwise FP8 (e4m3fn / e4m3fnuz per arch) with BF16
  combine, matching the existing fp8-dispatch/bf16-combine gate unchanged
- registered on h100/h200/b200 (EP8+EP16), mi355x (EP8+EP16),
  mi300x-tw/mi325x-tw (EP8). LL is NVIDIA-only (AMD LL trips a warp-group
  assert); AMD SKUs keep mori-only LL
- from-source in-image build seam across all three launchers:
  prepare_backend.sh uccl_prepare (Slurm/enroot + mi-amds) and the
  launch_mi-tw.sh Docker path (build-once-to-host + PYTHONPATH reuse);
  common.sh pins the source + emits UCCL_* network env. The AMD build swaps
  cudaMallocManaged->cudaMallocHost (CDNA lacks usable managed memory)
- tests + docs updated; .collx_sources/ and .collx_uccl_pfx/ gitignored

Adding a backend extends the matrix under the existing schema (new series /
case_ids), so no sweep-version bump.
Comment thread experimental/CollectiveX/runtime/prepare_backend.sh
Comment thread experimental/CollectiveX/bench/ep_uccl.py
…nstraint)

The internode combine kernel asserts num_max_rdma_chunked_send_tokens >=
num_warps_per_forwarder (ep/src/internode.cu:3091). The adapter's hand-rolled
Config(24, 8, 256) left num_max_rdma_chunked_send_tokens at its default (6),
which fails for EP16 (EP8 intranode has no RDMA path, so it was unaffected).

Mirror UCCL's own test_internode bench for the internode path: size the
NVLink+RDMA staging from a generous sizing Config, give each rank num_sms QPs,
and drive dispatch/combine with the per-world-size recommended configs
(Buffer.get_dispatch_config/get_combine_config), which set rdma-chunked-send to
20/12 for EP16 and satisfy the kernel constraints. Intranode EP8 keeps its
validated single-NVLink-buffer recipe unchanged.
…-limited)

The internode Config fix lands uccl-ep cross-node RDMA (the CPU proxy connects
to all peers and the light EP16 case passes dispatch/combine/correctness), but
its CPU-proxy throughput overruns the standardized 900s per-case wall-clock
budget (COLLX_RUN_TIMEOUT) on heavy token counts. Rather than weaken the guard
for one backend, scope uccl-ep to EP8 in the registry and mark EP16 an honest
unsupported coverage row on every supported SKU — mirroring the mori EP16
re-wall. The internode Config fix stays in ep_uccl.py so EP16 is one budget
change away if pursued later.

deepep-v2 and mori EP16 are untouched; uccl-ep LL stays NVIDIA-only EP8.
…m build)

The wrapper's setup.py has install_requires=["uccl"], which resolves to the PyPI
uccl metapackage -> the prebuilt uccl-cu12 wheel. That wheel exists only for
CUDA, so `python setup.py install` hard-fails on ROCm ("Could not find suitable
distribution for uccl-cu12"); NVIDIA masked the bug by resolving the cu12 wheel.
Our from-source ep build already installs uccl.ep into site-packages/uccl, so the
wrapper needs no PyPI dep. Install it with `pip install --no-deps` in both AMD
build paths (prepare_backend.sh uccl_prepare + launch_mi-tw.sh), making the
source build authoritative on both vendors. Validated on-metal mi325x-tw (gfx942):
ep build + wrapper --no-deps + import deep_ep all clean.
…nup perms)

The -tw docker build wrote the persisted deep_ep/uccl prefix into
$COLLX_DIR/.collx_uccl_pfx, i.e. under the workflow's isolated job root. The
build container runs as root, so those files are root-owned; the workflow's
"Cleanup isolated workspace" step (non-root runner) then fails to `rm -rf` them
("Permission denied"), reddening a leg whose benchmark actually succeeded
(status=success, rc=0). Move the prefix to a node-local /tmp path keyed on the
pinned commit (outside cleanup's scope) and bind-mount it read-write into the
build container and read-only into each case container (PYTHONPATH=/uccl_pfx).
Bonus: a second -tw leg on the same node reuses the build. Empty-array mount
expansion is set -u safe so the mori path is unaffected.
…keying, UCCL GID env, FP8 LL docs

- ep_uccl.py: vendor-key the normal-mode SM budget (24 CUDA / 64 HIP) to match UCCL's
  intranode bench (num_sms = 24 if torch.version.cuda else 64); a flat 24 understated AMD.
- launch_mi-tw.sh: key the uccl-ep build cache on commit + image content-id + arch (not
  commit alone) and publish atomically via a .ready marker written only after the import
  check, so an interrupted copy or failed import can no longer poison future jobs.
- common.sh: UCCL-EP reads only UCCL_* selectors — export UCCL_IB_GID_INDEX (RoCE) and
  UCCL_IB_HCA explicitly; drop the false NCCL_IB_HCA/NCCL_IB_GID_INDEX fallback claim.
- README + ep_uccl.py docstring: FP8 dispatch is caller-prequantized in normal mode; in
  low-latency the caller sends BF16 and the decode kernel quantizes to e4m3 internally.
…stale gitignore

- common.sh: UCCL-EP reads UCCL_IB_HCA and falls back to NCCL_IB_HCA, and its filter honors the
  leading '=' exact-match and ':port' syntax (ep/src/rdma.cpp). Export UCCL_IB_HCA="=$COLLX_RDMA_DEVICES"
  to mirror NCCL_IB_HCA rather than a bare name list that prefix-matches (mlx5_1 -> mlx5_1,10..19) and
  drops the port; fix the comment. Only the GID index has no NCCL fallback in UCCL's EP path (it reads
  only UCCL_IB_GID_INDEX, ep/include/rdma_util.hpp), so that stays an explicit UCCL_* export.
- README.md / docs/methodology.md: FP8 dispatch is caller-prequantized only in normal mode; the
  low-latency DeepEP and UCCL-EP kernels quantize internally from BF16 (MoRI LL stays caller-
  prequantized). Drop the blanket "every case uses layout-and-dispatch-v1 semantics" — low-latency
  cases use each backend's decode-kernel semantics.
- .gitignore: drop the stale .collx_uccl_pfx/ entry; the -tw build persists to node-local /tmp.
…llocation build cache

- ep_uccl.py: the low-latency capability guard used `and`, so it raised only when BOTH LL
  entry points were absent; De Morgan makes the intended check `or` — fail closed at buffer
  construction whenever EITHER low_latency_dispatch or get_low_latency_rdma_size_hint is
  missing, instead of a raw AttributeError deeper in the timed path. Name both in the message.
- prepare_backend.sh: cache the from-source UCCL build the way deepep_prepare does — build
  once per (cpu, arch, image, commit) under $COLLX_BACKEND_CACHE_ROOT behind an flock + .ready
  marker, persist the built deep_ep/uccl packages and put them on PYTHONPATH (write_rank_env
  carries it to the ranks), reused on every later allocation. Node-local fallback when no
  shared cache is mounted. Ends the full recompile on every uccl-ep shard on single-slurm/mi-amds.
- launch_mi-amds.sh: mount the /cx-cache backend cache for uccl-ep (mirrors single-slurm); the
  mori path is left untouched.
@Oseltamivir
Oseltamivir merged commit 75d7e55 into main Jul 21, 2026
15 checks passed
@Oseltamivir
Oseltamivir deleted the collectivex-uccl-ep branch July 21, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant