Skip to content

EP + Trainer integration on top of DistributedConfig (#45028) - #46126

Closed
AmineDiro wants to merge 1 commit into
huggingface:mainfrom
AmineDiro:feat/ep-sft-trainer-on-45028
Closed

AmineDiro wants to merge 1 commit into
huggingface:mainfrom
AmineDiro:feat/ep-sft-trainer-on-45028

Conversation

@AmineDiro

@AmineDiro AmineDiro commented May 20, 2026 •

Copy link
Copy Markdown
Member

Builds on #45028 (refactor-tp-dtensor) so that Expert Parallelism actually runs on any MoE model that has a base_ep_plan:

DistributedConfig(enable_expert_parallel=True, tp_size=N, fsdp_size=M)

I was validated end-to-end with trl.SFTTrainer on Qwen3-30B-A3B at multiple shapes (16k / 32k / 64k, 2n / 4n / 8n). Healthy training (loss bit-exact match against our prod fork at LR=0) and very competitive MFU 👏🏼 👏🏼

Best run: 64k ctx length, 2 nodes, EP=16 + sonicmoe + Liger + compile = 62.8 % window MFU / 35 % causal-adjusted, peak GPU mem 45 GB.

What does this PR do?

  • Register ep_router + moe_experts_ep_allreduce ParallelStyles (distributed/tensor_parallel.py). RouterParallel masks non-local expert scores. MoEExpertsParallel shards on the experts dim.
  • model._tp_plan → model.tp_plan so enable_expert_parallel=True actually selects _ep_plan (the property override at modeling_utils.py:1459).
  • Trainer._clip_grad_norm routes to the mesh-aware streaming clip (trainer.py) when on the native path. Stock accelerator.clip_grad_norm_ across cross-mesh DTensor grads crashes on the first optimizer.step.
  • Trainer.create_optimizer calls maybe_disable_foreach_and_fused_for_mixed_dtensor_groups helper; Adam fused/foreach rejects mixed Tensor/DTensor groups under fsdp_size=1.
  • Trainer.compute_loss divides loss_scale by tp_size for the DistributedConfig path (trainer.py). Companion to Trainer.compute_loss: fix loss over-counting under TP and EP-as-TP #45994 (which only handles parallelism_config.tp_size).
  • Biggest change is that I introduce Trainer.is_native_distributed flag, exclusive with is_fsdp_enabled / is_deepspeed_enabled @3outeille tell me what you think. This is made to make clear when we are running in native vs accelerate accross branches.
  • Qwen3MoE base_model_ep_plan simplified to use moe_experts_ep_allreduce: @ArthurZucker I wonder if we need to have this ep plan dynamically set by the user and not part of the model definition 🤔

Note:

This PR depends on :

Who can review?

@3outeille @ArthurZucker @SunMarc

@github-actions

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: qwen3_moe

@github-actions

Copy link
Copy Markdown
Contributor

View the CircleCI Test Summary for this PR:

https://huggingface.co/spaces/transformers-community/circle-ci-viz?pr=46126&sha=e8de65

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

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.

@AmineDiro
AmineDiro marked this pull request as ready for review May 21, 2026 06:58

@ArthurZucker ArthurZucker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ty, let's not push for wrong patterns

Comment on lines +506 to +512
"moe_experts_ep_allreduce": MoEExpertsParallel(
output_layouts=Replicate(),
shard_plan={
"gate_up_proj": Shard(0),
"down_proj": Shard(0),
},
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No no no no nonoooooo this is the worst pattern that was ever introduced in the lib, sorry you have to "pay the price" but EP should be back to how we did it: gate_up_proj gets "gemm" and down_proj as well, te module gets ep_allreduce!

@AmineDiro AmineDiro May 27, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no worries 🤣 ! Ok, I was just following the exact implementation pattern:

"moe_experts_allreduce": MoEExpertsParallel(

Just sharding on the dim 0 for the experts. If I go back to :

        "layers.*.mlp.experts.gate_up_proj": "grouped_gemm",
        "layers.*.mlp.experts.down_proj": "grouped_gemm",
        "layers.*.mlp.experts": "moe_experts_allreduce",

There is some implementation logic that uses the new TensorParallelStyle that needs to be rewritten, I guess

local_grads = [_replicate_dtensor(g).to_local() if isinstance(g, DTensor) else g for g in grads]
total_norm = torch.nn.utils.get_total_norm(local_grads, norm_type=norm_type)
torch.nn.utils.clip_grads_with_norm_(grads, max_norm=max_norm, total_norm=total_norm)
"""Mesh-aware grad-norm clip with O(1) extra memory per parameter."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove unrelated if unrelated

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh yes I created I different PR : #46113 for the grad clipping issue

Comment on lines -89 to -91
"layers.*.mlp.experts.gate_up_proj": "grouped_gemm",
"layers.*.mlp.experts.down_proj": "grouped_gemm",
"layers.*.mlp.experts": "moe_experts_allreduce",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is what we want

Comment on lines +825 to +839
# deepspeed and accelerate flags covering both trainer args and accelerate launcher.
# Treated as exclusive with `is_native_distributed`: if transformers already manages
# distribution, the accelerate plugin (e.g. left over from `accelerate launch --use_fsdp`)
# is not the responsible party — skip its code paths to avoid operating on a model
# accelerate didn't wrap.
_ds = getattr(self.accelerator.state, "deepspeed_plugin", None) is not None
_fsdp = getattr(self.accelerator.state, "fsdp_plugin", None) is not None
if self.is_native_distributed and (_ds or _fsdp):
logger.warning_once(
"`distributed_config` is set on the model AND accelerate has a "
f"{'deepspeed' if _ds else 'fsdp'}_plugin in state. Trusting `distributed_config` "
"(transformers-native) and ignoring the accelerate plugin."
)
self.is_deepspeed_enabled = _ds and not self.is_native_distributed
self.is_fsdp_enabled = _fsdp and not self.is_native_distributed

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not super clean IMO but in trainer soooo

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hah, agreed on cleanliness, but let me defend this 😆. I just tried to localize variables that clearly indicate which mode we are running, so we can do stuff like self.is_deepspeed_enabled or self.is_fsdp_enabled or self.is_native_distributed. Because we can be running in fsdp from accelerate of Zero3 from native and maybe have passed the DistributedConfig, we just want to avoid overlap and be explicit in the condition.
But happy to change this if you have a better idea!

@3outeille 3outeille mentioned this pull request May 28, 2026
5 tasks done
@AmineDiro AmineDiro closed this Aug 10, 2026
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.

3 participants