-
Notifications
You must be signed in to change notification settings - Fork 333
Add gemma4 unit tests #2151
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
Merged
Merged
Add gemma4 unit tests #2151
Changes from 4 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a5b0729
Add gemma4 unit tests
apsonawane 4854aec
Fix copilot comments
apsonawane 74fe16e
Merge branch 'main' into asonawane/gemma-tests
apsonawane a86d102
Update tests and add dummy model
apsonawane 2d68a73
Add speech tests
apsonawane a534c9e
Add speech model
apsonawane 5fbea1d
Update gitignore
apsonawane 578446a
Merge branch 'main' into asonawane/gemma-tests
apsonawane 58663c6
Fix tests
apsonawane fa13d49
Merge branch 'asonawane/gemma-tests' of https://github.com/microsoft/…
apsonawane b31e5fb
Address comments
apsonawane c7e27e9
fix
apsonawane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License | ||
|
|
||
| """ | ||
| Unit tests for Gemma4 multimodal model. | ||
| Tests cover model loading, text-only processing, and image understanding. | ||
|
|
||
| This file can be used in two ways: | ||
| 1. As a pytest module: pytest test_gemma4_models.py --test_models=/path/to/test_models | ||
| 2. As a standalone runner: python test_gemma4_models.py --cwd test/python --test_models test/test_models | ||
| """ | ||
|
|
||
| import argparse | ||
| import logging | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import onnxruntime_genai as og | ||
| import pytest | ||
| from _test_utils import run_subprocess | ||
|
|
||
| logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] - %(message)s", level=logging.DEBUG) | ||
| log = logging.getLogger("gemma4-tests") | ||
|
|
||
| GEMMA4_MODEL_NAME = "gemma4-vision-preprocessing" | ||
|
|
||
|
|
||
| def _get_gemma4_model_path(test_data_path): | ||
| """Return the Gemma4 model path, skipping if it doesn't exist.""" | ||
| model_path = os.path.join(test_data_path, GEMMA4_MODEL_NAME) | ||
| if not os.path.exists(model_path): | ||
| pytest.skip(f"Gemma4 test model not found at {model_path}") | ||
| return model_path | ||
|
|
||
|
|
||
| def _get_onnx_path(test_data_path, filename): | ||
| """Return a path to a dummy ONNX file under the Gemma4 model dir, skipping if missing.""" | ||
| path = os.path.join(test_data_path, GEMMA4_MODEL_NAME, filename) | ||
| if not os.path.exists(path): | ||
| pytest.skip(f"Gemma4 ONNX file not found at {path}") | ||
| return path | ||
|
|
||
|
|
||
| def _load_model_and_processor(test_data_path): | ||
| """Load the Gemma4 model and create its multimodal processor.""" | ||
| model_path = _get_gemma4_model_path(test_data_path) | ||
| model = og.Model(model_path) | ||
| return model, model.create_multimodal_processor() | ||
|
|
||
|
|
||
| def _to_numpy(tensor): | ||
| """Convert an onnxruntime-genai tensor to a numpy array.""" | ||
| if hasattr(tensor, "as_numpy"): | ||
| return tensor.as_numpy() | ||
| if hasattr(tensor, "numpy"): | ||
| return tensor.numpy() | ||
| return np.array(tensor) | ||
|
|
||
|
|
||
| def test_gemma4_model_load(test_data_path): | ||
| """Test that the Gemma4 model loads successfully.""" | ||
| model_path = _get_gemma4_model_path(test_data_path) | ||
| model = og.Model(model_path) | ||
| assert model is not None | ||
|
|
||
|
|
||
| def test_gemma4_text_only(test_data_path): | ||
| """Test text-only processing (no images).""" | ||
| _, processor = _load_model_and_processor(test_data_path) | ||
|
|
||
| inputs = processor("What is the capital of France?", images=None) | ||
|
|
||
| assert inputs is not None | ||
| assert "input_ids" in inputs | ||
|
|
||
| ids = _to_numpy(inputs["input_ids"]) | ||
| assert len(ids.shape) == 2, f"input_ids should be 2D, got shape {ids.shape}" | ||
| assert ids.shape[0] == 1, f"input_ids batch dim should be 1, got {ids.shape[0]}" | ||
|
|
||
| # Expected: BOS (2) + "What is the capital of France?" | ||
| expected_ids = [2, 3689, 563, 506, 5279, 529, 7001, 236881] | ||
| assert list(ids[0]) == expected_ids, f"input_ids mismatch: got {list(ids[0])}, expected {expected_ids}" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("relative_image_path", [Path("images") / "australia.jpg"]) | ||
| def test_gemma4_vision_basic(test_data_path, relative_image_path): | ||
| """Test basic image processing with Gemma4.""" | ||
| _, processor = _load_model_and_processor(test_data_path) | ||
|
|
||
| image_path = os.fspath(Path(test_data_path) / relative_image_path) | ||
| images = og.Images.open(image_path) | ||
|
|
||
| inputs = processor("<|image|>Describe this image", images=images) | ||
|
|
||
|
apsonawane marked this conversation as resolved.
|
||
| assert inputs is not None | ||
| assert "pixel_values" in inputs | ||
| assert "input_ids" in inputs | ||
|
|
||
| ids = _to_numpy(inputs["input_ids"]) | ||
| assert len(ids.shape) == 2, f"input_ids should be 2D, got shape {ids.shape}" | ||
| assert ids.shape[0] == 1, f"input_ids batch dim should be 1, got {ids.shape[0]}" | ||
| assert ids.shape[1] > 0, "input_ids should not be empty" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("relative_image_path", [Path("images") / "landscape.jpg"]) | ||
| def test_gemma4_vision_load_from_bytes(test_data_path, relative_image_path): | ||
| """Test loading images from bytes for Gemma4.""" | ||
| _, processor = _load_model_and_processor(test_data_path) | ||
|
|
||
| image_path = os.fspath(Path(test_data_path) / relative_image_path) | ||
| with open(image_path, "rb") as f: | ||
| images = og.Images.open_bytes(f.read()) | ||
|
|
||
| inputs = processor("<|image|>What is shown in this image?", images=images) | ||
|
apsonawane marked this conversation as resolved.
|
||
|
|
||
| assert inputs is not None | ||
| assert "pixel_values" in inputs | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "relative_image_paths", | ||
| [[Path("images") / "australia.jpg", Path("images") / "landscape.jpg"]], | ||
| ) | ||
| def test_gemma4_vision_multiple_images(test_data_path, relative_image_paths): | ||
| """Test processing multiple images with Gemma4.""" | ||
| _, processor = _load_model_and_processor(test_data_path) | ||
|
|
||
| image_paths = [os.fspath(Path(test_data_path) / p) for p in relative_image_paths] | ||
|
apsonawane marked this conversation as resolved.
|
||
| images = og.Images.open(*image_paths) | ||
|
|
||
| inputs = processor("<|image|><|image|>Compare these images", images=images) | ||
|
|
||
| assert inputs is not None | ||
| assert "pixel_values" in inputs | ||
| assert "input_ids" in inputs | ||
|
|
||
| ids = _to_numpy(inputs["input_ids"]) | ||
| assert len(ids.shape) == 2, f"input_ids should be 2D, got shape {ids.shape}" | ||
| assert ids.shape[0] == 1, f"input_ids batch dim should be 1, got {ids.shape[0]}" | ||
| assert ids.shape[1] > 0, "input_ids should not be empty" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("relative_image_path", [Path("images") / "australia.jpg"]) | ||
| def test_gemma4_processor_creates_token_type_ids(test_data_path, relative_image_path): | ||
| """Test that Gemma4 processor creates token_type_ids for image prompts.""" | ||
| _, processor = _load_model_and_processor(test_data_path) | ||
|
|
||
| image_path = os.fspath(Path(test_data_path) / relative_image_path) | ||
| images = og.Images.open(image_path) | ||
|
|
||
| inputs = processor("<|image|>Describe this image", images=images) | ||
|
|
||
| assert inputs is not None | ||
| assert "token_type_ids" in inputs | ||
|
|
||
|
|
||
| def test_gemma4_vision_model_io(test_data_path): | ||
| """Validate the vision ONNX model has expected inputs and outputs.""" | ||
| onnx = pytest.importorskip("onnx") | ||
|
|
||
| model = onnx.load(_get_onnx_path(test_data_path, "dummy_vision.onnx")) | ||
| input_names = {inp.name for inp in model.graph.input} | ||
| output_names = {out.name for out in model.graph.output} | ||
|
|
||
| assert "pixel_values" in input_names | ||
| assert "pixel_position_ids" in input_names | ||
| assert "image_features" in output_names | ||
|
|
||
| pv_input = next(i for i in model.graph.input if i.name == "pixel_values") | ||
| assert pv_input.type.tensor_type.elem_type == onnx.TensorProto.FLOAT, \ | ||
| "pixel_values must be float32" | ||
|
|
||
| dim0 = pv_input.type.tensor_type.shape.dim[0] | ||
| assert dim0.dim_param != "", \ | ||
| f"pixel_values dim-0 should be dynamic, got static dim_value={dim0.dim_value}" | ||
|
|
||
|
|
||
| def test_gemma4_embedding_model_io(test_data_path): | ||
| """Validate the embedding ONNX model has expected inputs and outputs.""" | ||
| onnx = pytest.importorskip("onnx") | ||
|
|
||
| model = onnx.load(_get_onnx_path(test_data_path, "dummy_embedding.onnx")) | ||
| input_names = {inp.name for inp in model.graph.input} | ||
| output_names = {out.name for out in model.graph.output} | ||
|
|
||
| assert "input_ids" in input_names | ||
| assert "image_features" in input_names | ||
| assert "token_type_ids" in input_names, "Gemma4 embedding requires token_type_ids" | ||
| assert "inputs_embeds" in output_names | ||
|
apsonawane marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def test_gemma4_text_model_io(test_data_path): | ||
| """Validate the text/decoder ONNX model has expected inputs and outputs.""" | ||
| onnx = pytest.importorskip("onnx") | ||
|
|
||
| model = onnx.load(_get_onnx_path(test_data_path, "dummy_text.onnx")) | ||
| input_names = {inp.name for inp in model.graph.input} | ||
| output_names = {out.name for out in model.graph.output} | ||
|
|
||
| assert "inputs_embeds" in input_names, "Decoder must accept inputs_embeds" | ||
| assert "attention_mask" in input_names | ||
| assert "position_ids" in input_names | ||
| assert "past_key_values.0.key" in input_names, "Decoder must have KV cache inputs" | ||
| assert "past_key_values.0.value" in input_names | ||
|
|
||
| assert "logits" in output_names | ||
| assert "present.0.key" in output_names, "Decoder must have KV cache outputs" | ||
| assert "present.0.value" in output_names | ||
|
|
||
|
|
||
| # Standalone runner functionality | ||
| def run_gemma4_vision_tests( | ||
| cwd: str | bytes | os.PathLike, | ||
| log: logging.Logger, | ||
| test_models: str | bytes | os.PathLike, | ||
| ): | ||
| """Run the vision model tests using pytest.""" | ||
| log.debug("Running: Gemma4 Vision Model Tests") | ||
|
|
||
| command = [ | ||
| sys.executable, | ||
| "-m", | ||
| "pytest", | ||
| "-sv", | ||
| "test_gemma4_models.py", | ||
| "--test_models", | ||
| test_models, | ||
| ] | ||
| run_subprocess(command, cwd=cwd, log=log).check_returncode() | ||
|
|
||
|
|
||
| def parse_arguments(): | ||
| """Parse command line arguments for standalone execution.""" | ||
| parser = argparse.ArgumentParser(description="Test runner for Gemma4 vision models") | ||
| parser.add_argument( | ||
| "--cwd", | ||
| help="Path to the current working directory", | ||
| default=Path(__file__).parent.resolve().absolute(), | ||
| ) | ||
| parser.add_argument( | ||
| "--test_models", | ||
| help="Path to the test_models directory", | ||
| default=Path(__file__).parent.parent.resolve().absolute() / "test_models", | ||
| ) | ||
| return parser.parse_args() | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point for standalone execution.""" | ||
| args = parse_arguments() | ||
|
|
||
| log.info("Running Gemma4 vision model tests") | ||
| log.info(f"Test models path: {args.test_models}") | ||
| log.info(f"Working directory: {args.cwd}") | ||
|
|
||
| run_gemma4_vision_tests(os.path.abspath(args.cwd), log, os.path.abspath(args.test_models)) | ||
|
|
||
| log.info("All tests completed successfully!") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Binary file not shown.
Binary file not shown.
82 changes: 82 additions & 0 deletions
82
test/test_models/gemma4-vision-preprocessing/genai_config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| { | ||
| "model": { | ||
| "bos_token_id": 2, | ||
| "context_length": 32768, | ||
| "decoder": { | ||
| "session_options": { | ||
| "log_id": "onnxruntime-genai", | ||
| "provider_options": [] | ||
| }, | ||
| "filename": "dummy_text.onnx", | ||
| "head_size": 256, | ||
| "hidden_size": 2048, | ||
| "inputs": { | ||
| "inputs_embeds": "inputs_embeds", | ||
| "attention_mask": "attention_mask", | ||
| "position_ids": "position_ids", | ||
| "past_key_names": "past_key_values.%d.key", | ||
| "past_value_names": "past_key_values.%d.value" | ||
| }, | ||
| "outputs": { | ||
| "logits": "logits", | ||
| "present_key_names": "present.%d.key", | ||
| "present_value_names": "present.%d.value" | ||
| }, | ||
| "num_attention_heads": 8, | ||
| "num_hidden_layers": 1, | ||
| "num_key_value_heads": 4 | ||
| }, | ||
| "eos_token_id": [ | ||
| 1, | ||
| 106 | ||
| ], | ||
| "pad_token_id": 0, | ||
| "type": "gemma4", | ||
| "vocab_size": 262144, | ||
| "embedding": { | ||
| "filename": "dummy_embedding.onnx", | ||
| "inputs": { | ||
| "input_ids": "input_ids", | ||
| "image_features": "image_features" | ||
| }, | ||
| "outputs": { | ||
| "inputs_embeds": "inputs_embeds" | ||
| }, | ||
| "session_options": { | ||
| "log_id": "onnxruntime-genai", | ||
| "provider_options": [] | ||
| } | ||
| }, | ||
| "vision": { | ||
| "filename": "dummy_vision.onnx", | ||
| "config_filename": "processor_config.json", | ||
| "inputs": { | ||
| "pixel_values": "pixel_values", | ||
| "pixel_position_ids": "pixel_position_ids" | ||
| }, | ||
| "outputs": { | ||
| "image_features": "image_features" | ||
| }, | ||
| "session_options": { | ||
| "log_id": "onnxruntime-genai", | ||
| "provider_options": [] | ||
| } | ||
| } | ||
| }, | ||
| "search": { | ||
| "diversity_penalty": 0.0, | ||
| "do_sample": false, | ||
| "early_stopping": true, | ||
| "length_penalty": 1.0, | ||
| "max_length": 32768, | ||
| "min_length": 0, | ||
| "no_repeat_ngram_size": 0, | ||
| "num_beams": 1, | ||
| "num_return_sequences": 1, | ||
| "past_present_share_buffer": true, | ||
| "repetition_penalty": 1.0, | ||
| "temperature": 1.0, | ||
| "top_k": 1, | ||
| "top_p": 1.0 | ||
| } | ||
| } |
27 changes: 27 additions & 0 deletions
27
test/test_models/gemma4-vision-preprocessing/processor_config.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| { | ||
| "processor": { | ||
| "name": "gemma4_image_processor", | ||
| "transforms": [ | ||
| { | ||
| "operation": { | ||
| "name": "decode_image", | ||
| "type": "DecodeImage", | ||
| "attrs": { | ||
| "color_space": "RGB" | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "operation": { | ||
| "name": "gemma4_image_transform", | ||
| "type": "Gemma4ImageTransform", | ||
| "attrs": { | ||
| "patch_size": 16, | ||
| "max_soft_tokens": 280, | ||
| "pooling_kernel_size": 3 | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.