Expert-parallel token dispatch: each rank trains on its own part of the batch - #48518
Expert-parallel token dispatch: each rank trains on its own part of the batch#48518qgallouedec wants to merge 29 commits into
Conversation
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Very cool ! and first all-to-all implementation of EP. I wonder if this works with different |
|
awesome work ! let's make sure it works with all experts impls (normally it should ootb since sentinels are the exception) and also think of how it can be extended to more advanced dispatch impls like DeepEP, i'm thinking something like a literal: experts_dispatch=all-reduce/all-to-all/DeepEP(through kernels for example) cc @3outeille |
393af53 to
d226b02
Compare
4a9e39d to
1329a33
Compare
| raise OSError("Expert-parallel token dispatch requires `torch>=2.7`.") | ||
| # The DTensor parameters are the expert-parallel experts (the expert parallel plan shards only them). | ||
| expert_modules = [ | ||
| module for module in model.modules() if any(is_dtensor(p) for p in module.parameters(recurse=False)) |
There was a problem hiding this comment.
Is this always sufficient ? i.e. all the Dtensors are by definition expert tensors ?
There was a problem hiding this comment.
after a3e218c it's guaranteed. maybe_distribute_model rewrites _ep_plan to keep only the expert styles before apply_tensor_parallelism runs, so the only DTensor params by the time we get here are the experts.
But it used to be enforced by a raise, which broke DeepseekV4: its EP plan also shards the lightning indexer, so tp test died at from_pretrained.
Those entries are now dropped with a warning and the modules stay replicated, which is what dispatch wants anyway since every rank holds a different batch. Both tests pass now.
d226b02 to
680e1ff
Compare
1329a33 to
5341f4b
Compare
should be fine for all four. Caveat: the EP mixin tests are CPU-only, so what I actually exercised is the default path. deepgemm and sonicmoe need a GPU test.
I like it (although I'm not really into it aha). Mega MoE is already the precedent, |
680e1ff to
d9394c9
Compare
a3e218c to
18ed512
Compare
VI-Arthur
left a comment
There was a problem hiding this comment.
Will checkout and fix myself!
| if expert_mesh is not None: | ||
| for module in expert_modules: | ||
| fully_shard(module, mesh=expert_mesh, reshard_after_forward=True, **fsdp_policy_kwargs) | ||
| else: | ||
| fsdp_policy_kwargs["ignored_params"] = {p for module in expert_modules for p in module.parameters()} |
There was a problem hiding this comment.
No, the way to say "we want only these FSDP" is here: https://github.com/VI-Arthur/transformers/blob/fix-ep-eager-waste/src/transformers/distributed/mixin.py#L202-L202
There was a problem hiding this comment.
This needs a small update but:
fsdp_plan = dict(getattr(model, "_fsdp_plan", None) or {})setting a custom plan will make sure onlyl these are used
There was a problem hiding this comment.
Should of course be in the distributed config
There was a problem hiding this comment.
Yes, the DTensor sniff is fragile: it happens to be exact only because the block above rewrites _ep_plan to expert-only styles, so it depends on something 40 lines away.
_fsdp_plan is already resolved at line 202 in this function, so it's the natural handle.
You said you'd take this one, so I'll leave it to you 👍
| replicated = sorted( | ||
| name | ||
| for name, style in model.tp_plan.items() | ||
| if style not in ("ep_router", "grouped_gemm", "moe_tp_experts") | ||
| ) |
There was a problem hiding this comment.
pretty sure default is replicate, so if you pass the ep plan hard coded, it won't follow the one form the config. You don't need any of that in that case no?
There was a problem hiding this comment.
it's not setting a default, it's dropping entries.
tp_plan reads _ep_plan under EP, and dispatch can only shard the experts, so the rewrite keeps grouped_gemm/moe_tp_experts and drops everything else the plan would otherwise shard.
Without it a plan that shards e.g. attention would still shard it, and each rank is training on its own tokens, so that would be wrong rather than just wasteful.
The warning lists whatever got dropped.
There was a problem hiding this comment.
Agreed it's the wrong place to hardcode the style names though. If it moves behind _fsdp_plan in the distributed config as you suggested, this block should go with it. 👌
| hidden_states = hidden_states.to_local() | ||
| with self.context_around_forward(module, mesh): | ||
| # The sharding leaves the module with its local expert count. | ||
| return dispatch_experts_forward( |
There was a problem hiding this comment.
if this func is only used here, declare it here
There was a problem hiding this comment.
Can do, it's ~60 lines and standalone, no moe.py helpers.
Only hesitation is cohesion: it's the fourth experts-forward variant and the other three live in moe.py, so tensor_parallel.py would start importing MoE kernels.
…he batch DistributedConfig(expert_parallel_dispatch=True) sends every selected (token, expert) pair to the rank that owns the expert with an all-to-all, runs the local experts, and sends the results back, instead of running the whole batch on every expert-parallel rank and all-reducing the outputs. Each rank trains on its own part of the batch: the parameters outside the experts are sharded with FSDP2 across every rank (the flattened (fsdp, tp) mesh) so FSDP2 owns their gradient reduction, the experts stay sharded across tp and, if set, fsdp, and the all-to-all backward accumulates their gradients across the group (scaled by 1/ep_size to match the data-parallel average). The Trainer gives each rank its own batches and counts tokens across all of them.
…rded_data_parallelism
…on, as the masked path does
…oken dispatch accelerate only shards and seeds the sampler when it sees more than one data-parallel rank, so with tp_size alone every rank drew its own random full batch.
… forward for the local experts, ids from the count exchange Also reject expert parallel plans that shard anything but the experts, since the ranks no longer see the same batch.
…h in the Trainer ep_dispatch_router and ep_dispatch_experts are substituted into the expert parallel plan when expert_parallel_dispatch is set, so no style carries a flag it ignores and nothing is stored on the modules. get_tp_size() is 1 under dispatch, which the loss scale, the token count and the total batch size all go through; non-random sampling strategies are rejected explicitly.
…rts implementation
…bine passes cost 15-25% of throughput
The dispatch kernel no longer assumes the grouped-GEMM parameter layout: the experts module's forward is called as a top-1 routing with unit weights, so any experts implementation (and any model-specific step inside it) works under dispatch.
… check the batches at init
…the experts implementation check
`expert_parallel_dispatch=True` raised when the expert parallel plan sharded anything but the experts. DeepseekV4's plan does: it shards the lightning indexer colwise and all-reduces the scorer, so `test_ep_forward_2` and `test_ep_backward_1` died in `from_pretrained`. Drop those entries with a warning instead. Under dispatch every rank trains on its own part of the batch, so the trunk is replicated and FSDP2-sharded across the whole mesh anyway, which is what dropping them gives.
d9394c9 to
3a8da11
Compare
18ed512 to
cf74929
Compare
Co-authored-by: VI-Arthur <arthurzucker1@gmail.com>
stevhliu
left a comment
There was a problem hiding this comment.
thanks for the docs!
i think this section may contradict the "Combining with FSDP2" section below it a bit? this was how i interpreted it:
- Token dispatch → dense weights are sharded across everyone
- Combining with FSDP2 → dense weights are replicated on every rank
so it feels like the Combining section "undoes" the first one. it'd be easier to follow i think if we clarified this with a sentence in the Combining section below
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
Dropping it left the parameter with no gradient reduction at all: FSDP2 treats the experts module as expert-owned and skips it, and every rank saw different tokens. fp32 certification on a tiny Muse-shaped model: post_expert_norm.weight at rel 0.80 and 0.50 against a 1-GPU reference under dispatch, 0.75 and 0.59 under dispatch2d; masked and 2-D at 2e-6. A ~0.75 error is the local quarter of the sum that never happened.
A missing or doubled gradient reduction leaves the forward and the loss untouched, so the loss-only check passed on both. Gather each sharded gradient back to the full parameter and compare every parameter's gradient against the single-process reference, and require the same set of parameters to have received one.
CI recapDashboard: View test results in Grafana |
Stacked on #48516 (2-D mesh), itself on #48205. Review this branch against
fsdp2-ep-2d-mesh.Why
EP today runs the whole batch on every rank: each rank keeps its experts, masks the others, and the group all-reduces the expert outputs after every MoE layer. Everything outside the experts (attention, embeddings, norms) does
tp_sizetimes the same work. At 64 GPUs on a 110B model, that is 32 ranks computing the same 2 sequences!With
expert_parallel_dispatch=True, each rank trains on its own part of the batch (the torchtitan / DeepSeek layout). At every MoE layer it routes its tokens, sends each (token, expert) pair to the rank that owns the expert with an all-to-all, runs its local experts, and gets the results back with a second all-to-all. Only the routed tokens travel.Results
Unique tokens per second per node (under masked EP the
tpranks replay the same batch). Qwen3-30B-A3B full fine-tuning, bf16, 8xH100, seq 2048:tp_size=8tp_size=8tp_size=4, fsdp_size=2tp_size=4, fsdp_size=2At scale, GLM-4.5-Air (110B) full fine-tuning on 64xH100 with
tp_size=32, fsdp_size=2, same TRL script for both arms:Both arms train (loss 3.9 -> 1.2 masked, 3.4 -> 1.3 dispatch over 20 steps).
Per-rank step time is longer under dispatch (two all-to-alls and a local experts pass per layer instead of one all-reduce), which is why the small-scale speedup is below
tp_size; with #48201's gating of the sentinel masking the 8-GPU numbers become 5.3x / 4.1x / 3.1x.Correctness
tp_size=8,4x2and2x4. Saved weights within 3.3e-6 of the single-process save.tp_size=4and2x2. The masked path certifies at 2.0e-6 on the same model, so this is the floor.test_fsdp2_expert_parallel_2d_vs_ddp[dispatch]at step 0.Limitations
dispatch_batches=Trueand non-random sampling strategies are rejected.fsdp_size=1the experts stay outside FSDP2, sofsdp_mixed_precisionandfsdp_cpu_offloaddo not apply to them.tpranks evaluate the same batches.