Skip to content

Add Qwen3.5 Quark GPTQ export support for hybrid attention models - #2545

Open
Vishal Jain (VishalX) wants to merge 3 commits into
microsoft:mainfrom
VishalX:quark/gptq-qwen3.5-builder
Open

Vishal Jain (VishalX) wants to merge 3 commits into
microsoft:mainfrom
VishalX:quark/gptq-qwen3.5-builder

Conversation

@VishalX

Copy link
Copy Markdown
Contributor

Summary

Enables end-to-end ONNX export of Qwen3.5 models when the source weights are pre-quantized with AMD Quark (GPTQ + pack_method=reorder). Qwen3.5 uses a hybrid architecture that interleaves standard full-attention layers with GatedDeltaNet linear-attention layers.

Two changes required for quantized hybrid checkpoints:

  1. make_matmul() returns the actual emitted node name — for pre-quantized weights this is <basename>NBits rather than <basename> — but all linear-attention call sites discarded the return value and used the original basename string to construct downstream output references. This produced a silently disconnected ONNX graph for quantized models.
  2. The quantized weight loader had no representation for linear-attention layers. Loading a hybrid Qwen3.5 checkpoint raised NotImplementedError on every linear_attn.* weight key.

Additionally, the MTP head build is now skipped when a quantization_config is present on the HuggingFace config, since pre-quantized checkpoints do not include MTP weights while mtp_num_hidden_layers in the config would otherwise cause the builder to attempt loading them.

Tested with Qwen3.5-4B and Qwen3.5-9B GPTQ INT4 checkpoints. Text-only and vision-language inference verified with latest ONNX Runtime.

Changes

src/python/py/models/builders/qwen.py

Fix make_matmul() return-value capture in linear-attention projections

make_matmul() emits a MatMulNBits node for pre-quantized weights whose name has an NBits suffix (e.g. /model/layers.0/linear_attn/qkv_proj/MatMulNBits). All five call sites in the Qwen3.5 linear-attention builder were discarding the return value, then using the original basename to form output_0 references — pointing to a non-existent node and corrupting the graph. Fixed in:

  • make_linear_attention_input_proj — 4 sites: in_proj_qkv, in_proj_z, in_proj_b, in_proj_a
  • make_linear_attention_output_proj — 1 site: out_proj

Skip MTP head for pre-quantized checkpoints

Qwen3_5ForConditionalGeneration configs carry mtp_num_hidden_layers: 1, but Quark/GPTQ checkpoints do not include MTP weights. The builder now checks for quantization_config on the HuggingFace config and skips the MTP head build when present, avoiding a ValueError at weight loading time.

src/python/py/models/loaders/base.py

Add QuantizedLinearAttention and wire it into the loader pipeline

  • New QuantizedLinearAttention class — holds the five quantized projections of a GatedDeltaNet layer (in_proj_qkv, in_proj_z, in_proj_a, in_proj_b, out_proj as QuantizedTensorModule) plus non-quantized fields (conv1d, norm as TensorModule; plain tensors A_log, dt_bias).

  • linear_attn field on QuantizedDecoderLayer — every decoder layer now holds a QuantizedLinearAttention() instance alongside the existing self_attn and mlp. It remains empty for full-attention layers and is populated for linear-attention layers.

  • 20 new regex handlers in QuantizedModel.__init__ — covering qweight, scales, qzeros, g_idx for each of the five quantized projections, plus A_log, dt_bias, conv1d.weight, and norm.weight. These slot into the existing tensor-map dispatch mechanism with no changes required to QuarkModel.

  • quantized_tensor_modules() and repack_quantized_tensors() updated — both iteration passes now include layer.linear_attn alongside layer.self_attn and layer.mlp, so unpack/repack and property-setting cover linear-attention weights automatically.

@VishalX
Vishal Jain (VishalX) requested a review from a team as a code owner September 10, 2026 12:45
Copilot AI lite review requested due to automatic review settings September 10, 2026 12:45
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI 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.

🟡 Changes recommended

The new MTP skip condition is overly broad (based on attribute presence rather than quant_method), risking unintended disabling of MTP export for other quantized flows.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Enables ONNX export for Qwen3.5 hybrid-attention models when using pre-quantized (Quark/GPTQ) checkpoints by fixing linear-attention graph wiring and extending the quantized weight loader to recognize linear_attn.* tensors.

Changes:

  • Capture and use make_matmul()’s returned node name for all Qwen3.5 linear-attention projections to prevent disconnected output_0 references for MatMulNBits nodes.
  • Extend the quantized loader’s tensor-key dispatch to support linear_attn projections and associated non-quantized tensors, and include them in quantized-module iteration/repacking.
  • Skip building the MTP head when quantization_config is present (intended to avoid missing-weight failures for certain pre-quantized checkpoints).
File summaries
File Description
src/python/py/models/loaders/base.py Adds QuantizedLinearAttention, wires linear_attn.* keys into quantized tensor dispatch, and includes linear-attention modules in quantized iteration/repack passes.
src/python/py/models/builders/qwen.py Fixes linear-attention matmul name propagation for quantized exports and introduces logic to skip MTP head for quantized configs.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/python/py/models/builders/qwen.py Outdated
Comment thread src/python/py/models/loaders/base.py
@VishalX

Copy link
Copy Markdown
Contributor Author

kunal-vaishnavi

- builders/qwen.py: capture make_matmul() return value at all five
  linear-attention call sites so MatMulNBits nodes are correctly wired;
  same fix for out_proj in make_linear_attention_output_proj; skip MTP
  head when checkpoint has a quantization_config (pre-quantized
  checkpoints do not include MTP weights)
- loaders/base.py: add QuantizedLinearAttention class and linear_attn
  field on QuantizedDecoderLayer; add 20 regex handlers for
  model.layers.N.linear_attn.* weight keys; include linear_attn in
  quantized_tensor_modules() and repack_quantized_tensors()

Co-Authored-By: Claude <noreply@anthropic.com>
- builders/qwen.py: gate MTP-head skip on quant_method == "quark" rather
  than mere presence of quantization_config, so other quantized flows
  (e.g. modelopt) are not affected
- test_quantized_model.py: add four unit tests for the linear_attn
  tensor-key dispatch added in the previous commit; tests are quantizer-
  agnostic and cover quantized projections, plain tensors (A_log/dt_bias/
  conv1d/norm), weight_scale alias (scales vs weight_scale), and key
  isolation from self_attn patterns

Co-Authored-By: Claude <noreply@anthropic.com>
GatedDeltaNet decay/beta gate projections (in_proj_b, in_proj_a) must
remain dense because their outputs feed exp() and softplus() in the
recurrence; quantization error there would be amplified and degrade
quality over long sequences.

c6f7afc added require_dense_linear_attention_gate() to the builder to
enforce this. For Quark GPTQ checkpoints that supply these weights as
pre-quantized int4, add dequantize_gate_to_dense() to the base loader
and call it for in_proj_b/in_proj_a in repack_quantized_tensors(),
following the same pattern as ModeloptModel.make_dense_linear_module().

Co-Authored-By: Claude <noreply@anthropic.com>
@kunal-vaishnavi

Copy link
Copy Markdown
Contributor

/azp run macOS arm64 Build

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).


qkv_name = f"{basename}/qkv_proj/MatMul"
self.make_matmul(attention.in_proj_qkv, qkv_name, root_input)
qkv_name = self.make_matmul(attention.in_proj_qkv, f"{basename}/qkv_proj/MatMul", root_input)

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.

Why do we need these changes to store the returned output from make_matmul?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

make_matmul returns the actual node name, and for quantized weights that name differs from the basename we pass in. make_matmul_nbits names the node f"{basename}NBits", so the output tensor is .../MatMulNBits/output_0, not .../MatMul/output_0.

The old linear-attention code discarded the return value and hardcoded f"{basename}/.../MatMul/output_0". That worked for dense MatMul, but for a pre-quantized (Quark/GPTQ) export the downstream Transpose/Split/Reshape nodes referenced a tensor that did not exist, leaving a disconnected output_0. Using the returned name fixes that and matches how the rest of this file consumes make_matmul (e.g. the shared-expert gate and the MTP fc).

@VishalX

Copy link
Copy Markdown
Contributor Author

kunal-vaishnavi let me know if this needs a rebase

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