Skip to content

Add Qwen3 model type support to Python transformer optimizer - #27556

Merged
tianleiwu merged 1 commit into
microsoft:mainfrom
Rishi-Dave:rishidave/feat/qwen3-optimizer-support
Mar 13, 2026
Merged

Add Qwen3 model type support to Python transformer optimizer#27556
tianleiwu merged 1 commit into
microsoft:mainfrom
Rishi-Dave:rishidave/feat/qwen3-optimizer-support

Conversation

@Rishi-Dave

Copy link
Copy Markdown
Contributor

Description

Add qwen3 to the Python transformer optimizer's model type registry, enabling graph optimization for Qwen3 models (e.g., Qwen3-Embedding-0.6B, ranked 4th on MTEB).

Motivation

Fixes #25083

Running optimum-cli export onnx --optimize O3 on Qwen3 models fails with:

ValueError: Unsupported model type: qwen3

This PR resolves that by registering the model type and fixing a fusion gap that blocked normalization fusions.

Changes

Model type registration (optimizer.py):

  • Add "qwen3": (Gpt2OnnxModel, "pytorch", 0) to MODEL_TYPES
  • Uses Gpt2OnnxModel (not BertOnnxModel) because its fuse_attention() calls FusionRotaryAttention, which searches on SkipSimplifiedLayerNormalization anchors — needed for RMSNorm-based models

Fusion option defaults (fusion_options.py):

  • Disable EmbedLayerNormalization (decoder-only, no BERT-style embedding)
  • Set AttentionMaskFormat.NoMask (causal masking is implicit)

SkipLayerNormalization fusion fallback (fusion_skiplayernorm.py):

  • When symbolic shape inference fails (common with dynamo-exported models), the fusion previously returned early, skipping all SkipLayerNormalization / SkipSimplifiedLayerNormalization fusions
  • Now it falls through with the safe default skip_index=1 (second Add input is skip), since both inputs are already verified as non-initializer dynamic tensors (lines 88-90)
  • This enables SkipSimplifiedLayerNormalization fusion on Qwen3 models where shape inference fails

Test (test_attention_fusion.py, qwen3_model_generator.py):

  • Synthetic Qwen3 decoder layer graph with pre-attention RMSNorm, Q/K/V projections, QK-Norm, simplified attention, output projection, residual connection, and post-attention RMSNorm
  • Verifies 3× SimplifiedLayerNormalization (pre-attn, Q-norm, K-norm) + 1× SkipSimplifiedLayerNormalization (residual + post-attn RMSNorm)

Verified on real model: Running the optimizer on an exported Qwen3-Embedding-0.6B (2-layer) reduces nodes from 208 → 150 (28% reduction). All 9 RMSNorm patterns fuse correctly: 5× SimplifiedLayerNormalization + 4× SkipSimplifiedLayerNormalization.

Scope note: Full RotaryEmbedding + MultiHeadAttention fusion for Qwen3's dynamo-exported graphs requires additional pattern matching work (static Slice indices, on-the-fly sin/cos computation, QK-Norm in Q/K paths, GQA expansion). That will be addressed in a follow-up PR.

Test Plan

  • test_attention_fusion.py::TestFusion::test_qwen3_normalization_fusion passes
  • All 14 existing tests in test_attention_fusion.py pass (no regressions)
  • All 4 tests in test_optimizer_huggingface_bert.py pass (bert, distillbert, roberta, xlm_roberta — no regressions from the SkipLayerNorm fallback change)
  • lintrunner -a clean

Register qwen3 in MODEL_TYPES (using Gpt2OnnxModel for RoPE/RMSNorm
support) with appropriate fusion option defaults: disable
EmbedLayerNorm and use NoMask for this decoder-only architecture.

Fix SkipLayerNormalization fusion fallback when symbolic shape inference
fails. Previously fusion was skipped entirely; now it proceeds with the
safe default skip_index=1 since both Add inputs are already verified as
non-initializer dynamic tensors.

Add Qwen3 test graph generator and unit test verifying
SimplifiedLayerNormalization and SkipSimplifiedLayerNormalization fusion
on a synthetic Qwen3 decoder layer.
@tianleiwu

Copy link
Copy Markdown
Contributor

/azp run Linux QNN CI Pipeline, Win_TRT_Minimal_CUDA_Test_CI, Windows ARM64 QNN CI Pipeline, Windows GPU Doc Gen CI Pipeline

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 4 pipeline(s).

@tianleiwu
tianleiwu merged commit 2927257 into microsoft:main Mar 13, 2026
109 of 113 checks passed
tianleiwu added a commit that referenced this pull request Mar 13, 2026
…sion.py (#27642)

# Description

This PR addresses a build error and subsequent test failures related to
recent changes in GridSample and the transformer optimizer. Related PRs:
#27201, #27556.

## Changes

### 1. Fix GridSample Build Error
- Removed an unused local variable `mode_str` in
`onnxruntime/core/providers/cuda/tensor/grid_sample.cc` that was causing
a warning (treated as error) about shadowing a member variable.
- Ref:
[`grid_sample.cc`](https://github.com/microsoft/onnxruntime/blob/c979a2407f/onnxruntime/core/providers/cuda/tensor/grid_sample.cc#L54)

### 2. Update GridSample Tests
- Updated
`onnxruntime/test/providers/cpu/tensor/grid_sample_test_custom.inc` to
use default execution providers in `RunTests` instead of a hardcoded
opset version, ensuring compatibility across different environments.

### 3. Revert Transformer Fusion Fallback
- Reverted a recent change in
`onnxruntime/python/tools/transformers/fusion_skiplayernorm.py` that
enabled a fallback for `SkipLayerNormalization` fusion when symbolic
shape inference fails.
- This revert was necessary to avoid regressions in GPT-2 tests where
model definitions contain typos that intentionally (or coincidentally)
break shape inference.
- Ref:
[`fusion_skiplayernorm.py`](https://github.com/microsoft/onnxruntime/blob/c979a2407f/onnxruntime/python/tools/transformers/fusion_skiplayernorm.py#L113)

### 4. Restore Transformer Test Parity
- Updated
`onnxruntime/test/python/transformers/test_attention_fusion.py`
specifically `test_qwen3_normalization_fusion` to match the expected
node counts after reverting the fusion fallback.
- Ref:
[`test_attention_fusion.py`](https://github.com/microsoft/onnxruntime/blob/c979a2407f/onnxruntime/test/python/transformers/test_attention_fusion.py#L398)

## Verification

- `build_cuda.sh` completed successfully.
- `onnxruntime/test/python/transformers/test_attention_fusion.py` passes
with "OK".
- `lintrunner -a` reports no issues.
tianleiwu pushed a commit that referenced this pull request Mar 16, 2026
### Description

Extend `FusionRotaryEmbeddings` to handle Qwen3's on-the-fly rotary
position embedding computation, where cos/sin values are computed from
`inv_freq` at runtime instead of being looked up from a pre-computed
cache.

This is a follow-up to #27556 (Qwen3 basic model type support). Depends
on #27556.

Part of #25083.

### Motivation and Context

Qwen3 models (ranked 4th on MTEB) compute RoPE differently from existing
supported models (Phi, LLaMA, etc.). Instead of pre-computing cos/sin
caches and looking them up via `Gather(cache, position_ids)`, Qwen3
computes them on-the-fly:

```python
freqs = inv_freq_expanded @ position_ids_expanded   # MatMul
emb = torch.cat((freqs, freqs), dim=-1)             # Concat
cos = emb.cos() * attention_scaling                  # Cos, Mul
sin = emb.sin() * attention_scaling                  # Sin, Mul
```

Additionally, TorchScript exports of Qwen3 insert `Cast` nodes in the
`rotate_half` pattern (from `torch.floor_divide` tracing), which the
existing path patterns don't account for.

### Changes

**`fusion_rotary_attention.py`:**
- Add Cast-tolerant `rotate_half` path patterns
(`rotate_half_x2_path_2_3`, `_2_4`, `rotate_half_x1_path_2_3`, `_2_4`)
that allow 1-2 Cast nodes between Unsqueeze and Div in the dynamic Slice
index computation
- Add `sin_path_5` / `cos_path_5` patterns matching the on-the-fly
computation: `MatMul → Transpose → Concat → Cos/Sin → Mul(scaling) →
Unsqueeze → Mul`, with optional Cast variant (the optimizer's earlier
Cast fusion pass may remove the Cast)
- Add `create_cos_sin_cache_from_on_the_fly_rope()` helper that extracts
`inv_freq` weights, computes cos/sin caches as model initializers, and
traces `position_ids` from the graph
- Handle per-layer vs shared node removal correctly (only remove
per-layer Unsqueeze/outer Mul nodes; shared MatMul/Cos/Sin nodes are
pruned automatically by the optimizer)

**`qwen3_model_generator.py`:**
- Add `include_rope=True` parameter to `create_qwen3_decoder_layer()`
- Generate full on-the-fly RoPE computation graph: `inv_freq`
initializer, `position_ids` input, MatMul/Transpose/Concat/Cos/Sin/Mul
nodes, and `rotate_half` pattern with dynamic Slice indices (including
Cast nodes from floor division)
- Apply RoPE to both Q and K paths

**`test_attention_fusion.py`:**
- Add `test_qwen3_rotary_embedding_fusion` verifying 2 RotaryEmbedding
nodes are fused along with 3 SimplifiedLayerNormalization and 1
SkipSimplifiedLayerNormalization

### Verification

- **Unit tests**: All 15 `test_attention_fusion.py` tests pass (14
existing + 1 new)
- **Real model**: Verified on Qwen3-Embedding-0.6B (28 layers): 56
RotaryEmbedding nodes fused (28 layers × 2 per layer for Q and K),
reducing total node count from 7416 → 4661 (37% reduction)
- **No regressions**: All changes are additive alternative path patterns
— existing models that use dynamic Slice indices or cache-based RoPE
never hit the new paths
- **Lint**: `lintrunner -a` clean on all modified files
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.

[Feature Request] No Support of Graph optimization for Qwen3 Embedding model

2 participants