Skip to content

fix(moe): map FSDP expert shards with torch.chunk layout - #4058

Open
MohammedAlkindi wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
MohammedAlkindi:MohammedAlkindi/fix/moe-expert-shard-chunk-layout
Open

MohammedAlkindi wants to merge 4 commits into
NVIDIA-NeMo:mainfrom
MohammedAlkindi:MohammedAlkindi/fix/moe-expert-shard-chunk-layout

Conversation

@MohammedAlkindi

@MohammedAlkindi MohammedAlkindi commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do ?

With ep_size: 1, grouped expert weights are FSDP2-sharded with Shard(0), which splits like torch.chunk, but get_expert_slice_for_rank and the Kimi K3 and Kimi Linear _split_experts_weights overrides assume an even split. HF conversion then fails whenever the expert count does not divide evenly, for 60 experts on 8 ranks with Expected local tensor first dimension to be 7 (experts 32:39), got 8.

Changelog

  • Add get_sharded_expert_range, which mirrors the torch.chunk layout, and use it at all three sites. Evenly divisible sizes get the same ranges as before.

Before your PR is "Ready for review"

Pre checks:

  • Make sure you read and followed Contributor guidelines
  • Did you write any new necessary tests?
  • Did you add or update any necessary documentation?

If you haven't finished some of the above items you can still open "Draft" PR.

Additional Information

  • Related to # (issue): none filed.

test_state_dict_utils.py on 5eed9bb: 33 passed, 1 skipped. With the 2ed5044 logic, the 10/4, 60/8 and 64/24 uneven-shard cases fail with that ValueError. Not run on multiple GPUs. AI-assisted; I ran these tests myself.

With ep_size 1, grouped expert weights are FSDP2-sharded with Shard(0)
on dp_shard_cp, and FSDP2 and DTensor split that axis like torch.chunk:
leading ranks hold ceil(n / world_size) experts and trailing ranks the
remainder or nothing. get_expert_slice_for_rank and the Kimi K3 and
Kimi Linear _split_experts_weights overrides assumed a balanced split
instead, so whenever the layouts differ (for example 60 experts on 8
ranks, 64 on 24, 128 on 12) a rank's local count mismatched and HF
conversion raised "Expected local tensor first dimension to be ...".

Use Shard.local_shard_size_and_offset, as the deepseek_v41 and
glm5_next adapters already do. Evenly divisible sizes, which the EP
path asserts, get the same ranges as before.

Signed-off-by: Mohammed Alkindi <[email protected]>
@copy-pr-bot

copy-pr-bot Bot commented Sep 29, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@HuiyingLi

Copy link
Copy Markdown
Contributor

/ok to test c1a42cd

@akoumpa

akoumpa commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

This is not a behavior-equivalent refactor: it changes uneven sharding from a balanced remainder distribution to DTensor's torch.chunk layout. That behavior change is the intended fix.

For 5 experts across 4 ranks, the computed (size, offset) values are:

Rank Old code New code
0 (2, 0) (2, 0)
1 (1, 2) (2, 2)
2 (1, 3) (1, 4)
3 (1, 4) (0, 5)

The actual Shard(0) layout follows the new column. In Shard.local_shard_size_and_offset, return 0, curr_local_size represents an empty shard at the end of the current tensor. Here, rank 3 therefore gets the empty expert range [5:5], whereas the old code incorrectly assigns it one expert.

Divisible splits remain equivalent; uneven splits can change.

@akoumpa

akoumpa commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Hi @MohammedAlkindi ,

Thanks for making this!

I looked into the uneven sharding path of local_shard_size_and_offset and it seems that it opens up a case where one rank can get 0 experts weights; I think this would open up all sort of issues (from low util to functional errors), so i'd prefer not to proceed with local_shard_size_and_offset in this case.

Exercise of the uneven shard path is model dependent and not all models may exercise it, but in the future, a coding agent may see this function used and copy the pattern, thus introducing a bug in a model that does exercise it.

If you'd like to pull out a shared helper and reuse it across the state adapters that would be much better.

Please let me know what you think. Thanks again!

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Sep 29, 2026
Replace the three Shard.local_shard_size_and_offset calls with one
get_sharded_expert_range helper in state_dict_utils, used by the
EP-free branch of get_expert_slice_for_rank and by the Kimi K3 and
Kimi Linear _split_experts_weights overrides. Its docstring states that
it mirrors an existing Shard(0) layout and must not be used to choose
a placement. Add tests pinning it to torch.chunk, including empty
trailing ranks.

Signed-off-by: Mohammed Alkindi <[email protected]>
@MohammedAlkindi

Copy link
Copy Markdown
Contributor Author

Agreed on the shared helper. 5eed9bb adds get_sharded_expert_range to state_dict_utils, and the EP-free branch and both Kimi overrides use it, with no local_shard_size_and_offset call left. On the empty rank: that comes from FSDP2, not the helper. fully_shard splits the expert dim through _chunk_with_empty (torch.chunk plus empty chunks), so 64 experts on 24 ranks already leaves ranks 22 and 23 with empty local tensors, and the old formula crashed on exactly those shapes. The helper only labels what a rank already holds, and its docstring says not to use it to choose a placement, which is aimed at the copy-the-pattern risk. New tests pin it to torch.chunk for six shapes, including empty trailing ranks.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added waiting-on-maintainers Waiting on maintainers to respond and removed waiting-on-customer Waiting on the original author to respond labels Sep 30, 2026

@HuiyingLi HuiyingLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Approve

Reviewed 89198045259f0908ce8d68cc1d690236bc9aeaaf against 112da8aa4c1128fec9186f81efa79c858db61a5b, including the shared helper, both Kimi overrides, and their native checkpoint load/save consumers. I found no introduced blocking issue.

The shared helper is appropriate here: it labels the shards FSDP already holds and removes the same incorrect range calculation from three consumers. It does not introduce empty shards or change expert placement. Supported EP configurations require divisibility, so their ranges remain unchanged; the separate balanced EP ownership helper would not describe these uneven FSDP shards correctly.

Validation:

  • The three changed native unit-test files: 55 passed, 1 skipped (the CUDA-only unit test was skipped in the CPU run).
  • Four real CPU/Gloo ranks with actual DTensors: 112/112 rank-case checks passed, covering shared splitting, both Kimi adapters, exact expert IDs/values/HF keys, empty tails, and replicated/inner-axis/HSDP/divisible-EP controls. The actual baseline produced 25 expected regression failures; all 72 control checks passed on both versions.
  • Four H100s, native AutoModel FSDP2 loading with EP/TP/CP=1: a tiny DeepSeek-V3 fixture with 5, 10, and 8 experts exercised local counts [2,2,1,0], [3,3,3,1], and [2,2,2,2]. After complete production setup, every parameter/buffer matched its source value, shape, and dtype exactly on every rank; fixed-input logits had maximum error 0. Native forward/backward, a finite nonzero-gradient optimizer step, and save/consolidation completed. All 15/30/24 expert HF tensors, respectively, matched independently derived post-training values exactly. The actual baseline failed during native loading for both 5 and 10 experts with the expected local-count errors.

One existing limitation remains visible: the full export check failed solely on e_score_correction_bias (maximum difference 0.002). With FSDP optimizations disabled for this topology, that replicated buffer diverges across training ranks and the checkpoint contains rank 1's value rather than the observer's rank 0 value. The unmodified baseline's 8-expert control reproduces the same failure, and every post-training parameter/buffer is identical between baseline and candidate within corresponding ranks. This is not an introduced finding or a full-checkpoint export pass.

Coverage is one-layer FP32, SDPA/torch experts, 16-token native training; Kimi coverage is distributed adapter components, not full Kimi checkpoint loading. The custom observer calls the native recipe and checkpointer and only inspects state; it does not substitute a loader or repair loaded values. Runtime: PyTorch 2.14.0a0+4fdf77b940.nv26.08, Transformers 5.18.0.

@HuiyingLi

Copy link
Copy Markdown
Contributor

/ok to test 8919804

@HuiyingLi HuiyingLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: Approve

Bot suggestion — second review, on 9588a15839027a90917c04e8620ba5e173f06597 against 3ea1de8d0aa902c3329b585d194c2e5274a85f3f. I found no introduced blocking issue.

The six changed files are unchanged from the first reviewed head. I rechecked the merge with current main, particularly its new upstream safetensors reader. The shared helper remains the right scope: it labels existing FSDP torch.chunk storage at all three consumers. The balanced EP ownership helper cannot describe those uneven shards; supported divisible EP layouts retain their previous ranges.

Fresh validation on this head:

  • Three native unit-test files: 55 passed, 1 CUDA-only test skipped in the CPU invocation.
  • Four real CPU/Gloo ranks, actual DTensors: 112/112 checks passed, including 5/10-expert uneven shards, empty tails, both Kimi adapters, exact expert IDs/values/HF keys, and replicated/inner-axis/HSDP/divisible-EP controls. Current main reproduces 25 expected failures; all 72 unaffected control checks pass on both versions.
  • Two H100s, native AutoModel FSDP2 (dp_shard=[0,1], EP/TP/CP/PP=1), loading the retained native five-expert checkpoint: the actual reader is _CachedMetadataHFReader; all 20 parameters/buffers match source values, shapes and dtypes exactly on each rank. Local expert counts are [3,2]; fixed-input logits have maximum error 0. Native forward/backward, two accumulated microbatches, one AdamW update and save/consolidation complete. All 15 expert HF tensors exactly match independently derived post-training values.

The full export gate still fails solely on the pre-existing replicated e_score_correction_bias: the saved value matches rank 1 and differs from rank 0 by up to 0.002. Current main reproduces the same per-rank bias and export failure. This is not a full-checkpoint export pass or a new PR finding.

The default candidate/main post-training comparison is not bitwise (maximum parameter difference 2.48e-7, equal loss). A separate strict-determinism pair, with identical recorded input hashes, produces bit-identical post-training parameters and buffers on both ranks. The specific source of default runtime variation was not isolated; the original failed exact comparison remains recorded.

Coverage is a tiny one-layer FP32 model, SDPA/torch_mm, 16-token training. Two ranks cannot distinguish the old/new partition formulas: the four-rank CPU tests provide that regression check, and the previous four-H100 native evidence remains separate. Kimi validation is adapter-component coverage, not full Kimi checkpoint loading. The observer uses the production recipe/checkpointer and only records state, reader selection and inputs; it does not replace loading or repair weights. Runtime: PyTorch 2.14.0a0+4fdf77b940.nv26.08, Transformers 5.18.0.

@svcnvidia-nemo-ci svcnvidia-nemo-ci removed the waiting-on-maintainers Waiting on maintainers to respond label Nov 15, 2026

This branch was previously deployed

3 inactive (outdated) deployments
public — 89198045 Deployed Nov 2, 2026 by copy-pr-bot[bot] via release / finalize / notify #6408
test — 89198045 Deployed Nov 2, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #12567
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants