Skip to content

Fix Gemma4 CUDA EP: GQA head_dim guard, provider_options, opset lowering - #250

Closed
justinchuby wants to merge 10 commits into
mainfrom
fix-kv-shared-gqa
Closed

Fix Gemma4 CUDA EP: GQA head_dim guard, provider_options, opset lowering#250
justinchuby wants to merge 10 commits into
mainfrom
fix-kv-shared-gqa

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Summary

Fixes Gemma4 CUDA EP inference pipeline:

1. GQA head_dim guard (per-layer)

ORT's GQA CUDA kernel max head_dim is 256. Gemma4 full_attention layers (4, 9, 14) have head_dim=512. Added per-layer gate in Gemma4TextModel.forward() so only compatible sliding_attention layers use GQA.

Model: 12 GQA + 23 Attention (was 15 GQA + 20 Attention)

2. Remove CUDA provider_options from genai_config

Explicit CUDA provider_options in genai_config.json conflicted with GenAI's internal session setup, causing NaN. Empty provider_options: [] lets GenAI handle CUDA configuration internally.

3. Float16 masks for fallback layers

Use float16 additive masks instead of bool masks for KV-shared and head_dim>256 layers. More robust on CUDA.

4. Opset 24→23 lowering

Implements the ort_lower_opset_for_ep flag (was declared but never wired up). Lowers opset from 24 to 23 for non-default EPs to avoid missing CUDA kernel registrations. Workaround until ORT adds opset 24 registrations (PR microsoft/onnxruntime#28365).

Testing

All 1214 build graph tests pass. GenAI generation: 12.5 tok/s with 4 memcpy.

justinchuby and others added 3 commits May 5, 2026 16:06
ORT's GroupQueryAttention CUDA kernel does not support head_dim > 256,
causing illegal memory access for Gemma4 full_attention layers
(head_dim=512). Add per-layer head_dim guard in Gemma4TextModel.forward()
so that only sliding_attention layers (head_dim=256) use GQA, while
full_attention layers (head_dim=512) fall back to standard Attention
with manual RoPE.

This changes the CUDA EP model from 15 GQA + 20 Attention to
12 GQA + 23 Attention, with layers 4, 9, 14 correctly using Attention.
The fallback infrastructure (mask + position_embeddings) is also
triggered for these layers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Two fixes for Gemma4 CUDA EP inference:

1. Remove CUDA provider_options from EP defaults. Explicit CUDA
   provider_options in genai_config.json conflict with GenAI's
   internal session setup (ClearOutput, ReuseEmbeddingsBuffer, etc.),
   causing NaN or crashes for multimodal CUDA models. GenAI's C++
   code handles all CUDA EP configuration internally.

2. Use float16 additive masks for all fallback Attention layers
   (KV-shared and head_dim>256 layers) instead of bool masks. Bool
   masks triggered NaN in ORT's CUDA ConvertAttnMaskToBias path.
   Float16 masks match the working default EP model's behavior.

Tested: CUDA EP model (12 GQA + 23 Attention) generates at 13.5 tok/s
through GenAI with valid output.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
ORT's CUDA EP registers kernels up to opset 23 for standard ops
(Reshape, RMSNormalization, etc.). When the model declares opset 24,
these ops fall to CPUExecutionProvider, creating ~280 MemcpyFromHost
and MemcpyToHost nodes that destroy inference performance.

This implements the ort_lower_opset_for_ep flag (which was declared
in _flags.py but never wired up). When enabled (default), the opset
is lowered from 24 to 23 for all non-default EPs after optimization.

Result: 282 memcpy → 4 memcpy for Gemma4 CUDA EP model.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 0d8a63591acac7

Model Sub-model Changes Status
gemma4 (gemma4) decoder 9 🟡
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 41 🟡
gemma4_text model 9 🟡
gemma4 (gemma4) / decoder — 9 change(s)

Op summary: 125 → 127 nodes

--- base
+++ head
@@ -47,13 +47,14 @@
 MatMul
 Constant
 Reshape
+Cast
 Mul
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
+CastLike
 Reshape
 Attention
 Transpose
@@ -90,13 +91,14 @@
 MatMul
 Constant
 Reshape
+Cast
 Mul
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
+CastLike
 Reshape
 Attention
 Transpose

Added nodes:

  • + Cast
  • + CastLike
  • + Cast
  • + CastLike

Removed nodes:

  • - CastLike
  • - CastLike

Connectivity changes:

  • node[53] Add: input_ids [101, 103] → [102, 103]
  • node[55] Div: input_ids [99, 105] → [100, 105]
  • node[105] RMSNormalization: input_ids [159, 43] → [159, 42]
gemma4 (gemma4) / vision_encoder — 41 change(s)

Op summary: 202 → 204 nodes

--- base
+++ head
@@ -50,14 +50,15 @@
 Reshape
 RMSNormalization
 RMSNormalization
+Cast
 Mul
 Constant
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
+CastLike
 Constant
 Gather
 Constant
@@ -188,15 +189,16 @@
 Constant
 CastLike
 Mul
+Cast
 Mul
 Constant
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
-Transpose
-MatMul
-Constant
-Reshape
+CastLike
+Transpose
+MatMul
+Constant
+Reshape

Added nodes:

  • + Cast
  • + CastLike
  • + Cast
  • + CastLike
  • + Transpose
  • + MatMul
  • + Constant
  • + Reshape

Removed nodes:

  • - CastLike
  • - CastLike
  • - Transpose
  • - MatMul
  • - Constant
  • - Reshape

Modified attributes:

  • node[113] Transpose: perm: [0, 1, 3, 2] → [0, 2, 1, 3]
  • node[148] Constant: value_int: 0 → -1
  • node[193] Constant: value_float: 1e-06 → None, value_ints: None → [-1]

Connectivity changes:

  • node[57] Add: input_ids [113, 115] → [114, 115]
  • node[59] Div: input_ids [108, 117] → [111, 117]
  • node[69] Gather: input_ids [10, 122] → [9, 123]
  • node[70] Gather: input_ids [9, 126] → [10, 123]
  • node[71] Gather: input_ids [10, 126] → [9, 127]
  • node[77] Slice: input_ids [109, 30, 32, 31] → [109, 29, 30, 31]
  • node[78] Slice: input_ids [110, 29, 30, 31] → [109, 30, 32, 31]
  • node[79] Slice: input_ids [110, 30, 32, 31] → [110, 29, 30, 31]
  • node[80] Slice: input_ids [135, 29, 33, 31] → [110, 30, 32, 31]
  • node[81] Slice: input_ids [135, 33, 30, 31] → [136, 29, 33, 31]
  • node[85] Mul: input_ids [142, 132] → [136, 132]
  • node[88] Slice: input_ids [136, 33, 30, 31] → [137, 29, 33, 31]
  • node[92] Mul: input_ids [149, 134] → [137, 134]
  • node[96] Slice: input_ids [137, 33, 30, 31] → [138, 29, 33, 31]
  • node[100] Mul: input_ids [157, 132] → [138, 132]
  • node[103] Slice: input_ids [138, 33, 30, 31] → [139, 29, 33, 31]
  • node[107] Mul: input_ids [164, 134] → [139, 134]
  • node[111] Transpose: input_ids [168] → [154]
  • node[112] Transpose: input_ids [118] → [169]
  • node[113] Transpose: input_ids [170] → [119]
  • node[153] Equal: input_ids [210, 206] → [209, 207]
  • node[174] Div: input_ids [230, 231] → [227, 232]
  • node[179] Div: input_ids [222, 231] → [221, 232]
  • node[190] Mul: input_ids [248, 248] → [246, 248]
gemma4_text / model — 9 change(s)

Op summary: 127 → 129 nodes

--- base
+++ head
@@ -49,13 +49,14 @@
 MatMul
 Constant
 Reshape
+Cast
 Mul
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
+CastLike
 Reshape
 Attention
 Transpose
@@ -92,13 +93,14 @@
 MatMul
 Constant
 Reshape
+Cast
 Mul
 ReduceMean
 Constant
-CastLike
 Add
 Sqrt
 Div
+CastLike
 Reshape
 Attention
 Transpose

Added nodes:

  • + Cast
  • + CastLike
  • + Cast
  • + CastLike

Removed nodes:

  • - CastLike
  • - CastLike

Connectivity changes:

  • node[55] Add: input_ids [104, 106] → [105, 106]
  • node[57] Div: input_ids [102, 108] → [103, 108]
  • node[107] RMSNormalization: input_ids [162, 44] → [162, 43]

Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 0d8a63591acac7

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 66 66 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 53 53 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 61 61 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 59 59 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 61 61 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 408 408 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.14286% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/mobius/models/gemma4.py 71.42% 4 Missing ⚠️
src/mobius/_builder.py 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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.

Pull request overview

This PR targets improved Gemma4 performance/correctness on ORT CUDA EP by (1) gating GQA usage per-layer based on head_dim compatibility, (2) removing/avoiding explicit CUDA provider_options in generated GenAI configs, and (3) wiring an opset 24→23 lowering workaround to prevent CUDA kernel fallback/memcpy explosions.

Changes:

  • Add per-layer GQA compatibility guard in Gemma4 text decoder and update fallback mask path.
  • Adjust ORT GenAI provider-options generation to return [] when no provider-specific options are needed (notably for CUDA defaults).
  • Implement flags.ort_lower_opset_for_ep in the build pipeline by lowering the default-domain opset import from 24 to 23 for non-default EPs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/mobius/models/gemma4.py Adds head_dim-based per-layer fallback from GQA to standard Attention and switches fallback masks to additive bias.
src/mobius/integrations/ort_genai/ep_config.py Makes make_provider_options() return an empty list when no options are present to avoid overriding GenAI internal CUDA setup.
src/mobius/_execution_providers.py Removes CUDA default provider_options from the EP registry to prevent GenAI conflicts.
src/mobius/_builder.py Wires opset-lowering flag into build_from_module() by mutating model opset imports post-optimization.

Comment thread src/mobius/integrations/ort_genai/ep_config.py Outdated
Comment thread src/mobius/_builder.py
Comment thread src/mobius/models/gemma4.py Outdated
Comment thread src/mobius/models/gemma4.py
Comment thread src/mobius/_execution_providers.py
@justinchuby
justinchuby marked this pull request as draft May 5, 2026 18:14
Tests now accept empty provider_options for CUDA EP, matching the
change where explicit CUDA provider_options were removed to avoid
conflicting with GenAI's internal EP configuration.

118 ort_genai tests pass.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
ORT now supports head_dim up to 512 in the GQA CUDA kernel.
Remove the _MAX_GQA_HEAD_DIM guard that forced full-attention
layers (head_dim=512) to fall back to standard Attention.

All non-shared layers now use GroupQueryAttention regardless of
head_dim. Only KV-shared layers still fall back to Attention
(they borrow K,V and have no own KV cache).

Signed-off-by: Justin Chu <justinchu@microsoft.com>
assert len(provider_opts) == 1
assert "cuda" in provider_opts[0]
assert isinstance(provider_opts, list)
pass # CUDA provider_options may be empty
Comment thread src/mobius/integrations/ort_genai/genai_config_test.py Fixed
GenAI derives its providers list from provider_options names in
genai_config.json (config.cpp:1763-1764). Empty provider_options
means GenAI uses CPU-only, even for CUDA models.

Revert the 'empty for CUDA' behavior — always emit [{"cuda": {}}]
so GenAI registers the CUDA execution provider. The empty options
dict is fine — GenAI handles CUDA configuration internally.

118 ort_genai tests pass.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
assert len(provider_options) == 1, f"{block} missing CUDA provider options"
assert "cuda" in provider_options[0], f"{block} has wrong EP in provider_options"
assert isinstance(provider_options, list), f"{block} invalid provider_options"
pass # CUDA provider_options may be empty
Comment thread src/mobius/integrations/ort_genai/ep_config.py Outdated
Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Member Author

@titaiwangms could you help with reviewing the logic that handles the attention bias? I wasn't sure it this is the right fix.

justinchuby and others added 2 commits May 5, 2026 18:48
Gemma4's parameterless V normalization (v / sqrt(mean(v²) + ε))
squared FP16 values directly via op.Mul(v, v). V projection outputs
can reach ~888, and 888² = 788,544 which overflows FP16 max (65504),
producing inf → mean(inf) → sqrt(inf) → v/inf = 0. This caused
all-zero V outputs on CUDA (CPU uses FP32 accumulation internally).

Fix: Cast to FP32 before squaring, compute the full RMSNorm in FP32,
then CastLike back to the input dtype. Applied to:
- _Gemma4ScaleFreeRMSNorm.forward (vision encoder, projector norms)
- Gemma4Attention.forward inline V norm (GQA path)
- Gemma4Attention.forward inline V norm (non-GQA path)

Result: F16 CUDA inference works — 151.5 tok/s on H200 (was NaN).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
Set stash_type=1 (FLOAT) on all RMSNormalization ops to ensure the
variance computation uses FP32 internally when input is FP16/BF16.
This prevents potential overflow when squaring large values.

Applied to apply_rms_norm() and OffsetRMSNorm (GatedRMSNorm already
had stash_type=1).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Justin Chu <justinchu@microsoft.com>
@justinchuby
justinchuby force-pushed the fix-kv-shared-gqa branch from a7708cc to ea4e95c Compare May 5, 2026 18:48
@justinchuby

Copy link
Copy Markdown
Member Author

Split into individual PRs: #253 (V norm FP16 fix), #254 (opset lowering), #255 (provider_options + float masks).

@justinchuby justinchuby closed this May 5, 2026
@justinchuby
justinchuby deleted the fix-kv-shared-gqa branch May 7, 2026 01:02
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