-
Notifications
You must be signed in to change notification settings - Fork 235
Add support for exporting the Qwen3-VL-Embedding model #1686
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 all commits
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -288,6 +288,16 @@ def init_model_configs(): | |||||||||||||||||||||||||||||||||||||||||||||||||||
| "AutoModelForImageTextToText", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Add support for Qwen3-VL-Embedding | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TasksManager._CUSTOM_CLASSES[("pt", "qwen3_vl", "feature-extraction")] = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "transformers", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Qwen3VLForConditionalGeneration", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TasksManager._CUSTOM_CLASSES[("pt", "qwen3_vl", "image-text-to-text")] = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "transformers", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Qwen3VLForConditionalGeneration", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if is_diffusers_available() and "fill" not in TasksManager._DIFFUSERS_TASKS_TO_MODEL_LOADERS: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TasksManager._DIFFUSERS_TASKS_TO_MODEL_LOADERS["fill"] = "FluxFillPipeline" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TasksManager._DIFFUSERS_TASKS_TO_MODEL_MAPPINGS["fill"] = {"flux": "FluxFillPipeline"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -450,6 +460,33 @@ def generate( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return super().generate(input_name, framework, int_dtype, float_dtype) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DummyQwen3VLInputGenerator(DummyVisionInputGenerator): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| SUPPORTED_INPUT_NAMES = ("pixel_values", "image_grid_thw") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, **kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def generate(self, input_name: str, framework: str = "pt", int_dtype: str = "int64", float_dtype: str = "fp32"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if input_name == "pixel_values": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # For Qwen3-VL-Embedding, the input shape is [batch_size, 3, 2, 16, 16] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.random_float_tensor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [self.batch_size, 3, 2, 16, 16], framework=framework, dtype=float_dtype | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if input_name == "image_grid_thw": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # For Qwen3-VL-Embedding, the input shape is [num_images, 3] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.random_int_tensor( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [1, 3], min_value=1, max_value=16, framework=framework, dtype=int_dtype | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return super().generate(input_name, framework, int_dtype, float_dtype) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @register_in_tasks_manager( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "qwen3_vl_text", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1898,6 +1935,154 @@ def patch_model_for_export(self, model: PreTrainedModel, model_kwargs: Optional[ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return super().patch_model_for_export(model, model_kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return CommonImageEmbeddingsModelPatcher(self, model, model_kwargs) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @register_in_tasks_manager( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "qwen3_vl", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| *[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "feature-extraction", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "image-text-to-text", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| library_name="transformers", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Qwen3VLOpenVINOConfig(BaseVLMOpenVINOConfig): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1942
to
+1946
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| "image-text-to-text", | |
| ], | |
| library_name="transformers", | |
| ) | |
| class Qwen3VLOpenVINOConfig(BaseVLMOpenVINOConfig): | |
| ], | |
| library_name="transformers", | |
| ) | |
| class Qwen3VLEmbeddingOpenVINOConfig(BaseVLMOpenVINOConfig): |
Copilot
AI
Apr 21, 2026
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.
behavior passed to __init__ is currently ignored because it isn’t forwarded to BaseVLMOpenVINOConfig.__init__ (which sets self._behavior). This can break with_behavior(...) / multi-part VLM export because the instance will always behave as VISION_EMBEDDINGS. Pass behavior=behavior to super().__init__(...) or set self._behavior = behavior after calling super().
| float_dtype=float_dtype, | |
| float_dtype=float_dtype, | |
| behavior=behavior, |
Copilot
AI
Apr 21, 2026
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.
In the inputs mapping, using the literal string "3" as a dimension name for image_grid_thw is inconsistent with the rest of the exporter configs (dimension names are descriptive identifiers). Rename this axis to something semantic (e.g. grid_dims/thw) or omit it if it’s intended to be a fixed size.
| "image_grid_thw": {0: "num_images", 1: "3"} | |
| "image_grid_thw": {0: "num_images", 1: "thw"} |
Copilot
AI
Apr 21, 2026
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.
generate_dummy_inputs hard-codes Torch tensors with float32/int64 dtypes and fixed shapes, ignoring framework, float_dtype, int_dtype, and user-provided input_shapes. This can cause dtype mismatches (e.g. fp16 export) and makes input-shape overrides ineffective. Prefer using the standard dummy input generator flow (super().generate_dummy_inputs(...)) with a dedicated DummyInputGenerator that respects the dtype/shape parameters.
| # For feature-extraction task, we need to generate inputs for get_image_features method | |
| import torch | |
| # Only return the inputs that the model actually accepts | |
| # Use shape [batch_size, 3, 2, 16, 16] for pixel_values | |
| dummy_inputs = { | |
| "pixel_values": torch.randn(1, 3, 2, 16, 16, dtype=torch.float32), | |
| "image_grid_thw": torch.tensor([[1, 16, 16]], dtype=torch.int64) | |
| } | |
| return dummy_inputs | |
| # Reuse the standard dummy input generator flow so framework, dtype, | |
| # and caller-provided shape overrides are all respected. | |
| return super().generate_dummy_inputs(framework=framework, **kwargs) |
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.
def patched_forward(self, pixel_values, image_grid_thw, **kwargs):
output = self.orig_forward(pixel_values, image_grid_thw, **kwargs)
if isinstance(output, tuple):
return output[0]
return output.last_hidden_state if hasattr(output, 'last_hidden_state') else output
Copilot
AI
Apr 21, 2026
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.
The custom patcher returned by patch_model_for_export doesn’t follow the ModelPatcher contract used by convert.py (which wraps patcher.patched_forward and assumes a dict-like output with .values(), and may reassign patcher.patched_forward). Here, model.forward is patched in __init__ and the patched forward returns a raw tensor, so future changes (or different export paths) can easily break. Prefer implementing this as a proper ModelPatcher subclass (or reuse CommonImageEmbeddingsModelPatcher) so patched_forward returns a dict keyed by config.outputs and patching is applied in __enter__/__exit__.
| # Create a simple patcher that doesn't rely on get_image_features or ModelPatcher | |
| class Qwen3VLImageEmbeddingsModelPatcher: | |
| def __init__(self, config, model, model_kwargs=None): | |
| self.config = config | |
| self.model = model | |
| self.model_kwargs = model_kwargs | |
| # Patch the forward method directly | |
| self.orig_forward = model.forward | |
| model.forward = self.patched_forward | |
| def patched_forward(self, pixel_values, image_grid_thw, **kwargs): | |
| # Get the original output | |
| output = self.orig_forward(pixel_values, image_grid_thw, **kwargs) | |
| # Return only the last_hidden_state to avoid type inference issues | |
| return output.last_hidden_state | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc_val, exc_tb): | |
| # Restore the original forward method | |
| self.model.forward = self.orig_forward | |
| class Qwen3VLImageEmbeddingsModelPatcher(ModelPatcher): | |
| def patched_forward(self, pixel_values, image_grid_thw, **kwargs): | |
| output = self._model(pixel_values, image_grid_thw, **kwargs) | |
| output_name = next(iter(self.config.outputs)) | |
| return {output_name: output.last_hidden_state} |
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.
DummyQwen3VLInputGenerator.generatehard-codes[batch, 3, 2, 16, 16]/[1, 3]shapes and doesn’t usenormalized_config(or the providedwidth/height) to derive shapes. This risks producing invalid dummy inputs for other Qwen3-VL checkpoints or future config changes. Derive these dimensions from the model config (e.g., patch size / temporal patch size / image size) or at least thread through the dummy-shape kwargs so callers can override them.