Clamp capacity to num_tokens in MoE gating drop branches (complete #5353)#8155
Clamp capacity to num_tokens in MoE gating drop branches (complete #5353)#8155ebarkhordar wants to merge 1 commit into
Conversation
torch.topk(x, k=capacity, dim=0) over the token dimension requires capacity <= num_tokens. PR deepspeedai#5353 added this clamp to top1gating's no-drop branch but not to the drop branches, where capacity feeds torch.topk: topkgating drop_policy='probs' and top1gating's drop path (via _top_idx) raise 'selected index k out of range' when capacity_factor * k > num_experts. Apply the same clamp to both drop branches and add a regression test per branch. Signed-off-by: Ehsan Barkhordar <realbarkhordar@gmail.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 737050a869
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| else: | ||
| # Same guard for the drop branch: capacity feeds torch.topk(..., dim=0) over the token | ||
| # dimension in _top_idx below, which requires capacity <= num_tokens. | ||
| capacity = min(capacity, torch.tensor(mask1.size(0)).to(capacity.device)) |
There was a problem hiding this comment.
Preserve dispatch capacity when clamping top-k
When tensor-model parallelism is enabled, MOELayer.forward later calls drop_tokens on the capacity dimension, and drop_tokens asserts that this dimension is divisible by the TP world size (deepspeed/moe/mappings.py:64-65). For top-1 with drop_tokens=True, capacity_factor > num_experts, and a local token count that is not TP-divisible, this line sets the dispatch capacity to num_tokens; _top_idx succeeds, but the subsequent TP drop still fails. Clamp only the k passed to _top_idx (or repad the dispatch capacity) instead of reusing the clamped value for _one_hot_to_float.
Useful? React with 👍 / 👎.
| # branch below selects tokens with torch.topk(..., k=capacity, dim=0) over the token | ||
| # dimension, which requires capacity <= num_tokens; #5353 added this same clamp to | ||
| # top1gating's no-drop branch but not to the drop branches. | ||
| capacity = min(capacity, torch.tensor(gates.size(0)).to(capacity.device)) |
There was a problem hiding this comment.
Keep the clamp out of the position policy
When callers use topkgating(..., drop_policy='position') with a configured capacity or min_capacity greater than the number of tokens, this clamp changes the returned dispatch capacity even though that branch never passes capacity to torch.topk(..., dim=0). Previously, for example, min_capacity=8 with 4 tokens returned a capacity width of 8; now it returns 4, violating the min-capacity/padding behavior and removing padding that downstream code may require. Move the clamp into the probs branch or use a separate top-k selection limit.
Useful? React with 👍 / 👎.
|
Nice, straightforward implementation. |
Problem
torch.topk(x, k=capacity, dim=0)over the token dimension requirescapacity <= num_tokens. In the MoE gating codecapacityisceil(num_tokens / num_experts * capacity_factor * k), so whenevercapacity_factor * k > num_expertsthe capacity exceeds the number of tokens and thetopkcall raisesRuntimeError: selected index k out of range.PR #5353 ("Ensure capacity does not exceed number of tokens") added the guard
capacity = min(capacity, num_tokens), but only totop1gating's no-drop branch. The two drop branches that actually feedcapacityintotorch.topk(..., dim=0)were left unguarded:topkgating,drop_policy='probs':torch.topk(topk_masked_gates, k=capacity, dim=0)(sharded_moe.py:410)top1gating,drop_tokens=True:_top_idx(mask1_rand, capacity)→torch.topk(source, k=capacity, dim=0)(sharded_moe.py:176/248)drop_policy='position'andtop2gatingusetorch.ltrather thantorch.topk, so they are unaffected and are left untouched.Fix
Apply the same
capacity <= num_tokensclamp #5353 introduced to the two drop branches. Reducingcapacitytonum_tokensis a no-op whenever capacity already fits, and it cannot drop any token that should route (a per-expert dispatch buffer never needs more slots than there are tokens).Invariant
capacitypassed totorch.topk(..., k=capacity, dim=0)over the token dimension is<= num_tokenson every gating path. Enumerated thetorch.topk(..., dim=0)call sites insharded_moe.pywithast(not grep): exactly two, both listed above;torch.topk(gates, k=k, dim=1)at :392 is over the expert dimension withk <= num_expertsand is not involved.Verification
Reproduced and fixed in a clean
python:3.10-slimDocker container at HEAD,pip install -e .,torch==2.5.1+cpu(confirmedsharded_moe.__file__resolves to the installed source):topkgating(logits[8,2], k=2, capacity_factor=2, drop_policy='probs')raisesRuntimeError: selected index k out of range, andtop1gating(logits[8,2], capacity_factor=4, drop_tokens=True)raises the same error via_top_idx.num_tokens, and no routed token is dropped.tests/unit/moe/test_moe.py(one per branch); both fail on the unpatched source and pass with the fix. The pre-existingtest_moe.pygating tests still pass.Not verified: I did not measure whether a real training configuration in the wild sets
capacity_factor * k > num_experts; the argument here is that the crash path exists and that #5353 already establishedcapacity > num_tokensas a condition worth guarding.