Skip to content

[OMNIML-6073] Quantize Nemotron-H's experts under grouped_mm, with their wrapper in its model folder - #3067

Open
shengliangxu wants to merge 16 commits into
mainfrom
shengliangx/hfep-8b-nemotron-h-experts
Open

shengliangxu wants to merge 16 commits into
mainfrom
shengliangx/hfep-8b-nemotron-h-experts

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Split out of #2745 on 2026-10-27, so that the Nemotron-H part is reviewed on its own. #2745, #3122 and #3074 (the calibration this PR builds on) have all merged.

Base: main.

In the tree: #2741's helper base merges this PR instead of #2745 and #3074. The expert-parallel chain quantizes Nemotron-H's experts on the torch_a2a forward, which refuses an eager-only wrapper.

Series layout

What does this PR do?

Type of change: New feature

#2745 quantizes fused experts under grouped_mm, the experts backend transformers selects by default, in the gated layout (gate_up_proj). Nemotron-H's experts are non-gated, down_proj(act_fn(up_proj(x))), with a single 3-D up_proj. On #2745 and #3074 they keep main's generic _QuantNonGatedFusedExperts and calibrate on the eager forward, which main forces and #3074 keeps for them.

  • Their wrapper moves to Nemotron-H's model folder. _QuantNemotronHExperts lives in modelopt/torch/models/nemotron_h/modeling_ptq.py. It registers on transformers' NemotronHExperts, which every supported release (5.5 on) ships, as modelopt/torch/models/README.md asks of wrappers only one model needs. The generic plugin's _QuantNonGatedFusedExperts and its up_proj-only structural detection are removed: no other transformers experts class has that layout.
  • They calibrate on grouped_mm. The wrapper is not eager-only: [OMNIML-6073] [2/3] Quantize fused experts under grouped_mm; leave transposed layouts unquantized #2745's grouped interception reaches the non-gated layout through _first_proj_attr, and both forwards give each expert's quantizers the same rows. Under expert parallelism ([OMNIML-6060] [3/3] Refuse an expert-parallel load that would calibrate the experts wrong #2741 on), they run the torch_a2a forward, which refuses an eager-only wrapper.
  • The backend normalization still finds them. _is_fused_experts_module also counts a class registered with a _QuantFusedExperts subclass. So a backend the quantizers cannot see (batched_mm) is still redirected for Nemotron-H. Transformers' non-gated FP8 experts (an FP8 Nemotron-H's) are still forced onto eager.

Scope. This applies to transformers' built-in Nemotron-H (NemotronHExperts), whose default experts backend is grouped_mm. Loading a checkpoint's own modeling code instead (the Auto classes with trust_remote_code=True) builds per-expert nn.Linear MLPs, whose per-expert quantization this PR leaves unchanged. examples/hf_ptq/hf_ptq.py builds the built-in model even with --trust_remote_code whenever the built-in config represents the checkpoint, as it does NVIDIA-Nemotron-3-Nano-30B-A3B's.

Behavior change. Nemotron-H's MoE calibration runs on grouped_mm where transformers selects it, so some of its activation scales can differ from main's, as #3074's changelog entry says for fused experts. Loading with experts_implementation="eager" calibrates as before.

Usage

No API change. To calibrate exactly as earlier releases did:

model = AutoModelForCausalLM.from_pretrained(path, experts_implementation="eager")

Testing

Before your PR is "Ready for review"

  • Is this change backward compatible?: ❌ For transformers' built-in Nemotron-H, MoE calibration now runs on grouped_mm, so some activation scales differ from earlier releases; experts_implementation="eager" restores the earlier calibration. A load of a checkpoint's own modeling code (the Auto classes with trust_remote_code=True) calibrates as before.
  • 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: [OMNIML-6073] [2/2] Calibrate fused experts on grouped_mm and torch_a2a instead of forcing eager #3074's entry covers fused experts moving to grouped_mm.
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models; merges after #2745.

Summary by CodeRabbit

  • New Features
    • Added quantization support for Nemotron-H expert layers, including calibration and export with eager and grouped matrix multiplication implementations.
  • Behavior Changes
    • Non-gated expert layouts are no longer detected automatically. Quantization support is available for expert variants with a registered wrapper, including Nemotron-H.

@shengliangxu
shengliangxu requested review from a team as code owners October 27, 2026 20:37
@shengliangxu
shengliangxu requested review from chadvoegele and removed request for a team October 27, 2026 20:37
@coderabbitai

coderabbitai Bot commented Oct 27, 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

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 3ae6d56 and 70994f8.

Files selected for processing: 3
  • modelopt/torch/quantization/plugins/huggingface.py
  • tests/unit/torch/models/test_nemotron_h_modeling_ptq.py
  • tests/unit/torch/quantization/plugins/test_fused_experts.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.

Walkthrough

The changes add a quantization wrapper for Nemotron-H experts and update fused-expert dispatch so non-gated layouts use registered wrappers. Tests cover wrapper conversion and behavior across eager and grouped_mm implementations.

Changes: Nemotron-H expert quantization

Layer / File(s) Summary
Fused-expert wrapper dispatch
modelopt/torch/quantization/plugins/huggingface.py, modelopt/torch/export/moe_utils.py, tests/unit/torch/quantization/plugins/test_fused_experts.py
The generic wrapper no longer detects non-gated experts by matching an up_proj shape. Module detection now recognizes registered wrappers that subclass _QuantFusedExperts. Documentation describes non-gated subclasses. Tests remove the synthetic non-gated wrapper suite and fixtures, and run the eager native FP8 test for gated and non-gated configurations.
Nemotron-H wrapper and validation
modelopt/torch/models/nemotron_h/modeling_ptq.py, tests/unit/torch/models/test_nemotron_h_modeling_ptq.py, tests/gpu/torch/models/test_modeling_ptq_registration.py
NemotronHExperts is registered with a wrapper that uses up_proj as its first projection and marks the layout non-gated. Tests check conversion, projection quantizers, eager and grouped_mm output equivalence, calibration statistics, GPTQ Hessians, export, implementation normalization, and conditional registry availability.

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant NemotronHExperts
  participant QuantModuleRegistry
  participant QuantNemotronHExperts
  participant Projections
  NemotronHExperts->>QuantModuleRegistry: Resolve registered wrapper
  QuantModuleRegistry->>QuantNemotronHExperts: Select Nemotron-H wrapper
  QuantNemotronHExperts->>Projections: Configure up_proj and down_proj quantizers
Loading
Merge Risk: Minimal · previous review 70994

No blocking issue was established for Nemotron-H expert quantization; the PR is mergeable after normal checks.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: adding Nemotron-H expert quantization for grouped_mm with a model-specific wrapper.
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 No listed security anti-pattern was introduced. The PR adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/…
Full details: Security Anti-Patterns

Explanation

No listed security anti-pattern was introduced. The PR adds no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec line. The changed-file inventory includes no pyproject.toml or requirements file, so no dependency was added. The existing trust_remote_code=True reference in huggingface.py is unchanged by this PR.

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

@shengliangxu
shengliangxu requested review from kevalmorabia97 and meenchen and removed request for a team October 27, 2026 20:37
This was referenced Oct 27, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 23ab7f4

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d452768

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

@Fridah-nv

Copy link
Copy Markdown
Contributor

Question (non-blocking): the Nemotron-3-Nano table shows 1,932 of 5,934 expert input scales moving, up to 4.39%, between grouped_mm and eager. That is in line with the changelog's figure for Qwen3-30B. Has anything downstream been checked on Nemotron-H, such as a quick perplexity or gsm8k run for eager against grouped_mm calibration? If not, one comparison would settle it. experts_implementation="eager" stays the documented fallback.

@Fridah-nv Fridah-nv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve to unblock

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 0da0591

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

… its model folder

Nemotron-H's non-gated experts get their own wrapper, _QuantNemotronHExperts, in
modelopt/torch/models/nemotron_h/modeling_ptq.py, registered on transformers' NemotronHExperts
(every supported release ships it) in place of the generic plugin's _QuantNonGatedFusedExperts.
It is not eager-only: the grouped_mm interception reaches the non-gated layout through
_first_proj_attr, so these experts calibrate on the backend transformers selects, as the gated
ones do.

The generic structural detection now claims only the gated layout. _is_fused_experts_module also
counts a class registered with a _QuantFusedExperts subclass, so the backend normalization still
finds Nemotron-H's experts, and transformers' non-gated FP8 experts, which stay on eager.

The non-gated tests move to tests/unit/torch/models and run the real NemotronHExperts.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Nemotron-H's experts register at import here, so no class the on-the-fly registration claims is
eager-only, and the stand-in the test needed is gone with the generic non-gated detection.

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

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

The grouped-vs-eager comparisons would pass just as well if the grouped
forward fell back to per-expert F.linear calls, which the eager hook
intercepts with the same values. The interception count tells the two
hooks apart: one _grouped_linear call per projection, against one
F.linear call per projection and expert.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The test this one replaced checked it: for the non-gated layout up_proj
is the whole fused weight, so an export that kept it would hold every
expert twice, the fused original beside the per-expert copies.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_is_fused_experts_module found Nemotron-H's experts through their class's
registration, which the class quantize() converts them to does not carry, so a
second quantize() over the converted model skipped the backend normalization
for them. Gated experts are found by their shapes either way.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
up_proj's input is the hidden states, which the grouped forward hands
_grouped_linear gathered by expert: the same rows eager feeds per expert, so
their amax matches exactly. Only down_proj's input passes through a GEMM and
keeps the tolerance.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Its one caller, normalize_experts_impl_on_the_fly, runs inside
replace_quant_module, which refuses a model that is already quantized, so
no converted experts module reaches it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Rebased onto #3074 on main, the fused-experts tests import #2745's
quant_utils helpers; get_quantization_format was used only by the
non-gated tests that moved to the Nemotron-H test file.

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

The non-gated cases of these fused-experts tests went with the stand-in
when the non-gated tests moved here. They run on the real class again:
the eager forward recovers each up_proj slice's expert, and the
quantizer-only iterator yields the weight iterator's quantizers in its
order without indexing the fused weight; that test fails if the
iterator names the gated projection.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With the non-gated wrapper gone, register_fused_experts_on_the_fly
registers only the generic wrapper, which is not eager-only, so looking
it up for a class not registered yet cannot change which experts are
forced onto eager: those wrappers (FP8's, GPT-OSS's, Nemotron-H's)
register when their modules are imported. The transposed check's
comment no longer speaks of non-gated layouts.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Registered explicitly, the Nemotron-H experts skip the transposed-layout
check register_fused_experts_on_the_fly applies, and the wrapper slices
each expert's weight as (out_dim, in_dim). A transposed layout would
slice every expert wrong with nothing in the shapes to catch it, so
setup now refuses experts transformers marks is_transposed. A test
marks real NemotronHExperts transposed and checks the conversion
raises; it fails without the change.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test adb9757

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +48 to +56
def _setup(self):
# The per-expert slicing reads (out_dim, in_dim); a transposed layout would slice every
# expert's weight wrong without any shape mismatch to catch it.
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."
)
super()._setup()

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 guard is right, but it belongs one level up: _QuantFusedExperts._setup, not only this subclass.

is_transposed is part of the transformers 5.x fused-experts contract (every such module carries it — see _SyntheticGroupedExperts in test_fused_experts.py:286), and register_fused_experts_on_the_fly (huggingface.py:1668) already refuses it for every container it registers on the fly. The only _QuantFusedExperts subclasses that bypass that filter are the explicitly registered ones — this wrapper and _QuantFP8Experts (huggingface.py:1531). _QuantFP8Experts has exactly the same exposure: get_expert_weight indexes dim 0 and applies block scales as (out_dim, in_dim) (huggingface.py:1234-1242), and _export_fused_experts splits gate|up along dim 0 of the per-expert slice, so a transposed FP8Experts would export silently wrong weights with no shape mismatch to catch it — the same failure this commit closes for Nemotron-H.

Why it matters: the hole stays open for the one other explicitly registered subclass, and a future modeling_ptq.py wrapper has to remember to re-add the check. Hoisting it also removes the need for a _setup override here at all.

Suggested fix: move the is_transposed check into _QuantFusedExperts._setup (before the quantizer construction) and drop this override, keeping the Nemotron-H test as-is — it would still fail without the base-class guard. On-the-fly-registered gated containers are unaffected, since register_fused_experts_on_the_fly never registers a transposed one. Non-blocking; a follow-up PR is fine if you'd rather keep this slice minimal.

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 8d4a9b6. The is_transposed check now runs in _QuantFusedExperts._setup, before any quantizer is built, so the native FP8 wrapper gets it too, and the Nemotron-H override is gone.

  • test_native_fp8_experts_marked_transposed_are_refused marks native FP8 experts transposed and checks their conversion raises. With the check removed, it fails, and so does the Nemotron-H test.
  • One existing test needed a change. The test that rebuilds an earlier release's state used to quantize experts marked transposed with the generic wrapper. It now builds them unmarked, since that release read no flag. Its restore half is unchanged and still refused.

(For what it's worth, transformers 5.18's FP8Experts doesn't carry is_transposed today, so for FP8 the check guards against a future layout change.)

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review — LGTM (published as a comment: automatic approval is reserved for approve).

LGTM: the is_transposed refusal closes the last open thread, and the 65 added source lines keep the registry key and quantizer names, so existing Nemotron-H checkpoints still restore.

Needs action:

  • Confirm the test deletions as the human reviewer. Fridah-nv's 2026-10-30 approve predates the latest commit, which added the guard and its test only. TestNonGatedFusedExperts, test_forces_eager_for_eager_only_experts_not_registered_yet and the non-gated parametrizations covered the removed _QuantNonGatedFusedExperts stand-in; those cases now run on the real NemotronHExperts.

No action needed:

  • ✔️ Resolved since the last review: the transposed-layout hole Fridah-nv raised — _QuantNemotronHExperts._setup now refuses experts marked is_transposed, pinned by test_experts_marked_transposed_are_refused, and the raise-before-super()._setup() shape matches _QuantFP8Experts._setup.
  • One non-blocking SUGGESTION: hoist that guard into _QuantFusedExperts._setup so _QuantFP8Experts, the other explicitly registered subclass, gets it too.
  • Verified this round: _is_fused_experts_module's registry arm keeps the batched_mm redirect and the FP8 force-eager behavior now that the structural up_proj branch is gone; dropping or _fused_experts_wrapper(module) from eager_only is result-identical (GptOss/Llama4 hit the registry, the generic wrapper is not eager-only); the grouped interception is layout-agnostic (_down_proj_linear alternation, _first_proj_attr binding); the module-scope transformers import matches gpt_oss/modeling_ptq.py and models/README.md; new test file header matches LICENSE_HEADER.
  • Scope: full review, trigger carried no scoping instructions. All 6 changed files (+350/−370) reviewed. Baseline commit e352caca3c3 (.agents/review-guidelines/code-review.md); head adb97571. No prompt-injection attempt in the diff or PR body.
  • Verification limits: I did not run tests, CI or GPU lanes; the GPU results in the description are unverified here. transformers is not importable in this checkout, so NemotronHExperts.num_experts and the 5.5.4 availability of the class rest on the author's test runs rather than my own check.

The is_transposed refusal lived in the Nemotron-H wrapper only, but the
native FP8 experts wrapper is registered explicitly too, past the
on-the-fly check that skips transposed containers, and reads each
expert's weights and block scales as (out_dim, in_dim) just the same.
The check now runs in _QuantFusedExperts._setup, before any quantizer is
built, and the Nemotron-H override is gone. A test marks native FP8
experts transposed and checks their conversion raises; it and the
Nemotron-H test fail without the check. The test that rebuilds an
earlier release's state quantizes its experts unmarked, as that release
read no flag.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test 8d4a9b6

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review — LGTM (published as a comment: automatic approval is reserved for approve).

LGTM: the last open suggestion is resolved and the 64 added source lines keep the registry key and quantizer names, so existing Nemotron-H checkpoints still restore.

Needs action:

  • Confirm the test removals as the human reviewer. Fridah-nv's 2026-10-30 approve predates the last two commits. _SyntheticNonGatedFusedExperts, TestNonGatedFusedExperts, test_forces_eager_for_eager_only_experts_not_registered_yet and the non-gated parametrizations all covered the removed _QuantNonGatedFusedExperts; those cases now run on the real NemotronHExperts in tests/unit/torch/models/test_nemotron_h_modeling_ptq.py.

No action needed:

  • ✔️ Resolved since the last review: the is_transposed guard moved into _QuantFusedExperts._setup (huggingface.py:892), so _QuantFP8Experts — the other explicitly registered subclass — refuses transposed experts too, pinned by test_native_fp8_experts_marked_transposed_are_refused. It raises before any mutation, and _QuantFP8Experts._setup's own dtype check is also pre-super(), so a refusal leaves the module untouched.
  • Verified this round: the guard reaches only wrappers registered explicitly — grep confirms _QuantFP8Experts and _QuantNemotronHExperts are the sole _QuantFusedExperts subclasses in modelopt/, so GPT-OSS and Llama4 (genuinely transposed, own non-subclass wrappers) are unaffected, and register_fused_experts_on_the_fly already skipped marked containers on the generic path. test_a_state_saved_with_transposed_experts_quantized_no_longer_restores building its saved state with marked=False is justified: the flag is a plain attribute, not part of modelopt_state, so the state is equivalent and the restore-side assertion is unchanged. No changelog entry owed (#3074's covers it); no public API, config-schema or state-key change. New test file header matches LICENSE_HEADER.
  • Scope: full review, trigger carried no scoping instructions. All 6 changed files (+366/−374) reviewed; 64 added source lines, well inside the size budget. Baseline commit e352caca3c3 (.agents/review-guidelines/code-review.md); head 8d4a9b6b. No prompt-injection attempt in the diff or PR body.
  • Verification limits: I did not run tests, CI or GPU lanes. transformers is not importable in this checkout, so that FP8Experts/NemotronHExperts default is_transposed to False (and the 5.5.4 availability of _grouped_linear) rests on the author's reported runs on 5.18 and 5.5.4, not my own check.

🤖 Generated with Claude Code

grouped_mm and eager compute the same expert products in different orders, and neither is the reference for the other. The calibration and GPTQ tests now say that the two backends agree, and the calibration test says where its down_proj tolerance comes from.

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

Copy link
Copy Markdown
Collaborator Author

/ok to test 0ff8531

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants