Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modelopt/torch/models/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ A `ModelSpec` holds one attribute per section, each `None` unless the model fill
|---|---|---|
| `moe_spec` (`MoESpec`) | MoE architecture facts only — block classes, expert projection naming, which layout that naming describes | the model is dense |
| `export_spec` (`ExportSpec`) | Per-model data *and policy* of the unified HF export path, e.g. whether grouped expert export is validated for this model | export needs nothing model-specific |
| `ep_spec` (`EPSpec`) | Per-model data of the expert-parallel PTQ path: the `base_model_ep_plan` to attach and the config it goes on, the transformers release it needs, or why the model cannot be expert-parallel yet | the model is dense, or stock transformers already ships all of it |

`None` means "the model has nothing to say about this", which stays distinct from a
filled-in-but-empty section.
Expand Down
5 changes: 5 additions & 0 deletions modelopt/torch/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

from .moe import is_moe
from .specs import (
EPSpec,
ExportSpec,
ModelSpec,
MoESpec,
Expand All @@ -52,6 +53,7 @@
# cannot drift silently: an entry that no longer exists fails at import, and one that is
# no longer used fails lint.
__all__ = [
"EPSpec",
"ExportSpec",
"MoESpec",
"ModelSpec",
Expand Down Expand Up @@ -87,6 +89,9 @@
phimoe,
nemotron,
nemotron_h,
minimax_m3_vl,
llama4_text,
nemotron_h_omni,
qwen2_moe,
qwen3,
qwen3_5_moe,
Expand Down
13 changes: 12 additions & 1 deletion modelopt/torch/models/deepseek_v3/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
``DeepseekMoE`` block of DeepSeek-MoE is a separate spec under ``deepseek``.
"""

from ..specs import ExportSpec, ModelSpec, MoESpec, register
from ..specs import EPSpec, ExportSpec, ModelSpec, MoESpec, register

__all__: list[str] = []

Expand All @@ -37,6 +37,17 @@
model_type="deepseek_v3",
min_transformers_version="5.5",
export_spec=ExportSpec(grouped_expert_export=True),
ep_spec=EPSpec(
# Stock transformers ships no plan for deepseek_v3 before 5.13; from 5.13 the config's
# own plan wins (see apply_distributed_support) and this one goes unused. No gate entry
# on purpose: the router is logits-only and the MoE block computes GLOBAL top-k ids, so
# the experts already receive unmasked global ids -- what the all-to-all dispatch wants.
Comment on lines +41 to +44

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 comment pairs two claims that are in tension and leaves the resolution unstated: "from 5.13 the config's own plan wins and this one goes unused", and (two sentences later) "adding a gate entry would mask them". Stock transformers' EP plans for the DeepSeek family do style the router (ep_router / megamoe_router, whose RouterParallel._prepare_output_fn remaps to local ids and masks non-local — see the TorchA2ARouter override in modelopt/torch/utils/moe.py). So on 5.13+, where the config's own plan is the one that applies, deepseek_v3 gets exactly the gate styling this spec deliberately omits, and the a2a dispatch's precondition ("the router must emit GLOBAL ids unmasked") is the load's problem rather than the spec's.

No change needed to the data — but a reader who takes "goes unused" at face value will conclude the masking hazard cannot arise for deepseek_v3 on recent transformers, which is the opposite of the truth. Worth one clause saying where that is handled (the load rewriting router/expert styles to torch_a2a_*), or dropping "goes unused" in favour of "stock's plan applies instead, and the load re-styles its router entry".

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.

#2748, next in the series, rewrites this comment when it moves the plan into BASE_MODEL_EP_PLAN for the Kimi adapters. Its text already says what you ask: "From 5.13 its own plan wins (an existing plan always does, see apply_distributed_support): the same experts, plus an ep_router entry for the gate, which the load swaps for torch_a2a_router (global ids, unmasked)."

The load does that swap in load_and_prepare_ep ("ep_router": "torch_a2a_router"). Rewording the comment here as well would only conflict with that rewrite, so this text stays as is.

base_model_ep_plan={
"layers.*.mlp.experts.gate_up_proj": "grouped_gemm",
"layers.*.mlp.experts.down_proj": "grouped_gemm",
"layers.*.mlp.experts": "moe_tp_experts",
},
),
moe_spec=MoESpec(
block_names=("DeepseekV3MoE",),
expert_linear_names=("gate_proj", "down_proj", "up_proj"),
Expand Down
9 changes: 8 additions & 1 deletion modelopt/torch/models/gpt_oss/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,21 @@

"""GPT-OSS specs (HF model type ``gpt_oss``)."""

from ..specs import ModelSpec, MoESpec, register
from ..specs import EPSpec, ModelSpec, MoESpec, register

__all__: list[str] = []

register(
ModelSpec(
model_type="gpt_oss",
min_transformers_version="5.5",
ep_spec=EPSpec(
unsupported=(
"GPT-OSS stores its experts transposed, (num_experts, in, out), with gate and up "
"interleaved in gate_up_proj, and the expert-parallel export cannot write that "
"layout."
),
),
moe_spec=MoESpec(
# GPT-OSS fuses gate and up into a single gate_up_proj.
# transformers names the block GptOssMLP; GptOssMoE is kept for the
Expand Down
11 changes: 10 additions & 1 deletion modelopt/torch/models/llama4/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@

"""Llama4 specs (HF model type ``llama4``)."""

from ..specs import ModelSpec, MoESpec, register
from ..specs import EPSpec, ModelSpec, MoESpec, register

__all__: list[str] = []

# transformers ships a base_model_ep_plan for Llama-4, which alone would route it into the

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] modelopt/torch/models/README.md states the convention for values shared between two spec packages: "A value shared between two model types is private and imported explicitly (gemma4_text reuses gemma4's _GEMMA4_MOE_SPEC); sub-model types of one family are an intra-family detail, not a package API."

EP_UNSUPPORTED here (imported by llama4_text/specs.py) and BASE_MODEL_EP_PLAN in nemotron_h/specs.py (imported by nemotron_h_omni/specs.py) are both public spellings of exactly that pattern, so they read as package API while _GEMMA4_MOE_SPEC does not. Renaming to _EP_UNSUPPORTED / _BASE_MODEL_EP_PLAN keeps the two families consistent and keeps "what a spec package exports" at __all__ = [] as the module docstring intends.

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.

Keeping them public, deliberately. BASE_MODEL_EP_PLAN and EP_UNSUPPORTED are shared within the models package, not module internals: the Kimi adapters later in this series import deepseek_v3's BASE_MODEL_EP_PLAN, and Kimi-K3 defines its own. Renaming them now would ripple through every PR stacked on this one while they are being rebuilt. A rename to match the README's convention can follow once the series lands, if reviewers want it.

# expert-parallel path.
EP_UNSUPPORTED = (
"Llama-4's MoE block routes tokens itself and runs every expert densely, rather than "
"dispatching its top-k through the experts interface, and its experts are stored transposed, "
"(num_experts, in, out), which the expert-parallel export cannot write."
)

# Llama4TextExperts is fused: one module holding 3-D gate_up_proj and down_proj
# parameters, run through torch.bmm. There is no (gate, up) pair left to fuse, and the
# grouped-export path (get_experts_list) does not apply. The block also carries a
Expand All @@ -27,6 +35,7 @@
ModelSpec(
model_type="llama4",
min_transformers_version="5.5",
ep_spec=EPSpec(unsupported=EP_UNSUPPORTED),
moe_spec=MoESpec(
block_names=("Llama4TextMoe",),
expert_linear_names=("gate_up_proj", "down_proj"),
Expand Down
18 changes: 18 additions & 0 deletions modelopt/torch/models/llama4_text/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""llama4_text specs (HF model type ``llama4_text``)."""

from . import specs
38 changes: 38 additions & 0 deletions modelopt/torch/models/llama4_text/specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Llama-4 text-only specs (HF model type ``llama4_text``).

The same architecture as ``llama4`` reached through ``Llama4ForCausalLM``, with its own
``base_model_ep_plan`` on the root config, so it is refused expert parallelism the same way.

Only that refusal is registered under this type, deliberately: no MoE or export section, so its
MoE and export lookups find none, as they did before this type had a spec. Giving it ``llama4``'s
MoE section would change how text-only Llama-4 checkpoints match MoE blocks in quantization and
export, which belongs with tests of those paths, not with its expert-parallel refusal.
"""

from ..llama4.specs import EP_UNSUPPORTED
from ..specs import EPSpec, ModelSpec, register

__all__: list[str] = []

register(
ModelSpec(
model_type="llama4_text",
min_transformers_version="5.5",
ep_spec=EPSpec(unsupported=EP_UNSUPPORTED),
Comment on lines +33 to +36

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 registration diverges from the repo's own precedent for exactly this situation, and leaves the registry asserting something false about the architecture.

gemma4_text — the same case, a text-only causal LM reaching the same MoE as its VLM sibling — shares the layout rather than omitting it, and says why:

# gemma4_text/specs.py
"""... The MoE block lives in the text model either way, so the layout is imported from
``gemma4`` rather than restated -- the two must not drift apart."""
moe_spec=_GEMMA4_MOE_SPEC,

Here llama4_text registers ep_spec only, so get_spec("llama4_text").moe_spec is None — which ModelSpec.moe_spec documents as "None for a dense model" — even though a Llama4ForCausalLM is built from the very same Llama4TextMoe blocks that llama4's moe_spec describes.

Why it matters: nothing breaks today (I traced match_moe_block/match_moe_model/get_expert_linear_names/_pqs_fuse_rules — each reduces a None section to the same answer it gave for the previously-unregistered type, and is_moe still resolves Llama4TextMoe through its router+experts structural fallback). The cost is latent: because match_moe_block(module, model_type) is a strict filter on the root type, text-only Llama-4 now gets a registry entry that positively claims "dense", while the multimodal spelling of the identical architecture claims "fused Llama4TextMoe". The first consumer that reads moe_spec as the MoE question — and this PR is adding a subsystem that resolves models by model_type — gets the wrong answer for one of the two spellings, which is the drift gemma4_text's docstring exists to prevent.

Suggested fix: either hoist llama4's MoESpec to a shared private value and reuse it, mirroring _GEMMA4_MOE_SPEC —

# llama4/specs.py
_LLAMA4_MOE_SPEC = MoESpec(
    block_names=("Llama4TextMoe",),
    expert_linear_names=("gate_up_proj", "down_proj"),
    fused_expert_names=True,
)

# llama4_text/specs.py
from ..llama4.specs import _LLAMA4_MOE_SPEC, _EP_UNSUPPORTED
...
        moe_spec=_LLAMA4_MOE_SPEC,
        ep_spec=EPSpec(unsupported=_EP_UNSUPPORTED),

— or, if the omission is deliberate, state it in the module docstring the way nemotron_h_omni/specs.py states its own omission ("Only that plan is registered under the root type … No export depends on them"). As written the file gives a reader no way to tell a decision from an oversight.

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.

Deliberate. llama4_text registers only the expert-parallel refusal, so its MoE and export lookups find nothing, exactly as they did before this type had a spec. You traced the same: no existing path changes.

Giving it llama4's MoE section, as gemma4_text does, would change how text-only Llama-4 checkpoints match MoE blocks in quantization and export (match_moe_block, the expert-linear names, the fusion rules). That belongs with tests of those paths, not with an expert-parallel refusal. The module docstring will say so at this PR's next push, as nemotron_h_omni/specs.py states its own omission.

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 docstring note is in 3b43cc6. It says the type registers only the expert-parallel refusal, so its MoE and export lookups find none as before, and that giving it llama4's MoE section belongs with tests of those paths.

)
)
18 changes: 18 additions & 0 deletions modelopt/torch/models/minimax_m3_vl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""minimax_m3_vl specs (HF model type ``minimax_m3_vl``)."""

from . import specs
33 changes: 33 additions & 0 deletions modelopt/torch/models/minimax_m3_vl/specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""MiniMax-M3 specs (HF model type ``minimax_m3_vl``).

transformers ships everything M3 needs to run expert-parallel: its MoE block calls the experts with
the router's top-k, its experts dispatch through the experts interface, and its text config carries a
``base_model_ep_plan``. The generic all-to-all transport takes over unchanged.
"""

from ..specs import EPSpec, ModelSpec, register

__all__: list[str] = []

register(
ModelSpec(
model_type="minimax_m3_vl",
min_transformers_version="5.13",
ep_spec=EPSpec(requires_transformers="5.13"),
)
)
13 changes: 12 additions & 1 deletion modelopt/torch/models/nemotron_h/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,26 @@

"""Nemotron-H specs (HF model type ``nemotron_h``)."""

from ..specs import ExportSpec, ModelSpec, MoESpec, register
from ..specs import EPSpec, ExportSpec, ModelSpec, MoESpec, register

__all__: list[str] = []

# Ungated experts, so ``up_proj``/``down_proj`` rather than a gate/up pair. The gate emits logits and
# the MoE block runs its own group-limited top-k, so the plan has no router entry and the experts
# receive global expert ids -- which the expert-parallel all-to-all experts forward routes on. The
# loader maps ``nemotron_experts_ep``, a name no transformers style carries, to that forward.
BASE_MODEL_EP_PLAN = {
"layers.*.mixer.experts.up_proj": "grouped_gemm",
"layers.*.mixer.experts.down_proj": "grouped_gemm",
"layers.*.mixer.experts": "nemotron_experts_ep",
}

register(
ModelSpec(
model_type="nemotron_h",
min_transformers_version="5.5",
export_spec=ExportSpec(grouped_expert_export=True),
ep_spec=EPSpec(base_model_ep_plan=BASE_MODEL_EP_PLAN),
moe_spec=MoESpec(
# NemotronHMOE experts (NemotronHMLP) use up_proj and down_proj only (no gate).
block_names=("NemotronHMOE",),
Expand Down
19 changes: 19 additions & 0 deletions modelopt/torch/models/nemotron_h_omni/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""NemotronH-Omni specs (HF model type ``nemotron_h_omni``)."""

from . import specs
51 changes: 51 additions & 0 deletions modelopt/torch/models/nemotron_h_omni/specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""NemotronH-Omni specs (HF model type ``nemotron_h_omni``).

The Omni VLM's language sub-model is a native NemotronH, so its MoE facts are NemotronH's.
What differs is where the expert-parallel plan has to be attached: on the nested language-model
config, not the VLM root. transformers aggregates each sub-model's plan up to the root WITH its
module prefix, so a plan there surfaces as ``language_model.<base>.layers.*`` and matches the real
parameter paths -- while a plan on the top config reaches no expert at all.

Only that plan is registered under the root type, so lookups of the MoE and export sections by
``nemotron_h_omni`` find neither. No export depends on them: on transformers 5 the language
model's experts are one fused container, which the export writes through its own handler, and
every step those lookups gate leaves a fused container as it is.
"""

from ..nemotron_h.specs import BASE_MODEL_EP_PLAN
from ..specs import EPSpec, ModelSpec, register

__all__: list[str] = []

register(
ModelSpec(
model_type="nemotron_h_omni",
# transformers ships the Omni VLM itself from 5.18. The checkpoint's own code
# (``trust_remote_code=True``) names the language-model config ``llm_config``, and
# transformers' port ``text_config``: the plan goes on whichever the config has, tried in
# the order the load reads them.
min_transformers_version="5.18",

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] min_transformers_version="5.18" sits awkwardly with what this spec is for. Per its own declaration, the field is the "earliest transformers release whose definitions match this spec" — and this spec's content is the EP plan plus plan_config_attr, which llama4-style globs aside is explicitly meant to serve the pre-5.18, remote-code checkpoint too (that is why llm_config is listed at all). As written, the spec says "nothing here applies before 5.18" while simultaneously carrying the attribute name only the pre-5.18 spelling uses.

Nothing enforces the field at runtime today, so there is no bug. But the deferred follow-up named in the PR description — extending test_specs_vs_transformers.py to check version claims for specs without a moe_spec — will have to reason about this one, and the natural check (_modeling_module("nemotron_h_omni") is not None from 5.18 on) will pass while quietly disagreeing with the llm_config entry. Worth one clause in the comment stating which half of the spec 5.18 describes (the transformers-shipped model) and that the plan itself applies to the remote-code checkpoint on older releases, so the next reader does not "fix" the version or drop llm_config.

Relatedly: EPSpec.requires_transformers is None here, i.e. "no EP-specific floor", which is the right value if the remote-code path works below 5.18 — that reading is only obvious once the above is written down.

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 comment above min_transformers_version already says which half 5.18 describes: "transformers ships the Omni VLM itself from 5.18. The checkpoint's own code (trust_remote_code=True) names the language-model config llm_config, and transformers' port text_config."

The plan applies to either spelling. requires_transformers is left unset, so the expert-parallel load's own 5.7 floor is what applies to the remote-code checkpoint. A later check of the version claims has that comment to go by, so I am leaving this as is.

ep_spec=EPSpec(
# Its own copy: the registry is process-wide, and a spec holding NemotronH's dict would
# carry any change to one plan into the other.
base_model_ep_plan=dict(BASE_MODEL_EP_PLAN),
plan_config_attr=("text_config", "llm_config"),
),
)
)
7 changes: 6 additions & 1 deletion modelopt/torch/models/qwen3_moe/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@

"""Qwen3-MoE specs (HF model type ``qwen3_moe``)."""

from ..specs import ExportSpec, ModelSpec, MoESpec, register
from ..specs import EPSpec, ExportSpec, ModelSpec, MoESpec, register

__all__: list[str] = []

register(
ModelSpec(
model_type="qwen3_moe",
min_transformers_version="5.5",
ep_spec=EPSpec(
# No plan here: qwen3_moe's own base_model_ep_plan ships in transformers, and it is
# the architecture the all-to-all path was built around. Its plan lands in 5.7.
requires_transformers="5.7",
),
moe_spec=MoESpec(
block_names=("Qwen3MoeSparseMoeBlock",),
expert_linear_names=("gate_proj", "down_proj", "up_proj"),
Expand Down
Loading
Loading