Skip to content

Fix gemma4 audio-encoder audio_features export rank (#570) - #444

Draft
justinchuby wants to merge 1 commit into
mainfrom
squad/570-gemma4-audio-rank
Draft

Fix gemma4 audio-encoder audio_features export rank (#570)#444
justinchuby wants to merge 1 commit into
mainfrom
squad/570-gemma4-audio-rank

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Problem (onnx-genai #570)

Every gemma4 (E2B/E4B) audio package is rejected at onnx-genai pipeline admission because the audio dataflow edge has mismatched ranks:

'audio_encoder.audio_features -> embedding.audio_features' has incompatible ranks:
producer rank 3, consumer rank 2.

The audio_encoder sub-model emitted rank-3 [B, T//4, H], while embedding consumes rank-2 [num_valid_frames, H] (its scatter expects per-token rows). The shipped gemma4-e2b-onnx export contains 0 Compress ops and ends at the projector MatMul — raw rank-3.

HF reference semantics

HuggingFace gemma4 selects audio_features[audio_mask] — a boolean-mask gather that both drops padding frames and flattens the batch axis, producing rank-2 [num_valid_frames, text_hidden_size] rows that align 1:1 with audio placeholder tokens before they are scattered into the language-model embeddings.

Fix

Move the mask-selection into _Gemma4AudioEncoderModel.forward: after the projector, flatten [B, T//4, H] -> [-1, H] and reuse the shared bf16-safe _dtype_safe_compress helper to emit rank-2 [num_valid_frames, H], while still returning downsampled_mask for the audio_features_mask diagnostic output. The task builder _build_audio no longer duplicates the flatten+Compress.

  • DRY / general: matches the encoder-free _Gemma4UnifiedAudioEmbedderModel (which already emitted rank-2) and covers all gemma4 Conformer-audio variants since they share _Gemma4AudioEncoderModel — no E2B-specific hack.
  • The image edge is already rank-2 (vision_encoder flattens batch); no change there.
  • Module docstrings updated to state the rank-2 contract.

Test

New TestGemma4AudioEncoderRank (non-vacuous):

  • asserts exported audio_encoder.audio_features rank == embedding.audio_features input rank == 2, trailing dim == hidden_size on both ends;
  • asserts a Compress node exists in the audio graph, so the reduction is a real mask-select, not a bare reshape that would misalign rows with placeholder tokens.

Validation

  • pytest src/mobius/models/gemma4_test.py → 20 passed (incl. 2 new)
  • pytest src/mobius/components/_gemma4_audio_test.py → 31 passed
  • ruff check on changed files → clean

Closes #570.

Do not merge — mobius PRs are merged by Justin.

The gemma4 audio dataflow edge
`audio_encoder.audio_features -> embedding.audio_features` was rank-3 on the
producer side but rank-2 on the consumer side, so onnx-genai pipeline admission
rejected every gemma4 audio (E2B/E4B) package:

    'audio_encoder.audio_features -> embedding.audio_features' has incompatible
    ranks: producer rank 3, consumer rank 2.

HF selects `audio_features[audio_mask]` (a boolean-mask gather) that both drops
padding frames and flattens the batch axis to rank-2
`[num_valid_frames, text_hidden_size]` before scattering rows into the
language-model embeddings.

Move that mask-selection into `_Gemma4AudioEncoderModel.forward`: after the
projector, flatten `[B, T//4, H] -> [-1, H]` and reuse the shared bf16-safe
`_dtype_safe_compress` helper to emit rank-2 `[num_valid_frames, H]`, matching
the `embedding` consumer and the encoder-free `_Gemma4UnifiedAudioEmbedderModel`.
The task builder `_build_audio` no longer duplicates the flatten+Compress. This
covers all gemma4 Conformer-audio variants since they share the module.

Add `TestGemma4AudioEncoderRank` locking the rank-2 contract: producer rank ==
consumer rank == 2 (trailing dim == hidden_size) and a Compress node is present
so the reduction is a real mask-select, not a bare reshape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 8a565d7fe6d687

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 68 68 +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 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 62 62 +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 60 60 +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 62 62 +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 413 413 +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.

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

Fixes Gemma4 audio package export incompatibility with onnxruntime-genai by ensuring the audio_encoder sub-model emits rank-2 audio_features ([num_valid_frames, hidden]) that match the embedding sub-model’s expected input rank, mirroring HuggingFace’s audio_features[audio_mask] semantics.

Changes:

  • Move padding-frame stripping + batch/time flattening into _Gemma4AudioEncoderModel.forward() via _dtype_safe_compress, so the audio_encoder output is rank-2 when a mask is provided.
  • Remove duplicate flatten+Compress logic from Gemma4Task._build_audio() and rely on the encoder module’s rank-2 contract.
  • Add regression tests asserting producer/consumer rank agreement and presence of a Compress op in the exported audio_encoder graph.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/mobius/tasks/_gemma4.py Removes task-level flatten+Compress; assumes audio_encoder already emits rank-2 features.
src/mobius/models/gemma4.py Implements mask-based selection inside the audio encoder model to produce rank-2 audio_features.
src/mobius/models/gemma4_test.py Adds tests to lock the rank-2 contract and ensure a real mask-select (Compress) exists in the audio graph.

Comment on lines 2644 to +2648
Output:
- ``audio_features [B, T//4, text_hidden_size]``: projected audio tokens
- ``audio_features [num_valid_frames, text_hidden_size]``: projected audio
tokens with padding frames stripped so output rows align 1:1 with audio
placeholder tokens. This mirrors HF, which selects
``audio_features[audio_mask]`` (a rank-2 gather) before scattering the
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 8a565d7fe6d687

Model Sub-model Changes Status
bert (feature-extraction) model 0
falcon model 0
gemma2 model 0
gemma4 (gemma4) decoder 0
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 0
gemma4_text model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0

No architecture changes detected.


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

@justinchuby

Copy link
Copy Markdown
Member Author

✅ Coordinator-verified, ready for your merge (not self-merged, per mobius policy).

Checked:

  • Root cause fix is correct: the rank-3→2 mask-selection is moved into _Gemma4AudioEncoderModel.forward (flatten [B,T//4,H]→[-1,H] + _dtype_safe_compress by downsampled_mask), so the exported audio_features producer rank now matches the embedding sub-model's rank-2 consumer — closing the onnx-genai #570 admission rejection.
  • Matches HF semantics: equivalent to audio_features[audio_mask] (rank-2 gather of valid frames), consistent with the documented gemma4 audio contract.
  • bf16-safe preserved: the old inline Cast(FLOAT)→Compress→CastLike is replaced by the shared _dtype_safe_compress helper (same Cast-around-Compress), and the task-level duplicate is removed — DRY, general across E2B/E4B/-wp-a.
  • downsampled_mask is None branch returns rank-3 only for eager unit calls; the export path raises if the mask is absent (_gemma4.py:666), so the consumer always sees rank-2.
  • Test is non-vacuous: TestGemma4AudioEncoderRank asserts producer rank == consumer rank == 2 (trailing dim == hidden_size) and that a Compress node exists (real mask-select, not a bare reshape). ruff clean; gemma4 (20) + audio (31) tests pass.

Suggest a quick manual e2e sanity (export gemma4-e2b → confirm it now passes onnx-genai pipeline_admission) before merge, since we couldn't run the full cross-repo admission here.

@justinchuby

Copy link
Copy Markdown
Member Author

This may break genai. Need to be careful

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.

2 participants