Skip to content

fix(export): unblock AWQ export for fused MoE experts (Nemotron-H)#2017

Draft
cjluo-nv wants to merge 1 commit into
mainfrom
fix-nemotronh-fused-awq-export
Draft

fix(export): unblock AWQ export for fused MoE experts (Nemotron-H)#2017
cjluo-nv wants to merge 1 commit into
mainfrom
fix-nemotronh-fused-awq-export

Conversation

@cjluo-nv

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: Bug fix

int4_awq PTQ on nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 could not produce a checkpoint. Calibration succeeded; export failed. Two independent defects, both reachable only on AWQ formats, and both specific to the fused-experts layout transformers>=5.5 uses for Nemotron-H (NemotronHExperts → 3-D packed params, wrapped as _QuantNonGatedFusedExperts). FP8 / NVFP4 were unaffected, which is why only AWQ was reported broken.

1. TypeError: object of type 'QuantNemotronHExperts' has no len()

requantize_resmooth_fused_llm_layers() called get_experts_list() for every MoE block, gated on "awq" in quantization_format. That helper addresses experts positionally (module.experts[i].<linear>), which only exists for the sequential nn.ModuleList layout — a fused wrapper has no __len__ and no per-expert submodules.

Fused blocks are now skipped. This is correct rather than merely crash-avoiding: resmoothing exists to give every expert in a group one common input pre_quant_scale, and a fused wrapper already shares a single input quantizer across all its experts, so there is nothing to reconcile.

2. IndexError: tuple index out of range

With #1 unblocked, _export_fused_experts()'s uncalibrated-expert fallback assigned a scalar amax. That is only valid for per-tensor formats; for block-wise formats (INT4 AWQ, block_sizes={-1: 128}) the exporter then evaluated weight.shape[-1] // weights_scaling_factor.shape[-1] on a 0-d tensor.

The fallback now derives a per-block amax shaped (out_features, n_blocks), matching what MaxCalibrator produces for a calibrated expert. This path is easy to hit on wide MoEs: 512 routed experts at top-22 leaves many experts unrouted for any modest calibration set.

The two are sequential — fixing #1 alone just moves the crash to #2.

Usage

python examples/llm_ptq/hf_ptq.py \
  --pyt_ckpt_path nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 \
  --qformat int4_awq \
  --export_path ./n3s_int4awq

Testing

  • End-to-end on 4×B200, full 120B model: previously died at export, now writes a complete checkpoint — 8 shards / 68 GB, quant_algo: W4A16_AWQ, group_size: 128, zero tracebacks. Expert weight_scale shapes verified at real dims: up_proj (2688, 8) = 1024/128 blocks, down_proj (1024, 21) = 2688/128 blocks; packed uint8 weights at half the output rows; all scales finite.
  • New regression test test_uncalibrated_expert_block_quant_amax_is_per_block — asserts an uncalibrated expert under a block-wise format gets a 2-D per-block amax. Verified it fails on the pre-fix fallback and passes after.
  • tests/unit/torch/export + tests/unit/torch/quantization: 962 passed, 8 skipped (one unrelated file skipped — test_pytorch_geometric_plugin.py, missing optional dep).
  • FP8 control on the same model still exports OK, confirming the AWQ-only gating.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ — the skip only applies to fused-expert blocks (previously a hard crash); the sequential nn.ModuleList path is untouched. The block-aware amax only changes the uncalibrated fallback, and only for block-wise formats; per-tensor formats still get the scalar amax 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 — no copied code, no new dependencies.
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: ✅ — 0.47 Bug Fixes.
  • Did you get Claude approval on this PR?: ❌ — not yet run.

Additional Information

Scope is not Nemotron-H-specific: any model using transformers>=5.5 fused experts (_QuantFusedExperts / _QuantNonGatedFusedExperts) with an AWQ format hits the same two crashes.

Two related issues found while debugging, deliberately left out of this PR as separate concerns:

  1. --moe_calib_experts_ratio is a silent no-op on the fused path — mode.py applies it via hasattr(module, "_moe_calib_experts_ratio"), and that attribute is defined on _QuantSparseSequentialMoe only. Export now succeeds, but unrouted experts get a weight-derived amax rather than an activation-calibrated one, so the accuracy tail is still open.
  2. hf_quant_config.json exclude_modules entries contain a literal NUL byte followed by a pt_name_sentinel suffix (i.e. lm_head. + \x00 + backbone.pt_name_sentinel). Pre-existing and format-independent (identical under FP8), but it looks like a hazard for downstream vLLM / TRT-LLM config parsing.

🤖 Generated with Claude Code

int4_awq PTQ on NVIDIA-Nemotron-3-Super-120B-A12B-BF16 could not produce a
checkpoint. Calibration succeeded; export failed. Two independent defects, both
only reachable on AWQ formats, and both specific to the fused-experts layout that
transformers >= 5.5 uses for NemotronH (NemotronHExperts -> 3-D packed params,
wrapped as _QuantNonGatedFusedExperts). FP8/NVFP4 were unaffected.

1) requantize_resmooth_fused_llm_layers() called get_experts_list() for every MoE
   block, gated on "awq" in quantization_format. That helper addresses experts
   positionally (module.experts[i].<linear>), which only exists for the
   sequential nn.ModuleList layout, so a fused wrapper raised
   "TypeError: object of type 'QuantNemotronHExperts' has no len()".
   Fused blocks are now skipped: resmoothing exists to give every expert in a
   group one common input pre_quant_scale, and a fused wrapper already shares a
   single input quantizer across all its experts, so there is nothing to
   reconcile.

2) With that unblocked, _export_fused_experts()'s uncalibrated-expert fallback
   assigned a scalar amax. That is only valid for per-tensor formats; for
   block-wise formats (INT4 AWQ, block_sizes={-1: 128}) the exporter then
   evaluated weight.shape[-1] // weights_scaling_factor.shape[-1] on a 0-d
   tensor and raised "IndexError: tuple index out of range". The fallback now
   derives a per-block amax shaped (out_features, n_blocks), matching what
   MaxCalibrator produces for a calibrated expert. This path is easy to hit on
   this model: 512 routed experts at top-22 leaves many experts unrouted for any
   modest calibration set.

Verified end-to-end on 4xB200: hf_ptq.py --qformat int4_awq on the full 120B now
writes a complete checkpoint (8 shards, 68G, W4A16_AWQ group_size=128), with
expert weight_scale shapes (2688, 8) and (1024, 21) matching 1024/128 and
2688/128 blocks. Adds a regression test that fails on the pre-fix fallback.

Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 24, 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 Jul 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 946f20bb-0987-4f35-94fa-63e539da57e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-nemotronh-fused-awq-export

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

@github-actions

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-2017/

Built to branch gh-pages at 2026-07-24 20:11 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.57143% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.35%. Comparing base (d143276) to head (1efcd42).

Files with missing lines Patch % Lines
modelopt/torch/export/unified_export_hf.py 40.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2017      +/-   ##
==========================================
- Coverage   78.36%   78.35%   -0.01%     
==========================================
  Files         518      518              
  Lines       58658    58669      +11     
==========================================
+ Hits        45965    45973       +8     
- Misses      12693    12696       +3     
Flag Coverage Δ
unit 54.96% <78.57%> (+<0.01%) ⬆️

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.

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