Skip to content

Commit dbcc8b6

Browse files
justinchubyCopilotCopilotgithub-code-quality[bot]
authored
Fix Gemma4 CUDA EP support and audio encoder accuracy (#172)
## Gemma4 multimodal CUDA support & audio encoder fixes ### Changes **CUDA EP compatibility** - Lower ONNX opset 24 → 23 for CUDA/TRT EPs (ORT ≤1.24.x lacks opset-24 kernels). Gated by `ort_lower_opset_for_ep` feature flag (default: on). - Restructure Gemma4 per-layer embedding from one `[V, L*D]` table (262144×8960 = 2.35B elements) to L separate `[V, D]` tables (262144×256 = 67M each). Avoids ORT CUDA Gather int32 overflow ([onnxruntime#28107](microsoft/onnxruntime#28107)) without any sharding workaround — each layer does its own small Gather directly. - Add generic `ort_shard_large_gathers` flag + `Embedding.shard_weight_dict()` safety net for other models that may hit the same int32 limit. **Audio encoder accuracy** - Implement `ClippableLinear` component with learned input/output clamping buffers (HF `Gemma4ClippableLinear`). Fixes audio encoder divergence: max diff 52.68 → 0.0003. - Fix off-by-one in `_build_causal_window_mask`: `context_left → context_left - 1` to match HF behavior. **Example script (`examples/gemma4_multimodal.py`)** - Fix audio feature extraction: use `Gemma4AudioFeatureExtractor` (not `WhisperFeatureExtractor`) - Add `--compare-hf` support for audio and vision+audio modes - Align `--ep`/`--device` flags with established CLI pattern ### Testing - All modes (text, vision, audio, vision+audio) verified on CUDA - Audio `--compare-hf --device cuda`: max diff ~0.0003 (float32) - Unit tests: 2329 passed (no regressions) - Lint clean --------- Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Signed-off-by: Justin Chu <justinchu@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent 8bd6457 commit dbcc8b6

55 files changed

Lines changed: 4063 additions & 171 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/skills/debugging-vl-pipeline/SKILL.md

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
---
22
name: debugging-vl-pipeline
33
description: >
4-
How to debug vision-language (VL) model output issues in mobius.
5-
Covers the systematic pipeline isolation methodology, common failure modes,
6-
stage-by-stage comparison with HuggingFace, and numerical tolerance
7-
expectations. Use this skill when ORT GenAI multimodal output is wrong,
4+
How to debug vision-language (VL) and multimodal (vision + audio) model
5+
output issues in mobius. Covers the systematic pipeline isolation
6+
methodology, common failure modes, stage-by-stage comparison with
7+
HuggingFace, numerical tolerance expectations, and CUDA EP-specific
8+
issues. Use this skill when ORT GenAI multimodal output is wrong,
89
garbled, or doesn't match HuggingFace.
910
---
1011

@@ -18,6 +19,8 @@ Use this skill when:
1819
- ONNX model logits diverge significantly from HuggingFace
1920
- The model generates text-only descriptions ignoring the image
2021
- Image features appear correct but decoder output is wrong
22+
- Audio transcription is garbled or wrong despite correct encoder output
23+
- CUDA EP crashes or produces different results than CPU
2124

2225
## Debugging methodology: isolate each stage
2326

@@ -433,14 +436,98 @@ spatial_merge_size = getattr(vc, "spatial_merge_size", 2)
433436
temporal_patch_size = getattr(vc, "temporal_patch_size", 2)
434437
```
435438

439+
### 6. ClippableLinear divergence (Gemma4)
440+
441+
**Symptoms:** Vision or audio encoder output has large max diff (> 1.0)
442+
against HuggingFace, even though weights are loaded correctly.
443+
444+
**Root cause:** Gemma4 uses `Gemma4ClippableLinear` with learned finite
445+
input/output activation clamping for ALL linear layers in its vision and
446+
audio encoders. Using plain `Linear` misses the clamping.
447+
448+
**Detection:** Check if the HuggingFace model uses `ClippableLinear`:
449+
```bash
450+
grep -n "ClippableLinear" transformers/models/<model>/modeling_<model>.py
451+
```
452+
453+
**Fix:** Use `ClippableLinear` (from `mobius.components`) for all
454+
affected linear layers. For vision attention: q/k/v/o_proj. For MLP:
455+
pass `linear_class=ClippableLinear` to the MLP component.
456+
457+
**Impact:**
458+
- Audio: max diff 52.68 → 0.0003 after fix
459+
- Vision: max diff 3.92 → 0.00007 after fix
460+
461+
### 7. Missing audio boundary markers
462+
463+
**Symptoms:** Audio transcription is garbled or completely wrong, even
464+
though the audio encoder output matches HuggingFace.
465+
466+
**Root cause:** HuggingFace wraps audio placeholder tokens with boundary
467+
markers in `input_ids`:
468+
```
469+
<|audio> (256000) + N × <|audio|> (258881) + <audio|> (258883)
470+
```
471+
If boundary markers are missing, the model cannot distinguish audio
472+
regions from text, producing wrong output.
473+
474+
**Fix:** Add boundary markers to `build_input_ids()` when constructing
475+
audio inputs, matching HuggingFace's token wrapping.
476+
477+
### 8. CUDA EP: ORT Gather int32 overflow
478+
479+
**Symptoms:** CUDA EP crashes or produces incorrect results for models
480+
with large embedding tables. CPU EP works correctly.
481+
482+
**Root cause:** ORT CUDA `gather_impl.cu` uses `int32` for element
483+
offset computation: `input_index = idx * cols + col_offset`. For
484+
tensors with > 2^31 elements (e.g. Gemma4 per-layer embedding
485+
[262144, 8960] = 2.35B elements), this overflows.
486+
487+
**ORT bug:** microsoft/onnxruntime#28107
488+
489+
**Workaround:** Split large embeddings into smaller tables via
490+
`nn.ModuleList` so each individual Gather stays under the int32 limit.
491+
Use Slice instead of Gather for column-wise indexing on large tensors.
492+
493+
### 9. CUDA EP: opset 24 kernel registration
494+
495+
**Symptoms:** ORT CUDA EP fails to find kernels for standard ops
496+
(Squeeze, Reshape, etc.) even though they work on CPU.
497+
498+
**Root cause:** ORT ≤1.24.x CUDA/TRT EPs don't register kernels for
499+
opset 24, even though the op semantics are unchanged from opset 23.
500+
501+
**Fix:** Use the `ort_lower_opset_for_ep` feature flag (enabled by
502+
default) which lowers the declared opset import from 24 to 23 for
503+
non-CPU EPs. See `src/mobius/_flags.py` and
504+
`src/mobius/_testing/ort_inference.py`.
505+
506+
### 10. Wrong audio feature extractor
507+
508+
**Symptoms:** Audio model produces completely wrong output. Audio
509+
encoder features don't match HuggingFace at all.
510+
511+
**Root cause:** Using `WhisperFeatureExtractor` instead of the
512+
model-specific feature extractor (e.g. `Gemma4AudioFeatureExtractor`).
513+
Different extractors produce different mel spectrograms.
514+
515+
**Fix:** Always use the correct feature extractor for the model:
516+
```python
517+
from transformers import AutoFeatureExtractor
518+
feature_extractor = AutoFeatureExtractor.from_pretrained(model_id)
519+
```
520+
436521
## Reference files
437522

438523
- **Integration tests:** `tests/integration_test.py`
439524
(`TestVLFullForward`, `TestQwen25VL3Model`)
440525
- **ORT GenAI tests:** `tests/ort_genai_test.py`
441526
(`TestOrtGenaiQwen25VL.test_multimodal_image_generation`)
442527
- **Example scripts:** `examples/qwen25_vl_ort_genai.py`,
443-
`examples/qwen3_vl_ort_genai.py`
528+
`examples/qwen3_vl_ort_genai.py`, `examples/gemma4_multimodal.py`
444529
- **genai_config reference:** `.github/skills/ort-genai-config/SKILL.md`
445530
- **ORT GenAI position_ids code (external):**
446531
`onnxruntime-genai/src/models/position_inputs.cpp:617-814`
532+
- **Feature flags:** `src/mobius/_flags.py`
533+
(`ort_lower_opset_for_ep`, `ort_cuda_grouped_rmsnorm_workaround`)

.github/skills/multimodal-models/SKILL.md

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
---
22
name: multimodal-models
33
description: >
4-
How to add multimodal (vision + language) models to mobius.
4+
How to add multimodal (vision + language + audio) models to mobius.
55
Covers projector variants (Gemma3, MLP, Linear), the VisionModel encoder,
6-
InputMixer, VisionLanguageTask, image token handling, and weight name
7-
mappings. Use this skill when adding a model that processes both images and
8-
text.
6+
InputMixer, VisionLanguageTask, image/audio token handling, ClippableLinear,
7+
and weight name mappings. Use this skill when adding a model that processes
8+
images, audio, or both alongside text.
99
---
1010

11-
# Skill: Multimodal (Vision + Language) Models
11+
# Skill: Multimodal (Vision + Language + Audio) Models
1212

1313
## When to use
1414

1515
Use this skill when adding a model that processes both images and text — such
16-
as Gemma3, LLaVA, LLaVA-NeXT, Phi-3-Vision, PaliGemma, InternVL2, Pixtral,
17-
Idefics2/3, Molmo, Florence2, or Video-LLaVA.
16+
as Gemma3, Gemma4, LLaVA, LLaVA-NeXT, Phi-3-Vision, PaliGemma, InternVL2,
17+
Pixtral, Idefics2/3, Molmo, Florence2, or Video-LLaVA — or a model that
18+
also processes audio (e.g. Gemma4 with speech/audio inputs).
1819

1920
## Architecture overview
2021

@@ -390,6 +391,90 @@ The vision pipeline is completely shared with Qwen3-VL — only the text
390391
decoder differs (hybrid DeltaNet + full attention). This means vision
391392
encoder bugs/fixes apply to both models equally.
392393

394+
## Gemma4: vision + audio multimodal
395+
396+
Gemma4 models (E2B, E4B, 26B-A4B, 31B) support **both vision and audio**
397+
inputs. The architecture has 4 sub-models: decoder, vision encoder, audio
398+
encoder, and embedding.
399+
400+
### Architecture
401+
402+
```
403+
pixel_values ──► [Vision Encoder] ──► image_features ──┐
404+
405+
audio_features ─► [Audio Encoder] ──► audio_features ──┤
406+
407+
input_ids ──────► [Embedding] ◄────────────────────────┘
408+
409+
410+
[Text Decoder] ──► logits
411+
```
412+
413+
### ClippableLinear (critical for Gemma4)
414+
415+
Gemma4's vision and audio encoders use `Gemma4ClippableLinear` — a
416+
`Linear` with learned finite input/output activation clamping:
417+
418+
```python
419+
x = Clip(x, input_min, input_max)
420+
x = x @ weight.T [+ bias]
421+
x = Clip(x, output_min, output_max)
422+
```
423+
424+
**This is the single most common source of Gemma4 divergence.** Using
425+
plain `Linear` instead of `ClippableLinear` causes:
426+
- Audio encoder max diff: 52.68 → 0.0003 after fix
427+
- Vision encoder max diff: 3.92 → 0.00007 after fix
428+
429+
Vision encoder uses ClippableLinear for ALL linear layers:
430+
- Q/K/V/O projections in `Gemma4VisionSelfAttention`
431+
- gate/up/down projections in MLP (via `linear_class=ClippableLinear`)
432+
433+
Audio encoder uses ClippableLinear for its linear layers as well.
434+
435+
See `reusable-components` skill for full `ClippableLinear` API reference.
436+
437+
### Audio boundary markers
438+
439+
HuggingFace wraps audio tokens with boundary markers that must be present
440+
in `input_ids` for correct generation:
441+
442+
```
443+
<|audio> (256000) + N × <|audio|> (258881) + <audio|> (258883)
444+
```
445+
446+
This parallels the image token pattern:
447+
```
448+
<|image> (255999) + N × <|image|> (258880) + <image|> (258882)
449+
```
450+
451+
**Missing audio boundary markers** cause garbled audio transcription output
452+
even when the audio encoder output is numerically correct.
453+
454+
### Per-layer embeddings (CUDA ORT workaround)
455+
456+
Gemma4 uses per-layer embedding: `embed_tokens_per_layer` with shape
457+
`[V, L*D]` where V=vocab_size, L=num_layers, D=per_layer_dim. For large
458+
models this creates a single Gather on a 2.35B-element tensor, which
459+
**overflows ORT's CUDA Gather kernel** (int32 offset computation in
460+
`gather_impl.cu`).
461+
462+
**Workaround:** Split into L separate `Embedding([V, D])` tables via
463+
`nn.ModuleList`. In `preprocess_weights`, split the HF weight column-wise:
464+
```python
465+
for i in range(num_layers):
466+
renamed[f"embed_tokens_per_layer.{i}.weight"] = value[:, i*D:(i+1)*D]
467+
```
468+
469+
Use `Slice` instead of `Gather` for per-layer projection indexing to
470+
avoid the large-tensor issue entirely.
471+
472+
### Audio feature extraction
473+
474+
Gemma4 uses `Gemma4AudioFeatureExtractor` (not `WhisperFeatureExtractor`).
475+
Using the wrong feature extractor produces completely different mel features
476+
and the model fails silently (produces garbage transcription).
477+
393478
## Testing multimodal models
394479

395480
### Image token count

.github/skills/phi4mm-component-parity/SKILL.md

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ description: >
55
embedding/projector, text decoder) matches HuggingFace output. Covers pipeline
66
isolation methodology, common failure modes from real debugging experience,
77
step-by-step debugging process, and integration test patterns. Applicable to
8-
any multimodal model with similar architecture (Phi4MM, future audio+vision
9-
models). Use this skill when multimodal ONNX model output diverges from
10-
HuggingFace, or when adding a new multimodal model with multiple encoders.
8+
any multimodal model with similar architecture (Phi4MM, Gemma4, future
9+
audio+vision models). Use this skill when multimodal ONNX model output
10+
diverges from HuggingFace, or when adding a new multimodal model with
11+
multiple encoders.
1112
---
1213

1314
# Skill: Multimodal Component Parity Debugging
@@ -371,6 +372,46 @@ the actual sequence includes fused image/audio tokens, the lengths diverge.
371372
**Fix:** Always derive `seq_len` from `inputs_embeds.shape[1]` when the
372373
model uses inputs_embeds as input.
373374

375+
### 8. ClippableLinear not used in encoder (Gemma4)
376+
377+
**Symptoms:** Encoder output has large numerical divergence (max diff > 1.0)
378+
from HuggingFace, despite all weights loading correctly.
379+
380+
**Root cause:** Some HuggingFace models (e.g. Gemma4) use
381+
`ClippableLinear` — a linear layer with learned finite input/output
382+
activation clipping — for ALL linear layers in their vision and audio
383+
encoders (attention q/k/v/o projections AND MLP gate/up/down projections).
384+
Using plain `Linear` misses the clamping and causes divergence.
385+
386+
**Detection:** Check HF source for `ClippableLinear`:
387+
```bash
388+
grep -n "ClippableLinear" transformers/models/<model>/modeling_<model>.py
389+
```
390+
391+
**Fix:** Use `ClippableLinear` from `mobius.components`:
392+
- For attention: use `ClippableLinear` for q/k/v/o_proj
393+
- For MLP: pass `linear_class=ClippableLinear` parameter
394+
395+
**Impact (Gemma4):**
396+
- Audio: max diff 52.68 → 0.0003
397+
- Vision: max diff 3.92 → 0.00007
398+
399+
### 9. Missing audio/image boundary tokens
400+
401+
**Symptoms:** Audio transcription or vision description is garbled, but
402+
encoder output matches HuggingFace.
403+
404+
**Root cause:** HuggingFace wraps modality placeholder tokens with
405+
boundary markers:
406+
- Image: `<|image>` (open) + N × `<|image|>` (pad) + `<image|>` (close)
407+
- Audio: `<|audio>` (open) + N × `<|audio|>` (pad) + `<audio|>` (close)
408+
409+
Missing boundary markers prevent the model from correctly identifying
410+
modality regions in the input sequence.
411+
412+
**Fix:** Ensure `build_input_ids` wraps placeholder tokens with the
413+
correct open/close marker token IDs.
414+
374415
## Step-by-step debugging process
375416

376417
### Phase 1: Text-only baseline
@@ -562,11 +603,16 @@ image_sizes = np.array([[384, 384]], dtype=np.int64)
562603
- **Integration tests:** `tests/phi4mm_integration_test.py`,
563604
`tests/integration_test.py`
564605
- **VL debugging skill:** `.github/skills/debugging-vl-pipeline/SKILL.md`
565-
- **Model implementation:** `src/mobius/models/phi.py`
566-
- **Audio components:** `src/mobius/components/_audio.py`
606+
- **Model implementation:** `src/mobius/models/phi.py`,
607+
`src/mobius/models/gemma4.py`
608+
- **Audio components:** `src/mobius/components/_audio.py`,
609+
`src/mobius/components/_gemma4_audio.py`
567610
- **Vision components:** `src/mobius/components/_vision.py`
611+
- **ClippableLinear:** `src/mobius/components/_gemma4_audio.py`
568612
- **LoRA component:** `src/mobius/components/_lora.py`
569613
- **Weight loading:** `src/mobius/_weight_loading.py`
614+
- **Feature flags:** `src/mobius/_flags.py`
570615
- **ORT GenAI config skill:** `.github/skills/ort-genai-config/SKILL.md`
571616
- **Weight name alignment skill:**
572617
`.github/skills/weight-name-alignment/SKILL.md`
618+
- **Gemma4 example:** `examples/gemma4_multimodal.py`

.github/skills/quality-checklist/SKILL.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ before the PR is merged.
8787
`testdata/golden/<cat>/<model>_generation.json`
8888
- [ ] `python -m pytest tests/e2e_golden_test.py -m generation -k "<model>"` passes
8989

90+
> **Speech-language models:** The golden generation script supports the
91+
> `speech-language` task type for models that process audio inputs (e.g.,
92+
> Gemma4). The `_generate_speech_language()` function in
93+
> `scripts/generate_golden.py` handles audio feature extraction and input
94+
> construction for these models. Ensure the correct feature extractor
95+
> (e.g. `Gemma4AudioFeatureExtractor`, not `WhisperFeatureExtractor`) is
96+
> auto-detected via `AutoFeatureExtractor.from_pretrained()`.
97+
9098
> **Why L4/L5 matter:** Graph-build tests (L1) only verify ONNX graph
9199
> construction; they never execute the graph with real data. A MatMul shape
92100
> mismatch that crashes at runtime, a wrong normalisation type, or a missing
@@ -113,6 +121,15 @@ python examples/<model>_text_generation.py --compare-hf --dtype bf16
113121
- [ ] `mobius build --model <hf-model-id> /tmp/out` completes without error
114122
- [ ] Output directory contains the expected ONNX files and `genai_config.json`
115123

124+
### 8a. Multi-EP correctness (CUDA)
125+
126+
- [ ] Model runs correctly with `--ep cuda` (or `--device cuda`)
127+
- [ ] CUDA results match CPU results (compare generation output)
128+
- [ ] No crashes from large tensor operations (see ORT Gather int32
129+
overflow: microsoft/onnxruntime#28107)
130+
- [ ] `ort_lower_opset_for_ep` flag handles opset 24→23 lowering for
131+
CUDA EP (enabled by default in `src/mobius/_flags.py`)
132+
116133
### 9. ORT GenAI runtime
117134

118135
- [ ] Model can be loaded with `ort_genai.Model(output_dir)` without error

0 commit comments

Comments
 (0)