[WebGPU] Fuse Q/K RMSNorm into GroupQueryAttention for Qwen3-style models - #28484
Conversation
Adds GroupQueryAttentionPreNormFusion optimizer that folds the pre-rotary Reshape -> SimplifiedLayerNormalization -> Reshape pattern on Q and K into kMSDomain GroupQueryAttention via new optional q_norm_weight/k_norm_weight inputs and an epsilon attribute. The WebGPU GQA decode fast path applies the RMSNorm inline in FusedQKRotaryEmbedding. Includes optimizer unit test.
Move the LayerNormProgram configuration/dispatch logic out of LayerNorm<>::ComputeInternal into a free RunLayerNormProgram helper exposed by layer_norm.h, so callers other than the LayerNorm kernel (e.g. the GroupQueryAttention prefill fallback that runs SimplifiedLayerNorm on Q and K when fused per-head RMSNorm is requested) do not need to duplicate the program-setup code. No behavior change. Emitted WGSL and dispatch are byte-identical.
Scope the fusion to the WebGPU EP only (drop JSEP from the compatible-EP set) and add explicit gates so the rewrite cannot land on configurations the WebGPU kernel does not implement: * do_rotary must be 1 -- the fused decode prologue is folded into the rotary kernel and the prefill fallback also runs only on the do_rotary=1 path. * Slots 1 (key) and 2 (value) must be wired -- excludes the packed-QKV form, which the WebGPU fused prologue does not support. * Hardened the already-fused early-skip to cover both inputs 14 and 15. Add unit tests covering each new gate (do_rotary=0 skip, packed-QKV skip, already-fused skip, JSEP EP skip) and update the existing fixture to register the optimizer with WebGPU only and to set do_rotary=1 on the GQA node.
Add defense-in-depth rejections in the CPU and CUDA contrib GroupQueryAttention kernels and in the JSEP TS dispatch so a hand-authored model that wires inputs 14/15 on those EPs fails fast instead of silently dropping the per-head Q/K RMS normalization. Each EP uses an EP-specific error message (CPU / CUDA / JSEP) so the diagnostic identifies which kernel rejected the node. The WebGPU EP is the only runtime that implements the prologue; the GroupQueryAttentionPreNormFusion optimizer pass is scoped accordingly.
CheckUnfusedGraph asserts that slot 14 on the GQA node is empty, but the SkipsAlreadyFusedNode test deliberately pre-wires slot 14 to exercise the already-fused early-skip path. Replace the shared checker with a local one that only verifies the surrounding SimplifiedLayerNormalization and Reshape ops were not removed.
There was a problem hiding this comment.
Pull request overview
This PR extends the MS-domain GroupQueryAttention contrib op to optionally fuse a Qwen3-style per-head Q/K RMSNorm prologue (via new optional inputs q_norm_weight/k_norm_weight and attribute qk_norm_epsilon), and adds a Level 2 graph transformer (GroupQueryAttentionPreNormFusion) to fold the exported Reshape -> SimplifiedLayerNormalization -> Reshape pattern into a single GroupQueryAttention node for the WebGPU EP.
Changes:
- Add
GroupQueryAttentionPreNormFusionoptimizer pass (WebGPU-only) plus a dedicated unit test. - Extend
GroupQueryAttentionschema with optional norm inputs and epsilon attribute; add EP-side defensive rejections where unsupported. - Implement WebGPU runtime support: decode path fuses norm into
FusedQKRotaryEmbedding, and prefill path falls back to standalone simplified layer norm dispatches using sharedRunLayerNormProgram.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/optimizer/group_query_attention_pre_norm_fusion_test.cc | New unit tests covering positive fusion and key rejection/gating scenarios |
| onnxruntime/core/providers/webgpu/nn/layer_norm.h | Exposes RunLayerNormProgram() helper for shared LayerNorm dispatch setup |
| onnxruntime/core/providers/webgpu/nn/layer_norm.cc | Refactors LayerNorm compute to reuse RunLayerNormProgram() |
| onnxruntime/core/providers/webgpu/math/unary_elementwise_ops.h | Namespace closing indentation fix |
| onnxruntime/core/optimizer/group_query_attention_pre_norm_fusion.h | Declares new Level 2 graph transformer for Q/K pre-norm fusion |
| onnxruntime/core/optimizer/group_query_attention_pre_norm_fusion.cc | Implements the fusion matcher and graph rewrite |
| onnxruntime/core/optimizer/graph_transformer_utils.cc | Registers the transformer (WebGPU-only compatibility set) |
| onnxruntime/core/graph/contrib_ops/bert_defs.cc | Extends GroupQueryAttention schema with new inputs/attribute |
| onnxruntime/contrib_ops/webgpu/bert/rotary_embedding.h | Adds has_qk_norm and new uniforms for fused norm+RoPE shader |
| onnxruntime/contrib_ops/webgpu/bert/rotary_embedding.cc | Implements fused RMSNorm+RoPE shader logic and updated IO/uniform wiring |
| onnxruntime/contrib_ops/webgpu/bert/group_query_attention.h | Adds kernel attribute storage for qk_norm_epsilon |
| onnxruntime/contrib_ops/webgpu/bert/group_query_attention.cc | Implements decode fused path and prefill fallback using LayerNorm helper |
| onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc | Rejects models that provide unsupported norm inputs |
| onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc | Rejects models that provide unsupported norm inputs |
| js/web/lib/wasm/jsep/webgpu/ops/group-query-attention.ts | Rejects models that provide unsupported norm inputs in JSEP implementation |
Comments suppressed due to low confidence (1)
onnxruntime/core/optimizer/group_query_attention_pre_norm_fusion.cc:50
- The pattern comment has the Reshape order reversed. The code matches: projection -> inner Reshape -> SLN -> outer Reshape -> consumer. Please fix the comment so it matches the actual matcher, otherwise future modifications are likely to break the intended semantics.
// Walks back from `consumer` via input slot `consumer_input_index` and matches:
// producer_proj -> Reshape(reshape_outer) -> SimplifiedLayerNormalization(sln) -> Reshape(reshape_inner) -> consumer
// On success returns true and fills the out-pointers. Each intermediate node must have a single
// consumer (the next op in the chain) and must not be a graph output.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ect WebGPU-only scope
…test MakeInput<int>(shape, min, max) calls Uniform(min, max - 1) which asserts when min == max. Switch the seqlens_k and total_seq_len inputs to the explicit-data overload so the (0,0) and (1,1) ranges are no longer treated as integer distributions.
- [WebGPU] Validate q/k_norm_weight is 1-D of length head_size in the GQA kernel so a hand-authored model with the wrong shape fails with INVALID_ARGUMENT instead of reading wrong offsets. - [Optimizer] Require SimplifiedLayerNormalization input/scale/output element types to match before fusing, since the fused GQA input slots reuse the projection's element type (T) and a mixed-type SLN would change the node's type constraints. - [JSEP] Reject the GQA node when q_norm_weight or k_norm_weight is present regardless of rank (including scalars), instead of only checking dims.length > 0.
199d5f7 to
1ee43af
Compare
- [WebGPU] Validate q/k_norm_weight is 1-D of length head_size in the GQA kernel so a hand-authored model with the wrong shape fails with INVALID_ARGUMENT instead of reading wrong offsets. - [Optimizer] Require SimplifiedLayerNormalization input/scale/output element types to match before fusing, since the fused GQA input slots reuse the projection's element type (T) and a mixed-type SLN would change the node's type constraints. - [JSEP] Reject the GQA node when q_norm_weight or k_norm_weight is present regardless of rank (including scalars), instead of only checking dims.length > 0.
## Summary This PR updates WebGPU release behavior to improve default runtime performance while preserving explicit user control over validation. In non-debug builds, the provider enables Dawn's `skip_validation` toggle by default **only when `validationMode` is not explicitly configured or when explicitly configured to a mode weaker than strict validation**. If `validationMode` is explicitly set to a stricter validation level (> disabled), that setting is honored and `skip_validation` is not forced. Debug build behavior is unchanged. ## Motivation Release users generally prioritize performance, while advanced users need predictable diagnostic control when they explicitly request a stronger validation mode. This change provides a release-friendly default without removing configurability or forcing performance penalties when users explicitly enable validation for debugging. ## Behavior Matrix | Build Type | validationMode Explicitly Set | Behavior | |---|---|---| | Debug | No | No behavior change | | Debug | Yes | Respect explicit validationMode | | Release | No | Enable skip_validation by default | | Release | Yes (validationMode ≤ disabled) | Enable skip_validation | | Release | Yes (validationMode > disabled) | Respect explicit validationMode; do not force skip_validation | ## Implementation - Track whether `validationMode` was explicitly provided during provider option parsing. - Plumb explicitness into WebGpuContext initialization. - In non-debug builds: apply `skip_validation` default only when `validationMode` is absent or weaker than strict validation; otherwise honor explicit choice. ## Why this approach - Improves release-path defaults for performance-sensitive scenarios. - Preserves explicit override semantics for diagnostics/troubleshooting. - Respects user intent: if they explicitly enable strong validation, that choice is honored. - Keeps the change narrowly scoped and low risk. ## Related PRs - [PR](#28581) to use direct dispatch instead of indirect dispatch in the FA kernels to work-around the Dawn inefficiency with basic valiation mode - This is no longer needed - [PR](microsoft/onnxruntime-genai#2177) fix to create the dummy ORT session with user provided device knobs is still needed for other knobs (validation mode is just one such knob) ## Motivation and Context Perf improvement on **Vulkan backend** for **Qwen3-1.7B** model on a **Windows** machine with **RTX 5060Ti** card <img width="387" height="200" alt="image" src="https://github.com/user-attachments/assets/f2d79200-f7cf-48cd-973c-3ba6bdf3c5da" /> The default state for validationMode to disabled greatly helps deocde TPS on Vulkan as it helps reduce the indirect dispatch inefficiency on Vulkan. There is a very small marginal improvement on D3D12 as well although not as marked as the inefficiency with indirect dispatch on D3D12 backend is greatly helped by D3D12's aggressive sub-allocator pooling (something Vulkan backend lacks). NOTE: The "Branch" perf improvement numbers also includes the fusion in #28484 but the fusion's decode TPS gains is under 10%. The rest are contributed by the validationMode default. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
qjia7
left a comment
There was a problem hiding this comment.
LGTM.
It would be better to add a WebGPU functional test with q_norm_weight and k_norm_weight wired up, covering the do_rotary=1 decode path and validating the expected output for sanity.
Thanks. Added more parity tests at the fusion level and the GQA op level. |
|
It seems that your new added test fails on MacOS. |
Yes- It is strange- |
It should be passing now @qjia7 |
Description
Adds a per-head Q/K RMS normalization prologue to the WebGPU
GroupQueryAttentioncontrib op so that Qwen3-style models can fold the standalone
SimplifiedLayerNormalizationdispatches on Q and K into the attention kernel.The MS-domain
GroupQueryAttentionschema gains two optional inputs and oneattribute:
q_norm_weight(input 14, optional, shape(head_size,))k_norm_weight(input 15, optional, shape(head_size,))qk_norm_epsilon(FLOAT attribute, default1e-6)A new Level 2 optimizer pass
GroupQueryAttentionPreNormFusionrewrites theReshape -> SimplifiedLayerNormalization -> Reshape -> GroupQueryAttentionpattern produced by the Qwen3 ONNX export into a single
GroupQueryAttentionnode that carries the norm weights directly. The pass is scoped to the WebGPU
EP only.
WebGPU runtime paths
FusedQKRotaryEmbedding.SimplifiedLayerNormdispatches into scratchqNorm/kNormtensors, then runs the unfusedFusedQKRotaryEmbedding. Matches the pre-fusion graph timing exactly so prefill cannot regress.Gating
The optimizer only rewrites nodes that the WebGPU kernel can handle:
do_rotary == 1(the fused decode path runs inside the rotary kernel; theprefill fallback also runs only on the
do_rotary=1branch)axis=-1, and the surrounding reshapes must collapse to(... head_size)/expand back to
(... hidden_size).Defense-in-depth on other EPs
The CPU, CUDA, and JSEP
GroupQueryAttentionkernels reject hand-authoredmodels that wire inputs 14/15 with EP-specific error messages, since none of
those runtimes implement the prologue:
GroupQueryAttention (CPU): q_norm_weight / k_norm_weight inputs are not supported...GroupQueryAttention (CUDA): q_norm_weight / k_norm_weight inputs are not supported...GroupQueryAttention (JSEP): q_norm_weight / k_norm_weight inputs are not supported...Motivation and Context
Perf improvement on D3D12 backend for Qwen3-1.7B model on a Windows machine with RTX 5060Ti card