Skip to content

Commit e599a0b

Browse files
authored
CollectiveX: FP8 precision + low-latency (decode) mode, unified sweep runtime (#2278)
1 parent 2003066 commit e599a0b

19 files changed

Lines changed: 1523 additions & 162 deletions

.github/workflows/collectivex-sweep.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ on:
2323
description: Keep only shards whose expert-parallel degree is in this comma-list (8 keeps EP8 only, dropping EP16 so GB SKUs co-schedule with 8-GPU SKUs; blank = all)
2424
type: string
2525
default: ''
26+
modes:
27+
description: "Keep only shards in this comma-list of modes (normal, low-latency); low-latency is a decode-only EP8 addition; blank = all"
28+
type: string
29+
default: ''
2630
concurrency:
2731
group: cx-${{ github.ref }}-${{ inputs.backend }}-${{ inputs.only_sku }}
2832
cancel-in-progress: false
@@ -44,12 +48,14 @@ jobs:
4448
INPUT_ONLY_SKU: ${{ inputs.only_sku }}
4549
INPUT_EXCLUDE_SKUS: ${{ inputs.exclude_skus }}
4650
INPUT_EP_SIZES: ${{ inputs.ep_sizes }}
51+
INPUT_MODES: ${{ inputs.modes }}
4752
run: |
4853
set -euo pipefail
4954
args=(--backend "$INPUT_BACKEND")
5055
[ -n "$INPUT_ONLY_SKU" ] && args+=(--only-sku "$INPUT_ONLY_SKU")
5156
[ -n "$INPUT_EXCLUDE_SKUS" ] && args+=(--exclude-skus "$INPUT_EXCLUDE_SKUS")
5257
[ -n "$INPUT_EP_SIZES" ] && args+=(--ep-sizes "$INPUT_EP_SIZES")
58+
[ -n "$INPUT_MODES" ] && args+=(--modes "$INPUT_MODES")
5359
python3 sweep_matrix.py "${args[@]}" --out matrix_full.json >/dev/null
5460
python3 - "$GITHUB_OUTPUT" <<'PY'
5561
import json
@@ -94,10 +100,10 @@ jobs:
94100
COLLX_SHARD_SKU: ${{ matrix.sku }}
95101
COLLECTIVEX_CANONICAL_GHA: '1'
96102
COLLECTIVEX_SOURCE_SHA: ${{ github.sha }}
97-
# Consolidated shards run one bounded build-group in one Slurm allocation.
98-
# MI300X's compute partition has a 180-minute ceiling; the other production
99-
# pools accept 300 minutes. Allocations release as soon as the shard finishes.
100-
COLLX_TIME: ${{ matrix.sku == 'mi300x' && '180' || '300' }}
103+
# Consolidated shards run one bounded build-group in one Slurm allocation;
104+
# every production pool accepts 300 minutes. Allocations release as soon as
105+
# the shard finishes.
106+
COLLX_TIME: '300'
101107
COLLECTIVEX_EXECUTION_ID: ${{ github.run_id }}_${{ github.run_attempt }}_${{ matrix.id }}
102108
COLLX_JOB_PARENT: ${{ matrix.launcher == 'mi-amds' && format('/tmp/inferencex-collectivex-parent-{0}-{1}-{2}', github.run_id, github.run_attempt, matrix.id) || '/tmp' }}
103109
COLLX_JOB_ROOT: ${{ matrix.launcher == 'mi-amds' && format('/tmp/inferencex-collectivex-parent-{0}-{1}-{2}/inferencex-collectivex-{0}-{1}-{2}', github.run_id, github.run_attempt, matrix.id) || format('/tmp/inferencex-collectivex-{0}-{1}-{2}', github.run_id, github.run_attempt, matrix.id) }}

experimental/CollectiveX/README.md

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,21 @@ responsibility. The full measurement methodology is in [docs/methodology.md](doc
1212
## Execution Profile
1313

1414
The workload uses packed placement and one pinned `fixed-profile` resource configuration per
15-
backend/topology; there is no tuning sweep. Dispatch and combine are fixed BF16 on every backend;
16-
precision is not a swept dimension. Every case runs the single normal-mode contract:
17-
18-
- Normal mode uses `layout-and-dispatch-v1`, rank-deduplicated token payloads, and activation-only
19-
combine. Coverage is uniform routing only.
15+
backend/topology; there is no tuning sweep. Combine is always BF16; dispatch precision is a swept
16+
dimension — a BF16 control plus a caller-prequantized FP8 dispatch on every backend. Coverage is
17+
uniform routing only. Cases run in one of two modes:
18+
19+
- `normal` uses `layout-and-dispatch-v1`, rank-deduplicated token payloads, and activation-only,
20+
unweighted rank-sum combine. It runs the full decode and prefill ladders.
21+
- `low-latency` uses each backend's decode-optimized kernel family: on DeepEP the legacy
22+
`deep_ep.Buffer` IBGDA `low_latency_dispatch`/`low_latency_combine` (a per-expert padded receive
23+
and a source-side gate-weighted combine); on MoRI the `IntraNodeLL` kernel (single-call,
24+
pure-intranode, same compact layout and unweighted rank-sum combine as `IntraNode`). It is a
25+
decode-phase-only, per-SKU-capability-gated addition whose runnable set differs from `normal`'s, so
26+
it is enabled from each SKU's `ll_backends` registry entry (currently DeepEP V2 EP8 on H100/H200/B200
27+
and MoRI EP8 on MI300X/MI325X/MI355X). Scoped single-node EP8 runs over the intra-node NVLink/XGMI
28+
low-latency path (no `/dev/gdrdrv` needed — validated on H200 with it absent); NVSHMEM/IBGDA on the
29+
wire is only a multi-node scale-out (EP16) concern.
2030

2131
Cases use a fixed timing profile from `configs/sweep.json`: 256 trials x 8 timed iterations (2048
2232
samples per component) with 32 synchronized full roundtrip warmups before each measured component at
@@ -29,13 +39,16 @@ Correctness is checked against an implementation-independent oracle that reprodu
2939
two-level reduction — intra-scale-up-domain FP32, then a BF16 cast of each domain's partial for the
3040
scale-out send. The combine gate is a tight max elementwise relative error below `8 * 2^-8`
3141
(denominator clamped at 0.02), which holds across scale-up and multi-node scale-out topologies
32-
alike. Any failed rank or point makes the case ineligible in the result it writes.
42+
alike. Under FP8 dispatch the oracle applies the same per-token cast round-trip to its semantic
43+
payload, so the dispatched-payload compare stays bit-exact and the combine gate is unchanged — the
44+
quantization is modeled, not tolerated. Any failed rank or point makes the case ineligible in the
45+
result it writes.
3346

3447
The matrix covers H100, H200, B200, B300, GB200, GB300, MI300X, MI325X, and MI355X. `sweep_matrix.py` materializes
3548
the requested SKUs, backends, EP sizes, and token ladders, then extracts strict per-shard controls
36-
and rejects missing, stale, malformed, or altered shard controls. `--only-sku`, `--exclude-skus`, and
37-
`--ep-sizes` select a subset; the matrix is generated per dispatch, with no frozen digest or locked
38-
case count.
49+
and rejects missing, stale, malformed, or altered shard controls. `--only-sku`, `--exclude-skus`,
50+
`--ep-sizes`, and `--precisions` select a subset; the matrix is generated per dispatch, with no
51+
frozen digest or locked case count.
3952

4053
| Systems | EP8 | EP16 |
4154
|---|---|---|
@@ -48,8 +61,8 @@ scale-up domain.
4861

4962
| Backend | Current scope |
5063
|---|---|
51-
| DeepEP V2 | PR #605 `ElasticBuffer` plus exact upstream #630 and #640 fixes: LSA for scale-up and GIN for x86 EP16 scale-out |
52-
| MoRI | AMD EP8 uses IntraNode-family kernels (MI355X IntraNode, MI300X/MI325X asyncLL); EP16 pins InterNodeV1 over 2x8 XGMI + RDMA |
64+
| DeepEP V2 | `normal` mode is PR #605 `ElasticBuffer` plus exact upstream #630 and #640 fixes: LSA for scale-up and GIN for x86 EP16 scale-out. FP8 dispatch via `use_fp8_dispatch` (blockwise e4m3fn) alongside BF16. `low-latency` mode is the legacy `deep_ep.Buffer` IBGDA decode kernels (per-expert padded layout, weighted combine, `use_fp8` e4m3fn), decode/EP8 only |
65+
| MoRI | `normal` mode uses the direct `IntraNode` kernel for scale-up EP8 on every CDNA SKU and pins `InterNodeV1` for EP16 over 2x8 XGMI + RDMA. `low-latency` mode selects the `IntraNodeLL` decode kernel (single-call, pure-intranode, same compact layout and unweighted combine as `IntraNode`), decode/EP8 only. FP8 dispatch is caller-prequantized (per-SKU e4m3fnuz on gfx942, e4m3fn on gfx950); combine stays BF16 (`quant_type=none`) alongside BF16 dispatch |
5366

5467
DeepEP V2 means the `ElasticBuffer` implementation introduced by
5568
[DeepEP PR #605](https://github.com/deepseek-ai/DeepEP/pull/605), not a newer legacy `Buffer` build.

experimental/CollectiveX/bench/ep_backend.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,31 @@ class EPBackend(abc.ABC):
6161
Subclasses implement the transport (create_buffer, dispatch, stage,
6262
combine, recv_tokens, inspect_dispatch, combine_transformed);
6363
everything the driver and the oracles need beyond that is provided here.
64-
Dispatch and combine are fixed BF16, so no adapter selects a precision codec.
64+
Combine is always BF16; an adapter that supports FP8 dispatch overrides
65+
SUPPORTED_PRECISIONS and the semantic_payload/_encode_dispatch hooks.
6566
"""
6667

6768
name: str = ""
6869
SUPPORTED_MODES: tuple = ("normal",)
70+
# Dispatch precisions the adapter realizes. BF16 is the universal control; an
71+
# adapter that also sends an FP8-quantized dispatch payload widens this.
72+
SUPPORTED_PRECISIONS: tuple = ("bf16",)
6973
stage_device_work = False
7074
combine_needs_redispatch = False
7175
dispatch_needs_combine_cleanup = False
7276
# Adapters that reduce activations and top-k weights independently must carry
7377
# the complete local weighted expert sum in the activation tensor.
7478
combine_weight_semantics = "unweighted-rank-sum"
7579
roundtrip_only = False
80+
# Realized wire formats recorded in the artifact. Combine is always BF16;
81+
# dispatch_dtype is overridden per-run by an FP8 adapter (e.g. "fp8-e4m3fn").
82+
dispatch_dtype = "bf16"
83+
combine_dtype = "bf16"
84+
# Logical byte model for one dispatched copy: bytes per activation value and
85+
# per-copy scale bytes. BF16 moves 2 bytes/value with no scale payload; an FP8
86+
# adapter sends 1 byte/value plus (for a blockwise codec) per-block FP32 scales.
87+
dispatch_value_bytes = 2
88+
dispatch_scale_bytes_per_copy = 0
7689

7790
def __init_subclass__(cls, **kwargs):
7891
super().__init_subclass__(**kwargs)
@@ -90,6 +103,11 @@ def __init__(self, args, rank, world_size, local_rank, device):
90103
self.mode = args.mode
91104
if self.mode not in self.SUPPORTED_MODES:
92105
raise ValueError(f"{self.name} does not support mode {self.mode!r}")
106+
self.precision = args.precision
107+
if self.precision not in self.SUPPORTED_PRECISIONS:
108+
raise ValueError(
109+
f"{self.name} does not support precision {self.precision!r}"
110+
)
93111

94112
# ---- Abstract transport contract -------------------------------------------------
95113

@@ -179,17 +197,44 @@ def _build_rank_inputs(self, args, tokens_per_rank) -> RankInputs:
179197
global_weights=w_g,
180198
)
181199

200+
def semantic_payload(self, x):
201+
"""The BF16 values the oracle should expect for a dispatched payload.
202+
203+
Identity for a backend that sends x unchanged. An FP8 backend overrides this
204+
to apply the exact quant->dequant round-trip the kernel transports, so the
205+
dispatched-payload compare stays bit-exact and the combine gate stays tight.
206+
"""
207+
return x
208+
209+
def _encode_dispatch(self, x):
210+
"""Return (dispatch_payload, oracle_semantic) for the source activations x.
211+
212+
Base identity: send x, no separate oracle payload (BF16). An FP8 adapter
213+
returns the caller-prequantized dispatch payload and the dequantized BF16 the
214+
oracle must expect after the backend's own dequant.
215+
"""
216+
return x, None
217+
182218
def make_problem(self, T, idx, weights, x):
183-
"""Assemble the per-shape problem namespace (BF16 dispatch sends x directly)."""
219+
"""Assemble the per-shape problem namespace.
220+
221+
dispatch_x is the payload actually sent (x itself in BF16; the caller-
222+
prequantized encoding under FP8). oracle_x, when set, is the dequantized BF16
223+
the combine oracle must expect, so the tight gate needs no tolerance change.
224+
"""
184225
import torch
185226

186-
return types.SimpleNamespace(
227+
dispatch_x, oracle_semantic = self._encode_dispatch(x)
228+
problem = types.SimpleNamespace(
187229
T=T,
188230
x=x,
189-
dispatch_x=x,
231+
dispatch_x=dispatch_x,
190232
topk_idx=idx.to(self._topk_idx_dtype()),
191233
topk_weights=weights.to(torch.float32),
192234
)
235+
if oracle_semantic is not None:
236+
problem.oracle_x = oracle_semantic
237+
return problem
193238

194239
def _topk_idx_dtype(self):
195240
"""Integer dtype the backend's kernels expect for top-k routing indices."""

0 commit comments

Comments
 (0)