Skip to content

CollectiveX: wire-basis bandwidth, nccl-ep LL hold, routed HT window, AsyncLL, DSv4-Pro workload - #2786

Open
Oseltamivir wants to merge 15 commits into
mainfrom
cx-measure-fixes
Open

CollectiveX: wire-basis bandwidth, nccl-ep LL hold, routed HT window, AsyncLL, DSv4-Pro workload#2786
Oseltamivir wants to merge 15 commits into
mainfrom
cx-measure-fixes

Conversation

@Oseltamivir

@Oseltamivir Oseltamivir commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Five measurement-integrity fixes, one commit each:

  1. Wire-basis bytes (wire_byte_provenance): DeepEP/UCCL/NCCL LL kernels move one copy per (token, expert); byte_provenance always carried the rank-deduplicated numerator, so LL "payload bandwidth" was a lower bound published as the wire rate (34.1% low on nccl-ep LL EP8 @t=128: 77.4 vs 117.4 MB) and not cross-backend comparable. Rows now carry both bases; bandwidth.py divides from the wire basis (fallback only understates). App-side consumption fix goes to InferenceX-app (packages/db/src/collectivex/reader.ts maps only byte_provenance today).
  2. nccl-ep low-latency held fleet-wide: the source's own comment says the T≤128 clamp is NOT a safety boundary for the un-fenced combine race (DeepEP [NV] dsr1 fp4 b200 trt agg mtp update #642's fence, absent upstream) — lower rungs are less likely, not immune. Green clamped rows are not publication-valid; removed from ll_backends until a fenced wheel ships. Methodology stops describing T=256 as a capacity limit.
  3. nccl-ep HT charges ncclEpUpdateHandle in the timed dispatch: it is documented as a per-step collective and production routing changes every MoE layer; excluding it made HT the one window omitting its routing work while deepep-v2/uccl-ep/MoRI/FlashInfer all carry theirs per call. kernel_generationnccl-ep-ht-routed (the per-row discriminator earlier NCCL measurement changes lacked).
  4. MoRI LL measures AsyncLL, the kernel SGLang actually deploys for low-latency (split-phase send+recv, block 64/rdma 32/warps 8; sglang moriep asserts AsyncLL). IntraNodeLL rows described an off-production path (kernel_generation intranode-ll discriminates them). Fails closed on wheels lacking the recv API — single-call AsyncLL returns before payload lands.
  5. Workload → deepseek-v4-pro: hidden 7168, top-k 6, 384 routed experts (the dsv4 shape the rest of InferenceX benchmarks). Workload name is in every case_id — clean identity break in the durable store.

Tests: all five CollectiveX test modules green; matrix regenerates 308 cases, all v4-pro, nccl LL absent, mori LL present. On-metal validation of (3) and (4) in flight on this branch: nccl-ep normal @h200 run 33319407695, mori low-latency @mi355x run 33319409233 — results will be posted here.


Note

Medium Risk
Changes alter published latency/bandwidth contracts, disable nccl-ep LL fleet-wide, and change DeepEP build pins—high impact for benchmark comparability but scoped to experimental CollectiveX harness/runtime.

Overview
CollectiveX tightens measurement integrity across EP backends: artifacts now record wire_byte_provenance alongside rank-deduplicated byte_provenance, and bandwidth.py divides GB/s from the wire basis so low-latency kernels that move one copy per (token, expert) are not understated (~34% on nccl-ep LL). Docs and report headers switch from “logical payload” to wire-basis semantics, with fallback only as a lower bound.

NCCL EP: high-throughput dispatch includes ncclEpUpdateHandle in the timed window (kernel_generation nccl-ep-ht-routed); low-latency is removed from ll_backends fleet-wide until a fenced wheel ships (documented race, not a safe T≤128 clamp). MoRI low-latency now benchmarks AsyncLL with split-phase send+recv (production/SGLang path), not IntraNodeLL, with fail-closed if recv APIs are missing.

The sweep workload becomes deepseek-v4-pro (top-k 6, 384 routed experts). DeepEP V2 build/cache pins cu13 NVSHMEM, torch 2.11, and NVCC dlink arch via NVCC_PREPEND_FLAGS; mi355x image and b300 node excludes update in platform_config.json. Tests add wire-basis preference for bandwidth fits.

Reviewed by Cursor Bugbot for commit b8b7467. Bugbot is set up for automated code reviews on this repo. Configure here.

…te definitions

DeepEP/UCCL/NCCL low-latency kernels move one copy per (token, expert)
assignment; normal mode and MoRI LL move rank-deduplicated (token, dest-rank)
copies. byte_provenance always carried the smaller deduplicated numerator, so
any bandwidth divided from it on a token-expert backend was a lower bound
published as the wire rate (34.1% low on nccl-ep LL EP8 at T=128: 77.4 MB vs
the 117.4 MB the kernels move) and not comparable across backends.

Rows now carry wire_byte_provenance beside byte_provenance: identical for
token-rank receives, per-assignment for token-expert ones, basis declared by
the existing logical_copies.wire discriminator. bandwidth.py divides from the
wire basis (falling back to the deduplicated one for pre-wire artifacts, which
only ever understates).
…nce ships

The T<=128 ladder clamp reduces exposure to the un-fenced shared-memory combine
race (DeepEP #642's fence, absent at our pin and at NVIDIA master); the source
has said all along it is not a safety boundary — lower rungs are less likely to
hit the race, not immune. Green clamped rows are therefore not
publication-valid, and describing T=256 as a capacity limit was wrong. Rows
held in the registry, normal mode unaffected.
…d HT dispatch

ncclEpUpdateHandle is documented as a per-step collective and production
routing changes every MoE layer, so a serving step pays it before every HT
dispatch at the handle's full token capacity. Excluding it (as NVIDIA's
ep_bench does) made HT dispatch the one window that omitted its routing work
while deepep-v2, uccl-ep, MoRI and FlashInfer all carry theirs per call.
kernel_generation bumps to nccl-ep-ht-routed — the per-row discriminator the
earlier NCCL measurement changes lacked.
…on deploys

SGLang's moriep dispatcher maps low-latency to AsyncLL split-phase
(dispatch_send/recv + combine_send/recv, block_num 64 rdma 32 warps 8) and
asserts that kernel type; IntraNodeLL is not selected by any engine, so the
previous rows measured an off-production path (kernel_generation intranode-ll
discriminates them). The timed windows now run AsyncLL send+recv back-to-back
and the adapter fails closed on wheels whose AsyncLL lacks the recv API —
single-call AsyncLL returns before any payload lands. Pending on-metal
validation on mi355x.
deepseek-v4-pro: hidden 7168, top-k 6, 384 routed experts — the dsv4 shape the
rest of InferenceX benchmarks (operatorx testlists, HF-config-derived). The
workload name is part of every case_id, so this is a clean identity break in
the durable store: no v3 row can collide with a v4-pro row. Ladders and seed
unchanged.
@Oseltamivir

Copy link
Copy Markdown
Collaborator Author

On-metal validation, both green:

MoRI AsyncLL @ mi355x (run 33319409233, LL EP8 bf16+fp8): kernel_generation=async-ll, full ladder correctness passed (max_relative_error 0.012, within combine tolerance), wire basis correctly stays rank-deduplicated for MoRI. The July image wheel does carry the split-phase recv API. T=1 roundtrip p50 125.1µs bf16 / 123.5µs fp8; pair period 90.2/95.0µs — slower than the old IntraNodeLL rows, as expected for the deployed RDMA-staged path; the async-ll generation string separates the two populations in the store.

nccl-ep HT routed window @ h200 (run 33319407695, normal EP8 bf16 decode+prefill): kernel_generation=nccl-ep-ht-routed, correctness passed everywhere, wire_byte_provenance present on every row. Dispatch now carries the per-step ncclEpUpdateHandle (T=1 p50 113.1µs, T=128 222.0µs, T=8192 prefill 2333µs) — the expected upward shift vs pre-routed rows, which the generation bump keeps separable.

App-side companion: SemiAnalysisAI/InferenceX-app#953 (reader divides rates from the wire basis; matrix flips nccl-ep LL cells to held).

Note both validations ran under the deepseek-v3 workload (dispatched before the v4-pro commit); the first post-merge sweep populates the deepseek-v4-pro identities.

@claude claude Bot left a comment

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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked whether the workload swap to topk=6 (from the old topk=8) invalidates the hardcoded COMBINE_REL_TOL = 8 * 2.0 ** -8 correctness-gate tolerance in ep_harness.py — it doesn't: the bound derives from topk (fewer stores with topk=6 only tightens the true error, so the topk=8-sized constant remains a valid, if slightly loose, upper bound).

Extended reasoning...

Confirmed findings already exist as inline comments for this PR (README.md still describing MoRI's old IntraNodeLL kernel, and methodology.md's byte-accounting section left describing only the old rank-deduplicated basis), so a full Overview/Security/Scrutiny/Other-factors writeup is not needed here per the findings-present path. I independently examined one additional candidate — whether the deepseek-v4-pro workload swap (topk 8 to 6) breaks the hardcoded COMBINE_REL_TOL tolerance in ep_harness.py, which was sized against topk=8 — and confirmed via the code comments and sweep.json that the bound is a valid (if now slightly loose) upper bound for topk=6, since the tree-summation/sequential accumulation error scales with topk and fewer stores only tightens the true error. This is not a correctness bug, so I'm recording it as ruled out rather than adding a new inline finding.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 experimental/CollectiveX/docs/methodology.md — The "Logical payload bandwidth is: logical_payload_bytes / measured_latency_seconds" section (methodology.md's authoritative English guide for the byte math) was not updated by this diff and still describes only the old rank-deduplicated basis, still citing MoRI's now-renamed IntraNodeLL as the dedup exemplar, with no mention of wire_byte_provenance or the fact that bandwidth.py now divides GB/s from the wire basis by default. A reader following this doc believes GB/s uses rank-deduplicated bytes, but the numbers now use per-assignment wire bytes for LL rows, understating nothing per se but making the doc actively wrong about which basis produced the published rate. …

    Extended reasoning...

    …Fix: rewrite this section to describe the wire basis as the default numerator, byte_provenance as the pre-wire fallback, and drop/replace the stale IntraNodeLL reference.

    bandwidth.py's _wire_bytes() (added by this diff) now prefers wire_byte_provenance and divides GB/s from per-assignment bytes for token-expert LL rows (e.g. nccl-ep LL), a materially different, larger numerator than the logical_payload_bytes this methodology.md section still describes as the sole basis. The PR description claims methodology.md was rewritten to describe the new wire-basis accounting, but this specific section (lines 449-464, under ## Measurement) has zero diff hunks touching it -- confirmed via git diff ... -- methodology.md showing no changes near these lines. A reader relying on this section (the repo's own authoritative English guide per AGENTS.md) to interpret bandwidth.py's GB/s figures, or to sanity-check logical_copies.wire, gets a description of a basis the code no longer uses by default, and the doc still cites the retired IntraNodeLL kernel as the MoRI LL example even…

    Verification: nit (documentation defect; no runtime/behavior failure). The claim is factually accurate. The four diff hunks in methodology.md are at @@ -19, @@ -87/88, @@ -210/211, and @@ -385/390 — none touch lines 449-471. That "Logical payload bandwidth" section is unchanged and still states the numerator is the rank-deduplicated logical count: line 449-451 "Logical payload bandwidth is:… | Severity: nit…

Comment thread experimental/CollectiveX/bench/ep_mori.py
…sis (review)

The Measurement section still described the rank-deduplicated count as the
sole GB/s numerator and cited the retired IntraNodeLL as the dedup exemplar;
bandwidth.py and the app now divide from wire_byte_provenance. The section now
names both accountings, which rows differ, the 34% understatement that
motivated the change, and the fallback behavior for pre-wire artifacts.
@Oseltamivir

Copy link
Copy Markdown
Collaborator Author

Both review findings are now addressed on the branch: the README MoRI row was synced to AsyncLL in 5d3b540, and 97944b4 rewrites methodology's "Logical payload bandwidth" section (the untouched lines ~449-464 the review flagged) around the wire basis — both accountings named, which rows differ, the 34% understatement, and the pre-wire fallback. All five test modules green after the main merge. Dispatching a full all-SKU sweep from this branch now; results will be posted here.

The full v4-pro sweep's two b300 EP16 legs each drew one of these nodes and
failed closed at the network-profile gate (rdma-port-5=inactive = the 5th
selector, mlx5_4). Idle-pool survey: exactly these two nodes have mlx5_4 port
state 1: DOWN; the other 12 idle nodes are 4: ACTIVE — a sick node pair
(likely one leaf/cable pair), not a rail-wide outage. SRE handback owed.
… regression is resolved

All gb300 deepep-v2 LL shards die at the first CUDA op after Buffer init with
cudaErrorUnknown (context poisoned during nvshmem symmetric-heap init over
MNNVL), on every rank, both degrees and precisions. Discrimination: gb300
normal mode green at n2/n4; gb200 LL green with the same image/pin/workload;
a main-branch control with the old workload fails identically — so neither
this PR nor the v4-pro shape. CI-shaped 2-node hand probe (job 27430 on the
pool) reproduces: bootstrap and buffer creation complete, then every rank's
next CUDA call returns unknown error. Driver is 580.159.03 (installed in the
KV-fix window ~2026-08-26..28, after the last green gb300 LL run). Platform
escalation owed; rows held so sweeps don't burn four known-red shards each.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e9ef099. Configure here.

Comment thread experimental/CollectiveX/configs/platform_config.json Outdated
…(mori >= #505)

AsyncLL under the DSv4-Pro workload (topk 6) dies on every shipped
mi35x-mori image: ROCm/mori#505 (AsyncLL slot assignment double-allocates
when top-k does not divide warpSize, fixed upstream 2026-07-31) postdates
them all. On-metal probe of this date-stamped nightly (job 41563, pure
mori.ops): AsyncLL topk6@384 and topk8@256 both pass, split-phase recv API
present. mi355x only — the -tw SKUs keep their image until separately
validated on their docker path.
@Oseltamivir

Copy link
Copy Markdown
Collaborator Author

Full-SKU validation ledger (v4-pro workload, all fixes applied)

Rerun 33356406487 (backend=all, exclude_skus=mi300x-tw,mi325x-tw): 69/70 shards green; with retry 33360247681, every attempted cell has a green artifact — the only reds across both runs are the h100 EP16 TCPStore rendezvous lottery striking one random shard per dispatch (bf16-n2 green in the main run, fp8-n2 green in the retry; documented pre-harness flake class).

Fixes validated on-metal in this rerun:

  • b300 EP16 (both precisions) green after excluding b300-001/b300-016 — idle-pool survey showed exactly those two nodes with mlx5_4 1: DOWN, all 12 others ACTIVE (sick node pair, not a rail outage; SRE handback owed).
  • mi355x mori low-latency green both precisions under topk 6 after the image bump to rocm/sgl-dev:v0.5.18-rocm720-mi35x-20260830: the AsyncLL crash was fix(ep): AsyncLL slot assignment double-allocates when top-k does not divide warpSize ROCm/mori#505 (top-k not dividing warpSize; fixed upstream 07-31, every mi35x-mori-tagged image predates it), bisected on-metal with a pure mori.ops probe (topk8@{256,384} pass, topk6@{256,384} assert). mori normal-mode and uccl-ep also green on the new image.
  • h200 EP16 green both precisions (previous red was a one-off hang-guard wedge).
  • gb300 low-latency held in the registry: nvshmem symmetric-heap init over MNNVL poisons the CUDA context on driver 580.159.03 (CI-shaped 2-node probe reproduces: buffer init completes, every rank's next CUDA call returns cudaErrorUnknown; gb300 normal mode and gb200 LL green with identical image/pin/workload; main-branch control fails identically). Platform escalation, not repo-fixable; rows restore when the driver interaction is resolved.

Both review findings were addressed earlier (README AsyncLL sync in 5d3b540; methodology wire-basis section rewrite in 97944b4).

…un-breaks gb300 LL

The gb300 LL cudaErrorUnknown was ours, not the platform's. 2x2 on the pool:
stock in-image deep_ep (built on nvidia-nvshmem-cu13==3.4.5) runs LL clean on
the exact r01 nodes and driver (580.159.03) where the CI stack dies; the CI
venv's nvidia-nvshmem-cu12==3.3.9 r12 host library survives sm90/sm100 but
poisons the CUDA context during symmetric-heap init over MNNVL on sm103 —
buffer creation returns, the next CUDA call on every rank fails. The earlier
driver-window attribution was a coincidence of timing (pin bump and driver
install overlapped; hold commit e9ef099 superseded).

The wheel spec now lives in common.sh (COLLX_DEEPEP_V2_NVSHMEM_SPEC), is
installed by prepare_backend, and keys the venv cache so every NVIDIA deepep
venv rebuilds once against cu13. gb300 ll_backends restored.
Root cause, finally isolated by holding everything else constant: a venv
identical to prepare_backend's recipe (same DeepEP pin, same nccl-cu13 2.30.4,
same nvshmem-cu13 3.4.5) is green with torch==2.11.0+cu130 and red with
torch==2.10.0+cu130 on the same gb300 r01 nodes (jobs 27760 vs 27430/27757).
torch 2.10.0's bundled CUDA-13 userland poisons the CUDA context during
nvshmem symmetric-heap init over MNNVL on sm103 + driver 580.159.03; sm90 and
sm100 are unaffected. Falsified along the way, in order: the v4-pro workload
(main control), the driver alone (stock image green on the failing nodes —
the user's r03 canary forced that 2x2), the nvshmem cu12 wheel (cu13 rebuild
still red), the DeepEP pin (old-pin venv red). 2.11.0 is what the cu130 image
ships. The torch spec joins the nvshmem spec in the venv cache key, so every
deepep venv rebuilds once.
The DeepEP build compiles every object with the right -gencode (setup.py
'Arch list: 10.3', all compile lines sm_103), but the final nvcc -dlink step
receives NO gencode and falls back to nvcc's CUDA-13 default, sm_75 — the
.so's single linked device image is sm_75 and its kernels can never load on
the target GPU. On gb300/sm103 that surfaced as cudaErrorUnknown at the first
legacy-kernel launch (LL and layout paths; ElasticBuffer JIT paths were
unaffected, which is why normal mode stayed green). Proven by instrumented
runs of prepare_backend's own build (job 27792: compile lines sm_103, step
9/9 -dlink bare, product sm_75) after hand builds of the identical recipe
produced sm_103 and ran LL clean on the failing nodes.

Fix: NVCC_PREPEND_FLAGS carries -gencode=arch=compute_<arch>,code=sm_<arch>
into every nvcc invocation including the dlink. COLLX_DEEPEP_V2_BUILD_GEN
(dlarch1) joins the cache key so the sm_75 venvs with valid .ready markers
rebuild instead of being reused.

The torch 2.10->2.11 bump (previous commit) stays: 2.11 matches the image and
the 2.10-vs-2.11 A/B was run against differently-linked binaries, so its
verdict is voided rather than reversed; the pin question can be revisited
upstream of this fix if anyone cares to re-test.
@Oseltamivir

Copy link
Copy Markdown
Collaborator Author

gb300 low-latency: root-caused, fixed, fleet-validated

Root cause: DeepEP builds with -rdc=true; every object compiled with the correct -gencode …sm_103, but the final nvcc -dlink step received no gencode and CUDA 13's nvcc default (sm_75) won the device link — the shipped _C.so carried a single sm_75 device image that can never load on the target GPU. First legacy-kernel launch → cudaErrorUnknown (indistinguishable from a poisoned context). ElasticBuffer paths JIT at runtime with the right arch, which is why normal mode stayed green and only LL/legacy kernels died. Proof: instrumented run of prepare_backend's own build (compile lines sm_103, step 9/9 -dlink bare, product sm_75 by cuobjdump), plus a green stock-image control on the exact failing nodes — credit to the r03 canary (job 27633) for killing the driver theory and forcing the stock-vs-CI 2×2.

Theories falsified on the way, each by one experiment: the v4-pro workload (main control failed identically), the driver (stock green on failing nodes, same 580.159.03), the nvshmem cu12 wheel (cu13 rebuild still red), the DeepEP pin (old-pin venv red too). The interim torch 2.10→2.11 verdict was confounded (hand builds took a different link path that preserved the arch) and is voided; the 2.11 pin stays because it matches the image.

Fix (commits on this branch): NVCC_PREPEND_FLAGS="-gencode=arch=compute_<arch>,code=sm_<arch>" on the DeepEP build — prepend flags reach every nvcc invocation including the dlink — plus COLLX_DEEPEP_V2_BUILD_GEN in the venv cache key so previously-built sm_75 venvs (valid .ready markers) rebuild instead of being reused. All build-affecting pins (torch, nvshmem, build-gen) now key the cache.

Validation:

  • gb300 LL run 33476729962: 4/4 green, correctness passed every rung; T=1 roundtrip p50 73.9µs bf16-EP8 / 75.9µs bf16-EP16 / 76.6–83.9µs fp8 — MNNVL-class LL latency at both degrees.
  • Uniform-generation fleet LL run 33477867072: 18/18 shards green across h100/h200/b200-nscale/b300/gb200/gb300 — every SKU's venv rebuilt under the fixed key, so link arch is now correct by construction fleet-wide rather than by build-path luck.

gb300 ll_backends is restored; the earlier hold commit is superseded. Upstream note: DeepEP's extension dlink losing the gencode looks reportable to deepseek-ai/DeepEP (their -rdc=true + torch cpp_extension ninja path), happy to file with the repro if wanted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant