Skip to content

[OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel - #2746

Open
shengliangxu wants to merge 27 commits into
shengliangx/hfep-base-9-specsfrom
shengliangx/hfep-9-ep-model-specs
Open

shengliangxu wants to merge 27 commits into
shengliangx/hfep-base-9-specsfrom
shengliangx/hfep-9-ep-model-specs

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Slice [3/3] of this PR's split (2026-10-30), so the model-spec work is reviewed and merged in pieces under the 500-line budget:

Base: shengliangx/hfep-base-9-specs, a branch (not a PR) that joins #2780 and #3135, the two PRs this one builds on. The diff below is this PR's alone. It merges after #2780 and #3135, then sits on main.

Re-cut: split from the former whole PR (head 2489583, pinned). Also adapted to #2779's collective load checks; see the end of Testing.

Series layout

What does this PR do?

Type of change: New feature

The plan decision and the expert-parallel load apply the model's spec (#3134), through the applier (#3135). Without them, an MoE that only its spec makes expert-parallel, such as DeepSeek-V3 before transformers 5.13, is decided as dense.

  • Deciding. Unless the run rules expert parallelism out (a single process, or an EP degree of 1), resolve_distribution_plan applies the model's spec to a copy of the config before rank 0 builds the model on meta.
    • Every rank applies it inside a collective, so a rank that cannot fails every rank.
    • An expert-parallel plan returns the prepared copy. Any other plan returns the config as read, which the export would otherwise write the spec's plan into.
    • Rank 0's knobs decide whether the spec applies, and rank 0 sends that decision as it does the plan: a rank whose own environment skipped the spec would load without the expert-parallel plan the others shard by, and fail once the mesh is up.
  • Auto and the floors. An EP degree left to auto resolves to 1 for a model whose spec refuses expert parallelism, or that needs a later transformers than the one installed: its spec's release or the expert-parallel load's 5.7 floor, whichever is later.
    • The load's floor holds for a model with no spec too (_spec_refuses_ep).
    • Only an explicit EP degree above 1, or a handed expert-parallel plan, meets the refusal: MODELOPT_FSDP_SIZE=world alone plans FSDP2 for GPT-OSS, and for Qwen3-MoE on transformers 5.5.
  • The left-whole refusal ([OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780) sees spec plans. At an EP degree of 1, which skips the spec, it still finds a spec-supplied plan's experts, through the plan the spec would supply (or the one it states prepare attaches), without applying anything.
    • For an MoE whose spec refuses expert parallelism (GPT-OSS, Llama-4), it offers every way out but sharding the experts, from one shared text.
    • Below the floor, it offers an upgrade instead of an EP degree.
  • Loading. The expert-parallel route applies the spec as it checks the load's arguments, inside [OMNIML-6054] [2/2] Route load_pretrained by the distribution plan #2779's collective "load checks" block, so a rank that cannot apply it fails every rank instead of leaving the others in the load.
    • A load handed the config that deciding prepared does not prepare it again, since a model's preparation can patch its modeling code. It still meets its spec's refusal and transformers floor (check_distributed_support).
    • A config= passed for the load with a plan handed in is the caller's own, so an expert-parallel load prepares a copy of it, once.
  • Why the spec's prepare runs before deciding here, and where it goes in [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748. The decision builds the model on the meta device and finds and counts its routed experts through the plan. For a model whose plan only its spec supplies, the spec must therefore be applied first. This PR applies the whole spec there, prepare included, and documents the interim contract on EPSpec.prepare: it must leave a whole-model or FSDP2 load as it would be without it, and may run more than once. No spec in this PR sets prepare.
    • [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748 adds the first specs that set it (Kimi-VL, Kimi-K2.5) and replaces the contract. The decision runs only a pure-import step (EPSpec.make_importable) and attaches the spec's stated plan (prepared_ep_plan) with no prepare run. prepare then runs only once the plan is decided expert-parallel: in a collective step after the decision, or in the load after its own checks for a plan handed in.
    • That puts prepare where models/hf.py's prepare_for_loading hooks already run: on the route that loads the model it prepares, never on one that does not. The hooks are scoped to the load as a context; prepare only runs on that route.

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("deepseek-ai/DeepSeek-V3")  # its spec supplies the plan; experts sharded over 8 ranks

Testing

  • tests/unit/torch/utils/test_model_load_utils.py (the decision's and the loads' tests) gains the spec's cases:

    • the spec applied before the decision, DeepSeek-V3's plan coming only from its spec, and the returned config carrying it for an expert-parallel plan only;
    • no spec for a run that rules expert parallelism out, by argument or by MODELOPT_EP_DEGREE=1;
    • auto picks no EP degree for GPT-OSS, nor below a spec's floor or the load's (DeepSeek-V3 on transformers 5.6.2); an explicit ep_size=2 meets the refusal;
    • on two real gloo ranks, every rank applying the spec, or not, as rank 0 decided, whatever its own knob says;
    • on two real gloo ranks, a rank that cannot apply the spec fails every rank, when deciding and, new here, when loading a handed expert-parallel plan;
    • the left-whole refusal for spec plans at an EP degree of 1, for GPT-OSS, and below the floor;
    • a decided expert-parallel load gets the spec-prepared config, prepared once, not at all when handed, and on a copy when passed as config= with a handed plan; GPT-OSS is refused with its config handed in, without preparing it.
  • Mutation-checked (earlier rounds): skipping the spec or applying it without expert parallelism, ignoring the EP knob, letting each rank decide the spec for itself, preparing a handed config twice or not at all, ignoring the spec's plan at an EP degree of 1, letting auto pick an EP degree for a model whose spec refuses expert parallelism or below a floor, and offering an EP degree below the floor each fail a test.

  • Checked locally, at the stack's tip: DeepSeek-V3, Nemotron-H and Nemotron-H-Omni configs prepared by their specs and swapped to the torch_a2a styles are written by the expert-parallel export with no base_model_ep_plan, as their sources have none. There the export writer itself puts back the source config.json's values ([OMNIML-6084] Export an expert-parallel model through the no-gather writer #2753). The loaded model's own config holds no spec plan only since b51123a (below).

  • On GPUs (1 node, EP4, an earlier revision): gpt-oss-20b under torchrun stopped at the plan decision, before any weights load.

  • 2026-10-30, split and re-parented:

    Pre-commit, mypy included, passes against main. CPU tests (test_model_load_utils.py, test_ep_loader.py, test_moe_a2a.py, tests/unit/torch/models; torch 2.14):

transformers Passed Skipped Failed
5.18 455 0 0
5.5.4 399 56 0
  • Rebased on 2026-10-30 (5d51cd6) onto main 99c3834 through the rebuilt hfep-base-9-specs ([OMNIML-6060] Load an expert-parallel plan through load_pretrained #2780 1bec6da + [OMNIML-6076] [2/3] Prepare a model's config for expert parallelism from its spec #3135 fe16583); no conflicts, this PR's own diff unchanged. 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 (467) and 5.5.4 (411, 56 skipped).
  • Review round, 2026-10-30 (d1dbf75):
    • The applier is imported at module scope, and the tests that stand in for it patch it where the load looks it up. Each fails if it patches moe instead.
    • The refusal of an MoE left whole on every rank offers FSDP2 only where the FSDP2 load supports the checkpoint, which excludes an MTP head it cannot build.
    • Pre-commit passes against main. The same CPU tests pass on transformers 5.18 (467) and 5.5.4 (411, 56 skipped).
    • Not run at that head: a multi-GPU load of a model whose plan only its spec supplies (added in the next round).
  • Review round, 2026-10-30 (ae518d6):
    • _EP_MIN_TRANSFORMERS comes in with the module-scope import from moe.
    • The GPU test test_an_expert_parallel_moe_loads_with_its_experts_sharded in tests/gpu/torch/utils/test_model_load_utils.py runs for two models. its-own-plan is Qwen3-MoE. its-spec-plan is DeepSeek-V3 saved with no plan of its own, so the plan comes only from its spec. Both run the real load_pretrained with the plan left to decide, and check each rank's expert slices and its own batch's logits against the whole model. The test is gated on 2 GPUs and transformers >= 5.7.
    • CPU, transformers 5.18: the saved DeepSeek-V3 checkpoint reads back with no plan. The decision, with its collectives stood in for 2 ranks, returns DistributionPlan(2, ep_size=2) with the spec's plan. The whole model builds and runs.
    • 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 (467) and 5.5.4 (411, 56 skipped).
    • On GPUs, 2026-10-30, at ae518d6: the four load_pretrained tests in tests/gpu/torch/utils/test_model_load_utils.py passed: both expert-parallel rows (its-spec-plan, its-own-plan) and the two dense tests. NVIDIA GB200, torch 2.11.0a0, transformers 5.9.0, reruns disabled:
GPUs Passed Failed Time
2 4 0 23 s
4 4 0 26 s

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive within the unreleased series.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: The whole PR was approved on 2026-10-28; re-review requested for this slice.

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2780 and #3135. The former whole PR's history (Llama-4 and MiniMax-M3 adapters added and then removed, review rounds) is pinned with its last head; the slices carry the net change.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 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 12, 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

The change adds expert-parallel specifications for model plans, version requirements, and unsupported cases. It applies these specifications to Hugging Face configs during distributed plan resolution and expert-parallel loading.

Changes: Expert-parallel support

Layer / File(s) Summary
Expert-parallel contracts and model registrations
modelopt/torch/models/specs.py, modelopt/torch/models/__init__.py, modelopt/torch/models/*/specs.py, modelopt/torch/models/llama4_text/*, modelopt/torch/models/minimax_m3_vl/*, modelopt/torch/models/nemotron_h_omni/*
Adds EPSpec to ModelSpec and registers expert-parallel plans, Transformers requirements, or unsupported declarations for DeepSeek-V3, GPT-OSS, Llama-4, Llama-4 text, MiniMax-M3-VL, Nemotron-H, Nemotron-H-Omni, and Qwen3-MoE.
Apply expert-parallel specifications
modelopt/torch/utils/moe.py, tests/unit/torch/models/test_ep_specs.py
Adds helpers to retrieve and apply model specifications. The application checks model support and version requirements, attaches plans when absent, and runs optional preparation callbacks. Tests cover these behaviors and error cases.
Use specifications in distributed loading
modelopt/torch/utils/plugins/model_load_utils.py, tests/unit/torch/utils/test_load_pretrained_distribution.py, tests/unit/torch/utils/test_resolve_distribution_plan.py
Applies model specifications during plan resolution and prepares configs for expert-parallel loading when needed. Tests cover plan selection, config forwarding, and rank consistency.

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Resolver as resolve_distribution_plan
  participant Ranks as Distributed ranks
  participant Support as apply_distributed_support
  participant Registry as Model spec registry
  Resolver->>Ranks: Broadcast whether to apply distributed support
  Resolver->>Support: Apply model spec to config
  Support->>Registry: Retrieve model expert-parallel spec
  Support-->>Resolver: Return prepared config
  Resolver->>Ranks: Resolve distribution plan
Loading
Merge Risk: Low · up to 3aaff

Default multi-GPU Qwen3-MoE loads can fail on Transformers 5.5 or 5.6. Users can set the EP degree to one, but the automatic planning regression should be fixed before merging.

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 PASS. The PR adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval, exec, or # nosec usage. New Transformers loading paths propagate trust_remote_code from …
Title check Passed The title clearly describes the main change: applying model specifications during expert-parallel decision and loading.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

PASS. The PR adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval, exec, or # nosec usage. New Transformers loading paths propagate trust_remote_code from the caller; the new API defaults it to False. The only added trust_remote_code=True instances are test arguments or documentation text, not hardcoded model/tokenizer loading. No dependency files changed.

Finishing Touches
Fix failing CI checks
  • Commit to this branch
  • Create a new PR
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

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-8-fused-experts branch from 6e6ac9c to bad2de9 Compare October 12, 2026 02:35
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 2142693 to 2e134ec Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 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-2746/

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

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 2e134ec

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (shengliangx/hfep-9b-ep-load-routing@07d4275). Learn more about missing BASE report.

Additional details and impacted files
@@                          Coverage Diff                           @@
##             shengliangx/hfep-9b-ep-load-routing    #2746   +/-   ##
======================================================================
  Coverage                                       ?   61.91%           
======================================================================
  Files                                          ?      655           
  Lines                                          ?    73164           
  Branches                                       ?        0           
======================================================================
  Hits                                           ?    45301           
  Misses                                         ?    27863           
  Partials                                       ?        0           
Flag Coverage Δ
unit 61.91% <100.00%> (?)

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 force-pushed the shengliangx/hfep-9-ep-model-specs branch from 2e134ec to fdd217a Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test fdd217a

@shengliangxu shengliangxu changed the title [11/19] Describe expert parallelism in the model specs [OMNIML-6076] [11/19] Describe expert parallelism in the model specs Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-8-fused-experts branch from 16e5c17 to 5c0d5fa Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from fdd217a to 0a3e056 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-8-fused-experts branch from 5c0d5fa to 698f459 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 0a3e056 to 67f1c8f Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 67f1c8f

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6076] [11/19] Describe expert parallelism in the model specs [OMNIML-6076] [11/20] Describe expert parallelism in the model specs Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-8-fused-experts branch from 698f459 to eaa41c2 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 67f1c8f to 2f18815 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6076] [11/20] Describe expert parallelism in the model specs [OMNIML-6076] [12/21] Describe expert parallelism in the model specs Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 2f18815

@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 added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6076] [12/21] Describe expert parallelism in the model specs [OMNIML-6076] [13/22] Describe expert parallelism in the model specs 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
The decision and the load imported apply_distributed_support and
check_distributed_support inside the functions that call them, with no
circular import or optional dependency to justify it: model_load_utils
already imports from moe at module scope. The tests that stand in for
the applier now patch it where the load looks it up.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The ways out of an MoE left whole on every rank offered FSDP2 for any
unquantized causal LM, but both FSDP2 loads refuse a Nemotron-H
checkpoint carrying MTP weights. Name that as well.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The GPU test of an expert-parallel load_pretrained covered Qwen3-MoE,
whose config ships its own plan. It now also loads DeepSeek-V3 saved
with no plan of its own, so the plan comes from its model spec, as on a
transformers before 5.13. For both, each rank's batch runs through the
sharded model and must give the whole model's logits, so the test checks
the all-to-all forward as well as each rank's expert slices.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
load_and_prepare_ep put the config the model keeps back to the plan the
checkpoint declares, which it inferred from the plan being an attribute
of the config it was handed. Since the decision and the load apply the
model spec, a plan the spec attached is such an attribute too, so the
kept config held ModelOpt's plan and an export would write it into
config.json: for Nemotron-H a style only ModelOpt registers, and so for
DeepSeek-V3 below transformers 5.13 or with a plan pinned to null.

The load now reads the declared plan from the checkpoint's config.json,
where the expert-parallel config sits in it, and the kept config gets
exactly that, or no plan of its own.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load reads the plan the checkpoint declares from its
config.json. For a handed config whose plan sits on a nested config, a
config.json that is no JSON object made the nested lookup raise on that
rank alone, outside the load's collective refusal, so the other ranks
went on into the load and waited for it.

A file a rank cannot read as a JSON object now declares no plan, and the
read runs inside the expert-parallel load's refusal block, so anything
else it raises reaches every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A config.json the expert-parallel load cannot read, or that is no JSON
object, declares no plan to keep, so the loaded model keeps none and an
export writes none, whatever the checkpoint declared. That loss is now
a warning instead of silence. The call-site comment says the read no
longer raises on the file, and why it stays in the collective block.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Left whole on every rank, a model whose spec marks expert parallelism
unsupported was refused with a RuntimeError, while the load refuses the
same spec with check_distributed_support's NotImplementedError. Raise
the same type on both, which is still a RuntimeError.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The warning for a config.json the load cannot read went through
warn_rank_0, so a rank other than 0 failing alone dropped the plan from
its model's config with no warning anywhere. Each rank now warns for
itself and names its rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, an MoE left whole
on every rank was refused with "Upgrade transformers" and no release,
though the spec's own refusal names it. Both now read the release from
one place, moe._ep_floor: the expert-parallel load's own, or the spec's
when that is later.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, an MoE left whole
on every rank was told to upgrade transformers to shard its experts,
even when no EP degree divides both its experts and the world size, or
its config names no count: there an upgrade shards nothing, and the
next run meets the refusal that names the real obstacle. Offer the
upgrade only where a degree exists, and let the other cases name what
stands in the way.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Below the release a model's expert parallelism needs, the left-whole
refusal tells the caller to upgrade transformers to shard the experts.
With the EP degree set to 1 by the caller, an upgrade alone leaves it at
1, and the next run meets the refusal again. Name raising the degree as
well there, with the knob the caller set it by.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The left-whole refusal test checked for a ValueError by the word
"divid" in the expected pattern, which the pattern for an EP degree to
raise beside an upgrade contains too, though that refusal stays a
RuntimeError. Check by the pattern of the decision's own refusal.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With no ranks to shard over, the plan holds the whole model whatever
the model is, but the EP degree argument was still parsed with the model
type, which consults the model's spec and its transformers floor for a
result that cannot change. Parse it without.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
resolve_distribution_plan applied the model's spec whenever the world and
the EP degree left room for expert parallelism, whatever class the load
builds the model with. A class the sharded loads refuse is planned whole on
every rank, or refused, so it is never expert-parallel, yet every rank ran
the spec's prepare first: a prepare that raises failed a load that loaded
the whole model on every rank before, and one that patches the process did
so for a load that never uses it. Skip the spec for such a class, as the
decision does.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With an expert-parallel plan handed in, the expert-parallel load prepares
its copy of a config= passed for the load once its own checks pass, the
rank's-own-GPU refusal among them, so a rank that cannot load runs no
spec prepare, which can import the checkpoint's code. A row of
test_a_sharded_load_refuses_what_it_cannot_honor holds it: its spec
applier fails if a rank without a GPU prepares anything.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel load keeps on the loaded model's config the plan the
checkpoint's config.json declares, read from the file. A plan set on a
config passed for the load, and not declared there, is not kept, so an
export writes out what the checkpoint declares; the docstring said only
the spec's and the config class's plans were left out.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
check_distributed_support and the refusal of an MoE left whole on every
rank both offered FSDP2 with the same caveat, what the FSDP2 load takes,
in two wordings that each round had to keep in step. Both now take it
from one helper in moe, with the knob spelled as the caller set it.

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

The plan decision applies the model's spec, prepare included, before it
knows the plan, wherever the plan could still be expert-parallel, so a
run whose auto EP degree comes out at 1 has run prepare too. That is now
the documented contract of EPSpec.prepare: it must leave a whole-model or
FSDP2 load as it would be without it, and may run more than once.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_sharded_load_kwargs took an hf_config as prepared by the model's spec
unless told otherwise, so a caller that left prepared out would skip the
spec for a config that needs it: DeepSeek-V3 below transformers 5.13
would load sharding nothing. Left out, it is now taken as not prepared;
preparing again is harmless by EPSpec.prepare's contract, skipping it is
not.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision now gives an EP degree it chose way to FSDP2 for the
whole model when the dense part is FSDP2-sharded, so the routing test's
case for the refusal of an expert-parallel plan that FSDP2-shards the
dense part asks for the EP degree, as a caller meeting that refusal has.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision's refusal of an MoE its spec rules out read the spec's
unsupported text to decide, then left it out, so a run that set nothing,
the one most users reach as auto resolves to 1 for these models, learned
only that the model cannot be expert-parallel. It now says why, as the
load's own refusal of the same spec does.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the model spec applied, an EP degree left to auto resolves to 1 for
an MoE below the transformers floor its spec states, and the plan
decision refuses to leave it whole, naming the upgrade that would shard
it, before the load's give-way could plan it whole. The give-way test's
below-the-floor case expects that refusal here.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision asks the model spec (_spec_refuses_ep) before the
expert-parallel load's refusal: auto resolves to an EP degree of 1 for a
model whose spec refuses expert parallelism or needs a later transformers,
and the left-whole refusal says why. The load refusal's own spec and floor
checks could no longer change a plan, so they are gone, with the floor they
spelled out beside _ep_floor's.

The give-way test's model-spec case patched a spec lookup the decision no
longer reads; GPT-OSS's left-whole case covers that refusal, now with the
MXFP4 checkpoint format its releases have, which the load refuses as well.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
and floor checks this PR leaves to the spec-aware decision. A plan handed
to the load reaches it without that decision, so the load asks the spec
itself, as load_pretrained's expert-parallel route does, and refuses with
the same NotImplementedError or RuntimeError.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers writes config.json as UTF-8. Read in the locale's encoding,
a non-ASCII byte under a non-UTF-8 locale raised UnicodeDecodeError, which
the plan-provenance read took for an unreadable file and dropped the plan
the checkpoint declares.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The load asked the spec only past its format check and its no-plan guard,
so a direct call for a model whose plan only its spec supplies, or whose
MXFP4 checkpoint the load refuses as well (GPT-OSS's), reported those
instead of the spec's reason, whose remedy is the one that loads. It now
asks the spec as soon as it has the config, as load_pretrained does.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test d8059b3

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

"spec",
lambda: (
world_size > 1
and auto_class in (None, AutoModelForCausalLM)

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 gate rules the spec out for an auto_class the sharded loads refuse, but not for _init_quantized_weights(), which _decide_distribution treats identically one line apart:

if (
    (auto_class not in (None, AutoModelForCausalLM) or _init_quantized_weights())
    and ep_size in ("auto", 1)
    and dense in ("auto", "replicate")
):
    # The sharded loads refuse this class and init_quantized_weights: load it whole, unbuilt.
    return DistributionPlan(world_size)

So on a multi-rank run with init_quantized_weights active and the EP degree left to auto, the gate is true (auto_class is None, _ep_degree(...) is "auto"), every rank applies the spec — prepare included — and the decision then returns the whole-model plan anyway. The comment just above states the rationale for excluding such a route: "the decision then plans the whole model on every rank, or refuses, without the spec, whose prepare could fail, or patch the process, for a load that never shards." That reasoning covers _init_quantized_weights() too, and today the cost is a config deepcopy plus an extra collective on a path that cannot shard; once #2748's specs set prepare, it becomes a process-level patch applied for a load that never uses it.

No spec in this PR sets prepare, and the returned config is still the unprepared one for a non-EP plan, so nothing is wrong at this head — but the gate reads as if it enumerates "expert parallelism is ruled out" exhaustively and misses one case the decision itself handles. Suggest making the two agree:

        lambda: (
            world_size > 1
            and auto_class in (None, AutoModelForCausalLM)
            and not _init_quantized_weights()
            and _ep_degree(ep_size, hf_config.model_type) != 1
        ),

If instead the asymmetry is deliberate — init_quantized_weights only forces the whole-model plan while dense resolves to "auto"/"replicate", whereas the auto_class refusal is unconditional — a half-line in the comment saying so would stop the next reader re-deriving it.

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.

The asymmetry is deliberate, and the suggested gate would regress the one case it leaves out.

A foreign auto_class rules the spec out whatever the knobs, because the decision then either loads the model whole or refuses the class, without building it. init_quantized_weights forces the whole-model plan only while nothing asks to shard.

With MODELOPT_EP_DEGREE=2 (or MODELOPT_FSDP_SIZE=world) inside the context, the decision still builds the model. For an MoE whose plan only its spec supplies (DeepSeek-V3 before transformers 5.13), it can find the experts only through the spec.

I checked this on CPU: a 2-rank stand-in of that model, MODELOPT_EP_DEGREE=2, inside init_quantized_weights(mtq.INT8_DEFAULT_CFG).

  • With the gate as it is, the load refuses with the in-context message: The expert-parallel load builds the model without from_pretrained, so it would skip init_quantized_weights' quantization; ....
  • With not _init_quantized_weights() added, the decision raises MODELOPT_EP_DEGREE=2 must divide the routed experts deepseek_v3 shards (0). instead.

You're right that with the EP degree left to auto, the spec is applied for a load that ends whole. That costs a config copy, one collective, and prepare. The EPSpec.prepare contract already requires prepare to be safe for a load that ends dense, and an auto EP degree that gives way runs it the same way.

Ruling the spec out inside the context only where the decision itself goes whole would avoid it: the EP degree auto, and the dense layout auto or replicate. I can make that change, with the half-line of comment you suggest, if you think it's worth it here.

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

Approving: the spec now reaches the decision and the expert-parallel load on every rank through collectives, and the one new commit's reordering is correct.

No action needed:

  • ✔️ Prior rounds' concerns stay closed: _declared_ep_plan still reads the checkpoint's config.json (so a spec-attached or class-default plan cannot reach an export), its read sits inside the EP load's collective refusal block, and _ep_floor/_fsdp2_way_out remain single-sourced in moe.py.
  • The new commit's check_distributed_support in load_and_prepare_ep sits inside the collective checks block, ahead of the format check and the no-plan guard, so a spec-refused MoE reports its own reason instead of "declare base_model_ep_plan". Dropping those checks from _ep_load_refusal is safe: _ep_degree returns 1 for a spec-refusing model, so auto never needs the give-way, and the new left-whole arms raise with matching types.
  • One non-blocking SUGGESTION inline (model_load_utils.py:1903): the spec gate excludes a refused auto_class but not _init_quantized_weights().
  • Baseline: .agents/review-guidelines/code-review.md at base c4cc146. Reviewed head d8059b3; full scope, all 6 changed files opened (+236 added source lines, under budget). No tests or CI run by me; no injection attempts in the diff.

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.

3 participants