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
1 change: 1 addition & 0 deletions docs/source/openvino/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Here is the list of the supported architectures :
- MiniCPM3
- MiniCPM-o
- MiniCPM-V
- MiniCPM-V-4.6
- Mistral
- Mixtral
- MobileBERT
Expand Down
96 changes: 96 additions & 0 deletions optimum/exporters/openvino/input_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,102 @@ def generate(self, input_name: str, framework: str = "pt", int_dtype: str = "int
return self.random_float_tensor(shape=[self.feat_size, self.batch_size, self.hidden_size])


class DummyMiniCPMV4_6ImageInputGenerator(DummyVisionInputGenerator):
"""Dummy inputs for the fully-fused MiniCPM-V-4.6 image feature extractor.

The exported graph takes the NaViT-packed ``pixel_values`` together with a set of
precomputed index / mask tensors (patch position ids, block-diagonal encoder and
window attention masks, window reordering indices, and spatial-merge gather
indices). This generator builds a self-consistent set for a small square dummy
grid so that all divisibility invariants (window 2x2 then merge 2x2) hold.
"""

SUPPORTED_INPUT_NAMES = (
"pixel_values",
"pos_ids",
"encoder_attention_mask",
"downsampled_attention_mask",
"window_index",
"reverse_window_index",
"window_attention_mask",
"merge_gather_index",
"final_gather_index",
)

def __init__(
self,
task: str,
normalized_config: NormalizedVisionConfig,
batch_size: int = DEFAULT_DUMMY_SHAPES["batch_size"],
num_channels: int = DEFAULT_DUMMY_SHAPES["num_channels"],
width: int = DEFAULT_DUMMY_SHAPES["width"],
height: int = DEFAULT_DUMMY_SHAPES["height"],
**kwargs,
):
super().__init__(task, normalized_config, batch_size, num_channels, width, height)
vision_config = normalized_config.config
self.patch_size = vision_config.patch_size
# Small square grid (8x8 patches) that stays divisible by the window (2x2)
# and, after the /2 window merge, by the merge kernel (2x2).
self.grid_h = 8
self.grid_w = 8
self.window_h, self.window_w = 2, 2
self.merge_h, self.merge_w = 2, 2
self.num_patches = self.grid_h * self.grid_w
self.merged_h = self.grid_h // self.window_h
self.merged_w = self.grid_w // self.window_w
self.merged_patches = self.merged_h * self.merged_w
self.final_h = self.merged_h // self.merge_h
self.final_w = self.merged_w // self.merge_w
self.final_patches = self.final_h * self.final_w

def generate(self, input_name: str, framework: str = "pt", int_dtype: str = "int64", float_dtype: str = "fp32"):
if input_name == "pixel_values":
return self.random_float_tensor(
shape=[1, self.num_channels, self.patch_size, self.num_patches * self.patch_size],
framework=framework,
dtype=float_dtype,
)
if input_name == "pos_ids":
return self.random_int_tensor(
shape=[self.num_patches], min_value=0, max_value=8, framework=framework, dtype=int_dtype
)
if input_name in ("encoder_attention_mask", "window_attention_mask"):
return self.constant_tensor(
shape=[1, self.num_patches, self.num_patches],
value=0.0,
framework=framework,
dtype=DTYPE_MAPPER.pt(float_dtype),
)
if input_name == "downsampled_attention_mask":
return self.constant_tensor(
shape=[1, self.merged_patches, self.merged_patches],
value=0.0,
framework=framework,
dtype=DTYPE_MAPPER.pt(float_dtype),
)
if input_name in ("window_index", "reverse_window_index"):
return self.random_int_tensor(
shape=[self.num_patches], min_value=0, max_value=self.num_patches, framework=framework, dtype=int_dtype
)
if input_name == "merge_gather_index":
return self.random_int_tensor(
shape=[self.merged_patches * self.window_h * self.window_w],
min_value=0,
max_value=self.num_patches,
framework=framework,
dtype=int_dtype,
)
if input_name == "final_gather_index":
return self.random_int_tensor(
shape=[self.final_patches * self.merge_h * self.merge_w],
min_value=0,
max_value=self.merged_patches,
framework=framework,
dtype=int_dtype,
)


class DummyPhi3VisionProjectionInputGenerator(DummyVisionInputGenerator):
SUPPORTED_INPUT_NAMES = ("input",)

Expand Down
138 changes: 138 additions & 0 deletions optimum/exporters/openvino/model_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
DummyGemma4VisionInputGenerator,
DummyKokoroInputGenerator,
DummyLLavaMultiModalProjectorInputGenerator,
DummyMiniCPMV4_6ImageInputGenerator,
DummyMiniCPMVImageInputGenerator,
DummyMiniCPMVResampleInputGenerator,
DummyPhi3VisionProjectionInputGenerator,
Expand Down Expand Up @@ -147,6 +148,7 @@
MambaPatcher,
MiniCPM3Patcher,
MiniCPMModelPatcher,
MiniCPMV4_6VisionEmbeddingsModelPatcher,
MiniCPMVImageEmbeddingsModelPatcher,
MiniCPMVResamplerModelPatcher,
MistralModelPatcher,
Expand Down Expand Up @@ -6895,6 +6897,142 @@ def outputs(self) -> Dict[str, Dict[int, str]]:
return super().outputs


class MiniCPMV4_6ConfigBehavior(str, enum.Enum):
LANGUAGE = "language"
VISION_EMBEDDINGS = "vision_embeddings"
TEXT_EMBEDDINGS = "text_embeddings"


@register_in_tasks_manager("minicpmv4_6", *["image-text-to-text"], library_name="transformers")
class MiniCPMV4_6OpenVINOConfig(BaseVLMOpenVINOConfig):
"""OpenVINO exporter configuration for MiniCPM-V-4.6.

MiniCPM-V-4.6 combines a NaViT-packed SigLIP-style vision encoder with a ViT
window-attention merger + downsample merger, and a ``qwen3_5_text`` hybrid
(linear + full attention) language backbone. Unlike the older resampler-based
``minicpmv`` architecture, image features are inserted through a simple
``masked_scatter`` on ``image_token_id`` and the text backbone uses standard
1D RoPE position ids (its ``rope_type`` is ``default`` with no mrope-section
effect), so no 3D position handling is required.
"""

SUPPORTED_BEHAVIORS = [model_type.value for model_type in MiniCPMV4_6ConfigBehavior]
NORMALIZED_CONFIG_CLASS = NormalizedVisionConfig
DUMMY_INPUT_GENERATOR_CLASSES = ()
MIN_TRANSFORMERS_VERSION = "5.7.0"
MODEL_TYPE = "minicpmv4_6"

def __init__(
self,
config: "PretrainedConfig",
task: str = "feature-extraction",
int_dtype: str = "int64",
float_dtype: str = "fp32",
behavior: MiniCPMV4_6ConfigBehavior = MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS,
preprocessors: Optional[List[Any]] = None,
):
super().__init__(
config=config,
task=task,
int_dtype=int_dtype,
float_dtype=float_dtype,
preprocessors=preprocessors,
behavior=behavior,
)
self._behavior = behavior
self._orig_config = config
if self._behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS and hasattr(config, "vision_config"):
self._config = config.vision_config
self.DUMMY_INPUT_GENERATOR_CLASSES = (DummyMiniCPMV4_6ImageInputGenerator,)
self._normalized_config = self.NORMALIZED_CONFIG_CLASS(self._config)

@property
def inputs(self) -> Dict[str, Dict[int, str]]:
if self._behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS:
return {
"pixel_values": {0: "batch_size", 3: "patch_seq"},
"pos_ids": {0: "num_patches"},
"encoder_attention_mask": {1: "num_patches", 2: "num_patches"},
"downsampled_attention_mask": {1: "merged_patches", 2: "merged_patches"},
"window_index": {0: "num_patches"},
"reverse_window_index": {0: "num_patches"},
"window_attention_mask": {1: "num_patches", 2: "num_patches"},
"merge_gather_index": {0: "merge_gather"},
"final_gather_index": {0: "final_gather"},
}
return {}

@property
def outputs(self) -> Dict[str, Dict[int, str]]:
if self._behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS:
return {"image_features": {0: "num_image_tokens"}}
return {}

def with_behavior(
self,
behavior: Union[str, MiniCPMV4_6ConfigBehavior],
):
if isinstance(behavior, str) and not isinstance(behavior, MiniCPMV4_6ConfigBehavior):
behavior = MiniCPMV4_6ConfigBehavior(behavior)

if behavior == MiniCPMV4_6ConfigBehavior.TEXT_EMBEDDINGS:
return get_vlm_text_embeddings_config(
"qwen3_5_text",
self._orig_config.text_config,
self.int_dtype,
self.float_dtype,
min_transformers_version=self.MIN_TRANSFORMERS_VERSION,
)

if behavior == MiniCPMV4_6ConfigBehavior.LANGUAGE:
# MiniCPM-V-4.6 feeds the qwen3_5_text backbone standard 1D (2D
# batched) position ids — its rope_type is ``default`` with no mrope
# section, so the 3D mrope position ids used by the standalone Qwen3.5
# VLM are not required here.
return get_vlm_text_generation_config(
"qwen3_5_text",
self._orig_config.text_config,
self.int_dtype,
self.float_dtype,
model_patcher=Qwen3_5ModelPatcher,
min_transformers_version=self.MIN_TRANSFORMERS_VERSION,
)

if behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS:
return self.__class__(
self._orig_config,
task=self.task,
int_dtype=self.int_dtype,
float_dtype=self.float_dtype,
behavior=behavior,
preprocessors=self._preprocessors,
)

@staticmethod
def get_model_for_behavior(model, behavior: Union[str, MiniCPMV4_6ConfigBehavior]):
if isinstance(behavior, str) and not isinstance(behavior, MiniCPMV4_6ConfigBehavior):
behavior = MiniCPMV4_6ConfigBehavior(behavior)

if behavior == MiniCPMV4_6ConfigBehavior.LANGUAGE:
return model

if behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS:
# top-level MiniCPMV4_6Model so the patcher can reach both the vision
# tower (with its window merger) and the downsample merger.
return model.model

if behavior == MiniCPMV4_6ConfigBehavior.TEXT_EMBEDDINGS:
text_embedding = model.model.get_input_embeddings()
text_embedding.config = model.model.language_model.config
return text_embedding

def patch_model_for_export(self, model: "PreTrainedModel", model_kwargs: Optional[Dict[str, Any]] = None):
model_kwargs = model_kwargs or {}
if self._behavior == MiniCPMV4_6ConfigBehavior.VISION_EMBEDDINGS:
return MiniCPMV4_6VisionEmbeddingsModelPatcher(self, model, model_kwargs)
return super().patch_model_for_export(model, model_kwargs)


@register_in_tasks_manager(
"kokoro",
*["text-to-audio"],
Expand Down
Loading
Loading