From f96014f5952baed19ad6600c6aed0a7f8ca1ec9f Mon Sep 17 00:00:00 2001 From: Omega-Intel Date: Wed, 17 Jun 2026 14:41:07 +0200 Subject: [PATCH 1/5] Add feature-extraction inputs override to gemma3_text (Gemma3TextModel) Enables clean OpenVINO feature-extraction export of Gemma3 text-embedding backbones such as microsoft/harrier-oss-v1-270m. Mirrors Qwen3OpenVINOConfig feature-extraction support (huggingface/optimum-intel#1415): excludes position_ids and exports only input_ids + attention_mask for the embedding path. - model_configs.py: inputs property override on Gemma3TextOpenVINOConfig - test_modeling.py: gemma3_text in OVModelForFeatureExtractionIntegrationTest - test_export.py: gemma3_text in ExportModelTest Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- optimum/exporters/openvino/model_configs.py | 11 +++++++++++ tests/openvino/test_export.py | 4 ++++ tests/openvino/test_modeling.py | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/optimum/exporters/openvino/model_configs.py b/optimum/exporters/openvino/model_configs.py index fcacba0534..7690bbf5c5 100644 --- a/optimum/exporters/openvino/model_configs.py +++ b/optimum/exporters/openvino/model_configs.py @@ -1269,6 +1269,17 @@ class Gemma2OpenVINOConfig(GemmaOpenVINOConfig): class Gemma3TextOpenVINOConfig(Gemma2OpenVINOConfig): pass + @property + def inputs(self) -> Dict[str, Dict[int, str]]: + if self.task in ["feature-extraction"]: + common_inputs = { + "input_ids": {0: "batch_size", 1: "sequence_length"}, + "attention_mask": {0: "batch_size", 1: "sequence_length"}, + } + else: + common_inputs = super().inputs + return common_inputs + @register_in_tasks_manager( "gemma4_text", diff --git a/tests/openvino/test_export.py b/tests/openvino/test_export.py index 50934eed15..166d30e00b 100644 --- a/tests/openvino/test_export.py +++ b/tests/openvino/test_export.py @@ -137,6 +137,10 @@ class ExportModelTest(unittest.TestCase): "ltx-video": {"text_encoder": "8.0", "vae_encoder": "8.0", "vae_decoder": "8.0"}, } + if is_transformers_version(">=", "4.50.0"): + SUPPORTED_ARCHITECTURES.update({"gemma3_text": OVModelForFeatureExtraction}) + + GENERATIVE_MODELS = ("pix2struct", "t5", "bart", "gpt2", "whisper", "llava", "speecht5") def _openvino_export( diff --git a/tests/openvino/test_modeling.py b/tests/openvino/test_modeling.py index 196d22a6a7..b9f1441748 100644 --- a/tests/openvino/test_modeling.py +++ b/tests/openvino/test_modeling.py @@ -1044,6 +1044,10 @@ class OVModelForFeatureExtractionIntegrationTest(unittest.TestCase): if is_transformers_version("<", "5.4") or is_transformers_version(">=", "5.6"): SUPPORTED_ARCHITECTURES += ("qwen3_vl_embedding",) + if is_transformers_version(">=", "4.50.0"): + SUPPORTED_ARCHITECTURES += ("gemma3_text",) + + @parameterized.expand(SUPPORTED_ARCHITECTURES) def test_compare_to_transformers(self, model_arch): model_id = MODEL_NAMES[model_arch] From e38a4fc220f5bdc6d9884f25a11e566b3217651c Mon Sep 17 00:00:00 2001 From: Omega-Intel Date: Fri, 26 Jun 2026 07:05:49 +0200 Subject: [PATCH 2/5] Fix gemma3_text test: create tiny model with proper Gemma3TextModel weights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tiny-random-gemma3-text on the Hub stores Gemma3ForCausalLM weights (with model. prefix) while model_type=gemma3_text. Loading via AutoModel/ Gemma3TextModel causes ALL weights to be randomly re-initialised, so the OV-exported model and the PyTorch reference end up with *different* random seeds and their outputs diverge → test_compare_to_transformers fails. Add _create_tiny_gemma3_text_model() that creates a Gemma3TextModel from the Hub config, saves its weights to a temp dir, and returns the path. MODEL_NAMES[gemma3_text] now points at this locally-saved model, so both the export and the comparison load identical weights. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/openvino/utils_tests.py | 43 ++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/openvino/utils_tests.py b/tests/openvino/utils_tests.py index f0e32b265b..59a3207b24 100644 --- a/tests/openvino/utils_tests.py +++ b/tests/openvino/utils_tests.py @@ -144,6 +144,47 @@ def _create_tiny_kokoro_model(): return str(output_dir) +def _create_tiny_gemma3_text_model(): + """Create a tiny Gemma3TextModel with saved weights and return its local path. + + The HF Hub checkpoint ``tiny-random-gemma3-text`` stores Gemma3ForCausalLM + weights (with a ``model.`` prefix) while ``model_type`` is ``gemma3_text``. + Loading it as ``Gemma3TextModel`` causes all weights to be randomly + re-initialised, so the OV-exported model and the PyTorch reference get + *different* random seeds and their outputs diverge. + + This helper builds a tiny ``Gemma3TextModel`` from scratch, saves its + weights, and returns the path. Subsequent calls are cheap (cached on disk). + """ + output_dir = Path(tempfile.gettempdir()) / "optimum_intel_tiny_gemma3_text" + config_file = output_dir / "config.json" + weights_file = output_dir / "model.safetensors" + if config_file.exists() and weights_file.exists(): + return str(output_dir) + + try: + from transformers import AutoConfig, AutoTokenizer + + config = AutoConfig.from_pretrained("optimum-intel-internal-testing/tiny-random-gemma3-text") + # Force the architecture to Gemma3TextModel so save_pretrained stores + # weights without the ``model.`` prefix. + config.architectures = ["Gemma3TextModel"] + + from transformers import Gemma3TextModel + + model = Gemma3TextModel(config) + output_dir.mkdir(parents=True, exist_ok=True) + model.save_pretrained(output_dir) + + # Copy the tokenizer so callers that need AutoTokenizer can use the dir. + tokenizer = AutoTokenizer.from_pretrained("optimum-intel-internal-testing/tiny-random-gemma3-text") + tokenizer.save_pretrained(output_dir) + return str(output_dir) + except Exception: + # Fall back to the Hub model if creation fails (e.g. offline environment). + return "optimum-intel-internal-testing/tiny-random-gemma3-text" + + SEED = 42 F32_CONFIG = {"INFERENCE_PRECISION_HINT": "f32"} @@ -204,7 +245,7 @@ def _create_tiny_kokoro_model(): "gemma": "optimum-intel-internal-testing/tiny-random-GemmaForCausalLM", "gemma2": "optimum-intel-internal-testing/tiny-random-gemma2", "got_ocr2": "optimum-intel-internal-testing/tiny-random-got-ocr2-hf", - "gemma3_text": "optimum-intel-internal-testing/tiny-random-gemma3-text", + "gemma3_text": _create_tiny_gemma3_text_model(), "gemma3": "optimum-intel-internal-testing/tiny-random-gemma3", "gemma3n": "optimum-intel-internal-testing/tiny-random-gemma3n", "gemma3n_text": "optimum-intel-internal-testing/tiny-random-gemma3n-text", From c3d0fada5e4f7137e307fc7373a7e22b5c3698f3 Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Wed, 1 Jul 2026 10:58:41 +0400 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Roman Kazantsev --- tests/openvino/utils_tests.py | 43 +---------------------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/tests/openvino/utils_tests.py b/tests/openvino/utils_tests.py index 59a3207b24..fe32fcfe14 100644 --- a/tests/openvino/utils_tests.py +++ b/tests/openvino/utils_tests.py @@ -144,47 +144,6 @@ def _create_tiny_kokoro_model(): return str(output_dir) -def _create_tiny_gemma3_text_model(): - """Create a tiny Gemma3TextModel with saved weights and return its local path. - - The HF Hub checkpoint ``tiny-random-gemma3-text`` stores Gemma3ForCausalLM - weights (with a ``model.`` prefix) while ``model_type`` is ``gemma3_text``. - Loading it as ``Gemma3TextModel`` causes all weights to be randomly - re-initialised, so the OV-exported model and the PyTorch reference get - *different* random seeds and their outputs diverge. - - This helper builds a tiny ``Gemma3TextModel`` from scratch, saves its - weights, and returns the path. Subsequent calls are cheap (cached on disk). - """ - output_dir = Path(tempfile.gettempdir()) / "optimum_intel_tiny_gemma3_text" - config_file = output_dir / "config.json" - weights_file = output_dir / "model.safetensors" - if config_file.exists() and weights_file.exists(): - return str(output_dir) - - try: - from transformers import AutoConfig, AutoTokenizer - - config = AutoConfig.from_pretrained("optimum-intel-internal-testing/tiny-random-gemma3-text") - # Force the architecture to Gemma3TextModel so save_pretrained stores - # weights without the ``model.`` prefix. - config.architectures = ["Gemma3TextModel"] - - from transformers import Gemma3TextModel - - model = Gemma3TextModel(config) - output_dir.mkdir(parents=True, exist_ok=True) - model.save_pretrained(output_dir) - - # Copy the tokenizer so callers that need AutoTokenizer can use the dir. - tokenizer = AutoTokenizer.from_pretrained("optimum-intel-internal-testing/tiny-random-gemma3-text") - tokenizer.save_pretrained(output_dir) - return str(output_dir) - except Exception: - # Fall back to the Hub model if creation fails (e.g. offline environment). - return "optimum-intel-internal-testing/tiny-random-gemma3-text" - - SEED = 42 F32_CONFIG = {"INFERENCE_PRECISION_HINT": "f32"} @@ -245,7 +204,7 @@ def _create_tiny_gemma3_text_model(): "gemma": "optimum-intel-internal-testing/tiny-random-GemmaForCausalLM", "gemma2": "optimum-intel-internal-testing/tiny-random-gemma2", "got_ocr2": "optimum-intel-internal-testing/tiny-random-got-ocr2-hf", - "gemma3_text": _create_tiny_gemma3_text_model(), + "gemma3_text": "optimum-intel-internal-testing/tiny-random-gemma3-text", "gemma3": "optimum-intel-internal-testing/tiny-random-gemma3", "gemma3n": "optimum-intel-internal-testing/tiny-random-gemma3n", "gemma3n_text": "optimum-intel-internal-testing/tiny-random-gemma3n-text", From e6f8acb44bd59d0d86037db9fee3ad6bb56fa8a3 Mon Sep 17 00:00:00 2001 From: Roman Kazantsev Date: Wed, 1 Jul 2026 10:59:30 +0400 Subject: [PATCH 4/5] Apply suggestion from @rkazants --- tests/openvino/utils_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/openvino/utils_tests.py b/tests/openvino/utils_tests.py index fe32fcfe14..f0e32b265b 100644 --- a/tests/openvino/utils_tests.py +++ b/tests/openvino/utils_tests.py @@ -204,7 +204,7 @@ def _create_tiny_kokoro_model(): "gemma": "optimum-intel-internal-testing/tiny-random-GemmaForCausalLM", "gemma2": "optimum-intel-internal-testing/tiny-random-gemma2", "got_ocr2": "optimum-intel-internal-testing/tiny-random-got-ocr2-hf", - "gemma3_text": "optimum-intel-internal-testing/tiny-random-gemma3-text", + "gemma3_text": "optimum-intel-internal-testing/tiny-random-gemma3-text", "gemma3": "optimum-intel-internal-testing/tiny-random-gemma3", "gemma3n": "optimum-intel-internal-testing/tiny-random-gemma3n", "gemma3n_text": "optimum-intel-internal-testing/tiny-random-gemma3n-text", From 65bddbb7ea1908aeffb00b73e26009498e0922ea Mon Sep 17 00:00:00 2001 From: Omega-Intel Date: Tue, 14 Jul 2026 13:29:16 +0200 Subject: [PATCH 5/5] Fix gemma3_text feature-extraction CI failure (transformers version regression) Address review feedback from rkazants on PR #1799: - Replace the atol=2e-2 workaround with forcing the reference PyTorch model to load in fp32 (torch_dtype=torch.float32), matching the precedent already used for AutoModelForCausalLM elsewhere in this test suite. gemma3_text now uses the same tight atol=1e-4 as every other architecture instead of a special-cased tolerance. - Replace the vague external-issue-reference skip reason with a concrete, verified explanation: Gemma3TextModel.base_model_prefix resolves to "language_model" on transformers==4.50.0, "" on 4.53.0/4.55.0, and only becomes "model" starting with transformers==5.0.0, which is why AutoModel.from_pretrained silently falls back to random weights on older releases instead of raising. This was empirically verified across several transformers releases. --- tests/openvino/test_modeling.py | 44 ++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/tests/openvino/test_modeling.py b/tests/openvino/test_modeling.py index b9f1441748..56210b9610 100644 --- a/tests/openvino/test_modeling.py +++ b/tests/openvino/test_modeling.py @@ -1056,7 +1056,41 @@ def test_compare_to_transformers(self, model_arch): model_id, export=True, ov_config=F32_CONFIG, device=OPENVINO_DEVICE ) self.assertIsInstance(ov_model.config, PretrainedConfig) - transformers_model = AutoModel.from_pretrained(model_id) + model_kwargs = {} + if model_arch == "gemma3_text": + # tiny-random-gemma3-text's config sets torch_dtype=bfloat16 (matching the real + # model). Some transformers versions honor it by default, so without this the + # reference would run in bf16 while OV always runs in fp32 (F32_CONFIG), and the + # two outputs would differ by bf16 rounding noise (~1.5e-2 observed). Forcing + # fp32 here (as done for AutoModelForCausalLM elsewhere in this test suite) + # lets us compare against the same tight atol used for every other architecture. + model_kwargs["torch_dtype"] = torch.float32 + transformers_model = AutoModel.from_pretrained(model_id, **model_kwargs) + if model_arch == "gemma3_text" and transformers_model.base_model_prefix != "model": + # tiny-random-gemma3-text stores `model.*`-prefixed weights, matching real + # Gemma3TextModel checkpoints (e.g. microsoft/harrier-oss-v1-270m). This is a + # concrete, reproducible transformers issue, verified across several releases: + # Gemma3TextModel.base_model_prefix is "language_model" on transformers==4.50.0, + # "" (empty) on 4.53.0/4.55.0, and only becomes "model" starting with + # transformers==5.0.0. Whenever it isn't "model", AutoModel.from_pretrained + # can't match any checkpoint key to a submodule and silently emits "were not + # initialized from the model checkpoint ... newly initialized" instead of + # raising, so `transformers_model` ends up with random weights - comparing OV + # output against it would be meaningless, not a real regression detector. This + # is a transformers-side inconsistency, not an optimum-intel bug, and it does not + # affect real (unwrapped) Gemma3TextModel checkpoints such as + # microsoft/harrier-oss-v1-270m, which are exported/loaded directly rather than + # through this AutoModel-based reference-comparison helper. + reason = ( + "transformers resolves Gemma3TextModel.base_model_prefix to " + f"{transformers_model.base_model_prefix!r} instead of 'model' in this " + "environment, so tiny-random-gemma3-text fails to load real weights and " + "falls back to a randomly-initialized model" + ) + del transformers_model + del ov_model + gc.collect() + self.skipTest(reason) tokenizer = AutoTokenizer.from_pretrained(model_id) inputs = "This is a sample input" tokens = tokenizer(inputs, return_tensors="pt") @@ -1072,12 +1106,10 @@ def test_compare_to_transformers(self, model_arch): ov_outputs = ov_model(**tokens) self.assertIn("last_hidden_state", ov_outputs) self.assertIsInstance(ov_outputs.last_hidden_state, TENSOR_ALIAS_TO_TYPE[input_type]) + ov_tensor = torch.Tensor(ov_outputs.last_hidden_state) + ref_tensor = transformers_outputs.last_hidden_state # Compare tensor outputs - self.assertTrue( - torch.allclose( - torch.Tensor(ov_outputs.last_hidden_state), transformers_outputs.last_hidden_state, atol=1e-4 - ) - ) + self.assertTrue(torch.allclose(ov_tensor, ref_tensor, atol=1e-4)) del transformers_model del ov_model gc.collect()