Skip to content

fix(quantization): Qwen3-VL MoE PTQ on transformers>=5.12 (NVBug 6518551) - #2046

Open
Edwardf0t1 wants to merge 1 commit into
mainfrom
fix/qwen3vl-moe-experts-transformers-5.12
Open

fix(quantization): Qwen3-VL MoE PTQ on transformers>=5.12 (NVBug 6518551)#2046
Edwardf0t1 wants to merge 1 commit into
mainfrom
fix/qwen3vl-moe-experts-transformers-5.12

Conversation

@Edwardf0t1

@Edwardf0t1 Edwardf0t1 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

Fixes NVBug 6518551 / OMNIML-5581: PTQ of Qwen3-VL-30B-A3B-Instruct on transformers 5.12.1 crashes during mtq.quantize:

File "modelopt/torch/quantization/plugins/huggingface.py", line 899, in _setup
    nn.Linear(self.hidden_size, expert_dim, bias=False)
AttributeError: 'QuantQwen3VLMoeTextExperts' object has no attribute 'hidden_size'. Did you mean: 'hidden_dim'?

Root cause. transformers 5.12 moved Qwen3VLMoeTextExperts onto the standard @use_experts_implementation fused layout:

transformers<5.12 transformers>=5.12
dims hidden_size, expert_dim hidden_dim, intermediate_dim
gate_up_proj (E, hidden, 2*expert_dim) (E, 2*intermediate_dim, hidden)
forward torch.bmm / @ F.linear twice per expert

The legacy _QuantQwen3VLMoeTextExperts wrapper only understands the older layout, yet stayed statically registered at import time. Because register_fused_experts_on_the_fly skips any type already in QuantModuleRegistry, that stale registration shadowed the generic _QuantFusedExperts — which already handles the new layout correctly (Mixtral, Qwen3-MoE, Qwen3.5-MoE, MiniMax-M2/M3 all go through it).

Fix. Detect the new layout via the _apply_gate class attribute that @use_experts_implementation sets, and skip the static registration so on-the-fly detection claims it with _QuantFusedExperts.

The legacy wrapper must stay registered on transformers<5.12: that layout is structurally indistinguishable from a generic fused-experts module (num_experts + 3-D gate_up_proj/down_proj), but its torch.bmm-based forward is not intercepted by the generic wrapper, so simply deleting the registration would silently quantize nothing. pyproject.toml supports transformers>=4.56,<5.13, so both layouts must keep working.

Usage

python hf_ptq.py --model Qwen3-VL-30B-A3B-Instruct \
  --recipe general/ptq/fp8_default-kv_fp8 \
  --dataset cnn_dailymail --calib_size 512 \
  --export_path Qwen3-VL-30B-A3B-Instruct-fp8_default-kv_fp8

Testing

New TestQwen3VLMoeTextExperts in tests/unit/torch/quantization/plugins/test_fused_experts.py exercises the real transformers module (not a synthetic stand-in) and adapts to the installed layout:

  • test_registration_matches_installed_layout — new layout is left to on-the-fly detection and resolves to _QuantFusedExperts; old layout keeps the explicit registration.
  • test_convert_and_forward_matches_reference — conversion succeeds and is numerically transparent before calibration.
  • test_quantize_collects_amax — calibration actually reaches the experts rather than silently no-op'ing.

Verified in two environments (there was no prior test coverage for quantized Qwen3-VL MoE experts):

transformers==4.57.6 (old) transformers==5.12.1 (new)
new tests, without the fix 3 passed 3 failed — reproduces the reported AttributeError
new tests, with the fix 3 passed 3 passed
full test_fused_experts.py 48 passed 48 passed
full tests/unit/torch/quantization/plugins/ 99 passed, 6 skipped 99 passed, 6 skipped

pre-commit run --files ... passes on all changed files.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ — transformers<5.12 keeps the legacy wrapper unchanged.
  • 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?: ✅
  • Did you get Claude approval on this PR?: ❌ — not yet run.

Additional Information

Reported by Kenny Kang (SW-GPU) on GB200×4, nvcr.io/nvidia/pytorch:26.05-py3, modelopt 0.46.0rc0. Committed for ModelOpt 0.46.0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Qwen3-VL MoE post-training quantization compatibility with Transformers 5.12 and later.
    • Improved handling of updated fused-expert layouts while preserving support for older layouts.
    • Ensured FP8 calibration produces valid activation ranges across supported layouts.
  • Tests

    • Added regression coverage for Qwen3-VL MoE expert conversion, numerical accuracy, and FP8 calibration across Transformers versions.

…551)

transformers 5.12 moved `Qwen3VLMoeTextExperts` onto the standard
`@use_experts_implementation` fused layout: `hidden_size`/`expert_dim` were
renamed to `hidden_dim`/`intermediate_dim`, `gate_up_proj` was transposed to
(num_experts, 2*intermediate_dim, hidden_dim), and the forward now calls
`F.linear` twice per expert instead of using `torch.bmm`.

The legacy `_QuantQwen3VLMoeTextExperts` wrapper only understands the older
layout, yet stayed statically registered. Since `register_fused_experts_on_the_fly`
skips any type already in `QuantModuleRegistry`, that registration shadowed the
generic `_QuantFusedExperts` that already handles the new layout correctly, and
conversion crashed with:

    AttributeError: 'QuantQwen3VLMoeTextExperts' object has no attribute 'hidden_size'

Detect the new layout via the `_apply_gate` class attribute set by the
`@use_experts_implementation` decorator and skip the static registration, letting
on-the-fly detection claim it. The legacy wrapper must stay registered on
transformers<5.12: that layout is structurally indistinguishable from a generic
fused-experts module, but its `torch.bmm`-based forward cannot be intercepted by
the generic wrapper, which would silently quantize nothing.

Add regression tests exercising the real transformers module on both layouts:
registration matches the installed layout, conversion is numerically transparent,
and calibration actually collects amax. All three reproduce the reported
AttributeError without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhiyu Cheng <zhiyuc@nvidia.com>
@Edwardf0t1
Edwardf0t1 requested review from a team as code owners August 3, 2026 06:09
@Edwardf0t1
Edwardf0t1 requested a review from shengliangxu August 3, 2026 06:09
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Qwen3-VL MoE quantization plugin now distinguishes legacy expert layouts from Transformers 5.12+ fused layouts. Tests cover registration, conversion equivalence, and FP8 calibration.

Changes

Qwen3-VL MoE compatibility

Layer / File(s) Summary
Layout-aware expert registration
modelopt/torch/quantization/plugins/huggingface.py, CHANGELOG.rst
The legacy wrapper remains for older Qwen3-VL MoE layouts. Classes marked with _apply_gate use generic fused-expert detection.
Qwen3-VL test setup
tests/unit/torch/quantization/plugins/test_fused_experts.py
Tests construct initialized experts and routing inputs for the supported forward signatures and layouts.
Conversion and calibration validation
tests/unit/torch/quantization/plugins/test_fused_experts.py
Tests verify layout-dependent registration, conversion equivalence, and nonzero FP8 calibration values.

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

Suggested reviewers: aanoosheh

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the quantization fix for Qwen3-VL MoE on transformers>=5.12.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 The only modelopt Python change adds documentation and a class-attribute registry guard; no prohibited security pattern, # nosec comment, example change, or dependency addition was introduced.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/qwen3vl-moe-experts-transformers-5.12
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qwen3vl-moe-experts-transformers-5.12

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

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/torch/quantization/plugins/test_fused_experts.py`:
- Around line 1429-1493: Add a brief justification comment before each local
transformers import in _make_qwen3_vl_moe_experts, _qwen3_vl_moe_is_new_layout,
and _experts_type, identifying transformers as an optional dependency and/or
explaining the version-dependent module path; keep the imports local unless
moving them to module scope is appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 96d93cc8-5a43-4a2d-9759-012150fdfc22

📥 Commits

Reviewing files that changed from the base of the PR and between 9d360af and 6ada6dd.

📒 Files selected for processing (3)
  • CHANGELOG.rst
  • modelopt/torch/quantization/plugins/huggingface.py
  • tests/unit/torch/quantization/plugins/test_fused_experts.py

Comment on lines +1429 to +1493
def _make_qwen3_vl_moe_experts():
"""Build a tiny real ``Qwen3VLMoeTextExperts`` with initialized weights."""
from transformers.models.qwen3_vl_moe.configuration_qwen3_vl_moe import Qwen3VLMoeTextConfig
from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts

config = Qwen3VLMoeTextConfig(
hidden_size=QWEN_HIDDEN_DIM,
intermediate_size=QWEN_HIDDEN_DIM,
moe_intermediate_size=QWEN_INTERMEDIATE_DIM,
num_experts=QWEN_NUM_EXPERTS,
num_experts_per_tok=QWEN_TOP_K,
num_hidden_layers=1,
num_attention_heads=2,
num_key_value_heads=2,
vocab_size=128,
)
# The fused forward of transformers>=5.12 dispatches on this; ``eager`` is the only
# backend that routes through ``F.linear`` and therefore through the quantizer hooks.
config._experts_implementation = "eager"
experts = Qwen3VLMoeTextExperts(config)
# Weights are created with ``torch.empty``; fill them so comparisons are meaningful.
torch.manual_seed(0)
with torch.no_grad():
for param in experts.parameters():
param.normal_(std=0.02)
return experts


def _qwen3_vl_moe_is_new_layout():
"""True when transformers>=5.12 moved the experts onto the generic fused layout."""
from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts

return hasattr(Qwen3VLMoeTextExperts, "_apply_gate")


def _qwen3_vl_moe_forward_args():
"""Routing inputs for the installed layout, sized so every expert is hit.

The two layouts disagree on both argument order and routing-weight shape, and the
pre-5.12 eval-mode forward computes a dense weighted sum over all experts. Zeroing the
weights outside the top-k keeps that dense path equal to the sparse per-expert loop.
"""
seq_len = QWEN_NUM_EXPERTS // QWEN_TOP_K
torch.manual_seed(0)
hidden_states = torch.randn(seq_len, QWEN_HIDDEN_DIM)
router_indices = torch.arange(QWEN_NUM_EXPERTS, dtype=torch.long).reshape(seq_len, QWEN_TOP_K)
top_k_weights = torch.softmax(torch.randn(seq_len, QWEN_TOP_K), dim=-1)

if _qwen3_vl_moe_is_new_layout():
return hidden_states, router_indices, top_k_weights

routing_weights = torch.zeros(seq_len, QWEN_NUM_EXPERTS)
routing_weights.scatter_(1, router_indices, top_k_weights)
return hidden_states.unsqueeze(0), routing_weights, router_indices


class TestQwen3VLMoeTextExperts:
"""The registered wrapper must match the installed transformers layout (nvbug 6518551)."""

@staticmethod
def _experts_type():
from transformers.models.qwen3_vl_moe.modeling_qwen3_vl_moe import Qwen3VLMoeTextExperts

return Qwen3VLMoeTextExperts

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a justification comment for the local transformers imports.

_make_qwen3_vl_moe_experts (lines 1431-1432), _qwen3_vl_moe_is_new_layout (line 1459), and _experts_type (line 1490) each import from transformers.models.qwen3_vl_moe... inside the function body. None of these local imports carries a comment naming the reason.

Move these imports to module scope, or add a brief comment justifying the local import (e.g., noting that the module path changed across transformers versions or that transformers is an optional dependency of the plugin under test).

As per path instructions for tests/**/*.py: "Imports inside functions or test methods without explicit justification... The only acceptable in-function imports are for circular imports or optional dependencies... and those should carry a brief comment naming the reason."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/torch/quantization/plugins/test_fused_experts.py` around lines
1429 - 1493, Add a brief justification comment before each local transformers
import in _make_qwen3_vl_moe_experts, _qwen3_vl_moe_is_new_layout, and
_experts_type, identifying transformers as an optional dependency and/or
explaining the version-dependent module path; keep the imports local unless
moving them to module scope is appropriate.

Source: Path instructions

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.42%. Comparing base (95ee9c4) to head (6ada6dd).
⚠️ Report is 43 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (95ee9c4) and HEAD (6ada6dd). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (95ee9c4) HEAD (6ada6dd)
examples 12 11
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2046      +/-   ##
==========================================
- Coverage   77.44%   69.42%   -8.03%     
==========================================
  Files         522      522              
  Lines       58452    58452              
==========================================
- Hits        45271    40579    -4692     
- Misses      13181    17873    +4692     
Flag Coverage Δ
examples 43.28% <100.00%> (-0.17%) ⬇️
gpu 32.03% <100.00%> (-26.11%) ⬇️
regression 15.07% <100.00%> (+0.07%) ⬆️

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.

@cjluo-nv cjluo-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bot review (bedrock-claude-opus-5) — DM the bot to share feedback.

Small, well-scoped fix: the static registration of the legacy _QuantQwen3VLMoeTextExperts wrapper is now skipped when the class carries _apply_gate (transformers>=5.12's @use_experts_implementation fused layout), so register_fused_experts_on_the_fly claims the module with the generic _QuantFusedExperts. That matches how Mixtral/Qwen3-MoE/MiniMax already flow through _fused_experts_wrapper_class, i.e. no new abstraction is introduced, and CHANGELOG + tests are included. No licensing surface touched. A few things I'd like the owner to eyeball before this lands:

  • Detection hinges on an undocumented transformers internal. The whole fix is not hasattr(Qwen3VLMoeTextExperts, "_apply_gate"). If that private attribute is renamed/removed in a later 5.x while the fused layout stays, the legacy wrapper silently re-registers and users get the same confusing AttributeError: ... has no attribute 'hidden_size'. The file already has a version constant precedent (TRANSFORMERS_VERSION_GE_5_0); consider OR-ing in a version check, and/or adding a guard in _QuantQwen3VLMoeTextExperts._setup (e.g. assert the legacy gate_up_proj orientation / absence of _apply_gate) so a future rename fails with an actionable message instead of the original bug. I can't verify from the repo that _apply_gate is set on the class in 5.12 — that rests entirely on the author's two-environment run.
  • Export path for the new layout is untested here. Coverage stops at calibration (test_quantize_collects_amax). Because the wrapper changes, the exported per-expert naming for Qwen3-VL MoE now goes through _export_fused_experts (per-expert <idx>.gate_proj/...) on >=5.12 vs. the legacy wrapper's unrolled nn.ModuleList naming on <5.12 — i.e. the exported checkpoint layout differs by installed transformers version for the same model. That is probably the intended/mainline behavior (same as Mixtral), but someone with unified-export context should confirm no downstream naming regression, since there is no Qwen3-VL-specific handling in modelopt/torch/export.
  • Tests hard-import transformers.models.qwen3_vl_moe. pyproject.toml allows transformers>=4.56, where that module doesn't exist yet; the three new tests would error (not skip) on the supported floor. A module/class-level pytest.importorskip("transformers.models.qwen3_vl_moe") would keep them hermetic.
  • Minor: test_registration_matches_installed_layout asserts global registry state (registered is None) for the real transformers class, so any other test in the session that quantizes a Qwen3-VL-MoE model on the fly without unregistering would make it fail spuriously. Low risk today, just noting the coupling.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants