Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion modelopt/torch/export/moe_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def _export_fused_experts(
"""Split fused MoE expert weights and export per-expert quantization scales.

Works with any module wrapped by ``_QuantFusedExperts`` (gated, with a fused
``gate_up_proj``) or ``_QuantNonGatedFusedExperts`` (non-gated, with a single
``gate_up_proj``) or by a non-gated subclass (``_is_gated = False``, with a single
``up_proj`` — e.g. NemotronH). Both store their projections as 3-D
``nn.Parameter`` tensors with per-expert quantizer ``nn.ModuleList`` s.

Expand Down
30 changes: 29 additions & 1 deletion modelopt/torch/models/nemotron_h/modeling_ptq.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,41 @@
"""Nemotron-H PTQ modeling (HF model type ``nemotron_h``)."""

import torch.nn as nn
from transformers.models.nemotron_h.modeling_nemotron_h import NemotronHExperts

from modelopt.torch.quantization.plugins.huggingface import _is_supported_hf_model
from modelopt.torch.quantization.nn import QuantModuleRegistry
from modelopt.torch.quantization.plugins.huggingface import (
_is_supported_hf_model,
_QuantFusedExperts,
)
from modelopt.torch.quantization.utils.layerwise_calib import LayerActivationCollector

__all__: list[str] = []


class _QuantNemotronHExperts(_QuantFusedExperts):

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 (non-blocking): the explicit registration skips the is_transposed and shape checks that register_fused_experts_on_the_fly applied, and the docstring says the wrapper relies on transformers' layout. A shape assert can't tell a consistently transposed pair, as you noted. But getattr(module, "is_transposed", False) needs no shape inference. Could setup raise when it is true, so a future layout flip fails loudly instead of slicing the experts wrong?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken in 0da0591. _QuantNemotronHExperts._setup now raises when transformers marks the experts is_transposed, before any quantizer is created, so a future layout flip fails at conversion instead of slicing every expert wrong. The docstring says so. test_experts_marked_transposed_are_refused converts a real NemotronHExperts marked transposed and checks the conversion raises. It fails without the change.

"""Quantized ``NemotronHExperts``: non-gated fused experts, ``down_proj(act_fn(up_proj(x)))``.

The container holds 3-D ``up_proj`` and ``down_proj`` parameters instead of the gated
``gate_up_proj``, and its forward runs ``up_proj`` then ``down_proj`` once per expert on the
eager path, or once per expert-sorted batch on ``grouped_mm``. So only the first projection's
name differs from :class:`_QuantFusedExperts`: its per-expert weight quantizers and shared input
quantizer live under ``up_proj_*``. Registered explicitly, these experts skip the
transposed-layout check of :func:`register_fused_experts_on_the_fly`: the wrapper relies on
transformers storing their weights ``(num_experts, out_dim, in_dim)``, as every supported
release does, and :class:`_QuantFusedExperts` refuses experts transformers marks
``is_transposed``.
"""

_first_proj_attr = "up_proj"
# Read by the export: a non-gated up_proj is exported whole, not split into gate|up halves.
_is_gated = False

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 explicit registration bypasses the layout guard the on-the-fly path applies, with nothing left to catch a silent mis-export if the upstream layout ever flips.

register_fused_experts_on_the_fly refuses a container whose first projection is stored transposed (huggingface.py:1531-1548: is_transposed, or getattr(module, wrapper._first_proj_attr).shape[-1] != module.down_proj.shape[1]) precisely because the generic machinery this wrapper reuses assumes (num_experts, out_dim, in_dim) — per-channel weight quantizers take axis=0 on each slice, and _export_fused_experts slices accordingly. Before this PR, Nemotron-H's experts went through that guard. Registered directly, they no longer do, and _QuantFusedExperts._setup asserts nothing about the layout.

Today that is safe: transformers' NemotronHExperts stores up_proj as (E, intermediate, hidden) and the pin is transformers>=5.5,<5.19. The risk is an upstream release that transposes it (as GptOssExperts already does) — calibration would collect per-input-channel amax and the export would write transposed per-expert weights, with no warning and no error, producing a checkpoint that loads and generates garbage.

One cheap line turns that into a loud failure at conversion:

    def _setup(self):
        # The generic fused-experts machinery this reuses -- axis-0 per-expert weight quantizers
        # and the per-expert export split -- assumes (num_experts, out_dim, in_dim). The explicit
        # registration skips register_fused_experts_on_the_fly's transposed-layout guard.
        assert self.up_proj.shape[-1] == self.down_proj.shape[1], (
            f"NemotronHExperts weights are not stored (num_experts, out_dim, in_dim): "
            f"up_proj {tuple(self.up_proj.shape)}, down_proj {tuple(self.down_proj.shape)}."
        )
        super()._setup()

Non-blocking, and reasonable to skip: _QuantFP8Experts is registered the same way and carries no such check either, so adding it here alone is slightly asymmetric. If you'd rather keep the diff minimal, a sentence in the class docstring recording that the layout assumption is now unguarded would also do.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Taken in a772386 as the docstring line you offer: _QuantNemotronHExperts's docstring says the explicit registration skips register_fused_experts_on_the_fly's transposed-layout check and that the wrapper relies on transformers storing these weights (num_experts, out_dim, in_dim), as every supported release (5.5 to 5.18) does. No assert: no release stores them otherwise, and _QuantFP8Experts, registered the same way, carries none either.

Comment on lines +45 to +47

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 explicit registration trades a loud failure for a silent one if the upstream layout ever changes.

register_fused_experts_on_the_fly refuses a container whose first projection is stored transposed and warns, precisely because the fused-experts export then slices the wrong axis and "writes silently wrong weights" (huggingface.py:1611-1624). Registering NemotronHExperts here bypasses that check — as the docstring states — and the non-gated export path has no split to catch it either: _export_fused_experts passes first_weight through whole (moe_utils.py:208-214), so a (num_experts, in_dim, out_dim) up_proj would export with transposed per-expert weights and an axis-0 weight scale computed over the input dim. Nothing fails; the checkpoint is just wrong at inference.

Since _setup already runs per module, a two-line invariant makes the assumption enforced rather than documented:

    def _setup(self):
        # Registered explicitly, so `register_fused_experts_on_the_fly`'s transposed-layout
        # guard never runs: pin the `(num_experts, out_dim, in_dim)` layout the export assumes.
        assert self.up_proj.shape[-1] == self.down_proj.shape[1], (
            f"NemotronHExperts weights are not stored (num_experts, out_dim, in_dim): "
            f"up_proj {tuple(self.up_proj.shape)}, down_proj {tuple(self.down_proj.shape)}"
        )
        super()._setup()

Same shape relation the generic guard uses, so it stays consistent with 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.

Not taken. For non-gated experts the proposed relation cannot tell the stored layout from a transposed one: up_proj (E, hidden, inter) with down_proj (E, inter, hidden) satisfies up_proj.shape[-1] == down_proj.shape[1] just as the stored pair does. What pins the layout is test_export_writes_up_and_down_proj_per_expert, which builds transformers' own NemotronHExperts with hidden 32 and intermediate 16 and asserts the exported per-expert shapes (16, 32) and (32, 16), so a release that stores them transposed fails it when the pin is raised.



if NemotronHExperts not in QuantModuleRegistry:
QuantModuleRegistry.register({NemotronHExperts: "hf.NemotronHExperts"})(_QuantNemotronHExperts)


def is_nemotron_h_model(model: nn.Module) -> bool:
return get_nemotron_h_decoder_layers(model) is not None

Expand Down
83 changes: 34 additions & 49 deletions modelopt/torch/quantization/plugins/huggingface.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,15 +824,14 @@ class _QuantFusedExperts(_QuantFunctionalMixin):
kernel) backends are not intercepted; ``normalize_experts_impl_on_the_fly``
redirects them to ``grouped_mm`` before calibration.

The non-gated variant (``up_proj`` instead of ``gate_up_proj``, used by
NemotronH) is handled by :class:`_QuantNonGatedFusedExperts` via the
A non-gated layout (a single ``up_proj``) is handled by a subclass that overrides the
``_first_proj_attr`` / ``_is_gated`` hooks below; each layout names the
first-projection quantizers after its backing parameter.
"""

# Name of the 3-D weight parameter feeding the first ``F.linear`` per expert.
# Gated experts fuse gate+up into ``gate_up_proj``; non-gated experts use a
# single ``up_proj`` (see _QuantNonGatedFusedExperts).
# single ``up_proj``, which their subclass names here.
_first_proj_attr = "gate_up_proj"
# Whether the first projection packs a gate half that must be split on export.
_is_gated = True
Expand Down Expand Up @@ -887,6 +886,14 @@ def _get_expert_idx_from_first_proj(self, weight: torch.Tensor) -> int:
return idx

def _setup(self):
# Each expert's weight is sliced as (out_dim, in_dim), and a transposed container would be
# sliced wrong with nothing in the shapes to catch it. register_fused_experts_on_the_fly skips
# such containers; this refuses them for the wrappers registered explicitly (FP8, Nemotron-H).
if getattr(self, "is_transposed", False):
raise NotImplementedError(
f"{type(self).__name__} quantizes expert weights stored (num_experts, out_dim, "
"in_dim), but these experts are marked is_transposed."
)
n = self.num_experts
setattr(self, self._first_proj_input_quantizer_attr, TensorQuantizer())
setattr(
Expand Down Expand Up @@ -1210,28 +1217,13 @@ def fold_weight(self, keep_attrs: bool = False):
self._fold_weight_quantizer(q, (weight_slice,), keep_attrs)


class _QuantNonGatedFusedExperts(_QuantFusedExperts):
"""Quantized wrapper for non-gated fused MoE experts.

Used by NemotronH (transformers 5.5+ ``NemotronHExperts``), whose experts
are a *non-gated* MLP: a single ``up_proj`` (no gate half) and a ``down_proj``,
both stored as 3-D ``nn.Parameter`` s indexed per expert.
"""

_first_proj_attr = "up_proj"
_is_gated = False
# The grouped_mm interception is validated for the gated layout only, so these keep
# calibrating on the eager forward.
_eager_only = True


class _QuantFP8Experts(_QuantFusedExperts):
"""Scale-aware PTQ for native Transformers FP8 experts, dequantized per expert."""

# The block scales apply in get_expert_weight, which only the eager forward reaches: transformers'
# FP8 grouped_mm forward runs its own kernels, which nothing here intercepts. So, unlike for the
# non-gated wrapper, the grouped interceptor's refusal never fires for these: what keeps them on
# eager is normalize_experts_impl_on_the_fly, which runs on convert and on restore.
# FP8 grouped_mm forward runs its own kernels, which nothing here intercepts. So the grouped
# interceptor's refusal never fires for these: what keeps them on eager is
# normalize_experts_impl_on_the_fly, which runs on convert and on restore.
_eager_only = True

@property
Expand Down Expand Up @@ -1617,17 +1609,13 @@ def register_sparse_moe_on_the_fly(model):


def _fused_experts_wrapper(module):
"""Return the ``_QuantFusedExperts`` subclass for a fused-MoE-experts module, else None.
"""Return :class:`_QuantFusedExperts` for a fused-MoE-experts module, else None.

Detects the standardized HuggingFace transformers 5.0+ fused expert pattern: ``num_experts``,
a 3-D ``down_proj`` and a 3-D first projection, either

* gated (:class:`_QuantFusedExperts`): a 3-D ``gate_up_proj`` fusing gate+up. Matches
``MixtralExperts``, ``Qwen2MoeExperts``, ``Qwen3MoeExperts``,
``Qwen3_5MoeExperts``, ``DeepseekV3NaiveMoe``, ``JambaExperts``,
``OlmoeExperts``, ``MiniMaxM2Experts``, ``MiniMaxM3VLExperts``, etc.
* non-gated (``_QuantNonGatedFusedExperts``): a 3-D ``up_proj`` with no
``gate_proj`` and no ``gate_up_proj``. Matches NemotronH ``NemotronHExperts``.
a 3-D ``down_proj`` and a 3-D ``gate_up_proj`` fusing gate+up. Matches ``MixtralExperts``,
``Qwen2MoeExperts``, ``Qwen3MoeExperts``, ``Qwen3_5MoeExperts``, ``DeepseekV3NaiveMoe``,
``JambaExperts``, ``OlmoeExperts``, ``MiniMaxM2Experts``, ``MiniMaxM3VLExperts``, etc. Experts
of another layout register a wrapper of their own, as Nemotron-H's non-gated ones do.

Matching is by shape alone, so GptOss and Llama4TextExperts -- transposed layouts with their own
explicit registrations -- match too; :func:`register_fused_experts_on_the_fly` never reaches
Expand All @@ -1645,24 +1633,23 @@ def _fused_experts_wrapper(module):
gate_up = getattr(module, "gate_up_proj", None)
if isinstance(gate_up, (nn.Parameter, Tensor)) and gate_up.dim() == 3:
return _QuantFusedExperts
up = getattr(module, "up_proj", None)
if isinstance(up, (nn.Parameter, Tensor)) and up.dim() == 3:
# Only claim non-gated experts that alternate up_proj then down_proj.
if getattr(module, "gate_proj", None) is None and gate_up is None:
return _QuantNonGatedFusedExperts
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[IMPORTANT Compatibility] Dropping the non-gated structural branch makes coverage of that layout keyed on the transformers class name, and an unclaimed non-gated fused container is now skipped silently.

What changes. On the base branch, any module with num_experts + 3-D down_proj + 3-D up_proj (no gate_proj/gate_up_proj) was claimed here and registered by register_fused_experts_on_the_fly. After this PR the only non-gated handler is the explicit QuantModuleRegistry.register({NemotronHExperts: ...}) in models/nemotron_h/modeling_ptq.py, which matches type(module) is transformers.models.nemotron_h.modeling_nemotron_h.NemotronHExperts exactly.

Why it matters. The PR argues no other transformers experts class has this layout, which I agree with for the classes shipped today — but the keying also excludes classes that aren't the transformers one. is_nemotron_h_model / get_nemotron_h_decoder_layers, 40 lines below the new registration in the same file, exists precisely because ModelOpt supports remote-code Nemotron-H checkpoints (model.backbone.layers). A remote-code or forked Nemotron-H whose experts are a fused non-gated container is a different class object, so it now gets no wrapper at all.

And the failure is quiet in a way the rest of this file deliberately avoids: register_fused_experts_on_the_fly just continues when wrapper is None (no warning, unlike the transposed-layout case right above), and assert_fused_experts_intercepted only scans isinstance(module, _QuantFusedExperts), so an unwrapped container is invisible to it too. The result is an otherwise-quantized checkpoint whose MoE weights — the bulk of the parameters — silently stay BF16, with no error and no warning at calibration or export time. That is the same class of silent fault assert_fused_experts_intercepted was written to turn into a loud one.

Suggested fix. Keep the removal (the generic wrapper really was unvalidated on grouped_mm), but make the gap loud. In register_fused_experts_on_the_fly, after wrapper = _fused_experts_wrapper(module) returns None, warn when the module still looks like a fused non-gated experts container:

        wrapper = _fused_experts_wrapper(module)
        if wrapper is None:
            # A fused container of a layout nothing claims: a non-gated `up_proj` from a class
            # other than the one its model folder registers (e.g. a remote-code Nemotron-H).
            # Leaving it unwrapped silently exports its experts unquantized, so say so.
            up = getattr(module, "up_proj", None)
            down = getattr(module, "down_proj", None)
            if (
                hasattr(module, "num_experts")
                and isinstance(up, (nn.Parameter, Tensor))
                and up.dim() == 3
                and isinstance(down, (nn.Parameter, Tensor))
                and down.dim() == 3
            ):
                warnings.warn(
                    f"Fused MoE experts '{name}' ({mod_type.__name__}) store a single 3-D "
                    "`up_proj` (non-gated), a layout the generic detection does not claim. "
                    "Their weights will be exported UNQUANTIZED. A non-gated class needs its "
                    "own wrapper, as `_QuantNemotronHExperts` is for transformers' "
                    "`NemotronHExperts`; register it on this class too."
                )
            continue

Either that, or widen the registration to also cover remote-code classes via CUSTOM_MODEL_PLUGINS (the pattern models/README.md prescribes for remote-code classes and that falcon/step3p5 already use) so the remote-code path keeps working rather than degrading quietly.

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.

Not adding the warning: no released checkpoint reaches this gap. The remote-code Nemotron-H MoE checkpoints, NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 and NVIDIA-Nemotron-3-Super-120B-A12B-BF16, build their experts in modeling_nemotron_h.py as an nn.ModuleList of NemotronHMLP with nn.Linear projections, which the per-module quantization covers and the fused detection never claimed, before or after this PR. The remote-code VL model, NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-BF16, builds its language model with AutoModelForCausalLM.from_config(config.llm_config), i.e. transformers' NemotronHForCausalLM, whose NemotronHExperts this PR registers. A fused container the detection does not recognize is left as it is, as the base already does for the split-gated layout (gate_proj + up_proj), so a warning for this one layout would guard a class nothing ships.

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] Dropping the structural non-gated branch makes an unregistered non-gated fused container fall through silently.

A module with num_experts, a 3-D up_proj and a 3-D down_proj and no gate used to be claimed automatically (the capability shipped in a prior release — see the CHANGELOG.rst entry for "Support non-gated fused MoE experts in unified HuggingFace export"). Now only transformers' own NemotronHExperts is covered by the explicit registration in models/nemotron_h/modeling_ptq.py. A remote-code or vendored class with the same layout — e.g. transformers_modules.<repo>.modeling_nemotron_h.NemotronHExperts, a different class object from the registered one — gets neither a wrapper nor a message: register_fused_experts_on_the_fly just moves on, calibration leaves the experts unquantized, and the user only learns about it later from the export's NotImplementedError: MoE model with experts type '...' is not supported.

The transposed-layout case right above already warns rather than going quiet. Mirroring that here would make the narrowing diagnosable, e.g. in register_fused_experts_on_the_fly after wrapper is None:

up = getattr(module, "up_proj", None)
if (
    wrapper is None
    and isinstance(up, (nn.Parameter, Tensor))
    and up.dim() == 3
    and getattr(module, "gate_proj", None) is None
):
    warnings.warn(
        f"Fused MoE experts '{name}' ({mod_type.__name__}) use a non-gated layout (a single "
        "3-D `up_proj`), which the generic detection no longer claims. Such a class needs its "
        "own quantized wrapper registered from `modelopt/torch/models/<model_type>/"
        "modeling_ptq.py`, as transformers' `NemotronHExperts` has. Leaving them unquantized."
    )

(Optional — if the layout is considered out of support, saying so loudly still beats the opaque export failure.)

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.

Not taken. The non-gated fused layout is transformers' own NemotronHExperts, which is registered. Nemotron-H's remote code builds its routed experts as an nn.ModuleList of per-expert MLPs (checked in Nemotron-3-Nano-30B-A3B's modeling_nemotron_h.py), so a trust_remote_code load never presents a fused 3-D up_proj container, and no other transformers class has the layout. A warning no supported checkpoint can trigger is not worth its lines here.

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] Dropping the structural non-gated branch makes an unrecognized non-gated fused-experts container fail silently: _fused_experts_wrapper returns None, register_fused_experts_on_the_fly skips it without a message, _is_fused_experts_module is False, and PTQ produces a checkpoint whose MoE is simply unquantized. That is a worse failure mode than the transposed-layout case right below, which warns and says what is needed ("such a class needs its own quantized wrapper").

Registry resolution does cover a subclass of NemotronHExperts, but only one that inherits forward unchanged (_get_registered_nn_class requires nn_cls.forward is nn_cls_.forward), so a subclass or a remote-code class with its own non-gated forward lands in the silent path.

Consider keeping a detection-only arm that warns instead of claiming the module, mirroring the transposed warning:

    up = getattr(module, "up_proj", None)
    if (
        isinstance(up, (nn.Parameter, Tensor))
        and up.dim() == 3
        and getattr(module, "gate_proj", None) is None
    ):
        warnings.warn(
            f"Fused MoE experts of type {type(module).__name__} have a non-gated layout "
            "(a single 3-D `up_proj`), which the generic detection does not claim. Leaving them "
            "unquantized; such a class needs its own quantized wrapper, as NemotronHExperts has."
        )
    return None

Non-blocking — the claim that no other transformers class has this layout looks right today; the value is in telling a user with a custom one why their experts came out in BF16.

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.

Not taken, as in the two earlier threads on this layout: transformers' NemotronHExperts is the only class with a fused 3-D up_proj, and it is registered, while Nemotron-H's remote code builds its experts as an nn.ModuleList of per-expert MLPs, which the per-module quantization covers. A warning no supported checkpoint can trigger is not worth its lines.



def _is_fused_experts_module(module):
"""Check if a module is a fused MoE expert container compatible with _QuantFusedExperts.

See :func:`_fused_experts_wrapper` for the recognized layouts (gated
``gate_up_proj`` and non-gated ``up_proj``). Transposed containers match too, though
registration skips them: :func:`normalize_experts_impl_on_the_fly` must still find them, to keep
GPT-OSS's own wrapper on eager, the only forward on which it quantizes ``down_proj``'s input (it
intercepts the eager forward's ``bmm``/``matmul``).
That is one :func:`_fused_experts_wrapper` detects, or one whose class registered a subclass of
it, as Nemotron-H's non-gated experts and transformers' FP8 experts do.
Transposed containers match too, though registration skips them:
:func:`normalize_experts_impl_on_the_fly` must still find them, to keep GPT-OSS's own wrapper on
eager, the only forward on which it quantizes ``down_proj``'s input (it intercepts the eager
forward's ``bmm``/``matmul``).
"""
return _fused_experts_wrapper(module) is not None
if _fused_experts_wrapper(module) is not None:
return True
wrapper = QuantModuleRegistry.get(type(module)) if hasattr(module, "num_experts") else None
return wrapper is not None and issubclass(wrapper, _QuantFusedExperts)


def register_fused_experts_on_the_fly(model):
Expand All @@ -1688,9 +1675,8 @@ def register_fused_experts_on_the_fly(model):
if wrapper is not None and (
getattr(module, "is_transposed", False)
# Stored `(num_experts, out_dim, in_dim)`, the first projection takes the dim down_proj
# puts out. That separates the layouts for a gated container, whose first projection is
# `2 * intermediate`, whether it says so or not; a non-gated one's two layouts have the
# same shapes, so only its own `is_transposed` gives it away.
# puts out. A gated container's first projection is `2 * intermediate`, so its shapes
# give a transposed layout away whether it says so or not.
or getattr(module, wrapper._first_proj_attr).shape[-1] != module.down_proj.shape[1]
):
# `_fused_experts_wrapper` matches on parameter SHAPE only, so it also claims a
Expand Down Expand Up @@ -1755,15 +1741,14 @@ def normalize_experts_impl_on_the_fly(model):
fallback_impl = "grouped_mm" if _HAS_GROUPED_LINEAR else "eager"
# A class with its own quantized wrapper -- GPT-OSS's quantizes down_proj's input by intercepting
# the eager forward's bmm/matmul -- only works on the eager forward, and so does a fused-experts
# wrapper that says so (FP8's), so keep main's forced eager for such a model. The plugins run in
# no set order, so a class register_fused_experts_on_the_fly registers may not be registered yet:
# the wrapper it will get decides then.
# wrapper that says so (FP8's), so keep main's forced eager for such a model. Those register
# when their module is imported; register_fused_experts_on_the_fly, which may run after this,
# registers only the generic wrapper, which is not eager-only.
eager_only = sorted(
{
type(module).__name__
for module in fused_experts
if (wrapper := QuantModuleRegistry.get(type(module)) or _fused_experts_wrapper(module))
is not None
if (wrapper := QuantModuleRegistry.get(type(module))) is not None
and (not issubclass(wrapper, _QuantFusedExperts) or wrapper._eager_only)
}
)
Expand Down
7 changes: 7 additions & 0 deletions tests/gpu/torch/models/test_modeling_ptq_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
else:
assert GptOssExperts in QuantModuleRegistry, "GptOssExperts not registered"

try:
from transformers.models.nemotron_h.modeling_nemotron_h import NemotronHExperts
except ImportError:
pass
else:
assert NemotronHExperts in QuantModuleRegistry, "NemotronHExperts not registered"

assert register_falcon_linears_on_the_fly in CUSTOM_MODEL_PLUGINS, "Falcon callback missing"
assert register_moe_linear_on_the_fly in CUSTOM_MODEL_PLUGINS, "Step callback missing"

Expand Down
Loading
Loading