Skip to content

Commit 2b1f539

Browse files
committed
Support gemma-4-12B unified (encoder-free) multimodal model
The gemma-4-12B "unified" variant is encoder-free: it consumes raw 48px merged pixel patches (patch_dim 6912) and raw 640-sample waveform frames directly, rather than the SigLIP image / Conformer log-mel audio contract of the standard gemma-4 (E2B/E4B) model. Register "gemma4_unified" as an MMM model type and route it to Gemma4MultiModalProcessor with a unified flag that adjusts two things: * Vision: no pixel-value trimming. The unified vision graph strips padding patches internally (position == -1), so the full padded (max_soft_tokens, 6912) pixel_values and position_ids are fed as-is instead of being trimmed to actual_soft_tokens * pooling^2. * Audio: each 640-sample frame is exactly one audio soft token, so the audio-token count equals the number of frames (no Conv2d stride-2 subsampling as in the Conformer speech encoder). Everything else (prompt image/audio token expansion, embedding fusion, per-layer-input decoder wiring) is shared with the standard gemma4 path. The paired preprocessing ops live in onnxruntime-extensions (Gemma4ImageTransform at patch_size=48/pooling=1, Gemma4UnifiedAudioFrames). Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com>
1 parent 3c4b66e commit 2b1f539

4 files changed

Lines changed: 25 additions & 7 deletions

File tree

src/models/gemma4_multimodal_processor.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ ProcessGemma4Prompt(const Generators::Tokenizer& tokenizer, const std::string& p
167167

168168
Gemma4MultiModalProcessor::Gemma4MultiModalProcessor(Config& config, const SessionInfo& session_info)
169169
: pixel_values_type_{session_info.GetInputDataType(config.model.vision.inputs.pixel_values)} {
170+
// gemma-4-12B "unified" is encoder-free: it consumes raw 48px merged pixel
171+
// patches (patch_dim 6912) and raw 640-sample waveform frames directly. The
172+
// preprocessing ops (Gemma4ImageTransform at patch_size=48/pooling=1 and
173+
// Gemma4UnifiedAudioFrames) already emit the final contract, so — unlike the
174+
// SigLIP/log-mel "gemma4" path — no pixel trimming or conv-subsampling of the
175+
// audio-token count is applied.
176+
unified_ = config.model.type == "gemma4_unified";
177+
170178
// Query pixel_position_ids type (int32 or int64) if the vision model has this input
171179
if (session_info.HasInput(config.model.vision.inputs.pixel_position_ids)) {
172180
pixel_position_ids_type_ = session_info.GetInputDataType(config.model.vision.inputs.pixel_position_ids);
@@ -285,10 +293,18 @@ std::unique_ptr<NamedTensors> Gemma4MultiModalProcessor::Process(const Tokenizer
285293
std::make_shared<Tensor>(std::move(mask)));
286294
}
287295

288-
// Compute audio_sizes: the speech encoder uses 2-stage Conv2d with stride=2 each
289-
int64_t t_after_1 = (time_dim - 1) / 2 + 1;
290-
int64_t t_after_2 = (t_after_1 - 1) / 2 + 1;
291-
num_audio_tokens = t_after_2;
296+
// Compute audio_sizes / audio-token count.
297+
// Unified: each 640-sample frame is exactly one audio soft token, so the
298+
// token count equals the number of frames (time_dim) — no subsampling.
299+
// Standard gemma4: the Conformer speech encoder applies 2-stage Conv2d with
300+
// stride=2 each, so the token count is reduced accordingly.
301+
if (unified_) {
302+
num_audio_tokens = time_dim;
303+
} else {
304+
int64_t t_after_1 = (time_dim - 1) / 2 + 1;
305+
int64_t t_after_2 = (t_after_1 - 1) / 2 + 1;
306+
num_audio_tokens = t_after_2;
307+
}
292308
std::array<int64_t, 1> audio_sizes_shape = {1};
293309
auto audio_sizes = OrtValue::CreateTensor<int64_t>(allocator, audio_sizes_shape);
294310
audio_sizes->GetTensorMutableData<int64_t>()[0] = num_audio_tokens;
@@ -320,7 +336,7 @@ std::unique_ptr<NamedTensors> Gemma4MultiModalProcessor::Process(const Tokenizer
320336
const int64_t num_padded_patches = (pv_dims == 3) ? pv_shape[1] : pv_shape[0];
321337
const int64_t patch_dim = (pv_dims == 3) ? pv_shape[2] : pv_shape[1];
322338

323-
if (actual_patches < num_padded_patches) {
339+
if (!unified_ && actual_patches < num_padded_patches) {
324340
// Trim: copy only the first actual_patches from the padded tensor.
325341
// The preprocessor outputs float32 data, so we trim using float32 strides,
326342
// then cast to the model's pixel_values type (float, fp16, or bf16).
@@ -365,7 +381,7 @@ std::unique_ptr<NamedTensors> Gemma4MultiModalProcessor::Process(const Tokenizer
365381
const int64_t num_padded_pos = (pos_dims == 3) ? pos_shape[1] : pos_shape[0];
366382
const int64_t pos_last_dim = (pos_dims == 3) ? pos_shape[2] : pos_shape[1];
367383

368-
if (actual_patches < num_padded_pos) {
384+
if (!unified_ && actual_patches < num_padded_pos) {
369385
// Trim position_ids: for 3D, copy per-batch with correct stride.
370386
// Detect the element type from the vision model's input to handle both int32 and int64.
371387
const int64_t pos_batch = (pos_dims == 3) ? pos_shape[0] : 1;

src/models/gemma4_multimodal_processor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct Gemma4MultiModalProcessor : Processor {
2020
ONNXTensorElementDataType audio_features_type_;
2121

2222
bool has_speech_{false};
23+
bool unified_{false}; // gemma-4-12B encoder-free "unified" variant
2324
size_t vision_soft_tokens_per_image_{260};
2425
};
2526

src/models/model.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ MultiModalProcessor::MultiModalProcessor(Config& config, const SessionInfo& sess
974974
{"phi4mm", Processor::Create<PhiMultiModalProcessor>},
975975
{"gemma3", Processor::Create<GemmaImageProcessor>},
976976
{"gemma4", Processor::Create<Gemma4MultiModalProcessor>},
977+
{"gemma4_unified", Processor::Create<Gemma4MultiModalProcessor>},
977978
{"mistral3", Processor::Create<Mistral3ImageProcessor>},
978979
{"fara", Processor::Create<QwenImageProcessor>},
979980
{"qwen2_5_vl", Processor::Create<QwenImageProcessor>},

src/models/model_type.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ struct ModelType {
6363

6464
inline static bool IsMMM(const std::string& model_type) {
6565
// Multi-modal model (MMM)
66-
static constexpr std::array<std::string_view, 2> MMM = {"gemma4", "phi4mm"};
66+
static constexpr std::array<std::string_view, 3> MMM = {"gemma4", "gemma4_unified", "phi4mm"};
6767
return std::find(MMM.begin(), MMM.end(), model_type) != MMM.end();
6868
}
6969

0 commit comments

Comments
 (0)