Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/test_openvino_preview_models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ jobs:
pytest tests/openvino/${{ matrix.test-pattern }} -k qwen3_5_compressed_tensors --durations=0
uv pip uninstall compressed-tensors

- name: MiniCPM-V-4.7 Validation
run: |
uv pip install "transformers>=5.18"
pytest tests/openvino/${{ matrix.test-pattern }} -k "minicpmv4_7" --durations=0

# test_quantization, test_cb_compatible and test_dflash are not matched by the matrix test-pattern glob, so run
# them explicitly and only once (the export job).
- name: MiniCPM-V-4.7 Quantization, Continuous Batching and DFlash Validation
Expand Down
17 changes: 13 additions & 4 deletions optimum/exporters/openvino/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@
if is_transformers_version(">=", "4.55"):
from transformers import Mxfp4Config

FORCE_ATTN_MODEL_CLASSES = {"phi3_v": "eager", "gemma2": "sdpa", "llama4": "sdpa"}
FORCE_ATTN_MODEL_CLASSES = {
"phi3_v": "eager",
# gemma2 does not support sdpa since transformers 5.21 (attention logit softcapping)
"gemma2": "eager" if is_transformers_version(">=", "5.21") else "sdpa",
"llama4": "sdpa",
}

if TYPE_CHECKING:
from optimum.intel.openvino.configuration import OVConfig
Expand Down Expand Up @@ -412,6 +417,7 @@ def main_export(

do_gptq_patching = False
do_quant_patching = False
do_awq_cuda_patching = False
patch_16bit = False
loading_kwargs = model_loading_kwargs or {}
if variant is not None:
Expand Down Expand Up @@ -451,6 +457,7 @@ def main_export(
do_gptq_patching = quant_method == "gptq"
do_bitnet_patching = quant_method == "bitnet"
do_ct_patching = quant_method == "compressed-tensors"
do_awq_cuda_patching = quant_method == "awq" and is_transformers_version("<", "5")

# compressed-tensors export relies on the OpenVINO PyTorch frontend
# compressed-tensors patcher, which is only available since 2026.3.
Expand Down Expand Up @@ -563,8 +570,9 @@ def main_export(
loading_kwargs["torch_dtype"] = dtype
# Patch the modules to export of GPTQ models w/o GPU
if do_quant_patching:
orig_cuda_check = torch.cuda.is_available
torch.cuda.is_available = lambda: True
if do_awq_cuda_patching:
orig_cuda_check = torch.cuda.is_available
torch.cuda.is_available = lambda: True

if do_gptq_patching:
from optimum.gptq import GPTQQuantizer
Expand Down Expand Up @@ -811,7 +819,8 @@ def bitnet_load_hook(self, state_dict, prefix, *args, **kwargs):
finally:
# Unpatch modules after quantized model export
if do_quant_patching:
torch.cuda.is_available = orig_cuda_check
if do_awq_cuda_patching:
torch.cuda.is_available = orig_cuda_check
if do_gptq_patching:
GPTQQuantizer.post_init_model = orig_post_init_model
if do_bitnet_patching:
Expand Down
10 changes: 6 additions & 4 deletions optimum/exporters/openvino/model_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@
GRANITEMOEHYBRID_ATTENTION_LAYER_TYPE,
GRANITEMOEHYBRID_MAMBA_LAYER_TYPE,
LTX2_ROPE_INPUTS,
get_qwen3_omni_thinker_encoders_owner,
get_qwen3_omni_thinker_text_model,
has_ltx2_extended_guidance_inputs,
)
from optimum.exporters.tasks import TasksManager
Expand Down Expand Up @@ -4921,25 +4923,25 @@ def get_model_for_behavior(model, behavior: Union[str, Qwen3OmniMoeConfigBehavio
return model

if behavior == Qwen3OmniMoeConfigBehavior.TEXT_EMBEDDINGS:
text_embedding = model.thinker.model.get_input_embeddings()
text_embedding = get_qwen3_omni_thinker_text_model(model.thinker).get_input_embeddings()
text_embedding.config = model.config
return text_embedding

if behavior == Qwen3OmniMoeConfigBehavior.VISION_EMBEDDINGS:
thinker_config = getattr(model.config, "thinker_config", model.config)
vision_model = model.thinker.visual
vision_model = get_qwen3_omni_thinker_encoders_owner(model.thinker).visual
vision_model.config = getattr(thinker_config, "vision_config", thinker_config)
return vision_model

if behavior == Qwen3OmniMoeConfigBehavior.VISION_EMBEDDINGS_POS:
thinker_config = getattr(model.config, "thinker_config", model.config)
vision_pos = model.thinker.visual.pos_embed
vision_pos = get_qwen3_omni_thinker_encoders_owner(model.thinker).visual.pos_embed
vision_pos.config = getattr(thinker_config, "vision_config", thinker_config)
return vision_pos

if behavior == Qwen3OmniMoeConfigBehavior.AUDIO_ENCODER:
thinker_config = getattr(model.config, "thinker_config", model.config)
audio_encoder = model.thinker.audio_tower
audio_encoder = get_qwen3_omni_thinker_encoders_owner(model.thinker).audio_tower
audio_encoder.config = getattr(thinker_config, "audio_config", thinker_config)
return audio_encoder

Expand Down
25 changes: 18 additions & 7 deletions optimum/exporters/openvino/model_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
GRANITEMOEHYBRID_ATTENTION_LAYER_TYPE,
GRANITEMOEHYBRID_MAMBA_LAYER_TYPE,
LTX2_ROPE_INPUTS,
get_qwen3_omni_thinker_text_model,
has_ltx2_extended_guidance_inputs,
)
from optimum.intel.utils.import_utils import (
Expand Down Expand Up @@ -5168,7 +5169,7 @@ def lm_forward(
use_cache=True,
):
pkv = DynamicCache(past_key_values)
outputs = self.thinker.model(
outputs = get_qwen3_omni_thinker_text_model(self.thinker)(
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
Expand All @@ -5187,7 +5188,7 @@ def lm_forward(
model.__orig_forward = model.forward
model.forward = types.MethodType(lm_forward, model)

thinker_model = model.thinker.model
thinker_model = get_qwen3_omni_thinker_text_model(model.thinker)
thinker_model.__orig_deepstack_process = thinker_model._deepstack_process
thinker_model._deepstack_process = types.MethodType(_deepstack_process_patched, thinker_model)

Expand All @@ -5196,7 +5197,7 @@ def lm_forward(
def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
self._model.forward = self._model.__orig_forward
thinker_model = self._model.thinker.model
thinker_model = get_qwen3_omni_thinker_text_model(self._model.thinker)
thinker_model._deepstack_process = thinker_model.__orig_deepstack_process


Expand Down Expand Up @@ -5678,11 +5679,15 @@ def __enter__(self):
super().__enter__()

self._model.config._orig_attn_implementation = self._model.config._attn_implementation
self._model.config._attn_implementation = "sdpa"
# gemma2 does not support sdpa since transformers 5.21
self._use_sdpa = is_transformers_version("<", "5.21")
if self._use_sdpa:
self._model.config._attn_implementation = "sdpa"

# starting from 4.53, we get unmatching outputs if we use the boolean mask
# TODO: This is an openvino issue (inconsistency between boolean and float masks)
ALL_MASK_ATTENTION_FUNCTIONS.register("sdpa", eager_mask_without_vmap)
if self._use_sdpa:
ALL_MASK_ATTENTION_FUNCTIONS.register("sdpa", eager_mask_without_vmap)

def __exit__(self, exc_type, exc_value, traceback):
super().__exit__(exc_type, exc_value, traceback)
Expand All @@ -5691,7 +5696,8 @@ def __exit__(self, exc_type, exc_value, traceback):
del self._model.config._orig_attn_implementation

# remove the eager_mask_without_vmap from the ALL_MASK_ATTENTION_FUNCTIONS
ALL_MASK_ATTENTION_FUNCTIONS.register("sdpa", sdpa_mask)
if self._use_sdpa:
ALL_MASK_ATTENTION_FUNCTIONS.register("sdpa", sdpa_mask)


class MiniCPMModelPatcher(OVDecoderModelPatcher):
Expand Down Expand Up @@ -10661,6 +10667,9 @@ def __init__(self, config, model, model_kwargs):

def patched_forward(pixel_values, image_position_ids):
outputs = embed_vision(pixel_values, image_position_ids)
if is_transformers_version(">=", "5.16"):
outputs = outputs.pooler_output

return {"last_hidden_state": outputs}

self.patched_forward = patched_forward
Expand Down Expand Up @@ -12693,7 +12702,9 @@ def image_embed_forward(self, pixel_values, image_grid_thw):

# --- rotary position ids (raster (h, w) flipped + 1, matches native) ---
position_ids = torch.stack([row, col], dim=-1).flip(-1) + 1
position_ids = position_ids[None]
# transformers < 5.16 expects batched (1, N, 2) vision rotary position ids, recent version expects (N, 2)
if is_transformers_version("<", "5.16"):
position_ids = position_ids[None]

# --- pixel-shuffle gather (merge_size x merge_size block reorder) ---
factor = cfg.merge_size
Expand Down
18 changes: 18 additions & 0 deletions optimum/exporters/openvino/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,24 @@ def load_preprocessors(
return preprocessors


def get_qwen3_omni_thinker_text_model(thinker):
"""
Returns the Qwen3-Omni Thinker text decoder (the module taking `inputs_embeds` and exposing `embed_tokens`).

Older transformers: `thinker.model` is the text model itself.
Newer transformers: `thinker.model` is a wrapper holding `visual`, `audio_tower` and `language_model`.
"""
return getattr(thinker.model, "language_model", thinker.model)


def get_qwen3_omni_thinker_encoders_owner(thinker):
"""
Returns the module that owns the `visual` and `audio_tower` encoders of a Qwen3-Omni Thinker
(`thinker` on older transformers, `thinker.model` on newer ones).
"""
return thinker.model if hasattr(thinker.model, "visual") else thinker


# TODO: remove load_minicpmv4_7_processor once official 4.7 checkpoints are published
def load_minicpmv4_7_processor(model_name_or_path: Union[str, Path], subfolder: str = ""):
from transformers import (
Expand Down
8 changes: 6 additions & 2 deletions optimum/intel/openvino/modeling_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1498,8 +1498,8 @@ def _update_model_kwargs_for_generation(
elif model_kwargs.get("cache_params") is not None:
model_kwargs["cache_position"] = torch.tensor([num_new_tokens], dtype=torch.long)

if "attention_mask" in model_kwargs:
attention_mask = model_kwargs["attention_mask"]
attention_mask = model_kwargs.get("attention_mask")
if attention_mask is not None:
model_kwargs["attention_mask"] = torch.cat(
[attention_mask, attention_mask.new_ones((attention_mask.shape[0], 1))], dim=-1
)
Expand Down Expand Up @@ -1533,6 +1533,10 @@ def prepare_inputs_for_generation(
cache_position = torch.tensor([self._past_length], device=input_ids.device)

if cache_position[0] > 0:
if attention_mask is None:
# recent transformers drop the attention mask in `generate` when it is all ones (no padding),
# restore it at the full context length before keeping only the last token
attention_mask = torch.ones_like(input_ids, dtype=torch.int64)
# decoding stage so it takes the last token
input_ids = input_ids[:, -1].unsqueeze(-1)

Expand Down
72 changes: 53 additions & 19 deletions optimum/intel/openvino/modeling_visual_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@
from transformers.models.qwen2_vl.modeling_qwen2_vl import Qwen2VLModel
from transformers.utils import ModelOutput


try:
from transformers.models.qwen2_vl.modeling_qwen2_vl import VisionRotaryEmbedding
except ImportError:
# renamed in newer transformers releases
from transformers.models.qwen2_vl.modeling_qwen2_vl import Qwen2VLVisionRotaryEmbedding as VisionRotaryEmbedding

from optimum.exporters.openvino import main_export
from optimum.exporters.openvino.stateful import ensure_stateful_is_available, model_has_input_output_name
from optimum.exporters.openvino.utils import save_config
Expand All @@ -64,16 +57,37 @@
TemporaryDirectory,
classproperty,
)
from optimum.intel.utils.import_utils import is_transformers_version
from optimum.intel.utils import is_transformers_version


if is_transformers_version(">=", "4.57"):
from transformers.models.qwen3_omni_moe.processing_qwen3_omni_moe import _get_feat_extract_output_lengths
from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLModel,
Qwen3VLVisionModel,
Qwen3VLVisionRotaryEmbedding,
)
from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLModel, Qwen3VLVisionModel

if is_transformers_version("<", "5.17"):
from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLVisionRotaryEmbedding

if is_transformers_version("<", "5.17"):
try:
from transformers.models.qwen2_vl.modeling_qwen2_vl import VisionRotaryEmbedding
except ImportError:
# renamed in newer transformers releases
from transformers.models.qwen2_vl.modeling_qwen2_vl import (
Qwen2VLVisionRotaryEmbedding as VisionRotaryEmbedding,
)
else:
# VisionRotaryEmbedding now takes a config and returns cos/sin, which is not what the OpenVINO vision embeddings model consumes
# https://github.com/huggingface/transformers/blob/v5.15.0/src/transformers/models/qwen2_vl/modeling_qwen2_vl.py#L239
class VisionRotaryEmbedding(torch.nn.Module):
def __init__(self, dim: int, theta: float = 10000.0) -> None:
super().__init__()
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float) / dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)

def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
return (position_ids.unsqueeze(-1) * self.inv_freq).flatten(1)

Qwen3VLVisionRotaryEmbedding = VisionRotaryEmbedding


if is_transformers_version(">=", "5.0.0"):
Expand Down Expand Up @@ -1646,6 +1660,10 @@ def _filter_unattended_tokens(self, input_ids, attention_mask, past_key_values):
target_length = input_ids.shape[1]
past_length = self.language_model._get_past_length(past_key_values)

if attention_mask is None:
# newer transformers no longer materialize a default attention mask in generate()
attention_mask = torch.ones(input_ids.shape, dtype=torch.long, device=input_ids.device)

extended_attention_mask = torch.ones(
(attention_mask.shape[0], past_length),
dtype=attention_mask.dtype,
Expand Down Expand Up @@ -4426,7 +4444,16 @@ def generate(self, *args, **kwargs):
_OVQwen3VLForCausalLM.get_placeholder_mask = Qwen3VLModel.get_placeholder_mask
_OVQwen3VLForCausalLM.get_rope_index = Qwen3VLModel.get_rope_index
_OVQwen3VLForCausalLM.get_video_features = Qwen3VLModel.get_video_features
_OVQwen3VLForCausalLM.rot_pos_emb = getattr(Qwen3VLVisionModel, "rot_pos_emb", None)
if hasattr(Qwen3VLVisionModel, "rot_pos_emb"):
_OVQwen3VLForCausalLM.rot_pos_emb = Qwen3VLVisionModel.rot_pos_emb
else:
# rot_pos_emb removed since transformers v5.17
def _qwen3_vl_rot_pos_emb(self, grid_thw):
from transformers.vision_utils import get_vision_position_ids

return self.rotary_pos_emb(get_vision_position_ids(grid_thw, self.spatial_merge_size))

_OVQwen3VLForCausalLM.rot_pos_emb = _qwen3_vl_rot_pos_emb
_OVQwen3VLForCausalLM.get_vision_position_ids = getattr(Qwen3VLModel, "get_vision_position_ids", None)


Expand Down Expand Up @@ -5696,6 +5723,11 @@ def _update_model_kwargs_for_generation(
return model_kwargs


def _flatten_video_frames(tensor):
# video tensors are (total_num_frames, ...) since v5.19 and previously (num_videos, num_frames, ...)
return tensor.flatten(0, 1) if tensor.ndim == 4 else tensor


class _OVGemma4ForCausalLM(_OVGemma3ForCausalLM):
additional_parts = ["text_embeddings_per_layer", "audio_embeddings"]

Expand Down Expand Up @@ -5755,8 +5787,8 @@ def get_multimodal_embeddings(
pixel_values_videos = kwargs.get("pixel_values_videos")
video_position_ids = kwargs.get("video_position_ids")
if pixel_values_videos is not None and video_position_ids is not None:
flatten_pixel_values_videos = pixel_values_videos.flatten(0, 1)
flatten_video_position_ids = video_position_ids.flatten(0, 1)
flatten_pixel_values_videos = _flatten_video_frames(pixel_values_videos)
flatten_video_position_ids = _flatten_video_frames(video_position_ids)
video_embeds = self.get_vision_embeddings(
flatten_pixel_values_videos, input_ids=input_ids, image_position_ids=flatten_video_position_ids
)
Expand Down Expand Up @@ -5815,6 +5847,7 @@ def prepare_inputs_for_generation(
input_features=None,
input_features_mask=None,
video_position_ids=None,
num_frames_per_video=None, # returned by the video processor, not needed since frames are flattened
**kwargs,
):
model_inputs = super().prepare_inputs_for_generation(
Expand Down Expand Up @@ -5978,9 +6011,9 @@ def get_multimodal_embeddings(
video_position_ids = kwargs.get("video_position_ids")
if pixel_values_videos is not None and video_position_ids is not None:
video_embeds = self.get_vision_embeddings(
pixel_values_videos.flatten(0, 1),
_flatten_video_frames(pixel_values_videos),
input_ids=input_ids,
image_position_ids=video_position_ids.flatten(0, 1),
image_position_ids=_flatten_video_frames(video_position_ids),
)
if video_embeds is not None:
inputs_embeds, attention_mask, position_ids = self.merge_vision_text_embeddings(
Expand All @@ -5990,7 +6023,7 @@ def get_multimodal_embeddings(
attention_mask=attention_mask,
position_ids=position_ids,
vision_token_id=self.config.video_token_id,
image_position_ids=video_position_ids.flatten(0, 1),
image_position_ids=_flatten_video_frames(video_position_ids),
)

if audio_input_features is not None:
Expand Down Expand Up @@ -6063,6 +6096,7 @@ def prepare_inputs_for_generation(
input_features_mask=None,
pixel_values_videos=None,
video_position_ids=None,
num_frames_per_video=None, # returned by the video processor, not needed since frames are flattened
**kwargs,
):
model_inputs = super().prepare_inputs_for_generation(
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
INSTALL_REQUIRE = [
"torch>=2.1",
"optimum~=2.3.0",
"transformers>=4.51,<5.16",
"transformers>=4.51,<5.23",
"setuptools",
"huggingface-hub>=0.23.2,<2.0",
"nncf>=3.3.0",
Expand Down
Loading
Loading