Skip to content

Fix Double Application of Softmax for Router Logits in MoE models - #45346

Open
ionut-anghelina wants to merge 3 commits into
huggingface:mainfrom
ionut-anghelina:dev/ionut/FixDoubleSoftmax
Open

Fix Double Application of Softmax for Router Logits in MoE models#45346
ionut-anghelina wants to merge 3 commits into
huggingface:mainfrom
ionut-anghelina:dev/ionut/FixDoubleSoftmax

Conversation

@ionut-anghelina

Copy link
Copy Markdown

No description provided.

ionut-anghelina and others added 2 commits March 30, 2026 08:18
Several MoE routers applied softmax to raw logits inside forward() but
returned the result as `router_logits`. The load_balancing_loss_func then
applied softmax again, computing the aux loss on softmax(softmax(logits))
which flattens the distribution toward uniform, rendering the load-balancing
loss ineffective.

Fix: use a separate `router_probs` variable for the softmaxed values used
in top-k routing, keeping `router_logits` as raw logits so the loss
function's single softmax is correct.

Source modular files fixed:
- mixtral/modular_mixtral.py (MixtralTopKRouter)
- qwen2_moe/modular_qwen2_moe.py (Qwen2MoeTopKRouter)
- qwen3_vl_moe/modular_qwen3_vl_moe.py (Qwen3VLMoeTextTopKRouter)

Downstream models regenerated by make fix-repo:
mixtral, minimax, qwen2_moe, olmoe, flex_olmo, qwen3_moe, qwen3_next,
qwen3_omni_moe, qwen3_vl_moe, qwen3_5_moe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add regression tests in mixtral and qwen2_moe to verify router_logits
  are raw logits (not softmax probabilities)
- Fix .to() dtype cast to use router_logits.dtype (model dtype) instead
  of router_probs.dtype (float32)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -89,6 +89,14 @@ def test_load_balancing_loss(self):
self.assertEqual(result.router_logits[0].shape, (91, config.num_local_experts))
torch.testing.assert_close(result.aux_loss.cpu(), torch.tensor(2, dtype=torch.float32), rtol=1e-2, atol=1e-2)

# Verify router_logits are raw logits, not softmax probabilities (regression test for double-softmax bug)

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.

Iirc, we have more appearances of that test in other models. It doesnt hurt to add them to all we have + maybe make it a generalized one in causal lm tester (because we now have ways to properly detect moes with the interface)

@vasqu

vasqu commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

@Rocketknight1 I'm not sure about the current state here so just left a comment here since it seemed the most recent state of things. Lmk if not or where I should properly look at

@vasqu

vasqu commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

Let's also add the fixes and closes statements for the issue and other PR please

@vasqu

vasqu commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Ok #45131 was merged instead, we can still use this PR for tests tho @Rocketknight1 @ionut-anghelina

@github-actions

Copy link
Copy Markdown
Contributor

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

run-slow: mixtral, qwen2_moe

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

@ErenAta16

Copy link
Copy Markdown

This diff only adds tests, test_modeling_mixtral.py and test_modeling_qwen2_moe.py, no source change in either model's router code.

Checked both MixtralTopKRouter.forward and Qwen2MoeTopKRouter.forward against current main: both compute router_logits = F.linear(hidden_states, self.weight) and return that raw value as the first element (the one captured by OutputRecorder(..., index=0) into outputs.router_logits). router_probs = softmax(router_logits, ...) is a separate local variable used only to compute top-k routing weights, it's never what gets returned as router_logits. So on current main, router_logits already looks like raw logits, not softmax output, which is what the new regression tests assert.

Where's the actual double-softmax happening? If it's in load_balancing_loss_func (e.g. if router_logits ever gets passed in already-softmaxed from some other call site) rather than in the router forward itself, that source fix seems to be missing from this diff, only the router-output-level regression test is here. Want to make sure I'm not missing the actual repro before assuming this needs a source change added.

@vasqu

vasqu commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

#45131 fixed it, and then this PR kind of got abandoned because it aimed to do the same 😅

@ErenAta16

Copy link
Copy Markdown

That explains it, #45131 (merged 2026-04-13) is exactly what I was seeing on current main: both routers already return raw logits, not probabilities. Confirms the analysis rather than contradicts it, the fix landed, this PR's tests just never got reconciled with that. Makes sense to close this one as superseded.

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.

5 participants