Skip to content

[OMNIML-6059] [1/21] Add the DP=EP all-to-all expert transport - #2738

Merged
shengliangxu merged 27 commits into
mainfrom
shengliangx/hfep-3-a2a-transport
Oct 24, 2026
Merged

shengliangxu merged 27 commits into
mainfrom
shengliangx/hfep-3-a2a-transport

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: New feature

First of 21 PRs that add expert-parallel (EP) PTQ for Hugging Face MoE models to examples/hf_ptq under torchrun. They merge in order:

  • [1/21]-[3/21], the building blocks: this all-to-all expert transport, amax synchronization that holds under data-parallel calibration ([2/21]), and fused-experts quantization under grouped_mm ([3/21]).
  • [4/21]-[12/21], the distribution plan and the loads it routes to: a DistributionPlan decided once per run, and load_pretrained loading by it, replicated, FSDP2-sharded, or with an MoE's experts sharded. After [12/21], an MoE whose expert-parallel plan ships with transformers loads and calibrates expert-parallel.
  • [13/21]-[17/21], model specs and export: the specs ([13/21]) make an MoE expert-parallel when transformers ships no usable plan for it, such as DeepSeek-V3, and refuse the ones the path cannot handle yet; [14/21]-[17/21] write a sharded model's Hugging Face checkpoint without gathering it, expert-parallel included.
  • [18/21]-[20/21], models that plug in through their specs: compressed-tensors experts decompressed as they load, and the Kimi models.
  • [21/21] runs it all from hf_ptq.py.

This PR adds the transport.

Calibrating a large MoE has no good option today. Replicating it needs the whole model on every rank. Sharding it with FSDP2 alone gathers every expert on every rank for each forward, so the expert weights dominate both memory and time, while each rank still only sees its own slice of tokens.

DP=EP instead: dense layers and the router run data-parallel, the routed experts are sharded across ranks, and tokens go to the rank that owns their expert by an all-to-all dispatch and come back by the combine. Each expert sees every token routed to it from all ranks, so its amax is collected over the global token pool rather than one rank's share. That is the accuracy point, not just a memory one. The expert matmul stays on transformers' own _grouped_linear, where [3/21] attaches ModelOpt's per-expert quantizers as in a single process. register_torch_a2a() installs the transport as a transformers experts implementation, lazily rather than at import.

The transport is plain all_to_all_single over NCCL, so it works across nodes at any rank count. Three things it has to get right:

  • The routing weights stay on the token's rank. The experts' results come back unweighted, and the rank that holds each token weights them in the routing weights' own dtype, as transformers' grouped_mm does: a router that returns fp32 weights (Mixtral's, DeepSeek-V3's) gets an fp32 product. The weights never travel, so no hop carries them, and their gradient comes from the local product.
  • The combine sums each token's top-k in a fixed order. A sum whose order depends on how tokens were distributed, or on the atomics index_add_ uses on CUDA, rounds differently from one EP degree or run to the next, so activation scales would shift and a checkpoint would not reproduce. The results come back in (token, slot) order, and each token's top-k rows are weighted and summed with view(n, top_k, H).sum(1), as transformers' grouped_mm does, a slice of tokens at a time. Each transport buffer is freed as soon as its stage is done: a whole fp32 copy of the results, beside buffers kept to the end, reached GiBs at calibration batch sizes.
  • Large transfers. all_to_all_single addresses the flattened buffer with element offsets, which overflow int32 once rows × hidden exceeds 2**31. The transport chunks along the hidden dimension, into slices as wide as the most rows any rank moves allows. Ranks agree on that row count with one all-reduce per layer; deciding it from local sizes desyncs the hop count and deadlocks.

A rank that receives more than 4x the most tokens any rank sent (a collapsed router piling the cluster's tokens onto a few experts) warns once, before the expert stage allocates for that count. A rank can receive at most world size times that, so the warning fires only above 4 ranks.

The incidents behind the transport's less obvious code:

Code Why Seen on
Hops over 2**31 elements go as hidden-column slices Keeps every hop within all_to_all_single's int32 element offsets, for an extra hop or two past 2**31. Added after a hang in EP calibration that has not been reproduced since: with the slicing disabled, such hops complete exactly on NCCL 2.29, 2.30 and 2.31, including at 32 ranks over the network on 2.29 and 2.30 Qwen3.5-MoE EP calibration at batch 8 x seq 4096: rows x hidden = 2.68e9
The host waits for the count exchange before reading the counts A read racing the collective's stream sized the dispatch from garbage, and the next all-to-all crashed Qwen3.5-MoE EP calibration
Each token's top-k is summed in a fixed order A bf16, order-dependent combine made exported activation scales EP-dependent Qwen3-30B-A3B, top-8: 27% of input scales differed from single-GPU, by up to 28%

Usage

Internal to the expert-parallel loader ([9/21]); nothing to call directly.

Testing

  • tests/unit/torch/utils/test_moe_a2a.py, under gloo on CPU:

    • transformers' fused Qwen3-MoE experts split over two ranks: through the registered torch_a2a forward they give the whole module's output and gradients, for the hidden states, the routing weights and each rank's own experts (mutation-checked);
    • hops split along the hidden dim, each within the per-hop limit, once that limit is shrunk below the transfer size, with a bit-identical result;
    • the collapse warning, its ratio lowered for two ranks: collapsed routing warns on the receiving rank only, and unequal batches with even routing warn nowhere (fails on the previous revision, which compared with the rank's own count);
    • with fp32 routing weights on bf16 experts, at EP=1 the torch_a2a forward gives grouped_mm's result bit for bit (fails on the previous revision, which rounded the weights to bf16);
    • the combine's temporaries stay within one slice of tokens, with the same result as unsliced (fails on the previous revision, whose fp32 [tokens, hidden] accumulator exceeds the slice);
    • a rank that sends nothing, its routing slots all -1 or no tokens at all, but serves its peer's tokens: its backward still runs the reverse all-to-all, so both ranks get their input gradients (the test's group times out in 10 s, so a regression fails instead of hanging);
    • experts whose biases the plan left whole (GPT-OSS's, with only the weights sharded) are refused before any collective.
  • tests/gpu/torch/utils/test_moe_a2a.py: with fp32 routing weights, at EP=1 the torch_a2a forward gives grouped_mm's result bit for bit on CUDA. Run on GB200, against the previous revision's index_add_ combine and this one (elements that differ from grouped_mm, of 4,194,304):

    Combine 1 GPU 4 GPUs, each rank its own one-rank group
    Previous (index_add_) 223 217 to 232 per rank
    This revision 0 0 on every rank
  • On a real model, run to run: two identical calibrations of Qwen3-30B-A3B in FP8 (fp8_default-kv_fp8), EP4 on 4 GB200 with the dense layers replicated, batch size 2, compared scale by scale (the previous combine's pair ran at two revisions that both had it, on transformers 5.13; this revision's on 5.9):

    Combine Input scales that differ KV-cache scales that differ Weight scales that differ
    Previous (index_add_) 262 of 18,624 2 of 96 0 of 18,624
    This revision 0 of 18,624 0 of 96 0 of 18,624
  • Peak memory of one layer's transport, on CPU, at 4096 tokens, hidden 4096, top-k 8, fp32 routing weights:

    Revision Peak RSS growth
    Before the bounded combine and the early frees 2122 MiB
    This revision 706 MiB
  • The two-rank tests run on cpu_dist_workers, the two gloo ranks main's unit tests keep up for the session (tests/unit/conftest.py), since spawning ranks cost each test about 14 s of the linux unit job. This PR makes those workers reseed after every job, so each job starts from the seed a freshly spawned rank gets; a job that draws a different amount of randomness on each rank otherwise leaves the next job's ranks out of step. Since both ranks then draw the same data, main's test_data_parallel now seeds its calibration data by rank: its group-MAX check would otherwise hold with or without the data-parallel amax sync. It fails when that sync is removed. test_autoquant's data-parallel test seeds its batches by rank for the same reason, and fails when the score sync is removed.

  • The result keeps the dtype it is given: fp32 and fp16 experts through the transport, in a one-rank group, match a dense reference in their own dtype. Tokens travel in the model's dtype.

  • The test of a hand-written expert function against a dense reference is dropped: the Qwen3-MoE experts test checks the same forward and gradients, and the dtype test runs a one-rank group. The drop is checked by mutation.

  • Locally on this revision, the series on main 6cd9ced: pre-commit (every hook, mypy included) over each PR of the stack against main; the changed tests in this PR and in [2/21], [3/21] and [8/21], and at the top of the stack the unit test files that run this transport, on transformers 5.18 and 5.5. CI runs the full unit lanes and the docs build.

  • The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.

  • On GPUs: tests/gpu/torch/utils/test_expert_parallel_load.py ([9/21]) runs a tiny Qwen3-MoE through this transport on every GPU and matches the unsharded model's logits; the series' model runs exercise it at 4 to 64 ranks (see [21/21]).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive; registered only when the expert-parallel loader asks for it.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [21/21].
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 1 of 21 of expert-parallel PTQ for Hugging Face MoE models. #2733 has merged, so this PR is based on main and merges first; the series then merges in order, [1/21] through [21/21].

Summary by CodeRabbit

  • New Features

    • Added support for running mixture-of-experts models across multiple devices, routing tokens to experts and combining their outputs.
    • Added support for per-expert biases and split handling of large transfers, with results accumulated in FP32 before conversion.
    • Added a warning when a device receives substantially more tokens than it sends.
  • Tests

    • Added coverage for distributed outputs, gradients, transfer splitting, imbalance warnings, and quantization synchronization.
    • Improved consistency by reseeding distributed workers between jobs.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Adds a lazily registered Torch all-to-all MoE expert-parallel path with Transformers integration. Adds distributed tests for dispatch, output and gradient correctness, chunking, and imbalance warnings. Updates worker reseeding and distributed quantization synchronization checks.

Changes: Torch A2A MoE expert parallelism

Layer / File(s) Summary
Token dispatch and output combination
modelopt/torch/utils/moe.py
Routes token–expert pairs to owning ranks, exchanges token counts, runs local experts, and combines outputs. Large transfers are chunked across the hidden dimension, and output accumulation uses FP32.
Transformers expert integration
modelopt/torch/utils/moe.py
Adds the Transformers experts forward and lazy registration of router and expert parallel styles. The expert style injects the process group and bypasses output reduction.
Distributed validation and worker setup
tests/_test_utils/torch/distributed/utils.py, tests/unit/conftest.py, tests/unit/torch/utils/test_moe_a2a.py
Reseeds distributed workers after teardown. Tests cover outputs and gradients, dtype preservation, chunking, imbalance warnings, and Qwen3-MoE integration.

Distributed quantization test adjustments

Layer / File(s) Summary
Rank-specific calibration and synchronization checks
tests/unit/torch/quantization/test_dist.py
Seeds each rank independently and completes quantizer reductions before reporting all local amax mismatches.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant SourceRank
  participant AllToAll
  participant DestinationRank
  participant LocalExpertFunction
  SourceRank->>AllToAll: Exchange counts and routed token data
  AllToAll->>DestinationRank: Deliver token groups
  DestinationRank->>LocalExpertFunction: Run local expert computation
  LocalExpertFunction->>AllToAll: Return weighted expert outputs
  AllToAll->>SourceRank: Deliver expert outputs
Loading

Suggested reviewers: kevalmorabia97, danielkorzekwa

Merge Risk: 🟡 Moderate · up to e256d

The new expert transport cannot be registered with a supported Transformers version. Add a compatible router-class import before merging.

Pre-merge checks: Passed 5 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 44.12% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
Check name Status Explanation
Title check Passed The title names the DP=EP all-to-all expert transport, which is the main change: the new modelopt/torch/utils/moe.py module adds a2a_moe, torch_a2a_experts_forward, and register_torch_a2a. The title a…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed No security anti-pattern from the custom check was introduced. The pull request changes one modelopt Python file and test files only; it adds no examples Python files or dependency files. The added co…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Title check

Explanation

The title names the DP=EP all-to-all expert transport, which is the main change: the new modelopt/torch/utils/moe.py module adds a2a_moe, torch_a2a_experts_forward, and register_torch_a2a. The title also carries the ticket and stack position tag [OMNIML-6059] [1/21], which is acceptable. The test and seeding changes are secondary and need not appear in the title.

Full details: Security Anti-Patterns

Explanation

No security anti-pattern from the custom check was introduced. The pull request changes one modelopt Python file and test files only; it adds no examples Python files or dependency files. The added code contains no unsafe torch.load, numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, externally derived eval()/exec(), or # nosec comments.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.23529% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.11%. Comparing base (6cd9ced) to head (858dd1b).

Files with missing lines Patch % Lines
modelopt/torch/utils/moe.py 88.23% 16 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2738      +/-   ##
==========================================
- Coverage   79.80%   79.11%   -0.69%     
==========================================
  Files         660      661       +1     
  Lines       74792    74928     +136     
==========================================
- Hits        59688    59282     -406     
- Misses      15104    15646     +542     
Flag Coverage Δ
examples-diffusers 19.90% <0.00%> (-0.04%) ⬇️
examples-gpt-oss 13.44% <0.00%> (-0.03%) ⬇️
examples-hf_ptq 23.34% <0.00%> (-0.08%) ⬇️
examples-llm_distill 13.52% <0.00%> (-0.03%) ⬇️
examples-llm_eval 17.17% <0.00%> (-0.04%) ⬇️
examples-llm_qat 17.34% <0.00%> (-0.07%) ⬇️
examples-llm_sparsity 16.76% <0.00%> (-0.04%) ⬇️
examples-megatron_bridge 26.39% <0.00%> (-0.17%) ⬇️
examples-specdec_bench 13.23% <0.00%> (-0.03%) ⬇️
examples-speculative_decoding 17.42% <0.00%> (-0.10%) ⬇️
examples-torch_onnx 21.22% <0.00%> (-0.06%) ⬇️
examples-torch_trt 15.15% <0.00%> (-0.03%) ⬇️
examples-vllm_serve 17.38% <0.00%> (-0.04%) ⬇️
gpu 58.91% <77.94%> (-1.17%) ⬇️
regression 15.04% <0.00%> (-0.04%) ⬇️
unit 61.11% <88.23%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-3-a2a-transport branch from d40b1c1 to cd7e9a6 Compare October 12, 2026 02:24
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-24 01:19 UTC

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test cd7e9a6

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 69f3a98

@shengliangxu shengliangxu changed the title [3/19] Add the DP=EP all-to-all expert transport [OMNIML-6059] [3/19] Add the DP=EP all-to-all expert transport Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-3-a2a-transport branch from 69f3a98 to 38c164d Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-3-a2a-transport branch from 38c164d to e301775 Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test e301775

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6059] [3/19] Add the DP=EP all-to-all expert transport [OMNIML-6059] [3/20] Add the DP=EP all-to-all expert transport Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-3-a2a-transport branch from e301775 to 7267471 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu changed the base branch from shengliangx/hfep-2-plan-decision to shengliangx/hfep-13b-load-routing October 13, 2026 19:39
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6059] [3/20] Add the DP=EP all-to-all expert transport [OMNIML-6059] [4/21] Add the DP=EP all-to-all expert transport Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7267471

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-3-a2a-transport branch from 7267471 to 5a6426d Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6059] [4/21] Add the DP=EP all-to-all expert transport [OMNIML-6059] [5/22] Add the DP=EP all-to-all expert transport Oct 14, 2026
…s' fused experts

Split over two ranks, Qwen3-MoE's fused experts must give the whole module's output and
gradients -- for the hidden states, the routing weights and each rank's own experts -- through
the registered torch_a2a forward, the path expert-parallel training and QAT run.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Spawning two ranks costs a unit test about 14 s on CI, far more than its job.
They now run on cpu_dist_workers, the two gloo ranks the unit tests keep up for
the session.

The workers now reseed after every job, as freshly spawned ranks would start it:
a job that draws a different amount of randomness on each rank otherwise leaves
the next job's ranks out of step, which failed two fused-expert tests on the
transformers 5.5 lane. The one-rank case runs as a one-rank expert-parallel
group on each worker.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The transport cast the hidden states to bf16 and handed bf16 back, so an
fp16 model's residual stream promoted to fp32 and its next linear failed,
and an fp32 model ran its experts on bf16-rounded tokens. Tokens now travel
in the model's dtype: bf16 models are unchanged, and the per-token combine
still accumulates in fp32.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The reseeding teardown starts every pool job from the same seed on both
ranks, so test_data_parallel calibrated both ranks on the same batches.
DDP gives them the same weights too, so its group-MAX check held whether
or not calibration synced the amax across data-parallel ranks. Drawing
the data from a per-rank seed makes the ranks' input amaxes differ, so
the check fails without the sync.

The check now runs after every rank has joined every all_reduce. A rank
that stopped at its first mismatch would leave its peer waiting in the
next one, hanging the pool's workers instead of failing the test.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
test_a2a_moe_forward_backward ran a2a_moe through a hand-written expert
function against a dense reference (with its _dense_moe and _router
helpers). test_fused_experts_backward_through_torch_a2a_matches_the_whole_module
sends the same hops through transformers' real experts and checks the
same forward and the hidden, routing-weight and expert gradients; the
one-rank subgroup is run by the dtype test. Detaching the routing-weight
or hidden-state hop, leaving the returned pairs unpermuted, or dropping
the routing weights each fails a remaining test.

The module docstring and the dtype comment now describe the comparison that
remains: transformers' fused experts, split over the ranks, against the
whole module on one process.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A transfer past the int32 element limit went as ceil(rows * H / limit)
even slices of the hidden dim. Rounding each slice's width up adds up to
`rows` elements to it, so a hop could still pass the limit (Q = 1572863
rows of H = 4096 make three 1366-wide slices of 2148530858 elements) and
hang the NCCL collective. A slice is now as wide as the most rows any
rank moves allow, limit // rows, which bounds every hop directly.

That row count is all-reduced once per layer, after the count exchange,
for both 2-D hops (the dispatch and the combine); each hop no longer runs
its own all_reduce and host read of the transfer size.

The chunking test now routes every rank's 12 rows evenly and records the
size of each 2-D hop: under a limit of 34, the old even split moves 36
elements per hop and fails it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
`warnings` is already a module import, and torch.distributed.nn.functional
has no circular-import or weight reason to stay local (it adds about 3 ms
to the module's import, and no warning: only calls are deprecated).

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
a2a_moe only passed its arguments on to _a2a_moe_torch, and neither read
num_global_experts: an expert's owner rank follows from the local count
alone. The transport is now a2a_moe itself, without that parameter.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
`assert grouped_mm_experts_forward is not None` held whenever the import
did; it never showed the whole-module reference ran through grouped_mm,
and the comparison does not depend on the path the reference takes. The
import goes with it.

packaging is a core dependency, so Version is imported at the top; the
transformers imports stay in their functions, now with the reason: the
transformers-free unit lane collects this file.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers 5.16.0 exports the router parallel style only as
EpRouterParallel; 5.16.1 restored RouterParallel as an alias. On
5.16.0, which the 5.6 floor admits, register_torch_a2a raised an
ImportError before registering the transport. It now falls back to
EpRouterParallel.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With every token routed to rank 0's experts, rank 1 runs transformers'
fused experts on an empty batch. The forward and the gradients still
match the whole module's on every rank, and the experts no token reached
get no gradient. The shared helper now asserts that, instead of a
non-zero gradient on every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… fp32 slices

a2a_moe cast the routing weights to the hidden states' dtype before weighting the experts' results.
A router that returns fp32 weights for bf16 experts, as Mixtral's does, then got a bf16 product where
transformers' grouped_mm forward computes an fp32 one, so torch_a2a changed the model's output even at
EP=1. The weights now never travel: the experts' results come back unweighted, and the rank that
holds each token multiplies them by its routing weights in the weights' own dtype. That drops the
routing-weight all-to-all, and the weights' gradient comes from the local product.

The combine then accumulated in fp32 by converting all [tokens * top_k, hidden] results at once, a
full fp32 copy beside the transport's buffers: 512 MiB at 4096 tokens, hidden 4096 and top_k 8, and
GiBs at the calibration batches the chunked transport serves. It now weights and sums a slice of rows
at a time, at most 2**25 fp32 elements (128 MiB) each, with the same result.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
a2a_moe kept all six of its [rows, hidden] buffers (the sent tokens, the received ones, the experts'
output in two orders, and the combine's two) until it returned, though each is dead once its stage is
done. Together they were the layer's peak memory. Dropping each as its stage finishes cuts the peak
RSS growth of one layer at 4096 tokens, hidden 4096 and top_k 8 from 1995 to 714 MiB on CPU; the
result is unchanged.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The bounded combine sums the routed results a slice of rows at a time. On a rank whose routing
slots are all -1 it ran no slice, so its output lost its autograd link to the combine, and its
backward skipped the reverse all-to-all its peers wait in. It now always runs at least one slice,
empty on such a rank, which keeps that link; forward results are unchanged.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>

@meenchen meenchen 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.

One minor comment, approve to unblock

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 6eca1d7

@sychen52 sychen52 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.

A few comments on the transport at 6eca1d7. They come from reading the code; nothing was run.

  • [P3] tests/unit/torch/quantization/test_autoquant.py:956 (outside the diff). _test_data_parallel_auto_quantize builds its data with model.get_input(), which draws from the global RNG, so both ranks get the same batches. Its "same costs and scores on all ranks" check then passes even without the data-parallel score sync. That was often true before too, because the workers start from seed 1234, but the reseed after every job now makes it certain. It's the same gap this PR closes in test_dist.py, and a torch.manual_seed(rank) right after SimpleLinear() would make the check meaningful there too. The other pooled data-parallel tests are fine as they are. test_data_parallel_moe_score_module and test_data_parallel_aumann_shapley use the same data on every rank on purpose, and they check the reduced values against a single-rank reference, so a dropped reduction still fails them.
  • Comment style. Several new comments read like incident notes: CAPS for emphasis, "observed: … all_to_all ran the full 3h NCCL timeout", "deterministic on the qwen3_5_moe_text EP path", "27% of input_scale values differed … by up to 28%", "the validated production path byte-for-byte unchanged". They do explain why, which helps. But CONTRIBUTING.md asks for one or two lines per comment, and puts benchmark numbers and root-cause analysis in the PR description. The longest are the combine comment (lines 208-219) and the count-exchange comment (148-153).

Comment thread modelopt/torch/utils/moe.py Outdated
Comment thread modelopt/torch/utils/moe.py Outdated
Comment thread modelopt/torch/utils/moe.py
Comment thread modelopt/torch/utils/moe.py Outdated
Comment thread modelopt/torch/utils/moe.py Outdated
Comment thread tests/unit/torch/utils/test_moe_a2a.py Outdated
Comment thread tests/unit/torch/utils/test_moe_a2a.py Outdated
The combine added each routed result into an fp32 [tokens, hidden] accumulator with index_add_, which
adds with atomics on CUDA. With fp32 routing weights (Mixtral's, DeepSeek-V3's) the order of a token's
adds, and with it the bf16 result, changed from run to run. The results now come back in (token, slot)
order, a -1 slot's row zero, and each token's top_k rows are weighted and summed with
view(-1, top_k, H).sum(1), the reduction transformers' grouped_mm forward uses: the same result every
run and at any EP degree. A slice of tokens at a time bounds the temporaries, and the fp32
accumulator is gone. A rank without tokens still runs one empty slice, which keeps its result on the
combine's graph.

A GPU test checks grouped_mm's result bit for bit on CUDA.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The a2a warned when a rank received over 4x what it sent itself. A rank receives about the average
share, so a rank with a small batch warned whenever the batches differed, balanced routing or not.
It now compares with the most tokens any rank sent, which rides on the all-reduce that already sizes
the hops, and the warning leaves the counts out of its text, so Python's filter shows it once rather
than for every layer and batch. The comment citing a trace a2a_moe no longer has goes with it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The torch_a2a experts forward indexes the experts' biases by local expert id, assuming the plan
sharded them over dim 0 like the weights. GPT-OSS's stock plan does, but a plan or spec that lists
only the weights leaves them whole, and every rank but the first then adds other experts' biases
without a word. The forward now refuses biases that do not have one row per local expert.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… arrives

The transport's docstrings and comments said ModelOpt quantizes the local experts through
_grouped_linear. On main ModelOpt hooks only the eager experts path, and quantize() even switches a
model to eager; the _grouped_linear override and the torch_a2a exception come with the fused-experts
PR. Until then the docs say what this PR does: the experts run through transformers'
_grouped_linear on the tokens dispatched to them from every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Several comments read like incident notes: capitals for emphasis, the model and batch an overflow
was observed on, how long it hung, which debug setting hid a race. CONTRIBUTING.md asks for a line
or two per comment; each now says why its code is there, and the numbers live in the PR description.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The test simulates 5.16.0, which exports the router style only as EpRouterParallel, by deleting
RouterParallel; on 5.16.0 there is none to delete, and monkeypatch.delattr raised instead of letting
the test check the fallback.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each job made a new gloo group on the session's two persistent cpu_dist_workers and left it, with its
backend, registered until the pool shut down. The pool asks jobs to undo the global state they set,
as test_hf_checkpoint_utils_dist.py does with its group; these jobs now destroy theirs too.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The pooled workers now reseed after every job, so both ranks of _test_data_parallel_auto_quantize
drew the same batches from model.get_input(), and its check that every rank ends with the same costs
and scores passed with the data-parallel score sync removed. Its batches are now seeded by rank,
after the model is built from the shared seed; without the sync the test fails.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 858dd1b

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

@sychen52, on the two points in your review body:

  • test_autoquant.py: fixed in 858dd1b. The batches are seeded by rank after the model is built, and with the data-parallel score sync removed the test now fails.
  • Comment style: fcf41ae trims the transport's comments to the reason for each piece of code. The incidents behind them, with their numbers, are now in the PR description.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants