MORI + Permute/Unpermute zero token fix - #155
Open
sudhu2k wants to merge 5 commits into
Open
Conversation
…P ranks TE's fused unpermute short-circuits on empty input and returns [0, H]. Wrap it in `unpermute()` to emit zeros(restore_shape) and preserve the autograd graph so combine backward can run on a cold rank. Remove the previous workaround that clamped `_MoriManager.num_out_tokens` to >=1, allowing an honest empty permute. Add unit and distributed cold-rank tests and fix MORI teardown order.
…patch-buffer gradient TE's fused permute backward short-circuits on an empty [0, H] gradient and does not restore the [R, H] dispatch-buffer shape. Add a cold-rank guard in `permute()` to use the native `index_select` path when `num_out_tokens == 0`, whose autograd emits `zeros([R, H])` for an empty selection. The forward path is a no-op, so there is no perf cost. This is symmetric with the existing cold-rank guard in `unpermute()`. Update the dispatcher test to verify backward now runs cleanly on a cold rank.
…ros workaround TE's fused unpermute short-circuits on an empty input and returns [0, H]. Instead of synthesizing zeros(restore_shape) and manually keeping it on the autograd graph, fall back to the native scatter path when permuted_tokens.numel() == 0. The native path naturally emits zeros(restore_shape) and preserves the empty input for backward, keeping the combine backward symmetric across ranks. This is complementary to the cold-rank guard in permute().
Remove the `ENABLE_EXPERIMENTAL` guard, manual traceback capture, and the now-unused `reset_mori_op` import from `test_cold_rank_fused_permute_sequential_mlp`. The underlying cold-rank permute/unpermute issues are resolved, so the test can run with the standard dispatcher test path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Under imbalanced routing, an expert-parallel (EP) rank can legitimately receive zero
tokens — a "cold" rank. With
moe_permute_fusionenabled, TransformerEngine's fusedpermute/unpermute kernels short-circuit on empty
[0, H]tensors, which corrupted the MORIcombine buffer on the forward pass and raised in the permute backward, deadlocking the job.
This PR makes cold ranks a first-class case on both the dispatch and combine seams, in both
the forward and backward passes. The fix is entirely on the Megatron side and requires no
TransformerEngine changes.
Motivation
Cold ranks arise in normal training whenever the router is skewed. The prior code masked the problem by clamping
num_out_tokensto a minimum of 1, which fabricated a dummy token row on cold ranks. That workaround avoided the immediate crash but left the forward combine buffer semantically wrong and did not address the backwardpass at all, so training under real skew was unsafe.
Root cause
The empty-tensor handling in TE's fused kernels fails only in the unpermute/combine direction —
which shows up on two distinct autograd functions:
Combine seam —
fused_unpermute(forward).On a cold rank the expert output is
[0, H]. TE's fused unpermute short-circuits(
if not inp.numel(): return inp) and returns[0, H]instead ofzeros(restore_shape).The collapsed shape then flows into
op.combine(x=[0, H],total_recv=0), corrupting thecombine forward and zeroing the combine backward.
Dispatch seam —
fused_permute(backward).On a cold rank the permute input is non-empty (local tokens exist) but the output is
empty, so the incoming gradient is
[0, H]. TE's permute backward short-circuits on that emptygradient and dereferences
ctx.probs, which is never set on the non-empty-input path, thenreturns a gradient of the wrong shape. The resulting exception fires only on cold ranks; they
abort backward and proceed to teardown while hot ranks block in MORI finalize
(
cuda.synchronize) waiting on collective work the cold ranks never posted — a hang thatsurfaces in process-group finalize/destroy but originates in the cold-rank backward.
Changes
megatron/core/transformer/moe/token_dispatcher.pyRemove the
num_out_tokens = max(num_out_tokens, 1)clamp in_MoriManager. With the guardsbelow, an honest empty permute (
[0, H]) is handled correctly end-to-end, so the dummy row is nolonger needed.
megatron/core/transformer/moe/moe_utils.pyAdd two symmetric cold-rank guards that fall back to the native (non-fused) paths, which have
correct native autograd for empty selections:
permute: the nativeindex_selectpath yields[0, H]on the forward (a no-op over zerorows) and, on the backward, scatters the incoming
[0, H]gradient intozeros_like(tokens),producing the correct
[R, H]dispatch-buffer gradient (a cold rank contributes no gradient).unpermute: the native scatter path fillszeros(restore_shape)on the forward and, on thebackward, computes
grad.gather(empty) = [0, H], keeping the empty input on the autograd graphso the combine backward stays symmetric across ranks. This replaces the previous workaround that
synthesized
zeros(restore_shape)and manually reattached it via+ permuted_tokens.sum() * 0.Both guards are no-ops on non-cold ranks and carry no performance cost (the fallback executes over
zero rows only).
Tests
tests/unit_tests/transformer/moe/test_moe_unpermute.py(new): unit test asserting that fusedunpermuteon[0, H]input restoreszeros(restore_shape)— verifying shape, dtype, device,and all-zero contents.
tests/unit_tests/transformer/moe/test_token_dispatcher.py: addsTestMoriColdRank, adistributed (8-way EP) test that forces all tokens onto the first
topkexperts so higher EPranks receive zero tokens, runs dispatch → SequentialMLP → combine, and asserts that
.backward()completes with the correct[R, H]dispatch-buffer gradient. MORI shmem isfinalized once at teardown, since MORI cannot finalize and reinitialize shmem within the same
process.