Skip to content

Add Gemma4 multimodal support (vision + audio) - #2103

Merged
apsonawane merged 10 commits into
mainfrom
asonawane/gemma4
May 4, 2026
Merged

Add Gemma4 multimodal support (vision + audio)#2103
apsonawane merged 10 commits into
mainfrom
asonawane/gemma4

Conversation

@apsonawane

Copy link
Copy Markdown
Contributor

Summary

Adds end-to-end support for Google Gemma 4 multimodal models in ORT GenAI, covering text-only (gemma4_text), vision-language, and any-to-any (vision + audio + text) variants.

Changes

Model registration

  • Register gemma4_text as LLM and gemma4 as MMM (multi-modal model)
  • MMM auto-detects speech support from speech.filename in genai_config — no separate gemma4_any_to_any type needed
  • Register Gemma4MultiModalProcessor in the processor factory

Gemma4 multimodal processor (gemma4_multimodal_processor.cpp/h)

  • Vision: Preprocesses images via Gemma4ImageTransform (onnxruntime-extensions), trims padded patches to actual count using num_soft_tokens from preprocessor, produces pixel_values + pixel_position_ids
  • Audio: Extracts mel features via Gemma4LogMel, computes audio_sizes for the pipeline, generates input_features_mask (all-True for single-clip inference), and expands <|audio|> placeholder tokens in the prompt
  • Prompt handling: Expands both <|image|> and <|audio|> tokens from the chat template into the correct number of soft tokens before encoding. Handles template-inserted tokens and auto-insertion when no template is available

KV cache — per-layer head_dim (kv_cache.cpp/h)

  • Auto-detects varying head_dim across layers from ONNX session input shapes (Gemma4 uses 256 for sliding-window layers, 512 for global attention layers)
  • Creates per-layer empty_pasts_ with correct head dimensions
  • Handles layer_shapes_[i][2] == 0 (unconstrained) in Update to avoid zero-size allocation
  • Updates layer_shapes_ sequence dimension for past_present_share_buffer mode

Position inputs — int64 support (position_inputs.cpp)

  • WindowedPositionInputs now supports both int32_t and int64_t for position_ids and attention_mask
  • Type-dispatching lambdas for all data access points (first window, subsequent windows, token generation)

Multi-modal pipeline (multi_modal.cpp/h)

  • DecoderState: Optional decoder_input_ids_ for models requiring input_ids alongside inputs_embeds
  • EmbeddingState: Handles empty audio_features tensor when embedding model requires it but no speech session exists (AllocateEmptyFeatures)
  • SpeechState: Manages 3D→2D reshape of speech output (ReshapeFeatures) before passing to embedding model
  • Pipeline: Conditional audio feature reshape and empty audio fallback based on num_audio_tokens_

MultiModalFeatures (multi_modal_features.cpp/h)

  • AllocateEmptyFeatures() — pre-allocates empty tensor for optional inputs
  • ReshapeFeatures() — in-place reshape with data copy and state pointer update
  • batch_size <= 0 support — skip batch dimension for 3D model outputs

Config (config.h/cpp)

  • Added pixel_position_ids to vision inputs
  • Added audio_token_id and boa_token_id to model config
  • Added PixelPositionIdsName default constant

Example script (common.py)

  • Added {"type": "audio"} entries for Gemma-style structured content in get_user_content

Testing

Tested with Gemma4 E2B model exported via mobius:

  • ✅ Text-only generation
  • ✅ Image description (detailed landscape analysis)
  • ✅ Audio transcription (Windows SAPI TTS → model correctly identifies speech content)
  • ✅ Image-only with any-to-any config (empty audio_features handled)
  • ✅ Mixed GQA + standard Attention with past_present_share_buffer=false
  • ✅ Per-layer head_dim KV cache (256/512)
  • ✅ int64 position_ids with WindowedPositionInputs

Copilot AI review requested due to automatic review settings April 27, 2026 17:51

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

Adds end-to-end Gemma4 multimodal (vision + optional audio) support to ONNX Runtime GenAI, including preprocessing, pipeline/runtime updates, and model registration/config plumbing.

Changes:

  • Register Gemma4 model types/processors and extend config to cover new inputs/IDs (e.g., pixel_position_ids, audio-related token IDs).
  • Extend multimodal pipeline/runtime to support Gemma4 decoder requirements (input_ids alongside inputs_embeds), optional/empty audio features, and speech output reshaping.
  • Enhance runtime primitives (KV cache per-layer head_dim, WindowedPositionInputs int64 support).

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/models/position_inputs.cpp Allow WindowedPositionInputs to accept int64 position_ids/attention_mask in addition to int32.
src/models/multi_modal_features.h Add APIs to allocate empty feature tensors and reshape features.
src/models/multi_modal_features.cpp Implement empty allocation + reshape and tweak batch-dim behavior for 3D tensors.
src/models/multi_modal.h Add optional decoder input_ids support (plus a new include).
src/models/multi_modal.cpp Pipeline updates for optional decoder input_ids, empty-audio fallback, and speech output reshape (3D→2D).
src/models/model_type.h Register gemma4_text as LLM and gemma4 as MMM.
src/models/model.h Include Gemma4 processor header.
src/models/model.cpp Enable MMM speech session based on speech.filename; register Gemma4 processor in factory.
src/models/kv_cache.h Add empty_pasts_ for per-layer empty past tensors.
src/models/kv_cache.cpp Auto-detect per-layer head_dim (Gemma4 256/512), handle unconstrained cache length, and sync per-layer shapes in shared-buffer mode.
src/models/gemma4_multimodal_processor.h New Gemma4 multimodal processor interface.
src/models/gemma4_multimodal_processor.cpp New preprocessing for Gemma4 vision (incl. patch trimming + pixel position ids) and audio (mel features + prompt expansion).
src/config.h Add pixel_position_ids naming, plus new token ID fields (audio_token_id, boa_token_id).
src/config.cpp Parse pixel_position_ids, audio_token_id, boa_token_id from config JSON.
examples/python/common.py Extend example structured user content to include {"type": "audio"} entries.
cmake/deps.txt Bump onnxruntime-extensions dependency SHA.

Comment thread src/models/kv_cache.cpp
Comment thread src/models/gemma4_multimodal_processor.cpp
Comment thread src/models/gemma4_multimodal_processor.cpp
Comment thread src/models/gemma4_multimodal_processor.cpp
Comment thread src/models/gemma4_multimodal_processor.cpp Outdated
Comment thread src/models/model.cpp Outdated
Comment thread src/config.h Outdated
Comment thread src/models/gemma4_multimodal_processor.cpp
Comment thread src/models/multi_modal.h Outdated
Comment thread src/models/multi_modal.cpp
@apsonawane
apsonawane requested a review from Copilot April 27, 2026 21:05

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

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Comment thread src/models/gemma4_multimodal_processor.cpp
Comment thread src/models/gemma4_multimodal_processor.cpp Outdated
Comment thread src/models/gemma4_multimodal_processor.cpp
@apsonawane
apsonawane enabled auto-merge (squash) April 28, 2026 23:46
@apsonawane
apsonawane merged commit b71f18b into main May 4, 2026
16 of 19 checks passed
@apsonawane
apsonawane deleted the asonawane/gemma4 branch May 4, 2026 18:29
hanbitmyths pushed a commit that referenced this pull request May 15, 2026
## Problem

PR #2103 (_Add Gemma4 multimodal support_) introduced an optional
`decoder_input_ids_` in `DecoderState` for models like Gemma4 that
require `input_ids` alongside `inputs_embeds`. The guard condition was:

```cpp
if (model_.session_info_.HasInput(model_.config_->model.decoder.inputs.input_ids)) {
```

However, `model_.session_info_` is a **combined** `SessionInfo`
aggregating inputs from ALL sessions — decoder, embedding, vision, and
speech. Since the **embedding** session always takes `input_ids` as its
primary input, `HasInput("input_ids")` returns `true` for every VLM that
has an embedding model, even when the decoder ONNX itself has no
`input_ids` input.

## Impact

Any VLM using the 3-model pipeline (embedding + decoder) where the
**decoder does not have `input_ids`** will fail at inference with:

```
RuntimeError: Invalid input name: input_ids
```

Confirmed broken: **mistral3** (Ministral-3B / Pixtral family), whose
decoder only accepts `inputs_embeds`.

## Fix

Scope the check to a **decoder-only** `SessionInfo` so it only fires
when the decoder ONNX itself declares `input_ids` as an input:

```cpp
// Use a decoder-only SessionInfo to avoid false positives from the embedding session
SessionInfo decoder_only_info;
decoder_only_info.Add(*model_.decoder_session_);
if (decoder_only_info.HasInput(model_.config_->model.decoder.inputs.input_ids)) {
    decoder_input_ids_ = std::make_unique<DefaultInputIDs>(*this);
    decoder_input_ids_->Add();
}
```

Gemma4 is unaffected — its decoder ONNX genuinely has `input_ids` as an
input, so the narrowed check still fires correctly for that model.

## Verification

Tested with the exported Ministral-3B-Instruct-2512 (mistral3 type,
3-model VLM):
- `generator.set_inputs(inputs)` no longer throws
- Token generation succeeds end-to-end

Regressed by: #2103

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Akshay Sonawane <asonawane@microsoft.com>
baijumeswani pushed a commit that referenced this pull request May 19, 2026
…2167)

## Summary

Make the speech sub-model optional in `PhiMultiModalProcessor`,
mirroring
the pattern already used by `Gemma4MultiModalProcessor` (added in
#2103).

A `phi4mm` `genai_config.json` with the `speech` block cleared
(`filename` and `config_filename` both empty / removed) can now load
vision-only and skip the speech ONNX entirely, reducing peak
memory usage for image-only use cases.

## Problem

PR #2103 added auto-detection of `speech.filename` in `model.cpp` and
allocation of an empty `audio_features` tensor in `multi_modal.cpp`
when the embedding ONNX requires it. However, `PhiMultiModalProcessor`
itself was not gated and still unconditionally:

1. Calls
`session_info.GetInputDataType(config.model.speech.inputs.audio_embeds)`
   (and `audio_sizes`) in the initializer list.
   `SessionInfo` aggregates inputs only from sessions that were actually
loaded — with no speech session, this throws: "Model input was not
found: audio_embeds"
2. Calls `OrtxCreateSpeechFeatureExtractor(audio_processor_, "")` with
an
empty `speech.config_filename`, which fails to open the file.
3. Registers `AudioEmbeds` / `AudioAttentionMask` / `AudioSizes`
mappings against an unconfigured speech section.

The net effect is that even though `model.cpp` correctly skips loading
`speech.onnx`, the processor construction in `MultiModalProcessor`'s
factory still throws during `og.Model(...)`, blocking vision-only
phi4mm loads.

## Fix

Apply the exact same gating pattern that `Gemma4MultiModalProcessor`
already uses:

- Add a `bool has_speech_{false}` member to `PhiMultiModalProcessor`.
- Move all audio-related construction (audio dtype queries,
`OrtxCreateSpeechFeatureExtractor`, three `AddMapping` calls) inside
an `if (!speech.config_filename.empty() && fs::exists(...))` block
that sets `has_speech_ = true` on success.
- Throw a precise error if `speech.filename` is set but the audio
processor config file is missing on disk.
- In `Process()`, throw a clear user-facing error if `payload.audios`
is supplied while `has_speech_` is `false`.
- Gate the audio feature extraction and the audio output emit block
on `payload.audios && has_speech_`.

No changes to vision-only image processing or to the
`ProcessImageAudioPrompt` helper — the existing `if (audio_sizes)`
guards inside it correctly produce `num_audios == 0` on the
vision-only path, so `audio_projection_mode` resolves to `1`
(Vision, language) as expected.


## Usage

To run phi4mm vision-only:

```json
"speech": { "filename": "", "config_filename": "" }

(or remove the speech block entirely from genai_config.json). The speech ONNX and audio_processor_config.json no longer need to be present on disk.

---------

Co-authored-by: Bollavaram <manasab@amd.com>
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