A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 03:30 UTC
Add GLM4.1V model (Draft) (#19331)
Signed-off-by: zRzRzRzRzRzRzR <2448370773@qq.com> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Yuxuan Zhang committed
Jul 1, 2025 at 20:48 UTC
ed70f3c64f684750edea087e286cbf264e7cc3f3
17 files changed
+1946
-16
docs/models/supported_models.md
+2
-1
@@ -553,6 +553,7 @@ Specified using `--task generate`.
553
| `FuyuForCausalLM` | Fuyu | T + I | `adept/fuyu-8b` etc. | | ✅︎ | ✅︎ |
554
| `Gemma3ForConditionalGeneration` | Gemma 3 | T + I<sup>+</sup> | `google/gemma-3-4b-it`, `google/gemma-3-27b-it`, etc. | ✅︎ | ✅︎ | ⚠️ |
555
| `GLM4VForCausalLM`<sup>^</sup> | GLM-4V | T + I | `THUDM/glm-4v-9b`, `THUDM/cogagent-9b-20241220` etc. | ✅︎ | ✅︎ | ✅︎ |
556
+| `Glm4vForConditionalGeneration` | GLM-4.1V-Thinking | T + I<sup>E+</sup> + V<sup>E+</sup> | `THUDM/GLM-4.1V-9B-Thinkg`, etc. | ✅︎ | ✅︎ | ✅︎ |
557
| `GraniteSpeechForConditionalGeneration` | Granite Speech | T + A | `ibm-granite/granite-speech-3.3-8b` | ✅︎ | ✅︎ | ✅︎ |
558
| `H2OVLChatModel` | H2OVL | T + I<sup>E+</sup> | `h2oai/h2ovl-mississippi-800m`, `h2oai/h2ovl-mississippi-2b`, etc. | | ✅︎ | ✅︎\* |
559
| `Idefics3ForConditionalGeneration` | Idefics3 | T + I | `HuggingFaceM4/Idefics3-8B-Llama3` etc. | ✅︎ | | ✅︎ |
@@ -583,7 +584,7 @@ Specified using `--task generate`.
584
| `SkyworkR1VChatModel` | Skywork-R1V-38B | T + I | `Skywork/Skywork-R1V-38B` | | ✅︎ | ✅︎ |
585
| `SmolVLMForConditionalGeneration` | SmolVLM2 | T + I | `SmolVLM2-2.2B-Instruct` | ✅︎ | | ✅︎ |
586
| `TarsierForConditionalGeneration` | Tarsier | T + I<sup>E+</sup> | `omni-search/Tarsier-7b`,`omni-search/Tarsier-34b` | | ✅︎ | ✅︎ |
586
-| `Tarsier2ForConditionalGeneration`<sup>^</sup> | Tarsier2 | T + I<sup>E+</sup> + V<sup>E+</sup> | `omni-research/Tarsier2-Recap-7b`,`omni-research/Tarsier2-7b-0115` | | ✅︎ | ✅︎ |
587
+| `Tarsier2ForConditionalGeneration`<sup>^</sup> | Tarsier2 | T + I<sup>E+</sup> + V<sup>E+</sup> | `omni-research/Tarsier2-Recap-7b`,`omni-research/Tarsier2-7b-0115` | | ✅︎ | ✅︎ |
588
589
<sup>^</sup> You need to set the architecture name via `--hf-overrides` to match the one in vLLM.
590
• For example, to use DeepSeek-VL2 series models:
examples/offline_inference/vision_language.py
+39
-1
@@ -248,6 +248,42 @@ def run_glm4v(questions: list[str], modality: str) -> ModelRequestData:
248
)
249
250
251
+# GLM-4.1V
252
+def run_glm4_1v(questions: list[str], modality: str) -> ModelRequestData:
253
+ model_name = "THUDM/GLM-4.1V-9B-Thinking"
254
+
255
+ engine_args = EngineArgs(
256
+ model=model_name,
257
+ max_model_len=4096,
258
+ max_num_seqs=2,
259
+ mm_processor_kwargs={
260
+ "size": {"shortest_edge": 12544, "longest_edge": 47040000},
261
+ "fps": 1,
262
+ },
263
+ limit_mm_per_prompt={modality: 1},
264
+ enforce_eager=True,
265
+ )
266
+
267
+ if modality == "image":
268
+ placeholder = "<|begin_of_image|><|image|><|end_of_image|>"
269
+ elif modality == "video":
270
+ placeholder = "<|begin_of_video|><|video|><|end_of_video|>"
271
+
272
+ prompts = [
273
+ (
274
+ "[gMASK]<sop><|system|>\nYou are a helpful assistant.<|user|>\n"
275
+ f"{placeholder}"
276
+ f"{question}<|assistant|>assistant\n"
277
+ )
278
+ for question in questions
279
+ ]
280
+
281
+ return ModelRequestData(
282
+ engine_args=engine_args,
283
+ prompts=prompts,
284
+ )
285
+
286
+
287
# H2OVL-Mississippi
288
def run_h2ovl(questions: list[str], modality: str) -> ModelRequestData:
289
assert modality == "image"
@@ -1114,6 +1150,7 @@ model_example_map = {
1150
"fuyu": run_fuyu,
1151
"gemma3": run_gemma3,
1152
"glm4v": run_glm4v,
1153
+ "glm4_1v": run_glm4_1v,
1154
"h2ovl_chat": run_h2ovl,
1155
"idefics3": run_idefics3,
1156
"internvl_chat": run_internvl,
@@ -1172,10 +1209,11 @@ def get_multi_modal_input(args):
1209
if args.modality == "video":
1210
# Input video and question
1211
video = VideoAsset(name="baby_reading", num_frames=args.num_frames).np_ndarrays
1212
+ metadata = VideoAsset(name="baby_reading", num_frames=args.num_frames).metadata
1213
vid_questions = ["Why is this video funny?"]
1214
1215
return {
1178
- "data": video,
1216
+ "data": [(video, metadata)] if args.model_type == "glm4_1v" else video,
1217
"questions": vid_questions,
1218
}
1219
tests/entrypoints/openai/test_video.py
+1
-1
@@ -50,7 +50,7 @@ async def client(server):
50
@pytest.fixture(scope="session")
51
def base64_encoded_video() -> dict[str, str]:
52
return {
53
- video_url: encode_video_base64(fetch_video(video_url))
53
+ video_url: encode_video_base64(fetch_video(video_url)[0])
54
for video_url in TEST_VIDEO_URLS
55
}
56
tests/models/multimodal/generation/test_common.py
+28
@@ -309,6 +309,34 @@ VLM_TEST_SETTINGS = {
309
num_logprobs=10,
310
marks=[large_gpu_mark(min_gb=32)],
311
),
312
+ "glm4_1v": VLMTestInfo(
313
+ models=["THUDM/GLM-4.1V-9B-Thinking"],
314
+ test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
315
+ prompt_formatter=lambda img_prompt: f"<|user|>\n{img_prompt}<|assistant|>", # noqa: E501
316
+ img_idx_to_prompt=lambda idx: "<|begin_of_image|><|image|><|end_of_image|>", # noqa: E501
317
+ video_idx_to_prompt=lambda idx: "<|begin_of_video|><|video|><|end_of_video|>", # noqa: E501
318
+ max_model_len=2048,
319
+ max_num_seqs=2,
320
+ get_stop_token_ids=lambda tok: [151329, 151336, 151338],
321
+ num_logprobs=10,
322
+ image_size_factors=[(), (0.25,), (0.25, 0.25, 0.25), (0.25, 0.2, 0.15)],
323
+ auto_cls=AutoModelForImageTextToText,
324
+ ),
325
+ "glm4_1v-video": VLMTestInfo(
326
+ models=["THUDM/GLM-4.1V-9B-Thinking"],
327
+ # GLM4.1V require include video metadata for input
328
+ test_type=VLMTestType.CUSTOM_INPUTS,
329
+ max_model_len=4096,
330
+ max_num_seqs=2,
331
+ auto_cls=AutoModelForImageTextToText,
332
+ patch_hf_runner=model_utils.glm4_1v_patch_hf_runner,
333
+ custom_test_opts=[CustomTestOptions(
334
+ inputs=custom_inputs.video_with_metadata_glm4_1v(),
335
+ limit_mm_per_prompt={"video": 1},
336
+ )],
337
+ # This is needed to run on machine with 24GB VRAM
338
+ vllm_runner_kwargs={"gpu_memory_utilization": 0.95},
339
+ ),
340
"h2ovl": VLMTestInfo(
341
models = [
342
"h2oai/h2ovl-mississippi-800m",
tests/models/multimodal/generation/vlm_utils/custom_inputs.py
+20
@@ -129,3 +129,23 @@ def windows_attention_image_qwen2_5_vl():
129
130
wrapped_sf = ImageSizeWrapper(type=SizeType.SIZE_FACTOR, data=[0.5])
131
return build_single_image_inputs([image], [prompt], wrapped_sf)
132
+
133
+
134
+def video_with_metadata_glm4_1v():
135
+ video_array = VIDEO_ASSETS[0].np_ndarrays
136
+ metadata = VIDEO_ASSETS[0].metadata
137
+ question = "Describe the video."
138
+ video_prompt = "<|begin_of_video|><|video|><|end_of_video|>"
139
+ formatted_prompt = f"<|user|>\n{video_prompt}{question}<|assistant|>\n"
140
+
141
+ scales = [0.1, 0.2, 0.25]
142
+ video_input = [[(rescale_video_size(video_array, scale), metadata)]
143
+ for scale in scales]
144
+ prompts = [formatted_prompt] * len(video_input)
145
+
146
+ return [
147
+ PromptWithMultiModalInput(
148
+ prompts=prompts,
149
+ video_data=video_input,
150
+ )
151
+ ]
tests/models/multimodal/generation/vlm_utils/model_utils.py
+24
@@ -16,9 +16,11 @@ import torch
16
from PIL.Image import Image
17
from transformers import (AutoConfig, AutoTokenizer, BatchFeature,
18
GenerationConfig, GenerationMixin)
19
+from transformers.video_utils import VideoMetadata
20
21
from vllm.sequence import SampleLogprobs
22
from vllm.transformers_utils.tokenizer import patch_padding_side
23
+from vllm.utils import is_list_of
24
25
from .....conftest import HfRunner, ImageAsset, ImageTestAssets
26
from .types import RunnerOutput
@@ -373,6 +375,28 @@ def glm4v_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
375
return hf_model
376
377
378
+def glm4_1v_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
379
+ """Patches and returns an instance of the HfRunner to use for GLM4.1V."""
380
+ hf_processor = hf_model.processor
381
+
382
+ def processor(*args, videos=None, **kwargs):
383
+ if videos is not None and is_list_of(videos, tuple):
384
+ # If videos is a list of tuples, we assume each tuple contains
385
+ # (video_array, metadata) as in the case of GLM4.1V.
386
+ video_metadata = [[VideoMetadata(**video[1])] for video in videos]
387
+ videos = [[video[0]] for video in videos]
388
+ else:
389
+ video_metadata = None
390
+
391
+ return hf_processor(*args,
392
+ videos=videos,
393
+ video_metadata=video_metadata,
394
+ **kwargs)
395
+
396
+ hf_model.processor = processor
397
+ return hf_model
398
+
399
+
400
def h2ovl_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
401
"""Patches and returns an instance of the HfRunner to use for H2OVL."""
402
tests/models/multimodal/processing/test_common.py
+24
@@ -24,6 +24,22 @@ from ....multimodal.utils import random_audio, random_image, random_video
24
from ...registry import HF_EXAMPLE_MODELS
25
26
27
+def glm4_1v_patch_mm_data(mm_data: MultiModalDataDict) -> MultiModalDataDict:
28
+ """
29
+ Patch the multimodal data for GLM4.1V model.
30
+ """
31
+ # Ensure video metadata is included
32
+ if "video" in mm_data:
33
+ video = mm_data["video"]
34
+ mm_data["video"] = (video, {
35
+ "total_num_frames": len(video),
36
+ "fps": len(video),
37
+ "duration": 1,
38
+ "video_backend": "opencv"
39
+ })
40
+ return mm_data
41
+
42
+
43
def _test_processing_correctness(
44
model_id: str,
45
hit_rate: float,
@@ -154,6 +170,11 @@ _IGNORE_MM_KEYS = {
170
"ultravox": {"audio_features"},
171
}
172
173
+MM_DATA_PATCHES = {
174
+ # GLM4.1V requires video metadata to be included in the input
175
+ "glm4v": glm4_1v_patch_mm_data,
176
+}
177
+
178
179
def _test_processing_correctness_one(
180
model_config: ModelConfig,
@@ -166,6 +187,8 @@ def _test_processing_correctness_one(
187
):
188
model_type = model_config.hf_config.model_type
189
ignore_mm_keys = _IGNORE_MM_KEYS.get(model_type, set[str]())
190
+ if model_type in MM_DATA_PATCHES:
191
+ mm_data = MM_DATA_PATCHES[model_type](mm_data)
192
193
if isinstance(prompt, str):
194
text_prompt = prompt
@@ -245,6 +268,7 @@ def _test_processing_correctness_one(
268
"adept/fuyu-8b",
269
"google/gemma-3-4b-it",
270
"THUDM/glm-4v-9b",
271
+ "THUDM/GLM-4.1V-9B-Thinking",
272
"ibm-granite/granite-speech-3.3-2b",
273
"h2oai/h2ovl-mississippi-800m",
274
"OpenGVLab/InternVL2-1B",
tests/models/registry.py
+1
@@ -338,6 +338,7 @@ _MULTIMODAL_EXAMPLE_MODELS = {
338
"GLM4VForCausalLM": _HfExamplesInfo("THUDM/glm-4v-9b",
339
trust_remote_code=True,
340
hf_overrides={"architectures": ["GLM4VForCausalLM"]}), # noqa: E501
341
+ "Glm4vForConditionalGeneration": _HfExamplesInfo("THUDM/GLM-4.1V-9B-Thinking", min_transformers_version="4.53"), # noqa: E501
342
"H2OVLChatModel": _HfExamplesInfo("h2oai/h2ovl-mississippi-800m",
343
extras={"2b": "h2oai/h2ovl-mississippi-2b"}, # noqa: E501
344
max_transformers_version="4.48", # noqa: E501
tests/multimodal/test_utils.py
+3
-1
@@ -172,7 +172,9 @@ async def test_fetch_video_http(video_url: str, num_frames: int):
172
video_sync = connector.fetch_video(video_url, num_frames=num_frames)
173
video_async = await connector.fetch_video_async(video_url,
174
num_frames=num_frames)
175
- assert np.array_equal(video_sync, video_async)
175
+ # Check that the video frames are equal and metadata are same
176
+ assert np.array_equal(video_sync[0], video_async[0])
177
+ assert video_sync[1] == video_async[1]
178
179
180
# Used for the next two tests related to `merge_and_sort_multimodal_metadata`.
vllm/assets/video.py
+25
-1
@@ -3,7 +3,7 @@
3
4
from dataclasses import dataclass
5
from functools import lru_cache
6
-from typing import ClassVar, Literal, Optional
6
+from typing import Any, ClassVar, Literal, Optional
7
8
import cv2
9
import numpy as np
@@ -77,6 +77,24 @@ def video_to_pil_images_list(path: str,
77
]
78
79
80
+def video_get_metadata(path: str) -> dict[str, Any]:
81
+ cap = cv2.VideoCapture(path)
82
+ if not cap.isOpened():
83
+ raise ValueError(f"Could not open video file {path}")
84
+
85
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
86
+ fps = cap.get(cv2.CAP_PROP_FPS)
87
+ duration = total_frames / fps if fps > 0 else 0
88
+
89
+ metadata = {
90
+ "total_num_frames": total_frames,
91
+ "fps": fps,
92
+ "duration": duration,
93
+ "video_backend": "opencv"
94
+ }
95
+ return metadata
96
+
97
+
98
VideoAssetName = Literal["baby_reading"]
99
100
@@ -105,6 +123,12 @@ class VideoAsset:
123
ret = video_to_ndarrays(video_path, self.num_frames)
124
return ret
125
126
+ @property
127
+ def metadata(self) -> dict[str, Any]:
128
+ video_path = download_video_asset(self.filename)
129
+ ret = video_get_metadata(video_path)
130
+ return ret
131
+
132
def get_audio(self, sampling_rate: Optional[float] = None) -> npt.NDArray:
133
"""
134
Read audio data from the video asset, used in Qwen2.5-Omni examples.
vllm/entrypoints/chat_utils.py
+4
@@ -515,6 +515,8 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
515
if modality in ("image", "image_embeds"):
516
if model_type == "chatglm":
517
return "<|begin_of_image|><|endoftext|><|end_of_image|>"
518
+ if model_type == "glm4v":
519
+ return "<|begin_of_image|><|image|><|end_of_image|>"
520
if model_type in ("phi3_v", "phi4mm"):
521
return f"<|image_{current_count}|>"
522
if model_type in ("minicpmo", "minicpmv"):
@@ -563,6 +565,8 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
565
elif modality == "video":
566
if model_type == "internvl_chat":
567
return "<video>"
568
+ if model_type == "glm4v":
569
+ return "<|begin_of_video|><|video|><|end_of_video|>"
570
if model_type in ("qwen2_vl", "qwen2_5_vl"):
571
return "<|vision_start|><|video_pad|><|vision_end|>"
572
if model_type == "qwen2_5_omni":
vllm/model_executor/layers/rotary_embedding.py
+119
@@ -23,6 +23,7 @@
23
# See the License for the specific language governing permissions and
24
# limitations under the License.
25
"""Rotary Positional Embeddings."""
26
+import itertools
27
import math
28
from typing import Any, Optional, Union
29
@@ -1118,6 +1119,15 @@ class MRotaryEmbedding(RotaryEmbedding):
1119
audio_feature_lengths=audio_feature_lengths,
1120
use_audio_in_video=use_audio_in_video,
1121
)
1122
+ elif "glm4v" in hf_config.model_type:
1123
+ return cls._glm4v_get_input_positions_tensor(
1124
+ input_tokens=input_tokens,
1125
+ hf_config=hf_config,
1126
+ image_grid_thw=image_grid_thw,
1127
+ video_grid_thw=video_grid_thw,
1128
+ context_len=context_len,
1129
+ seq_len=seq_len,
1130
+ )
1131
else:
1132
return cls._vl_get_input_positions_tensor(
1133
input_tokens=input_tokens,
@@ -1129,6 +1139,115 @@ class MRotaryEmbedding(RotaryEmbedding):
1139
seq_len=seq_len,
1140
)
1141
1142
+ @classmethod
1143
+ def _glm4v_get_input_positions_tensor(
1144
+ cls,
1145
+ input_tokens: list[int],
1146
+ hf_config: PretrainedConfig,
1147
+ image_grid_thw: Union[list[list[int]], torch.Tensor],
1148
+ video_grid_thw: Union[list[list[int]], torch.Tensor],
1149
+ context_len: int = 0,
1150
+ seq_len: Optional[int] = None,
1151
+ ) -> tuple[torch.Tensor, int]:
1152
+ """Get mrope input positions and delta value for GLM4V."""
1153
+
1154
+ image_token_id = hf_config.image_token_id
1155
+ video_start_token_id = hf_config.video_start_token_id
1156
+ video_end_token_id = hf_config.video_end_token_id
1157
+ spatial_merge_size = hf_config.vision_config.spatial_merge_size
1158
+ llm_pos_ids_list: list = []
1159
+
1160
+ if not (image_grid_thw is None and video_grid_thw is None):
1161
+ if isinstance(image_grid_thw, torch.Tensor):
1162
+ image_grid_thw = image_grid_thw.tolist()
1163
+
1164
+ input_token_type: list[str] = []
1165
+ video_check_flg = False
1166
+ for token in input_tokens:
1167
+ if token == video_start_token_id:
1168
+ video_check_flg = True
1169
+ elif token == video_end_token_id:
1170
+ video_check_flg = False
1171
+
1172
+ if (token == image_token_id) and (video_check_flg is False):
1173
+ input_token_type.append("image")
1174
+ elif (token == image_token_id) and (video_check_flg is True):
1175
+ input_token_type.append("video")
1176
+ else:
1177
+ input_token_type.append("text")
1178
+
1179
+ input_type_group: list[tuple[str, int, int]] = []
1180
+ for key, group_iter in itertools.groupby(
1181
+ enumerate(input_token_type), lambda x: x[1]):
1182
+ group_list = list(group_iter)
1183
+ start_index = group_list[0][0]
1184
+ end_index = group_list[-1][0] + 1
1185
+ input_type_group.append((key, start_index, end_index))
1186
+
1187
+ video_frame_num = 1
1188
+ mm_data_idx = 0
1189
+ for modality_type, start_idx, end_idx in input_type_group:
1190
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(
1191
+ llm_pos_ids_list) > 0 else 0
1192
+ if modality_type == "image":
1193
+ t, h, w = (
1194
+ image_grid_thw[mm_data_idx][0],
1195
+ image_grid_thw[mm_data_idx][1],
1196
+ image_grid_thw[mm_data_idx][2],
1197
+ )
1198
+ llm_grid_t, llm_grid_h, llm_grid_w = \
1199
+ t, h // spatial_merge_size, w // spatial_merge_size
1200
+
1201
+ t_index = torch.arange(llm_grid_t).view(-1, 1).expand(
1202
+ -1, llm_grid_h * llm_grid_w).flatten()
1203
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(
1204
+ llm_grid_t, -1, llm_grid_w).flatten()
1205
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(
1206
+ llm_grid_t, llm_grid_h, -1).flatten()
1207
+ llm_pos_ids_list.append(
1208
+ torch.stack([t_index, h_index, w_index]) + st_idx)
1209
+ mm_data_idx += 1
1210
+
1211
+ elif modality_type == "video":
1212
+ t, h, w = (
1213
+ video_frame_num,
1214
+ image_grid_thw[mm_data_idx][1],
1215
+ image_grid_thw[mm_data_idx][2],
1216
+ )
1217
+ llm_grid_t, llm_grid_h, llm_grid_w = \
1218
+ t, h // spatial_merge_size, w // spatial_merge_size
1219
+
1220
+ for t_idx in range(llm_grid_t):
1221
+ t_index = torch.tensor(t_idx).view(-1, 1).expand(
1222
+ -1, llm_grid_h * llm_grid_w).flatten()
1223
+ h_index = torch.arange(llm_grid_h).view(
1224
+ 1, -1, 1).expand(1, -1, llm_grid_w).flatten()
1225
+ w_index = torch.arange(llm_grid_w).view(
1226
+ 1, 1, -1).expand(1, llm_grid_h, -1).flatten()
1227
+ llm_pos_ids_list.append(
1228
+ torch.stack([t_index, h_index, w_index]) + st_idx)
1229
+
1230
+ mm_data_idx += 1
1231
+ video_frame_num += 1
1232
+
1233
+ else:
1234
+ text_len = end_idx - start_idx
1235
+ llm_pos_ids_list.append(
1236
+ torch.arange(text_len).view(1, -1).expand(3, -1) +
1237
+ st_idx)
1238
+ video_frame_num = 1
1239
+
1240
+ else:
1241
+ text_len = len(input_tokens)
1242
+ llm_pos_ids_list.append(
1243
+ torch.arange(text_len).view(1, -1).expand(3, -1))
1244
+
1245
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
1246
+ llm_positions = llm_positions[:, context_len:seq_len]
1247
+ mrope_position_delta = (llm_positions.max() + 1 -
1248
+ len(input_tokens)).item()
1249
+ return llm_positions, mrope_position_delta
1250
+
1251
@classmethod
1252
def _vl_get_input_positions_tensor(
1253
cls,
vllm/model_executor/models/glm4_1v.py
new
+1589
@@ -0,0 +1,1589 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+
3
+# Adapted from
4
+# https://github.com/huggingface/transformers/blob/main/src/transformers/models/Glm4v/modeling_Glm4v.py
5
+# Copyright 2025 The vLLM team.
6
+# Copyright 2025 The ZhipuAI Team.
7
+# Copyright 2025 The HuggingFace Inc. team.
8
+# All rights reserved.
9
+#
10
+# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
11
+# and OPT implementations in this library. It has been modified from its
12
+# original forms to accommodate minor architectural differences compared
13
+# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
14
+#
15
+# Licensed under the Apache License, Version 2.0 (the "License");
16
+# you may not use this file except in compliance with the License.
17
+# You may obtain a copy of the License at
18
+#
19
+# http://www.apache.org/licenses/LICENSE-2.0
20
+#
21
+# Unless required by applicable law or agreed to in writing, software
22
+# distributed under the License is distributed on an "AS IS" BASIS,
23
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+# See the License for the specific language governing permissions and
25
+# limitations under the License.
26
+"""Inference-only GLM-4V model compatible with HuggingFace weights."""
27
+
28
+import math
29
+from collections.abc import Iterable, Mapping, Sequence
30
+from functools import partial
31
+from typing import Any, Callable, Literal, Optional, TypedDict, Union
32
+
33
+import numpy as np
34
+import torch
35
+import torch.nn as nn
36
+import torch.nn.functional as F
37
+from einops import rearrange
38
+from transformers import BatchFeature
39
+from transformers.models.glm4v.configuration_glm4v import (Glm4vConfig,
40
+ Glm4vVisionConfig)
41
+from transformers.models.glm4v.image_processing_glm4v import (
42
+ Glm4vImageProcessor, smart_resize)
43
+from transformers.models.glm4v.video_processing_glm4v import (
44
+ Glm4vVideoProcessor)
45
+from transformers.video_utils import VideoMetadata
46
+
47
+from vllm.config import VllmConfig
48
+from vllm.distributed import parallel_state
49
+from vllm.distributed import utils as dist_utils
50
+from vllm.logger import init_logger
51
+from vllm.model_executor import SamplingMetadata
52
+from vllm.model_executor.layers.layernorm import RMSNorm
53
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
54
+ MergedColumnParallelLinear,
55
+ QKVParallelLinear,
56
+ RowParallelLinear)
57
+from vllm.model_executor.layers.quantization import QuantizationConfig
58
+from vllm.model_executor.layers.quantization.gptq import GPTQConfig
59
+from vllm.model_executor.layers.quantization.gptq_marlin import (
60
+ GPTQMarlinConfig)
61
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
62
+from vllm.model_executor.models.module_mapping import MultiModelKeys
63
+from vllm.multimodal import MULTIMODAL_REGISTRY
64
+from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
65
+ MultiModalKwargs, VideoItem)
66
+from vllm.multimodal.parse import (ImageSize, MultiModalDataItems,
67
+ MultiModalDataParser)
68
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
69
+ BaseProcessingInfo, PromptReplacement,
70
+ PromptUpdate)
71
+from vllm.multimodal.profiling import BaseDummyInputsBuilder
72
+from vllm.platforms import _Backend
73
+from vllm.sequence import IntermediateTensors
74
+from vllm.transformers_utils.config import uses_mrope
75
+
76
+from ..layers.activation import SiluAndMul
77
+from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
78
+ SupportsMultiModal, SupportsPP)
79
+from .qwen2_vl import _qwen2vl_field_config, apply_rotary_pos_emb_vision
80
+from .utils import (AutoWeightsLoader, WeightsMapper,
81
+ init_vllm_registered_model, maybe_prefix,
82
+ merge_multimodal_embeddings)
83
+from .vision import get_vit_attn_backend
84
+
85
+logger = init_logger(__name__)
86
+
87
+# For profile run
88
+_MAX_FRAMES_PER_VIDEO = 600
89
+
90
+# === Vision Inputs === #
91
+
92
+
93
+class Glm4vImagePixelInputs(TypedDict):
94
+ type: Literal["pixel_values"]
95
+ pixel_values: torch.Tensor
96
+ """Shape:
97
+ `(num_patches, num_channels * patch_size * patch_size)`
98
+ """
99
+
100
+ image_grid_thw: torch.Tensor
101
+ """Shape: `(num_images, 3)`
102
+ This should be in `(grid_t, grid_h, grid_w)` format.
103
+ """
104
+
105
+
106
+class Glm4vImageEmbeddingInputs(TypedDict):
107
+ type: Literal["image_embeds"]
108
+ image_embeds: torch.Tensor
109
+ """Supported types:
110
+ - List[`torch.Tensor`]: A list of tensors holding all images' features.
111
+ Each tensor holds an image's features.
112
+ - `torch.Tensor`: A tensor holding all images' features
113
+ (concatenation of all images' feature tensors).
114
+
115
+ Tensor shape: `(num_image_features, hidden_size)`
116
+ - `num_image_features` varies based on
117
+ the number and resolution of the images.
118
+ - `hidden_size` must match the hidden size of language model backbone.
119
+ """
120
+
121
+ image_grid_thw: torch.Tensor
122
+ """Shape: `(num_images, 3)`
123
+ This should be in `(grid_t, grid_h, grid_w)` format.
124
+ """
125
+
126
+
127
+Glm4vImageInputs = Union[Glm4vImagePixelInputs, Glm4vImageEmbeddingInputs]
128
+
129
+
130
+class Glm4vVideoPixelInputs(TypedDict):
131
+ type: Literal["pixel_values_videos"]
132
+ pixel_values_videos: torch.Tensor
133
+ """Shape:
134
+ `(num_patches,
135
+ num_channels * temporal_patch_size * patch_size * patch_size)`
136
+ """
137
+ # video_metadata: Union[list[VideoMetadata], list[dict]]
138
+ video_grid_thw: Union[list[torch.Tensor], torch.Tensor]
139
+ """Shape: `(num_videos, num_frames, 3)` or `(1, num_frames, 3)`
140
+ for single video.
141
+ Each entry represents [grid_t, grid_h, grid_w] format where:
142
+ - grid_t: Temporal grid size (usually 1 for processed video)
143
+ - grid_h: Height grid size
144
+ - grid_w: Width grid size
145
+ This describes the grid structure of the video patches.
146
+ """
147
+
148
+
149
+class Glm4vVideoEmbeddingInputs(TypedDict):
150
+ type: Literal["video_embeds"]
151
+
152
+ video_embeds: torch.Tensor
153
+ """
154
+ Tensor shape: `(num_video_patches, hidden_size)`
155
+ - `num_video_patches`: Total number of video patches across all frames
156
+ - `hidden_size`: Must match the hidden size of language model backbone
157
+ """
158
+
159
+ video_grid_thw: torch.Tensor
160
+ """Shape: `(num_videos, 1, 3)` or `(1, 1, 3)` for single video
161
+ Each entry represents [grid_t, grid_h, grid_w] format where:
162
+ - grid_t: Temporal grid size (usually 1 for processed video)
163
+ - grid_h: Height grid size
164
+ - grid_w: Width grid size
165
+ This describes the grid structure of the video patches.
166
+ """
167
+
168
+
169
+Glm4vVideoInputs = Union[Glm4vVideoPixelInputs, Glm4vVideoEmbeddingInputs]
170
+
171
+# === Vision Encoder === #
172
+
173
+
174
+class Glm4vVisionMLP(nn.Module):
175
+
176
+ def __init__(
177
+ self,
178
+ in_features: int,
179
+ hidden_features: int,
180
+ bias: bool = False,
181
+ quant_config: Optional[QuantizationConfig] = None,
182
+ ):
183
+ super().__init__()
184
+ self.gate_up_proj = MergedColumnParallelLinear(
185
+ input_size=in_features,
186
+ output_sizes=[hidden_features] * 2,
187
+ bias=bias,
188
+ quant_config=quant_config,
189
+ )
190
+ self.down_proj = RowParallelLinear(
191
+ hidden_features,
192
+ in_features,
193
+ bias=bias,
194
+ quant_config=quant_config,
195
+ )
196
+ self.act_fn = SiluAndMul()
197
+
198
+ def forward(self, x: torch.Tensor):
199
+ x, _ = self.gate_up_proj(x)
200
+ x = self.act_fn(x)
201
+ x, _ = self.down_proj(x)
202
+ return x
203
+
204
+
205
+def all_gather_interleave(local_tensor, hidden_size: int, tp_size: int):
206
+ """All-gather the input tensor interleavely across model parallel group."""
207
+ import torch.distributed as dist
208
+
209
+ gathered_tensors = [torch.zeros_like(local_tensor) for _ in range(tp_size)]
210
+ dist.all_gather(
211
+ gathered_tensors,
212
+ local_tensor,
213
+ group=parallel_state.get_tp_group().device_group,
214
+ )
215
+
216
+ gathered_tensors_split = [
217
+ torch.split(tensor, hidden_size // tp_size, -1)
218
+ for tensor in gathered_tensors
219
+ ]
220
+ ordered_tensors = [
221
+ tensor for pair in zip(*gathered_tensors_split) for tensor in pair
222
+ ]
223
+ result_tensor = torch.cat(ordered_tensors, dim=-1)
224
+ return result_tensor
225
+
226
+
227
+class Glm4vVisionAttention(nn.Module):
228
+
229
+ def __init__(
230
+ self,
231
+ embed_dim: int,
232
+ num_heads: int,
233
+ projection_size: int,
234
+ quant_config: Optional[QuantizationConfig] = None,
235
+ prefix: str = "",
236
+ ) -> None:
237
+ super().__init__()
238
+ # Per attention head and per partition values.
239
+ self.tp_size = parallel_state.get_tensor_model_parallel_world_size()
240
+ self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
241
+ self.hidden_size_per_attention_head = dist_utils.divide(
242
+ projection_size, num_heads)
243
+ self.num_attention_heads_per_partition = dist_utils.divide(
244
+ num_heads, self.tp_size)
245
+
246
+ self.qkv = QKVParallelLinear(
247
+ hidden_size=embed_dim,
248
+ head_size=self.hidden_size_per_attention_head,
249
+ total_num_heads=num_heads,
250
+ total_num_kv_heads=num_heads,
251
+ bias=False,
252
+ quant_config=quant_config,
253
+ prefix=f"{prefix}.qkv",
254
+ )
255
+ self.proj = RowParallelLinear(
256
+ input_size=projection_size,
257
+ output_size=embed_dim,
258
+ quant_config=quant_config,
259
+ prefix=f"{prefix}.proj",
260
+ bias=False,
261
+ )
262
+
263
+ # Detect attention implementation.
264
+ self.attn_backend: _Backend = get_vit_attn_backend(support_fa=True)
265
+ if self.attn_backend not in {
266
+ _Backend.FLASH_ATTN,
267
+ _Backend.TORCH_SDPA,
268
+ _Backend.XFORMERS,
269
+ }:
270
+ raise RuntimeError(
271
+ f"GLM-4V does not support {self.attn_backend} backend now.")
272
+
273
+ def split_qkv(self, qkv: torch.Tensor) -> tuple[torch.Tensor, ...]:
274
+ # [s, b, 3 * head * head_dim]
275
+ seq_len, bs, _ = qkv.shape
276
+ if self.tp_size > 1:
277
+ qkv = all_gather_interleave(qkv, self.qkv.hidden_size,
278
+ self.tp_size)
279
+
280
+ # [s, b, 3 * head * head_dim] -> 3 * [s, b, head * head_dim]
281
+ q, k, v = qkv.chunk(3, dim=2)
282
+
283
+ # 3 * [s, b, head * head_dim]
284
+ if self.tp_size > 1:
285
+ splitter = partial(
286
+ dist_utils.split_tensor_along_last_dim,
287
+ num_partitions=self.tp_size,
288
+ )
289
+ q = splitter(q)[self.tp_rank]
290
+ k = splitter(k)[self.tp_rank]
291
+ v = splitter(v)[self.tp_rank]
292
+
293
+ # 3 * [s, b, head * head_dim] -> 3 * [s, b, head, head_dim]
294
+ new_shape = (
295
+ seq_len,
296
+ bs,
297
+ self.num_attention_heads_per_partition,
298
+ self.hidden_size_per_attention_head,
299
+ )
300
+ q, k, v = (x.view(*new_shape) for x in (q, k, v))
301
+ return q, k, v
302
+
303
+ def forward(
304
+ self,
305
+ x: torch.Tensor,
306
+ cu_seqlens: torch.Tensor,
307
+ rotary_pos_emb: torch.Tensor,
308
+ max_seqlen: Optional[int] = None, # Only used for Flash Attention
309
+ seqlens: Optional[list[int]] = None, # Only used for xFormers
310
+ ) -> torch.Tensor:
311
+ # [s, b, c] --> [s, b, head * 3 * head_dim]
312
+ x, _ = self.qkv(x)
313
+
314
+ # [s, b, 3 * head * head_dim] -> 3 * [s, b, head, head_dim]
315
+ q, k, v = self.split_qkv(x)
316
+ batch_size = q.shape[1]
317
+
318
+ q, k, v = (rearrange(x, "s b ... -> b s ...").contiguous()
319
+ for x in (q, k, v))
320
+ if rotary_pos_emb is not None:
321
+ q = apply_rotary_pos_emb_vision(q, rotary_pos_emb)
322
+ k = apply_rotary_pos_emb_vision(k, rotary_pos_emb)
323
+
324
+ if self.attn_backend == _Backend.FLASH_ATTN:
325
+ # from vllm_flash_attn.flash_attn_interface import (
326
+ # flash_attn_varlen_func)
327
+ from flash_attn import flash_attn_varlen_func
328
+
329
+ q, k, v = (rearrange(x, "b s ... -> (b s) ...") for x in [q, k, v])
330
+
331
+ output = flash_attn_varlen_func(
332
+ q,
333
+ k,
334
+ v,
335
+ cu_seqlens_q=cu_seqlens,
336
+ cu_seqlens_k=cu_seqlens,
337
+ max_seqlen_q=max_seqlen,
338
+ max_seqlen_k=max_seqlen,
339
+ dropout_p=0,
340
+ causal=False,
341
+ )
342
+
343
+ context_layer = rearrange(output,
344
+ "(b s) ... -> b s ...",
345
+ b=batch_size)
346
+ elif self.attn_backend == _Backend.TORCH_SDPA:
347
+ # Execute attention entry by entry for speed & less VRAM.
348
+ outputs = []
349
+ for i in range(1, len(cu_seqlens)):
350
+ start_idx = cu_seqlens[i - 1]
351
+ end_idx = cu_seqlens[i]
352
+ q_i = q[:, start_idx:end_idx]
353
+ k_i = k[:, start_idx:end_idx]
354
+ v_i = v[:, start_idx:end_idx]
355
+ q_i, k_i, v_i = (rearrange(x, "b s h d -> b h s d")
356
+ for x in [q_i, k_i, v_i])
357
+ output_i = F.scaled_dot_product_attention(q_i,
358
+ k_i,
359
+ v_i,
360
+ dropout_p=0.0)
361
+ output_i = rearrange(output_i, "b h s d -> b s h d ")
362
+ outputs.append(output_i)
363
+ context_layer = torch.cat(outputs, dim=1)
364
+ elif self.attn_backend == _Backend.XFORMERS:
365
+ from xformers import ops as xops
366
+ from xformers.ops.fmha.attn_bias import BlockDiagonalMask
367
+
368
+ attn_bias = BlockDiagonalMask.from_seqlens(q_seqlen=seqlens,
369
+ kv_seqlen=None,
370
+ device=q.device)
371
+
372
+ context_layer = xops.memory_efficient_attention_forward(
373
+ q, k, v, attn_bias=attn_bias, p=0, scale=None)
374
+
375
+ context_layer = rearrange(context_layer,
376
+ "b s h d -> s b (h d)").contiguous()
377
+
378
+ output, _ = self.proj(context_layer)
379
+ return output
380
+
381
+
382
+class Glm4vVisionBlock(nn.Module):
383
+
384
+ def __init__(
385
+ self,
386
+ dim: int,
387
+ num_heads: int,
388
+ mlp_hidden_dim: int,
389
+ norm_layer: Optional[Callable[[int], nn.Module]] = None,
390
+ quant_config: Optional[QuantizationConfig] = None,
391
+ prefix: str = "",
392
+ ) -> None:
393
+ super().__init__()
394
+ if norm_layer is None:
395
+ norm_layer = partial(nn.LayerNorm, eps=1e-6)
396
+ self.norm1 = norm_layer(dim)
397
+ self.norm2 = norm_layer(dim)
398
+ self.attn = Glm4vVisionAttention(
399
+ embed_dim=dim,
400
+ num_heads=num_heads,
401
+ projection_size=dim,
402
+ quant_config=quant_config,
403
+ prefix=f"{prefix}.attn",
404
+ )
405
+ self.mlp = Glm4vVisionMLP(
406
+ dim,
407
+ mlp_hidden_dim,
408
+ bias=False,
409
+ quant_config=quant_config,
410
+ )
411
+
412
+ def forward(
413
+ self,
414
+ x: torch.Tensor,
415
+ cu_seqlens: torch.Tensor,
416
+ rotary_pos_emb: torch.Tensor,
417
+ max_seqlen: Optional[int] = None, # Only used for Flash Attention
418
+ seqlens: Optional[list[int]] = None, # Only used for xFormers
419
+ ) -> torch.Tensor:
420
+ x = x + self.attn(
421
+ self.norm1(x),
422
+ cu_seqlens=cu_seqlens,
423
+ rotary_pos_emb=rotary_pos_emb,
424
+ max_seqlen=max_seqlen,
425
+ seqlens=seqlens,
426
+ )
427
+
428
+ x = x + self.mlp(self.norm2(x))
429
+ return x
430
+
431
+
432
+class Glm4vVisionPatchEmbed(nn.Module):
433
+
434
+ def __init__(
435
+ self,
436
+ patch_size: int = 14,
437
+ temporal_patch_size: int = 1,
438
+ in_channels: int = 3,
439
+ hidden_size: int = 1536,
440
+ ) -> None:
441
+ super().__init__()
442
+ self.patch_size = patch_size
443
+ self.temporal_patch_size = temporal_patch_size
444
+ self.hidden_size = hidden_size
445
+
446
+ kernel_size = (temporal_patch_size, patch_size, patch_size)
447
+ self.proj = nn.Conv3d(
448
+ in_channels,
449
+ hidden_size,
450
+ kernel_size=kernel_size,
451
+ stride=kernel_size,
452
+ bias=True,
453
+ )
454
+
455
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
456
+ L, C = x.shape
457
+ x = x.view(L, -1, self.temporal_patch_size, self.patch_size,
458
+ self.patch_size)
459
+ x = self.proj(x).view(L, self.hidden_size)
460
+ return x
461
+
462
+
463
+class Glm4vPatchMerger(nn.Module):
464
+
465
+ def __init__(
466
+ self,
467
+ d_model: int,
468
+ context_dim: int,
469
+ quant_config: Optional[QuantizationConfig] = None,
470
+ bias: bool = False,
471
+ ) -> None:
472
+ super().__init__()
473
+ self.hidden_size = d_model
474
+ self.proj = ColumnParallelLinear(self.hidden_size,
475
+ self.hidden_size,
476
+ bias=bias,
477
+ gather_output=True)
478
+ self.post_projection_norm = nn.LayerNorm(self.hidden_size)
479
+ self.gate_up_proj = MergedColumnParallelLinear(
480
+ input_size=self.hidden_size,
481
+ output_sizes=[context_dim] * 2,
482
+ bias=bias,
483
+ quant_config=quant_config,
484
+ )
485
+ self.down_proj = RowParallelLinear(
486
+ context_dim,
487
+ self.hidden_size,
488
+ bias=bias,
489
+ quant_config=quant_config,
490
+ )
491
+ self.act_fn = SiluAndMul()
492
+ self.extra_activation_func = nn.GELU()
493
+
494
+ def forward(self, x: torch.Tensor):
495
+ x, _ = self.proj(x)
496
+ x = self.extra_activation_func(self.post_projection_norm(x))
497
+ gate_up, _ = self.gate_up_proj(x)
498
+ x = self.act_fn(gate_up)
499
+ x, _ = self.down_proj(x)
500
+ return x
501
+
502
+
503
+class Glm4vVisionEmbeddings(nn.Module):
504
+
505
+ def __init__(self, config: Glm4vVisionConfig):
506
+ super().__init__()
507
+ self.config = config
508
+ self.embed_dim = config.hidden_size
509
+ self.image_size = config.image_size
510
+ self.patch_size = config.patch_size
511
+
512
+ self.num_patches = (self.image_size // self.patch_size)**2
513
+ self.num_positions = self.num_patches
514
+ self.position_embedding = nn.Embedding(self.num_positions,
515
+ self.embed_dim)
516
+ self.register_buffer(
517
+ "position_ids",
518
+ torch.arange(self.num_positions).expand((1, -1)),
519
+ persistent=False,
520
+ )
521
+
522
+ def forward(self, embeddings, lengths, image_shapes, h_coords,
523
+ w_coords) -> torch.Tensor:
524
+ pos_embed_weight = self.position_embedding.weight
525
+ hidden_size = pos_embed_weight.shape[1]
526
+ total_seq = h_coords.shape[0]
527
+ device = pos_embed_weight.device
528
+
529
+ # Move coordinates to correct device
530
+ h_coords, w_coords = h_coords.to(device), w_coords.to(device)
531
+
532
+ # Handle empty sequence case
533
+ if total_seq == 0:
534
+ adapted_pos_embed = torch.empty(0,
535
+ hidden_size,
536
+ device=device,
537
+ dtype=pos_embed_weight.dtype)
538
+ else:
539
+ # Convert inputs to tensors if needed
540
+ if isinstance(lengths, list):
541
+ lengths = torch.tensor(lengths,
542
+ device=device,
543
+ dtype=torch.long)
544
+ if not isinstance(image_shapes, torch.Tensor):
545
+ image_shapes = torch.tensor(image_shapes,
546
+ device=device,
547
+ dtype=torch.long)
548
+
549
+ # Prepare 2D position embedding
550
+ orig_size_sq = pos_embed_weight.shape[0]
551
+ orig_size = int(orig_size_sq**0.5)
552
+ pos_embed_2d = (pos_embed_weight.view(
553
+ orig_size, orig_size,
554
+ hidden_size).permute(2, 0,
555
+ 1).unsqueeze(0).to(device=device,
556
+ dtype=torch.float32))
557
+
558
+ # Calculate target dimensions for each patch
559
+ target_h = torch.cat([
560
+ image_shapes[i, 1].repeat(lengths[i])
561
+ for i in range(len(lengths))
562
+ ]).to(device=device, dtype=torch.float32)
563
+ target_w = torch.cat([
564
+ image_shapes[i, 2].repeat(lengths[i])
565
+ for i in range(len(lengths))
566
+ ]).to(device=device, dtype=torch.float32)
567
+
568
+ # Normalize coordinates to [-1, 1] range for grid_sample
569
+ h_coords = h_coords.to(device=device, dtype=torch.float32)
570
+ w_coords = w_coords.to(device=device, dtype=torch.float32)
571
+ norm_w = ((w_coords + 0.5) / target_w) * 2 - 1
572
+ norm_h = ((h_coords + 0.5) / target_h) * 2 - 1
573
+
574
+ # Create sampling grid
575
+ grid = (torch.stack((norm_w, norm_h),
576
+ dim=-1).unsqueeze(0).unsqueeze(2))
577
+
578
+ # Perform bicubic interpolation
579
+ interpolated_embed_fp32 = F.grid_sample(
580
+ pos_embed_2d,
581
+ grid,
582
+ mode="bicubic",
583
+ align_corners=False,
584
+ padding_mode="border",
585
+ )
586
+
587
+ # Reshape and convert back to original dtype
588
+ adapted_pos_embed_fp32 = (
589
+ interpolated_embed_fp32.squeeze(0).squeeze(-1).permute(1, 0))
590
+ adapted_pos_embed = adapted_pos_embed_fp32.to(
591
+ pos_embed_weight.dtype).to(embeddings.device)
592
+
593
+ # Add adapted position encoding to embeddings
594
+ embeddings = embeddings + adapted_pos_embed
595
+ return embeddings
596
+
597
+
598
+class Glm4vVisionRotaryEmbedding(nn.Module):
599
+
600
+ def __init__(self, dim: int, theta: float = 10000.0) -> None:
601
+ super().__init__()
602
+ self.dim = dim
603
+ self.theta = theta
604
+ inv_freq = 1.0 / (theta
605
+ **(torch.arange(0, dim, 2, dtype=torch.float) / dim))
606
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
607
+ self._seq_len_cached = 0
608
+ self._freqs_cached = None
609
+
610
+ def update_freqs_cache(self, seqlen: int) -> None:
611
+ if seqlen > self._seq_len_cached:
612
+ seqlen *= 2
613
+ self._seq_len_cached = seqlen
614
+ self.inv_freq = 1.0 / (self.theta**(torch.arange(
615
+ 0,
616
+ self.dim,
617
+ 2,
618
+ dtype=torch.float,
619
+ device=self.inv_freq.device,
620
+ ) / self.dim))
621
+ seq = torch.arange(seqlen,
622
+ device=self.inv_freq.device,
623
+ dtype=self.inv_freq.dtype)
624
+ freqs = torch.outer(seq, self.inv_freq)
625
+ self._freqs_cached = freqs
626
+
627
+ def forward(self, seqlen: int) -> torch.Tensor:
628
+ self.update_freqs_cache(seqlen)
629
+ return self._freqs_cached[:seqlen]
630
+
631
+
632
+class Glm4vVisionTransformer(nn.Module):
633
+
634
+ def __init__(
635
+ self,
636
+ vision_config: Glm4vVisionConfig,
637
+ norm_eps: float = 1e-6,
638
+ quant_config: Optional[QuantizationConfig] = None,
639
+ prefix: str = "",
640
+ ) -> None:
641
+ super().__init__()
642
+
643
+ patch_size = vision_config.patch_size
644
+ temporal_patch_size = vision_config.temporal_patch_size
645
+ in_channels = vision_config.in_channels
646
+ depth = vision_config.depth
647
+ self.hidden_size = vision_config.hidden_size
648
+ self.num_heads = vision_config.num_heads
649
+
650
+ self.patch_size = vision_config.patch_size
651
+ self.spatial_merge_size = vision_config.spatial_merge_size
652
+ self.out_hidden_size = vision_config.out_hidden_size
653
+
654
+ self.patch_embed = Glm4vVisionPatchEmbed(
655
+ patch_size=patch_size,
656
+ temporal_patch_size=temporal_patch_size,
657
+ in_channels=in_channels,
658
+ hidden_size=self.hidden_size,
659
+ )
660
+
661
+ norm_layer = partial(RMSNorm, eps=norm_eps)
662
+ head_dim = self.hidden_size // self.num_heads
663
+ self.rotary_pos_emb = Glm4vVisionRotaryEmbedding(head_dim // 2)
664
+ self.blocks = nn.ModuleList([
665
+ Glm4vVisionBlock(
666
+ dim=self.hidden_size,
667
+ num_heads=self.num_heads,
668
+ mlp_hidden_dim=vision_config.out_hidden_size,
669
+ norm_layer=norm_layer,
670
+ quant_config=quant_config,
671
+ prefix=f"{prefix}.blocks.{layer_idx}",
672
+ ) for layer_idx in range(depth)
673
+ ])
674
+ self.merger = Glm4vPatchMerger(
675
+ d_model=vision_config.out_hidden_size,
676
+ context_dim=vision_config.intermediate_size,
677
+ quant_config=quant_config,
678
+ bias=False,
679
+ )
680
+ self.embeddings = Glm4vVisionEmbeddings(vision_config)
681
+
682
+ self.post_conv_layernorm = RMSNorm(vision_config.hidden_size,
683
+ eps=vision_config.rms_norm_eps)
684
+ self.downsample = nn.Conv2d(
685
+ in_channels=vision_config.hidden_size,
686
+ out_channels=vision_config.out_hidden_size,
687
+ kernel_size=vision_config.spatial_merge_size,
688
+ stride=vision_config.spatial_merge_size,
689
+ )
690
+ self.post_layernorm = RMSNorm(vision_config.hidden_size,
691
+ eps=vision_config.rms_norm_eps)
692
+
693
+ self.attn_backend: _Backend = get_vit_attn_backend(support_fa=True)
694
+
695
+ @property
696
+ def dtype(self) -> torch.dtype:
697
+ return self.patch_embed.proj.weight.dtype
698
+
699
+ @property
700
+ def device(self) -> torch.device:
701
+ return self.patch_embed.proj.weight.device
702
+
703
+ def rot_pos_emb(self, grid_thw: torch.Tensor) -> torch.Tensor:
704
+ pos_ids = []
705
+ for t, h, w in grid_thw:
706
+ hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
707
+ wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
708
+ hpos_ids = (hpos_ids.reshape(
709
+ h // self.spatial_merge_size,
710
+ self.spatial_merge_size,
711
+ w // self.spatial_merge_size,
712
+ self.spatial_merge_size,
713
+ ).permute(0, 2, 1, 3).flatten())
714
+ wpos_ids = (wpos_ids.reshape(
715
+ h // self.spatial_merge_size,
716
+ self.spatial_merge_size,
717
+ w // self.spatial_merge_size,
718
+ self.spatial_merge_size,
719
+ ).permute(0, 2, 1, 3).flatten())
720
+ pos_ids.append(
721
+ torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
722
+ pos_ids = torch.cat(pos_ids, dim=0)
723
+ max_grid_size = grid_thw[:, 1:].max()
724
+ rotary_pos_emb_full = self.rotary_pos_emb(max_grid_size)
725
+ rotary_pos_emb = rotary_pos_emb_full[pos_ids].flatten(1)
726
+ return rotary_pos_emb, pos_ids
727
+
728
+ def compute_attn_mask_seqlen(
729
+ self,
730
+ cu_seqlens: torch.Tensor,
731
+ ) -> tuple[Optional[int], Optional[list[int]]]:
732
+ max_seqlen, seqlens = None, None
733
+ seqlens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
734
+ if self.attn_backend == _Backend.FLASH_ATTN:
735
+ max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
736
+ return max_seqlen, seqlens
737
+
738
+ def forward(
739
+ self,
740
+ x: torch.Tensor,
741
+ grid_thw: torch.Tensor,
742
+ ) -> torch.Tensor:
743
+ # patchify
744
+ x = x.to(device=self.device, dtype=self.dtype)
745
+ x = self.patch_embed(x)
746
+ x = self.post_conv_layernorm(x)
747
+
748
+ # compute position embedding
749
+ rotary_pos_emb, image_type_ids = self.rot_pos_emb(grid_thw)
750
+ # compute cu_seqlens
751
+ cu_seqlens = torch.repeat_interleave(grid_thw[:, 1] * grid_thw[:, 2],
752
+ grid_thw[:, 0]).cumsum(
753
+ dim=0, dtype=torch.int32)
754
+ cu_seqlens = F.pad(cu_seqlens, (1, 0), "constant", 0)
755
+
756
+ # pre-compute seqlens for attn mask to reduce cuMemcpy operations
757
+ max_seqlen, seqlens = self.compute_attn_mask_seqlen(cu_seqlens)
758
+ x = self.embeddings(x, seqlens, grid_thw, image_type_ids[:, 0],
759
+ image_type_ids[:, 1])
760
+
761
+ # transformers
762
+ x = x.unsqueeze(1)
763
+ for blk in self.blocks:
764
+ x = blk(
765
+ x,
766
+ cu_seqlens=cu_seqlens,
767
+ rotary_pos_emb=rotary_pos_emb,
768
+ max_seqlen=max_seqlen,
769
+ seqlens=seqlens,
770
+ )
771
+
772
+ # adapter
773
+ x = self.post_layernorm(x)
774
+
775
+ x = x.view(-1, self.spatial_merge_size, self.spatial_merge_size,
776
+ x.shape[-1])
777
+ x = x.permute(0, 3, 1, 2)
778
+ x = self.downsample(x).view(-1, self.out_hidden_size)
779
+ x = self.merger(x)
780
+
781
+ return x
782
+
783
+ def load_weights(self, weights: Iterable[tuple[str,
784
+ torch.Tensor]]) -> set[str]:
785
+ stacked_params_mapping = [
786
+ # (param_name, shard_name, shard_id)
787
+ ("attn.qkv.", "attn.q.", "q"),
788
+ ("attn.qkv.", "attn.k.", "k"),
789
+ ("attn.qkv.", "attn.v.", "v"),
790
+ ("gate_up_proj", "gate_proj", 0),
791
+ ("gate_up_proj", "up_proj", 1),
792
+ ]
793
+ params_dict = dict(self.named_parameters(remove_duplicate=False))
794
+ loaded_params: set[str] = set()
795
+
796
+ for name, loaded_weight in weights:
797
+ for param_name, weight_name, shard_id in stacked_params_mapping:
798
+ if weight_name not in name:
799
+ continue
800
+ name = name.replace(weight_name, param_name)
801
+
802
+ param = params_dict[name]
803
+ weight_loader = param.weight_loader
804
+ weight_loader(param, loaded_weight, shard_id)
805
+ break
806
+ else:
807
+ param = params_dict[name]
808
+ weight_loader = getattr(param, "weight_loader",
809
+ default_weight_loader)
810
+ weight_loader(param, loaded_weight)
811
+ loaded_params.add(name)
812
+ return loaded_params
813
+
814
+
815
+class Glm4vProcessingInfo(BaseProcessingInfo):
816
+
817
+ def get_hf_config(self):
818
+ return self.ctx.get_hf_config(Glm4vConfig)
819
+
820
+ def get_tokenizer(self):
821
+ return self.ctx.tokenizer
822
+
823
+ def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
824
+ return {"image": None, "video": 1}
825
+
826
+ def get_image_processor(self) -> Glm4vImageProcessor:
827
+ return self.get_hf_processor().image_processor
828
+
829
+ def get_video_processor(self) -> Glm4vVideoProcessor:
830
+ return self.get_hf_processor().video_processor
831
+
832
+ def _get_vision_info(
833
+ self,
834
+ *,
835
+ image_width: int,
836
+ image_height: int,
837
+ num_frames: int = 16,
838
+ do_resize: bool = True,
839
+ max_image_pixels: int = 28 * 28 * 2 * 30000,
840
+ ) -> tuple[ImageSize, int]:
841
+ hf_config = self.get_hf_config()
842
+ vision_config = hf_config.vision_config
843
+ patch_size = vision_config.patch_size
844
+ merge_size = vision_config.spatial_merge_size
845
+ temporal_patch_size = vision_config.temporal_patch_size
846
+ if do_resize:
847
+ resized_height, resized_width = smart_resize(
848
+ num_frames=num_frames
849
+ if num_frames > temporal_patch_size else temporal_patch_size,
850
+ height=image_height,
851
+ width=image_width,
852
+ factor=patch_size * merge_size,
853
+ max_pixels=max_image_pixels,
854
+ )
855
+ preprocessed_size = ImageSize(width=resized_width,
856
+ height=resized_height)
857
+ else:
858
+ preprocessed_size = ImageSize(width=image_width,
859
+ height=image_height)
860
+
861
+ # NOTE: Frames are padded to be divisible by `temporal_patch_size`
862
+ # https://github.com/huggingface/transformers/blob/v4.48.3/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py#L294
863
+ padded_num_frames = num_frames + num_frames % temporal_patch_size
864
+
865
+ grid_t = max(padded_num_frames // temporal_patch_size, 1)
866
+ grid_h = preprocessed_size.height // patch_size
867
+ grid_w = preprocessed_size.width // patch_size
868
+
869
+ num_patches = grid_t * grid_h * grid_w
870
+ num_vision_tokens = num_patches // (merge_size**2)
871
+
872
+ return preprocessed_size, num_vision_tokens
873
+
874
+ def get_image_size_with_most_features(self) -> ImageSize:
875
+ max_image_size, _ = self._get_vision_info(image_width=9999999,
876
+ image_height=9999999)
877
+ return max_image_size
878
+
879
+ def get_num_image_tokens(
880
+ self,
881
+ *,
882
+ image_width: int,
883
+ image_height: int,
884
+ ) -> int:
885
+ _, num_image_tokens = self._get_vision_info(
886
+ image_width=image_width,
887
+ image_height=image_height,
888
+ max_image_pixels=28 * 28 * 2 * 6144,
889
+ )
890
+ return num_image_tokens
891
+
892
+ def get_max_image_tokens(self) -> int:
893
+ target_width, target_height = self.get_image_size_with_most_features()
894
+
895
+ return self.get_num_image_tokens(
896
+ image_width=target_width,
897
+ image_height=target_height,
898
+ )
899
+
900
+ def get_num_video_tokens(
901
+ self,
902
+ *,
903
+ image_width: int,
904
+ image_height: int,
905
+ num_frames: int,
906
+ ) -> int:
907
+ _, num_video_tokens = self._get_vision_info(
908
+ image_width=image_width,
909
+ image_height=image_height,
910
+ num_frames=num_frames,
911
+ max_image_pixels=28 * 28 * 2 * 30000,
912
+ )
913
+ return num_video_tokens
914
+
915
+ def _get_max_video_frames(self, max_tokens: int) -> int:
916
+ target_width, target_height = self.get_image_size_with_most_features()
917
+
918
+ num_frames = 0
919
+
920
+ while True:
921
+ next_num_frames = num_frames + 1
922
+ next_max_tokens = self.get_num_video_tokens(
923
+ image_width=target_width,
924
+ image_height=target_height,
925
+ num_frames=next_num_frames,
926
+ )
927
+ if next_max_tokens > max_tokens or next_max_tokens == 0:
928
+ break
929
+
930
+ num_frames = next_num_frames
931
+
932
+ return num_frames
933
+
934
+ def get_num_frames_with_most_features(
935
+ self,
936
+ seq_len: int,
937
+ mm_counts: Mapping[str, int],
938
+ ) -> int:
939
+ max_images = mm_counts.get("image", 0)
940
+ max_videos = mm_counts.get("video", 0)
941
+
942
+ max_image_tokens = self.get_max_image_tokens() * max_images
943
+ max_total_frames = self._get_max_video_frames(seq_len -
944
+ max_image_tokens)
945
+ max_frames_per_video = min(max_total_frames // max(max_videos, 1),
946
+ _MAX_FRAMES_PER_VIDEO)
947
+
948
+ return max(max_frames_per_video, 1)
949
+
950
+ def _get_video_second_idx(self, metadata: dict[str, Any],
951
+ total_frames: int) -> list[int]:
952
+ video_processor = self.get_video_processor()
953
+
954
+ video_fps = metadata.get("fps", 2.0)
955
+ meta_frames = metadata.get("total_num_frames", total_frames)
956
+ max_frame_idx = meta_frames - 1
957
+ duration = metadata.get("duration",
958
+ round(max_frame_idx / video_fps) + 1)
959
+ if duration <= video_processor.max_duration:
960
+ n = int(math.floor(duration * video_processor.fps))
961
+ frame_indices = [
962
+ min(
963
+ max_frame_idx,
964
+ int(math.ceil(i * video_fps / video_processor.fps)),
965
+ ) for i in range(n)
966
+ ]
967
+ else:
968
+ num_samples = int(video_processor.max_duration *
969
+ video_processor.fps)
970
+ if num_samples >= meta_frames:
971
+ frame_indices = list(range(meta_frames))
972
+ else:
973
+ target_seconds = np.linspace(0,
974
+ duration,
975
+ num_samples,
976
+ endpoint=True)
977
+ frame_indices = [
978
+ min(max_frame_idx, int(math.ceil(t * video_fps)))
979
+ for t in target_seconds
980
+ ]
981
+
982
+ seen, uniq = set(), []
983
+ for idx in frame_indices:
984
+ if idx not in seen:
985
+ seen.add(idx)
986
+ uniq.append(idx)
987
+ if len(uniq) & 1:
988
+ uniq.append(uniq[-1])
989
+ frame_indices = uniq
990
+
991
+ full_second_idxs = [int(idx / video_fps) for idx in frame_indices]
992
+ timestamps_list = full_second_idxs[::2]
993
+ selected_timestamps = []
994
+ for idx in range(0, len(timestamps_list)):
995
+ selected_timestamps.append(timestamps_list[idx])
996
+ return selected_timestamps
997
+
998
+
999
+class Glm4vDummyInputsBuilder(BaseDummyInputsBuilder[Glm4vProcessingInfo]):
1000
+
1001
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1002
+ num_images = mm_counts.get("image", 0)
1003
+ num_videos = mm_counts.get("video", 0)
1004
+
1005
+ hf_config = self.info.get_hf_config()
1006
+ hf_processor = self.info.get_hf_processor()
1007
+ tokenizer = self.info.get_tokenizer()
1008
+
1009
+ image_token: str = hf_processor.image_token
1010
+ video_token_ids = [
1011
+ hf_config.video_start_token_id,
1012
+ hf_processor.video_token_id,
1013
+ hf_config.video_end_token_id,
1014
+ ]
1015
+ video_token = tokenizer.decode(video_token_ids)
1016
+
1017
+ return image_token * num_images + video_token * num_videos
1018
+
1019
+ def get_dummy_mm_data(
1020
+ self,
1021
+ seq_len: int,
1022
+ mm_counts: Mapping[str, int],
1023
+ ) -> MultiModalDataDict:
1024
+ num_images = mm_counts.get("image", 0)
1025
+ num_videos = mm_counts.get("video", 0)
1026
+
1027
+ target_width, target_height = (
1028
+ self.info.get_image_size_with_most_features())
1029
+ target_num_frames = self.info.get_num_frames_with_most_features(
1030
+ seq_len, mm_counts)
1031
+ return {
1032
+ "image":
1033
+ self._get_dummy_images(width=target_width,
1034
+ height=target_height,
1035
+ num_images=num_images),
1036
+ "video":
1037
+ self._get_dummy_videos(
1038
+ width=target_width,
1039
+ height=target_height,
1040
+ num_frames=target_num_frames,
1041
+ num_videos=num_videos,
1042
+ ),
1043
+ }
1044
+
1045
+ def _get_dummy_videos(
1046
+ self,
1047
+ *,
1048
+ width: int,
1049
+ height: int,
1050
+ num_frames: int,
1051
+ num_videos: int,
1052
+ ) -> list[VideoItem]:
1053
+ video = np.full((num_frames, width, height, 3), 255, dtype=np.uint8)
1054
+ video_items = []
1055
+ for i in range(num_videos):
1056
+ video_metadata = {
1057
+ "fps": 2.0,
1058
+ "duration": num_frames / 2.0,
1059
+ "total_num_frames": num_frames,
1060
+ "video_backend": "opencv",
1061
+ }
1062
+ video_item = (video.copy(), video_metadata)
1063
+ video_items.append(video_item)
1064
+
1065
+ return video_items
1066
+
1067
+
1068
+class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1069
+
1070
+ def _get_data_parser(self) -> MultiModalDataParser:
1071
+ return MultiModalDataParser(video_needs_metadata=True)
1072
+
1073
+ def _call_hf_processor(
1074
+ self,
1075
+ prompt: str,
1076
+ mm_data: Mapping[str, object],
1077
+ mm_kwargs: Mapping[str, object],
1078
+ tok_kwargs: Mapping[str, object],
1079
+ ) -> BatchFeature:
1080
+ mm_data = dict(mm_data)
1081
+ processor = self.info.get_hf_processor(**mm_kwargs)
1082
+
1083
+ # GLM-4.1V use `image_token_id` as video placeholder, we need to
1084
+ # replace it with `video_token_id` for video processing. So we
1085
+ # separate video processing from image processing.
1086
+ if ("videos" in mm_data and isinstance(mm_data["videos"], list)
1087
+ and len(mm_data["videos"]) > 0):
1088
+ video_grid_thw_lst = []
1089
+ pixel_values_videos_lst = []
1090
+ for item in mm_data.pop("videos", []):
1091
+ video_array, metadata = item
1092
+
1093
+ # FIXME(Isotr0py): Activate the below logic after we can disable
1094
+ # resampling from video loader backend.
1095
+ # assert metadata["total_num_frames"] == len(video_array), (
1096
+ # f"Total frames {metadata['total_num_frames']} does not "
1097
+ # f"match the length of video array {len(video_array)}.")
1098
+
1099
+ # NOTE: Temporary workaround for resampled videos.
1100
+ # this can cause a divergence with HF implementation if
1101
+ # the input video is resampled in advance.
1102
+
1103
+ if metadata["total_num_frames"] != len(video_array):
1104
+ logger.warning(
1105
+ "Total frames in metadata "
1106
+ "(%s) does not match the length of "
1107
+ "video array %s. This can "
1108
+ "be because the video is resampled "
1109
+ "in advance. This may cause "
1110
+ "a divergence with HF implementation.",
1111
+ metadata["total_num_frames"],
1112
+ len(video_array),
1113
+ )
1114
+ metadata["total_num_frames"] = len(video_array)
1115
+ metadata = VideoMetadata(**metadata)
1116
+
1117
+ video_mm_data = dict()
1118
+ video_mm_data["videos"] = [[video_array]]
1119
+ video_mm_data["video_metadata"] = [[metadata]]
1120
+
1121
+ video_outputs = super()._call_hf_processor(
1122
+ prompt="<|begin_of_video|><|video|><|end_of_video|>",
1123
+ mm_data=video_mm_data,
1124
+ mm_kwargs=mm_kwargs,
1125
+ tok_kwargs=tok_kwargs,
1126
+ )
1127
+ input_ids = video_outputs.pop("input_ids")
1128
+ input_ids[input_ids == processor.image_token_id] = (
1129
+ processor.video_token_id)
1130
+ video_placeholder = processor.tokenizer.batch_decode(
1131
+ input_ids)[0]
1132
+ prompt = prompt.replace(
1133
+ "<|begin_of_video|><|video|><|end_of_video|>",
1134
+ video_placeholder,
1135
+ )
1136
+
1137
+ grid_t = len(video_outputs["video_grid_thw"])
1138
+ _, grid_h, grid_w = video_outputs["video_grid_thw"][0]
1139
+ grid_thw = torch.tensor([[grid_t, grid_h, grid_w]])
1140
+
1141
+ video_grid_thw_lst.append(grid_thw)
1142
+ pixel_values_videos_lst.append(
1143
+ video_outputs["pixel_values_videos"])
1144
+ video_outputs = dict(
1145
+ pixel_values_videos=torch.cat(pixel_values_videos_lst),
1146
+ video_grid_thw=torch.cat(video_grid_thw_lst),
1147
+ )
1148
+ else:
1149
+ video_outputs = dict()
1150
+
1151
+ processed_outputs = super()._call_hf_processor(
1152
+ prompt=prompt,
1153
+ mm_data=mm_data,
1154
+ mm_kwargs=mm_kwargs,
1155
+ tok_kwargs=tok_kwargs,
1156
+ )
1157
+ combined_outputs = dict(
1158
+ processed_outputs,
1159
+ **video_outputs,
1160
+ )
1161
+ return BatchFeature(combined_outputs)
1162
+
1163
+ def _get_mm_fields_config(
1164
+ self,
1165
+ hf_inputs: BatchFeature,
1166
+ hf_processor_mm_kwargs: Mapping[str, object],
1167
+ ) -> Mapping[str, MultiModalFieldConfig]:
1168
+ return _qwen2vl_field_config(hf_inputs)
1169
+
1170
+ def _get_prompt_updates(
1171
+ self,
1172
+ mm_items: MultiModalDataItems,
1173
+ hf_processor_mm_kwargs: Mapping[str, Any],
1174
+ out_mm_kwargs: MultiModalKwargs,
1175
+ ) -> Sequence[PromptUpdate]:
1176
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1177
+ image_processor = self.info.get_image_processor(
1178
+ **hf_processor_mm_kwargs)
1179
+ tokenizer = self.info.get_tokenizer()
1180
+ hf_config = self.info.get_hf_config()
1181
+
1182
+ boi_token_id = hf_config.image_start_token_id
1183
+ eoi_token_id = hf_config.image_end_token_id
1184
+
1185
+ bov_token_id = hf_config.video_start_token_id
1186
+ eov_token_id = hf_config.video_end_token_id
1187
+
1188
+ merge_length = image_processor.merge_size**2
1189
+
1190
+ def get_image_replacement_glm4v(item_idx: int):
1191
+ grid_thw = out_mm_kwargs["image_grid_thw"][item_idx]
1192
+ assert isinstance(grid_thw, torch.Tensor)
1193
+
1194
+ num_tokens = int(grid_thw.prod()) // merge_length
1195
+ return [hf_processor.image_token_id] * num_tokens
1196
+
1197
+ def get_video_replacement_glm4v(item_idx: int):
1198
+ grid_thw = out_mm_kwargs["video_grid_thw"][item_idx]
1199
+ assert isinstance(grid_thw, torch.Tensor)
1200
+
1201
+ video, metadata = mm_items["video"][item_idx]
1202
+ timestamps = self.info._get_video_second_idx(metadata, len(video))
1203
+ frames_idx_token = [
1204
+ tokenizer.encode(str(i), add_special_tokens=False)
1205
+ for i in timestamps
1206
+ ]
1207
+ num_tokens_per_frame = int(grid_thw[1:].prod()) // merge_length
1208
+ placeholder = []
1209
+ placeholder.append(bov_token_id)
1210
+ for frame_idx in frames_idx_token:
1211
+ placeholder.append(boi_token_id)
1212
+ placeholder.extend([hf_processor.video_token_id] *
1213
+ num_tokens_per_frame)
1214
+ placeholder.append(eoi_token_id)
1215
+ placeholder.extend(frame_idx)
1216
+ placeholder.append(eov_token_id)
1217
+ return placeholder
1218
+
1219
+ return [
1220
+ PromptReplacement(
1221
+ modality="image",
1222
+ target=hf_processor.image_token,
1223
+ replacement=get_image_replacement_glm4v,
1224
+ ),
1225
+ PromptReplacement(
1226
+ modality="video",
1227
+ target="<|begin_of_video|><|video|><|end_of_video|>",
1228
+ replacement=get_video_replacement_glm4v,
1229
+ ),
1230
+ ]
1231
+
1232
+
1233
+@MULTIMODAL_REGISTRY.register_processor(
1234
+ Glm4vMultiModalProcessor,
1235
+ info=Glm4vProcessingInfo,
1236
+ dummy_inputs=Glm4vDummyInputsBuilder,
1237
+)
1238
+class Glm4vForConditionalGeneration(nn.Module, SupportsMultiModal,
1239
+ SupportsLoRA, SupportsPP):
1240
+ packed_modules_mapping = {
1241
+ "qkv_proj": [
1242
+ "q_proj",
1243
+ "k_proj",
1244
+ "v_proj",
1245
+ ],
1246
+ "gate_up_proj": [
1247
+ "gate_proj",
1248
+ "up_proj",
1249
+ ],
1250
+ }
1251
+
1252
+ # To ensure correct weight loading and mapping.
1253
+ hf_to_vllm_mapper = WeightsMapper(
1254
+ orig_to_new_prefix={
1255
+ "lm_head.": "language_model.lm_head.",
1256
+ "model.language_model.": "language_model.model.",
1257
+ "model.visual.": "visual.",
1258
+ })
1259
+
1260
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1261
+ super().__init__()
1262
+ config: Glm4vConfig = vllm_config.model_config.hf_config
1263
+ quant_config = vllm_config.quant_config
1264
+ multimodal_config = vllm_config.model_config.multimodal_config
1265
+
1266
+ self.config = config
1267
+ self.multimodal_config = multimodal_config
1268
+
1269
+ self.visual = Glm4vVisionTransformer(
1270
+ config.vision_config,
1271
+ norm_eps=getattr(config, "rms_norm_eps", 1e-5),
1272
+ quant_config=self._maybe_ignore_quant_config(quant_config),
1273
+ prefix=maybe_prefix(prefix, "visual"),
1274
+ )
1275
+
1276
+ self.language_model = init_vllm_registered_model(
1277
+ vllm_config=vllm_config,
1278
+ prefix=maybe_prefix(prefix, ""),
1279
+ architectures=["Glm4ForCausalLM"],
1280
+ )
1281
+
1282
+ self.make_empty_intermediate_tensors = (
1283
+ self.language_model.make_empty_intermediate_tensors)
1284
+
1285
+ def _maybe_ignore_quant_config(self, quant_config: QuantizationConfig):
1286
+ # GPTQ configs do not have a list of ignored modules, however AutoGPTQ
1287
+ # seems to avoid vision encoder sections for some models.
1288
+ if isinstance(quant_config, (GPTQConfig, GPTQMarlinConfig)):
1289
+ return None
1290
+ return quant_config
1291
+
1292
+ def _validate_and_reshape_mm_tensor(self, mm_input: object,
1293
+ name: str) -> torch.Tensor:
1294
+ if not isinstance(mm_input, (torch.Tensor, list)):
1295
+ raise ValueError(
1296
+ f"Incorrect type of {name}. Got type: {type(mm_input)}")
1297
+ if isinstance(mm_input, torch.Tensor):
1298
+ if mm_input.ndim == 2:
1299
+ return mm_input
1300
+ if mm_input.ndim != 3:
1301
+ raise ValueError(f"{name} should be 2D or batched 3D tensor. "
1302
+ f"Got ndim: {mm_input.ndim} "
1303
+ f"(shape={mm_input.shape})")
1304
+ return torch.concat(list(mm_input))
1305
+ else:
1306
+ return torch.concat(mm_input)
1307
+
1308
+ def _parse_and_validate_image_input(
1309
+ self, **kwargs: object) -> Optional[Glm4vImageInputs]:
1310
+ pixel_values = kwargs.pop("pixel_values", None)
1311
+ image_embeds = kwargs.pop("image_embeds", None)
1312
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
1313
+
1314
+ if pixel_values is None and image_embeds is None:
1315
+ return None
1316
+
1317
+ if pixel_values is not None:
1318
+ pixel_values = self._validate_and_reshape_mm_tensor(
1319
+ pixel_values, "image pixel values")
1320
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1321
+ image_grid_thw, "image grid_thw")
1322
+
1323
+ if not isinstance(pixel_values, (torch.Tensor, list)):
1324
+ raise ValueError("Incorrect type of image pixel values. "
1325
+ f"Got type: {type(pixel_values)}")
1326
+
1327
+ return Glm4vImagePixelInputs(
1328
+ type="pixel_values",
1329
+ pixel_values=pixel_values,
1330
+ image_grid_thw=image_grid_thw,
1331
+ )
1332
+
1333
+ if image_embeds is not None:
1334
+ image_embeds = self._validate_and_reshape_mm_tensor(
1335
+ image_embeds, "image embeds")
1336
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1337
+ image_grid_thw, "image grid_thw")
1338
+
1339
+ if not isinstance(image_embeds, torch.Tensor):
1340
+ raise ValueError("Incorrect type of image embeddings. "
1341
+ f"Got type: {type(image_embeds)}")
1342
+ return Glm4vImageEmbeddingInputs(
1343
+ type="image_embeds",
1344
+ image_embeds=image_embeds,
1345
+ image_grid_thw=image_grid_thw,
1346
+ )
1347
+
1348
+ def _parse_and_validate_video_input(
1349
+ self, **kwargs: object) -> Optional[Glm4vVideoInputs]:
1350
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1351
+ video_embeds = kwargs.pop("video_embeds", None)
1352
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
1353
+ if pixel_values_videos is None and video_embeds is None:
1354
+ return None
1355
+ if pixel_values_videos is not None:
1356
+ pixel_values_videos = self._validate_and_reshape_mm_tensor(
1357
+ pixel_values_videos, "video pixel values")
1358
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1359
+ video_grid_thw, "video grid_thw")
1360
+
1361
+ return Glm4vVideoPixelInputs(
1362
+ type="pixel_values_videos",
1363
+ # video_metadata=video_metadata,
1364
+ pixel_values_videos=pixel_values_videos,
1365
+ video_grid_thw=video_grid_thw,
1366
+ )
1367
+
1368
+ if video_embeds is not None:
1369
+ video_embeds = self._validate_and_reshape_mm_tensor(
1370
+ video_embeds, "video embeds")
1371
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1372
+ video_grid_thw, "video grid_thw")
1373
+
1374
+ if not isinstance(video_embeds, torch.Tensor):
1375
+ raise ValueError("Incorrect type of video embeddings. "
1376
+ f"Got type: {type(video_embeds)}")
1377
+ return Glm4vVideoEmbeddingInputs(
1378
+ type="video_embeds",
1379
+ video_embeds=video_embeds,
1380
+ video_grid_thw=video_grid_thw,
1381
+ )
1382
+
1383
+ def _process_image_input(
1384
+ self, image_input: Glm4vImageInputs) -> tuple[torch.Tensor, ...]:
1385
+ grid_thw = image_input["image_grid_thw"]
1386
+ assert grid_thw.ndim == 2
1387
+
1388
+ if image_input["type"] == "image_embeds":
1389
+ image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1390
+ else:
1391
+ pixel_values = image_input["pixel_values"].type(self.visual.dtype)
1392
+ image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
1393
+
1394
+ merge_size = self.visual.spatial_merge_size
1395
+ sizes = grid_thw.prod(-1) // merge_size // merge_size
1396
+ return image_embeds.split(sizes.tolist())
1397
+
1398
+ def _process_video_input(
1399
+ self, video_input: Glm4vVideoInputs) -> tuple[torch.Tensor, ...]:
1400
+ grid_thw = video_input["video_grid_thw"]
1401
+ assert grid_thw.ndim == 2
1402
+
1403
+ device = self.visual.device
1404
+ flat_grid_thw = torch.cat([
1405
+ torch.tensor([[1, h, w]] * t, device=device)
1406
+ for t, h, w in grid_thw
1407
+ ])
1408
+ if video_input["type"] == "video_embeds":
1409
+ video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1410
+ else:
1411
+ pixel_values_videos = video_input["pixel_values_videos"].type(
1412
+ self.visual.dtype)
1413
+ video_embeds = self.visual(pixel_values_videos,
1414
+ grid_thw=flat_grid_thw)
1415
+
1416
+ # Split concatenated embeddings for each video item.
1417
+ merge_size = self.visual.spatial_merge_size
1418
+ sizes = grid_thw.prod(-1) // merge_size // merge_size
1419
+
1420
+ return video_embeds.split(sizes.tolist())
1421
+
1422
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1423
+ mm_input_by_modality = {}
1424
+
1425
+ # Preserve the order of modalities if there are multiple of them
1426
+ # from the order of kwargs.
1427
+ for input_key in kwargs:
1428
+ if (input_key in ("pixel_values", "image_embeds")
1429
+ and "image" not in mm_input_by_modality):
1430
+ mm_input_by_modality["image"] = (
1431
+ self._parse_and_validate_image_input(**kwargs))
1432
+ if (input_key in ("pixel_values_videos", "video_embeds")
1433
+ and "video" not in mm_input_by_modality):
1434
+ mm_input_by_modality["video"] = (
1435
+ self._parse_and_validate_video_input(**kwargs))
1436
+ return mm_input_by_modality
1437
+
1438
+ def get_language_model(self) -> torch.nn.Module:
1439
+ return self.language_model
1440
+
1441
+ def get_multimodal_embeddings(
1442
+ self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
1443
+ mm_input_by_modality = self._parse_and_validate_multimodal_inputs(
1444
+ **kwargs)
1445
+ if not mm_input_by_modality:
1446
+ return None
1447
+
1448
+ # The result multimodal_embeddings is tuple of tensors, with each
1449
+ # tensor correspoending to a multimodal data item (image or video).
1450
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1451
+
1452
+ # NOTE: It is important to iterate over the keys in this dictionary
1453
+ # to preserve the order of the modalities.
1454
+ for modality in mm_input_by_modality:
1455
+ multimodal_input = mm_input_by_modality[modality]
1456
+ if modality == "image":
1457
+ vision_embeddings = self._process_image_input(multimodal_input)
1458
+ multimodal_embeddings += vision_embeddings
1459
+ if modality == "video":
1460
+ video_embeddings = self._process_video_input(multimodal_input)
1461
+ multimodal_embeddings += video_embeddings
1462
+ return multimodal_embeddings
1463
+
1464
+ def get_input_embeddings(
1465
+ self,
1466
+ input_ids: torch.Tensor,
1467
+ multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
1468
+ ) -> torch.Tensor:
1469
+ inputs_embeds = self.language_model.get_input_embeddings(input_ids)
1470
+ if (multimodal_embeddings is not None
1471
+ and len(multimodal_embeddings) != 0
1472
+ and all(embed.numel() > 0 for embed in multimodal_embeddings)):
1473
+ inputs_embeds = merge_multimodal_embeddings(
1474
+ input_ids,
1475
+ inputs_embeds,
1476
+ multimodal_embeddings,
1477
+ [self.config.image_token_id, self.config.video_token_id],
1478
+ )
1479
+ return inputs_embeds
1480
+
1481
+ def get_input_embeddings_v0(
1482
+ self,
1483
+ input_ids: torch.Tensor,
1484
+ image_input: Optional[Glm4vImageInputs] = None,
1485
+ video_input: Optional[Glm4vVideoInputs] = None,
1486
+ ) -> torch.Tensor:
1487
+ inputs_embeds = self.get_input_embeddings(input_ids)
1488
+ if image_input is not None:
1489
+ image_embeds = self._process_image_input(image_input)
1490
+ inputs_embeds = merge_multimodal_embeddings(
1491
+ input_ids,
1492
+ inputs_embeds,
1493
+ image_embeds,
1494
+ placeholder_token_id=self.config.image_token_id,
1495
+ )
1496
+
1497
+ if video_input is not None:
1498
+ video_embeds = self._process_video_input(video_input)
1499
+ inputs_embeds = merge_multimodal_embeddings(
1500
+ input_ids,
1501
+ inputs_embeds,
1502
+ video_embeds,
1503
+ placeholder_token_id=self.config.video_token_id,
1504
+ )
1505
+ return inputs_embeds
1506
+
1507
+ def forward(
1508
+ self,
1509
+ input_ids: torch.Tensor,
1510
+ positions: torch.Tensor,
1511
+ intermediate_tensors: Optional[IntermediateTensors] = None,
1512
+ inputs_embeds: Optional[torch.Tensor] = None,
1513
+ **kwargs: object,
1514
+ ) -> Union[torch.Tensor, IntermediateTensors]:
1515
+ """Run forward pass for GLM-4V.
1516
+
1517
+ Args:
1518
+ input_ids: Flattened (concatenated) input_ids corresponding to a
1519
+ batch.
1520
+ positions: Flattened (concatenated) position ids corresponding to a
1521
+ batch.
1522
+ **NOTE**: If mrope is enabled (default setting for GLM-4V
1523
+ opensource models), the shape will be `(3, seq_len)`,
1524
+ otherwise it will be `(seq_len,).
1525
+ pixel_values: Pixel values to be fed to a model.
1526
+ `None` if no images are passed.
1527
+ image_grid_thw: Tensor `(n_images, 3)` of image 3D grid in LLM.
1528
+ `None` if no images are passed.
1529
+ pixel_values_videos: Pixel values of videos to be fed to a model.
1530
+ `None` if no videos are passed.
1531
+ video_grid_thw: Tensor `(n_videos, 3)` of video 3D grid in LLM.
1532
+ `None` if no videos are passed.
1533
+ second_per_grid_ts: Tensor `(num_videos)` of video time interval (
1534
+ in seconds) for each grid along the temporal dimension in the
1535
+ 3D position IDs. `None` if no videos are passed.
1536
+ """
1537
+ if intermediate_tensors is not None:
1538
+ inputs_embeds = None
1539
+
1540
+ # NOTE: In v1, inputs_embeds is always generated at model runner from
1541
+ # `get_multimodal_embeddings` and `get_input_embeddings`, this
1542
+ # condition is only for v0 compatibility.
1543
+ elif inputs_embeds is None:
1544
+ image_input = self._parse_and_validate_image_input(**kwargs)
1545
+ video_input = self._parse_and_validate_video_input(**kwargs)
1546
+
1547
+ if image_input is None and video_input is None:
1548
+ inputs_embeds = None
1549
+ else:
1550
+ if uses_mrope(self.config):
1551
+ assert positions.ndim == 2 and positions.size(0) == 3, (
1552
+ "multimodal section rotary embedding requires "
1553
+ f"(3, seq_len) positions, but got {positions.size()}")
1554
+ inputs_embeds = self.get_input_embeddings_v0(
1555
+ input_ids,
1556
+ image_input=image_input,
1557
+ video_input=video_input)
1558
+ input_ids = None
1559
+
1560
+ hidden_states = self.language_model.model(
1561
+ input_ids=input_ids,
1562
+ positions=positions,
1563
+ intermediate_tensors=intermediate_tensors,
1564
+ inputs_embeds=inputs_embeds,
1565
+ )
1566
+ return hidden_states
1567
+
1568
+ def compute_logits(
1569
+ self,
1570
+ hidden_states: torch.Tensor,
1571
+ sampling_metadata: SamplingMetadata,
1572
+ ) -> Optional[torch.Tensor]:
1573
+ return self.language_model.compute_logits(hidden_states,
1574
+ sampling_metadata)
1575
+
1576
+ def load_weights(self, weights: Iterable[tuple[str,
1577
+ torch.Tensor]]) -> set[str]:
1578
+ loader = AutoWeightsLoader(self)
1579
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1580
+
1581
+ def get_mm_mapping(self) -> MultiModelKeys:
1582
+ """
1583
+ Get the module prefix in multimodal models
1584
+ """
1585
+ return MultiModelKeys.from_string_field(
1586
+ language_model="language_model",
1587
+ connector="visual.merger.",
1588
+ tower_model="visual.",
1589
+ )
vllm/model_executor/models/registry.py
+1
@@ -190,6 +190,7 @@ _MULTIMODAL_MODELS = {
190
"FuyuForCausalLM": ("fuyu", "FuyuForCausalLM"),
191
"Gemma3ForConditionalGeneration": ("gemma3_mm", "Gemma3ForConditionalGeneration"), # noqa: E501
192
"GLM4VForCausalLM": ("glm4v", "GLM4VForCausalLM"),
193
+ "Glm4vForConditionalGeneration": ("glm4_1v", "Glm4vForConditionalGeneration"), # noqa: E501
194
"GraniteSpeechForConditionalGeneration": ("granite_speech", "GraniteSpeechForConditionalGeneration"), # noqa: E501
195
"H2OVLChatModel": ("h2ovl", "H2OVLChatModel"),
196
"InternVLChatModel": ("internvl", "InternVLChatModel"),
vllm/multimodal/inputs.py
+5
-3
@@ -57,10 +57,12 @@ which are treated as image embeddings;
57
these are directly passed to the model without HF processing.
58
"""
59
60
-VideoItem: TypeAlias = Union[HfVideoItem, "torch.Tensor"]
60
+VideoItem: TypeAlias = Union[HfVideoItem, "torch.Tensor",
61
+ tuple[HfVideoItem, dict[str, Any]]]
62
"""
62
-A `transformers.image_utils.VideoInput` representing a single video
63
-item, which can be passed to a HuggingFace `VideoProcessor`.
63
+A `transformers.video_utils.VideoInput` representing a single video item.
64
+This can be passed to a HuggingFace `VideoProcessor`
65
+with `transformers.video_utils.VideoMetadata`.
66
67
Alternatively, a 3-D tensor or batch of 2-D tensors,
68
which are treated as video embeddings;
vllm/multimodal/parse.py
+40
-2
@@ -224,8 +224,14 @@ class ImageEmbeddingItems(EmbeddingItems):
224
225
class VideoProcessorItems(ProcessorBatchItems[HfVideoItem]):
226
227
- def __init__(self, data: Sequence[HfVideoItem]) -> None:
227
+ def __init__(
228
+ self,
229
+ data: Sequence[HfVideoItem],
230
+ metadata: Optional[Union[dict[str, Any],
231
+ list[Optional[dict[str, Any]]]]] = None,
232
+ ) -> None:
233
super().__init__(data, "video")
234
+ self.metadata = metadata
235
236
def get_num_frames(self, item_idx: int) -> int:
237
return len(self.get(item_idx))
@@ -320,6 +326,7 @@ class MultiModalDataParser:
326
*,
327
target_sr: Optional[float] = None,
328
audio_resample_method: Literal["librosa", "scipy"] = "librosa",
329
+ video_needs_metadata: bool = False,
330
) -> None:
331
super().__init__()
332
@@ -327,6 +334,7 @@ class MultiModalDataParser:
334
target_sr=target_sr,
335
method=audio_resample_method,
336
)
337
+ self.video_needs_metadata = video_needs_metadata
338
339
def _is_embeddings(
340
self, data: object
@@ -361,6 +369,21 @@ class MultiModalDataParser:
369
370
assert_never(audio)
371
372
+ def _get_video_with_metadata(
373
+ self,
374
+ video: VideoItem,
375
+ ) -> tuple[np.ndarray, Optional[dict[str, Any]]]:
376
+ if isinstance(video, tuple):
377
+ return video
378
+ if isinstance(video, list):
379
+ return np.array(video), None
380
+ if isinstance(video, np.ndarray):
381
+ return video, None
382
+ if isinstance(video, torch.Tensor):
383
+ return video.numpy(), None
384
+
385
+ assert_never(video)
386
+
387
def _parse_audio_data(
388
self,
389
data: ModalityData[AudioItem],
@@ -433,10 +456,25 @@ class MultiModalDataParser:
456
data_items = [data]
457
elif isinstance(data, (np.ndarray, torch.Tensor)):
458
data_items = [elem for elem in data]
459
+ elif isinstance(data, tuple) and len(data) == 2:
460
+ data_items = [data]
461
else:
462
data_items = data
463
439
- return VideoProcessorItems(data_items)
464
+ new_videos = list[tuple[np.ndarray, Optional[dict[str, Any]]]]()
465
+ metadata_lst: list[Optional[dict[str, Any]]] = []
466
+ for data_item in data_items:
467
+ video, metadata = self._get_video_with_metadata(data_item)
468
+ if self.video_needs_metadata:
469
+ new_videos.append((video, metadata))
470
+ metadata_lst.append(metadata)
471
+ else:
472
+ new_videos.append(video)
473
+
474
+ if not self.video_needs_metadata:
475
+ metadata = None
476
+
477
+ return VideoProcessorItems(new_videos, metadata=metadata_lst)
478
479
def _get_subparsers(self) -> Mapping[str, ModalityDataParser]:
480
return {
vllm/multimodal/video.py
+21
-6
@@ -24,6 +24,7 @@ def resize_video(frames: npt.NDArray, size: tuple[int, int]) -> npt.NDArray:
24
dtype=frames.dtype)
25
# lazy import cv2 to avoid bothering users who only use text models
26
import cv2
27
+
28
for i, frame in enumerate(frames):
29
resized_frame = cv2.resize(frame, (new_width, new_height))
30
resized_frames[i] = resized_frame
@@ -92,14 +93,16 @@ class OpenCVVideoBackend(VideoLoader):
93
continue
94
if not vr.isBackendBuiltIn(backend):
95
_, abi, api = vr.getStreamBufferedBackendPluginVersion(backend)
95
- if (abi < 1 or (abi == 1 and api < 2)):
96
+ if abi < 1 or (abi == 1 and api < 2):
97
continue
98
api_pref = backend
99
break
100
return api_pref
101
102
@classmethod
102
- def load_bytes(cls, data: bytes, num_frames: int = -1) -> npt.NDArray:
103
+ def load_bytes(cls,
104
+ data: bytes,
105
+ num_frames: int = -1) -> tuple[npt.NDArray, dict]:
106
import cv2
107
108
backend = cls().get_cv2_video_api()
@@ -108,6 +111,9 @@ class OpenCVVideoBackend(VideoLoader):
111
raise ValueError("Could not open video stream")
112
113
total_frames_num = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
114
+ original_fps = cap.get(cv2.CAP_PROP_FPS)
115
+ duration = total_frames_num / original_fps if original_fps > 0 else 0
116
+
117
full_read = num_frames == -1 or total_frames_num < num_frames
118
if full_read:
119
num_frames = total_frames_num
@@ -125,18 +131,27 @@ class OpenCVVideoBackend(VideoLoader):
131
132
i = 0
133
for idx in range(total_frames_num):
128
- ok = cap.grab() # next img
134
+ ok = cap.grab()
135
if not ok:
136
break
131
- if idx in frame_idx: # only decompress needed
137
+ if idx in frame_idx:
138
ret, frame = cap.retrieve()
139
if ret:
140
frames[i] = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
141
i += 1
136
- # we expect all frames loaded
142
+
143
assert i == num_frames, (f"Expected reading {num_frames} frames, "
144
f"but only loaded {i} frames from video.")
139
- return frames
145
+
146
+ # Use transformers transformers.video_utils.VideoMetadata format
147
+ metadata = {
148
+ "total_num_frames": total_frames_num,
149
+ "fps": original_fps,
150
+ "duration": duration,
151
+ "video_backend": "opencv"
152
+ }
153
+
154
+ return frames, metadata
155
156
157
class VideoMediaIO(MediaIO[npt.NDArray]):