Skip to content

[OMNIML-6060] Load an expert-parallel plan through load_pretrained - #2780

Open
shengliangxu wants to merge 32 commits into
shengliangx/hfep-6a2-ep-loader-corefrom
shengliangx/hfep-9b-ep-load-routing
Open

shengliangxu wants to merge 32 commits into
shengliangx/hfep-6a2-ep-loader-corefrom
shengliangx/hfep-9b-ep-load-routing

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Re-parented on 2026-10-30 onto #2741 [2/3] (#3136), the expert-parallel loader's core, when #2741 was split. This PR also adopts #2779's newer collective load checks; see the end of Testing.

Base: #2741 [2/3] (#3136). The diff below is this PR's alone. It merges after #2741 [3/3] (#2741), whose refusals keep load_pretrained's new expert-parallel route from quantizing unsupported experts wrong.

Series layout

What does this PR do?

Type of change: New feature

#2779 routes load_pretrained by a DistributionPlan, but refuses a plan that shards the routed experts: the expert-parallel loader (#2741) comes later in the series. With it in, this PR adds that route:

  • An expert-parallel plan loads through load_and_prepare_ep, handed the plan itself. Its arguments are checked with the FSDP2 load's, in _sharded_load_kwargs inside [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's collective "load checks" block; _load_sharded then runs the load. That load replicates the dense part; FSDP2-sharding it comes with [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742, a sibling of this PR. Its freeze and require_lm_head options pass through, and so do the Hub options (token, cache_dir, local_files_only), which its config read and from_pretrained use. The sharded loads' refusals from [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 cover it too: a Nemotron-H checkpoint's MTP head, which load_and_prepare_ep does not build, is refused rather than exported unquantized. Until [OMNIML-6061] Load the dense part sharded, and size the load warmup to each rank #2742 is in, an expert-parallel plan that also shards the dense part is refused before anything loads, naming the ways out: replicate it anyway, or FSDP2-shard the whole model. A plan left to decide shards the dense part when a whole copy of it and a rank's share of the experts would exceed 60% of a GPU.
  • The decision refuses to leave an expert-parallel MoE whole on every rank unasked, naming the expert count the EP degree must divide and every way out: shard its experts (an EP degree is offered only when one divides both the experts and the world), shard the whole model with FSDP2 (an unquantized checkpoint), load it in a single process, or ask for the replication: MODELOPT_EP_DEGREE=1 with MODELOPT_FSDP_SIZE=1, or a replicated plan handed to load_pretrained, which skips the decision. A replication both knobs ask for loads, with a note that every rank holds the whole MoE. So [OMNIML-6054] Decide how a model is distributed, from the model built on the meta device #2737's refusals of routed experts the decision can neither count nor shard by now advise MODELOPT_EP_DEGREE=1 with MODELOPT_FSDP_SIZE=1 (ep_size=1 with dense='replicate' for a caller passing the arguments): an EP degree of 1 alone now meets the refusal above.

This closes the series' second phase: an MoE whose expert-parallel plan ships with transformers, such as Qwen3-MoE, now loads expert-parallel through load_pretrained and calibrates. The model specs come next (#2746):

Usage

# torchrun --nproc_per_node 8 quantize.py
from modelopt.torch.utils.distributed import distributed_run
from modelopt.torch.utils.plugins.model_load_utils import load_pretrained

with distributed_run():
    model = load_pretrained("Qwen/Qwen3-30B-A3B")  # experts sharded over the 8 ranks
    # ... mtq.quantize(model, ...), export_hf_checkpoint(model, ...)

Testing

transformers Passed Skipped Failed
5.18 210 0 0
5.5.4 182 28 0
GPUs Passed Failed
2 4 0
4 4 0
  • Rebased on 2026-10-30 (d78aaf8) onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136's restack (b726fb3) after [OMNIML-6060] [1/3] Add require_transformers, and build a VLM without its LM head #3131 merged to main (62a5c48). The base under [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 now carries [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's newer round, so 7 hunks in 3 files conflicted. All are resolved by applying this PR's change to the new text:
  • Review round, 2026-10-30 (50e64f0):
    • The dtype refusal names FSDP2's mp_policy for the FSDP2 load only (90c914a).
    • A rank without a GPU of its own is refused before the expert-parallel load too, as before the FSDP2 load. The remedy names the way to a whole model for the route, from one helper shared with the placement refusal (50e64f0).
    • test_a_sharded_load_refuses_what_it_cannot_honor gains a no_gpu row and an mp_policy assertion. Each fails without its change.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (469) and 5.5.4 (427, 31 skipped).
  • Review round, 2026-10-30 (3c353af):
    • A whole-model load refuses the options only the expert-parallel load takes (freeze, require_lm_head) before anything loads, naming that load. Before, they reached from_pretrained and failed inside transformers.
    • test_a_load_refuses_what_it_cannot_load gains ep-option-on-a-whole-load, which fails without the change.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (470) and 5.5.4 (428, 31 skipped).
  • Review round, 2026-10-30 (b1f5d8c):
    • The left-whole refusal names the knobs as the caller set them: ep_size/dense arguments, which override the environment knobs, or the knobs (b1f5d8c). A new explicit_degree_one_as_an_argument row fails without it.
    • The docstring says the expert-parallel load freezes the model unless freeze=False, unlike the other loads (1072e8f).
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (471) and 5.5.4 (428, 32 skipped).
  • Review round, 2026-10-30 (4a1e020):
    • The refusal of an MoE left whole on every rank is a ValueError, like the decision's other refusals of what its inputs resolve to (d0a63ff).
    • A load handed an expert-parallel plan and no config reads the checkpoint's config once, for its model type and for the load, as the FSDP2 load already did (4a1e020). The EP-loader test asserts the load gets that read.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (471) and 5.5.4 (428, 32 skipped).
  • Review round, 2026-10-30 (4040ea3):
    • freeze or require_lm_head passed as None asks nothing, as other options passed as None do, rather than reading as False and dropping the LM head (83d9ce0).
    • The expert-parallel route returns the model unfrozen unless freeze=True, as the other routes do (69e56a9).
    • A decided expert-parallel layout says it was decided when it refuses a whole-model placement, as the FSDP2 route already did (4040ea3).
    • Each change fails a test when reverted. Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (472) and 5.5.4 (428, 33 skipped).
  • Restack and review round, 2026-10-30 (8f9c8d9), onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 1491b66. The rebase had no conflicts, and this PR's own commits are unchanged; only the first commit's context moved.
    • A decided expert-parallel layout now takes the load's own freeze and require_lm_head, rather than refusing them as a whole-model placement (1db4cc3). A new its-own-options row fails without the change.
    • The docstring of _refuse_a_rank_without_its_gpu says what the check adds beside [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136's GPU check in the expert-parallel load, which now refuses a run where only some ranks have a GPU (8f9c8d9).
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (476) and 5.5.4 (430, 35 skipped).
  • Review round, 2026-10-30 (d466147):
    • A sharded load now refuses an option that only the other sharded load takes by naming that load, rather than with the whole-model way out, which would refuse the option again. broadcast_chunk_size=None asks the FSDP2 load to broadcast every layer at once. The expert-parallel load used to drop it silently and now refuses it as the FSDP2 load's option. The two new rows of test_a_load_refuses_what_it_cannot_load each fail without the change.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (478) and 5.5.4 (432, 35 skipped).
  • Restack and review round, 2026-10-30 (b692242), onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 56353ef on main 059588b, after [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 reworked the load checks:
  • Review round, 2026-10-30 (1692141), onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 75bc2d6:
    • An EP degree left to the plan decision gives way to FSDP2 for the whole model when the dense part, replicated beside a rank's share of the experts, does not fit one GPU, instead of planning a layout the expert-parallel load refuses (1692141). An EP degree the caller asks for still stands and is refused by the load, naming the ways out. test_what_a_rank_holds_with_the_dense_part_replicated_must_fit asserts both outcomes, and its decided case fails without the change.
    • The restack kept [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136's trimmed load_and_prepare_ep docstring and comments, plus this PR's sentence on hub_options.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (515) and 5.5.4 (463, 41 skipped).
  • Restack and review round, 2026-10-30 (093c652), onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 ab06860 on main d464d30:
    • An EP degree the decision chose now also gives way to none when the expert-parallel load would refuse the model, by its plan (the load's own style checks, now one function, _ep_plan_refusal) or by its checkpoint format. Such an MoE is not refused for being left whole either (9cc4589). test_an_moe_the_expert_parallel_load_refuses_is_decided_without_it fails without the change in both of its cases.
    • The refusal of an expert-parallel plan that FSDP2-shards the dense part says that an EP degree left to the decision gives way (381219c).
    • [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's texts written before this load now name it: the auto_class refusal, the no-group refusal and the docstring (093c652).
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (519) and 5.5.4 (464, 44 skipped).
  • Review round, 2026-10-31 (1960174):
    • An EP degree the decision chose now gives way for every reason the config tells why the expert-parallel load would refuse the model: a model spec ruling it out (Llama-4's, GPT-OSS's), a transformers below the load's floor or the spec's, the checkpoint format, or the plan's styles. The its-spec and its-transformers cases fail without the change.
    • Each give-way now says why on rank 0. The CHANGELOG names only the MoEs the expert-parallel load takes as refused when left whole.
    • Pre-commit passes against main. test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (521) and 5.5.4 (464, 46 skipped).
  • Restacked on 2026-10-31 (fac2c56) onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 7747c5d, which moved test_ep_loader.py into test_model_load_utils.py. This PR's one change to that file, the Hub-options checks in test_the_weights_load_in_the_checkpoints_dtype_through_its_class, now sits in the moved section. Its own diff is otherwise unchanged. Pre-commit passes against main. test_model_load_utils.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (521) and 5.5.4 (464, 46 skipped).
  • Review round, 2026-10-31 (3fc02b8):
    • The expert-parallel load's transformers floor is one constant, _EP_MIN_TRANSFORMERS, that the decision and the load both read (6210606).
    • The decision now resolves whether the expert-parallel load refuses an MoE whatever its EP degree, so an EP degree of 1 the caller set no longer meets the left-whole refusal for a model that load refuses (3fc02b8). The give-way test runs each refusal with the degree on auto and with MODELOPT_EP_DEGREE=1, and the set-to-1 cases fail without the change.
    • Pre-commit passes against main. test_model_load_utils.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (525) and 5.5.4 (464, 50 skipped).
  • Review round, 2026-10-31 (3c40b57):
    • load_and_prepare_ep refuses what the plan decision gives way for (_ep_load_refusal), after its own floor and format checks. An EP degree asked for, or a plan handed in, no longer loads a model whose spec rules expert parallelism out. The load test's new GPT-OSS case fails without the change.
    • Pre-commit passes against main. test_model_load_utils.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (526) and 5.5.4 (465, 50 skipped).
  • Restacked on 2026-10-31 (c2d18e6) onto [OMNIML-6060] [2/3] Load an MoE expert-parallel: shard the experts at load time #3136 930bb0f, on the new main e352cac and [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 c78d2e5, with Claude's 04:08Z review round folded in:
    • [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's texts meet the expert-parallel load. Its new init_quantized_weights refusal names the expert-parallel route's own way to a whole model, and a new test covers both a knob and a handed plan; the plain FSDP2 wording fails it. The loading guide [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779 added now describes the expert-parallel route, which this PR's docstring paragraphs moved into (5455a0c). The decided EP degree is no longer forced to 1 in load_pretrained.
    • _ep_load_refusal returns the error itself, so a floor refusal is a RuntimeError whichever floor it trips (eda4c88). A spec's floor above the load's own is tested (608b0b8). Both cases fail without their change.
    • Pre-commit passes against main. test_model_load_utils.py, test_moe_a2a.py and tests/unit/torch/models pass on transformers 5.18 (536) and 5.5.4 (471, 54 skipped).
  • Review round, 2026-10-31 (f7b11fe): the dense-sharding give-way test also leaves the EP degree to the decision, where a caller asking for FSDP2 makes the decided degree give way; it fails if only rank 0's GPU memory may. Pre-commit passes against main. The same CPU tests pass on transformers 5.18 (537) and 5.5.4 (471, 55 skipped).

Before your PR is "Ready for review"

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2741 [3/3] (#2741). Split from #2774, which first routed every load in one PR and whose non-expert-parallel half is now #2779: this half needs the expert-parallel loader, so it lands after it. Applying the model spec in the decision and the load, which this PR carried before the series was reordered, moved to #2746 with the specs.

Summary by CodeRabbit

  • New Features
    • Pretrained MoE models can load with routed experts sharded across ranks under torchrun. Distribution plans also support FSDP2 sharding or loading a full copy on each rank.
  • Bug Fixes
    • Loading now reports an error when a routed-expert model would otherwise remain unsharded.
  • Documentation
    • Updated loading guidance and changelog details for distribution plans.

@copy-pr-bot

copy-pr-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

load_pretrained now supports expert-parallel loading for routed-expert MoE models. Distribution planning rejects plans that leave those models unsharded. Unit and GPU tests cover loader dispatch, plan validation, and expert placement.

Changes: MoE Expert-Parallel Loading

Layer / File(s) Summary
Validate MoE distribution plans
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_resolve_distribution_plan.py
Plan resolution raises when routed experts would remain unsharded. Tests cover non-divisible expert counts, EP degree 1, and FSDP2 sharding as an alternative.
Dispatch expert-parallel loads
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_load_pretrained_distribution.py, CHANGELOG.rst
load_pretrained routes EP plans through load_and_prepare_ep and passes the distribution plan and supported options. Unit tests cover dispatch and refusal cases. The changelog describes EP sharding and plan-resolution settings.
Verify distributed expert sharding
tests/gpu/torch/utils/test_load_pretrained_distribution.py
GPU tests check that EP loading assigns contiguous expert slices to ranks and produces the expected distribution plan.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant load_pretrained
  participant _load_sharded
  participant load_and_prepare_ep
  load_pretrained->>_load_sharded: Pass checkpoint, distribution plan, and load options
  _load_sharded->>load_and_prepare_ep: Load checkpoint using EP plan and supported options
Loading
Merge Risk: Minimal · up to dde99

Expert-parallel plans are routed to the EP loader with their options, and no current behavior defect warrants blocking merge. Add focused coverage for loading with require_lm_head disabled to protect that supported path.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed The pull request changes no security-sensitive loading pattern covered by this check. In the only changed modelopt Python file, trust_remote_code remains caller-configurable with a default of `False…
Title check Passed The title clearly and concisely describes the main change: routing an expert-parallel plan through load_pretrained.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

The pull request changes no security-sensitive loading pattern covered by this check. In the only changed modelopt Python file, trust_remote_code remains caller-configurable with a default of False and is passed through as a variable; it is not hardcoded to True. The diff adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval, exec, or # nosec. No pyproject.toml or requirements.txt dependency changes are present.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2780/

Built to branch gh-pages at 2026-10-27 00:08 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 2f18815 to d68e7ff Compare October 14, 2026 00:14
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch from 68f620d to 5c5cde0 Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6060] [13/21] Load an expert-parallel plan through load_pretrained [OMNIML-6060] [14/22] Load an expert-parallel plan through load_pretrained Oct 14, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from d68e7ff to da76af0 Compare October 14, 2026 01:47
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch from 5c5cde0 to c892568 Compare October 14, 2026 01:47
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from da76af0 to 646405a Compare October 14, 2026 16:15
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch 3 times, most recently from c892568 to 596bb8f Compare October 14, 2026 16:16
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.86%. Comparing base (adc762a) to head (07d4275).

Additional details and impacted files
@@                       Coverage Diff                        @@
##           shengliangx/hfep-6c-fp8-dequant    #2780   +/-   ##
================================================================
  Coverage                            61.86%   61.86%           
================================================================
  Files                                  649      649           
  Lines                                73059    73064    +5     
================================================================
+ Hits                                 45195    45200    +5     
  Misses                               27864    27864           
Flag Coverage Δ
unit 61.86% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu
shengliangxu removed this pull request from stack #2786 October 14, 2026 18:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch from 596bb8f to 8c34d8d Compare October 14, 2026 18:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 646405a to 678d634 Compare October 14, 2026 18:13
@shengliangxu
shengliangxu added this pull request to stack #2802 October 14, 2026 18:14
@shengliangxu shengliangxu changed the title [OMNIML-6060] [14/22] Load an expert-parallel plan through load_pretrained [OMNIML-6060] [13/21] Load an expert-parallel plan through load_pretrained Oct 14, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 8c34d8d

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch from 8c34d8d to d2db2ee Compare October 14, 2026 21:19
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch 2 times, most recently from 22676c9 to df0496c Compare October 14, 2026 21:36
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch 2 times, most recently from 06fdac7 to cd46f22 Compare October 14, 2026 21:46
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test cd46f22

@shengliangxu
shengliangxu removed this pull request from stack #2802 October 15, 2026 02:51
load_and_prepare_ep freezes the model it loads unless told otherwise,
which the FSDP2 and whole-model loads do not. load_pretrained decides
which load runs from the world size and the model, so the same call
returned a frozen model on some runs and not on others. Pass freeze=False
unless the caller asks for freeze, as the other loads return the model.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…acement

With neither a plan nor MODELOPT_EP_DEGREE, the plan decision can shard
an MoE's experts, and the expert-parallel load then refuses options that
place a whole model, such as the device_map hf_ptq passes, as if the
caller had asked for it. Say the layout was decided, and how to keep the
load whole, as the FSDP2 load already does when rank 0's GPU memory chose
to shard: the measured flag _refuse_sharded_load_options takes now also
covers a layout the decision chose for the model's routed experts.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal of a whole-model placement on a decided layout read as
placement every option the FSDP2 load does not take, freeze and
require_lm_head among them. So with neither a plan nor MODELOPT_EP_DEGREE,
load_pretrained(..., freeze=True) or require_lm_head=False, the expert-
parallel load's own options, was refused as if it placed a whole model.
An option either sharded load takes places nothing: _refuse_sharded_load_options,
where the placement a measured layout refuses is now computed, leaves both
loads' options out of it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load now refuses a run in which only some ranks
have a GPU of their own, so no rank is left waiting in its collectives.
The refusal before it still covers a run in which none has one, and names
the way to a whole model, which the load's own check does not.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A sharded load refused an option only the other sharded load takes as
one that places a whole model, and named the way to a whole model on
every rank, where the whole-model load refuses the option again. It now
says which load takes it.

Which options a sharded load counts as passed now follows the one rule
the whole-model load drops them by, _ASKS_NOTHING's: an FSDP2-only option
asks nothing at the FSDP2 load's own default, any other at None. So
broadcast_chunk_size=None, which asks the FSDP2 load to broadcast all of a
source's layers at once and was dropped without a word on the
expert-parallel load, is refused as the FSDP2 load's option there, and
cpu_offload=False, what hf_ptq passes by default, asks nothing of it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal of an option only the other sharded load takes said which
load takes it, but not what to do. It now says to drop the options, or to
pass a plan that takes that load: one with ep_size > 1 for the
expert-parallel load's, and one with ep_size=1 and dense='fsdp' for the
FSDP2 load's, since an expert-parallel plan that FSDP2-shards the dense
part is refused too.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A launch that shows each process one GPU, bound to cuda:0, gives every
rank a GPU of its own whatever its local rank, and the GPU refusal before
the expert-parallel load takes it so through the shared predicate. A case
of the expert-parallel load's routing test, on local rank 1 with one GPU
visible, holds it: under the predicate's older reading, by local rank
alone, that rank is refused.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The backward-breaking entry for a distributed load_pretrained still said
an MoE loads without expert parallelism until the expert-parallel load
lands. This is that load: an MoE has its routed experts sharded across the
ranks, and is refused where it would otherwise load whole on every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the EP degree left to the plan decision, an MoE whose dense part,
replicated beside a rank's share of the routed experts, does not fit one
GPU was planned expert-parallel with the dense part FSDP2-sharded, a
layout the expert-parallel load refuses, as it replicates the dense part.
Such a load, which sharded the whole model with FSDP2 before, now failed.
An EP degree the decision chose now gives way to FSDP2 for the whole
model there; one the caller asked for stands, and the load refuses it
naming the ways out.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With nothing asking for expert parallelism, the plan decision committed
any MoE whose config carries a base_model_ep_plan to the expert-parallel
load, the ones it refuses included: a plan that shards no expert weights
through grouped_gemm, or styles them by one it drops (GPT-OSS's and
Llama-4's below their specs), and a checkpoint format it does not load.
Its refusals named no way back, and some came only after the download.

An EP degree the decision chose now gives way to none there, decided by
the load's own checks, which _ep_plan_refusal now holds for both, and
such an MoE is not refused for being left whole, as it cannot be
expert-parallel here. An EP degree the caller asked for still meets the
load's refusal.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal of an expert-parallel plan that FSDP2-shards the dense part
said a plan left to decide shards it when a rank's share would not fit,
which the decision no longer does: an EP degree it chose gives way to
FSDP2 for the whole model there, and only one the caller asked for meets
this refusal. Say that, keeping the 60% and the ways out.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
meet the expert-parallel load. A launch with no process group that sets
MODELOPT_EP_DEGREE needs only the group, as one that sets
MODELOPT_FSDP_SIZE does. The expert-parallel load builds the
checkpoint's own class, a VLM's image-text class included, and its
refusal of an auto_class names its own way to a whole model. The
docstring and the decision's auto_class refusal say which class each
sharded load builds.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…y so

The plan decision's give-way for a model the expert-parallel load would
refuse read the plan's styles and the checkpoint format, but not a model
spec ruling expert parallelism out (Llama-4's, GPT-OSS's, whose experts
the load refuses once built) nor a transformers below the load's floor or
the spec's: an EP degree it chose still committed those to a refusal,
after the download. _ep_load_refusal now gathers every reason the config
tells, and the decision gives way for any of them.

Each give-way now says why on rank 0, as the plan's other outcomes do:
the load's reason, or for an FSDP2-sharded dense part the knobs that
shard the experts anyway. The changelog names only the MoEs the
expert-parallel load takes as refused when left whole.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The decision's give-way and load_and_prepare_ep each spelled 5.7 out, so
raising the load's floor in one place would leave the decision planning
expert parallelism for a model the load then refuses.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Only an EP degree the decision picked consulted the load's refusal, so an
EP degree of 1 the caller set left an MoE the load refuses (GPT-OSS by its
spec, a quantized checkpoint, a plan sharding no expert weights) to the
left-whole refusal, which offered expert parallelism as the way out.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The load checked a plan's styles alone, so an EP degree asked for, or a
plan handed in, loaded a model whose spec rules expert parallelism out
(GPT-OSS's, Llama-4's) or states a later transformers, which the decision
plans without expert parallelism. The load now asks the same
_ep_load_refusal, after its own floor and format checks, which keep their
messages. The load tests' stand-in configs carry a model type for the spec
lookup, one no spec names.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_ep_load_refusal now returns the error rather than its message: a model
spec ruling expert parallelism out and a checkpoint format the load does
not take are NotImplementedError, a transformers below the floor a spec
states is the RuntimeError require_transformers raised, and a plan's styles
stay a ValueError. The load raises it as it is, so a floor refusal has one
type whichever floor it is below; the plan decision only prints it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The give-way test tripped only the load's own floor. A case with a spec
stating 5.13 on transformers 5.10 decides it by the spec's floor, which a
string comparison would put below 5.7.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
#2779's guide, written while no load sharded the routed experts, said a
plan with ep_size > 1 is refused. It now lists the expert-parallel route,
how the decision picks an EP degree and when it gives way, the way to a
whole model on that route, and what the expert-parallel load builds, takes
and refuses beside the FSDP2 load.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
#2779 refuses a sharded load inside init_quantized_weights, whose
from_pretrained the sharded loads never call. The expert-parallel load
meets that refusal too, naming its own way to a whole model: an EP degree
of 1 as well, for a plan decided from MODELOPT_EP_DEGREE or handed in.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test c2d18e6

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

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

Claude review — LGTM: prior rounds' concerns are resolved and I found no defect in the expert-parallel routing, but two existing tests are removed, so a human signs off.

Baseline: .agents/review-guidelines/code-review.md at base 930bb0f0e1c. Reviewed head c2d18e6c473, full scope (the trigger carried no scoping instructions); all 5 changed files opened, nothing skipped. 351 added source lines, inside the size budget. I ran no tests and report no CI result — findings are from source only.

Needs action:

  • Confirm the two removed tests as a maintainer: test_no_ep_degree_dividing_the_experts_leaves_them_whole and test_an_expert_parallel_plan_is_refused both covered behaviour this PR replaces, and new rows assert the new outcome — justified from the diff, but a bot cannot auto-approve a test removal.
  • Add the MODELOPT_FSDP_SIZE=world + auto-degree row to test_an_ep_capable_moe_may_be_fsdp2_sharded_instead — only the memory-driven half of the new give-way at model_load_utils.py:1654 is pinned (inline).

No action needed:

  • ✔️ Resolved since the last rounds: the EP route's init_quantized_weights refusal now names its own way to a whole model, _ep_load_refusal returns the error so a spec's higher transformers floor raises as a RuntimeError, load_and_prepare_ep meets the refusals the decision gives way for, and the loading guide describes the EP route.
  • Traced clean: _route is reached with an identical plan on every rank at every call site; _refuse_a_rank_without_its_gpu sits inside the collective load checks block; _decide_distribution runs on rank 0 only and broadcast_from ships its new ValueError to every rank — no rank can be stranded in a collective. plan, hub_options, freeze and require_lm_head all have live use paths, and the _SHARDED_LOAD_OPTIONS[route] filter keeps stray None options off load_and_prepare_ep's fixed signature. _ckpt_quant_method reads text_config/llm_config, so a VLM whose language model is quantized is still refused on the EP route. CHANGELOG.rst amends the same unreleased 0.49.0 entries.
  • Two smaller suggestions inline: the module-level get_spec import makes line 802's deferred-import comment stale, and _ep_load_refusal's floor/format branches are dead inside load_and_prepare_ep.

🤖 Generated with Claude Code

Comment on lines +648 to +649
monkeypatch.setenv("MODELOPT_EP_DEGREE", "1")
monkeypatch.setenv("MODELOPT_FSDP_SIZE", "world")

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] The new give-way at model_load_utils.py:1654 fires for two different reasons, and only one of them is pinned.

if chosen and ep_size > 1 and dense == "fsdp" is reached either because the memory heuristic chose "fsdp" (dense_chosen true — covered by test_what_a_rank_holds_with_the_dense_part_replicated_must_fit, whose decided case asserts DistributionPlan(4, dense="fsdp")) or because the caller asked for it with MODELOPT_FSDP_SIZE=world / dense="fsdp" while leaving the EP degree on auto. Nothing covers the second: this test pins the degree to 1 itself, so the auto degree never has to give way, and test_the_env_knobs_override_the_automatic_plan sets the degree explicitly (where it is meant to stand).

That leaves a live mutation gap. Narrowing the condition to chosen and dense_chosen and ... would pass CI, and MODELOPT_FSDP_SIZE=world on an 8-expert MoE over 4 ranks would again decide DistributionPlan(4, 4, "fsdp") — a plan _route refuses outright, so the load would fail where it now succeeds. The else arm of the why message ("is to be sharded") is likewise unexercised.

Cheapest fix is a parametrized row here rather than a new test:

@needs_qwen3_moe_ep_plan
@pytest.mark.parametrize("ep_degree", [None, "1"], ids=["auto", "set-to-1"])
def test_an_ep_capable_moe_may_be_fsdp2_sharded_instead(
    run_as, serve, monkeypatch, tmp_path, ep_degree
):
    """Asked for dense sharding, the MoE is sharded whole by FSDP2, which is not the waste of a
    replica on every rank: an EP degree left to the decision gives way, as the expert-parallel
    load replicates the dense part."""
    if ep_degree is not None:
        monkeypatch.setenv("MODELOPT_EP_DEGREE", ep_degree)
    monkeypatch.setenv("MODELOPT_FSDP_SIZE", "world")
    run_as(2)
    serve(_qwen3_moe(num_experts=8))

    assert resolve_distribution_plan(str(tmp_path))[0] == DistributionPlan(2, dense="fsdp")

The auto row fails if the dense_chosen reason is the only one honored.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken in f7b11fe. The test now runs with the EP degree left to the decision as well as pinned to 1. Narrowing the give-way to chosen and dense_chosen and ... fails the auto case.

from transformers.conversion_mapping import get_model_conversion_mapping
from transformers.core_model_loading import WeightConverter, dot_natural_key, rename_source_key

from modelopt.torch.models.specs import get_spec

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] This top-level import pulls in the whole modelopt.torch.models package, which makes the deliberately deferred import at line 802 — and its comment — stale.

modelopt/torch/models/__init__.py imports every per-model subpackage for its registration side effect, so from modelopt.torch.models.specs import ... executes the parent __init__ and registers every spec at this module's import time. Line 802 then says:

    # Importing modelopt.torch.models registers every model spec; only callers of this need them.
    from modelopt.torch.models.hf import prepare_model_for_loading

which no longer buys anything — the package is already imported by the time load_pretrained runs. Either hoist prepare_model_for_loading to the top beside get_spec and drop the comment, or import get_spec lazily inside _ep_load_refusal to keep the laziness the comment claims. (I checked for a cycle: nothing under modelopt/torch/models/ imports model_load_utils, so this is import cost and a misleading comment, not a breakage.)

# The refusals the plan decision gives way for, which an EP degree asked for, or a plan
# handed in, meets here: a model spec ruling expert parallelism out or stating a later
# transformers, and the plan's styles.
if (refusal := _ep_load_refusal(hf_config, ep_plan)) is not None:

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] Two of _ep_load_refusal's four branches are unreachable from here, so the load now checks the same two things twice.

By the time this line runs, load_and_prepare_ep has already called moe.require_transformers(_EP_MIN_TRANSFORMERS, ...) and refused _ckpt_quant_method(hf_config) not in _EP_SOURCE_FORMATS with its fuller "it loads unquantized checkpoints… Start from an unquantized copy" wording. _ep_load_refusal repeats both: its base-floor check can never fire here (only the spec's higher floor can), and its terse f"The expert-parallel load cannot dequantize a {method!r} checkpoint." is dead in this path while being the message the decision prints.

Not a defect — the decision needs all four — but it is two redundant checks on the load path and two messages for the same refusal. Worth either splitting the spec-only part out for the load, or moving this call above the floor/format checks and deleting the duplicates (accepting the terser format message, or moving the fuller text into _ep_load_refusal so both callers say the same thing).

…ed for

The decided EP degree gives way to FSDP2 for the whole model either when
rank 0's GPU memory chose to shard the dense part or when the caller asked
for it. The test pinned the EP degree to 1 itself, so only the first was
checked; it now also leaves the degree to the decision.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test f7b11fe

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

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

Claude review — LGTM: the one open action item from the last round is now pinned, and I found no defect in the expert-parallel routing; a human signs off only because existing tests are removed.

Baseline: .agents/review-guidelines/code-review.md at base 930bb0f0e1c. Reviewed head f7b11fea, full scope (the trigger carried no scoping instructions); all 5 changed files opened, nothing skipped. 351 added source lines, inside the size budget. I ran no tests and report no CI result — findings are from source only.

Needs action:

  • Confirm the removed/replaced tests as a maintainer: test_no_ep_degree_dividing_the_experts_leaves_them_whole, test_an_expert_parallel_plan_is_refused, test_a_load_it_decides_keeps_the_experts_whole and the ep-knob match in test_what_needs_the_group_is_refused_in_a_launch_that_sets_none_up all pinned behaviour this PR replaces, and new rows assert the new outcome — justified from the diff, but a bot cannot auto-approve a test removal.

No action needed:

  • ✔️ Resolved: test_an_ep_capable_moe_may_be_fsdp2_sharded_instead now parametrizes ep_degree over [None, "1"], so the caller-asked half of the give-way at model_load_utils.py:1654 is pinned alongside the memory-driven half in test_what_a_rank_holds_with_the_dense_part_replicated_must_fit.
  • Traced clean this round: _decide_distribution resolves refusal before the "auto" give-aways and the dense-memory estimate, so the order is self-consistent — and because the left-whole ValueError is gated on refusal is None, a quantized MoE too large to replicate now has a working way out (MODELOPT_FSDP_SIZE=1) rather than the dead end an earlier round flagged. The module-scope from modelopt.torch.models.specs import get_spec imports modelopt.torch.models, which registers every spec, so get_spec is populated even when resolve_distribution_plan is called directly.
  • Two earlier suggestions remain open and stay non-blocking: the deferred-import comment at model_load_utils.py:802 is stale, and _ep_load_refusal's format branch is unreachable inside load_and_prepare_ep.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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.

2 participants