diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml
index 4590b4ddefbf..19180a7ef7f6 100644
--- a/docs/source/en/_toctree.yml
+++ b/docs/source/en/_toctree.yml
@@ -1066,6 +1066,8 @@
title: Gemma3n
- local: model_doc/git
title: GIT
+ - local: model_doc/glm46v
+ title: Glm46V
- local: model_doc/glm4v
title: glm4v
- local: model_doc/glm4v_moe
diff --git a/docs/source/en/model_doc/glm46v.md b/docs/source/en/model_doc/glm46v.md
new file mode 100644
index 000000000000..6e099d7aaef6
--- /dev/null
+++ b/docs/source/en/model_doc/glm46v.md
@@ -0,0 +1,34 @@
+# GLM-4.6V
+
+## Glm46VConfig
+
+[[autodoc]] Glm46VConfig
+
+## Glm46VImageProcessor
+
+[[autodoc]] Glm46VImageProcessor
+ - preprocess
+
+## Glm46VVideoProcessor
+
+[[autodoc]] Glm46VVideoProcessor
+ - preprocess
+
+## Glm46VImageProcessorFast
+
+[[autodoc]] Glm46VImageProcessorFast
+ - preprocess
+
+## Glm46VProcessor
+
+[[autodoc]] Glm46VProcessor
+
+## Glm46VModel
+
+[[autodoc]] Glm46VModel
+ - forward
+
+## Glm46VForConditionalGeneration
+
+[[autodoc]] Glm46VForConditionalGeneration
+ - forward
diff --git a/docs/source/en/model_doc/glm4v.md b/docs/source/en/model_doc/glm4v.md
index 874b71435b48..98f6d8e761c5 100644
--- a/docs/source/en/model_doc/glm4v.md
+++ b/docs/source/en/model_doc/glm4v.md
@@ -170,6 +170,11 @@ print(output_text)
[[autodoc]] Glm4vConfig
+
+## Glm4vVisionConfig
+
+[[autodoc]] Glm4vVisionConfig
+
## Glm4vTextConfig
[[autodoc]] Glm4vTextConfig
@@ -193,6 +198,11 @@ print(output_text)
[[autodoc]] Glm4vProcessor
+## Glm4vVisionModel
+
+[[autodoc]] Glm4vVisionModel
+ - forward
+
## Glm4vTextModel
[[autodoc]] Glm4vTextModel
diff --git a/docs/source/en/model_doc/glm4v_moe.md b/docs/source/en/model_doc/glm4v_moe.md
index c814fdb5becd..8fab75298dfc 100644
--- a/docs/source/en/model_doc/glm4v_moe.md
+++ b/docs/source/en/model_doc/glm4v_moe.md
@@ -22,7 +22,7 @@ rendered properly in your Markdown viewer.
-# Glm4vMoe
+# Glm4vMoeMoe
## Overview
@@ -48,10 +48,20 @@ The model also introduces a **Thinking Mode** switch, allowing users to balance
[[autodoc]] Glm4vMoeConfig
+
+## Glm4vMoeVisionConfig
+
+[[autodoc]] Glm4vMoeVisionConfig
+
## Glm4vMoeTextConfig
[[autodoc]] Glm4vMoeTextConfig
+## Glm4vMoeVisionModel
+
+[[autodoc]] Glm4vMoeVisionModel
+ - forward
+
## Glm4vMoeTextModel
[[autodoc]] Glm4vMoeTextModel
@@ -65,4 +75,4 @@ The model also introduces a **Thinking Mode** switch, allowing users to balance
## Glm4vMoeForConditionalGeneration
[[autodoc]] Glm4vMoeForConditionalGeneration
- - forward
+ - forward
\ No newline at end of file
diff --git a/src/transformers/models/__init__.py b/src/transformers/models/__init__.py
index 3534ce6719d0..76b7a9a32ac6 100644
--- a/src/transformers/models/__init__.py
+++ b/src/transformers/models/__init__.py
@@ -142,6 +142,9 @@
from .git import *
from .glm import *
from .glm4 import *
+ from .glm4v import *
+ from .glm4v_moe import *
+ from .glm46v import *
from .glpn import *
from .got_ocr2 import *
from .gpt2 import *
diff --git a/src/transformers/models/auto/configuration_auto.py b/src/transformers/models/auto/configuration_auto.py
index 9a3b2ec5ecc2..e17a41263504 100644
--- a/src/transformers/models/auto/configuration_auto.py
+++ b/src/transformers/models/auto/configuration_auto.py
@@ -172,11 +172,14 @@
("git", "GitConfig"),
("glm", "GlmConfig"),
("glm4", "Glm4Config"),
+ ("glm46v", "Glm46VConfig"),
("glm4_moe", "Glm4MoeConfig"),
("glm4v", "Glm4vConfig"),
("glm4v_moe", "Glm4vMoeConfig"),
("glm4v_moe_text", "Glm4vMoeTextConfig"),
+ ("glm4v_moe_vision", "Glm4vMoeVisionConfig"),
("glm4v_text", "Glm4vTextConfig"),
+ ("glm4v_vision", "Glm4vVisionConfig"),
("glpn", "GLPNConfig"),
("got_ocr2", "GotOcr2Config"),
("gpt-sw3", "GPT2Config"),
@@ -620,11 +623,14 @@
("git", "GIT"),
("glm", "GLM"),
("glm4", "GLM4"),
+ ("glm46v", "Glm46V"),
("glm4_moe", "Glm4MoE"),
("glm4v", "GLM4V"),
("glm4v_moe", "GLM4VMOE"),
("glm4v_moe_text", "GLM4VMOE"),
+ ("glm4v_moe_vision", "Glm4vMoeVisionModel"),
("glm4v_text", "GLM4V"),
+ ("glm4v_vision", "Glm4vVisionModel"),
("glpn", "GLPN"),
("got_ocr2", "GOT-OCR2"),
("gpt-sw3", "GPT-Sw3"),
@@ -983,6 +989,8 @@
("gemma3n_audio", "gemma3n"),
("gemma3n_text", "gemma3n"),
("gemma3n_vision", "gemma3n"),
+ ("glm4v_vision", "glm4v"),
+ ("glm4v_moe_vision", "glm4v_moe"),
("glm4v_text", "glm4v"),
("glm4v_moe_text", "glm4v_moe"),
("idefics3_vision", "idefics3"),
diff --git a/src/transformers/models/auto/image_processing_auto.py b/src/transformers/models/auto/image_processing_auto.py
index 29282d276366..1de08b72934c 100644
--- a/src/transformers/models/auto/image_processing_auto.py
+++ b/src/transformers/models/auto/image_processing_auto.py
@@ -109,6 +109,7 @@
("gemma3", ("Gemma3ImageProcessor", "Gemma3ImageProcessorFast")),
("gemma3n", ("SiglipImageProcessor", "SiglipImageProcessorFast")),
("git", ("CLIPImageProcessor", "CLIPImageProcessorFast")),
+ ("glm46v", ("Glm46VImageProcessor", "Glm46VImageProcessorFast")),
("glm4v", ("Glm4vImageProcessor", "Glm4vImageProcessorFast")),
("glpn", ("GLPNImageProcessor", "GLPNImageProcessorFast")),
("got_ocr2", ("GotOcr2ImageProcessor", "GotOcr2ImageProcessorFast")),
diff --git a/src/transformers/models/auto/modeling_auto.py b/src/transformers/models/auto/modeling_auto.py
index 257fb95fdea7..acdf6c9db280 100644
--- a/src/transformers/models/auto/modeling_auto.py
+++ b/src/transformers/models/auto/modeling_auto.py
@@ -175,11 +175,14 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin):
("git", "GitModel"),
("glm", "GlmModel"),
("glm4", "Glm4Model"),
+ ("glm46v", "Glm46VModel"),
("glm4_moe", "Glm4MoeModel"),
("glm4v", "Glm4vModel"),
("glm4v_moe", "Glm4vMoeModel"),
("glm4v_moe_text", "Glm4vMoeTextModel"),
+ ("glm4v_moe_vision", "Glm4vMoeVisionModel"),
("glm4v_text", "Glm4vTextModel"),
+ ("glm4v_vision", "Glm4vVisionModel"),
("glpn", "GLPNModel"),
("got_ocr2", "GotOcr2Model"),
("gpt-sw3", "GPT2Model"),
@@ -1032,6 +1035,7 @@ class _BaseModelWithGenerate(PreTrainedModel, GenerationMixin):
("gemma3", "Gemma3ForConditionalGeneration"),
("gemma3n", "Gemma3nForConditionalGeneration"),
("git", "GitForCausalLM"),
+ ("glm46v", "Glm46VForConditionalGeneration"),
("glm4v", "Glm4vForConditionalGeneration"),
("glm4v_moe", "Glm4vMoeForConditionalGeneration"),
("got_ocr2", "GotOcr2ForConditionalGeneration"),
diff --git a/src/transformers/models/auto/processing_auto.py b/src/transformers/models/auto/processing_auto.py
index 691e4afc96e7..eac5f695aa25 100644
--- a/src/transformers/models/auto/processing_auto.py
+++ b/src/transformers/models/auto/processing_auto.py
@@ -75,6 +75,7 @@
("gemma3", "Gemma3Processor"),
("gemma3n", "Gemma3nProcessor"),
("git", "GitProcessor"),
+ ("glm46v", "Glm46VProcessor"),
("glm4v", "Glm4vProcessor"),
("glm4v_moe", "Glm4vProcessor"),
("got_ocr2", "GotOcr2Processor"),
diff --git a/src/transformers/models/auto/tokenization_auto.py b/src/transformers/models/auto/tokenization_auto.py
index c830fee86987..afd9069f17ff 100644
--- a/src/transformers/models/auto/tokenization_auto.py
+++ b/src/transformers/models/auto/tokenization_auto.py
@@ -308,6 +308,7 @@
("git", ("BertTokenizer", "BertTokenizerFast" if is_tokenizers_available() else None)),
("glm", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
("glm4", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
+ ("glm46v", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
("glm4_moe", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
("glm4v", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
("glm4v_moe", (None, "PreTrainedTokenizerFast" if is_tokenizers_available() else None)),
diff --git a/src/transformers/models/auto/video_processing_auto.py b/src/transformers/models/auto/video_processing_auto.py
index 9a396c4b9557..217687e7fce1 100644
--- a/src/transformers/models/auto/video_processing_auto.py
+++ b/src/transformers/models/auto/video_processing_auto.py
@@ -53,6 +53,7 @@
else:
VIDEO_PROCESSOR_MAPPING_NAMES = OrderedDict(
[
+ ("glm46v", "Glm46VVideoProcessor"),
("glm4v", "Glm4vVideoProcessor"),
("instructblip", "InstructBlipVideoVideoProcessor"),
("instructblipvideo", "InstructBlipVideoVideoProcessor"),
diff --git a/src/transformers/models/glm46v/__init__.py b/src/transformers/models/glm46v/__init__.py
new file mode 100644
index 000000000000..c5bb0c40f19d
--- /dev/null
+++ b/src/transformers/models/glm46v/__init__.py
@@ -0,0 +1,31 @@
+# Copyright 2025 The HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import TYPE_CHECKING
+
+from ...utils import _LazyModule
+from ...utils.import_utils import define_import_structure
+
+
+if TYPE_CHECKING:
+ from .configuration_glm46v import *
+ from .image_processing_glm46v import *
+ from .image_processing_glm46v_fast import *
+ from .modeling_glm46v import *
+ from .processing_glm46v import *
+ from .video_processing_glm46v import *
+else:
+ import sys
+
+ _file = globals()["__file__"]
+ sys.modules[__name__] = _LazyModule(__name__, _file, define_import_structure(_file), module_spec=__spec__)
diff --git a/src/transformers/models/glm46v/configuration_glm46v.py b/src/transformers/models/glm46v/configuration_glm46v.py
new file mode 100644
index 000000000000..125019940f13
--- /dev/null
+++ b/src/transformers/models/glm46v/configuration_glm46v.py
@@ -0,0 +1,106 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from ...configuration_utils import PreTrainedConfig
+from ..auto import CONFIG_MAPPING, AutoConfig
+
+
+class Glm46VConfig(PreTrainedConfig):
+ r"""
+ This is the configuration class to store the configuration of a [`Glm4vModel`]. It is used to instantiate a
+ GLM-4.6V model according to the specified arguments, defining the model architecture. Instantiating a
+ configuration with the defaults will yield a similar configuration to that of
+ GLM-4.1V-9B-Thinking [zai-org/GLM-4.1V-9B-Thinking](https://huggingface.co/zai-org/GLM-4.1V-9B-Thinking).
+
+ Configuration objects inherit from [`PreTrainedConfig`] and can be used to control the model outputs. Read the
+ documentation from [`PreTrainedConfig`] for more information.
+
+ Args:
+ text_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Glm4vTextConfig`):
+ The config object or dictionary of the text backbone.
+ vision_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Glm4vVisionConfig`):
+ The config object or dictionary of the vision backbone.
+ image_token_id (`int`, *optional*, defaults to 151343):
+ The image token index to encode the image prompt.
+ video_token_id (`int`, *optional*, defaults to 151344):
+ The video token index to encode the image prompt.
+ image_start_token_id (`int`, *optional*, defaults to 151339):
+ The image start token index to encode the start of image.
+ image_end_token_id (`int`, *optional*, defaults to 151340):
+ The image end token index to encode the end of image.
+ video_start_token_id (`int`, *optional*, defaults to 151361):
+ The video start token index to encode the start of video.
+ video_end_token_id (`int`, *optional*, defaults to 151362):
+ The video end token index to encode the end of video.
+
+ ```python
+ >>> from transformers import Glm46VForConditionalGeneration, Glm46VConfig
+
+ >>> # Initializing a GLM-4.6V style configuration
+ >>> configuration = Glm46VConfig()
+
+ >>> # Initializing a model from the GLM-4.6V style configuration
+ >>> model = Glm4vForConditionalGeneration(configuration)
+
+ >>> # Accessing the model configuration
+ >>> configuration = model.config
+ ```"""
+
+ model_type = "glm46v"
+ sub_configs = {"text_config": AutoConfig, "vision_config": AutoConfig}
+ keys_to_ignore_at_inference = ["past_key_values"]
+
+ def __init__(
+ self,
+ text_config=None,
+ vision_config=None,
+ image_token_id=151343,
+ video_token_id=151344,
+ image_start_token_id=151339,
+ image_end_token_id=151340,
+ video_start_token_id=151361,
+ video_end_token_id=151362,
+ **kwargs,
+ ):
+ if isinstance(vision_config, dict):
+ vision_config["model_type"] = vision_config.get("model_type", "glm4v_vision")
+ self.vision_config = CONFIG_MAPPING[vision_config["model_type"]](**vision_config)
+ elif vision_config is None:
+ self.vision_config = CONFIG_MAPPING["glm4v_vision"]()
+
+ if isinstance(text_config, dict):
+ text_config["model_type"] = text_config.get("model_type", "glm4v_text")
+ self.text_config = CONFIG_MAPPING[text_config["model_type"]](**text_config)
+ elif text_config is None:
+ self.text_config = CONFIG_MAPPING["glm4v_text"]()
+
+ self.image_token_id = image_token_id
+ self.video_token_id = video_token_id
+ self.video_start_token_id = video_start_token_id
+ self.video_end_token_id = video_end_token_id
+ self.image_start_token_id = image_start_token_id
+ self.image_end_token_id = image_end_token_id
+
+ super().__init__(**kwargs)
+
+
+__all__ = ["Glm46VConfig"]
diff --git a/src/transformers/models/glm46v/image_processing_glm46v.py b/src/transformers/models/glm46v/image_processing_glm46v.py
new file mode 100644
index 000000000000..bb0f46f8b052
--- /dev/null
+++ b/src/transformers/models/glm46v/image_processing_glm46v.py
@@ -0,0 +1,481 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import math
+from typing import Optional, Union
+
+import numpy as np
+
+from ...image_processing_utils import BaseImageProcessor, BatchFeature
+from ...image_transforms import convert_to_rgb, resize, to_channel_dimension_format
+from ...image_utils import (
+ OPENAI_CLIP_MEAN,
+ OPENAI_CLIP_STD,
+ ChannelDimension,
+ ImageInput,
+ PILImageResampling,
+ get_image_size,
+ infer_channel_dimension_format,
+ is_scaled_image,
+ make_flat_list_of_images,
+ to_numpy_array,
+ valid_images,
+ validate_preprocess_arguments,
+)
+from ...processing_utils import ImagesKwargs
+from ...utils import TensorType, logging
+from ...video_utils import VideoInput
+
+
+logger = logging.get_logger(__name__)
+
+
+class Glm46VImageProcessorKwargs(ImagesKwargs, total=False):
+ """
+ patch_size (`int`, *optional*, defaults to 14):
+ The spatial patch size of the vision encoder.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The temporal patch size of the vision encoder.
+ merge_size (`int`, *optional*, defaults to 2):
+ The merge size of the vision encoder to llm encoder.
+ """
+
+ patch_size: int
+ temporal_patch_size: int
+ merge_size: int
+
+
+def smart_resize(
+ num_frames: int,
+ height: int,
+ width: int,
+ temporal_factor: int = 2,
+ factor: int = 28,
+ min_pixels: int = 112 * 112,
+ max_pixels: int = 14 * 14 * 2 * 2 * 2 * 6144,
+):
+ if num_frames < temporal_factor:
+ raise ValueError(f"t:{num_frames} must be larger than temporal_factor:{temporal_factor}")
+ if height < factor or width < factor:
+ raise ValueError(f"height:{height} or width:{width} must be larger than factor:{factor}")
+ elif max(height, width) / min(height, width) > 200:
+ raise ValueError(
+ f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}"
+ )
+ h_bar = round(height / factor) * factor
+ w_bar = round(width / factor) * factor
+ t_bar = round(num_frames / temporal_factor) * temporal_factor
+
+ if t_bar * h_bar * w_bar > max_pixels:
+ beta = math.sqrt((num_frames * height * width) / max_pixels)
+ h_bar = max(factor, math.floor(height / beta / factor) * factor)
+ w_bar = max(factor, math.floor(width / beta / factor) * factor)
+ elif t_bar * h_bar * w_bar < min_pixels:
+ beta = math.sqrt(min_pixels / (num_frames * height * width))
+ h_bar = math.ceil(height * beta / factor) * factor
+ w_bar = math.ceil(width * beta / factor) * factor
+
+ return h_bar, w_bar
+
+
+class Glm46VImageProcessor(BaseImageProcessor):
+ r"""
+ Constructs a GLM-4V image processor that dynamically resizes images based on the original images.
+
+ Args:
+ do_resize (`bool`, *optional*, defaults to `True`):
+ Whether to resize the image's (height, width) dimensions.
+ size (`Dict[str, int]` *optional*, defaults to `{"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 15000}`):
+ Size of the image's `(height, width)` dimensions after resizing. Can be overridden by the `size` parameter
+ in the `preprocess` method. Available options are:
+ - `{"height": int, "width": int}`: The image will be resized to the exact size `(height, width)`.
+ Do NOT keep the aspect ratio.
+ - `{"shortest_edge": int, "longest_edge": int}`: The image will be resized to a maximum size respecting
+ the aspect ratio and keeping the shortest edge less or equal to `shortest_edge` and the longest edge
+ less or equal to `longest_edge`.
+ - `{"max_height": int, "max_width": int}`: The image will be resized to the maximum size respecting the
+ aspect ratio and keeping the height less or equal to `max_height` and the width less or equal to
+ `max_width`.
+ resample (`PILImageResampling`, *optional*, defaults to `Resampling.BICUBIC`):
+ Resampling filter to use when resizing the image.
+ do_rescale (`bool`, *optional*, defaults to `True`):
+ Whether to rescale the image by the specified scale `rescale_factor`.
+ rescale_factor (`int` or `float`, *optional*, defaults to `1/255`):
+ Scale factor to use if rescaling the image.
+ do_normalize (`bool`, *optional*, defaults to `True`):
+ Whether to normalize the image.
+ image_mean (`float` or `List[float]`, *optional*, defaults to `[0.48145466, 0.4578275, 0.40821073]`):
+ Mean to use if normalizing the image. This is a float or list of floats for each channel in the image.
+ image_std (`float` or `List[float]`, *optional*, defaults to `[0.26862954, 0.26130258, 0.27577711]`):
+ Standard deviation to use if normalizing the image. This is a float or list of floats for each channel in the image.
+ do_convert_rgb (`bool`, *optional*, defaults to `True`):
+ Whether to convert the image to RGB.
+ patch_size (`int`, *optional*, defaults to 14):
+ The spatial patch size of the vision encoder.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The temporal patch size of the vision encoder.
+ merge_size (`int`, *optional*, defaults to 2):
+ The merge size of the vision encoder to llm encoder.
+ """
+
+ model_input_names = ["pixel_values", "image_grid_thw"]
+ valid_kwargs = Glm46VImageProcessorKwargs
+
+ def __init__(
+ self,
+ do_resize: bool = True,
+ size: Optional[dict[str, int]] = None,
+ resample: PILImageResampling = PILImageResampling.BICUBIC,
+ do_rescale: bool = True,
+ rescale_factor: Union[int, float] = 1 / 255,
+ do_normalize: bool = True,
+ image_mean: Optional[Union[float, list[float]]] = None,
+ image_std: Optional[Union[float, list[float]]] = None,
+ do_convert_rgb: bool = True,
+ patch_size: int = 14,
+ temporal_patch_size: int = 2,
+ merge_size: int = 2,
+ **kwargs,
+ ) -> None:
+ super().__init__(**kwargs)
+ if size is not None and ("shortest_edge" not in size or "longest_edge" not in size):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+ elif size is None:
+ size = {"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 15000}
+ self.size = size
+
+ self.do_resize = do_resize
+ self.resample = resample
+ self.do_rescale = do_rescale
+ self.rescale_factor = rescale_factor
+ self.do_normalize = do_normalize
+ self.image_mean = image_mean if image_mean is not None else OPENAI_CLIP_MEAN
+ self.image_std = image_std if image_std is not None else OPENAI_CLIP_STD
+
+ self.patch_size = patch_size
+ self.temporal_patch_size = temporal_patch_size
+ self.merge_size = merge_size
+ self.do_convert_rgb = do_convert_rgb
+
+ def _preprocess(
+ self,
+ images: Union[ImageInput, VideoInput],
+ do_resize: Optional[bool] = None,
+ size: Optional[dict[str, int]] = None,
+ resample: Optional[PILImageResampling] = None,
+ do_rescale: Optional[bool] = None,
+ rescale_factor: Optional[float] = None,
+ do_normalize: Optional[bool] = None,
+ image_mean: Optional[Union[float, list[float]]] = None,
+ image_std: Optional[Union[float, list[float]]] = None,
+ patch_size: Optional[int] = None,
+ temporal_patch_size: Optional[int] = None,
+ merge_size: Optional[int] = None,
+ do_convert_rgb: Optional[bool] = None,
+ data_format: Optional[ChannelDimension] = ChannelDimension.FIRST,
+ input_data_format: Optional[Union[str, ChannelDimension]] = None,
+ ):
+ """
+ Preprocess an image or batch of images. Copy of the `preprocess` method from `CLIPImageProcessor`.
+
+ Args:
+ images (`ImageInput`):
+ Image or batch of images to preprocess. Expects pixel values ranging from 0 to 255. If pixel values range from 0 to 1, set `do_rescale=False`.
+ vision_info (`List[Dict]`, *optional*):
+ Optional list of dictionaries containing additional information about vision inputs.
+ do_resize (`bool`, *optional*, defaults to `self.do_resize`):
+ Whether to resize the image.
+ size (`Dict[str, int]`, *optional*, defaults to `self.size`):
+ Size of the image after resizing. `shortest_edge` and `longest_edge` keys must be present.
+ resample (`PILImageResampling`, *optional*, defaults to `self.resample`):
+ Resampling filter to use if resizing the image. This can be one of the `PILImageResampling` enums.
+ do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
+ Whether to rescale the image.
+ rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
+ Scale factor to use if rescaling the image.
+ do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
+ Whether to normalize the image.
+ image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
+ Mean to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
+ image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
+ Standard deviation to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
+ patch_size (`int`, *optional*, defaults to `self.patch_size`):
+ The spatial patch size of the vision encoder.
+ temporal_patch_size (`int`, *optional*, defaults to `self.temporal_patch_size`):
+ The temporal patch size of the vision encoder.
+ merge_size (`int`, *optional*, defaults to `self.merge_size`):
+ The merge size of the vision encoder to llm encoder.
+ do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
+ Whether to convert the image to RGB.
+ data_format (`ChannelDimension`, *optional*, defaults to `ChannelDimension.FIRST`):
+ The channel dimension format for the output image. Can be one of:
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
+ - Unset: Use the channel dimension format of the input image.
+ input_data_format (`ChannelDimension` or `str`, *optional*):
+ The channel dimension format for the input image. Can be one of:
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
+ - `"none"` or `ChannelDimension.NONE`: image in (height, width) format. - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
+ """
+ images = make_flat_list_of_images(images)
+
+ if do_convert_rgb:
+ images = [convert_to_rgb(image) for image in images]
+
+ # All transformations expect numpy arrays.
+ images = [to_numpy_array(image) for image in images]
+
+ if do_rescale and is_scaled_image(images[0]):
+ logger.warning_once(
+ "It looks like you are trying to rescale already rescaled images. If the input"
+ " images have pixel values between 0 and 1, set `do_rescale=False` to avoid rescaling them again."
+ )
+ if input_data_format is None:
+ # We assume that all images have the same channel dimension format.
+ input_data_format = infer_channel_dimension_format(images[0])
+
+ height, width = get_image_size(images[0], channel_dim=input_data_format)
+ resized_height, resized_width = height, width
+ processed_images = []
+ for image in images:
+ if do_resize:
+ resized_height, resized_width = smart_resize(
+ num_frames=temporal_patch_size,
+ height=height,
+ width=width,
+ temporal_factor=temporal_patch_size,
+ factor=patch_size * merge_size,
+ min_pixels=size["shortest_edge"],
+ max_pixels=size["longest_edge"],
+ )
+ image = resize(
+ image, size=(resized_height, resized_width), resample=resample, input_data_format=input_data_format
+ )
+
+ if do_rescale:
+ image = self.rescale(image, scale=rescale_factor, input_data_format=input_data_format)
+
+ if do_normalize:
+ image = self.normalize(
+ image=image, mean=image_mean, std=image_std, input_data_format=input_data_format
+ )
+
+ image = to_channel_dimension_format(image, data_format, input_channel_dim=input_data_format)
+ processed_images.append(image)
+
+ patches = np.array(processed_images)
+ if data_format == ChannelDimension.LAST:
+ patches = patches.transpose(0, 3, 1, 2)
+ if patches.shape[0] % temporal_patch_size != 0:
+ repeats = np.repeat(
+ patches[-1][np.newaxis], temporal_patch_size - (patches.shape[0] % temporal_patch_size), axis=0
+ )
+ patches = np.concatenate([patches, repeats], axis=0)
+ channel = patches.shape[1]
+ grid_t = patches.shape[0] // temporal_patch_size
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
+ patches = patches.reshape(
+ grid_t,
+ temporal_patch_size,
+ channel,
+ grid_h // merge_size,
+ merge_size,
+ patch_size,
+ grid_w // merge_size,
+ merge_size,
+ patch_size,
+ )
+ patches = patches.transpose(0, 3, 6, 4, 7, 2, 1, 5, 8)
+ flatten_patches = patches.reshape(
+ grid_t * grid_h * grid_w, channel * temporal_patch_size * patch_size * patch_size
+ )
+
+ return flatten_patches, (grid_t, grid_h, grid_w)
+
+ def preprocess(
+ self,
+ images: ImageInput,
+ do_resize: Optional[bool] = None,
+ size: Optional[dict[str, int]] = None,
+ resample: Optional[PILImageResampling] = None,
+ do_rescale: Optional[bool] = None,
+ rescale_factor: Optional[float] = None,
+ do_normalize: Optional[bool] = None,
+ image_mean: Optional[Union[float, list[float]]] = None,
+ image_std: Optional[Union[float, list[float]]] = None,
+ patch_size: Optional[int] = None,
+ temporal_patch_size: Optional[int] = None,
+ merge_size: Optional[int] = None,
+ do_convert_rgb: Optional[bool] = None,
+ return_tensors: Optional[Union[str, TensorType]] = None,
+ data_format: Optional[ChannelDimension] = ChannelDimension.FIRST,
+ input_data_format: Optional[Union[str, ChannelDimension]] = None,
+ ):
+ """
+ Args:
+ images (`ImageInput`):
+ Image to preprocess. Expects a single or batch of images with pixel values ranging from 0 to 255. If
+ passing in images with pixel values between 0 and 1, set `do_rescale=False`.
+ do_resize (`bool`, *optional*, defaults to `self.do_resize`):
+ Whether to resize the image.
+ size (`Dict[str, int]`, *optional*, defaults to `self.size`):
+ Size of the image after resizing. Shortest edge of the image is resized to size["shortest_edge"], with
+ the longest edge resized to keep the input aspect ratio.
+ resample (`int`, *optional*, defaults to `self.resample`):
+ Resampling filter to use if resizing the image. This can be one of the enum `PILImageResampling`. Only
+ has an effect if `do_resize` is set to `True`.
+ do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
+ Whether to rescale the image.
+ rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
+ Rescale factor to rescale the image by if `do_rescale` is set to `True`.
+ do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
+ Whether to normalize the image.
+ image_mean (`float` or `List[float]`, *optional*, defaults to `self.image_mean`):
+ Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
+ image_std (`float` or `List[float]`, *optional*, defaults to `self.image_std`):
+ Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to
+ `True`.
+ The max pixels of the image to resize the image.
+ patch_size (`int`, *optional*, defaults to `self.patch_size`):
+ The spatial patch size of the vision encoder.
+ temporal_patch_size (`int`, *optional*, defaults to `self.temporal_patch_size`):
+ The temporal patch size of the vision encoder.
+ merge_size (`int`, *optional*, defaults to `self.merge_size`):
+ The merge size of the vision encoder to llm encoder.
+ do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
+ Whether to convert the image to RGB.
+ return_tensors (`str` or `TensorType`, *optional*):
+ The type of tensors to return. Can be one of:
+ - Unset: Return a list of `np.ndarray`.
+ - `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
+ - `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
+ data_format (`ChannelDimension` or `str`, *optional*, defaults to `ChannelDimension.FIRST`):
+ The channel dimension format for the output image. Can be one of:
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
+ - Unset: Use the channel dimension format of the input image.
+ input_data_format (`ChannelDimension` or `str`, *optional*):
+ The channel dimension format for the input image. If unset, the channel dimension format is inferred
+ from the input image. Can be one of:
+ - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
+ - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
+ - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
+
+ """
+ # Try to use config values if set, otherwise fallback to global defaults
+ size = size if size is not None else self.size
+ if size is not None and ("shortest_edge" not in size or "longest_edge" not in size):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+ elif size is None:
+ size = {"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 15000}
+
+ do_resize = do_resize if do_resize is not None else self.do_resize
+ resample = resample if resample is not None else self.resample
+ do_rescale = do_rescale if do_rescale is not None else self.do_rescale
+ rescale_factor = rescale_factor if rescale_factor is not None else self.rescale_factor
+ do_normalize = do_normalize if do_normalize is not None else self.do_normalize
+ image_mean = image_mean if image_mean is not None else self.image_mean
+ image_std = image_std if image_std is not None else self.image_std
+ patch_size = patch_size if patch_size is not None else self.patch_size
+ temporal_patch_size = temporal_patch_size if temporal_patch_size is not None else self.temporal_patch_size
+ merge_size = merge_size if merge_size is not None else self.merge_size
+ do_convert_rgb = do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
+
+ if images is not None:
+ images = self.fetch_images(images)
+ images = make_flat_list_of_images(images)
+
+ if images is not None and not valid_images(images):
+ raise ValueError("Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, or torch.Tensor")
+
+ validate_preprocess_arguments(
+ rescale_factor=rescale_factor,
+ do_normalize=do_normalize,
+ image_mean=image_mean,
+ image_std=image_std,
+ do_resize=do_resize,
+ size=size,
+ resample=resample,
+ )
+
+ data = {}
+ if images is not None:
+ pixel_values, vision_grid_thws = [], []
+ for image in images:
+ patches, image_grid_thw = self._preprocess(
+ image,
+ do_resize=do_resize,
+ size=size,
+ resample=resample,
+ do_rescale=do_rescale,
+ rescale_factor=rescale_factor,
+ do_normalize=do_normalize,
+ image_mean=image_mean,
+ image_std=image_std,
+ patch_size=patch_size,
+ temporal_patch_size=temporal_patch_size,
+ merge_size=merge_size,
+ data_format=data_format,
+ do_convert_rgb=do_convert_rgb,
+ input_data_format=input_data_format,
+ )
+ pixel_values.extend(patches)
+ vision_grid_thws.append(image_grid_thw)
+ pixel_values = np.array(pixel_values)
+ vision_grid_thws = np.array(vision_grid_thws)
+ data.update({"pixel_values": pixel_values, "image_grid_thw": vision_grid_thws})
+
+ return BatchFeature(data=data, tensor_type=return_tensors)
+
+ def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
+ """
+ A utility that returns number of image patches for a given image size.
+
+ Args:
+ height (`int`):
+ Height of the input image.
+ width (`int`):
+ Width of the input image.
+ images_kwargs (`dict`, *optional*)
+ Any kwargs to override defaults of the image processor.
+ Returns:
+ `int`: Number of image patches per image.
+ """
+ patch_size = images_kwargs.get("patch_size", self.patch_size)
+ merge_size = images_kwargs.get("merge_size", self.merge_size)
+ size = images_kwargs.get("size", {"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 15000})
+
+ factor = patch_size * merge_size
+ resized_height, resized_width = smart_resize(
+ num_frames=self.temporal_patch_size,
+ height=height,
+ width=width,
+ factor=factor,
+ min_pixels=size["shortest_edge"],
+ max_pixels=size["longest_edge"],
+ temporal_factor=self.temporal_patch_size,
+ )
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
+ return grid_h * grid_w
+
+
+__all__ = ["Glm46VImageProcessor"]
diff --git a/src/transformers/models/glm46v/image_processing_glm46v_fast.py b/src/transformers/models/glm46v/image_processing_glm46v_fast.py
new file mode 100644
index 000000000000..f86b78153783
--- /dev/null
+++ b/src/transformers/models/glm46v/image_processing_glm46v_fast.py
@@ -0,0 +1,183 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Union
+
+import torch
+from torchvision.transforms.v2 import functional as F
+
+from ...image_processing_utils import BatchFeature
+from ...image_processing_utils_fast import BaseImageProcessorFast, group_images_by_shape, reorder_images
+from ...image_utils import OPENAI_CLIP_MEAN, OPENAI_CLIP_STD, ImageInput, PILImageResampling, SizeDict
+from ...processing_utils import Unpack
+from ...utils import TensorType, auto_docstring
+from .image_processing_glm46v import Glm46VImageProcessorKwargs, smart_resize
+
+
+@auto_docstring
+class Glm46VImageProcessorFast(BaseImageProcessorFast):
+ do_resize = True
+ resample = PILImageResampling.BICUBIC
+ size = {"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 15000}
+ do_rescale = True
+ do_normalize = True
+ image_mean = OPENAI_CLIP_MEAN
+ image_std = OPENAI_CLIP_STD
+ do_convert_rgb = True
+ patch_size = 14
+ temporal_patch_size = 2
+ merge_size = 2
+ valid_kwargs = Glm46VImageProcessorKwargs
+ model_input_names = ["pixel_values", "image_grid_thw"]
+
+ def __init__(self, **kwargs: Unpack[Glm46VImageProcessorKwargs]):
+ super().__init__(**kwargs)
+ if self.size is not None and (
+ self.size.get("shortest_edge", None) is None or self.size.get("longest_edge", None) is None
+ ):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+
+ def _further_process_kwargs(
+ self,
+ size: Optional[SizeDict] = None,
+ **kwargs,
+ ) -> dict:
+ """
+ Update kwargs that need further processing before being validated
+ Can be overridden by subclasses to customize the processing of kwargs.
+ """
+ if size is not None and ("shortest_edge" not in size or "longest_edge" not in size):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+
+ return super()._further_process_kwargs(size=size, **kwargs)
+
+ def _preprocess(
+ self,
+ images: list["torch.Tensor"],
+ do_resize: bool,
+ size: SizeDict,
+ interpolation: Optional["F.InterpolationMode"],
+ do_rescale: bool,
+ rescale_factor: float,
+ do_normalize: bool,
+ image_mean: Optional[Union[float, list[float]]],
+ image_std: Optional[Union[float, list[float]]],
+ patch_size: int,
+ temporal_patch_size: int,
+ merge_size: int,
+ disable_grouping: Optional[bool],
+ return_tensors: Optional[Union[str, TensorType]],
+ **kwargs,
+ ) -> BatchFeature:
+ """
+ Preprocess an image or batch of images. Copy of the `preprocess` method from `CLIPImageProcessor`.
+ """
+
+ grouped_images, grouped_images_index = group_images_by_shape(images, disable_grouping=disable_grouping)
+ resized_images_grouped = {}
+ for shape, stacked_images in grouped_images.items():
+ height, width = stacked_images.shape[-2:]
+ if do_resize:
+ resized_height, resized_width = smart_resize(
+ num_frames=temporal_patch_size,
+ height=height,
+ width=width,
+ temporal_factor=temporal_patch_size,
+ factor=patch_size * merge_size,
+ min_pixels=size.shortest_edge,
+ max_pixels=size.longest_edge,
+ )
+ stacked_images = self.resize(
+ stacked_images,
+ size=SizeDict(height=resized_height, width=resized_width),
+ interpolation=interpolation,
+ )
+ resized_images_grouped[shape] = stacked_images
+
+ resized_images = reorder_images(resized_images_grouped, grouped_images_index)
+
+ grouped_images, grouped_images_index = group_images_by_shape(resized_images, disable_grouping=disable_grouping)
+ processed_images_grouped = {}
+ processed_grids = {}
+
+ for shape, stacked_images in grouped_images.items():
+ resized_height, resized_width = stacked_images.shape[-2:]
+
+ patches = self.rescale_and_normalize(
+ stacked_images, do_rescale, rescale_factor, do_normalize, image_mean, image_std
+ )
+ if patches.ndim == 4: # (B, C, H, W)
+ patches = patches.unsqueeze(1) # (B, T=1, C, H, W)
+
+ if patches.shape[1] % temporal_patch_size != 0:
+ repeats = patches[:, -1:].repeat(
+ 1, temporal_patch_size - (patches.shape[1] % temporal_patch_size), 1, 1, 1
+ )
+ patches = torch.cat([patches, repeats], dim=1)
+
+ batch_size, t_len, channel = patches.shape[:3]
+ grid_t = t_len // temporal_patch_size
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
+
+ patches = patches.view(
+ batch_size,
+ grid_t,
+ temporal_patch_size,
+ channel,
+ grid_h // merge_size,
+ merge_size,
+ patch_size,
+ grid_w // merge_size,
+ merge_size,
+ patch_size,
+ )
+ # (B, grid_t, gh, gw, mh, mw, C, tp, ph, pw)
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
+
+ flatten_patches = patches.reshape(
+ batch_size,
+ grid_t * grid_h * grid_w,
+ channel * temporal_patch_size * patch_size * patch_size,
+ )
+
+ processed_images_grouped[shape] = flatten_patches
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
+
+ processed_images = reorder_images(processed_images_grouped, grouped_images_index)
+ processed_grids = reorder_images(processed_grids, grouped_images_index)
+
+ pixel_values = torch.cat(processed_images, dim=0)
+ image_grid_thw = torch.tensor(processed_grids)
+
+ return BatchFeature(
+ data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
+ )
+
+ @auto_docstring
+ def preprocess(
+ self,
+ images: ImageInput,
+ **kwargs: Unpack[Glm46VImageProcessorKwargs],
+ ) -> BatchFeature:
+ return super().preprocess(images, **kwargs)
+
+
+__all__ = ["Glm46VImageProcessorFast"]
diff --git a/src/transformers/models/glm46v/modeling_glm46v.py b/src/transformers/models/glm46v/modeling_glm46v.py
new file mode 100644
index 000000000000..7fc18482c4da
--- /dev/null
+++ b/src/transformers/models/glm46v/modeling_glm46v.py
@@ -0,0 +1,835 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import itertools
+from dataclasses import dataclass
+from typing import Any, Optional, Union
+
+import torch
+import torch.nn as nn
+
+from ...cache_utils import Cache
+from ...generation import GenerationMixin
+from ...modeling_outputs import ModelOutput
+from ...modeling_utils import PreTrainedModel
+from ...processing_utils import Unpack
+from ...utils import TransformersKwargs, auto_docstring, can_return_tuple, is_torchdynamo_compiling
+from ..auto import AutoModel
+from .configuration_glm46v import Glm46VConfig
+
+
+@auto_docstring
+class Glm46VPreTrainedModel(PreTrainedModel):
+ config: Glm46VConfig
+ base_model_prefix = "model"
+ input_modalities = ["image", "video", "text"]
+ supports_gradient_checkpointing = True
+ _no_split_modules = None
+ _skip_keys_device_placement = "past_key_values"
+ _supports_flash_attn = True
+ _supports_sdpa = True
+
+ _can_compile_fullgraph = True
+ _supports_attention_backend = True
+ _can_record_outputs = None
+
+
+@dataclass
+@auto_docstring(
+ custom_intro="""
+ Base class for Llava outputs, with hidden states and attentions.
+ """
+)
+class Glm46VModelOutputWithPast(ModelOutput):
+ r"""
+ past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
+ It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache).
+
+ Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
+ `past_key_values` input) to speed up sequential decoding.
+ rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*):
+ The rope index difference between sequence length and multimodal rope.
+ """
+
+ last_hidden_state: Optional[torch.FloatTensor] = None
+ past_key_values: Optional[Cache] = None
+ hidden_states: Optional[tuple[torch.FloatTensor]] = None
+ attentions: Optional[tuple[torch.FloatTensor]] = None
+ rope_deltas: Optional[torch.LongTensor] = None
+
+
+@auto_docstring
+class Glm46VModel(Glm46VPreTrainedModel):
+ base_model_prefix = ""
+ _checkpoint_conversion_mapping = {}
+ # Reference: fix gemma3 grad acc #37208
+ accepts_loss_kwargs = False
+ config: Glm46VConfig
+ _no_split_modules = None
+
+ def __init__(self, config):
+ super().__init__(config)
+ self.visual = AutoModel.from_config(config.vision_config)
+ self.language_model = AutoModel.from_config(config.text_config)
+ self.rope_deltas = None # cache rope_deltas here
+
+ # Initialize weights and apply final processing
+ self.post_init()
+
+ def get_input_embeddings(self):
+ return self.language_model.get_input_embeddings()
+
+ def set_input_embeddings(self, value):
+ self.language_model.set_input_embeddings(value)
+
+ def set_decoder(self, decoder):
+ self.language_model = decoder
+
+ def get_decoder(self):
+ return self.language_model
+
+ def get_rope_index(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ image_grid_thw: Optional[torch.LongTensor] = None,
+ video_grid_thw: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ """
+ Calculate the 3D rope index based on image and video's temporal, height and width in LLM.
+
+ Explanation:
+ Each embedding sequence contains vision embedding and text embedding or just contains text embedding.
+
+ For pure text embedding sequence, the rotary position embedding has no difference with modern LLMs.
+ Examples:
+ input_ids: [T T T T T], here T is for text.
+ temporal position_ids: [0, 1, 2, 3, 4]
+ height position_ids: [0, 1, 2, 3, 4]
+ width position_ids: [0, 1, 2, 3, 4]
+
+ For vision and text embedding sequence, we calculate 3D rotary position embedding for vision part
+ and 1D rotary position embedding for text part.
+ Examples:
+ Temporal (Time): 3 patches, representing different segments of the video in time.
+ Height: 2 patches, dividing each frame vertically.
+ Width: 2 patches, dividing each frame horizontally.
+ We also have some important parameters:
+ fps (Frames Per Second): The video's frame rate, set to 1. This means one frame is processed each second.
+ tokens_per_second: This is a crucial parameter. It dictates how many "time-steps" or "temporal tokens" are conceptually packed into a one-second interval of the video. In this case, we have 25 tokens per second. So each second of the video will be represented with 25 separate time points. It essentially defines the temporal granularity.
+ temporal_patch_size: The number of frames that compose one temporal patch. Here, it's 2 frames.
+ interval: The step size for the temporal position IDs, calculated as tokens_per_second * temporal_patch_size / fps. In this case, 25 * 2 / 1 = 50. This means that each temporal patch will be have a difference of 50 in the temporal position IDs.
+ input_ids: [V V V V V V V V V V V V T T T T T], here V is for vision.
+ vision temporal position_ids: [0, 0, 0, 0, 50, 50, 50, 50, 100, 100, 100, 100]
+ vision height position_ids: [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1]
+ vision width position_ids: [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
+ text temporal position_ids: [101, 102, 103, 104, 105]
+ text height position_ids: [101, 102, 103, 104, 105]
+ text width position_ids: [101, 102, 103, 104, 105]
+ Here we calculate the text start position_ids as the max vision position_ids plus 1.
+
+ Args:
+ input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
+ Indices of input sequence tokens in the vocabulary. Padding will be ignored by default should you provide
+ it.
+ image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
+ The temporal, height and width of feature shape of each image in LLM.
+ video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
+ The temporal, height and width of feature shape of each video in LLM.
+ attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*):
+ Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`:
+
+ - 1 for tokens that are **not masked**,
+ - 0 for tokens that are **masked**.
+
+ Returns:
+ position_ids (`torch.LongTensor` of shape `(3, batch_size, sequence_length)`)
+ mrope_position_deltas (`torch.Tensor` of shape `(batch_size)`)
+ """
+
+ spatial_merge_size = self.config.vision_config.spatial_merge_size
+ image_token_id = self.config.image_token_id
+ video_start_token_id = self.config.video_start_token_id
+ video_end_token_id = self.config.video_end_token_id
+
+ mrope_position_deltas = []
+ if input_ids is not None and (image_grid_thw is not None or video_grid_thw is not None):
+ total_input_ids = input_ids
+ if attention_mask is None:
+ attention_mask = torch.ones_like(total_input_ids)
+ position_ids = torch.ones(
+ 3,
+ input_ids.shape[0],
+ input_ids.shape[1],
+ dtype=input_ids.dtype,
+ device=input_ids.device,
+ )
+ image_index, video_index = 0, 0
+ video_group_index = 0
+ attention_mask = attention_mask.to(total_input_ids.device)
+ for i, input_ids in enumerate(total_input_ids):
+ input_ids = input_ids[attention_mask[i] == 1]
+ input_tokens = input_ids.tolist()
+
+ input_token_type = []
+ video_check_flg = False
+ for token in input_tokens:
+ if token == video_start_token_id:
+ video_check_flg = True
+ elif token == video_end_token_id:
+ video_check_flg = False
+
+ if token == image_token_id and not video_check_flg:
+ input_token_type.append("image")
+ elif token == image_token_id and video_check_flg:
+ input_token_type.append("video")
+ else:
+ input_token_type.append("text")
+
+ input_type_group = []
+ for key, group in itertools.groupby(enumerate(input_token_type), lambda x: x[1]):
+ group = list(group)
+ start_index = group[0][0]
+ end_index = group[-1][0] + 1
+ input_type_group.append((key, start_index, end_index))
+
+ llm_pos_ids_list = []
+ video_frame_num = 1
+ for modality_type, start_idx, end_idx in input_type_group:
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
+
+ if modality_type == "image":
+ t, h, w = (
+ image_grid_thw[image_index][0],
+ image_grid_thw[image_index][1],
+ image_grid_thw[image_index][2],
+ )
+ llm_grid_t, llm_grid_h, llm_grid_w = (
+ t.item(),
+ h.item() // spatial_merge_size,
+ w.item() // spatial_merge_size,
+ )
+
+ t_index = torch.arange(llm_grid_t).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten()
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(llm_grid_t, -1, llm_grid_w).flatten()
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(llm_grid_t, llm_grid_h, -1).flatten()
+ llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + st_idx)
+
+ image_index += 1
+ video_frame_num = 1
+
+ elif modality_type == "video":
+ t, h, w = (
+ video_frame_num,
+ video_grid_thw[video_index][1],
+ video_grid_thw[video_index][2],
+ )
+
+ llm_grid_t, llm_grid_h, llm_grid_w = (
+ t,
+ h.item() // spatial_merge_size,
+ w.item() // spatial_merge_size,
+ )
+
+ for t_idx in range(llm_grid_t):
+ t_index = torch.tensor(t_idx).view(-1, 1).expand(-1, llm_grid_h * llm_grid_w).flatten()
+
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(1, -1, llm_grid_w).flatten()
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(1, llm_grid_h, -1).flatten()
+ llm_pos_ids_list.append(torch.stack([t_index, h_index, w_index]) + st_idx)
+
+ video_group_index += 1
+
+ if video_group_index >= video_grid_thw[video_index][0]:
+ video_index += 1
+ video_group_index = 0
+
+ video_frame_num += 1
+
+ else:
+ text_len = end_idx - start_idx
+ llm_pos_ids_list.append(torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
+
+ video_frame_num = 1
+
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
+ position_ids[..., i, attention_mask[i] == 1] = llm_positions.to(position_ids.device)
+ mrope_position_deltas.append(llm_positions.max() + 1 - len(total_input_ids[i]))
+ mrope_position_deltas = torch.tensor(mrope_position_deltas, device=input_ids.device).unsqueeze(1)
+ return position_ids, mrope_position_deltas
+ else:
+ if attention_mask is not None:
+ position_ids = attention_mask.long().cumsum(-1) - 1
+ position_ids.masked_fill_(attention_mask == 0, 1)
+ position_ids = position_ids.unsqueeze(0).expand(3, -1, -1).to(attention_mask.device)
+ max_position_ids = position_ids.max(0, keepdim=False)[0].max(-1, keepdim=True)[0]
+ mrope_position_deltas = max_position_ids + 1 - attention_mask.shape[-1]
+ else:
+ position_ids = (
+ torch.arange(input_ids.shape[1], device=input_ids.device)
+ .view(1, 1, -1)
+ .expand(3, input_ids.shape[0], -1)
+ )
+ mrope_position_deltas = torch.zeros(
+ [input_ids.shape[0], 1],
+ device=input_ids.device,
+ dtype=input_ids.dtype,
+ )
+
+ return position_ids, mrope_position_deltas
+
+ def get_video_features(
+ self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None
+ ):
+ """
+ Encodes videos into continuous embeddings that can be forwarded to the language model.
+
+ Args:
+ pixel_values_videos (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`):
+ The tensors corresponding to the input videos.
+ video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
+ The temporal, height and width of feature shape of each video in LLM.
+ """
+ pixel_values_videos = pixel_values_videos.type(self.visual.dtype)
+ # reshape video_grid_thw -> [b, 3] -> [1, h, w] * frames
+ temp_frames_hw = []
+ for t, h, w in video_grid_thw:
+ repeated_row = torch.tensor([1, h.item(), w.item()]).unsqueeze(0).repeat(t, 1)
+ temp_frames_hw.append(repeated_row)
+ flattened_video_grid_thw = torch.cat(temp_frames_hw, dim=0)
+ video_embeds = self.visual(pixel_values_videos, grid_thw=flattened_video_grid_thw)
+ split_sizes = (video_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist()
+ video_embeds = torch.split(video_embeds, split_sizes)
+ return video_embeds
+
+ def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None):
+ """
+ Encodes images into continuous embeddings that can be forwarded to the language model.
+
+ Args:
+ pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, image_size, image_size)`):
+ The tensors corresponding to the input images.
+ image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
+ The temporal, height and width of feature shape of each image in LLM.
+ """
+ pixel_values = pixel_values.type(self.visual.dtype)
+ image_embeds = self.visual(pixel_values, grid_thw=image_grid_thw)
+ split_sizes = (image_grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist()
+ image_embeds = torch.split(image_embeds, split_sizes)
+ return image_embeds
+
+ def get_placeholder_mask(
+ self,
+ input_ids: torch.LongTensor,
+ inputs_embeds: torch.FloatTensor,
+ image_features: Optional[torch.FloatTensor] = None,
+ video_features: Optional[torch.FloatTensor] = None,
+ ):
+ """
+ Obtains multimodal placeholder mask from `input_ids` or `inputs_embeds`, and checks that the placeholder token count is
+ equal to the length of multimodal features. If the lengths are different, an error is raised.
+ """
+ if input_ids is None:
+ special_image_mask = inputs_embeds == self.get_input_embeddings()(
+ torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device)
+ )
+ special_image_mask = special_image_mask.all(-1)
+ special_video_mask = inputs_embeds == self.get_input_embeddings()(
+ torch.tensor(self.config.video_token_id, dtype=torch.long, device=inputs_embeds.device)
+ )
+ special_video_mask = special_video_mask.all(-1)
+ else:
+ # GLM-4.1V and GLM-4.5V special_video_mask is special_image_mask
+ special_image_mask = input_ids == self.config.image_token_id
+ special_video_mask = input_ids == self.config.image_token_id
+
+ n_image_tokens = special_image_mask.sum()
+ special_image_mask = special_image_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device)
+ if image_features is not None and inputs_embeds[special_image_mask].numel() != image_features.numel():
+ raise ValueError(
+ f"Image features and image tokens do not match: tokens: {n_image_tokens}, features {image_features.shape[0]}"
+ )
+
+ n_video_tokens = special_video_mask.sum()
+ special_video_mask = special_video_mask.unsqueeze(-1).expand_as(inputs_embeds).to(inputs_embeds.device)
+ if video_features is not None and inputs_embeds[special_video_mask].numel() != video_features.numel():
+ raise ValueError(
+ f"Videos features and video tokens do not match: tokens: {n_video_tokens}, features {video_features.shape[0]}"
+ )
+
+ return special_image_mask, special_video_mask
+
+ @auto_docstring
+ @can_return_tuple
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ pixel_values: Optional[torch.Tensor] = None,
+ pixel_values_videos: Optional[torch.FloatTensor] = None,
+ image_grid_thw: Optional[torch.LongTensor] = None,
+ video_grid_thw: Optional[torch.LongTensor] = None,
+ rope_deltas: Optional[torch.LongTensor] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> Union[tuple, Glm46VModelOutputWithPast]:
+ r"""
+ image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
+ The temporal, height and width of feature shape of each image in LLM.
+ video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
+ The temporal, height and width of feature shape of each video in LLM.
+ rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*):
+ The rope index difference between sequence length and multimodal rope.
+ """
+ if (input_ids is None) ^ (inputs_embeds is not None):
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
+
+ if inputs_embeds is None:
+ inputs_embeds = self.get_input_embeddings()(input_ids)
+
+ if pixel_values is not None:
+ image_embeds = self.get_image_features(pixel_values, image_grid_thw)
+ image_embeds = torch.cat(image_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype)
+ image_mask, _ = self.get_placeholder_mask(input_ids, inputs_embeds, image_features=image_embeds)
+ inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
+
+ if pixel_values_videos is not None:
+ video_embeds = self.get_video_features(pixel_values_videos, video_grid_thw)
+ video_embeds = torch.cat(video_embeds, dim=0).to(inputs_embeds.device, inputs_embeds.dtype)
+ _, video_mask = self.get_placeholder_mask(input_ids, inputs_embeds, video_features=video_embeds)
+ inputs_embeds = inputs_embeds.masked_scatter(video_mask, video_embeds)
+
+ if position_ids is None:
+ attention_mask_tensor = (
+ attention_mask if not isinstance(attention_mask, dict) else attention_mask["full_attention"]
+ )
+ if attention_mask_tensor is not None and attention_mask_tensor.ndim == 4:
+ attention_mask_tensor = torch.diagonal(attention_mask_tensor[:, 0], dim1=1, dim2=2)
+ # Only apply conversion for floating point tensors (inverted masks)
+ if attention_mask_tensor.dtype.is_floating_point:
+ attention_mask_tensor = attention_mask_tensor / torch.finfo(attention_mask_tensor.dtype).min
+ attention_mask_tensor = (1.0 - attention_mask_tensor).int()
+
+ # Calculate RoPE index once per generation in the pre-fill stage only.
+ # When compiling, we can't check tensor values thus we check only input length
+ # It is safe to assume that `length!=1` means we're in pre-fill because compiled
+ # models currently cannot do asssisted decoding
+ prefill_compiled_stage = is_torchdynamo_compiling() and (
+ (input_ids is not None and input_ids.shape[1] != 1)
+ or (inputs_embeds is not None and inputs_embeds.shape[1] != 1)
+ )
+ prefill_noncompiled_stage = not is_torchdynamo_compiling() and (
+ (cache_position is not None and cache_position[0] == 0)
+ or (past_key_values is None or past_key_values.get_seq_length() == 0)
+ )
+ if (prefill_compiled_stage or prefill_noncompiled_stage) or self.rope_deltas is None:
+ position_ids, rope_deltas = self.get_rope_index(
+ input_ids,
+ image_grid_thw,
+ video_grid_thw,
+ attention_mask=attention_mask_tensor,
+ )
+ self.rope_deltas = rope_deltas
+ # then use the prev pre-calculated rope-deltas to get the correct position ids
+ else:
+ batch_size, seq_length, _ = inputs_embeds.shape
+ delta = (
+ (cache_position[0] + self.rope_deltas).to(inputs_embeds.device)
+ if cache_position is not None
+ else 0
+ )
+ position_ids = torch.arange(seq_length, device=inputs_embeds.device)
+ position_ids = position_ids.view(1, -1).expand(batch_size, -1)
+ if cache_position is not None: # otherwise `deltas` is an int `0`
+ delta = delta.repeat_interleave(batch_size // delta.shape[0], dim=0)
+ position_ids = position_ids.add(delta)
+ position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
+
+ outputs = self.language_model(
+ input_ids=None,
+ position_ids=position_ids,
+ attention_mask=attention_mask,
+ past_key_values=past_key_values,
+ inputs_embeds=inputs_embeds,
+ cache_position=cache_position,
+ **kwargs,
+ )
+
+ return Glm46VModelOutputWithPast(
+ last_hidden_state=outputs.last_hidden_state,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ rope_deltas=self.rope_deltas,
+ )
+
+
+@dataclass
+@auto_docstring(
+ custom_intro="""
+ Base class for Glm46V causal language model (or autoregressive) outputs.
+ """
+)
+class Glm46VCausalLMOutputWithPast(ModelOutput):
+ r"""
+ loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided):
+ Language modeling loss (for next-token prediction).
+ logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.vocab_size)`):
+ Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax).
+ past_key_values (`Cache`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
+ It is a [`~cache_utils.Cache`] instance. For more details, see our [kv cache guide](https://huggingface.co/docs/transformers/en/kv_cache).
+
+ Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
+ `past_key_values` input) to speed up sequential decoding.
+ rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*):
+ The rope index difference between sequence length and multimodal rope.
+ """
+
+ loss: Optional[torch.FloatTensor] = None
+ logits: Optional[torch.FloatTensor] = None
+ past_key_values: Optional[Cache] = None
+ hidden_states: Optional[tuple[torch.FloatTensor]] = None
+ attentions: Optional[tuple[torch.FloatTensor]] = None
+ rope_deltas: Optional[torch.LongTensor] = None
+
+
+class Glm46VForConditionalGeneration(Glm46VPreTrainedModel, GenerationMixin):
+ _checkpoint_conversion_mapping = {}
+ _tied_weights_keys = {"lm_head.weight": "model.language_model.embed_tokens.weight"}
+ # Reference: fix gemma3 grad acc #37208
+ accepts_loss_kwargs = False
+
+ def __init__(self, config):
+ super().__init__(config)
+ self.model = Glm46VModel(config)
+ self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False)
+
+ self.post_init()
+
+ def get_input_embeddings(self):
+ return self.model.get_input_embeddings()
+
+ def set_input_embeddings(self, value):
+ self.model.set_input_embeddings(value)
+
+ def set_decoder(self, decoder):
+ self.model.set_decoder(decoder)
+
+ def get_decoder(self):
+ return self.model.get_decoder()
+
+ def get_video_features(
+ self, pixel_values_videos: torch.FloatTensor, video_grid_thw: Optional[torch.LongTensor] = None
+ ):
+ return self.model.get_video_features(pixel_values_videos, video_grid_thw)
+
+ def get_image_features(self, pixel_values: torch.FloatTensor, image_grid_thw: Optional[torch.LongTensor] = None):
+ return self.model.get_image_features(pixel_values, image_grid_thw)
+
+ # Make modules available through conditional class for BC
+ @property
+ def language_model(self):
+ return self.model.language_model
+
+ @property
+ def visual(self):
+ return self.model.visual
+
+ @can_return_tuple
+ @auto_docstring
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ labels: Optional[torch.LongTensor] = None,
+ pixel_values: Optional[torch.Tensor] = None,
+ pixel_values_videos: Optional[torch.FloatTensor] = None,
+ image_grid_thw: Optional[torch.LongTensor] = None,
+ video_grid_thw: Optional[torch.LongTensor] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ logits_to_keep: Union[int, torch.Tensor] = 0,
+ **kwargs: Unpack[TransformersKwargs],
+ ) -> Union[tuple, Glm46VCausalLMOutputWithPast]:
+ r"""
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
+ image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
+ The temporal, height and width of feature shape of each image in LLM.
+ video_grid_thw (`torch.LongTensor` of shape `(num_videos, 3)`, *optional*):
+ The temporal, height and width of feature shape of each video in LLM.
+
+ Example:
+
+ ```python
+ >>> from PIL import Image
+ >>> import requests
+ >>> from transformers import AutoProcessor, Glm46VForConditionalGeneration
+
+ >>> model = Glm46VForConditionalGeneration.from_pretrained("THUDM/GLM-4.1V-9B-Thinking")
+ >>> processor = AutoProcessor.from_pretrained("THUDM/GLM-4.1V-9B-Thinking")
+
+ >>> messages = [
+ {
+ "role": "user",
+ "content": [
+ {"type": "image"},
+ {"type": "text", "text": "What is shown in this image?"},
+ ],
+ },
+ ]
+ >>> url = "https://www.ilankelman.org/stopsigns/australia.jpg"
+ >>> image = Image.open(requests.get(url, stream=True).raw)
+
+ >>> text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
+ >>> inputs = processor(text=[text], images=[image], vision_infos=[vision_infos])
+
+ >>> # Generate
+ >>> generate_ids = model.generate(inputs.input_ids, max_length=30)
+ >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
+ "The image shows a street scene with a red stop sign in the foreground. In the background, there is a large red gate with Chinese characters ..."
+ ```"""
+ outputs = self.model(
+ input_ids=input_ids,
+ pixel_values=pixel_values,
+ pixel_values_videos=pixel_values_videos,
+ image_grid_thw=image_grid_thw,
+ video_grid_thw=video_grid_thw,
+ position_ids=position_ids,
+ attention_mask=attention_mask,
+ past_key_values=past_key_values,
+ inputs_embeds=inputs_embeds,
+ cache_position=cache_position,
+ **kwargs,
+ )
+
+ hidden_states = outputs[0]
+
+ # Only compute necessary logits, and do not upcast them to float if we are not computing the loss
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
+
+ loss = None
+ if labels is not None:
+ loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size)
+
+ return Glm46VCausalLMOutputWithPast(
+ loss=loss,
+ logits=logits,
+ past_key_values=outputs.past_key_values,
+ hidden_states=outputs.hidden_states,
+ attentions=outputs.attentions,
+ rope_deltas=outputs.rope_deltas,
+ )
+
+ def prepare_inputs_for_generation(
+ self,
+ input_ids,
+ past_key_values=None,
+ attention_mask=None,
+ inputs_embeds=None,
+ cache_position=None,
+ position_ids=None,
+ use_cache=True,
+ pixel_values=None,
+ pixel_values_videos=None,
+ image_grid_thw=None,
+ video_grid_thw=None,
+ **kwargs,
+ ):
+ # Overwritten -- in specific circumstances we don't want to forward image inputs to the model
+
+ model_inputs = super().prepare_inputs_for_generation(
+ input_ids,
+ past_key_values=past_key_values,
+ attention_mask=attention_mask,
+ inputs_embeds=inputs_embeds,
+ cache_position=cache_position,
+ position_ids=position_ids,
+ pixel_values=pixel_values,
+ pixel_values_videos=pixel_values_videos,
+ image_grid_thw=image_grid_thw,
+ video_grid_thw=video_grid_thw,
+ use_cache=use_cache,
+ **kwargs,
+ )
+
+ # GLM-4.1V position_ids are prepareed with rope_deltas in forward
+ model_inputs["position_ids"] = None
+
+ if cache_position[0] != 0:
+ model_inputs["pixel_values"] = None
+ model_inputs["pixel_values_videos"] = None
+
+ return model_inputs
+
+ def _get_image_nums_and_video_nums(
+ self,
+ input_ids: Optional[torch.LongTensor],
+ inputs_embeds: Optional[torch.Tensor] = None,
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ """
+ Get the number of images and videos for each sample to calculate the separation length of the sample tensor.
+ These parameters are not passed through the processor to avoid unpredictable impacts from interface modifications.
+
+ Args:
+ input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`):
+ Indices of input sequence tokens in the vocabulary.
+
+ Returns:
+ image_nums (`torch.LongTensor` of shape `(batch_size, num_images_sample)`)
+ video_nums (`torch.LongTensor` of shape `(batch_size, num_videos_sample)`)
+ """
+
+ if inputs_embeds is not None:
+ is_image = (
+ inputs_embeds
+ == self.get_input_embeddings()(
+ torch.tensor(self.config.image_start_token_id, dtype=torch.long, device=inputs_embeds.device)
+ )
+ )[..., 0]
+ is_video_start = (
+ inputs_embeds
+ == self.get_input_embeddings()(
+ torch.tensor(self.config.video_start_token_id, dtype=torch.long, device=inputs_embeds.device)
+ )
+ )[..., 0]
+ is_video_end = (
+ inputs_embeds
+ == self.get_input_embeddings()(
+ torch.tensor(self.config.video_end_token_id, dtype=torch.long, device=inputs_embeds.device)
+ )
+ )[..., 0]
+ else:
+ is_image = input_ids == self.config.image_start_token_id
+ is_video_start = input_ids == self.config.video_start_token_id
+ is_video_end = input_ids == self.config.video_end_token_id
+
+ # Cumulative sum to track if we're inside a video span
+ # We'll assume well-formed video tags (i.e. matching starts and ends)
+ video_level = torch.cumsum(is_video_start.int() - is_video_end.int(), dim=1)
+ inside_video = video_level > 0 # shape (batch_size, seq_length)
+
+ # Mask out image tokens that are inside video spans
+ standalone_images = is_image & (~inside_video)
+
+ # Count per batch
+ image_counts = standalone_images.sum(dim=1)
+ video_counts = is_video_start.sum(dim=1)
+
+ return image_counts, video_counts
+
+ def _expand_inputs_for_generation(
+ self,
+ expand_size: int = 1,
+ is_encoder_decoder: bool = False,
+ input_ids: Optional[torch.LongTensor] = None,
+ **model_kwargs,
+ ) -> tuple[torch.LongTensor, dict[str, Any]]:
+ # Overwritten -- Support for expanding tensors without a batch size dimension
+ # e.g., pixel_values, image_grid_thw, pixel_values_videos, video_grid_thw, second_per_grid_t
+ # pixel_values.shape[0] is sum(seqlen_images for samples)
+ # image_grid_thw.shape[0] is sum(num_images for samples)
+
+ if expand_size == 1:
+ return input_ids, model_kwargs
+
+ visual_keys = ["pixel_values", "image_grid_thw", "pixel_values_videos", "video_grid_thw", "second_per_grid_ts"]
+
+ def _expand_dict_for_generation_visual(dict_to_expand):
+ image_grid_thw = model_kwargs.get("image_grid_thw", None)
+ video_grid_thw = model_kwargs.get("video_grid_thw", None)
+ image_nums, video_nums = self._get_image_nums_and_video_nums(
+ input_ids, inputs_embeds=model_kwargs.get("inputs_embeds", None)
+ )
+
+ def _repeat_interleave_samples(x, lengths, repeat_times):
+ samples = torch.split(x, lengths)
+ repeat_args = [repeat_times] + [1] * (x.dim() - 1)
+ result = torch.cat([sample.repeat(*repeat_args) for sample in samples], dim=0)
+ return result
+
+ for key in dict_to_expand:
+ if key == "pixel_values":
+ # split images into samples
+ samples = torch.split(image_grid_thw, list(image_nums))
+ # compute the sequence length of images for each sample
+ lengths = [torch.prod(sample, dim=1).sum() for sample in samples]
+ dict_to_expand[key] = _repeat_interleave_samples(
+ dict_to_expand[key], lengths=lengths, repeat_times=expand_size
+ )
+ elif key == "image_grid_thw":
+ # get the num of images for each sample
+ lengths = list(image_nums)
+ dict_to_expand[key] = _repeat_interleave_samples(
+ dict_to_expand[key], lengths=lengths, repeat_times=expand_size
+ )
+ elif key == "pixel_values_videos":
+ samples = torch.split(video_grid_thw, list(video_nums))
+ lengths = [torch.prod(sample, dim=1).sum() for sample in samples]
+ dict_to_expand[key] = _repeat_interleave_samples(
+ dict_to_expand[key], lengths=lengths, repeat_times=expand_size
+ )
+ elif key == "video_grid_thw":
+ lengths = list(video_nums)
+ dict_to_expand[key] = _repeat_interleave_samples(
+ dict_to_expand[key], lengths=lengths, repeat_times=expand_size
+ )
+ elif key == "second_per_grid_ts":
+ dict_to_expand[key] = _repeat_interleave_samples(
+ dict_to_expand[key], lengths=list(video_nums), repeat_times=expand_size
+ )
+ return dict_to_expand
+
+ def _expand_dict_for_generation(dict_to_expand):
+ for key in dict_to_expand:
+ if (
+ key != "cache_position"
+ and dict_to_expand[key] is not None
+ and isinstance(dict_to_expand[key], torch.Tensor)
+ and key not in visual_keys
+ ):
+ dict_to_expand[key] = dict_to_expand[key].repeat_interleave(expand_size, dim=0)
+ return dict_to_expand
+
+ model_kwargs = _expand_dict_for_generation_visual(model_kwargs)
+
+ if input_ids is not None:
+ input_ids = input_ids.repeat_interleave(expand_size, dim=0)
+
+ model_kwargs = _expand_dict_for_generation(model_kwargs)
+
+ if is_encoder_decoder:
+ if model_kwargs.get("encoder_outputs") is None:
+ raise ValueError("If `is_encoder_decoder` is True, make sure that `encoder_outputs` is defined.")
+ model_kwargs["encoder_outputs"] = _expand_dict_for_generation(model_kwargs["encoder_outputs"])
+
+ return input_ids, model_kwargs
+
+
+__all__ = ["Glm46VModel", "Glm46VPreTrainedModel", "Glm46VForConditionalGeneration"]
diff --git a/src/transformers/models/glm46v/modular_glm46v.py b/src/transformers/models/glm46v/modular_glm46v.py
new file mode 100644
index 000000000000..0bb86b0aacf7
--- /dev/null
+++ b/src/transformers/models/glm46v/modular_glm46v.py
@@ -0,0 +1,217 @@
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Union
+
+import numpy as np
+
+from ...configuration_utils import PreTrainedConfig
+from ...video_utils import VideoMetadata
+from ..auto import CONFIG_MAPPING, AutoConfig, AutoModel
+from ..glm4v.image_processing_glm4v import Glm4vImageProcessor
+from ..glm4v.image_processing_glm4v_fast import Glm4vImageProcessorFast
+from ..glm4v.modeling_glm4v import Glm4vForConditionalGeneration, Glm4vModel, Glm4vPreTrainedModel
+from ..glm4v.processing_glm4v import Glm4vProcessor
+from ..glm4v.video_processing_glm4v import Glm4vVideoProcessor
+
+
+class Glm46VConfig(PreTrainedConfig):
+ r"""
+ This is the configuration class to store the configuration of a [`Glm4vModel`]. It is used to instantiate a
+ GLM-4.6V model according to the specified arguments, defining the model architecture. Instantiating a
+ configuration with the defaults will yield a similar configuration to that of
+ GLM-4.1V-9B-Thinking [zai-org/GLM-4.1V-9B-Thinking](https://huggingface.co/zai-org/GLM-4.1V-9B-Thinking).
+
+ Configuration objects inherit from [`PreTrainedConfig`] and can be used to control the model outputs. Read the
+ documentation from [`PreTrainedConfig`] for more information.
+
+ Args:
+ text_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Glm4vTextConfig`):
+ The config object or dictionary of the text backbone.
+ vision_config (`Union[PreTrainedConfig, dict]`, *optional*, defaults to `Glm4vVisionConfig`):
+ The config object or dictionary of the vision backbone.
+ image_token_id (`int`, *optional*, defaults to 151343):
+ The image token index to encode the image prompt.
+ video_token_id (`int`, *optional*, defaults to 151344):
+ The video token index to encode the image prompt.
+ image_start_token_id (`int`, *optional*, defaults to 151339):
+ The image start token index to encode the start of image.
+ image_end_token_id (`int`, *optional*, defaults to 151340):
+ The image end token index to encode the end of image.
+ video_start_token_id (`int`, *optional*, defaults to 151361):
+ The video start token index to encode the start of video.
+ video_end_token_id (`int`, *optional*, defaults to 151362):
+ The video end token index to encode the end of video.
+
+ ```python
+ >>> from transformers import Glm46VForConditionalGeneration, Glm46VConfig
+
+ >>> # Initializing a GLM-4.6V style configuration
+ >>> configuration = Glm46VConfig()
+
+ >>> # Initializing a model from the GLM-4.6V style configuration
+ >>> model = Glm4vForConditionalGeneration(configuration)
+
+ >>> # Accessing the model configuration
+ >>> configuration = model.config
+ ```"""
+
+ model_type = "glm46v"
+ sub_configs = {"text_config": AutoConfig, "vision_config": AutoConfig}
+ keys_to_ignore_at_inference = ["past_key_values"]
+
+ def __init__(
+ self,
+ text_config=None,
+ vision_config=None,
+ image_token_id=151343,
+ video_token_id=151344,
+ image_start_token_id=151339,
+ image_end_token_id=151340,
+ video_start_token_id=151361,
+ video_end_token_id=151362,
+ **kwargs,
+ ):
+ if isinstance(vision_config, dict):
+ vision_config["model_type"] = vision_config.get("model_type", "glm4v_vision")
+ self.vision_config = CONFIG_MAPPING[vision_config["model_type"]](**vision_config)
+ elif vision_config is None:
+ self.vision_config = CONFIG_MAPPING["glm4v_vision"]()
+
+ if isinstance(text_config, dict):
+ text_config["model_type"] = text_config.get("model_type", "glm4v_text")
+ self.text_config = CONFIG_MAPPING[text_config["model_type"]](**text_config)
+ elif text_config is None:
+ self.text_config = CONFIG_MAPPING["glm4v_text"]()
+
+ self.image_token_id = image_token_id
+ self.video_token_id = video_token_id
+ self.video_start_token_id = video_start_token_id
+ self.video_end_token_id = video_end_token_id
+ self.image_start_token_id = image_start_token_id
+ self.image_end_token_id = image_end_token_id
+
+ super().__init__(**kwargs)
+
+
+class Glm46VPreTrainedModel(Glm4vPreTrainedModel):
+ _can_record_outputs = None
+ _no_split_modules = None
+
+
+class Glm46VModel(Glm4vModel):
+ _no_split_modules = None
+
+ def __init__(self, config):
+ super().__init__(config)
+ self.visual = AutoModel.from_config(config.vision_config)
+ self.language_model = AutoModel.from_config(config.text_config)
+
+
+class Glm46VForConditionalGeneration(Glm4vForConditionalGeneration):
+ pass
+
+
+class Glm46VProcessor(Glm4vProcessor):
+ def replace_frame_token_id(self, timestamp_sec):
+ return f"<|begin_of_image|>{self.image_token}<|end_of_image|>{timestamp_sec:.1f} seconds"
+
+
+class Glm46VImageProcessor(Glm4vImageProcessor):
+ pass
+
+
+class Glm46VImageProcessorFast(Glm4vImageProcessorFast):
+ pass
+
+
+class Glm46VVideoProcessor(Glm4vVideoProcessor):
+ def sample_frames(
+ self,
+ metadata: VideoMetadata,
+ fps: Optional[Union[int, float]] = None,
+ **kwargs,
+ ):
+ if metadata is None or getattr(metadata, "fps", None) is None:
+ raise ValueError(
+ "Asked to sample frames per second but no video metadata was provided which is required when sampling in Glm46V. "
+ "Please pass in `VideoMetadata` object or set `do_sample_frames=False`"
+ )
+
+ total_frames = metadata.total_num_frames
+ max_frame_idx = total_frames - 1
+ duration = metadata.duration or round(max_frame_idx / metadata.fps) + 1
+
+ DYNAMIC_FPS_THRES = {30: 3, 300: 1, 2400: 0.5}
+ MAX_FRAME_COUNT_DYNAMIC = 640
+ MAX_DURATION = 2400
+ effective_duration = min(duration, MAX_DURATION)
+ if effective_duration <= 30:
+ target_fps = DYNAMIC_FPS_THRES[30]
+ elif effective_duration <= 300:
+ target_fps = DYNAMIC_FPS_THRES[300]
+ else:
+ target_fps = DYNAMIC_FPS_THRES[2400]
+ extract_t = int(effective_duration * target_fps * self.temporal_patch_size)
+ extract_t = min(extract_t, MAX_FRAME_COUNT_DYNAMIC)
+
+ duration_per_frame = 1 / metadata.fps
+ timestamps = [i * duration_per_frame for i in range(total_frames)]
+ max_second = int(duration)
+
+ if total_frames < extract_t:
+ frame_indices = np.linspace(0, total_frames - 1, extract_t, dtype=int).tolist()
+ else:
+ frame_indices = []
+ current_second = 0
+ inv_fps = 1 / (self.temporal_patch_size * target_fps)
+ for frame_index in range(total_frames):
+ if timestamps[frame_index] >= current_second:
+ current_second += inv_fps
+ frame_indices.append(frame_index)
+ if current_second >= max_second:
+ break
+
+ if len(frame_indices) < extract_t:
+ if len(frame_indices) == 0:
+ start, end = 0, max(total_frames - 1, 0)
+ else:
+ start, end = frame_indices[0], frame_indices[-1]
+ frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
+ elif len(frame_indices) > extract_t:
+ frame_indices = np.linspace(0, total_frames - 1, extract_t, dtype=int).tolist()
+
+ seen, uniq = set(), []
+ for idx in frame_indices:
+ if idx not in seen:
+ seen.add(idx)
+ uniq.append(idx)
+
+ if len(uniq) & 1:
+ uniq.append(uniq[-1])
+
+ return np.array(uniq)
+
+
+__all__ = [
+ "Glm46VConfig",
+ "Glm46VModel",
+ "Glm46VPreTrainedModel",
+ "Glm46VForConditionalGeneration",
+ "Glm46VProcessor",
+ "Glm46VImageProcessor",
+ "Glm46VImageProcessorFast",
+ "Glm46VVideoProcessor",
+]
diff --git a/src/transformers/models/glm46v/processing_glm46v.py b/src/transformers/models/glm46v/processing_glm46v.py
new file mode 100644
index 000000000000..ce6a17d3178b
--- /dev/null
+++ b/src/transformers/models/glm46v/processing_glm46v.py
@@ -0,0 +1,279 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Union
+
+import numpy as np
+
+from ...image_processing_utils import BatchFeature
+from ...image_utils import ImageInput
+from ...processing_utils import MultiModalData, ProcessingKwargs, ProcessorMixin, Unpack
+from ...tokenization_utils_base import PreTokenizedInput, TextInput
+from ...utils import logging
+from ...video_utils import VideoInput
+
+
+logger = logging.get_logger(__name__)
+
+
+class Glm46VProcessorKwargs(ProcessingKwargs, total=False):
+ _defaults = {
+ "text_kwargs": {
+ "padding": False,
+ "return_token_type_ids": False,
+ "return_mm_token_type_ids": False,
+ },
+ "videos_kwargs": {"return_metadata": True},
+ }
+
+
+class Glm46VProcessor(ProcessorMixin):
+ r"""
+ Constructs a GLM-4V processor which wraps a GLM-4V image processor and a GLM-4 tokenizer into a single processor.
+ [`~Glm46VProcessor.__call__`] and [`~Glm46VProcessor.decode`] for more information.
+ Args:
+ image_processor ([`Glm46VProcessor`], *optional*):
+ The image processor is a required input.
+ tokenizer ([`PreTrainedTokenizerFast`], *optional*):
+ The tokenizer is a required input.
+ video_processor ([`Glm46VVideoProcessor`], *optional*):
+ The video processor is a required input.
+ chat_template (`str`, *optional*): A Jinja template which will be used to convert lists of messages
+ in a chat into a tokenizable string.
+ """
+
+ def __init__(self, image_processor=None, tokenizer=None, video_processor=None, chat_template=None, **kwargs):
+ self.image_token = "<|image|>" if not hasattr(tokenizer, "image_token") else tokenizer.image_token
+ self.video_token = "<|video|>" if not hasattr(tokenizer, "video_token") else tokenizer.video_token
+ self.image_token_id = (
+ tokenizer.image_token_id
+ if getattr(tokenizer, "image_token_id", None)
+ else tokenizer.convert_tokens_to_ids(self.image_token)
+ )
+ self.video_token_id = (
+ tokenizer.video_token_id
+ if getattr(tokenizer, "video_token_id", None)
+ else tokenizer.convert_tokens_to_ids(self.video_token)
+ )
+ super().__init__(image_processor, tokenizer, video_processor, chat_template=chat_template)
+
+ def __call__(
+ self,
+ images: Optional[ImageInput] = None,
+ text: Union[TextInput, PreTokenizedInput, list[TextInput], list[PreTokenizedInput]] = None,
+ videos: Optional[VideoInput] = None,
+ **kwargs: Unpack[Glm46VProcessorKwargs],
+ ) -> BatchFeature:
+ """
+ Main method to prepare for the model one or several sequences(s) and image(s). This method forwards the `text`
+ and `kwargs` arguments to PreTrainedTokenizerFast's [`~PreTrainedTokenizerFast.__call__`] if `text` is not `None` to encode
+ the text.
+
+ Args:
+ images (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor`, `List[PIL.Image.Image]`, `List[np.ndarray]`, `List[torch.Tensor]`):
+ The image or batch of images to be prepared. Each image can be a PIL image, NumPy array or PyTorch
+ tensor. Both channels-first and channels-last formats are supported.
+ text (`str`, `List[str]`, `List[List[str]]`):
+ The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
+ (pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
+ `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
+ videos (`np.ndarray`, `torch.Tensor`, `List[np.ndarray]`, `List[torch.Tensor]`):
+ The image or batch of videos to be prepared. Each video can be a 4D NumPy array or PyTorch
+ tensor, or a nested list of 3D frames. Both channels-first and channels-last formats are supported.
+ return_tensors (`str` or [`~utils.TensorType`], *optional*):
+ If set, will return tensors of a particular framework. Acceptable values are:
+ - `'pt'`: Return PyTorch `torch.Tensor` objects.
+ - `'np'`: Return NumPy `np.ndarray` objects.
+
+ Returns:
+ [`BatchFeature`]: A [`BatchFeature`] with the following fields:
+
+ - **input_ids** -- List of token ids to be fed to a model. Returned when `text` is not `None`.
+ - **attention_mask** -- List of indices specifying which tokens should be attended to by the model (when
+ `return_attention_mask=True` or if *"attention_mask"* is in `self.model_input_names` and if `text` is not
+ `None`).
+ - **pixel_values** -- Pixel values to be fed to a model. Returned when `images` is not `None`.
+ - **pixel_values_videos** -- Pixel values of videos to be fed to a model. Returned when `videos` is not `None`.
+ - **image_grid_thw** -- List of image 3D grid in LLM. Returned when `images` is not `None`.
+ - **video_grid_thw** -- List of video 3D grid in LLM. Returned when `videos` is not `None`.
+ """
+ output_kwargs = self._merge_kwargs(
+ Glm46VProcessorKwargs,
+ tokenizer_init_kwargs=self.tokenizer.init_kwargs,
+ **kwargs,
+ )
+ if images is not None:
+ image_inputs = self.image_processor(images=images, **output_kwargs["images_kwargs"])
+ image_grid_thw = image_inputs["image_grid_thw"]
+ else:
+ image_inputs = {}
+ image_grid_thw = None
+
+ if videos is not None:
+ videos_inputs = self.video_processor(videos=videos, **output_kwargs["videos_kwargs"])
+ # If user has not requested video metadata, pop it
+ if not kwargs.get("return_metadata"):
+ video_metadata = videos_inputs.pop("video_metadata")
+ else:
+ video_metadata = videos_inputs["video_metadata"]
+ video_grid_thw = videos_inputs["video_grid_thw"]
+ else:
+ videos_inputs = {}
+ video_grid_thw = None
+
+ if not isinstance(text, list):
+ text = [text]
+
+ text = text.copy() # below lines change text in-place
+ if image_grid_thw is not None:
+ merge_length = self.image_processor.merge_size**2
+ index = 0
+ for i in range(len(text)):
+ while self.image_token in text[i]:
+ num_image_tokens = image_grid_thw[index].prod() // merge_length
+ text[i] = text[i].replace(self.image_token, "<|placeholder|>" * num_image_tokens, 1)
+ index += 1
+ text[i] = text[i].replace("<|placeholder|>", self.image_token)
+
+ if video_grid_thw is not None:
+ merge_length = self.video_processor.merge_size**2
+ video_index = 0
+ for i in range(len(text)):
+ while self.video_token in text[i]:
+ num_frames = video_grid_thw[video_index][0]
+ video_structure = ""
+
+ metadata = video_metadata[video_index]
+ if metadata.fps is None:
+ logger.warning_once(
+ "SmolVLM requires frame timestamps to construct prompts, but the `fps` of the input video could not be inferred. "
+ "Probably `video_metadata` was missing from inputs and you passed pre-sampled frames. "
+ "Defaulting to `fps=24`. Please provide `video_metadata` for more accurate results."
+ )
+ metadata.fps = 24 if metadata.fps is None else metadata.fps
+ timestamps = metadata.timestamps[::2] # mrope
+
+ unique_timestamps = []
+ for idx in range(0, len(timestamps)):
+ unique_timestamps.append(timestamps[idx])
+
+ selected_timestamps = unique_timestamps[:num_frames]
+ while len(selected_timestamps) < num_frames:
+ selected_timestamps.append(selected_timestamps[-1] if selected_timestamps else 0)
+
+ for frame_idx in range(num_frames):
+ timestamp_sec = selected_timestamps[frame_idx]
+ frame_structure = self.replace_frame_token_id(timestamp_sec)
+ video_structure += frame_structure
+
+ text[i] = text[i].replace(self.video_token, video_structure, 1)
+ num_image_tokens = (
+ video_grid_thw[video_index].prod() // merge_length // video_grid_thw[video_index][0]
+ )
+ for frame_idx in range(num_frames):
+ if self.image_token in text[i]:
+ text[i] = text[i].replace(self.image_token, "<|placeholder|>" * num_image_tokens, 1)
+
+ video_index += 1
+
+ text[i] = text[i].replace("<|placeholder|>", self.image_token)
+ return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
+ return_mm_token_type_ids = output_kwargs["text_kwargs"].pop("return_mm_token_type_ids", False)
+ text_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
+ self._check_special_mm_tokens(text, text_inputs, modalities=["image", "video"])
+
+ if return_mm_token_type_ids:
+ array_ids = np.array(text_inputs["input_ids"])
+ mm_token_type_ids = np.zeros_like(text_inputs["input_ids"])
+ mm_token_type_ids[array_ids == self.image_token_id] = 1
+ text_inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
+ return BatchFeature(data={**text_inputs, **image_inputs, **videos_inputs}, tensor_type=return_tensors)
+
+ def _get_num_multimodal_tokens(self, image_sizes=None, video_sizes=None, **kwargs):
+ """
+ Computes the number of placeholder tokens needed for multimodal inputs with the given sizes.
+ Args:
+ image_sizes (`list[list[int]]`, *optional*):
+ The input sizes formatted as (height, width) per each image.
+ video_sizes (`list[list[int]]`, *optional*):
+ The input sizes formatted as (num_frames, height, width) per each video.
+ Returns:
+ `MultiModalData`: A `MultiModalData` object holding number of tokens per each of the provided
+ input modalities, along with other useful data.
+ """
+
+ vision_data = {}
+ if image_sizes is not None:
+ images_kwargs = Glm46VProcessorKwargs._defaults.get("images_kwargs", {})
+ images_kwargs.update(kwargs)
+ merge_size = images_kwargs.get("merge_size", None) or self.image_processor.merge_size
+
+ num_image_patches = [
+ self.image_processor.get_number_of_image_patches(*image_size, images_kwargs)
+ for image_size in image_sizes
+ ]
+ num_image_tokens = [(num_patches // merge_size**2) for num_patches in num_image_patches]
+ vision_data.update({"num_image_tokens": num_image_tokens, "num_image_patches": num_image_patches})
+
+ if video_sizes is not None:
+ videos_kwargs = Glm46VProcessorKwargs._defaults.get("videos_kwargs", {})
+ videos_kwargs.update(kwargs)
+ num_video_patches = [
+ self.video_processor.get_number_of_video_patches(*video_size, videos_kwargs)
+ for video_size in video_sizes
+ ]
+ num_video_tokens = [(num_patches // merge_size**2) for num_patches in num_video_patches]
+ vision_data["num_video_tokens"] = num_video_tokens
+
+ return MultiModalData(**vision_data)
+
+ def post_process_image_text_to_text(
+ self, generated_outputs, skip_special_tokens=True, clean_up_tokenization_spaces=False, **kwargs
+ ):
+ """
+ Post-process the output of the model to decode the text.
+
+ Args:
+ generated_outputs (`torch.Tensor` or `np.ndarray`):
+ The output of the model `generate` function. The output is expected to be a tensor of shape `(batch_size, sequence_length)`
+ or `(sequence_length,)`.
+ skip_special_tokens (`bool`, *optional*, defaults to `True`):
+ Whether or not to remove special tokens in the output. Argument passed to the tokenizer's `batch_decode` method.
+ clean_up_tokenization_spaces (`bool`, *optional*, defaults to `False`):
+ Whether or not to clean up the tokenization spaces. Argument passed to the tokenizer's `batch_decode` method.
+ **kwargs:
+ Additional arguments to be passed to the tokenizer's `batch_decode method`.
+
+ Returns:
+ `list[str]`: The decoded text.
+ """
+ return self.tokenizer.batch_decode(
+ generated_outputs,
+ skip_special_tokens=skip_special_tokens,
+ clean_up_tokenization_spaces=clean_up_tokenization_spaces,
+ **kwargs,
+ )
+
+ def replace_frame_token_id(self, timestamp_sec):
+ return f"<|begin_of_image|>{self.image_token}<|end_of_image|>{timestamp_sec:.1f} seconds"
+
+
+__all__ = ["Glm46VProcessor"]
diff --git a/src/transformers/models/glm46v/video_processing_glm46v.py b/src/transformers/models/glm46v/video_processing_glm46v.py
new file mode 100644
index 000000000000..f8e6d248502b
--- /dev/null
+++ b/src/transformers/models/glm46v/video_processing_glm46v.py
@@ -0,0 +1,281 @@
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# This file was automatically generated from src/transformers/models/glm46v/modular_glm46v.py.
+# Do NOT edit this file manually as any edits will be overwritten by the generation of
+# the file from the modular. If any change should be done, please apply the change to the
+# modular_glm46v.py file directly. One of our CI enforces this.
+# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
+# coding=utf-8
+# Copyright 2025 the HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Optional, Union
+
+import numpy as np
+import torch
+
+from ...image_processing_utils import BatchFeature
+from ...image_utils import (
+ OPENAI_CLIP_MEAN,
+ OPENAI_CLIP_STD,
+ ChannelDimension,
+ PILImageResampling,
+ SizeDict,
+ get_image_size,
+)
+from ...processing_utils import Unpack, VideosKwargs
+from ...utils import TensorType, add_start_docstrings
+from ...video_processing_utils import BASE_VIDEO_PROCESSOR_DOCSTRING, BaseVideoProcessor
+from ...video_utils import VideoMetadata, group_videos_by_shape, reorder_videos
+from .image_processing_glm46v import smart_resize
+
+
+class Glm46VVideoProcessorInitKwargs(VideosKwargs, total=False):
+ max_image_size: dict[str, int]
+ patch_size: int
+ temporal_patch_size: int
+ merge_size: int
+ max_duration: int
+
+
+@add_start_docstrings(
+ "Constructs a fast GLM-4V image processor that dynamically resizes videos based on the original videos.",
+ BASE_VIDEO_PROCESSOR_DOCSTRING,
+ """
+ patch_size (`int`, *optional*, defaults to 14):
+ The spacial patch size of the vision encoder.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The temporal patch size of the vision encoder.
+ merge_size (`int`, *optional*, defaults to 2):
+ The merge size of the vision encoder to llm encoder.
+ """,
+)
+class Glm46VVideoProcessor(BaseVideoProcessor):
+ resample = PILImageResampling.BICUBIC
+ size = {"shortest_edge": 112 * 112, "longest_edge": 28 * 28 * 2 * 30000}
+ max_image_size = {"longest_edge": 28 * 28 * 2 * 30000}
+ image_mean = OPENAI_CLIP_MEAN
+ image_std = OPENAI_CLIP_STD
+ do_resize = True
+ do_rescale = True
+ do_normalize = True
+ do_convert_rgb = True
+ do_sample_frames = True
+ patch_size = 14
+ temporal_patch_size = 2
+ max_duration = 300
+ merge_size = 2
+ valid_kwargs = Glm46VVideoProcessorInitKwargs
+ num_frames = 16
+ fps = 2
+
+ model_input_names = ["pixel_values_videos", "video_grid_thw"]
+
+ def __init__(self, **kwargs: Unpack[Glm46VVideoProcessorInitKwargs]):
+ super().__init__(**kwargs)
+ if self.size is not None and (
+ self.size.get("shortest_edge", None) is None or self.size.get("longest_edge", None) is None
+ ):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+
+ def _further_process_kwargs(
+ self,
+ size: Optional[SizeDict] = None,
+ **kwargs,
+ ) -> dict:
+ """
+ Update kwargs that need further processing before being validated
+ Can be overridden by subclasses to customize the processing of kwargs.
+ """
+ if size is not None and ("shortest_edge" not in size or "longest_edge" not in size):
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
+
+ return super()._further_process_kwargs(size=size, **kwargs)
+
+ def sample_frames(
+ self,
+ metadata: VideoMetadata,
+ fps: Optional[Union[int, float]] = None,
+ **kwargs,
+ ):
+ """
+ Args:
+ metadata (`VideoMetadata`):
+ Metadata of the video containing information about total duration, fps and total number of frames.
+ fps (`int` or `float`, *optional*):
+ Target frames to sample per second. Defaults to `self.fps`.
+ Returns:
+ np.ndarray:
+ Indices to sample video frames.
+ """
+ if metadata is None or getattr(metadata, "fps", None) is None:
+ raise ValueError(
+ "Asked to sample frames per second but no video metadata was provided which is required when sampling in Glm46V. "
+ "Please pass in `VideoMetadata` object or set `do_sample_frames=False`"
+ )
+
+ total_frames = metadata.total_num_frames
+ max_frame_idx = total_frames - 1
+ duration = metadata.duration or round(max_frame_idx / metadata.fps) + 1
+
+ DYNAMIC_FPS_THRES = {30: 3, 300: 1, 2400: 0.5}
+ MAX_FRAME_COUNT_DYNAMIC = 640
+ MAX_DURATION = 2400
+ effective_duration = min(duration, MAX_DURATION)
+ if effective_duration <= 30:
+ target_fps = DYNAMIC_FPS_THRES[30]
+ elif effective_duration <= 300:
+ target_fps = DYNAMIC_FPS_THRES[300]
+ else:
+ target_fps = DYNAMIC_FPS_THRES[2400]
+ extract_t = int(effective_duration * target_fps * self.temporal_patch_size)
+ extract_t = min(extract_t, MAX_FRAME_COUNT_DYNAMIC)
+
+ duration_per_frame = 1 / metadata.fps
+ timestamps = [i * duration_per_frame for i in range(total_frames)]
+ max_second = int(duration)
+
+ if total_frames < extract_t:
+ frame_indices = np.linspace(0, total_frames - 1, extract_t, dtype=int).tolist()
+ else:
+ frame_indices = []
+ current_second = 0
+ inv_fps = 1 / (self.temporal_patch_size * target_fps)
+ for frame_index in range(total_frames):
+ if timestamps[frame_index] >= current_second:
+ current_second += inv_fps
+ frame_indices.append(frame_index)
+ if current_second >= max_second:
+ break
+
+ if len(frame_indices) < extract_t:
+ if len(frame_indices) == 0:
+ start, end = 0, max(total_frames - 1, 0)
+ else:
+ start, end = frame_indices[0], frame_indices[-1]
+ frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
+ elif len(frame_indices) > extract_t:
+ frame_indices = np.linspace(0, total_frames - 1, extract_t, dtype=int).tolist()
+
+ seen, uniq = set(), []
+ for idx in frame_indices:
+ if idx not in seen:
+ seen.add(idx)
+ uniq.append(idx)
+
+ if len(uniq) & 1:
+ uniq.append(uniq[-1])
+
+ return np.array(uniq)
+
+ def _preprocess(
+ self,
+ videos: list[torch.Tensor],
+ do_convert_rgb: bool = True,
+ do_resize: bool = True,
+ size: Optional[SizeDict] = None,
+ interpolation: PILImageResampling = PILImageResampling.BICUBIC,
+ do_rescale: bool = True,
+ rescale_factor: float = 1 / 255.0,
+ do_normalize: bool = True,
+ image_mean: Optional[Union[float, list[float]]] = None,
+ image_std: Optional[Union[float, list[float]]] = None,
+ patch_size: Optional[int] = None,
+ temporal_patch_size: Optional[int] = None,
+ merge_size: Optional[int] = None,
+ return_tensors: Optional[Union[str, TensorType]] = None,
+ **kwargs,
+ ):
+ grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
+ resized_videos_grouped = {}
+
+ for shape, stacked_videos in grouped_videos.items():
+ B, T, C, H, W = stacked_videos.shape
+ num_frames, height, width = T, H, W
+ if do_resize:
+ resized_height, resized_width = smart_resize(
+ num_frames=num_frames,
+ height=height,
+ width=width,
+ temporal_factor=temporal_patch_size,
+ factor=patch_size * merge_size,
+ min_pixels=size.shortest_edge,
+ max_pixels=size.longest_edge,
+ )
+ stacked_videos = stacked_videos.view(B * T, C, H, W)
+ stacked_videos = self.resize(
+ stacked_videos,
+ size=SizeDict(height=resized_height, width=resized_width),
+ interpolation=interpolation,
+ )
+ stacked_videos = stacked_videos.view(B, T, C, resized_height, resized_width)
+ resized_videos_grouped[shape] = stacked_videos
+ resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
+
+ # Group videos by size for further processing
+ # Needed in case do_resize is False, or resize returns videos with different sizes
+ grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
+ processed_videos_grouped = {}
+ processed_grids = {}
+ for shape, stacked_videos in grouped_videos.items():
+ resized_height, resized_width = get_image_size(stacked_videos[0], channel_dim=ChannelDimension.FIRST)
+
+ # Fused rescale and normalize
+ stacked_videos = self.rescale_and_normalize(
+ stacked_videos, do_rescale, rescale_factor, do_normalize, image_mean, image_std
+ )
+ patches = stacked_videos
+
+ # Check that videos have `num_frames` divisible by `temporal_patch_size`
+ if patches.shape[1] % temporal_patch_size != 0:
+ repeats = patches[:, -1:].repeat(1, temporal_patch_size - 1, 1, 1, 1)
+ patches = torch.cat([patches, repeats], dim=1)
+ batch_size, grid_t, channel = patches.shape[:3]
+ grid_t = grid_t // temporal_patch_size
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
+
+ patches = patches.view(
+ batch_size,
+ grid_t,
+ temporal_patch_size,
+ channel,
+ grid_h // merge_size,
+ merge_size,
+ patch_size,
+ grid_w // merge_size,
+ merge_size,
+ patch_size,
+ )
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
+ flatten_patches = patches.reshape(
+ batch_size,
+ grid_t * grid_h * grid_w,
+ channel * temporal_patch_size * patch_size * patch_size,
+ )
+
+ processed_videos_grouped[shape] = flatten_patches
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
+
+ processed_videos = reorder_videos(processed_videos_grouped, grouped_videos_index)
+ processed_grids = reorder_videos(processed_grids, grouped_videos_index)
+ pixel_values_videos = torch.cat(processed_videos, dim=0)
+ video_grid_thw = torch.tensor(processed_grids)
+ data = {
+ "pixel_values_videos": pixel_values_videos,
+ "video_grid_thw": video_grid_thw,
+ }
+
+ return BatchFeature(data=data, tensor_type=return_tensors)
+
+
+__all__ = ["Glm46VVideoProcessor"]
diff --git a/src/transformers/models/glm4v/__init__.py b/src/transformers/models/glm4v/__init__.py
index 4216c137fbe2..65d56c9a6467 100644
--- a/src/transformers/models/glm4v/__init__.py
+++ b/src/transformers/models/glm4v/__init__.py
@@ -19,8 +19,10 @@
if TYPE_CHECKING:
from .configuration_glm4v import *
+ from .image_processing_glm4v import *
from .modeling_glm4v import *
from .processing_glm4v import *
+ from .video_processing_glm4v import *
else:
import sys
diff --git a/src/transformers/models/glm4v/configuration_glm4v.py b/src/transformers/models/glm4v/configuration_glm4v.py
index c8f2ef75ca71..7370a80b52f2 100644
--- a/src/transformers/models/glm4v/configuration_glm4v.py
+++ b/src/transformers/models/glm4v/configuration_glm4v.py
@@ -32,39 +32,35 @@ class Glm4vVisionConfig(PreTrainedConfig):
GLM-4.1V-9B-Thinking [THUDM/GLM-4.1V-9B-Thinking](https://huggingface.co/THUDM/GLM-4.1V-9B-Thinking).
Args:
- hidden_size (`int`, *optional*, defaults to 1536):
- Dimensionality of the encoder layers and the pooler layer.
- depth (`int`, *optional*, defaults to 24):
- Number of layers (depth) in the model.
- attention_bias (`bool`, *optional*, defaults to `False`):
- Whether to add a bias to the queries, keys and values.
- intermediate_size (`int`, *optional*, defaults to 13696):
- Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
- hidden_act (`str` or `function`, *optional*, defaults to `"selu"`):
- The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
- `"relu"`, `"selu"` and `"gelu_new"` are supported.
- hidden_dropout_prob (`float`, *optional*, defaults to 0.0):
- The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.
- attention_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for attention weights.
- projection_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for the projection layer.
- initializer_range (`float`, *optional*, defaults to 0.02):
- The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
- image_size (`int` or `list[int]`, *optional*, defaults to `[336, 336]`):
- The size (resolution) of each image.
- patch_size (`int`, *optional*, defaults to `14`):
- The size (resolution) of each patch.
- num_channels (`int`, *optional*, defaults to 3):
- The number of input channels.
- out_hidden_size (`int`, *optional*, defaults to 4096):
- The output hidden size of the vision model.
- rms_norm_eps (`float`, *optional*, defaults to 1e-05):
- The epsilon used by the rms normalization layers.
- spatial_merge_size (`int`, *optional*, defaults to 2):
- The size used for merging spatial dimensions.
- temporal_patch_size (`int`, *optional*, defaults to 2):
- The size used for patches along the temporal dimension.
+ depth (`int`, *optional*, defaults to 24):
+ Number of layers (depth) in the model.
+ hidden_size (`int`, *optional*, defaults to 1536):
+ Dimensionality of the encoder layers and the pooler layer.
+ hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
+ `"relu"`, `"selu"` and `"gelu_new"` are supported.
+ attention_bias (`bool`, *optional*, defaults to `False`):
+ Whether to add a bias to the queries, keys and values.
+ attention_dropout (`float`, *optional*, defaults to 0.0):
+ Dropout probability for attention weights.
+ num_heads (``, *optional*, defaults to 12):
+ in_channels (``, *optional*, defaults to 3):
+ image_size (`int` or `list[int]`, *optional*, defaults to 336):
+ The size (resolution) of each image.
+ patch_size (`int`, *optional*, defaults to 14):
+ The size (resolution) of each patch.
+ rms_norm_eps (`float`, *optional*, defaults to 1e-05):
+ The epsilon used by the rms normalization layers.
+ spatial_merge_size (`int`, *optional*, defaults to 2):
+ The size used for merging spatial dimensions.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The size used for patches along the temporal dimension.
+ out_hidden_size (`int`, *optional*, defaults to 4096):
+ The output hidden size of the vision model.
+ intermediate_size (`int`, *optional*, defaults to 13696):
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
+ initializer_range (`float`, *optional*, defaults to 0.02):
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
Example:
```python
@@ -80,7 +76,7 @@ class Glm4vVisionConfig(PreTrainedConfig):
>>> configuration = model.config
```"""
- model_type = "glm4v"
+ model_type = "glm4v_vision"
base_config_key = "vision_config"
def __init__(
@@ -334,4 +330,4 @@ def __init__(
super().__init__(**kwargs)
-__all__ = ["Glm4vConfig", "Glm4vTextConfig"]
+__all__ = ["Glm4vConfig", "Glm4vTextConfig", "Glm4vVisionConfig"]
diff --git a/src/transformers/models/glm4v/convert_glm4v_mgt_weights_to_hf.py b/src/transformers/models/glm4v/convert_glm4v_mgt_weights_to_hf.py
index 722dab5759fc..fb57f66a9ae0 100644
--- a/src/transformers/models/glm4v/convert_glm4v_mgt_weights_to_hf.py
+++ b/src/transformers/models/glm4v/convert_glm4v_mgt_weights_to_hf.py
@@ -710,6 +710,7 @@ def offset_layer(x, offset=llm_layer_offset):
if "vision_config" in model_config:
vision_config = {
+ "model_type": "glm4v_vision",
"hidden_size": model_config["vision_config"].get("hidden_size", 1536),
"depth": model_config["vision_config"].get("num_layers", 24),
"num_heads": model_config["vision_config"].get("num_attention_heads", 12),
diff --git a/src/transformers/models/glm4v/modeling_glm4v.py b/src/transformers/models/glm4v/modeling_glm4v.py
index 689b7e0b5e23..47ad72ac96ce 100644
--- a/src/transformers/models/glm4v/modeling_glm4v.py
+++ b/src/transformers/models/glm4v/modeling_glm4v.py
@@ -1680,4 +1680,4 @@ def _expand_dict_for_generation(dict_to_expand):
return input_ids, model_kwargs
-__all__ = ["Glm4vForConditionalGeneration", "Glm4vModel", "Glm4vPreTrainedModel", "Glm4vTextModel"]
+__all__ = ["Glm4vForConditionalGeneration", "Glm4vModel", "Glm4vPreTrainedModel", "Glm4vTextModel", "Glm4vVisionModel"]
diff --git a/src/transformers/models/glm4v/modular_glm4v.py b/src/transformers/models/glm4v/modular_glm4v.py
index a6e303d8e7bb..2df8b6f9d04a 100644
--- a/src/transformers/models/glm4v/modular_glm4v.py
+++ b/src/transformers/models/glm4v/modular_glm4v.py
@@ -69,39 +69,35 @@ class Glm4vVisionConfig(PreTrainedConfig):
GLM-4.1V-9B-Thinking [THUDM/GLM-4.1V-9B-Thinking](https://huggingface.co/THUDM/GLM-4.1V-9B-Thinking).
Args:
- hidden_size (`int`, *optional*, defaults to 1536):
- Dimensionality of the encoder layers and the pooler layer.
- depth (`int`, *optional*, defaults to 24):
- Number of layers (depth) in the model.
- attention_bias (`bool`, *optional*, defaults to `False`):
- Whether to add a bias to the queries, keys and values.
- intermediate_size (`int`, *optional*, defaults to 13696):
- Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
- hidden_act (`str` or `function`, *optional*, defaults to `"selu"`):
- The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
- `"relu"`, `"selu"` and `"gelu_new"` are supported.
- hidden_dropout_prob (`float`, *optional*, defaults to 0.0):
- The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.
- attention_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for attention weights.
- projection_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for the projection layer.
- initializer_range (`float`, *optional*, defaults to 0.02):
- The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
- image_size (`int` or `list[int]`, *optional*, defaults to `[336, 336]`):
- The size (resolution) of each image.
- patch_size (`int`, *optional*, defaults to `14`):
- The size (resolution) of each patch.
- num_channels (`int`, *optional*, defaults to 3):
- The number of input channels.
- out_hidden_size (`int`, *optional*, defaults to 4096):
- The output hidden size of the vision model.
- rms_norm_eps (`float`, *optional*, defaults to 1e-05):
- The epsilon used by the rms normalization layers.
- spatial_merge_size (`int`, *optional*, defaults to 2):
- The size used for merging spatial dimensions.
- temporal_patch_size (`int`, *optional*, defaults to 2):
- The size used for patches along the temporal dimension.
+ depth (`int`, *optional*, defaults to 24):
+ Number of layers (depth) in the model.
+ hidden_size (`int`, *optional*, defaults to 1536):
+ Dimensionality of the encoder layers and the pooler layer.
+ hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
+ `"relu"`, `"selu"` and `"gelu_new"` are supported.
+ attention_bias (`bool`, *optional*, defaults to `False`):
+ Whether to add a bias to the queries, keys and values.
+ attention_dropout (`float`, *optional*, defaults to 0.0):
+ Dropout probability for attention weights.
+ num_heads (``, *optional*, defaults to 12):
+ in_channels (``, *optional*, defaults to 3):
+ image_size (`int` or `list[int]`, *optional*, defaults to 336):
+ The size (resolution) of each image.
+ patch_size (`int`, *optional*, defaults to 14):
+ The size (resolution) of each patch.
+ rms_norm_eps (`float`, *optional*, defaults to 1e-05):
+ The epsilon used by the rms normalization layers.
+ spatial_merge_size (`int`, *optional*, defaults to 2):
+ The size used for merging spatial dimensions.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The size used for patches along the temporal dimension.
+ out_hidden_size (`int`, *optional*, defaults to 4096):
+ The output hidden size of the vision model.
+ intermediate_size (`int`, *optional*, defaults to 13696):
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
+ initializer_range (`float`, *optional*, defaults to 0.02):
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
Example:
```python
@@ -117,7 +113,7 @@ class Glm4vVisionConfig(PreTrainedConfig):
>>> configuration = model.config
```"""
- model_type = "glm4v"
+ model_type = "glm4v_vision"
base_config_key = "vision_config"
def __init__(
@@ -1653,7 +1649,7 @@ def __call__(
for frame_idx in range(num_frames):
timestamp_sec = selected_timestamps[frame_idx]
- frame_structure = f"<|begin_of_image|>{self.image_token}<|end_of_image|>{int(timestamp_sec)}"
+ frame_structure = self.replace_frame_token_id(timestamp_sec)
video_structure += frame_structure
text[i] = text[i].replace(self.video_token, video_structure, 1)
@@ -1679,13 +1675,18 @@ def __call__(
text_inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
return BatchFeature(data={**text_inputs, **image_inputs, **videos_inputs}, tensor_type=return_tensors)
+ def replace_frame_token_id(self, timestamp_sec):
+ return f"<|begin_of_image|>{self.image_token}<|end_of_image|>{int(timestamp_sec)}"
+
__all__ = [
"Glm4vConfig",
"Glm4vTextConfig",
+ "Glm4vVisionConfig",
"Glm4vForConditionalGeneration",
"Glm4vModel",
"Glm4vPreTrainedModel",
"Glm4vProcessor",
"Glm4vTextModel",
+ "Glm4vVisionModel",
]
diff --git a/src/transformers/models/glm4v/processing_glm4v.py b/src/transformers/models/glm4v/processing_glm4v.py
index 80b151566e5a..73202f073f9b 100644
--- a/src/transformers/models/glm4v/processing_glm4v.py
+++ b/src/transformers/models/glm4v/processing_glm4v.py
@@ -180,7 +180,7 @@ def __call__(
for frame_idx in range(num_frames):
timestamp_sec = selected_timestamps[frame_idx]
- frame_structure = f"<|begin_of_image|>{self.image_token}<|end_of_image|>{int(timestamp_sec)}"
+ frame_structure = self.replace_frame_token_id(timestamp_sec)
video_structure += frame_structure
text[i] = text[i].replace(self.video_token, video_structure, 1)
@@ -271,5 +271,8 @@ def post_process_image_text_to_text(
**kwargs,
)
+ def replace_frame_token_id(self, timestamp_sec):
+ return f"<|begin_of_image|>{self.image_token}<|end_of_image|>{int(timestamp_sec)}"
+
__all__ = ["Glm4vProcessor"]
diff --git a/src/transformers/models/glm4v_moe/configuration_glm4v_moe.py b/src/transformers/models/glm4v_moe/configuration_glm4v_moe.py
index efd7c27af72f..3d158e1b19cb 100644
--- a/src/transformers/models/glm4v_moe/configuration_glm4v_moe.py
+++ b/src/transformers/models/glm4v_moe/configuration_glm4v_moe.py
@@ -32,39 +32,35 @@ class Glm4vMoeVisionConfig(PreTrainedConfig):
GLM-4.1V-9B-Thinking [THUDM/GLM-4.1V-9B-Thinking](https://huggingface.co/THUDM/GLM-4.1V-9B-Thinking).
Args:
- hidden_size (`int`, *optional*, defaults to 1536):
- Dimensionality of the encoder layers and the pooler layer.
- depth (`int`, *optional*, defaults to 24):
- Number of layers (depth) in the model.
- attention_bias (`bool`, *optional*, defaults to `False`):
- Whether to add a bias to the queries, keys and values.
- intermediate_size (`int`, *optional*, defaults to 13696):
- Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
- hidden_act (`str` or `function`, *optional*, defaults to `"selu"`):
- The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
- `"relu"`, `"selu"` and `"gelu_new"` are supported.
- hidden_dropout_prob (`float`, *optional*, defaults to 0.0):
- The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.
- attention_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for attention weights.
- projection_dropout (`float`, *optional*, defaults to 0.0):
- Dropout probability for the projection layer.
- initializer_range (`float`, *optional*, defaults to 0.02):
- The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
- image_size (`int` or `list[int]`, *optional*, defaults to `[336, 336]`):
- The size (resolution) of each image.
- patch_size (`int`, *optional*, defaults to `14`):
- The size (resolution) of each patch.
- num_channels (`int`, *optional*, defaults to 3):
- The number of input channels.
- out_hidden_size (`int`, *optional*, defaults to 4096):
- The output hidden size of the vision model.
- rms_norm_eps (`float`, *optional*, defaults to 1e-05):
- The epsilon used by the rms normalization layers.
- spatial_merge_size (`int`, *optional*, defaults to 2):
- The size used for merging spatial dimensions.
- temporal_patch_size (`int`, *optional*, defaults to 2):
- The size used for patches along the temporal dimension.
+ depth (`int`, *optional*, defaults to 24):
+ Number of layers (depth) in the model.
+ hidden_size (`int`, *optional*, defaults to 1536):
+ Dimensionality of the encoder layers and the pooler layer.
+ hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
+ The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
+ `"relu"`, `"selu"` and `"gelu_new"` are supported.
+ attention_bias (`bool`, *optional*, defaults to `False`):
+ Whether to add a bias to the queries, keys and values.
+ attention_dropout (`float`, *optional*, defaults to 0.0):
+ Dropout probability for attention weights.
+ num_heads (``, *optional*, defaults to 12):
+ in_channels (``, *optional*, defaults to 3):
+ image_size (`int` or `list[int]`, *optional*, defaults to 336):
+ The size (resolution) of each image.
+ patch_size (`int`, *optional*, defaults to 14):
+ The size (resolution) of each patch.
+ rms_norm_eps (`float`, *optional*, defaults to 1e-05):
+ The epsilon used by the rms normalization layers.
+ spatial_merge_size (`int`, *optional*, defaults to 2):
+ The size used for merging spatial dimensions.
+ temporal_patch_size (`int`, *optional*, defaults to 2):
+ The size used for patches along the temporal dimension.
+ out_hidden_size (`int`, *optional*, defaults to 4096):
+ The output hidden size of the vision model.
+ intermediate_size (`int`, *optional*, defaults to 13696):
+ Dimensionality of the "intermediate" (i.e., feed-forward) layer in the Transformer encoder.
+ initializer_range (`float`, *optional*, defaults to 0.02):
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
Example:
```python
@@ -80,7 +76,7 @@ class Glm4vMoeVisionConfig(PreTrainedConfig):
>>> configuration = model.config
```"""
- model_type = "glm4v_moe"
+ model_type = "glm4v_moe_vision"
base_config_key = "vision_config"
def __init__(
@@ -206,7 +202,7 @@ class Glm4vMoeTextConfig(PreTrainedConfig):
>>> configuration = model.config
```"""
- model_type = "Glm4vMoe_text"
+ model_type = "glm4v_moe_text"
keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Glm4vMoe`
base_model_tp_plan = {
@@ -374,4 +370,4 @@ def __init__(
super().__init__(**kwargs)
-__all__ = ["Glm4vMoeConfig", "Glm4vMoeTextConfig"]
+__all__ = ["Glm4vMoeConfig", "Glm4vMoeTextConfig", "Glm4vMoeVisionConfig"]
diff --git a/src/transformers/models/glm4v_moe/convert_glm4v_moe_mgt_weights_to_hf.py b/src/transformers/models/glm4v_moe/convert_glm4v_moe_mgt_weights_to_hf.py
index 1949966d2738..d8b08716b6c4 100644
--- a/src/transformers/models/glm4v_moe/convert_glm4v_moe_mgt_weights_to_hf.py
+++ b/src/transformers/models/glm4v_moe/convert_glm4v_moe_mgt_weights_to_hf.py
@@ -713,7 +713,7 @@ def offset_layer(x, offset=llm_layer_offset):
if "vision_config" in model_config:
vision_config = {
- "model_type": "glm4v_moe",
+ "model_type": "glm4v_moe_vision",
"hidden_size": model_config["vision_config"].get("hidden_size", 1536),
"depth": model_config["vision_config"].get("num_layers", 24),
"num_heads": model_config["vision_config"].get("num_attention_heads", 12),
diff --git a/src/transformers/models/glm4v_moe/modeling_glm4v_moe.py b/src/transformers/models/glm4v_moe/modeling_glm4v_moe.py
index 692ccd2ea9fa..631505562bc6 100644
--- a/src/transformers/models/glm4v_moe/modeling_glm4v_moe.py
+++ b/src/transformers/models/glm4v_moe/modeling_glm4v_moe.py
@@ -591,115 +591,6 @@ class Glm4vMoeCausalLMOutputWithPast(ModelOutput):
aux_loss: Optional[torch.FloatTensor] = None
-@auto_docstring
-class Glm4vMoeTextModel(Glm4vMoePreTrainedModel):
- config: Glm4vMoeTextConfig
- input_modalities = "text"
-
- def __init__(self, config: Glm4vMoeTextConfig):
- super().__init__(config)
- self.padding_idx = config.pad_token_id
- self.vocab_size = config.vocab_size
-
- self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
- self.layers = nn.ModuleList(
- [Glm4vMoeTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
- )
- self.norm = Glm4vMoeRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
- self.rotary_emb = Glm4vMoeTextRotaryEmbedding(config=config)
-
- self.gradient_checkpointing = False
- # Initialize weights and apply final processing
- self.post_init()
-
- @auto_docstring
- @check_model_inputs()
- def forward(
- self,
- input_ids: Optional[torch.LongTensor] = None,
- attention_mask: Optional[torch.Tensor] = None,
- position_ids: Optional[torch.LongTensor] = None,
- past_key_values: Optional[Cache] = None,
- inputs_embeds: Optional[torch.FloatTensor] = None,
- use_cache: Optional[bool] = None,
- cache_position: Optional[torch.LongTensor] = None,
- **kwargs: Unpack[FlashAttentionKwargs],
- ) -> MoeModelOutputWithPast:
- if (input_ids is None) ^ (inputs_embeds is not None):
- raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
-
- # torch.jit.trace() doesn't support cache objects in the output
- if use_cache and past_key_values is None and not torch.jit.is_tracing():
- past_key_values = DynamicCache(config=self.config)
-
- if inputs_embeds is None:
- inputs_embeds = self.embed_tokens(input_ids)
-
- if cache_position is None:
- past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
- cache_position = torch.arange(
- past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
- )
-
- # the hard coded `3` is for temporal, height and width.
- if position_ids is None:
- position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1)
- elif position_ids.ndim == 2:
- position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
-
- # NOTE: we need to pass text position ids for packing. Qwen2-VL uses 3D positions
- # where each dim indicates visual spatial positions for temporal/height/width grids.
- # There are two scenarios when FA2-like packed masking might be activated.
- # 1. User specifically passed packed `position_ids` and no attention mask.
- # In this case we expect the useer to create correct position ids for all 3 grids
- # and prepend text-only position ids to it. The final tensor will be [4, bs, seq-len]
- # 2. User runs forward with no attention mask and no position ids. In this case, position ids
- # are prepared by the model (`get_rope_index`) as `[4, bs, seq-len]` tensor. Text-only positions are
- # prepended by us when creating positions so that the mask is constructed correctly. NOTE: failing to pass
- # text-only positions will cause incorrect mask construction, do not change `prepare_input_for_generation`
- if position_ids.ndim == 3 and position_ids.shape[0] == 4:
- text_position_ids = position_ids[0]
- position_ids = position_ids[1:]
- else:
- # If inputs are not packed (usual 3D positions), do not prepare mask from position_ids
- text_position_ids = None
-
- mask_kwargs = {
- "config": self.config,
- "input_embeds": inputs_embeds,
- "attention_mask": attention_mask,
- "cache_position": cache_position,
- "past_key_values": past_key_values,
- "position_ids": text_position_ids,
- }
- # Create the masks
- causal_mask = create_causal_mask(**mask_kwargs)
-
- hidden_states = inputs_embeds
-
- # create position embeddings to be shared across the decoder layers
- position_embeddings = self.rotary_emb(hidden_states, position_ids)
-
- for i, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]):
- layer_outputs = decoder_layer(
- hidden_states,
- position_embeddings=position_embeddings,
- attention_mask=causal_mask,
- position_ids=position_ids,
- past_key_values=past_key_values,
- cache_position=cache_position,
- **kwargs,
- )
- hidden_states = layer_outputs
-
- hidden_states = self.norm(hidden_states)
-
- return MoeModelOutputWithPast(
- last_hidden_state=hidden_states,
- past_key_values=past_key_values,
- )
-
-
class Glm4vMoeisionMlp(nn.Module):
def __init__(self, config, bias: bool = False):
super().__init__()
@@ -976,6 +867,7 @@ def forward(
return hidden_states
+@auto_docstring
class Glm4vMoeVisionModel(Glm4vMoePreTrainedModel):
config: Glm4vMoeVisionConfig
input_modalities = ["image", "video"]
@@ -1087,6 +979,115 @@ def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor) -> torch.
return hidden_states
+@auto_docstring
+class Glm4vMoeTextModel(Glm4vMoePreTrainedModel):
+ config: Glm4vMoeTextConfig
+ input_modalities = "text"
+
+ def __init__(self, config: Glm4vMoeTextConfig):
+ super().__init__(config)
+ self.padding_idx = config.pad_token_id
+ self.vocab_size = config.vocab_size
+
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
+ self.layers = nn.ModuleList(
+ [Glm4vMoeTextDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
+ )
+ self.norm = Glm4vMoeRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
+ self.rotary_emb = Glm4vMoeTextRotaryEmbedding(config=config)
+
+ self.gradient_checkpointing = False
+ # Initialize weights and apply final processing
+ self.post_init()
+
+ @auto_docstring
+ @check_model_inputs()
+ def forward(
+ self,
+ input_ids: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ past_key_values: Optional[Cache] = None,
+ inputs_embeds: Optional[torch.FloatTensor] = None,
+ use_cache: Optional[bool] = None,
+ cache_position: Optional[torch.LongTensor] = None,
+ **kwargs: Unpack[FlashAttentionKwargs],
+ ) -> MoeModelOutputWithPast:
+ if (input_ids is None) ^ (inputs_embeds is not None):
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
+
+ # torch.jit.trace() doesn't support cache objects in the output
+ if use_cache and past_key_values is None and not torch.jit.is_tracing():
+ past_key_values = DynamicCache(config=self.config)
+
+ if inputs_embeds is None:
+ inputs_embeds = self.embed_tokens(input_ids)
+
+ if cache_position is None:
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
+ cache_position = torch.arange(
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
+ )
+
+ # the hard coded `3` is for temporal, height and width.
+ if position_ids is None:
+ position_ids = cache_position.view(1, 1, -1).expand(3, inputs_embeds.shape[0], -1)
+ elif position_ids.ndim == 2:
+ position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
+
+ # NOTE: we need to pass text position ids for packing. Qwen2-VL uses 3D positions
+ # where each dim indicates visual spatial positions for temporal/height/width grids.
+ # There are two scenarios when FA2-like packed masking might be activated.
+ # 1. User specifically passed packed `position_ids` and no attention mask.
+ # In this case we expect the useer to create correct position ids for all 3 grids
+ # and prepend text-only position ids to it. The final tensor will be [4, bs, seq-len]
+ # 2. User runs forward with no attention mask and no position ids. In this case, position ids
+ # are prepared by the model (`get_rope_index`) as `[4, bs, seq-len]` tensor. Text-only positions are
+ # prepended by us when creating positions so that the mask is constructed correctly. NOTE: failing to pass
+ # text-only positions will cause incorrect mask construction, do not change `prepare_input_for_generation`
+ if position_ids.ndim == 3 and position_ids.shape[0] == 4:
+ text_position_ids = position_ids[0]
+ position_ids = position_ids[1:]
+ else:
+ # If inputs are not packed (usual 3D positions), do not prepare mask from position_ids
+ text_position_ids = None
+
+ mask_kwargs = {
+ "config": self.config,
+ "input_embeds": inputs_embeds,
+ "attention_mask": attention_mask,
+ "cache_position": cache_position,
+ "past_key_values": past_key_values,
+ "position_ids": text_position_ids,
+ }
+ # Create the masks
+ causal_mask = create_causal_mask(**mask_kwargs)
+
+ hidden_states = inputs_embeds
+
+ # create position embeddings to be shared across the decoder layers
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
+
+ for i, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]):
+ layer_outputs = decoder_layer(
+ hidden_states,
+ position_embeddings=position_embeddings,
+ attention_mask=causal_mask,
+ position_ids=position_ids,
+ past_key_values=past_key_values,
+ cache_position=cache_position,
+ **kwargs,
+ )
+ hidden_states = layer_outputs
+
+ hidden_states = self.norm(hidden_states)
+
+ return MoeModelOutputWithPast(
+ last_hidden_state=hidden_states,
+ past_key_values=past_key_values,
+ )
+
+
@auto_docstring
class Glm4vMoeModel(Glm4vMoePreTrainedModel):
base_model_prefix = ""
@@ -1909,4 +1910,10 @@ def _expand_dict_for_generation(dict_to_expand):
return input_ids, model_kwargs
-__all__ = ["Glm4vMoeForConditionalGeneration", "Glm4vMoeModel", "Glm4vMoePreTrainedModel", "Glm4vMoeTextModel"]
+__all__ = [
+ "Glm4vMoeForConditionalGeneration",
+ "Glm4vMoeModel",
+ "Glm4vMoePreTrainedModel",
+ "Glm4vMoeTextModel",
+ "Glm4vMoeVisionModel",
+]
diff --git a/src/transformers/models/glm4v_moe/modular_glm4v_moe.py b/src/transformers/models/glm4v_moe/modular_glm4v_moe.py
index 5ed96e5fef50..c69ca8439315 100644
--- a/src/transformers/models/glm4v_moe/modular_glm4v_moe.py
+++ b/src/transformers/models/glm4v_moe/modular_glm4v_moe.py
@@ -45,6 +45,7 @@
Glm4vForConditionalGeneration,
Glm4vTextModel,
Glm4vTextRotaryEmbedding,
+ Glm4vVisionModel,
rotate_half,
)
from ..qwen3_vl_moe.modeling_qwen3_vl_moe import (
@@ -150,7 +151,7 @@ class Glm4vMoeTextConfig(Glm4MoeConfig):
>>> configuration = model.config
```"""
- model_type = "Glm4vMoe_text"
+ model_type = "glm4v_moe_text"
base_config_key = "text_config"
keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Glm4vMoe`
@@ -490,6 +491,11 @@ class Glm4vMoeCausalLMOutputWithPast(Qwen3VLMoeCausalLMOutputWithPast):
pass
+@auto_docstring
+class Glm4vMoeVisionModel(Glm4vVisionModel):
+ pass
+
+
@auto_docstring
class Glm4vMoeTextModel(Glm4vTextModel):
def forward(
@@ -647,8 +653,10 @@ def forward(
__all__ = [
"Glm4vMoeConfig",
"Glm4vMoeTextConfig",
+ "Glm4vMoeVisionConfig",
"Glm4vMoeForConditionalGeneration",
"Glm4vMoeModel", # noqa: F822
"Glm4vMoePreTrainedModel",
- "Glm4vMoeTextModel", # noqa: F822
+ "Glm4vMoeTextModel",
+ "Glm4vMoeVisionModel",
]
diff --git a/tests/models/glm46v/__init__.py b/tests/models/glm46v/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tests/models/glm46v/test_modeling_glm46v.py b/tests/models/glm46v/test_modeling_glm46v.py
new file mode 100644
index 000000000000..6034a3dbde3d
--- /dev/null
+++ b/tests/models/glm46v/test_modeling_glm46v.py
@@ -0,0 +1,585 @@
+# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Testing suite for the PyTorch GLM-4.6V model."""
+
+import copy
+import unittest
+
+from transformers import (
+ AutoProcessor,
+ Glm46VConfig,
+ Glm46VForConditionalGeneration,
+ Glm46VModel,
+ is_torch_available,
+)
+from transformers.testing_utils import (
+ Expectations,
+ cleanup,
+ require_deterministic_for_xpu,
+ require_flash_attn,
+ require_torch,
+ require_torch_gpu,
+ slow,
+ torch_device,
+)
+
+from ...generation.test_utils import GenerationTesterMixin
+from ...test_configuration_common import ConfigTester
+from ...test_modeling_common import (
+ ModelTesterMixin,
+ floats_tensor,
+ ids_tensor,
+)
+
+
+if is_torch_available():
+ import torch
+
+
+class Glm46VVisionText2TextModelTester:
+ def __init__(
+ self,
+ parent,
+ batch_size=3,
+ seq_length=7,
+ num_channels=3,
+ ignore_index=-100,
+ image_size=112,
+ video_start_token_id=3,
+ video_end_token_id=4,
+ image_start_token_id=5,
+ image_end_token_id=6,
+ image_token_id=7,
+ video_token_id=8,
+ is_training=True,
+ text_config={
+ "vocab_size": 99,
+ "hidden_size": 16,
+ "intermediate_size": 22,
+ "num_hidden_layers": 2,
+ "num_attention_heads": 2,
+ "num_key_value_heads": 1,
+ "output_channels": 64,
+ "hidden_act": "silu",
+ "max_position_embeddings": 512,
+ "rope_parameters": {"type": "default", "mrope_section": [2, 1, 1]},
+ "rope_theta": 10000,
+ "tie_word_embeddings": True,
+ "bos_token_id": 0,
+ "eos_token_id": 0,
+ "pad_token_id": 0,
+ },
+ vision_config={
+ "depth": 2,
+ "hidden_act": "silu",
+ "hidden_size": 48,
+ "out_hidden_size": 16,
+ "intermediate_size": 22,
+ "patch_size": 14,
+ "spatial_merge_size": 1,
+ "temporal_patch_size": 2,
+ },
+ ):
+ self.parent = parent
+ self.ignore_index = ignore_index
+ self.bos_token_id = text_config["bos_token_id"]
+ self.eos_token_id = text_config["eos_token_id"]
+ self.pad_token_id = text_config["pad_token_id"]
+ self.video_start_token_id = video_start_token_id
+ self.video_end_token_id = video_end_token_id
+ self.image_start_token_id = image_start_token_id
+ self.image_end_token_id = image_end_token_id
+ self.image_token_id = image_token_id
+ self.video_token_id = video_token_id
+ self.text_config = text_config
+ self.vision_config = vision_config
+ self.batch_size = batch_size
+ self.num_channels = num_channels
+ self.image_size = image_size
+ self.is_training = is_training
+ self.hidden_size = text_config["hidden_size"]
+ self.num_hidden_layers = text_config["num_hidden_layers"]
+ self.num_attention_heads = text_config["num_attention_heads"]
+ self.vocab_size = text_config["vocab_size"]
+ self.num_image_tokens = 64
+ self.seq_length = seq_length + self.num_image_tokens
+
+ def get_config(self):
+ return Glm46VConfig(
+ text_config=self.text_config,
+ vision_config=self.vision_config,
+ image_token_id=self.image_token_id,
+ video_token_id=self.video_token_id,
+ video_start_token_id=self.video_start_token_id,
+ video_end_token_id=self.video_end_token_id,
+ image_start_token_id=self.image_start_token_id,
+ image_end_token_id=self.image_end_token_id,
+ )
+
+ def prepare_config_and_inputs(self):
+ config = self.get_config()
+ patch_size = config.vision_config.patch_size
+ temporal_patch_size = config.vision_config.temporal_patch_size
+ pixel_values = floats_tensor(
+ [
+ self.batch_size * (self.image_size**2) // (patch_size**2),
+ self.num_channels * (patch_size**2) * temporal_patch_size,
+ ]
+ )
+
+ return config, pixel_values
+
+ def prepare_config_and_inputs_for_common(self):
+ config_and_inputs = self.prepare_config_and_inputs()
+ config, pixel_values = config_and_inputs
+ input_ids = ids_tensor([self.batch_size, self.seq_length], self.vocab_size)
+ attention_mask = torch.ones(input_ids.shape, dtype=torch.long, device=torch_device)
+
+ input_ids[input_ids == self.video_token_id] = self.pad_token_id
+ input_ids[input_ids == self.image_token_id] = self.pad_token_id
+ input_ids[input_ids == self.video_start_token_id] = self.pad_token_id
+ input_ids[input_ids == self.image_start_token_id] = self.pad_token_id
+ input_ids[input_ids == self.video_end_token_id] = self.pad_token_id
+ input_ids[input_ids == self.image_end_token_id] = self.pad_token_id
+
+ input_ids[:, 0] = self.image_start_token_id
+ input_ids[:, 1 : 1 + self.num_image_tokens] = self.image_token_id
+ input_ids[:, 1 + self.num_image_tokens] = self.image_end_token_id
+ patch_size = config.vision_config.patch_size
+ patches_per_side = self.image_size // patch_size
+
+ inputs_dict = {
+ "pixel_values": pixel_values,
+ "image_grid_thw": torch.tensor(
+ [[1, patches_per_side, patches_per_side]] * self.batch_size, device=torch_device
+ ),
+ "input_ids": input_ids,
+ "attention_mask": attention_mask,
+ }
+ return config, inputs_dict
+
+
+@require_torch
+class Glm46VModelTest(ModelTesterMixin, GenerationTesterMixin, unittest.TestCase):
+ all_model_classes = (Glm46VModel, Glm46VForConditionalGeneration) if is_torch_available() else ()
+
+ model_split_percents = [0.7, 0.9] # model too big to split at 0.5
+ _is_composite = True
+
+ def setUp(self):
+ self.model_tester = Glm46VVisionText2TextModelTester(self)
+ self.config_tester = ConfigTester(self, config_class=Glm46VConfig, has_text_modality=False)
+
+ def test_config(self):
+ self.config_tester.run_common_tests()
+
+ # GLM4V has images shaped as (bs*patch_len, dim) so we can't slice to batches in generate
+ def prepare_config_and_inputs_for_generate(self, batch_size=2):
+ config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
+
+ # We don't want a few model inputs in our model input dictionary for generation tests
+ input_keys_to_ignore = [
+ # we don't want to mask attention heads
+ # we don't want encoder-decoder models to start from filled decoder ids
+ "decoder_input_ids",
+ "decoder_attention_mask",
+ # we'll set cache use in each test differently
+ "use_cache",
+ # Ignore labels if it is in the input dict
+ "labels",
+ # model-specific exceptions should overload/overwrite this function
+ ]
+
+ # The diff from the general `prepare_config_and_inputs_for_generate` lies here
+ patch_size = config.vision_config.patch_size
+ filtered_image_length = batch_size * (self.model_tester.image_size**2) // (patch_size**2)
+ filtered_inputs_dict = {
+ k: v[:batch_size, ...] if isinstance(v, torch.Tensor) else v
+ for k, v in inputs_dict.items()
+ if k not in input_keys_to_ignore
+ }
+ filtered_inputs_dict["pixel_values"] = inputs_dict["pixel_values"][:filtered_image_length]
+
+ # It is important set `eos_token_id` to `None` to avoid early stopping (would break for length-based checks)
+ text_gen_config = config.get_text_config(decoder=True)
+ if text_gen_config.eos_token_id is not None and text_gen_config.pad_token_id is None:
+ text_gen_config.pad_token_id = (
+ text_gen_config.eos_token_id
+ if isinstance(text_gen_config.eos_token_id, int)
+ else text_gen_config.eos_token_id[0]
+ )
+ text_gen_config.eos_token_id = None
+ text_gen_config.forced_eos_token_id = None
+
+ return config, filtered_inputs_dict
+
+ @unittest.skip(reason="No available kernels - not supported")
+ def test_sdpa_can_dispatch_on_flash(self):
+ pass
+
+ @unittest.skip(reason="Size mismatch")
+ def test_multi_gpu_data_parallel_forward(self):
+ pass
+
+ @unittest.skip("Error with compilation")
+ def test_generate_from_inputs_embeds_with_static_cache(self):
+ pass
+
+ def test_inputs_embeds(self):
+ config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
+
+ for model_class in self.all_model_classes:
+ model = model_class(config)
+ model.to(torch_device)
+ model.eval()
+
+ inputs = copy.deepcopy(self._prepare_for_class(inputs_dict, model_class))
+
+ input_ids = inputs["input_ids"]
+ del inputs["input_ids"]
+ del inputs["pixel_values"]
+ del inputs["image_grid_thw"]
+
+ wte = model.get_input_embeddings()
+ inputs["inputs_embeds"] = wte(input_ids)
+ with torch.no_grad():
+ model(**inputs)[0]
+
+ def test_inputs_embeds_matches_input_ids(self):
+ config, inputs_dict = self.model_tester.prepare_config_and_inputs_for_common()
+
+ for model_class in self.all_model_classes:
+ model = model_class(config)
+ model.to(torch_device)
+ model.eval()
+
+ inputs = self._prepare_for_class(inputs_dict, model_class)
+ input_ids = inputs["input_ids"]
+ del inputs["input_ids"]
+ del inputs["pixel_values"]
+ del inputs["image_grid_thw"]
+
+ inputs_embeds = model.get_input_embeddings()(input_ids)
+
+ with torch.no_grad():
+ out_ids = model(input_ids=input_ids, **inputs)[0]
+ out_embeds = model(inputs_embeds=inputs_embeds, **inputs)[0]
+ torch.testing.assert_close(out_embeds, out_ids)
+
+
+@require_torch
+class Glm46VIntegrationTest(unittest.TestCase):
+ def setUp(self):
+ cleanup(torch_device, gc_collect=True)
+
+ self.processor = AutoProcessor.from_pretrained("THUDM/GLM-4.1V-9B-Thinking")
+ self.message = [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image",
+ "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg",
+ },
+ {"type": "text", "text": "What kind of dog is this?"},
+ ],
+ }
+ ]
+ self.message2 = [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image",
+ "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/coco_sample.png",
+ },
+ {"type": "text", "text": "What kind of dog is this?"},
+ ],
+ }
+ ]
+
+ def tearDown(self):
+ cleanup(torch_device, gc_collect=True)
+
+ @slow
+ def test_small_model_integration_test(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype="auto", device_map="auto"
+ )
+
+ inputs = self.processor.apply_chat_template(
+ self.message, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt"
+ )
+ expected_input_ids = [151331, 151333, 151336, 198, 151339, 151343, 151343, 151343, 151343, 151343, 151343, 151343, 151343, 151343, 151343, 151343, 151343] # fmt: skip
+ assert expected_input_ids == inputs.input_ids[0].tolist()[:17]
+
+ expected_pixel_slice = torch.tensor(
+ [
+ [-0.0988, -0.0842, -0.0842],
+ [-0.5660, -0.5514, -0.4200],
+ [-0.0259, -0.0259, -0.0259],
+ [-0.1280, -0.0988, -0.2010],
+ [-0.4638, -0.5806, -0.6974],
+ [-1.2083, -1.2229, -1.2083],
+ ],
+ dtype=torch.float32,
+ device="cpu",
+ )
+ assert torch.allclose(expected_pixel_slice, inputs.pixel_values[:6, :3], atol=3e-3)
+
+ # verify generation
+ inputs = inputs.to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ output = model.generate(**inputs, max_new_tokens=30)
+ EXPECTED_DECODED_TEXT = "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically"
+ self.assertEqual(
+ self.processor.decode(output[0], skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ def test_small_model_integration_test_batch(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype="auto", device_map="auto"
+ )
+ batch_messages = [self.message] * 2
+ inputs = self.processor.apply_chat_template(
+ batch_messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt"
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ # it should not matter whether two images are the same size or not
+ output = model.generate(**inputs, max_new_tokens=30)
+
+ EXPECTED_DECODED_TEXT = [
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically",
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture has a stocky body, thick fur, and a face that's"
+ ] # fmt: skip
+ self.assertEqual(
+ self.processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ def test_small_model_integration_test_with_video(self):
+ processor = AutoProcessor.from_pretrained("THUDM/GLM-4.1V-9B-Thinking", max_image_size={"longest_edge": 50176})
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype=torch.float16, device_map="auto"
+ )
+ questions = ["Describe this video."]
+ video_urls = ["https://huggingface.co/datasets/hf-internal-testing/fixtures_videos/resolve/main/tennis.mp4"]
+ messages = [
+ [
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "video",
+ "video": video_url,
+ },
+ {"type": "text", "text": question},
+ ],
+ }
+ ]
+ for question, video_url in zip(questions, video_urls)
+ ]
+ inputs = processor.apply_chat_template(
+ messages, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt", padding=True
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ output = model.generate(**inputs, max_new_tokens=30)
+ EXPECTED_DECODED_TEXT = ["\n012345Describe this video.\nGot it, let's analyze the video. First, the scene is an indoor tennis court. There are two players: one in a white shirt"] # fmt: skip
+
+ self.assertEqual(
+ processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ @require_deterministic_for_xpu
+ def test_small_model_integration_test_expand(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype="auto", device_map="auto"
+ )
+ inputs = self.processor.apply_chat_template(
+ self.message, tokenize=True, add_generation_prompt=True, return_dict=True, return_tensors="pt"
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ output = model.generate(**inputs, max_new_tokens=30, do_sample=False, num_beams=2, num_return_sequences=2)
+
+ # fmt: off
+ EXPECTED_DECODED_TEXTS = Expectations(
+ {
+
+ (None, None): ["\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically",
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat, specifically"
+ ],
+ ("xpu", None): ["\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture is not a dog; it's a cat. Specifically, it looks",
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture is not a dog; it's a cat, specifically a Pallas"
+ ],
+ }
+ )
+ # fmt: on
+ EXPECTED_DECODED_TEXT = EXPECTED_DECODED_TEXTS.get_expectation()
+
+ decoded_text = self.processor.batch_decode(output, skip_special_tokens=True)
+ self.assertEqual(decoded_text, EXPECTED_DECODED_TEXT)
+
+ @slow
+ def test_small_model_integration_test_batch_wo_image(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype="auto", device_map="auto"
+ )
+ message_wo_image = [
+ {"role": "user", "content": [{"type": "text", "text": "Who are you?"}]},
+ ]
+ batched_messages = [self.message, message_wo_image]
+ inputs = self.processor.apply_chat_template(
+ batched_messages,
+ tokenize=True,
+ add_generation_prompt=True,
+ return_dict=True,
+ return_tensors="pt",
+ padding=True,
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ # it should not matter whether two images are the same size or not
+ output = model.generate(**inputs, max_new_tokens=30)
+
+ EXPECTED_DECODED_TEXT = [
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically",
+ "\nWho are you?\nGot it, let's look at the user's question: \"Who are you?\" This is a common question when someone is just starting a conversation"
+ ] # fmt: skip
+ self.assertEqual(
+ self.processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ def test_small_model_integration_test_batch_different_resolutions(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", dtype="auto", device_map="auto"
+ )
+ batched_messages = [self.message, self.message2]
+ inputs = self.processor.apply_chat_template(
+ batched_messages,
+ tokenize=True,
+ add_generation_prompt=True,
+ return_dict=True,
+ return_tensors="pt",
+ padding=True,
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ # it should not matter whether two images are the same size or not
+ output = model.generate(**inputs, max_new_tokens=30)
+
+ EXPECTED_DECODED_TEXT = [
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically",
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. Wait, the animals here are cats, not dogs. The question is about a dog, but",
+ ] # fmt: skip
+ self.assertEqual(
+ self.processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ @require_flash_attn
+ @require_torch_gpu
+ def test_small_model_integration_test_batch_flashatt2(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking",
+ dtype=torch.bfloat16,
+ attn_implementation="flash_attention_2",
+ device_map="auto",
+ )
+ batched_messages = [self.message, self.message2]
+ inputs = self.processor.apply_chat_template(
+ batched_messages,
+ tokenize=True,
+ add_generation_prompt=True,
+ return_dict=True,
+ return_tensors="pt",
+ padding=True,
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ # it should not matter whether two images are the same size or not
+ output = model.generate(**inputs, max_new_tokens=30)
+
+ EXPECTED_DECODED_TEXT = [
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog. Wait, it's a cat,",
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. Wait, the animals here are cats, not dogs. The question is about a dog, but"
+ ] # fmt: skip
+ self.assertEqual(
+ self.processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
+
+ @slow
+ @require_flash_attn
+ @require_torch_gpu
+ def test_small_model_integration_test_batch_wo_image_flashatt2(self):
+ model = Glm46VForConditionalGeneration.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking",
+ dtype=torch.bfloat16,
+ attn_implementation="flash_attention_2",
+ device_map="auto",
+ )
+ message_wo_image = [
+ {"role": "user", "content": [{"type": "text", "text": "Who are you?"}]},
+ ]
+ batched_messages = [self.message, message_wo_image]
+ inputs = self.processor.apply_chat_template(
+ batched_messages,
+ tokenize=True,
+ add_generation_prompt=True,
+ return_dict=True,
+ return_tensors="pt",
+ padding=True,
+ ).to(torch_device)
+
+ # This model on the hub has `do_sample=True`.
+ torch.manual_seed(42)
+
+ # it should not matter whether two images are the same size or not
+ output = model.generate(**inputs, max_new_tokens=30)
+
+ EXPECTED_DECODED_TEXT = [
+ "\nWhat kind of dog is this?\nGot it, let's look at the image. The animal in the picture doesn't look like a dog; it's actually a cat. Specifically",
+ "\nWho are you?\nGot it, let's look at the user's question: \"Who are you?\" This is a common question when someone is just starting a conversation"
+ ] # fmt: skip
+
+ self.assertEqual(
+ self.processor.batch_decode(output, skip_special_tokens=True),
+ EXPECTED_DECODED_TEXT,
+ )
diff --git a/tests/models/glm46v/test_processor_glm46v.py b/tests/models/glm46v/test_processor_glm46v.py
new file mode 100644
index 000000000000..4e9fb4449d67
--- /dev/null
+++ b/tests/models/glm46v/test_processor_glm46v.py
@@ -0,0 +1,279 @@
+# Copyright 2025 The HuggingFace Team. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import inspect
+import shutil
+import tempfile
+import unittest
+
+import numpy as np
+
+from transformers import AutoProcessor
+from transformers.testing_utils import require_av, require_torch, require_vision
+from transformers.utils import is_torch_available, is_vision_available
+
+from ...test_processing_common import ProcessorTesterMixin, url_to_local_path
+
+
+if is_vision_available():
+ from transformers import Glm46VProcessor
+
+if is_torch_available():
+ import torch
+
+
+@require_vision
+@require_torch
+class Glm46VProcessorTest(ProcessorTesterMixin, unittest.TestCase):
+ processor_class = Glm46VProcessor
+
+ @classmethod
+ def setUpClass(cls):
+ cls.tmpdirname = tempfile.mkdtemp()
+ processor = Glm46VProcessor.from_pretrained(
+ "THUDM/GLM-4.1V-9B-Thinking", patch_size=4, size={"shortest_edge": 12 * 12, "longest_edge": 18 * 18}
+ )
+ processor.save_pretrained(cls.tmpdirname)
+ cls.image_token = processor.image_token
+
+ def get_tokenizer(self, **kwargs):
+ return AutoProcessor.from_pretrained(self.tmpdirname, **kwargs).tokenizer
+
+ def get_image_processor(self, **kwargs):
+ return AutoProcessor.from_pretrained(self.tmpdirname, **kwargs).image_processor
+
+ def get_video_processor(self, **kwargs):
+ return AutoProcessor.from_pretrained(self.tmpdirname, **kwargs).video_processor
+
+ def get_processor(self, **kwargs):
+ return AutoProcessor.from_pretrained(self.tmpdirname, **kwargs)
+
+ @classmethod
+ def tearDownClass(cls):
+ shutil.rmtree(cls.tmpdirname, ignore_errors=True)
+
+ @require_torch
+ @require_av
+ def _test_apply_chat_template(
+ self,
+ modality: str,
+ batch_size: int,
+ return_tensors: str,
+ input_name: str,
+ processor_name: str,
+ input_data: list[str],
+ ):
+ processor = self.get_processor()
+ if processor.chat_template is None:
+ self.skipTest("Processor has no chat template")
+
+ if processor_name not in self.processor_class.get_attributes():
+ self.skipTest(f"{processor_name} attribute not present in {self.processor_class}")
+
+ batch_messages = [
+ [
+ {
+ "role": "user",
+ "content": [{"type": "text", "text": "Describe this."}],
+ },
+ ]
+ ] * batch_size
+
+ # Test that jinja can be applied
+ formatted_prompt = processor.apply_chat_template(batch_messages, add_generation_prompt=True, tokenize=False)
+ self.assertEqual(len(formatted_prompt), batch_size)
+
+ # Test that tokenizing with template and directly with `self.tokenizer` gives same output
+ formatted_prompt_tokenized = processor.apply_chat_template(
+ batch_messages, add_generation_prompt=True, tokenize=True, return_tensors=return_tensors
+ )
+ add_special_tokens = True
+ if processor.tokenizer.bos_token is not None and formatted_prompt[0].startswith(processor.tokenizer.bos_token):
+ add_special_tokens = False
+ tok_output = processor.tokenizer(
+ formatted_prompt, return_tensors=return_tensors, add_special_tokens=add_special_tokens
+ )
+ expected_output = tok_output.input_ids
+ self.assertListEqual(expected_output.tolist(), formatted_prompt_tokenized.tolist())
+
+ # Test that kwargs passed to processor's `__call__` are actually used
+ tokenized_prompt_100 = processor.apply_chat_template(
+ batch_messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ padding="max_length",
+ truncation=True,
+ return_tensors=return_tensors,
+ max_length=100,
+ )
+ self.assertEqual(len(tokenized_prompt_100[0]), 100)
+
+ # Test that `return_dict=True` returns text related inputs in the dict
+ out_dict_text = processor.apply_chat_template(
+ batch_messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ return_tensors=return_tensors,
+ )
+ self.assertTrue(all(key in out_dict_text for key in ["input_ids", "attention_mask"]))
+ self.assertEqual(len(out_dict_text["input_ids"]), batch_size)
+ self.assertEqual(len(out_dict_text["attention_mask"]), batch_size)
+
+ # Test that with modality URLs and `return_dict=True`, we get modality inputs in the dict
+ for idx, url in enumerate(input_data[:batch_size]):
+ batch_messages[idx][0]["content"] = [batch_messages[idx][0]["content"][0], {"type": modality, "url": url}]
+
+ out_dict = processor.apply_chat_template(
+ batch_messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ return_tensors=return_tensors,
+ fps=2
+ if isinstance(input_data[0], str)
+ else None, # by default no more than 2 frames per second, otherwise too slow
+ do_sample_frames=bool(isinstance(input_data[0], str)), # don't sample frames if decoded video is used
+ )
+ input_name = getattr(self, input_name)
+ self.assertTrue(input_name in out_dict)
+ self.assertEqual(len(out_dict["input_ids"]), batch_size)
+ self.assertEqual(len(out_dict["attention_mask"]), batch_size)
+
+ if modality == "video":
+ # qwen pixels don't scale with bs same way as other models, calculate expected video token count based on video_grid_thw
+ expected_video_token_count = 0
+ for thw in out_dict["video_grid_thw"]:
+ expected_video_token_count += thw[0] * thw[1] * thw[2]
+ mm_len = expected_video_token_count
+ else:
+ mm_len = batch_size * 4
+ self.assertEqual(len(out_dict[input_name]), mm_len)
+
+ return_tensor_to_type = {"pt": torch.Tensor, "np": np.ndarray, None: list}
+ for k in out_dict:
+ self.assertIsInstance(out_dict[k], return_tensor_to_type[return_tensors])
+
+ @require_av
+ def test_apply_chat_template_video_frame_sampling(self):
+ processor = self.get_processor()
+ if processor.chat_template is None:
+ self.skipTest("Processor has no chat template")
+
+ signature = inspect.signature(processor.__call__)
+ if "videos" not in {*signature.parameters.keys()} or (
+ signature.parameters.get("videos") is not None
+ and signature.parameters["videos"].annotation == inspect._empty
+ ):
+ self.skipTest("Processor doesn't accept videos at input")
+
+ messages = [
+ [
+ {
+ "role": "user",
+ "content": [
+ {"type": "video"},
+ {"type": "text", "text": "What is shown in this video?"},
+ ],
+ },
+ ]
+ ]
+
+ formatted_prompt = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
+ self.assertEqual(len(formatted_prompt), 1)
+
+ formatted_prompt_tokenized = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=True)
+ expected_output = processor.tokenizer(formatted_prompt, return_tensors=None).input_ids
+ self.assertListEqual(expected_output, formatted_prompt_tokenized)
+
+ out_dict = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=True, return_dict=True)
+ self.assertListEqual(list(out_dict.keys()), ["input_ids", "attention_mask"])
+
+ # Add video URL for return dict and load with `num_frames` arg
+ messages[0][0]["content"][0] = {
+ "type": "video",
+ "url": url_to_local_path(
+ "https://huggingface.co/datasets/raushan-testing-hf/videos-test/resolve/main/tiny_video.mp4"
+ ),
+ }
+
+ # Load with `video_fps` arg
+ video_fps = 10
+ out_dict_with_video = processor.apply_chat_template(
+ messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ video_fps=video_fps,
+ )
+ self.assertTrue(self.videos_input_name in out_dict_with_video)
+ self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 8)
+
+ # Load the whole video
+ out_dict_with_video = processor.apply_chat_template(
+ messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ do_sample_frames=False,
+ )
+ self.assertTrue(self.videos_input_name in out_dict_with_video)
+ self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 24)
+
+ # Load video as a list of frames (i.e. images). NOTE: each frame should have same size
+ # because we assume they come from one video
+ messages[0][0]["content"][0] = {
+ "type": "video",
+ "url": [
+ url_to_local_path(
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/australia.jpg"
+ ),
+ url_to_local_path(
+ "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/australia.jpg"
+ ),
+ ],
+ }
+ out_dict_with_video = processor.apply_chat_template(
+ messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ do_sample_frames=False,
+ )
+ self.assertTrue(self.videos_input_name in out_dict_with_video)
+ self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 4)
+
+ # When the inputs are frame URLs/paths we expect that those are already
+ # sampled and will raise an error is asked to sample again.
+ with self.assertRaisesRegex(
+ ValueError, "Sampling frames from a list of images is not supported! Set `do_sample_frames=False`"
+ ):
+ out_dict_with_video = processor.apply_chat_template(
+ messages,
+ add_generation_prompt=True,
+ tokenize=True,
+ return_dict=True,
+ do_sample_frames=True,
+ )
+
+ def test_model_input_names(self):
+ processor = self.get_processor()
+
+ text = self.prepare_text_inputs(modalities=["image", "video"])
+ image_input = self.prepare_image_inputs()
+ video_inputs = self.prepare_video_inputs()
+ inputs_dict = {"text": text, "images": image_input, "videos": video_inputs}
+ inputs = processor(**inputs_dict, return_tensors="pt", do_sample_frames=False)
+
+ self.assertSetEqual(set(inputs.keys()), set(processor.model_input_names))
diff --git a/tests/models/glm46v/test_video_processing_glm46v.py b/tests/models/glm46v/test_video_processing_glm46v.py
new file mode 100644
index 000000000000..fc62901a71be
--- /dev/null
+++ b/tests/models/glm46v/test_video_processing_glm46v.py
@@ -0,0 +1,334 @@
+# coding=utf-8
+# Copyright 2025 HuggingFace Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import numpy as np
+
+from transformers.image_utils import IMAGENET_STANDARD_MEAN, IMAGENET_STANDARD_STD
+from transformers.testing_utils import require_torch, require_vision
+from transformers.utils import is_torch_available, is_torchvision_available, is_vision_available
+
+from ...test_video_processing_common import VideoProcessingTestMixin, prepare_video_inputs
+
+
+if is_torch_available():
+ from PIL import Image
+
+if is_vision_available():
+ if is_torchvision_available():
+ from transformers import Glm46VVideoProcessor
+ from transformers.models.glm46v.video_processing_glm46v import smart_resize
+
+
+class Glm46VVideoProcessingTester:
+ def __init__(
+ self,
+ parent,
+ batch_size=5,
+ num_frames=8,
+ num_channels=3,
+ min_resolution=30,
+ max_resolution=80,
+ temporal_patch_size=2,
+ patch_size=14,
+ merge_size=2,
+ do_resize=True,
+ size=None,
+ do_normalize=True,
+ image_mean=IMAGENET_STANDARD_MEAN,
+ image_std=IMAGENET_STANDARD_STD,
+ do_convert_rgb=True,
+ ):
+ size = size if size is not None else {"longest_edge": 20, "shortest_edge": 10}
+ self.parent = parent
+ self.batch_size = batch_size
+ self.num_frames = num_frames
+ self.num_channels = num_channels
+ self.min_resolution = min_resolution
+ self.max_resolution = max_resolution
+ self.do_resize = do_resize
+ self.size = size
+ self.do_normalize = do_normalize
+ self.image_mean = image_mean
+ self.image_std = image_std
+ self.do_convert_rgb = do_convert_rgb
+ self.temporal_patch_size = temporal_patch_size
+ self.patch_size = patch_size
+ self.merge_size = merge_size
+
+ def prepare_video_processor_dict(self):
+ return {
+ "do_resize": self.do_resize,
+ "size": self.size,
+ "do_normalize": self.do_normalize,
+ "image_mean": self.image_mean,
+ "image_std": self.image_std,
+ "do_convert_rgb": self.do_convert_rgb,
+ "do_sample_frames": True,
+ }
+
+ def prepare_video_metadata(self, videos):
+ video_metadata = []
+ for video in videos:
+ if isinstance(video, list):
+ num_frames = len(video)
+ elif hasattr(video, "shape"):
+ if len(video.shape) == 4: # (T, H, W, C)
+ num_frames = video.shape[0]
+ else:
+ num_frames = 1
+ else:
+ num_frames = self.num_frames
+
+ metadata = {
+ "fps": 2,
+ "duration": num_frames / 2,
+ "total_num_frames": num_frames,
+ }
+ video_metadata.append(metadata)
+ return video_metadata
+
+ def expected_output_video_shape(self, videos):
+ grid_t = self.num_frames // self.temporal_patch_size
+ hidden_dim = self.num_channels * self.temporal_patch_size * self.patch_size * self.patch_size
+ seq_len = 0
+ for video in videos:
+ if isinstance(video, list) and isinstance(video[0], Image.Image):
+ video = np.stack([np.array(frame) for frame in video])
+ elif hasattr(video, "shape"):
+ pass
+ else:
+ video = np.array(video)
+
+ if hasattr(video, "shape") and len(video.shape) >= 3:
+ if len(video.shape) == 4:
+ t, height, width = video.shape[:3]
+ elif len(video.shape) == 3:
+ height, width = video.shape[:2]
+ t = 1
+ else:
+ t, height, width = self.num_frames, self.min_resolution, self.min_resolution
+ else:
+ t, height, width = self.num_frames, self.min_resolution, self.min_resolution
+
+ resized_height, resized_width = smart_resize(
+ t,
+ height,
+ width,
+ factor=self.patch_size * self.merge_size,
+ min_pixels=self.size["shortest_edge"],
+ max_pixels=self.size["longest_edge"],
+ )
+ grid_h, grid_w = resized_height // self.patch_size, resized_width // self.patch_size
+ seq_len += grid_t * grid_h * grid_w
+ return [seq_len, hidden_dim]
+
+ def prepare_video_inputs(self, equal_resolution=False, return_tensors="pil"):
+ videos = prepare_video_inputs(
+ batch_size=self.batch_size,
+ num_frames=self.num_frames,
+ num_channels=self.num_channels,
+ min_resolution=self.min_resolution,
+ max_resolution=self.max_resolution,
+ equal_resolution=equal_resolution,
+ return_tensors=return_tensors,
+ )
+ return videos
+
+
+@require_torch
+@require_vision
+class Glm46VVideoProcessingTest(VideoProcessingTestMixin, unittest.TestCase):
+ fast_video_processing_class = Glm46VVideoProcessor if is_torchvision_available() else None
+ input_name = "pixel_values_videos"
+
+ def setUp(self):
+ super().setUp()
+ self.video_processor_tester = Glm46VVideoProcessingTester(self)
+
+ @property
+ def video_processor_dict(self):
+ return self.video_processor_tester.prepare_video_processor_dict()
+
+ def test_video_processor_from_dict_with_kwargs(self):
+ video_processor = self.fast_video_processing_class.from_dict(self.video_processor_dict)
+ self.assertEqual(video_processor.size, {"longest_edge": 20, "shortest_edge": 10})
+
+ video_processor = self.fast_video_processing_class.from_dict(
+ self.video_processor_dict, size={"longest_edge": 42, "shortest_edge": 42}
+ )
+ self.assertEqual(video_processor.size, {"longest_edge": 42, "shortest_edge": 42})
+
+ def test_call_pil(self):
+ for video_processing_class in self.video_processor_list:
+ video_processing = video_processing_class(**self.video_processor_dict)
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False, return_tensors="pil"
+ )
+
+ for video in video_inputs:
+ self.assertIsInstance(video[0], Image.Image)
+
+ video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
+ encoded_videos = video_processing(
+ video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+ encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
+ self.input_name
+ ]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ def test_call_numpy(self):
+ for video_processing_class in self.video_processor_list:
+ video_processing = video_processing_class(**self.video_processor_dict)
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False, return_tensors="np"
+ )
+
+ video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
+ encoded_videos = video_processing(
+ video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
+ self.input_name
+ ]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ def test_call_pytorch(self):
+ for video_processing_class in self.video_processor_list:
+ video_processing = video_processing_class(**self.video_processor_dict)
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False, return_tensors="pt"
+ )
+ video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
+ encoded_videos = video_processing(
+ video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+ encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
+ self.input_name
+ ]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ @unittest.skip("Skip for now, the test needs adjustment for GLM-4.1V")
+ def test_call_numpy_4_channels(self):
+ for video_processing_class in self.video_processor_list:
+ # Test that can process videos which have an arbitrary number of channels
+ # Initialize video_processing
+ video_processor = video_processing_class(**self.video_processor_dict)
+
+ # create random numpy tensors
+ self.video_processor_tester.num_channels = 4
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False, return_tensors="np"
+ )
+
+ # Test not batched input
+ encoded_videos = video_processor(
+ video_inputs[0],
+ return_tensors="pt",
+ input_data_format="channels_last",
+ image_mean=(0.0, 0.0, 0.0, 0.0),
+ image_std=(1.0, 1.0, 1.0, 1.0),
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ # Test batched
+ encoded_videos = video_processor(
+ video_inputs,
+ return_tensors="pt",
+ input_data_format="channels_last",
+ image_mean=(0.0, 0.0, 0.0, 0.0),
+ image_std=(1.0, 1.0, 1.0, 1.0),
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ def test_nested_input(self):
+ """Tests that the processor can work with nested list where each video is a list of arrays"""
+ for video_processing_class in self.video_processor_list:
+ video_processing = video_processing_class(**self.video_processor_dict)
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False, return_tensors="np"
+ )
+
+ video_inputs_nested = [list(video) for video in video_inputs]
+ video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
+
+ # Test not batched input
+ encoded_videos = video_processing(
+ video_inputs_nested[0], video_metadata=[video_metadata[0]], return_tensors="pt"
+ )[self.input_name]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ # Test batched
+ encoded_videos = video_processing(video_inputs_nested, video_metadata=video_metadata, return_tensors="pt")[
+ self.input_name
+ ]
+ expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
+ self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
+
+ def test_call_sample_frames(self):
+ for video_processing_class in self.video_processor_list:
+ video_processor_dict = self.video_processor_dict.copy()
+ video_processing = video_processing_class(**video_processor_dict)
+
+ prev_num_frames = self.video_processor_tester.num_frames
+ self.video_processor_tester.num_frames = 8
+ prev_min_resolution = getattr(self.video_processor_tester, "min_resolution", None)
+ prev_max_resolution = getattr(self.video_processor_tester, "max_resolution", None)
+ self.video_processor_tester.min_resolution = 56
+ self.video_processor_tester.max_resolution = 112
+
+ video_inputs = self.video_processor_tester.prepare_video_inputs(
+ equal_resolution=False,
+ return_tensors="torch",
+ )
+
+ metadata = [[{"total_num_frames": 8, "fps": 4}]]
+ batched_metadata = metadata * len(video_inputs)
+
+ encoded_videos = video_processing(video_inputs[0], return_tensors="pt", video_metadata=metadata)[
+ self.input_name
+ ]
+ encoded_videos_batched = video_processing(
+ video_inputs, return_tensors="pt", video_metadata=batched_metadata
+ )[self.input_name]
+
+ self.assertIsNotNone(encoded_videos)
+ self.assertIsNotNone(encoded_videos_batched)
+ self.assertEqual(len(encoded_videos.shape), 2)
+ self.assertEqual(len(encoded_videos_batched.shape), 2)
+
+ with self.assertRaises(ValueError):
+ video_processing(video_inputs[0], return_tensors="pt")[self.input_name]
+
+ self.video_processor_tester.num_frames = prev_num_frames
+ if prev_min_resolution is not None:
+ self.video_processor_tester.min_resolution = prev_min_resolution
+ if prev_max_resolution is not None:
+ self.video_processor_tester.max_resolution = prev_max_resolution
diff --git a/tests/models/glm4v_moe/test_modeling_glm4v_moe.py b/tests/models/glm4v_moe/test_modeling_glm4v_moe.py
index f291a868ab17..afae9ef118a8 100644
--- a/tests/models/glm4v_moe/test_modeling_glm4v_moe.py
+++ b/tests/models/glm4v_moe/test_modeling_glm4v_moe.py
@@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Testing suite for the PyTorch GLM-4.1V model."""
+"""Testing suite for the PyTorch GLM-4.5V model."""
import copy
import unittest
diff --git a/utils/check_docstrings.py b/utils/check_docstrings.py
index 79c150de0260..293fb722b705 100644
--- a/utils/check_docstrings.py
+++ b/utils/check_docstrings.py
@@ -235,6 +235,8 @@ class DecoratedItem:
"GPTSanJapaneseConfig",
"GitConfig",
"GitVisionConfig",
+ "Glm4vVisionConfig",
+ "Glm4vMoeVisionConfig",
"GraphormerConfig",
"GroupViTTextConfig",
"GroupViTVisionConfig",
diff --git a/utils/models_to_deprecate.py b/utils/models_to_deprecate.py
index c3c4eaf8e4d8..14565904d7e3 100644
--- a/utils/models_to_deprecate.py
+++ b/utils/models_to_deprecate.py
@@ -92,8 +92,8 @@
"gemma3": ["gemma3_text"],
"gemma3n": ["gemma3n_audio", "gemma3n_text", "gemma3n_vision"],
"gpt2": ["cpm", "dialogpt", "gpt-sw3", "megatron_gpt2"],
- "glm4v_moe": ["glm4v_moe_text"],
- "glm4v": ["glm4v_text"],
+ "glm4v_moe": ["glm4v_moe_text", "glm4v_moe_vision"],
+ "glm4v": ["glm4v_text", "glm4v_vision"],
"idefics3": ["idefics3_vision"],
"internvl": ["internvl_vision"],
"layoutlmv2": ["layoutxlm"],