Skip to content

[OMNIML-6076] [13/21] Describe expert parallelism in the model specs - #2746

Open
shengliangxu wants to merge 29 commits into
shengliangx/hfep-9b-ep-load-routingfrom
shengliangx/hfep-9-ep-model-specs
Open

shengliangxu wants to merge 29 commits into
shengliangx/hfep-9b-ep-load-routingfrom
shengliangx/hfep-9-ep-model-specs

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: New feature

Before it can shard a model's experts, the expert-parallel path needs three facts about the model:

  • which modules are the routed experts, and how they shard;
  • which transformers release first ships that information;
  • for a VLM, which nested config the plan must be attached to.

All three are facts about the architecture, so they belong with the model's other facts in modelopt.torch.models.

  • EPSpec, a new ModelSpec section, and apply_distributed_support, the one applier that reads it. Most models become data in the spec package they already had:
    • deepseek_v3 carries the plan stock transformers ships only from 5.13;
    • qwen3_moe and minimax_m3_vl carry only the transformers release their own plan needs.
    • An existing plan on the config always wins, since overwriting it would substitute our idea of the layout for the model's.
  • One thing stays code: EPSpec.prepare, the escape hatch for checkpoints that ship their own modeling code ([18/21] to [20/21]). A spec whose plan only prepare attaches states that plan too (EPSpec.prepared_ep_plan), so it can be read without running prepare, which patches the model's code. Nemotron-H needs none. Its gate emits logits and the block runs its own top-k, so the experts receive global expert ids, which the all-to-all experts forward ([1/21]) routes on directly.
  • Nemotron-H-Omni is registered separately. Its experts live in the language sub-model, and transformers aggregates a sub-model's plan up to the root with its module prefix, so a plan on the VLM root would match nothing, silently. A test pins the placement and that the two spellings share one plan. transformers ships the Omni model itself from 5.18, which its spec records. The plan goes on the language-model config, which the checkpoint's own code names llm_config and transformers' 5.18 port text_config; the spec lists both, text_config first as the load reads them, and the first one a config has takes the plan. On a tiny port built on CPU, the plan on text_config names its NemotronHExperts parameters; the port has not been run expert-parallel on GPUs. Its root type registers only the expert-parallel plan, so is_moe by the root type is False for its NemotronHMoE blocks. No export depends on the missing MoE and export sections: on transformers 5 its experts are one fused container, exported by its own handler (checked locally, see Testing).
  • A plan goes where the load reads it. apply_distributed_support refuses a spec whose plan_config_attr is neither text_config nor llm_config, the two nested configs the expert-parallel load reads a plan and the expert count from (one module-level tuple), instead of attaching a plan that would be read as none and plan the MoE as dense. plan_config_attr may list several spellings, the first one the config has taking the plan; each must be one the load reads, and a config with none of them is refused, naming them all. The spellings are checked for every spec, whether the spec or its prepare supplies the plan, before anything is attached or prepared.
  • EPSpec.unsupported refuses a model whose own plan would route it into the expert-parallel path before that path can handle it. The refusal comes before anything is loaded and names a single process first, and FSDP2 (--use_fsdp2 or MODELOPT_FSDP_SIZE=world) only for an unquantized causal LM: GPT-OSS ships MXFP4, which the FSDP2 load refuses, and Llama-4's checkpoint is multimodal. A spec's transformers-floor refusal says the same.
    • GPT-OSS and Llama-4 store their experts transposed, which the fused-experts export cannot write (GPT-OSS also interleaves gate and up).
    • Llama-4's MoE block also routes tokens itself rather than through the experts interface.
  • The decision and the load apply the spec. [12/21] carried this before the series was reordered.
    • 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; without it, DeepSeek-V3 before 5.13 would show no experts to shard. Every rank applies it under 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. Neither the spec's plan nor the load's torch_a2a swap of it reaches an exported config.json: this PR adds no export path, and the expert-parallel export ([17/21]) writes each config's base_model_ep_plan as the source config.json declares it (none for DeepSeek-V3 before 5.13, Nemotron-H or Nemotron-H-Omni).
    • A load handed that config does not prepare it again, since a model's preparation can patch its modeling code; a load that decides for itself prepares it once.
    • A handed config 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, as deciding does.
    • An EP degree left to auto resolves to 1 for a model whose spec refuses expert parallelism or needs a later transformers than the one installed (one predicate, _spec_refuses_ep, asks check_distributed_support), so its spec is skipped and 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.
    • At an EP degree of 1, which skips the spec, the check that refuses an MoE held whole on every rank ([12/21]) still finds a spec-supplied plan's experts. It reads the plan the spec would supply, or the one it states prepare attaches, without applying anything. DeepSeek-V3 at MODELOPT_EP_DEGREE=1, replicated, is refused like a native-plan MoE instead of loading whole on every rank, and the refusal reports the expert count found through the spec's plan, which an EP degree has to divide.
    • For an MoE whose spec refuses expert parallelism (GPT-OSS, Llama-4), that refusal offers the ways out its sibling for an expert-parallel-capable MoE offers besides sharding the experts, from one shared text: FSDP2 (--use_fsdp2 in hf_ptq, or MODELOPT_FSDP_SIZE=world) if the checkpoint is an unquantized causal LM, a single process, or a replicated plan handed in (DistributionPlan(world_size, 1, 'replicate')).
    • 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. The gate reads the EP knob through the decision's own _ep_degree, so a malformed knob fails every rank before any spec runs.

Usage

from modelopt.torch.utils.moe import apply_distributed_support

hf_config = apply_distributed_support(hf_config.model_type, hf_config, ckpt_path=path)
# raises NotImplementedError for gpt_oss / llama4 / llama4_text, before any weight loads

Testing

  • tests/unit/torch/models/test_ep_specs.py:
    • every registered plan is well formed, and every EP section declares something;
    • the Omni placement and plan sharing, the plan landing under either name (llm_config or text_config), and on text_config for a config with both;
    • the applier's version floor, plan attachment, existing-plan precedence and prepare ordering;
    • a spec attaching its plan where the load reads none refused with the config untouched, whether the spec or its prepare supplies the plan;
    • the refusals: GPT-OSS, Llama-4 and Llama-4-text are refused with their configs untouched, naming a single process and FSDP2 only for an unquantized causal LM, and a refusal comes before the rest of the section.
  • tests/unit/torch/utils/test_resolve_distribution_plan.py and test_load_pretrained_distribution.py gain the spec's cases, which [12/21] carried before the reorder:
    • 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;
    • on two real gloo ranks, a rank that cannot apply the spec failing every rank;
    • no spec for a run that rules expert parallelism out, by argument or by MODELOPT_EP_DEGREE=1, so GPT-OSS's spec does not refuse an FSDP2 run;
    • auto picks no EP degree for GPT-OSS: MODELOPT_FSDP_SIZE=world alone plans FSDP2, and an explicit ep_size=2 meets the spec's refusal;
    • a Qwen3-MoE whose spec floor is unmet plans FSDP2 under auto and is refused at an explicit ep_size=2; the decision test of an MoE without a plan runs on transformers 5.5 again;
    • on two real gloo ranks (cpu_dist_workers), every rank applying the spec, or not, as rank 0 decided, whatever its own knob says;
    • DeepSeek-V3 refused when left whole on every rank, at an explicit EP degree of 1 too, as a native-plan MoE is, and allowed FSDP2 instead; GPT-OSS left whole refused as an MoE that cannot be expert-parallel yet, by the decision rather than by its spec, naming a single process and FSDP2 only for an unquantized causal LM; the left-whole refusal names the expert count, DeepSeek-V3's at an EP degree of 1 included;
    • the plan a spec would supply, read without running prepare: the spec's own plan, or the one it states prepare attaches, and nothing for a model with no spec or EP section, or whose spec refuses expert parallelism, and a spec below its transformers floor keeping its plan;
    • a decided expert-parallel load handed the spec-prepared config, and the config prepared once, not at all when handed, and on a copy when passed as config= with a handed plan;
    • GPT-OSS refused with its config handed in, without preparing it.
  • Mutation-checked: 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, ignoring the plan a spec states for prepare, letting auto pick an EP degree for a model whose spec refuses expert parallelism or below a spec's floor, leaving the spec plan's experts out of the refusal's count, attaching a plan where the load reads none, dropping the single process or FSDP2's unquantized-causal-LM condition from the left-whole and spec refusals, reading the Omni plan off llm_config alone, trying llm_config before text_config, checking a plan spelling only when the spec supplies the plan itself, hiding the plan of a spec below its transformers floor, and dropping the EP-unsupported branch each fail a test.
  • Tests the applier and decision tests already cover are dropped, and the refusal test names its three models rather than looping over whichever specs refuse. Each drop is checked by mutation.
  • Checked locally, not as a unit test: an FP8, INT4-AWQ or W4A8-AWQ export of a tiny NemotronH writes byte-identical tensors with nemotron_h's MoE and export sections and without them (transformers 5.18 and 5.5.4 on torch 2.14, 5.18 on torch 2.8).
  • 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 as the expert-parallel load does are written by the expert-parallel export with no base_model_ep_plan, as their sources have none (transformers 5.18 on torch 2.14 and 2.8, and 5.5.4); a bare save_pretrained of the same configs writes the swapped plan.
  • Locally on this revision, the stack rebased on main 0638a27: pre-commit over the whole PR against main, at every PR of the stack; each fix's own tests at its PR; and at the stack's tip the unit tests of the changed areas (tests/unit/torch/utils, export, models, the fused-experts quantization tests and the hf_ptq example tests) on transformers 5.18 and 5.5 and on torch 2.8. They pass apart from the diffusers-export and GGML tests, which fail the same way on main in that environment. CI runs the full unit suite.
  • The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.
  • On GPUs (1 node, EP4): gpt-oss-20b under torchrun stopped at the plan decision with the spec's refusal message, before any weights load. That run predates auto picking no EP degree for GPT-OSS: left to auto, a replicated run now stops at the decision's refusal ("cannot be quantized expert-parallel yet, and the plan replicates it ..."), still before any weights load; this was not re-run on GPU. MiniMax-M3 runs on stock transformers 5.13 code (see [21/21]).

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive.
  • 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 [21/21].
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 13 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after [12/21]. The branch's history adds a Llama-4 and a MiniMax-M3 adapter and then removes them. The Llama-4 adapter's routing was numerically wrong (it scaled expert outputs where Llama-4 scales inputs), and the M3 one only repeated what transformers 5.13 ships. Review the net diff.

Summary by CodeRabbit

  • New Features
    • Added expert-parallel configuration support for DeepSeek-V3, Nemotron-H, Nemotron-H-Omni, and Qwen3-MoE.
    • Added model registrations for MiniMax-M3-VL and Llama-4 Text.
    • Model loading now applies model-specific expert-parallel settings, including nested configurations, when selecting distribution plans.
  • Compatibility
    • GPT-OSS and Llama-4 variants are identified as unsupported for expert-parallel loading.
    • Expert-parallel support requires minimum Transformers versions for applicable models.

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

Priority: Normal

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 6
Check name Status Explanation
Title check Passed The title names the main change: adding expert-parallel (EP) descriptions to the model specs. The raw summary confirms this. It adds EPSpec, per-model EP plans, Transformers version floors, and unsu…
Docstring Coverage Passed Docstring coverage is 82.93% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 41 functions across 18 files.
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 …
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Title check

Explanation

The title names the main change: adding expert-parallel (EP) descriptions to the model specs. The raw summary confirms this. It adds EPSpec, per-model EP plans, Transformers version floors, and unsupported-model refusals to the specs, and the PR objectives match this scope. The bracketed ticket and stack-position prefix are noise but do not make the title misleading. The title is specific and accurate, so it passes.

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: 💡 1
🛠️ 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

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@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-25 01:02 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.
✅ Project coverage is 61.75%. Comparing base (e2a88f1) to head (e2db64b).

Additional details and impacted files
@@                           Coverage Diff                           @@
##           shengliangx/hfep-9b-ep-load-routing    #2746      +/-   ##
=======================================================================
+ Coverage                                61.70%   61.75%   +0.04%     
=======================================================================
  Files                                      649      655       +6     
  Lines                                    72892    72990      +98     
=======================================================================
+ Hits                                     44976    45072      +96     
- Misses                                   27916    27918       +2     
Flag Coverage Δ
unit 61.75% <100.00%> (+0.04%) ⬆️

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-9-ep-model-specs branch from fdd217a to 0a3e056 Compare October 13, 2026 00:53
@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-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 DP=EP path needs three things from a model before it can shard its
experts: which modules the routed experts are and how they shard, which
transformers release first ships that information, and -- for a VLM -- which
nested config the plan has to be attached to. All three are facts about the
architecture, so they belong with the model's other facts.

So this adds an EPSpec section to ModelSpec, and apply_distributed_support,
the one applier that reads it. Most models become data in the spec package
they already had: deepseek_v3 carries the plan stock transformers does not
ship, while qwen3_moe and gpt_oss carry only the release their own plan needs.
An existing plan on the config always wins, since overwriting it would
substitute our idea of the layout for the model's.

Two things resisted being data and kept code, deliberately. NemotronH needs a
custom parallel style, because its gate emits logits only and the
group-limited top-k happens inside the MoE block -- so the usual ep_router has
nothing to remap and the remap has to move to the experts' input. transformers
resolves styles through a global registry rather than from config, so that
registration is code, in nemotron_h/ep.py. And EPSpec.prepare is the escape
hatch for checkpoints carrying their own modeling code.

NemotronH-Omni is registered separately because the difference is real: its
experts live in the language sub-model, and transformers aggregates a
sub-model's plan up to the root WITH its module prefix -- so a plan on
llm_config matches the actual parameter paths while a plan on the VLM root
matches nothing at all, which fails as silence rather than as an error. A test
pins both the placement and that the two spellings share one plan.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
keep_compressed asked a model's prepare hook to keep its experts packed, for
a frozen speculative-decoding target. PTQ always needs the decompressed
weights and hf_ptq never set it, so the knob and its pass-through go. prepare
now receives ckpt_path and trust_remote_code only, which the test pins.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The applier they exercise enforces each model's transformers floor, which
imports transformers. The sibling model tests skip without it the same way.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers ships a base_model_ep_plan for gpt_oss, so a torchrun run took
the expert-parallel path and failed only at quantization, minutes into the
load. The loader drops GptOssExperts' own quantized wrapper so that the
generic grouped-GEMM one takes over, and that one refuses experts stored
transposed: the fused-experts export cannot write them. It would split
gate and up on the wrong axis, and GPT-OSS also interleaves them.

Add EPSpec.unsupported. apply_distributed_support refuses a model that
names a reason, before anything is applied or loaded, and points at FSDP2.
Mark gpt_oss with it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Per-model expert-parallel support in the same structure as the model specs:
one package per HF model type under modelopt/torch/models, data in specs.py, and
code in ep.py only where the model genuinely needs it. These two need code.

Llama-4 is the one MoE that does not follow the transformers sparse-dispatch
convention -- its block routes inline and computes every expert densely, so
there is no router-to-experts seam for the transport to wrap; ep.py adapts it to
the ExpertsInterface contract instead. llama4_text reuses llama4's adapter and
differs only in where the plan lives -- the config itself rather than a nested
text config, since there is no VLM wrapper.

MiniMax-M3 already follows the convention, with fused [E, out, in] expert
params, but hardcodes a per-expert loop in its experts forward, so it only needs
its experts class routed through the ExpertsInterface.

The declarative half is covered by the spec tests. The prepare hooks patch real
transformers classes, so they are exercised by the PTQ runs rather than by unit
tests.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The Llama-4 adapter rewrote Llama4TextMoe.forward to dispatch its top-k
through the experts interface, but it scaled each expert's output by the
router score where Llama-4 scales the input, and SwiGLU does not commute
with that: on a tiny config the outputs differ by up to 6.9 on values near
11. Its experts are also stored transposed, which the expert-parallel export
cannot write, so the run failed at quantization anyway. Drop the adapter and
refuse llama4 and llama4_text the way gpt_oss is refused: transformers ships
a plan for both, which would otherwise route them into the expert-parallel
path.

Every transformers release with minimax_m3_vl already decorates
MiniMaxM3VLExperts exactly as the M3 adapter did and ships the text
config's base_model_ep_plan, and the loader selects the all-to-all experts
forward after loading, so the adapter changed nothing at runtime. Keep only
its transformers floor.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
nemotron_h/ep.py registered a NemotronHExpertsEP parallel style for the
plan's nemotron_experts_ep entry, but load_and_prepare_ep maps that entry to
torch_a2a_experts before loading, so the style never ran. Registering it
imported transformers' all_reduce_forward, which 5.18 no longer has, so
applying Nemotron-H's spec failed on CI's latest transformers; its _prepare_*
hooks are not called from 5.16 on either.

The plan moves into the spec that both spellings of the architecture share,
with no prepare hook.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
transformers 5.18 added nemotron_h_omni, so its spec can no longer claim remote code;
test_specs_vs_transformers fails on the latest transformers until it names the release.
Expert parallelism still needs the checkpoint's code, whose llm_config the plan targets.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
For several models the spec is what supplies the expert-parallel plan: DeepSeek-V3 ships none of
its own, so deciding without the spec would read its MoE as dense. Unless the run rules expert
parallelism out (a single process, or an EP degree of 1), the model's spec is applied to the config
before rank 0 builds the model to decide, and the returned config carries it. A load prepares that
config once, and not at all when handed the one resolve_distribution_plan returned, since a model's
preparation can patch its modeling code. A run that rules expert parallelism out applies no spec,
so a model whose spec refuses expert parallelism, like GPT-OSS, still loads FSDP2-sharded.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each rank read its own MODELOPT_EP_DEGREE to decide whether to apply
the model spec, though the knobs are rank 0's. A rank whose environment
ruled expert parallelism out skipped the spec rank 0 applied, loaded
without the expert-parallel plan the others shard by, and raised once
the mesh was up, leaving them waiting. Rank 0 decides it and sends it,
as it does the plan.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An EP degree of 1 skips the model's spec, so the guard against leaving an
expert-parallel MoE whole on every rank found routed experts only through
the config's own base_model_ep_plan. DeepSeek-V3, whose plan comes only
from its spec, therefore loaded whole on every rank at MODELOPT_EP_DEGREE=1
with a replicated dense part, where Qwen3-MoE, which carries its plan,
refused. The guard now also looks for the experts through the plan the
spec would supply. The spec itself stays skipped: it refuses models that
cannot be expert-parallel, which an EP degree of 1 is how to load.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
At an EP degree of 1 the guard against holding an expert-parallel MoE
whole on every rank falls back on the plan the model's spec would supply,
since that degree skips the spec. It read only the spec's
base_model_ep_plan, so a remote-code model whose prepare attaches the
plan, after patching the checkpoint's modeling, showed no routed experts
and loaded whole on every rank. Running prepare there is not an option:
it patches the model's code, for a run that is not expert-parallel.

EPSpec.prepared_ep_plan states the plan prepare attaches, and the plan
the guard reads includes it. prepare still attaches the plan itself; the
spec only states it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The refusal of an MoE held whole on every rank told every model to raise
MODELOPT_EP_DEGREE above 1. GPT-OSS and Llama-4 carry plans of their own,
so they reach that refusal too, but their specs refuse expert
parallelism: following the advice would only have hit that refusal next.
For them the error now says they cannot be quantized expert-parallel yet
and points at FSDP2 alone, as their specs' own refusal does. Both
messages name the ways to ask for FSDP2: --use_fsdp2 in hf_ptq, or
MODELOPT_FSDP_SIZE=world.

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

The test played rank 1 against a stand-in broadcast that answered with
rank 0's decision. The pooled job that checks every rank takes rank 0's
plan now covers the spec too: rank 1's own EP knob says the opposite of
rank 0's, and rank 1 must apply DeepSeek-V3's spec exactly when rank 0
did, over the real broadcast, or it would load without the plan the
others shard by.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
test_the_plan_a_spec_would_supply imported modelopt.torch.models and
modelopt.torch.utils.moe inside the test, for no circular-import or
optional-dependency reason: the module's transformers guard already
precedes its other modelopt imports.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- test_some_model_declares_expert_parallel_support goes: with no spec
  registered, the named-model applier and decision tests fail first.
- test_a_model_type_with_no_spec_is_left_alone goes: every multi-rank
  dense decision test runs the same early return.
- test_the_plan_is_attached_when_the_config_has_none goes:
  test_the_spec_is_applied_before_the_decision attaches the real
  deepseek_v3 plan.
- The refusal test is parametrized over gpt_oss, llama4 and llama4_text by
  name rather than over whichever specs refuse, so a lost refusal fails it
  instead of shrinking it. It replaces
  test_models_whose_own_plan_would_misroute_them_are_refused.
- The two prepare probes are one test.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
modelopt.torch.models exports EPSpec, but specs.py, where it is defined,
left it out of its __all__.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The expert-parallel route of a sharded load handed a plan but no config
read the config without token, cache_dir or local_files_only, as the
FSDP2 route did before. It now reads it with the Hub options the load was
given.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
A config passed for the load is the caller's own. With the plan
decided by the load, resolve_distribution_plan prepares a copy of it
through the model spec. With a plan handed in, the expert-parallel load
took the config raw, so the spec's preparation never ran: DeepSeek-V3
failed for want of the expert-parallel plan the spec adds, and a model
the spec refuses was not refused. The load now prepares a copy, once,
as deciding does.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The config resolve_distribution_plan prepared comes back to the load
with its plan, and the load does not prepare it again: preparing can
patch modeling code. It skipped the spec's checks with the preparation,
so a hand-made plan and config loaded GPT-OSS or Llama-4 expert-parallel
although their specs refuse it. check_distributed_support now runs the
spec's refusal and transformers floor on their own, and
apply_distributed_support starts with it.

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

A multi-rank run that leaves the EP degree to auto checks the model's spec before its plan is
decided, so a model whose expert parallelism needs a newer transformers (Qwen3-MoE below 5.7)
stops there. Like the refusal of a model that cannot be expert-parallel yet, the error now says it
can also be quantized without expert parallelism.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…very rank on a spec error

resolve_distribution_plan applies the model's spec to a copy of the config and returns that copy
only when the plan it decides is expert-parallel. Any other plan returns the config as read: a
decision that lands on FSDP2, as when no EP degree divides the experts, would otherwise build the
model from a config carrying a base_model_ep_plan its checkpoint never had, and the export would
write it into config.json. Every rank applies the spec under a collective, so a rank that cannot
fails every rank instead of leaving the others waiting in the plan's broadcast.

A config missing the sub-model config its spec targets is told how the checkpoint's own code names
it. The nemotron_h_omni spec says it registers only the expert-parallel plan for now.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the EP degree left to auto, MODELOPT_FSDP_SIZE=world still applied GPT-OSS's or Llama-4's
spec, whose refusal stopped the run before the plan was decided, although the run asked only
for FSDP2; and the refusal of such an MoE left whole on every rank names exactly that knob. Auto
now resolves to an EP degree of 1 for a model whose spec refuses expert parallelism, so the
spec is skipped and FSDP2 shards the model whole; only an EP degree asked for explicitly meets
the spec's refusal. The spec gate reads the EP knob through the decision's own _ep_degree, so
the two cannot disagree, and a malformed knob fails every rank before any spec runs.

At an EP degree of 1, the refusal of an MoE whose plan only its spec supplies reported 0
experts, since the count was read only when the config's own plan found experts. The experts
found through the spec's plan now count too, so DeepSeek-V3 at MODELOPT_EP_DEGREE=1 is told the
expert count an EP degree has to divide.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An MoE whose spec refuses expert parallelism and whose plan replicates it on every rank was
pointed at FSDP2 alone, unconditionally. The FSDP2 load refuses a quantized checkpoint, which
GPT-OSS's MXFP4 one is, so that refusal now offers what its sibling for an expert-parallel-capable
MoE offers beside sharding the experts: FSDP2 if the checkpoint is unquantized, a single process,
or a replicated plan handed in. The two messages share that text.

apply_distributed_support attached a VLM's plan to whatever config attribute its spec named, but
the load reads a nested plan, and the expert count, only from text_config and llm_config: a plan
put anywhere else was read as none and the MoE planned as a dense model. A spec naming another
attribute is now refused, and _ep_config and _num_experts_from_config read the same tuple.

Docs: the models README lists the ep_spec section, EPSpec.plan_config_attr names the two
attributes it can take, and nemotron_h_omni's docstring says why no export needs the MoE and
export sections its root type lacks: its language model's experts are one fused container, which
the export writes through its own handler whatever the root type.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The spec's refusal of expert parallelism, and of a transformers below its floor, offered FSDP2 as
the way around it, and the decision's refusals offered FSDP2 for any unquantized checkpoint. The
FSDP2 load builds a causal LM and reads its tensors as stored, so neither GPT-OSS's MXFP4
checkpoint nor the multimodal Llama-4 can take it. The spec's refusals now name a single process
first and FSDP2 only for an unquantized causal LM, and the decision's say the same.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
An EP degree left to auto resolved to 1 only for a spec that refuses expert parallelism outright.
A model below its spec's transformers floor (Qwen3-MoE before 5.7, MiniMax-M3 before 5.13) still
had its spec applied before the plan was decided, so a multi-rank FSDP2 or replicated run, which
never asked for expert parallelism, was refused by the floor, whose remedy then offered the FSDP2
it had asked for. Auto now resolves to 1 wherever check_distributed_support refuses the model
(_spec_refuses_ep), so only an explicit EP degree or a handed expert-parallel plan meets it.

EPSpec.plan_config_attr also takes several spellings, the first one the config has taking the plan.
NemotronH-Omni lists llm_config (the checkpoint's own code) and text_config (transformers' 5.18
port): the port's config has no llm_config, so every multi-rank run of it was refused before its
plan was decided, an FSDP2 or replicated one included.

check_distributed_support's remedy names MODELOPT_FSDP_SIZE=world beside --use_fsdp2, as the
decision's refusal does, for a caller outside hf_ptq.

Tests: a Qwen3-MoE whose spec floor is unmet plans FSDP2 under auto and is refused at an explicit
EP degree; the Omni plan lands on either spelling; the MoE-without-a-plan decision test runs on
transformers 5.5 again.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
apply_distributed_support refused a plan_config_attr the expert-parallel load does not read only
for a spec that attaches a base_model_ep_plan itself. It now checks every spec's spellings before
anything is attached or prepare runs, so a spec whose prepare supplies the plan meets the same
refusal.

nemotron_h_omni lists text_config before llm_config, the order _ep_config and
_num_experts_from_config read them, so a config carrying both takes the plan on the config its
expert count is read from.

_spec_ep_plan's docstring now says what it does: only a spec refusing expert parallelism outright
reports no plan; a model below its spec's transformers floor keeps its plan, so the decision still
refuses holding its experts whole on every rank. Its test pins that case.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test e2db64b

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.

1 participant