CollectiveX MVP v0.1#2004
Conversation
| rsync -a --delete --delete-excluded \ | ||
| --exclude='__pycache__/' --exclude='results/' --exclude='.cx_workloads/' \ | ||
| --exclude='configs/platforms.yaml' --exclude='private-infra.md' \ | ||
| --exclude='goal.md' --exclude='notes.md' \ | ||
| "$repo_root/experimental/CollectiveX" "$stage_dir/experimental/" >/dev/null 2>&1 \ | ||
| || cx_die "staging CollectiveX failed" |
There was a problem hiding this comment.
🔴 The setup step writes the shard JSON to experimental/CollectiveX/results/.shard_${matrix.id}.json and sets CX_SHARD_FILE=results/.shard_${matrix.id}.json (relative), but cx_stage_repo (runtime/common.sh:145-150) rsyncs the CollectiveX tree with --exclude='results/' --delete-excluded and drops the shard file — so for every staged single-tray SKU (b300 always; gb200/gb300 with EP4 via CX_NODES<=1), the [ -f "$CX_SHARD_FILE" ] guard at run_in_container.sh:458 fails and execution falls into the single-bench else branch (line 556+), silently running one wrong-config default (uniform/decode/bf16, empty case_id) instead of the shard's N scheduled cases. Downstream make_bundle will catch this via missing_identity/coverage but only after GPU allocation was spent on the wrong workload. Cheap fix: allow-list the shard file through the rsync (--include='experimental/CollectiveX/results/' --include='experimental/CollectiveX/results/.shard_*.json' before the results/ exclude), copy the shard file into the stage dir after the rsync, or resolve CX_SHARD_FILE against the original repo root in run_in_container.sh's SHARD guard the way the rack (EP8) launchers already do (see launch_gb300-nv.sh:92-93 / launch_gb200-nv.sh cx_ep_cases).
Extended reasoning...
The bug
The sweep workflow's shard-fanout step writes the resolved case list to experimental/CollectiveX/results/.shard_${matrix.id}.json:
# .github/workflows/collectivex-sweep.yml
env:
CX_SHARD_FILE: results/.shard_${{ matrix.id }}.json # RELATIVE path
...
- name: Extract shard from matrix artifact
working-directory: experimental/CollectiveX
run: |
...
json.dump({...,'cases':s['cases']}, open('results/.shard_${{ matrix.id }}.json','w'))The physical file therefore lands at $REPO/experimental/CollectiveX/results/.shard_<id>.json, and CX_SHARD_FILE=results/.shard_<id>.json is interpreted relative to the container's cwd, which is /ix/experimental/CollectiveX.
For every SKU that requires CX_STAGE_DIR (b300 always; gb200/gb300 with EP4 via the CX_NODES<=1 delegate path in launch_gb200-nv.sh:57 / launch_gb300-nv.sh:47), the launcher calls:
# launch_b300.sh:34, launch_gb200-nv.sh:52, launch_gb300-nv.sh:24
MOUNT_SRC="$(cx_stage_repo "$REPO_ROOT" "$CX_STAGE_DIR")"which rsyncs the tree with an exclude that drops results/:
# experimental/CollectiveX/runtime/common.sh:145-150
rsync -a --delete --delete-excluded \
--exclude='__pycache__/' --exclude='results/' --exclude='.cx_workloads/' \
--exclude='configs/platforms.yaml' --exclude='private-infra.md' \
--exclude='goal.md' --exclude='notes.md' \
"$repo_root/experimental/CollectiveX" "$stage_dir/experimental/"Both --exclude='results/' and --delete-excluded guarantee that the shard file the workflow just wrote is missing from the stage dir.
The consequence at runtime
The container mounts $MOUNT_SRC:/ix, cwd=/ix/experimental/CollectiveX. Inside run_in_container.sh, the SHARD guard resolves CX_SHARD_FILE relative to that cwd:
# runtime/run_in_container.sh:458
if [ -n "${CX_SHARD_FILE:-}" ] && [ -f "${CX_SHARD_FILE:-/nonexistent}" ]; then
# SHARD mode — sweep every scheduled case
...
else
# Single-bench (workflow_dispatch) path
# uses ${CX_MODE:-normal}, ${CX_PHASE:-decode}, ${CX_ROUTING:-uniform},
# ${CX_DISPATCH_DTYPE:-bf16}, empty CX_CASE_ID/CX_SUITE/CX_WORKLOAD_NAME, ...The file resolves to /ix/experimental/CollectiveX/results/.shard_<id>.json — which is missing because rsync excluded it — so the test fails and the else branch runs a single default case with none of the shard's identity, N times cheaper than the intended N-case sweep.
Why the rack (EP8) paths escape
The rack-scale launchers iterate cases themselves in the launcher on the SUBMIT host (not inside the container). Their case-list helpers explicitly resolve the shard file against the original checkout when the relative path misses:
# launch_gb300-nv.sh cx_ep8_cases (and launch_gb200-nv.sh cx_ep_cases)
local sf="${CX_SHARD_FILE:-}"
[ -n "$sf" ] && [ ! -f "$sf" ] && [ -f "$CX_DIR/$sf" ] && sf="$CX_DIR/$sf"The same workaround is absent from run_in_container.sh:458, so the EP4 single-tray path — which shares the b300/gb200-EP4/gb300-EP4 launchers with the staged mount — hits the missing file.
Affected sweeps
Every single-tray staged shard in the v1 promoted matrix, per sweep_matrix.py + configs/suites.yaml platforms:
- b300 (all shards; launch_b300.sh is single-node)
- gb200 EP4 (CX_NODES<=1 -> run_in_container.sh)
- gb300 EP4 (CX_NODES<=1 -> run_in_container.sh)
The h100-dgxc/h200-dgxc/b200-dgxc/mi325x/mi355x paths do not set CX_STAGE_DIR in this workflow (cx_stage_repo becomes a no-op) and are unaffected.
Concrete walk-through (b300 shard)
- Setup job resolves matrix; writes
experimental/CollectiveX/results/.shard_b300-deepep.jsonon the checkout with e.g. 24 cases (varied phase/dtype/routing/eplb across ep-core-v1 + ep-routing-v1). - Sweep job on the b300 runner exports
CX_SHARD_FILE=results/.shard_b300-deepep.json, checks out the repo, and callslaunch_b300.sh. launch_b300.sh:34->cx_stage_reporsyncs to$CX_STAGE_DIR/job_<id>/experimental/CollectiveX/with--exclude='results/' --delete-excluded. The shard file is not copied.srun --container-workdir=$MOUNT_DIR/experimental/CollectiveX ... run_in_container.sh. cwd inside container =/ix/experimental/CollectiveX.run_in_container.sh:458tests[ -f "results/.shard_b300-deepep.json" ]-> that resolves to/ix/experimental/CollectiveX/results/.shard_b300-deepep.json-> missing.- Execution falls into the else branch at line 556+. It dispatches
${CX_BENCH}once withCX_MODE=normal,CX_PHASE=decode,CX_ROUTING=uniform,CX_DISPATCH_DTYPE=bf16, emptyCX_CASE_ID, emptyCX_SUITE, emptyCX_WORKLOAD_NAME, emptyCX_REQUIRED_PUBLICATION. - One result JSON is produced with no case_id and mismatched identity; the other 23 scheduled cases never run.
- Aggregate job's
make_bundle.py validate_expected_coveragecomputesmissing_identity + missing + identity_mismatchagainstmatrix_full.jsonandraise SystemExit(...)— the whole aggregate fails, after b300 GPU-time was spent on the wrong workload.
Impact
For every b300/gb200-EP4/gb300-EP4 shard promoted through v1 (three of the eight SKUs in ep-core-v1 + ep-routing-v1), the sweep silently runs one wrong-config default point instead of the scheduled N-case sweep. Bundle validation catches the divergence but only post-hoc, so the failure is loud yet wasteful: GPU allocations spent, aggregate job red, invalidating the v1 dataset this PR is producing.
Fix
Any one of:
-
Allow the shard file through the rsync in
cx_stage_repo(runtime/common.sh:146):rsync -a --delete --delete-excluded \ --include='experimental/CollectiveX/results/' \ --include='experimental/CollectiveX/results/.shard_*.json' \ --exclude='__pycache__/' --exclude='results/' ...
-
Copy the shard file into the stage dir after the rsync completes:
[ -n "${CX_SHARD_FILE:-}" ] && [ -f "$repo_root/experimental/CollectiveX/$CX_SHARD_FILE" ] \ && cp -a "$repo_root/experimental/CollectiveX/$CX_SHARD_FILE" \ "$stage_dir/experimental/CollectiveX/$CX_SHARD_FILE"
-
Mirror the rack (EP8) launcher workaround in
run_in_container.sh:458:sf="${CX_SHARD_FILE:-}" # $CX_DIR is not set inside the container; use the fixed workdir instead. [ -n "$sf" ] && [ ! -f "$sf" ] && [ -f "/ix/experimental/CollectiveX/$sf" ] \ && sf="/ix/experimental/CollectiveX/$sf" if [ -n "$sf" ] && [ -f "$sf" ]; then ...
Approach (1) or (2) is the smallest change with the least surface area.
| elif _run(["ibstat", "-l"]): | ||
| devices = [d.strip() for d in _run(["ibstat", "-l"]).splitlines() if d.strip()] | ||
| return { |
There was a problem hiding this comment.
🟡 _rdma() calls _run(["ibstat", "-l"]) twice at env_capture.py:178-179 — once in the elif condition and once in the comprehension body. If the second invocation returns None (which _run does on shutil.which miss, TimeoutExpired/OSError, or nonzero exit), .splitlines() raises AttributeError and takes down env_capture.py under run_in_container.sh's set -euo pipefail. The trigger is genuinely rare (both calls are microseconds apart on a stable IB stack, and this branch runs only when ibv_devinfo is absent), so nit — but the fix is a one-line refactor mirroring the ibv_devinfo branch just above.
Extended reasoning...
The defect. env_capture._rdma() has an asymmetry between its two RDMA-listing branches:
listing = _run(["ibv_devinfo", "-l"]) # assigned once, iterated once
if listing:
for line in listing.splitlines()[1:]:
...
elif _run(["ibstat", "-l"]): # called once (as a truthiness check)
devices = [d.strip() for d in _run(["ibstat", "-l"]).splitlines() if d.strip()] # called AGAINThe ibv_devinfo branch just above does the right thing: assign once, reuse. The ibstat branch does not.
Why the crash is theoretical but real. _run() returns None on any of: shutil.which(cmd[0]) failing (line 51), subprocess.TimeoutExpired/OSError (line 57), or out.returncode != 0 (line 59). If the first call returns a truthy string but the second returns None — a transient OS timer glitch, an OOM-killed helper, a stray nonzero exit under load — then None.splitlines() raises AttributeError. Under run_in_container.sh's set -euo pipefail (line 33), that aborts the whole shard step before any GPU benchmark runs.
Step-by-step proof of the theoretical crash path:
- Node has
ibstatin$PATHbut noibv_devinfo(a real config: MI355X-style stacks withibstatonly). - First call:
_run(["ibstat", "-l"])succeeds → returns"mlx5_0\nmlx5_1\n"→ elif condition is truthy. - Second call: a transient nonzero exit (e.g.
ibstatracing an IB-driver reload, timer wraparound, PID-namespace hiccup) →out.returncode != 0→_runreturnsNone. None.splitlines()→AttributeError: 'NoneType' object has no attribute 'splitlines'→ Python exits nonzero →set -eabortsrun_in_container.sh→ the shard step fails before GPU work.
Why this is nit, not normal. Every verifier converged on the same practical assessment: ibstat -l is a fast local device listing with no network/filesystem dependency, so a transient failure between two back-to-back calls (microseconds apart) is extremely improbable. The elif branch itself only runs when ibv_devinfo is absent, which is uncommon on the target runners since both binaries come from the same InfiniBand userspace stack. And env_capture.py produces a diagnostic/provenance artifact — even a genuine crash here would break provenance capture, not the benchmark measurement. The defect exists but doesn't justify blocking merge.
The fix. One-line refactor to mirror the ibv_devinfo branch:
else:
listing = _run(["ibstat", "-l"])
if listing:
devices = [d.strip() for d in listing.splitlines() if d.strip()]Same idiom the file uses immediately above. Eliminates the wasted subprocess call and the theoretical None-deref in one change. Worth doing as a follow-up cleanup, but the PR does not need to block for it.
| "required_publication": env("CX_REQUIRED_PUBLICATION") or None, | ||
| "backend": backend, | ||
| "phase": phase, | ||
| "ep": integer("CX_EP", integer("CX_NGPUS", 1)), | ||
| "gpus_per_node": integer("CX_GPUS_PER_NODE", integer("CX_NGPUS", 1)), | ||
| "scale_up_domain": integer("CX_SCALE_UP_DOMAIN", integer("CX_NGPUS", 1)), | ||
| "dispatch_dtype": env("CX_DISPATCH_DTYPE", "bf16"), | ||
| "mode": env("CX_MODE", "normal"), | ||
| "contract": env("CX_MEASUREMENT_CONTRACT", "layout-and-dispatch-v1"), | ||
| "routing": env("CX_ROUTING", "uniform"), | ||
| "eplb": enabled("CX_EPLB"), | ||
| "combine_quant_mode": env("CX_COMBINE_QUANT_MODE", "none"), | ||
| "resource_mode": env("CX_RESOURCE_MODE", "tuned"), | ||
| "activation_profile": env("CX_ACTIVATION_PROFILE", "normal"), | ||
| "placement": env("CX_PLACEMENT", "packed"), | ||
| "routing_step": env("CX_ROUTING_STEP", "0"), | ||
| "uneven_tokens": env("CX_UNEVEN_TOKENS", "none"), | ||
| "tokens_ladder": env("CX_TOKENS_LADDER"), | ||
| "canonical": enabled("CX_CANONICAL"), | ||
| "sampling_contract": "fixed-512-v1", | ||
| "samples_per_point": integer("CX_SAMPLES_PER_POINT", 512), | ||
| "iters": integer("CX_ITERS", 8), | ||
| "trials": integer("CX_TRIALS", 64), | ||
| "warmup": integer("CX_WARMUP", 32), | ||
| "warmup_semantics": env( | ||
| "CX_WARMUP_SEMANTICS", "full-roundtrip-per-trial-point-v1" | ||
| ), |
There was a problem hiding this comment.
🟡 cx_emit_ep_failed_case (runtime/common.sh:256-287) builds failure.case without the hidden/topk/experts/nodes keys, but every matrix case emitted by sweep_matrix.py always carries all four. On the first sweep where any case exhausts its retries (flashinfer intermittent MNNVL, HybridEP/UCCL empty-rank, any deterministic rc=5), make_bundle's _identity_differences reports the same case_id four times as hidden=None!=7168,topk=None!=8,experts=None!=256,nodes=None!=1, and validate_expected_coverage piles on by re-listing that case in missing, so the aggregate job aborts with a dual-report that hides the real signal (the case failed all retries — the intended fail-closed behavior). Fix in either place is fine: add the four fields to cx_emit_ep_failed_case from CX_HIDDEN/CX_TOPK/CX_EXPERTS (defaults 7168/8/256) and CX_NGPUS/SLURM_NNODES, or make _identity_differences skip these fields when the actual doc is a failed-case.
Extended reasoning...
The observed behavior
With the PR merged and any sweep that produces a failed-case record for a scheduled case, the aggregate job will fail with a message like:
bundle: expected-matrix coverage failed (
missing_identity=0 missing=['cxv1-...'] extra=[] duplicates=[]
identity_mismatch=['cxv1-...:hidden=None!=7168,topk=None!=8,experts=None!=256,nodes=None!=1'])
The same case_id appears in both missing and identity_mismatch, and the mismatch string names four fields that have nothing to do with why the case actually failed.
Step-by-step proof
Take a concrete promoted case, say h100-dgxc/deepep/decode under ep-core-v1 (uniform, canonical, deepseek-v3-v1 defaults). sweep_matrix.py:181-186 builds the matrix entry with:
{
...,
"hidden": "", # h==7168 -> "" sentinel
"topk": "", # t==8 -> ""
"experts": "", # e==256 -> ""
"nodes": "1", # always str
...
}When every one of the 4 flashinfer attempts wedges on the intermittent MNNVL completion-flag deadlock (documented in run_in_container.sh around line 526), the last attempt's cx_emit_ep_failed_case writes a failed_*.json whose failure.case dict is missing the four keys entirely — the emitter reads CX_DISPATCH_DTYPE/CX_MODE/etc. but has no CX_HIDDEN/CX_TOPK/CX_EXPERTS/SLURM_NNODES reads.
aggregate_results.py keeps that failed-case doc as the newest for that case_id. Then make_bundle.py runs validate_expected_coverage:
_expected_case_identity(matrix_case)—"hidden" in caseis true (value""), soidentity["hidden"] = int("" or 7168) = 7168. Same for topk/experts (8/256)."nodes" in caseis true,identity["nodes"] = int("1") = 1. Expected identity contains{hidden: 7168, topk: 8, experts: 256, nodes: 1, ...}._actual_case_identity(failed_doc)(the failed-case branch, line 184-195) copiesfailure.caseverbatim, calls_expected_case_identity. None ofhidden/topk/experts/nodesare in that dict, so theif field in case:guard skips all four. Actual identity contains everything except the four scheduled shape fields._identity_differencesiterates the expected identity's items;actual_identity.get("hidden")isNone,None != 7168->hidden=None!=7168. Same for the other three.validate_expected_coverage(line 294-298) hits thedifferencesbranch, appends the case_id toidentity_mismatch, and does not add it toactual{}. Thenmissing = set(expected) - set(actual)(line 301) also contains that case_id. Line 319 raises the dual-reportSystemExit.
validate_results.py:validate_doc's failed-case schema (v5, ~lines 234-243) requires a different, smaller field set that happens to match what the emitter writes, so it stays silent about this desync. Only make_bundle notices, and only in a way that obscures the real cause.
Why this fires in practice
The PR explicitly builds in retry logic — CX_FLASHINFER_RETRIES defaults to 3 attempts, and both the container and rack launchers loop attempts and preserve a failed_*.json when all attempts fail. Retry-exhaustion is expected behavior for known intermittents, but the aggregate step will now report those as identity_mismatch + missing for hidden/topk/experts/nodes — the least informative signal possible.
Impact
Bundle validation still correctly rejects the incomplete run (the intended fail-closed behavior), and no incorrect data ships, so this is a diagnostic-clarity regression rather than a correctness bug. It will, however, cost real triage time in CI: an operator staring at hidden=None!=7168,topk=None!=8,experts=None!=256,nodes=None!=1 will not obviously infer "one flashinfer case exhausted its retries."
Fix
Either add the four fields to cx_emit_ep_failed_case (read CX_HIDDEN/CX_TOPK/CX_EXPERTS with defaults 7168/8/256, and CX_NGPUS/SLURM_NNODES for nodes), or teach _identity_differences/_actual_case_identity to drop these fields when the actual doc is a failed-case. Either way the two validators stay in sync.
758fa52 to
1c5b901
Compare
7e5f80a to
28cbac4
Compare
4ff5841 to
bffd8d6
Compare
|
|
||
| # Canonical workload shapes, verified against the upstream model configuration. | ||
| workloads: | ||
| deepseek-v3: |
There was a problem hiding this comment.
Easy to increment
…le docs - ep_harness.run_sweep already returns nonzero (agreed cross-rank via MIN) on a non-success outcome so a persistent oracle failure fails the leg instead of riding as green; make summarize.py a pure renderer that calls the invalid count out loudly and drop its dead exit-1 gate + unused non-markdown branch. Workflow drops the now-vestigial --markdown flag. - Delete unscheduled mi325x plumbing (capability row + platform block, config REQUIRED set, launcher branches, ep_mori arch pin) and rename the misnamed shared AMD image/commit vars (CX_IMAGE_AMD_MORI_MI325 -> CX_IMAGE_AMD_MORI, CX_MORI_COMMIT_MI325 -> CX_MORI_COMMIT_AMD). Matrix still yields mi300x + mi355x legs (12 cases each). - Reconcile methodology.md and README with what the backend actually emits: no detached sample docs, no image/squash hashing, no attempt_id/point_id (attempt_ordinal only), outcome is success|invalid with a structured validity, component measurement order rotates per trial (not "roundtrip first"), MI300X listed alongside MI355X.
Mechanical rename of the runtime prefix across launchers, common.sh, config/probe/stage, prepare_backend, ep_harness, summarize, the sweep workflow, and the seam-contract test. ep_mori.py's five reads of the MoRI kernel-type env var were missed by the sweep and are updated here to COLLX_MORI_KERNEL_TYPE, matching the renamed setters in launch_mi-amds.sh / common.sh / config.py (the reader would otherwise orphan to the intranode default and break EP16 internode-v1). Rename-only: 897 insertions / 897 deletions, no behavior change.
…odology) Addresses two results-quality gaps: - decode ladder extended 128 -> 512 tok/rank and prefill densified 2 -> 4 points ([256,512,1024,2048]) via suites.yaml overrides, so the payload trend is readable (prefill previously had only 2 points => no trend). - timing profile 8:64:32 -> 8:128:32 (512 -> 1024 timed samples/point) to tighten the p99 tail that drives the chart and reduce point-to-point bumpiness. EP_TIMING_PROFILE derives from the constants; config.py manual defaults + the seam-contract fixture updated to match. 26 tests green. METHODOLOGY CHANGE: these make new results incomparable with prior v1 numbers. version is deliberately left at 1 (operator-only bump). Do NOT dispatch a publishing sweep until the operator bumps suites.yaml version and updates the FE COLLECTIVEX_VERSIONS, or v1 continuity breaks.
…idated) Hardware validation on 2-node MI355X (16/16 ranks established cross-node QPs over the RoCE fabric) refutes the prior 'device-initiated cross-node RDMA (ROCm SHMEM) does not complete' basis for the mi355x x mori x EP16 override. The cell stays gated (unchanged matrix) but for the accurate reason: it now depends only on the operator provisioning the MI355X internode RDMA selectors in the network-config secret. Remove the override once those land. mi300x x mori x EP16 is untested (distinct init failure) and unchanged. 26 tests green.
The mi325x-amds/mi325x-tw GHA runners are online again (were offline when db71b86 dropped the plumbing), so restore mi325x as a scheduled SKU. It is gfx942 — identical to mi300x — so it rejoins every AMD grouping mi300x is in: capability PLATFORMS + deepep-v2 nvidia-only row, suites platforms list, config REQUIRED + runner set, common.sh image/canonical-env/lock/stage/NCCL groupings, and the mi-amds launcher (256 CPUs + device mounts, MoRI env, asyncll EP8 kernel). mori EP8 (decode+prefill) is runnable; EP16 is walled (unvalidated internode + needs network selectors, like mi300x). Matrix now yields 3 runnable AMD mori shard-cells (mi300x/mi325x/mi355x). 26 tests green.
mi325x-mori legs failed at ep_mori.py: 'MoRI has no pinned GPU architecture for runner mi325x'. The re-add (36b0830) restored the capability/config/ launcher/common.sh groupings but missed the per-runner MoRI GPU-arch pin — mi325x is gfx942 (like mi300x), so add it to that branch. Comment updated to note MI300X/MI325X both use the AsyncLL XGMI kernel.
…perator network overlay)
…EP16 Track per-SKU scale-out RDMA fabric selectors in configs/network-config.json (un-gitignored) and overlay them onto the base operator config via config.py:_network_overlay — SKUs absent there fall back to the network secret, a missing/invalid file is a no-op. mi355x internode selectors (rdma0-7, gid 1, eno0) were hardware-validated (16/16 ranks, 4 QP/PE), so drop the mi355x×mori×16 override — its EP16 leg is now schedulable. mi300x/mi325x EP16 stay walled (no validated internode selectors: mi300x debug cluster node-boot broken, mi325x GHA nodes on an unreachable pod cluster). 26 tests green.
…twork-config.json) mi355x internode selectors now ship in configs/network-config.json and are overlaid by config.py, so the mi355x×mori×16 override is no longer needed — its EP16 leg is schedulable. mi300x/mi325x EP16 remain walled (no validated internode selectors).
Replace deepep-hybrid with a vendor-neutral nccl-ep backend and finish the backend-registry consolidation into configs/backends.json. nccl-ep (bench/ep_nccl_ep.py): the collective-library EP baseline on RCCL (AMD) / NCCL (NVIDIA). Not the naive equal-split all_to_all_single — a routing-aware token-rank variable-split all-to-all that satisfies the oracle contract: dispatch sends each token once per distinct destination rank (top-k row masked to that rank), combine is the reverse variable all-to-all + unweighted rank-sum. No source pin or build; runs on any image collective. Schedulable on both vendors, all SKUs, EP8/EP16. deepep-hybrid removed entirely: adapter, capability entry + b200 EP16 override, run_ep choices/import, backends.json entry, config.py registry emit, common.sh (registry names, probe, source-pin, canonical unset), prepare_backend.sh (both build fns + dispatch + persisted env), and the ep_harness label map + tests. Backend source pins + images now live in configs/backends.json, validated by config.py backend-registry and consumed by common.sh (fail-closed). 30 tests + 6 subtests green; no hybrid refs remain in code.
The per-SKU launchers rejected nccl-ep at setup (unsupported EP backend): add nccl-ep to the allowlists (single-slurm + gb-nv: deepep-v2|nccl-ep; mi-amds: mori|nccl-ep) and keep pinned-source staging deepep-v2-only (nccl-ep has no source build).
… provenance The commit uniquely determines its tree and submodule gitlinks, so rev-parse HEAD at source fetch already enforces them — the separate tree pin was redundant. The nccl-check upstream_commit was provenance-only (the patch is applied by stage.py rewrite-deepep-v2, which needs no hash). Remove both from configs/backends.json, the config.py registry emit, common.sh (required names + source-pin format repo|commit|fmt), prepare_backend.sh (cache key + DEEPEP_TREE export/persist), and the registry test. 30 tests + 6 subtests green.
Rip out the nccl-ep backend entirely (adapter, capability entry, run_ep choices/import + dead dist-init branch, launcher gates, common.sh + prepare_backend probes, ep_harness label map, workflow choice, tests). The matrix is back to deepep-v2 + mori. Plus dead-code/slop removal (reviewed, no remaining consumers): drop unused EPBackend oracle_layout/payload_unit + redundant WorkloadSpec fields (routing/seed/hidden/topk/experts/num_logical) + RankInputs.global_tokens; mori dead label/tuning vars; routing SOURCE_ID_CONTRACT + token_offset; probe verify_cache_mount; numpy from requirements; README/methodology trim. 30 tests + 6 subtests green; matrix generates deepep-v2 (9) + mori (4).
| return expected | ||
|
|
||
|
|
||
| def _run_expert_oracle( |
There was a problem hiding this comment.
evals; checks validity of the emitted result.
Move the timing profile, routing seed, and measured + conditioning token ladders out of ep_harness constants into configs/suites.yaml as the sole source; the matrix bakes them into every scheduled case and run_ep receives them only through required CLI args (no module defaults, no cross-check guards). Decode ladder extended to 512 tok/rank; prefill densified to 256..2048. Dedupe the oracle expert-transform coefficients and column pattern between transform and expectation; unify the oracle report shape.
…e operator-config merge Drop artifact fields nothing consumes: the top-level case object, outcome.validity, shape, realized_placement/device_count, oracle ordering_contract/order_stable, and the scheduled-case canonical/timing/samples/warmup_semantics duplicates (all still required fields survive; the frontend keys off identity.case_factors.case). Move the operator-config overlay merge from the workflow's inline python into config.py merge-operator-config (one tested codec); drop backends.json documentation-only schema_version/notes/platforms/patches and the stage ownership tag. Update methodology.md to the emitted shape.
…step The merge-operator-config step runs before the launcher cd's into $COLLX_SOURCE_ROOT, in a cwd where the relative experimental/CollectiveX path does not exist (the sweep source is staged under $COLLX_SOURCE_ROOT, not checked out into the workspace). The prior inline heredoc needed no file path; the extracted subcommand did. Reference config.py by its absolute staged path, matching the Extract step.
…ioning ladder The untimed warm-up now walks the measured token ladder ascending (8 rounds per shape, cold-jump-safe) and reuses those problems in Pass 1, instead of a distinct conditioning ladder. Removes conditioning_ladders from suites.yaml, the --conditioning-ladder argv (config.py both codecs), the WorkloadSpec conditioning fields, and retain_global (every shape retains its global trace). create_buffer sizes from the measured maximum (the old value folded in the ramp but never exceeded that max, so the JIT dir is stable). Docs + tests updated.
| # Inputs determine the communicator capacity. | ||
| backend.create_buffer(spec) | ||
|
|
||
| # ---- Pass 0: settle clocks/fabric untimed by walking the measured shapes |
| backend.warm(problems[T], CONDITIONING_ROUNDS_PER_SHAPE) | ||
| torch.cuda.synchronize() | ||
| dist.barrier() | ||
| # ---- Pass 1: run the expert oracle over each warmed deterministic problem. ---- |
There was a problem hiding this comment.
Confirm correctness
| "pre_input_unchanged": pre_input_unchanged, | ||
| } | ||
|
|
||
| # ---- Pass 2: every backend uses the same rotated point order. |
There was a problem hiding this comment.
actual benchmark
| rt_pool[T] += _reduce_vec(torch, dist, device, measured["roundtrip"], MAX) | ||
|
|
||
| # ---- Pass 3: prove timed inputs were immutable and repeat the full oracle. ---- | ||
| for T in ladder: |
There was a problem hiding this comment.
Second round of correctness, comparing to pass 1
| import shutil | ||
|
|
||
|
|
||
| EXCLUDES = {"__pycache__", "results", ".collx_workloads", ".collx_backend", ".collx_sources", |
There was a problem hiding this comment.
Ensure repo copy is clean
| raise SystemExit(1) | ||
|
|
||
|
|
||
| def rewrite_deepep_v2(args) -> None: |
There was a problem hiding this comment.
… h100 deepep-v2 EP8 Consolidate per-SKU platform identity, placement, operator-field requirements, canonical policy, and scale-out RDMA selectors into one tracked configs/platform_config.json. capability.py, runtime/config.py, the launchers, and prepare_backend.sh all derive from it; suites.yaml 'platforms: all' resolves to the registry. Removes the separate network-config.json (selectors move to the mi355x 'network' block) and the gitignored platforms.yaml indirection. Falsify the h100-dgxc deepep-v2 wall for EP8 (on-node ElasticBuffer + dispatch/ combine validated 2026-07-11): schedule EP8 (LSA), keep EP16 walled pending a GIN-over-RoCE 2-node test. MoRI kernel type is now derived by the adapter from (arch, scope) rather than an env var; block/warp tuples unchanged. Routing counter uses keyed BLAKE2b to match the documented oracle. Shared launcher prologue/tail + registry-driven topology validation.
Adding .shards to stage.py EXCLUDES (in 2424fb4) dropped the per-leg control JSON (COLLX_SHARD_FILE=.shards/<leg>.json) from the compute-visible staged tree. The cross-node preflight then failed 'test -r shard' -> exit 11 -> repository-stage failure on EVERY leg (all SKUs, both backends). Remove .shards from EXCLUDES; keep .venv/.pytest_cache (local cruft, absent on runners). Extend StageTests to assert .shards/<file> is staged.
…platform_config.json Operator decision (2026-07-12): move runner-local operator config (partition, account, qos, squash_dir, exclude_nodes) and scale-out RDMA selectors from the GHA secrets into the tracked platform_config.json 'operator'/'network' blocks, derived from live cluster state + a recovered manual config. An operator config document, when present, still overrides per field; path '-' (no document, known SKU) uses the registry baseline. collx_load_operator_config falls back to the registry-only sentinel when no operator file exists but the SKU is known. Complete baselines (run without secrets): h100, h200, b200, b300, mi355x. Still secret-fed: gb200 (no path), mi300x/mi325x (unreachable at derivation), gb300 (squash_dir/enroot_cache_path not recoverable). audit_salt stays secret.
The 'run without secrets' path requires this: deleting the base secret makes
COLLECTIVEX_OPERATOR_CONFIG_CONTENT empty, and json.loads('') crashed merge
before the registry-baseline fallback was reached, failing every leg at merge.
Treat an empty/blank base as {"runners":{}} so the merged file is still written
(launcher finds a config -> uses the tracked platform_config.json operator
baseline). A present base is still parsed strictly; overlays already skip empty.
Verified: complete-baseline SKUs emit full config; gb200/gb300 fail-closed with
validation-missing markers.
Summary
CollectiveX v1 expert-parallel communication benchmark and its isolated GitHub Actions publication path.
Benchmark and publication contract
release_tag=unversioned; only an explicitv1tag with the locked full-matrix digest emits a release marker.Validation
f1ca85f9689922b90edd5767b9ff2a902f6b896f32f68b2ca086dde3fd2157d0.8e262178f770b0cdde12b7ec71604afd87251fa55685d4594f29717153ad6bbd.git diff --checkpass.中文说明
本 PR 完成 CollectiveX v1 专家并行通信基准测试及隔离式 GitHub Actions 发布链路。代码已准备执行三轮完整、无 canary 的 V1 运行;本 PR 目前不声称已有完成 promotion 的实测结果。
基准测试与发布约定
release_tag=unversioned;只有显式选择v1且匹配固定完整矩阵摘要时才生成 release marker。cxpublication-v1-*NDJSON。验证
f1ca85f9689922b90edd5767b9ff2a902f6b896f32f68b2ca086dde3fd2157d0。8e262178f770b0cdde12b7ec71604afd87251fa55685d4594f29717153ad6bbd。git diff --check通过。