-
Notifications
You must be signed in to change notification settings - Fork 138
Add gemma-4-12B unified (encoder-free) preprocessing support #1091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
3c46bbb
ba71805
2f42800
920f54b
ff3d01b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,8 @@ Operation::KernelRegistry SpeechFeatureExtractor::kernel_registry_ = { | |
| {"NemoLogMel", []() { return CreateKernelInstance(&NemoLogMel::Compute); }}, | ||
| {"PerFeatureNormalize", []() { return CreateKernelInstance(&PerFeatureNormalize::Compute); }}, | ||
| {"Phi4AudioEmbed", []() { return CreateKernelInstance(&Phi4AudioEmbed::Compute); }}, | ||
| {"Gemma4LogMel", []() { return CreateKernelInstance(&Gemma4LogMel::Compute); }}}; | ||
| {"Gemma4LogMel", []() { return CreateKernelInstance(&Gemma4LogMel::Compute); }}, | ||
| {"Gemma4UnifiedAudioFrames", []() { return CreateKernelInstance(&Gemma4UnifiedAudioFrames::Compute); }}}; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you think we can have a more generic audio processor for gemma? so say instead of having both for instance, with the Qwen-2.5VL image processor, instead of adding a new op for Smart Resize, which the model uses, we added smart resize parameters to finetune the pre-existing Resize op.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great suggestion — done in
Both branches now share the same One back-compat note: I also propagated the op rename to the companion onnxruntime-genai (microsoft/onnxruntime-genai#2286) and mobius PRs. Let me know if you'd prefer I drop the |
||
|
|
||
| SpeechFeatureExtractor::SpeechFeatureExtractor() : OrtxObjectImpl(extObjectKind_t::kOrtxKindFeatureExtractor) {} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "feature_extraction": { | ||
| "sequence": [ | ||
| { | ||
| "operation": { | ||
| "name": "audio_decoder", | ||
| "type": "AudioDecoder" | ||
| } | ||
| }, | ||
| { | ||
| "operation": { | ||
| "name": "gemma4_unified_audio_frames", | ||
| "type": "Gemma4UnifiedAudioFrames", | ||
| "attrs": { | ||
| "audio_samples_per_token": 640, | ||
| "sampling_rate": 16000, | ||
| "padding_value": 0.0 | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| "processor": { | ||
| "name": "gemma_4_unified_image_processing", | ||
| "transforms": [ | ||
| { | ||
| "operation": { | ||
| "name": "decode_image", | ||
| "type": "DecodeImage", | ||
| "attrs": { | ||
| "color_space": "RGB" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "operation": { | ||
| "name": "gemma4_image_transform", | ||
| "type": "Gemma4ImageTransform", | ||
| "attrs": { | ||
| "patch_size": 48, | ||
| "max_soft_tokens": 280, | ||
| "pooling_kernel_size": 1 | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -446,4 +446,40 @@ TEST(ExtractorTest, TestGemma4AudioFeatureExtractionMultiFile) { | |
| ASSERT_EQ(err, kOrtxOK); | ||
| ASSERT_EQ(mask_dims, 2ULL); | ||
| ASSERT_EQ(mask_shape[0], 2); | ||
| } | ||
| } | ||
|
|
||
| TEST(ExtractorTest, TestGemma4UnifiedAudioFrames) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we add a multi-file
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| // gemma-4-12B "unified" (encoder-free) audio: raw 16 kHz waveform chunked | ||
| // into fixed 640-sample frames. Pipeline: AudioDecoder -> Gemma4UnifiedAudioFrames | ||
| const char* audio_path[] = {"data/jfk.flac"}; | ||
| OrtxObjectPtr<OrtxRawAudios> raw_audios; | ||
| extError_t err = OrtxLoadAudios(raw_audios.ToBeAssigned(), audio_path, 1); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| OrtxObjectPtr<OrtxFeatureExtractor> feature_extractor( | ||
| OrtxCreateSpeechFeatureExtractor, "data/models/gemma-4-unified/audio_feature_extraction.json"); | ||
| OrtxObjectPtr<OrtxTensorResult> result; | ||
| err = OrtxFeatureExtraction(feature_extractor.get(), raw_audios.get(), result.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| // Output 0: raw waveform frames — float (batch, num_tokens, 640) | ||
| OrtxObjectPtr<OrtxTensor> tensor; | ||
| err = OrtxTensorResultGetAt(result.get(), 0, tensor.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| const float* data{}; | ||
| const int64_t* shape{}; | ||
| size_t num_dims; | ||
| err = OrtxGetTensorData(tensor.get(), reinterpret_cast<const void**>(&data), &shape, &num_dims); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| ASSERT_EQ(num_dims, 3ULL); // (batch, num_tokens, samples_per_token) | ||
| ASSERT_EQ(shape[0], 1); // single audio | ||
| ASSERT_EQ(shape[2], 640); // 640 raw samples per token | ||
| EXPECT_GT(shape[1], 0); // at least one frame | ||
|
|
||
| // All values finite and within the normalized PCM range. | ||
| for (int64_t i = 0; i < std::min<int64_t>(shape[1] * 640, 5000); ++i) { | ||
| ASSERT_TRUE(std::isfinite(data[i])) << "frame value at index " << i << " is not finite"; | ||
| ASSERT_LE(std::abs(data[i]), 4.0f) << "frame value at index " << i << " out of range"; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,6 +407,120 @@ TEST(ProcessorTest, TestGemma4ImageProcessing) { | |
| EXPECT_EQ(nst_peek[0], 260) << "num_soft_tokens should be 260 for australia.jpg (HF reference)"; | ||
| } | ||
|
|
||
| TEST(ProcessorTest, TestGemma4UnifiedImageProcessing) { | ||
| // gemma-4-12B "unified" (encoder-free) vision preprocessing. | ||
| // | ||
| // The unified model consumes 48px MERGED patches (patch_dim = 48*48*3 = 6912) | ||
| // directly, with no SigLIP encoder to pool 3x3 teacher patches. HuggingFace | ||
| // produces these via (16px patchify -> 3x3 patches_merge). That is provably | ||
| // identical to a direct 48px patchify, so the unified contract is generated by | ||
| // reusing Gemma4ImageTransform with patch_size=48, pooling_kernel_size=1. | ||
| // | ||
| // This test verifies (a) the 6912-dim contract and (b) that the reused op's | ||
| // top-left 48px patch matches the 16px teacher patch from the standard config. | ||
| const char* image_path[] = {"data/processor/australia.jpg"}; | ||
|
|
||
| OrtxObjectPtr<OrtxRawImages> raw_images{}; | ||
| extError_t err = OrtxLoadImages(raw_images.ToBeAssigned(), image_path, 1, nullptr); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| // --- unified config: 48px merged patches --- | ||
| OrtxObjectPtr<OrtxProcessor> processor; | ||
| err = OrtxCreateProcessor(processor.ToBeAssigned(), "data/models/gemma-4-unified/image_processor.json"); | ||
| if (err != kOrtxOK) { | ||
| std::cout << "Error: " << OrtxGetLastErrorMessage() << std::endl; | ||
| } | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| OrtxObjectPtr<OrtxTensorResult> result; | ||
| err = OrtxImagePreProcess(processor.get(), raw_images.get(), result.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| OrtxObjectPtr<OrtxTensor> tensor; | ||
| err = OrtxTensorResultGetAt(result.get(), 0, tensor.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| const float* pv_data{}; | ||
| const int64_t* shape{}; | ||
| size_t num_dims; | ||
| err = OrtxGetTensorData(tensor.get(), reinterpret_cast<const void**>(&pv_data), &shape, &num_dims); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| ASSERT_EQ(num_dims, 3ULL); | ||
| ASSERT_EQ(shape[0], 1); | ||
| constexpr int64_t kMaxSoftTokens = 280; | ||
| constexpr int64_t kMergedPatchDim = 48 * 48 * 3; // 6912 | ||
| ASSERT_EQ(shape[1], kMaxSoftTokens); | ||
| ASSERT_EQ(shape[2], kMergedPatchDim); | ||
|
|
||
| // position_ids — merged 48-grid coordinates. | ||
| err = OrtxTensorResultGetAt(result.get(), 1, tensor.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| const int64_t* pos_data{}; | ||
| err = OrtxGetTensorData(tensor.get(), reinterpret_cast<const void**>(&pos_data), &shape, &num_dims); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| EXPECT_EQ(pos_data[0], 0); // patch 0 x | ||
| EXPECT_EQ(pos_data[1], 0); // patch 0 y | ||
|
Comment on lines
+459
to
+467
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
|
|
||
| // num_soft_tokens: merged count = teacher-grid / 9. For australia.jpg the | ||
| // teacher grid is 60x39 -> merged 20x13 = 260 (same value as the standard | ||
| // config, which reports it before pooling). | ||
| OrtxObjectPtr<OrtxTensor> nst_tensor; | ||
| err = OrtxTensorResultGetAt(result.get(), 2, nst_tensor.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| const int64_t* nst{}; | ||
| const int64_t* nst_shape{}; | ||
| size_t nst_dims{}; | ||
| err = OrtxGetTensorData(nst_tensor.get(), reinterpret_cast<const void**>(&nst), &nst_shape, &nst_dims); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| EXPECT_EQ(nst[0], 260) << "merged soft-token count for australia.jpg (HF reference)"; | ||
| const int64_t num_merged = nst[0]; | ||
| // Last real merged patch position: (20-1, 13-1) = (19, 12). | ||
|
Comment on lines
+478
to
+487
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in |
||
| EXPECT_EQ(pos_data[(num_merged - 1) * 2], 19); | ||
| EXPECT_EQ(pos_data[(num_merged - 1) * 2 + 1], 12); | ||
| // Padding beyond real patches is (-1, -1). | ||
| for (int64_t i = num_merged; i < kMaxSoftTokens; ++i) { | ||
| EXPECT_EQ(pos_data[i * 2], -1); | ||
| EXPECT_EQ(pos_data[i * 2 + 1], -1); | ||
| } | ||
|
|
||
| // Copy merged patch 0 before running the second (teacher) config, which | ||
| // invalidates pv_data. | ||
| std::vector<float> merged_patch0(pv_data, pv_data + kMergedPatchDim); | ||
|
justinchuby marked this conversation as resolved.
Outdated
|
||
|
|
||
| // --- standard config: 16px teacher patches --- | ||
| OrtxObjectPtr<OrtxProcessor> teacher_proc; | ||
| err = OrtxCreateProcessor(teacher_proc.ToBeAssigned(), "data/models/gemma-4/image_processor.json"); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| OrtxObjectPtr<OrtxTensorResult> teacher_result; | ||
| err = OrtxImagePreProcess(teacher_proc.get(), raw_images.get(), teacher_result.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| OrtxObjectPtr<OrtxTensor> teacher_tensor; | ||
| err = OrtxTensorResultGetAt(teacher_result.get(), 0, teacher_tensor.ToBeAssigned()); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
| const float* teacher_pv{}; | ||
| const int64_t* teacher_shape{}; | ||
| size_t teacher_dims{}; | ||
| err = OrtxGetTensorData(teacher_tensor.get(), reinterpret_cast<const void**>(&teacher_pv), &teacher_shape, | ||
| &teacher_dims); | ||
| ASSERT_EQ(err, kOrtxOK); | ||
|
|
||
| // Merged patch 0 is the top-left 48x48 image block, HWC. Its top-left 16x16 | ||
| // sub-block must equal teacher patch 0 (also the top-left 16x16 block, HWC). | ||
| // merged flat index: ((r*48 + col)*3 + c) for r,col in [0,16) | ||
| // teacher flat index: ((r*16 + col)*3 + c) | ||
| constexpr int64_t kTeacherPatchDim = 16 * 16 * 3; // 768 | ||
| for (int64_t r = 0; r < 16; ++r) { | ||
|
justinchuby marked this conversation as resolved.
|
||
| for (int64_t col = 0; col < 16; ++col) { | ||
| for (int64_t c = 0; c < 3; ++c) { | ||
| float m = merged_patch0[(r * 48 + col) * 3 + c]; | ||
| float t = teacher_pv[(r * 16 + col) * 3 + c]; | ||
| ASSERT_NEAR(m, t, 1e-6f) << "merged vs teacher mismatch at r=" << r << " col=" << col << " c=" << c; | ||
| } | ||
| } | ||
| } | ||
| (void)kTeacherPatchDim; | ||
| } | ||
|
|
||
| TEST(ProcessorTest, TestGemma4ImageProcessingMultiImage) { | ||
| // Verify batched processing works with multiple images of different sizes. | ||
| const char* image_paths[] = {"data/processor/standard_s.jpg", "data/processor/australia.jpg"}; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for
audio_samples_per_tokenandfeature_sizealiasing: if both keys are provided with different values, the current behavior silently takes whichever appears last. can we explicitly detect and reject conflicting values? silent override here could hide config mistakes and produce unexpected frame shapes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in
920f54b.Gemma4UnifiedAudioFrames::Initnow tracksaudio_samples_per_tokenandfeature_sizeseparately and, if both are provided with different values, returnskOrtxErrorInvalidArgument("they are aliases and must match") instead of silently taking the last one.