main
py 2,392 lines 86.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Adapted from
5 # https://github.com/huggingface/transformers/blob/main/src/transformers/models/Glm4v/modeling_Glm4v.py
6 # Copyright 2025 The vLLM team.
7 # Copyright 2025 The ZhipuAI Team.
8 # Copyright 2025 The HuggingFace Inc. team.
9 # All rights reserved.
10 #
11 # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
12 # and OPT implementations in this library. It has been modified from its
13 # original forms to accommodate minor architectural differences compared
14 # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
15 #
16 # Licensed under the Apache License, Version 2.0 (the "License");
17 # you may not use this file except in compliance with the License.
18 # You may obtain a copy of the License at
19 #
20 # http://www.apache.org/licenses/LICENSE-2.0
21 #
22 # Unless required by applicable law or agreed to in writing, software
23 # distributed under the License is distributed on an "AS IS" BASIS,
24 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25 # See the License for the specific language governing permissions and
26 # limitations under the License.
27 """Inference-only GLM-4.1V & GLM-4.6V-Flash, AutoGLM-Phone-9B model
28 compatible with HuggingFace weights."""
29
30 import math
31 from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
32 from functools import partial
33 from typing import Annotated, Any, Literal, TypeAlias
34
35 import numpy as np
36 import torch
37 import torch.nn as nn
38 import torch.nn.functional as F
39 from einops import rearrange
40 from transformers import BatchFeature, Glm4vProcessor
41 from transformers.image_processing_base import ImageProcessingMixin
42 from transformers.models.glm4v.configuration_glm4v import (
43 Glm4vTextConfig,
44 Glm4vVisionConfig,
45 )
46 from transformers.models.glm4v.image_processing_glm4v import (
47 Glm4vImageProcessor,
48 smart_resize,
49 )
50 from transformers.models.glm4v.video_processing_glm4v import Glm4vVideoProcessor
51 from transformers.video_processing_utils import BaseVideoProcessor
52 from transformers.video_utils import VideoMetadata
53
54 from vllm.config import VllmConfig
55 from vllm.config.multimodal import MultiModalDummyOptions, VideoDummyOptions
56 from vllm.distributed import get_tensor_model_parallel_world_size, parallel_state
57 from vllm.distributed import utils as dist_utils
58 from vllm.inputs import MultiModalDataDict
59 from vllm.logger import init_logger
60 from vllm.model_executor.layers.attention import (
61 MMEncoderAttention,
62 )
63 from vllm.model_executor.layers.conv import Conv2dLayer, Conv3dLayer
64 from vllm.model_executor.layers.fusion.mm_input_norm import (
65 IdentityInputNorm,
66 build_mm_input_norm,
67 )
68 from vllm.model_executor.layers.layernorm import RMSNorm
69 from vllm.model_executor.layers.linear import (
70 ColumnParallelLinear,
71 MergedColumnParallelLinear,
72 QKVParallelLinear,
73 RowParallelLinear,
74 )
75 from vllm.model_executor.layers.quantization import QuantizationConfig
76 from vllm.model_executor.layers.quantization.compressed_tensors import (
77 compressed_tensors,
78 )
79 from vllm.model_executor.layers.rotary_embedding import get_rope
80 from vllm.model_executor.layers.rotary_embedding.common import (
81 ApplyRotaryEmb,
82 )
83 from vllm.model_executor.models.module_mapping import MultiModelKeys
84 from vllm.multimodal import MULTIMODAL_REGISTRY
85 from vllm.multimodal.inputs import (
86 MultiModalFeatureSpec,
87 MultiModalFieldConfig,
88 MultiModalKwargsItem,
89 MultiModalKwargsItems,
90 VideoItem,
91 )
92 from vllm.multimodal.parse import ImageSize, MultiModalDataItems, MultiModalDataParser
93 from vllm.multimodal.processing import (
94 BaseDummyInputsBuilder,
95 BaseMultiModalProcessor,
96 BaseProcessingInfo,
97 PromptReplacement,
98 PromptUpdate,
99 PromptUpdateDetails,
100 cached_encode,
101 )
102 from vllm.sequence import IntermediateTensors
103 from vllm.transformers_utils.processor import get_processor_cls_name_from_config
104 from vllm.transformers_utils.utils import convert_model_repo_to_path
105 from vllm.utils.tensor_schema import TensorSchema, TensorShape
106 from vllm.utils.torch_utils import async_tensor_h2d
107 from vllm.v1.attention.backends.registry import AttentionBackendEnum
108 from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
109
110 from ..layers.activation import SiluAndMul
111 from .interfaces import (
112 MultiModalEmbeddings,
113 SupportsEncoderCudaGraph,
114 SupportsLoRA,
115 SupportsMRoPE,
116 SupportsMultiModal,
117 SupportsPP,
118 )
119 from .qwen2_vl import _create_qwen2vl_field_factory
120 from .utils import (
121 AutoWeightsLoader,
122 WeightsMapper,
123 init_vllm_registered_model,
124 maybe_prefix,
125 )
126 from .vision import (
127 get_vit_attn_backend,
128 is_vit_use_data_parallel,
129 run_dp_sharded_mrope_vision_model,
130 )
131
132 logger = init_logger(__name__)
133
134 # For profile run
135 _MAX_FRAMES_PER_VIDEO = 600
136
137
138 def _to_video_metadata(metadata: Mapping[str, Any]) -> VideoMetadata:
139 return VideoMetadata(
140 **{k: metadata[k] for k in metadata if k != "do_sample_frames"}
141 )
142
143
144 # === Vision Inputs === #
145
146
147 class Glm4vImagePixelInputs(TensorSchema):
148 """Dimensions:
149 - np: Number of patches
150 - cpp: Number of channels * patch_size * patch_size
151 - ni: Number of images
152 - g: Grid dimensions (3 for grid_t, grid_h, grid_w)
153 """
154
155 type: Literal["pixel_values"] = "pixel_values"
156
157 pixel_values: Annotated[torch.Tensor, TensorShape("np", "cpp")]
158 image_grid_thw: Annotated[torch.Tensor, TensorShape("ni", 3)]
159
160
161 class Glm4vImageEmbeddingInputs(TensorSchema):
162 """Dimensions:
163 - f: Number of image features (varies based on image resolution)
164 - h: Hidden size (must match language model backbone)
165 - n: Number of images
166 - g: Grid dimensions (3 for grid_t, grid_h, grid_w)
167 """
168
169 type: Literal["image_embeds"] = "image_embeds"
170
171 image_embeds: Annotated[torch.Tensor, TensorShape("f", "h")]
172 image_grid_thw: Annotated[torch.Tensor, TensorShape("n", 3)]
173
174
175 Glm4vImageInputs: TypeAlias = Glm4vImagePixelInputs | Glm4vImageEmbeddingInputs
176
177
178 class Glm4vVideoPixelInputs(TensorSchema):
179 """Dimensions:
180 - np: Number of patches
181 - ctpp: Number of channels * temporal_patch_size *
182 patch_size * patch_size
183 - f: Number of frames
184 - g: Grid dimensions (3 for grid_t which is usually 1 for processed
185 video, grid_h, grid_w)
186 """
187
188 type: Literal["pixel_values_videos"] = "pixel_values_videos"
189
190 pixel_values_videos: Annotated[torch.Tensor, TensorShape("np", "ctpp")]
191 video_grid_thw: Annotated[torch.Tensor, TensorShape("f", 3)]
192
193
194 class Glm4vVideoEmbeddingInputs(TensorSchema):
195 """Dimensions:
196 - p: Number of video patches across all frames
197 - h: Hidden size (must match language model backbone)
198 - f: Number of frames
199 - g: Grid dimensions (3 for grid_t which is usually 1 for processed
200 video, grid_h, grid_w)
201 """
202
203 type: Literal["video_embeds"] = "video_embeds"
204
205 video_embeds: Annotated[torch.Tensor, TensorShape("p", "h")]
206 video_grid_thw: Annotated[torch.Tensor, TensorShape("f", 3)]
207
208
209 Glm4vVideoInputs: TypeAlias = Glm4vVideoPixelInputs | Glm4vVideoEmbeddingInputs
210
211 # ==== Vision Encoder ==== #
212
213
214 class Glm4vVisionMLP(nn.Module):
215 def __init__(
216 self,
217 in_features: int,
218 hidden_features: int,
219 bias: bool = False,
220 quant_config: QuantizationConfig | None = None,
221 prefix: str = "",
222 ):
223 super().__init__()
224 use_data_parallel = is_vit_use_data_parallel()
225 self.gate_up_proj = MergedColumnParallelLinear(
226 input_size=in_features,
227 output_sizes=[hidden_features] * 2,
228 bias=bias,
229 quant_config=quant_config,
230 prefix=f"{prefix}.gate_up_proj",
231 disable_tp=use_data_parallel,
232 )
233 self.down_proj = RowParallelLinear(
234 hidden_features,
235 in_features,
236 bias=bias,
237 quant_config=quant_config,
238 prefix=f"{prefix}.down_proj",
239 disable_tp=use_data_parallel,
240 )
241 self.act_fn = SiluAndMul()
242
243 def forward(self, x: torch.Tensor):
244 x, _ = self.gate_up_proj(x)
245 x = self.act_fn(x)
246 x, _ = self.down_proj(x)
247 return x
248
249
250 def all_gather_interleave(local_tensor, hidden_size: int, tp_size: int):
251 """All-gather the input tensor interleavely across model parallel group."""
252 import torch.distributed as dist
253
254 gathered_tensors = [torch.zeros_like(local_tensor) for _ in range(tp_size)]
255 dist.all_gather(
256 gathered_tensors,
257 local_tensor,
258 group=parallel_state.get_tp_group().device_group,
259 )
260
261 gathered_tensors_split = [
262 torch.split(tensor, hidden_size // tp_size, -1) for tensor in gathered_tensors
263 ]
264 ordered_tensors = [
265 tensor for pair in zip(*gathered_tensors_split) for tensor in pair
266 ]
267 result_tensor = torch.cat(ordered_tensors, dim=-1)
268 return result_tensor
269
270
271 class Glm4vVisionAttention(nn.Module):
272 def __init__(
273 self,
274 embed_dim: int,
275 num_heads: int,
276 projection_size: int,
277 quant_config: QuantizationConfig | None = None,
278 prefix: str = "",
279 ) -> None:
280 super().__init__()
281 # Per attention head and per partition values.
282 use_data_parallel = is_vit_use_data_parallel()
283 self.tp_size = (
284 1 if use_data_parallel else get_tensor_model_parallel_world_size()
285 )
286 self.tp_rank = (
287 0 if use_data_parallel else parallel_state.get_tensor_model_parallel_rank()
288 )
289 self.hidden_size_per_attention_head = dist_utils.divide(
290 projection_size, num_heads
291 )
292 self.num_attention_heads_per_partition = dist_utils.divide(
293 num_heads, self.tp_size
294 )
295
296 self.qkv = QKVParallelLinear(
297 hidden_size=embed_dim,
298 head_size=self.hidden_size_per_attention_head,
299 total_num_heads=num_heads,
300 total_num_kv_heads=num_heads,
301 bias=False,
302 quant_config=quant_config,
303 # Change qkv prefix to align with GLM-4.5V-FP8 quantization cfg
304 prefix=f"{prefix}.qkv_proj"
305 if isinstance(quant_config, compressed_tensors.CompressedTensorsConfig)
306 else f"{prefix}.qkv",
307 disable_tp=use_data_parallel,
308 )
309 self.proj = RowParallelLinear(
310 input_size=projection_size,
311 output_size=embed_dim,
312 quant_config=quant_config,
313 prefix=f"{prefix}.proj",
314 bias=False,
315 disable_tp=use_data_parallel,
316 )
317
318 self.attn = MMEncoderAttention(
319 num_heads=self.num_attention_heads_per_partition,
320 head_size=self.hidden_size_per_attention_head,
321 scale=self.hidden_size_per_attention_head**-0.5,
322 prefix=f"{prefix}.attn",
323 )
324
325 self.apply_rotary_emb = ApplyRotaryEmb(enforce_enable=True)
326
327 def split_qkv(self, qkv: torch.Tensor) -> tuple[torch.Tensor, ...]:
328 # [s, b, 3 * head * head_dim]
329 seq_len, bs, _ = qkv.shape
330
331 # [s, b, 3 * head * head_dim] -> 3 * [s, b, head * head_dim]
332 q, k, v = qkv.chunk(3, dim=2)
333
334 # 3 * [s, b, head * head_dim] -> 3 * [s, b, head, head_dim]
335 new_shape = (
336 seq_len,
337 bs,
338 self.num_attention_heads_per_partition,
339 self.hidden_size_per_attention_head,
340 )
341 q, k, v = (x.view(*new_shape) for x in (q, k, v))
342 return q, k, v
343
344 def forward(
345 self,
346 x: torch.Tensor,
347 cu_seqlens: torch.Tensor,
348 rotary_pos_emb_cos: torch.Tensor,
349 rotary_pos_emb_sin: torch.Tensor,
350 max_seqlen: torch.Tensor | None = None, # Only used for Flash Attention
351 ) -> torch.Tensor:
352 # [s, b, c] --> [s, b, head * 3 * head_dim]
353 x, _ = self.qkv(x)
354
355 # [s, b, 3 * head * head_dim] -> 3 * [s, b, head, head_dim]
356 q, k, v = self.split_qkv(x)
357
358 q, k, v = (rearrange(x, "s b ... -> b s ...").contiguous() for x in (q, k, v))
359 if rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:
360 # [2 * b, s, heads, head_dim]
361 qk_concat = torch.cat([q, k], dim=0)
362 qk_rotated = self.apply_rotary_emb(
363 qk_concat,
364 rotary_pos_emb_cos,
365 rotary_pos_emb_sin,
366 )
367 q, k = torch.chunk(qk_rotated, 2, dim=0)
368
369 context_layer = self.attn(
370 query=q,
371 key=k,
372 value=v,
373 cu_seqlens=cu_seqlens,
374 max_seqlen=max_seqlen,
375 )
376 context_layer = rearrange(context_layer, "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 def __init__(
384 self,
385 dim: int,
386 num_heads: int,
387 mlp_hidden_dim: int,
388 norm_layer: Callable[[int], nn.Module] | None = None,
389 quant_config: QuantizationConfig | None = None,
390 prefix: str = "",
391 ) -> None:
392 super().__init__()
393 if norm_layer is None:
394 norm_layer = partial(nn.LayerNorm, eps=1e-6)
395 self.norm1 = norm_layer(dim)
396 self.norm2 = norm_layer(dim)
397 self.attn = Glm4vVisionAttention(
398 embed_dim=dim,
399 num_heads=num_heads,
400 projection_size=dim,
401 quant_config=quant_config,
402 prefix=f"{prefix}.attn",
403 )
404 self.mlp = Glm4vVisionMLP(
405 dim,
406 mlp_hidden_dim,
407 bias=False,
408 quant_config=quant_config,
409 prefix=f"{prefix}.mlp",
410 )
411
412 def forward(
413 self,
414 x: torch.Tensor,
415 cu_seqlens: torch.Tensor,
416 rotary_pos_emb_cos: torch.Tensor,
417 rotary_pos_emb_sin: torch.Tensor,
418 max_seqlen: int | None = None, # Only used for Flash Attention
419 ) -> torch.Tensor:
420 x_attn = self.attn(
421 self.norm1(x),
422 cu_seqlens=cu_seqlens,
423 rotary_pos_emb_cos=rotary_pos_emb_cos,
424 rotary_pos_emb_sin=rotary_pos_emb_sin,
425 max_seqlen=max_seqlen,
426 )
427 x_fused_norm, residual = self.norm2(x, residual=x_attn)
428 x = residual + self.mlp(x_fused_norm)
429
430 return x
431
432
433 class Glm4vVisionPatchEmbed(nn.Module):
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 = Conv3dLayer(
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, self.patch_size)
458 x = self.proj(x).view(L, self.hidden_size)
459 return x
460
461
462 class Glm4vPatchMerger(nn.Module):
463 def __init__(
464 self,
465 d_model: int,
466 context_dim: int,
467 quant_config: QuantizationConfig | None = None,
468 bias: bool = False,
469 prefix: str = "",
470 ) -> None:
471 super().__init__()
472 use_data_parallel = is_vit_use_data_parallel()
473 self.hidden_size = d_model
474 self.proj = ColumnParallelLinear(
475 self.hidden_size,
476 self.hidden_size,
477 bias=bias,
478 gather_output=True,
479 quant_config=quant_config,
480 prefix=f"{prefix}.proj",
481 disable_tp=use_data_parallel,
482 )
483 self.post_projection_norm = nn.LayerNorm(self.hidden_size)
484 self.gate_up_proj = MergedColumnParallelLinear(
485 input_size=self.hidden_size,
486 output_sizes=[context_dim] * 2,
487 bias=bias,
488 quant_config=quant_config,
489 prefix=f"{prefix}.gate_up_proj",
490 disable_tp=use_data_parallel,
491 )
492 self.down_proj = RowParallelLinear(
493 context_dim,
494 self.hidden_size,
495 bias=bias,
496 quant_config=quant_config,
497 prefix=f"{prefix}.down_proj",
498 disable_tp=use_data_parallel,
499 )
500 self.act_fn = SiluAndMul()
501 self.extra_activation_func = nn.GELU()
502
503 def forward(self, x: torch.Tensor):
504 x, _ = self.proj(x)
505 x = self.extra_activation_func(self.post_projection_norm(x))
506 gate_up, _ = self.gate_up_proj(x)
507 x = self.act_fn(gate_up)
508 x, _ = self.down_proj(x)
509 return x
510
511
512 class Glm4vVisionEmbeddings(nn.Module):
513 def __init__(self, config: Glm4vVisionConfig):
514 super().__init__()
515 self.config = config
516 self.embed_dim = config.hidden_size
517 self.image_size = config.image_size
518 self.patch_size = config.patch_size
519
520 self.num_patches = (self.image_size // self.patch_size) ** 2
521 self.num_positions = self.num_patches
522 self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
523 self.register_buffer(
524 "position_ids",
525 torch.arange(self.num_positions).expand((1, -1)),
526 persistent=False,
527 )
528
529 def forward(
530 self, embeddings, lengths, image_shapes, h_coords, w_coords
531 ) -> torch.Tensor:
532 pos_embed_weight = self.position_embedding.weight
533 hidden_size = pos_embed_weight.shape[1]
534 total_seq = h_coords.shape[0]
535 device = pos_embed_weight.device
536
537 # Move coordinates to correct device. The only caller already hands
538 # these over pinned + non-blocking, so this is normally a no-op.
539 h_coords, w_coords = h_coords.to(device), w_coords.to(device)
540
541 # Handle empty sequence case
542 if total_seq == 0:
543 adapted_pos_embed = torch.empty(
544 0, hidden_size, device=device, dtype=pos_embed_weight.dtype
545 )
546 else:
547 # Prepare 2D position embedding
548 orig_size_sq = pos_embed_weight.shape[0]
549 orig_size = int(orig_size_sq**0.5)
550 pos_embed_2d = (
551 pos_embed_weight.view(orig_size, orig_size, hidden_size)
552 .permute(2, 0, 1)
553 .unsqueeze(0)
554 .to(device=device, dtype=torch.float32)
555 )
556
557 # Calculate target dimensions for each patch. `lengths` and
558 # `image_shapes` are host data, so expand them with numpy and move
559 # the result across once rather than per-image.
560 # Shapes are cycled: in data parallel mode some GPUs might not
561 # have all image shapes.
562 shapes_np = np.asarray(image_shapes)
563 shape_idx = np.arange(len(lengths)) % shapes_np.shape[0]
564 target_h = async_tensor_h2d(
565 np.repeat(shapes_np[shape_idx, 1], lengths),
566 device=device,
567 dtype=torch.float32,
568 )
569 target_w = async_tensor_h2d(
570 np.repeat(shapes_np[shape_idx, 2], lengths),
571 device=device,
572 dtype=torch.float32,
573 )
574
575 # Normalize coordinates to [-1, 1] range for grid_sample
576 h_coords = h_coords.to(device=device, dtype=torch.float32)
577 w_coords = w_coords.to(device=device, dtype=torch.float32)
578 norm_w = ((w_coords + 0.5) / target_w) * 2 - 1
579 norm_h = ((h_coords + 0.5) / target_h) * 2 - 1
580
581 # Create sampling grid
582 grid = torch.stack((norm_w, norm_h), dim=-1).unsqueeze(0).unsqueeze(2)
583
584 # Perform bicubic interpolation
585 interpolated_embed_fp32 = F.grid_sample(
586 pos_embed_2d,
587 grid,
588 mode="bicubic",
589 align_corners=False,
590 padding_mode="border",
591 )
592
593 # Reshape and convert back to original dtype
594 adapted_pos_embed_fp32 = (
595 interpolated_embed_fp32.squeeze(0).squeeze(-1).permute(1, 0)
596 )
597 adapted_pos_embed = adapted_pos_embed_fp32.to(pos_embed_weight.dtype).to(
598 embeddings.device
599 )
600
601 # Add adapted position encoding to embeddings
602 embeddings = embeddings + adapted_pos_embed
603 return embeddings
604
605
606 class Glm4vVisionTransformer(nn.Module):
607 hf_to_vllm_mapper = WeightsMapper(
608 orig_to_new_stacked={
609 ".attn.q.": (".attn.qkv.", "q"),
610 ".attn.k.": (".attn.qkv.", "k"),
611 ".attn.v.": (".attn.qkv.", "v"),
612 ".gate_proj": (".gate_up_proj", 0),
613 ".up_proj": (".gate_up_proj", 1),
614 }
615 )
616
617 def __init__(
618 self,
619 text_config: Glm4vTextConfig,
620 vision_config: Glm4vVisionConfig,
621 norm_eps: float = 1e-6,
622 quant_config: QuantizationConfig | None = None,
623 input_norm: nn.Module | None = None,
624 prefix: str = "",
625 ) -> None:
626 super().__init__()
627
628 use_data_parallel = is_vit_use_data_parallel()
629 self.tp_size = (
630 1 if use_data_parallel else get_tensor_model_parallel_world_size()
631 )
632
633 patch_size = vision_config.patch_size
634 temporal_patch_size = vision_config.temporal_patch_size
635 in_channels = vision_config.in_channels
636 depth = vision_config.depth
637 self.hidden_size = vision_config.hidden_size
638 self.num_heads = vision_config.num_heads
639
640 self.patch_size = vision_config.patch_size
641 self.spatial_merge_size = vision_config.spatial_merge_size
642 self.out_hidden_size = vision_config.out_hidden_size
643
644 self.patch_embed = Glm4vVisionPatchEmbed(
645 patch_size=patch_size,
646 temporal_patch_size=temporal_patch_size,
647 in_channels=in_channels,
648 hidden_size=self.hidden_size,
649 )
650 self.input_norm = input_norm if input_norm is not None else IdentityInputNorm()
651
652 norm_layer = partial(RMSNorm, eps=norm_eps)
653 head_dim = self.hidden_size // self.num_heads
654 self.rotary_pos_emb = get_rope(
655 head_size=head_dim,
656 max_position=8192,
657 is_neox_style=True,
658 rope_parameters={"partial_rotary_factor": 0.5},
659 )
660 self.blocks = nn.ModuleList(
661 [
662 Glm4vVisionBlock(
663 dim=self.hidden_size,
664 num_heads=self.num_heads,
665 mlp_hidden_dim=vision_config.out_hidden_size,
666 norm_layer=norm_layer,
667 quant_config=quant_config,
668 prefix=f"{prefix}.blocks.{layer_idx}",
669 )
670 for layer_idx in range(depth)
671 ]
672 )
673 self.merger = Glm4vPatchMerger(
674 d_model=vision_config.out_hidden_size,
675 context_dim=vision_config.intermediate_size,
676 quant_config=quant_config,
677 bias=False,
678 prefix=f"{prefix}.merger",
679 )
680 self.embeddings = Glm4vVisionEmbeddings(vision_config)
681
682 self.post_conv_layernorm = RMSNorm(
683 vision_config.hidden_size, eps=vision_config.rms_norm_eps
684 )
685 self.downsample = Conv2dLayer(
686 in_channels=vision_config.hidden_size,
687 out_channels=vision_config.out_hidden_size,
688 kernel_size=vision_config.spatial_merge_size,
689 stride=vision_config.spatial_merge_size,
690 )
691 self.post_layernorm = RMSNorm(
692 vision_config.hidden_size, eps=vision_config.rms_norm_eps
693 )
694
695 self.attn_backend = get_vit_attn_backend(
696 head_size=head_dim,
697 dtype=torch.get_default_dtype(),
698 )
699
700 @property
701 def dtype(self) -> torch.dtype:
702 return self.patch_embed.proj.weight.dtype
703
704 @property
705 def device(self) -> torch.device:
706 return self.patch_embed.proj.weight.device
707
708 def rot_pos_emb(
709 self, grid_thw: list[list[int]]
710 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
711 device = self.device
712 pos_ids = []
713 for t, h, w in grid_thw:
714 hpos_ids = torch.arange(h, device=device).unsqueeze(1).expand(-1, w)
715 wpos_ids = torch.arange(w, device=device).unsqueeze(0).expand(h, -1)
716 hpos_ids = (
717 hpos_ids.reshape(
718 h // self.spatial_merge_size,
719 self.spatial_merge_size,
720 w // self.spatial_merge_size,
721 self.spatial_merge_size,
722 )
723 .permute(0, 2, 1, 3)
724 .flatten()
725 )
726 wpos_ids = (
727 wpos_ids.reshape(
728 h // self.spatial_merge_size,
729 self.spatial_merge_size,
730 w // self.spatial_merge_size,
731 self.spatial_merge_size,
732 )
733 .permute(0, 2, 1, 3)
734 .flatten()
735 )
736 pos_ids.append(torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1))
737 pos_ids = torch.cat(pos_ids, dim=0)
738 max_grid_size = max(max(h, w) for _, h, w in grid_thw)
739
740 # Use pre-computed cos_sin_cache from RotaryEmbedding
741 cos, sin = self.rotary_pos_emb.get_cos_sin(max_grid_size)
742
743 cos_combined = cos[pos_ids].flatten(1)
744 sin_combined = sin[pos_ids].flatten(1)
745 return cos_combined, sin_combined, pos_ids
746
747 def compute_attn_mask_seqlen(
748 self,
749 cu_seqlens: torch.Tensor,
750 ) -> torch.Tensor | None:
751 max_seqlen = None
752 if self.attn_backend in {
753 AttentionBackendEnum.FLASH_ATTN,
754 AttentionBackendEnum.ROCM_AITER_FA,
755 AttentionBackendEnum.TRITON_ATTN,
756 }:
757 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
758 return max_seqlen
759
760 def pos_embeds_interpolate(self, grid_thw: list[list[int]]) -> torch.Tensor:
761 """Pre-compute absolute position embeddings for all input samples.
762 The original `self.embeddings` fused token embeddings and position embeddings
763 in one call, which prevented preparing position embeddings as static metadata
764 required by CUDA graph capture / replay. This method decouples the two by
765 feeding an all-zero token tensor to `self.embeddings`. The module therefore only
766 performs bicubic interpolation based on the coordinates and returns pure
767 position embeddings. These are cached in `prepare_encoder_metadata` and later
768 added to the patch tokens in `forward` via `x = x + pos_embeds`, keeping the
769 forward graph compatible with CUDA graph replay. Coordinate generation matches
770 `rot_pos_emb` exactly to guarantee spatial alignment.
771 """
772 device = self.embeddings.position_embedding.weight.device
773 dtype = self.dtype
774 all_embeds = []
775
776 for t, h, w in grid_thw:
777 # Use the same coordinate generation logic as rot_pos_emb
778 # to ensure consistent positional embedding interpolation
779 h_coords = torch.arange(h).unsqueeze(1).expand(-1, w)
780 w_coords = torch.arange(w).unsqueeze(0).expand(h, -1)
781 h_coords = (
782 h_coords.reshape(
783 h // self.spatial_merge_size,
784 self.spatial_merge_size,
785 w // self.spatial_merge_size,
786 self.spatial_merge_size,
787 )
788 .permute(0, 2, 1, 3)
789 .flatten()
790 )
791 w_coords = (
792 w_coords.reshape(
793 h // self.spatial_merge_size,
794 self.spatial_merge_size,
795 w // self.spatial_merge_size,
796 self.spatial_merge_size,
797 )
798 .permute(0, 2, 1, 3)
799 .flatten()
800 )
801
802 lengths = [h * w] * t
803 image_shapes = [[t, h, w]]
804
805 # Build the coordinates on the host (cheap integer math) but move
806 # them across pinned + non-blocking, so the consumer's
807 # `.to(device)` below is a no-op rather than a blocking H2D.
808 h_coords_repeated = async_tensor_h2d(h_coords.repeat(t), device=device)
809 w_coords_repeated = async_tensor_h2d(w_coords.repeat(t), device=device)
810
811 embeds = self.embeddings(
812 embeddings=torch.zeros(
813 h * w * t, self.hidden_size, device=device, dtype=dtype
814 ),
815 lengths=lengths,
816 image_shapes=image_shapes,
817 h_coords=h_coords_repeated,
818 w_coords=w_coords_repeated,
819 )
820 all_embeds.append(embeds)
821
822 return torch.cat(all_embeds, dim=0).to(dtype)
823
824 def prepare_encoder_metadata(
825 self,
826 grid_thw_list: list[list[int]],
827 *,
828 max_batch_size: int | None = None,
829 max_frames_per_batch: int | None = None,
830 max_seqlen_override: int | None = None,
831 device: torch.device | None = None,
832 ) -> dict[str, torch.Tensor | None]:
833 """Compute encoder metadata from grid_thw_list.
834
835 Shared by the eager forward path, CUDA graph capture, and
836 CUDA graph replay to avoid duplicated implementation.
837
838 Args:
839 grid_thw_list: Grid configurations as list of [t, h, w].
840 max_batch_size: If set, pad cu_seqlens to this size
841 (needed for CUDA graph capture/replay).
842 max_frames_per_batch: If set, overrides max_batch_size for
843 cu_seqlens padding. For video inputs each item contributes
844 T attention sequences (frames); this sizes the buffer to
845 the total frame budget so video replays never overflow.
846 max_seqlen_override: If set, use this value for max_seqlen
847 instead of computing from cu_seqlens (needed for CUDA
848 graph capture to cover worst-case replay scenarios).
849 device: Device to place tensors on. Defaults to self.device.
850
851 """
852 if device is None:
853 device = self.device
854
855 metadata: dict[str, torch.Tensor | None] = {}
856
857 # Positional embeddings
858 metadata["pos_embeds"] = self.pos_embeds_interpolate(grid_thw_list)
859 rotary_cos, rotary_sin, _ = self.rot_pos_emb(grid_thw_list)
860 metadata["rotary_pos_emb_cos"] = rotary_cos
861 metadata["rotary_pos_emb_sin"] = rotary_sin
862
863 # cu_seqlens from grid_thw
864 grid_thw_np = np.array(grid_thw_list, dtype=np.int32)
865 patches_per_frame = grid_thw_np[:, 1] * grid_thw_np[:, 2]
866 cu_seqlens = np.repeat(patches_per_frame, grid_thw_np[:, 0]).cumsum(
867 dtype=np.int32
868 )
869 cu_seqlens = np.concatenate([np.zeros(1, dtype=np.int32), cu_seqlens])
870
871 # Pad cu_seqlens to the required number of sequences.
872 # For videos each item contributes T frames = T attention sequences,
873 # so the total can exceed max_batch_size. max_frames_per_batch
874 # overrides the pad target when set.
875 pad_to = (
876 max_frames_per_batch if max_frames_per_batch is not None else max_batch_size
877 )
878 if pad_to is not None:
879 num_seqs = len(cu_seqlens) - 1
880 if num_seqs < pad_to:
881 cu_seqlens = np.concatenate(
882 [
883 cu_seqlens,
884 np.full(
885 pad_to - num_seqs,
886 cu_seqlens[-1],
887 dtype=np.int32,
888 ),
889 ]
890 )
891
892 # sequence_lengths (backend-specific)
893 metadata["sequence_lengths"] = MMEncoderAttention.maybe_compute_seq_lens(
894 self.attn_backend, cu_seqlens, device
895 )
896
897 # max_seqlen
898 if max_seqlen_override is not None:
899 max_seqlen_val = max_seqlen_override
900 else:
901 max_seqlen_val = MMEncoderAttention.compute_max_seqlen(
902 self.attn_backend, cu_seqlens
903 )
904 # Keep max_seqlen on CPU: attention wrappers call .item() on it,
905 # and having it on GPU would capture a wasteful D2H copy in CUDA
906 # graphs without changing behavior (the scalar is baked at capture).
907 metadata["max_seqlen"] = torch.tensor(max_seqlen_val, dtype=torch.int32)
908
909 # Recompute cu_seqlens (backend-specific transformation)
910 metadata["cu_seqlens"] = MMEncoderAttention.maybe_recompute_cu_seqlens(
911 self.attn_backend,
912 cu_seqlens,
913 self.hidden_size,
914 self.tp_size,
915 device,
916 )
917
918 return metadata
919
920 def forward(
921 self,
922 x: torch.Tensor,
923 grid_thw: torch.Tensor | list[list[int]],
924 *,
925 encoder_metadata: dict[str, torch.Tensor] | None = None,
926 ) -> torch.Tensor:
927 if encoder_metadata is None:
928 if not isinstance(grid_thw, list):
929 grid_thw = grid_thw.tolist()
930 encoder_metadata = self.prepare_encoder_metadata(grid_thw)
931
932 # patchify
933 x = self.input_norm(x.to(device=self.device), self.dtype)
934 x = self.patch_embed(x)
935 x = self.post_conv_layernorm(x)
936
937 pos_embeds = encoder_metadata["pos_embeds"]
938 x = x + pos_embeds
939
940 # transformers
941 x = x.unsqueeze(1)
942 for blk in self.blocks:
943 x = blk(
944 x,
945 cu_seqlens=encoder_metadata["cu_seqlens"],
946 rotary_pos_emb_cos=encoder_metadata["rotary_pos_emb_cos"],
947 rotary_pos_emb_sin=encoder_metadata["rotary_pos_emb_sin"],
948 max_seqlen=encoder_metadata["max_seqlen"],
949 )
950
951 # adapter
952 x = self.post_layernorm(x)
953
954 x = x.view(-1, self.spatial_merge_size, self.spatial_merge_size, x.shape[-1])
955 x = x.permute(0, 3, 1, 2)
956 x = self.downsample(x).view(-1, self.out_hidden_size)
957 x = self.merger(x)
958
959 return x
960
961 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
962 loader = AutoWeightsLoader(self)
963 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
964
965
966 class Glm4vProcessingInfo(BaseProcessingInfo):
967 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
968 return {"image": None, "video": 1}
969
970 def get_image_processor(self, **kwargs: object) -> Glm4vImageProcessor:
971 return self.get_hf_processor(**kwargs).image_processor
972
973 def get_video_processor(self, **kwargs: object) -> Glm4vVideoProcessor:
974 return self.get_hf_processor(**kwargs).video_processor
975
976 def _get_processor_class_name(self) -> str | None:
977 return get_processor_cls_name_from_config(
978 convert_model_repo_to_path(self.ctx.model_config.model),
979 revision=self.ctx.model_config.revision,
980 )
981
982 @staticmethod
983 def _get_longest_edge(size: Any, config_name: str) -> int:
984 if isinstance(size, dict):
985 longest_edge = size.get("longest_edge")
986 else:
987 longest_edge = getattr(size, "longest_edge", None)
988
989 if longest_edge is None:
990 raise ValueError(f"{config_name} must define longest_edge")
991
992 return int(longest_edge)
993
994 def get_mm_max_tokens_per_item(
995 self,
996 seq_len: int,
997 mm_counts: Mapping[str, int],
998 ) -> Mapping[str, int] | None:
999 processor_class_name = self._get_processor_class_name()
1000 if processor_class_name == "Glm4vProcessor":
1001 return None
1002
1003 if processor_class_name is None:
1004 processor = self.get_hf_processor()
1005 if isinstance(processor, Glm4vProcessor):
1006 return None
1007
1008 result: dict[str, int] = {}
1009
1010 if mm_counts.get("image", 0) > 0:
1011 result["image"] = self.get_max_image_tokens()
1012
1013 if mm_counts.get("video", 0) > 0:
1014 max_pixels = self._get_video_max_pixels()
1015
1016 vision_config = self.get_hf_config().vision_config
1017 temporal_patch_size = vision_config.temporal_patch_size
1018 patch_size = vision_config.patch_size
1019 merge_size = vision_config.spatial_merge_size
1020
1021 max_vision_tokens = max_pixels // (
1022 temporal_patch_size * patch_size**2 * merge_size**2
1023 )
1024
1025 # GLMGA supports up to 640 frames (max_frames).
1026 max_grid_t = 640 // temporal_patch_size
1027
1028 tokenizer = self.get_tokenizer()
1029 max_ts_tokens = max(
1030 len(tokenizer.encode(f"{t:.1f} seconds", add_special_tokens=False))
1031 for t in range(min(max_grid_t, 300))
1032 )
1033
1034 result["video"] = max_vision_tokens + max_grid_t * (2 + max_ts_tokens) + 2
1035
1036 return result
1037
1038 def get_data_parser(self):
1039 return MultiModalDataParser(
1040 video_needs_metadata=True,
1041 expected_hidden_size=self._get_expected_hidden_size(),
1042 )
1043
1044 def _get_vision_info(
1045 self,
1046 *,
1047 image_width: int,
1048 image_height: int,
1049 num_frames: int = 16,
1050 do_resize: bool = True,
1051 max_image_pixels: int = 28 * 28 * 2 * 30000,
1052 ) -> tuple[ImageSize, int]:
1053 hf_config = self.get_hf_config()
1054 vision_config = hf_config.vision_config
1055 patch_size = vision_config.patch_size
1056 merge_size = vision_config.spatial_merge_size
1057 temporal_patch_size = vision_config.temporal_patch_size
1058 if do_resize:
1059 resized_height, resized_width = smart_resize(
1060 num_frames=num_frames
1061 if num_frames > temporal_patch_size
1062 else temporal_patch_size,
1063 height=image_height,
1064 width=image_width,
1065 factor=patch_size * merge_size,
1066 max_pixels=max_image_pixels,
1067 )
1068 preprocessed_size = ImageSize(width=resized_width, height=resized_height)
1069 else:
1070 preprocessed_size = ImageSize(width=image_width, height=image_height)
1071
1072 # NOTE: Frames are padded to be divisible by `temporal_patch_size`
1073 # https://github.com/huggingface/transformers/blob/v5.13.0/src/transformers/models/qwen2_vl/video_processing_qwen2_vl.py#L249-L252
1074 padded_num_frames = num_frames + (-num_frames % temporal_patch_size)
1075
1076 grid_t = max(padded_num_frames // temporal_patch_size, 1)
1077 grid_h = preprocessed_size.height // patch_size
1078 grid_w = preprocessed_size.width // patch_size
1079
1080 num_patches = grid_t * grid_h * grid_w
1081 num_vision_tokens = num_patches // (merge_size**2)
1082
1083 return preprocessed_size, num_vision_tokens
1084
1085 def _get_image_max_pixels(self) -> int:
1086 """Read max_pixels from the HF image processor config.
1087
1088 Despite the name, ``longest_edge`` is a pixel **area** (total pixel
1089 count), not an edge length. The HF processor passes it directly to
1090 ``smart_resize`` as the ``max_pixels`` argument, which constrains
1091 ``t_bar * h_bar * w_bar <= max_pixels``.
1092 """
1093 mm_kwargs = self.ctx.get_merged_mm_kwargs({})
1094 if (override_max_pixels := mm_kwargs.get("max_pixels")) is not None:
1095 return int(override_max_pixels)
1096
1097 image_processor_config = self.ctx.get_hf_image_processor_config()
1098 if not image_processor_config.get("size"):
1099 image_processor_config, _ = ImageProcessingMixin.get_image_processor_dict(
1100 self.ctx.model_config.model,
1101 revision=self.ctx.model_config.revision,
1102 )
1103 size = image_processor_config["size"]
1104 if override_size := mm_kwargs.get("size"):
1105 size = size | override_size
1106
1107 return self._get_longest_edge(size, "GLM4V image processor size")
1108
1109 def _get_video_max_pixels(self) -> int:
1110 mm_kwargs = self.ctx.get_merged_mm_kwargs({})
1111 if (override_max_pixels := mm_kwargs.get("max_pixels")) is not None:
1112 return int(override_max_pixels)
1113
1114 video_processor_config, _ = BaseVideoProcessor.get_video_processor_dict(
1115 self.ctx.model_config.model,
1116 revision=self.ctx.model_config.revision,
1117 )
1118 size = video_processor_config["size"]
1119 if override_size := mm_kwargs.get("size"):
1120 size = size | override_size
1121
1122 return self._get_longest_edge(size, "GLM4V video processor size")
1123
1124 def get_image_size_with_most_features(self) -> ImageSize:
1125 # Use num_frames=1 for single-image budget estimation.
1126 # _get_vision_info defaults to num_frames=16 (video), which
1127 # makes smart_resize constrain 16*H*W <= max_pixels, vastly
1128 # underestimating the spatial budget for a single image and
1129 # causing encoder cache overflow for large images
1130 # (see https://github.com/vllm-project/vllm/issues/34040).
1131 max_image_size, _ = self._get_vision_info(
1132 image_width=9999999,
1133 image_height=9999999,
1134 num_frames=1,
1135 max_image_pixels=self._get_image_max_pixels(),
1136 )
1137 return max_image_size
1138
1139 def get_num_image_tokens(
1140 self,
1141 *,
1142 image_width: int,
1143 image_height: int,
1144 ) -> int:
1145 _, num_image_tokens = self._get_vision_info(
1146 image_width=image_width,
1147 image_height=image_height,
1148 num_frames=1,
1149 max_image_pixels=self._get_image_max_pixels(),
1150 )
1151 return num_image_tokens
1152
1153 def get_max_image_tokens(self) -> int:
1154 target_width, target_height = self.get_image_size_with_most_features()
1155
1156 return self.get_num_image_tokens(
1157 image_width=target_width,
1158 image_height=target_height,
1159 )
1160
1161 def _get_max_video_frames(self, max_tokens: int) -> int:
1162 target_width, target_height = self.get_image_size_with_most_features()
1163
1164 max_video_pixels = self._get_video_max_pixels()
1165 num_frames_with_most_features = 1
1166 max_vision_tokens = 0
1167
1168 for num_frames in range(1, _MAX_FRAMES_PER_VIDEO + 1):
1169 _, num_vision_tokens = self._get_vision_info(
1170 image_width=target_width,
1171 image_height=target_height,
1172 num_frames=num_frames,
1173 max_image_pixels=max_video_pixels,
1174 )
1175
1176 if (
1177 0 < num_vision_tokens <= max_tokens
1178 and num_vision_tokens > max_vision_tokens
1179 ):
1180 num_frames_with_most_features = num_frames
1181 max_vision_tokens = num_vision_tokens
1182
1183 return num_frames_with_most_features
1184
1185 def get_num_frames_with_most_features(
1186 self,
1187 seq_len: int,
1188 mm_counts: Mapping[str, int],
1189 ) -> int:
1190 max_images = mm_counts.get("image", 0)
1191
1192 max_image_tokens = self.get_max_image_tokens() * max_images
1193 return self._get_max_video_frames(seq_len - max_image_tokens)
1194
1195 def _get_video_second_idx_glm4v(
1196 self, metadata: dict[str, Any], total_frames: int
1197 ) -> list[int]:
1198 video_processor = self.get_video_processor()
1199
1200 video_fps = metadata.get("fps", video_processor.fps)
1201 meta_frames = metadata.get("total_num_frames", total_frames)
1202 max_frame_idx = meta_frames - 1
1203 duration = metadata.get("duration", round(max_frame_idx / video_fps) + 1)
1204 do_sample_frames = metadata["do_sample_frames"]
1205 if not do_sample_frames:
1206 frame_indices = metadata["frames_indices"]
1207 else:
1208 if duration <= video_processor.max_duration:
1209 n = int(math.floor(duration * video_processor.fps))
1210 frame_indices = [
1211 min(
1212 max_frame_idx,
1213 int(math.ceil(i * video_fps / video_processor.fps)),
1214 )
1215 for i in range(n)
1216 ]
1217 else:
1218 num_samples = int(video_processor.max_duration * video_processor.fps)
1219 if num_samples >= meta_frames:
1220 frame_indices = list(range(meta_frames))
1221 else:
1222 target_seconds = np.linspace(
1223 0, duration, num_samples, endpoint=True
1224 )
1225 frame_indices = [
1226 min(max_frame_idx, int(math.ceil(t * video_fps)))
1227 for t in target_seconds
1228 ]
1229
1230 seen, uniq = set(), []
1231 for idx in frame_indices:
1232 if idx not in seen:
1233 seen.add(idx)
1234 uniq.append(idx)
1235 if len(uniq) & 1:
1236 uniq.append(uniq[-1])
1237 frame_indices = uniq
1238
1239 full_second_idxs = [int(idx / video_fps) for idx in frame_indices]
1240 timestamps_list = full_second_idxs[::2]
1241 selected_timestamps = []
1242 for idx in range(0, len(timestamps_list)):
1243 selected_timestamps.append(timestamps_list[idx])
1244 return selected_timestamps
1245
1246 def _get_video_second_idx_glm46v(
1247 self, metadata: dict[str, Any], total_frames: int
1248 ) -> list[int]:
1249 video_processor = self.get_video_processor()
1250
1251 video_fps = metadata["fps"]
1252 meta_frames = metadata.get("total_num_frames", total_frames)
1253 max_frame_idx = meta_frames - 1
1254 duration = metadata.get("duration", round(max_frame_idx / video_fps) + 1)
1255
1256 do_sample_frames = metadata.get("do_sample_frames", True)
1257 if not do_sample_frames:
1258 frame_indices = metadata["frames_indices"]
1259 else:
1260 DYNAMIC_FPS_THRES = {30: 3, 300: 1, 2400: 0.5}
1261 MAX_FRAME_COUNT_DYNAMIC = 640
1262 MAX_DURATION = 2400
1263
1264 effective_duration = min(duration, MAX_DURATION)
1265 if effective_duration <= 30:
1266 target_fps = DYNAMIC_FPS_THRES[30]
1267 elif effective_duration <= 300:
1268 target_fps = DYNAMIC_FPS_THRES[300]
1269 else:
1270 target_fps = DYNAMIC_FPS_THRES[2400]
1271
1272 temporal_patch_size = getattr(video_processor, "temporal_patch_size", 1)
1273 extract_t = int(effective_duration * target_fps * temporal_patch_size)
1274 extract_t = min(extract_t, MAX_FRAME_COUNT_DYNAMIC)
1275
1276 duration_per_frame = 1 / video_fps
1277 timestamps = [i * duration_per_frame for i in range(meta_frames)]
1278 max_second = int(duration)
1279
1280 if meta_frames < extract_t:
1281 frame_indices = np.linspace(
1282 0, meta_frames - 1, extract_t, dtype=int
1283 ).tolist()
1284 else:
1285 frame_indices = []
1286 current_second = 0.0
1287 inv_fps = 1 / (temporal_patch_size * target_fps)
1288 for frame_index in range(meta_frames):
1289 if timestamps[frame_index] >= current_second:
1290 current_second += inv_fps
1291 frame_indices.append(frame_index)
1292 if current_second >= max_second:
1293 break
1294
1295 if len(frame_indices) < extract_t:
1296 if len(frame_indices) == 0:
1297 start, end = 0, max(meta_frames - 1, 0)
1298 else:
1299 start, end = frame_indices[0], frame_indices[-1]
1300 frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
1301 elif len(frame_indices) > extract_t:
1302 frame_indices = np.linspace(
1303 0, meta_frames - 1, extract_t, dtype=int
1304 ).tolist()
1305
1306 seen, uniq = set(), []
1307 for idx in frame_indices:
1308 if idx not in seen:
1309 seen.add(idx)
1310 uniq.append(idx)
1311
1312 if len(uniq) & 1:
1313 uniq.append(uniq[-1])
1314
1315 frame_indices = uniq
1316 full_second_idxs = [int(idx / video_fps) for idx in frame_indices]
1317 timestamps_list = full_second_idxs[::2]
1318 selected_timestamps = []
1319 for idx in range(len(timestamps_list)):
1320 selected_timestamps.append(timestamps_list[idx])
1321 return selected_timestamps
1322
1323 def _is_glmga_model(self, processor: object) -> bool:
1324 """Detect GLMGA variant via its Glmga sub-processors."""
1325 for attr in ("image_processor", "video_processor"):
1326 sub = getattr(processor, attr, None)
1327 if sub and "Glmga" in type(sub).__name__:
1328 return True
1329 return False
1330
1331 def _get_video_second_idx_glmga(
1332 self, metadata: dict[str, Any], total_frames: int
1333 ) -> list[int]:
1334 """Fixed fps=2 frame selection matching GlmgaVideoProcessor.sample_frames."""
1335 video_processor = self.get_video_processor()
1336
1337 video_fps = metadata["fps"]
1338 meta_frames = metadata.get("total_num_frames", total_frames)
1339 max_frame_idx = meta_frames - 1
1340 duration = metadata.get("duration", round(max_frame_idx / video_fps) + 1)
1341
1342 do_sample_frames = metadata.get("do_sample_frames", True)
1343 if not do_sample_frames:
1344 frame_indices = metadata["frames_indices"]
1345 else:
1346 target_fps = 2
1347 max_frames = getattr(video_processor, "max_frames", 640)
1348 extract_t = int(duration * target_fps)
1349 extract_t = min(extract_t, max_frames)
1350
1351 duration_per_frame = 1 / video_fps
1352 timestamps = [i * duration_per_frame for i in range(meta_frames)]
1353
1354 if meta_frames < extract_t:
1355 frame_indices = [
1356 math.floor(i * meta_frames / extract_t) for i in range(extract_t)
1357 ]
1358 else:
1359 frame_indices = []
1360 current_second = 0.0
1361 inv_fps = 1 / target_fps
1362 for frame_index in range(meta_frames):
1363 if timestamps[frame_index] >= current_second:
1364 current_second += inv_fps
1365 frame_indices.append(frame_index)
1366 if current_second >= duration - inv_fps:
1367 break
1368
1369 if len(frame_indices) < extract_t:
1370 if len(frame_indices) == 0:
1371 start, end = 0, max(meta_frames - 1, 0)
1372 else:
1373 start, end = frame_indices[0], frame_indices[-1]
1374 frame_indices = np.linspace(start, end, extract_t, dtype=int).tolist()
1375 elif len(frame_indices) > extract_t:
1376 frame_indices = np.linspace(
1377 0, meta_frames - 1, extract_t, dtype=int
1378 ).tolist()
1379
1380 seen, uniq = set(), []
1381 for idx in frame_indices:
1382 if idx not in seen:
1383 seen.add(idx)
1384 uniq.append(idx)
1385
1386 if len(uniq) & 1:
1387 uniq.append(uniq[-1])
1388
1389 frame_indices = uniq
1390 full_second_idxs = [int(idx / video_fps) for idx in frame_indices]
1391 timestamps_list = full_second_idxs[::2]
1392 return list(timestamps_list)
1393
1394 def _construct_video_placeholder(
1395 self,
1396 video_array: np.ndarray,
1397 metadata: dict[str, Any],
1398 grid_thw: torch.Tensor,
1399 ) -> list[int]:
1400 hf_processor = self.get_hf_processor()
1401 tokenizer = self.get_tokenizer()
1402 image_processor = hf_processor.image_processor
1403
1404 hf_config = self.get_hf_config()
1405 boi_token_id = hf_config.image_start_token_id
1406 eoi_token_id = hf_config.image_end_token_id
1407 bov_token_id = hf_config.video_start_token_id
1408 eov_token_id = hf_config.video_end_token_id
1409 merge_length = image_processor.merge_size**2
1410
1411 assert isinstance(grid_thw, torch.Tensor)
1412
1413 if isinstance(hf_processor, Glm4vProcessor):
1414 timestamps = self._get_video_second_idx_glm4v(metadata, len(video_array))
1415 elif self._is_glmga_model(hf_processor):
1416 timestamps = self._get_video_second_idx_glmga(metadata, len(video_array))
1417 else:
1418 timestamps = self._get_video_second_idx_glm46v(metadata, len(video_array))
1419
1420 timestamp_format = (
1421 "{}" if isinstance(hf_processor, Glm4vProcessor) else "{:.1f} seconds"
1422 )
1423 frames_idx_token = [
1424 tokenizer.encode(timestamp_format.format(i), add_special_tokens=False)
1425 for i in timestamps
1426 ]
1427 T, H, W = grid_thw
1428 num_tokens_per_frame = int(H * W) // merge_length
1429 placeholder = []
1430 placeholder.append(bov_token_id)
1431 frame_embed_token_id = hf_processor.image_token_id
1432 for frame_idx in frames_idx_token:
1433 placeholder.append(boi_token_id)
1434 placeholder.extend([frame_embed_token_id] * num_tokens_per_frame)
1435 placeholder.append(eoi_token_id)
1436 placeholder.extend(frame_idx)
1437 placeholder.append(eov_token_id)
1438
1439 return placeholder
1440
1441
1442 class Glm4vDummyInputsBuilder(BaseDummyInputsBuilder[Glm4vProcessingInfo]):
1443 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1444 num_images = mm_counts.get("image", 0)
1445 num_videos = mm_counts.get("video", 0)
1446
1447 hf_config = self.info.get_hf_config()
1448 tokenizer = self.info.get_tokenizer()
1449
1450 image_token = tokenizer.decode([hf_config.image_token_id])
1451 video_token_ids = [
1452 hf_config.video_start_token_id,
1453 hf_config.video_token_id,
1454 hf_config.video_end_token_id,
1455 ]
1456 video_token = tokenizer.decode(video_token_ids)
1457
1458 return image_token * num_images + video_token * num_videos
1459
1460 def get_dummy_mm_data(
1461 self,
1462 seq_len: int,
1463 mm_counts: Mapping[str, int],
1464 mm_options: MultiModalDummyOptions,
1465 ) -> MultiModalDataDict:
1466 target_width, target_height = self.info.get_image_size_with_most_features()
1467 target_num_frames = self.info.get_num_frames_with_most_features(
1468 seq_len, mm_counts
1469 )
1470
1471 return {
1472 "image": self._get_dummy_images(
1473 width=target_width,
1474 height=target_height,
1475 num_images=mm_counts.get("image", 0),
1476 overrides=mm_options.get("image"),
1477 ),
1478 "video": self._get_dummy_videos(
1479 width=target_width,
1480 height=target_height,
1481 num_frames=target_num_frames,
1482 num_videos=mm_counts.get("video", 0),
1483 overrides=mm_options.get("video"),
1484 ),
1485 }
1486
1487 def _get_dummy_videos(
1488 self,
1489 *,
1490 width: int,
1491 height: int,
1492 num_frames: int,
1493 num_videos: int,
1494 overrides: VideoDummyOptions | None = None,
1495 ) -> list[VideoItem]:
1496 # GLM 4.6V requires at least 2 frames
1497 num_frames = max(num_frames, 2)
1498 if overrides and overrides.num_frames:
1499 overrides.num_frames = max(overrides.num_frames, 2)
1500
1501 videos = super()._get_dummy_videos(
1502 width=width,
1503 height=height,
1504 num_frames=num_frames,
1505 num_videos=num_videos,
1506 overrides=overrides,
1507 )
1508 videos = [v.copy() for v in videos]
1509
1510 video_items: list[VideoItem] = []
1511 for video in videos:
1512 video_num_frames = video.shape[0]
1513 video_metadata = {
1514 "fps": 2.0,
1515 "duration": video_num_frames / 2.0,
1516 "total_num_frames": video_num_frames,
1517 "frames_indices": list(range(video_num_frames)),
1518 "video_backend": "opencv",
1519 "do_sample_frames": False,
1520 }
1521 video_items.append((video, video_metadata))
1522
1523 return video_items
1524
1525
1526 class Glm4vMultiModalProcessor(BaseMultiModalProcessor[Glm4vProcessingInfo]):
1527 @staticmethod
1528 def _get_direct_path_inputs(
1529 mm_data: Mapping[str, object],
1530 mm_kwargs: Mapping[str, object],
1531 ) -> tuple[Mapping[str, object], Mapping[str, object]]:
1532 prepared_data = dict(mm_data)
1533 prepared_kwargs = dict(mm_kwargs)
1534
1535 videos = prepared_data.get("videos")
1536 if not (isinstance(videos, list) and len(videos) > 0):
1537 return prepared_data, prepared_kwargs
1538
1539 hf_videos = []
1540 hf_video_metadata = []
1541 for item in videos:
1542 if isinstance(item, tuple) and len(item) == 2:
1543 video_array, metadata = item
1544 hf_videos.append(video_array)
1545 if isinstance(metadata, VideoMetadata):
1546 hf_video_metadata.append(metadata)
1547 elif isinstance(metadata, Mapping):
1548 hf_video_metadata.append(_to_video_metadata(metadata))
1549 if "do_sample_frames" in metadata:
1550 prepared_kwargs["do_sample_frames"] = metadata[
1551 "do_sample_frames"
1552 ]
1553 elif metadata is not None:
1554 raise TypeError(
1555 "Video metadata must be a mapping or VideoMetadata, "
1556 f"got {type(metadata)}"
1557 )
1558 else:
1559 hf_videos.append(item)
1560
1561 prepared_data["videos"] = hf_videos
1562 if hf_video_metadata:
1563 prepared_data["video_metadata"] = hf_video_metadata
1564 prepared_kwargs["return_metadata"] = True
1565
1566 return prepared_data, prepared_kwargs
1567
1568 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1569 return self.dummy_inputs.get_dummy_text(mm_counts)
1570
1571 def _apply_hf_processor_main(
1572 self,
1573 mm_items: MultiModalDataItems,
1574 hf_kwargs: Mapping[str, object],
1575 ) -> BatchFeature:
1576 hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1577 mm_items, hf_kwargs
1578 )
1579
1580 if not hf_data:
1581 return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1582
1583 prompt_text = hf_data.pop("text")
1584 assert isinstance(prompt_text, str)
1585
1586 processor = self.info.get_hf_processor(**hf_kwargs)
1587
1588 if not isinstance(processor, Glm4vProcessor):
1589 prepared_data, prepared_kwargs = self._get_direct_path_inputs(
1590 hf_data, hf_kwargs
1591 )
1592 processed_data = self.info.ctx.call_hf_processor(
1593 self.info.get_hf_processor(**prepared_kwargs),
1594 dict(text=prompt_text, **prepared_data),
1595 prepared_kwargs,
1596 )
1597 return self._finalize_hf_mm_data(
1598 hf_data, hf_kwargs, passthrough_data, processed_data
1599 )
1600
1601 if (
1602 "videos" in hf_data
1603 and isinstance(videos := hf_data["videos"], list)
1604 and len(videos) > 0
1605 ):
1606 # Videos are processed item-wise below; remove them from hf_data so
1607 # the combined call at the end does not re-sample the raw frames.
1608 del hf_data["videos"]
1609 video_grid_thw_lst = []
1610 pixel_values_videos_lst = []
1611 for item in videos:
1612 video_array, metadata = item
1613
1614 # don't update mm_kwargs inplace
1615 video_mm_kwargs = dict(**hf_kwargs)
1616 video_mm_kwargs["do_sample_frames"] = metadata.get(
1617 "do_sample_frames", True
1618 )
1619
1620 video_mm_data = dict()
1621 video_mm_data["videos"] = [[video_array]]
1622 video_mm_data["video_metadata"] = [[_to_video_metadata(metadata)]]
1623
1624 video_outputs = self.info.ctx.call_hf_processor(
1625 self.info.get_hf_processor(**video_mm_kwargs),
1626 dict(
1627 text="<|begin_of_video|><|video|><|end_of_video|>",
1628 **video_mm_data,
1629 ),
1630 video_mm_kwargs,
1631 )
1632 input_ids = video_outputs.pop("input_ids")
1633
1634 input_ids[input_ids == processor.image_token_id] = (
1635 processor.video_token_id
1636 )
1637 video_placeholder = processor.tokenizer.batch_decode(input_ids)[0]
1638 prompt_text = prompt_text.replace(
1639 "<|begin_of_video|><|video|><|end_of_video|>",
1640 video_placeholder,
1641 1,
1642 )
1643
1644 video_grid_thw_lst.append(video_outputs["video_grid_thw"])
1645 pixel_values_videos_lst.append(video_outputs["pixel_values_videos"])
1646 video_outputs = dict(
1647 pixel_values_videos=torch.cat(pixel_values_videos_lst),
1648 video_grid_thw=torch.cat(video_grid_thw_lst),
1649 )
1650 else:
1651 video_outputs = dict()
1652
1653 processed_data = self.info.ctx.call_hf_processor(
1654 self.info.get_hf_processor(**hf_kwargs),
1655 dict(text=prompt_text, **hf_data),
1656 hf_kwargs,
1657 )
1658 input_ids = processed_data["input_ids"]
1659 input_ids[input_ids == processor.video_token_id] = processor.image_token_id
1660 processed_data["input_ids"] = input_ids
1661
1662 processed_data.update(video_outputs)
1663 return self._finalize_hf_mm_data(
1664 hf_data, hf_kwargs, passthrough_data, processed_data
1665 )
1666
1667 def _get_mm_fields_config(
1668 self,
1669 hf_inputs: BatchFeature,
1670 hf_processor_mm_kwargs: Mapping[str, object],
1671 ) -> Mapping[str, MultiModalFieldConfig]:
1672 return _create_qwen2vl_field_factory(
1673 self.info.get_hf_config().vision_config.spatial_merge_size
1674 )(hf_inputs)
1675
1676 def _get_prompt_updates(
1677 self,
1678 mm_items: MultiModalDataItems,
1679 hf_processor_mm_kwargs: Mapping[str, Any],
1680 out_mm_kwargs: MultiModalKwargsItems,
1681 ) -> Sequence[PromptUpdate]:
1682 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1683 image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
1684 tokenizer = self.info.get_tokenizer()
1685 vocab = tokenizer.get_vocab()
1686
1687 merge_length = image_processor.merge_size**2
1688
1689 def get_image_replacement(item_idx: int):
1690 out_item = out_mm_kwargs["image"][item_idx]
1691 grid_thw = out_item["image_grid_thw"].data
1692 assert isinstance(grid_thw, torch.Tensor)
1693
1694 num_tokens = int(grid_thw.prod()) // merge_length
1695 return [hf_processor.image_token_id] * num_tokens
1696
1697 def get_video_replacement(item_idx: int):
1698 out_item = out_mm_kwargs["video"][item_idx]
1699 grid_thw = out_item["video_grid_thw"].data
1700 assert isinstance(grid_thw, torch.Tensor)
1701
1702 video, metadata = mm_items["video"][item_idx]
1703 placeholder = self.info._construct_video_placeholder(
1704 video, metadata, grid_thw
1705 )
1706 return PromptUpdateDetails.select_token_id(
1707 placeholder,
1708 embed_token_id=hf_processor.image_token_id,
1709 )
1710
1711 return [
1712 PromptReplacement(
1713 modality="image",
1714 target=cached_encode(
1715 tokenizer, hf_processor.image_token, add_special_tokens=False
1716 ),
1717 replacement=get_image_replacement,
1718 ),
1719 PromptReplacement(
1720 modality="video",
1721 target=[
1722 vocab["<|begin_of_video|>"],
1723 vocab["<|video|>"],
1724 vocab["<|end_of_video|>"],
1725 ],
1726 replacement=get_video_replacement,
1727 ),
1728 ]
1729
1730
1731 @MULTIMODAL_REGISTRY.register_processor(
1732 Glm4vMultiModalProcessor,
1733 info=Glm4vProcessingInfo,
1734 dummy_inputs=Glm4vDummyInputsBuilder,
1735 )
1736 class Glm4vForConditionalGeneration(
1737 nn.Module,
1738 SupportsMultiModal,
1739 SupportsEncoderCudaGraph,
1740 SupportsLoRA,
1741 SupportsPP,
1742 SupportsMRoPE,
1743 ):
1744 packed_modules_mapping = {
1745 "qkv_proj": [
1746 "q_proj",
1747 "k_proj",
1748 "v_proj",
1749 ],
1750 "gate_up_proj": ["gate_up_proj"],
1751 }
1752
1753 # To ensure correct weight loading and mapping.
1754 hf_to_vllm_mapper = WeightsMapper(
1755 orig_to_new_prefix={
1756 "lm_head.": "language_model.lm_head.",
1757 "model.language_model.": "language_model.model.",
1758 "model.visual.": "visual.",
1759 }
1760 )
1761
1762 supports_encoder_tp_data = True
1763 supports_tower_connector_lora = True
1764 supports_mm_device_do_normalize = True
1765
1766 @classmethod
1767 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1768 if modality.startswith("image"):
1769 return "<|begin_of_image|><|image|><|end_of_image|>"
1770 if modality.startswith("video"):
1771 return "<|begin_of_video|><|video|><|end_of_video|>"
1772
1773 raise ValueError("Only image or video modality is supported")
1774
1775 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1776 super().__init__()
1777 config = vllm_config.model_config.hf_config
1778 quant_config = vllm_config.quant_config
1779 multimodal_config = vllm_config.model_config.multimodal_config
1780
1781 self.config = config
1782 self.model_config = vllm_config.model_config
1783 self.multimodal_config = multimodal_config
1784 assert multimodal_config is not None
1785 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1786 self.is_multimodal_pruning_enabled = (
1787 multimodal_config.is_multimodal_pruning_enabled()
1788 )
1789
1790 with self._mark_tower_model(vllm_config, {"image", "video"}):
1791 self.visual = Glm4vVisionTransformer(
1792 config.text_config,
1793 config.vision_config,
1794 norm_eps=getattr(config, "rms_norm_eps", 1e-5),
1795 quant_config=quant_config,
1796 input_norm=build_mm_input_norm(self.model_config),
1797 prefix=maybe_prefix(prefix, "visual"),
1798 )
1799
1800 if config.model_type in ("glm4v", "glm_ocr", "glmga"):
1801 architectures = ["Glm4ForCausalLM"]
1802 elif config.model_type == "glm4v_moe":
1803 architectures = ["Glm4MoeForCausalLM"]
1804 else:
1805 architectures = None
1806
1807 with self._mark_language_model(vllm_config):
1808 self.language_model = init_vllm_registered_model(
1809 vllm_config=vllm_config,
1810 hf_config=config.text_config,
1811 prefix=maybe_prefix(prefix, "language_model"),
1812 architectures=architectures,
1813 )
1814
1815 self.make_empty_intermediate_tensors = (
1816 self.language_model.make_empty_intermediate_tensors
1817 )
1818
1819 def _parse_and_validate_image_input(
1820 self, **kwargs: object
1821 ) -> Glm4vImageInputs | None:
1822 pixel_values = kwargs.pop("pixel_values", None)
1823 image_embeds = kwargs.pop("image_embeds", None)
1824 image_grid_thw = kwargs.pop("image_grid_thw", None)
1825
1826 if pixel_values is None and image_embeds is None:
1827 return None
1828
1829 if pixel_values is not None:
1830 return Glm4vImagePixelInputs(
1831 type="pixel_values",
1832 pixel_values=pixel_values,
1833 image_grid_thw=image_grid_thw,
1834 )
1835
1836 if image_embeds is not None:
1837 return Glm4vImageEmbeddingInputs(
1838 type="image_embeds",
1839 image_embeds=image_embeds,
1840 image_grid_thw=image_grid_thw,
1841 )
1842 raise AssertionError("image input must contain pixels or embeddings")
1843
1844 def _parse_and_validate_video_input(
1845 self, **kwargs: object
1846 ) -> Glm4vVideoInputs | None:
1847 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1848 video_embeds = kwargs.pop("video_embeds", None)
1849 video_grid_thw = kwargs.pop("video_grid_thw", None)
1850
1851 if pixel_values_videos is None and video_embeds is None:
1852 return None
1853
1854 if pixel_values_videos is not None:
1855 return Glm4vVideoPixelInputs(
1856 type="pixel_values_videos",
1857 pixel_values_videos=pixel_values_videos,
1858 video_grid_thw=video_grid_thw,
1859 )
1860
1861 if video_embeds is not None:
1862 return Glm4vVideoEmbeddingInputs(
1863 type="video_embeds",
1864 video_embeds=video_embeds,
1865 video_grid_thw=video_grid_thw,
1866 )
1867 raise AssertionError("video input must contain pixels or embeddings")
1868
1869 def _process_image_input(
1870 self, image_input: Glm4vImageInputs
1871 ) -> tuple[torch.Tensor, ...]:
1872 grid_thw = image_input["image_grid_thw"]
1873 assert grid_thw.ndim == 2
1874
1875 if image_input["type"] == "image_embeds":
1876 image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1877 else:
1878 pixel_values = image_input["pixel_values"]
1879 if self.use_data_parallel:
1880 return run_dp_sharded_mrope_vision_model(
1881 self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d"
1882 )
1883 else:
1884 image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
1885
1886 merge_size = self.visual.spatial_merge_size
1887 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1888 return image_embeds.split(sizes)
1889
1890 def _process_video_input(
1891 self, video_input: Glm4vVideoInputs
1892 ) -> tuple[torch.Tensor, ...]:
1893 grid_thw = video_input["video_grid_thw"]
1894 assert grid_thw.ndim == 2
1895
1896 if video_input["type"] == "video_embeds":
1897 video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1898 else:
1899 pixel_values_videos = video_input["pixel_values_videos"]
1900 if self.use_data_parallel:
1901 return run_dp_sharded_mrope_vision_model(
1902 self.visual,
1903 pixel_values_videos,
1904 grid_thw.tolist(),
1905 rope_type="rope_3d",
1906 )
1907 else:
1908 video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw)
1909
1910 # Split concatenated embeddings for each video item.
1911 merge_size = self.visual.spatial_merge_size
1912 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1913 return video_embeds.split(sizes)
1914
1915 # -- SupportsEncoderCudaGraph protocol methods --
1916
1917 def get_encoder_cudagraph_config(self):
1918 from vllm.v1.worker.encoder_cudagraph_defs import (
1919 EncoderCudaGraphConfig,
1920 )
1921
1922 # When EVS pruning is enabled, embed_multimodal post-processes both
1923 # image and video embeddings (mrope positions are appended for image,
1924 # prune+append for video). The encoder CUDA graph path bypasses that
1925 # post-process, producing inconsistent embedding formats vs eager. So
1926 # disable CUDA graph for all modalities when pruning is on.
1927 modalities = [] if self.is_multimodal_pruning_enabled else ["image", "video"]
1928
1929 # Compute max_frames_per_video for budget sizing.
1930 max_frames = self.get_max_frames_per_video() if "video" in modalities else 1
1931
1932 return EncoderCudaGraphConfig(
1933 modalities=modalities,
1934 buffer_keys=[
1935 "pixel_values",
1936 "pos_embeds",
1937 "rotary_pos_emb_cos",
1938 "rotary_pos_emb_sin",
1939 "cu_seqlens",
1940 "max_seqlen",
1941 "sequence_lengths",
1942 ],
1943 out_hidden_size=self.visual.out_hidden_size,
1944 max_frames_per_video=max_frames,
1945 )
1946
1947 def get_input_modality(
1948 self,
1949 mm_kwargs: dict[str, Any],
1950 ) -> str:
1951 if "image_grid_thw" in mm_kwargs:
1952 return "image"
1953 elif "video_grid_thw" in mm_kwargs:
1954 return "video"
1955 raise AssertionError("This line should be unreachable.")
1956
1957 def get_max_frames_per_video(self) -> int:
1958 return _MAX_FRAMES_PER_VIDEO
1959
1960 def get_encoder_cudagraph_budget_range(
1961 self,
1962 vllm_config,
1963 ) -> tuple[int, int]:
1964 # Min: estimated smallest possible encoder input.
1965 # 224x224 image → 16x16 patches (patch_size=14)
1966 # spatial_merge_size=2 → 8x8 = 64 tokens
1967 min_budget = 64
1968 # Max: capped by max_num_batched_tokens
1969 max_budget = min(
1970 vllm_config.scheduler_config.max_num_batched_tokens,
1971 vllm_config.model_config.max_model_len,
1972 )
1973 return (min_budget, max_budget)
1974
1975 def _get_pixel_values_by_modality(
1976 self,
1977 mm_kwargs: dict[str, Any],
1978 ) -> torch.Tensor:
1979 if self.get_input_modality(mm_kwargs) == "image":
1980 pixel_values = mm_kwargs["pixel_values"]
1981 else:
1982 pixel_values = mm_kwargs["pixel_values_videos"]
1983 return pixel_values
1984
1985 def _get_grid_thw_by_modality(
1986 self,
1987 mm_kwargs: dict[str, Any],
1988 ) -> list[list[int]]:
1989 grid_thw_key = f"{self.get_input_modality(mm_kwargs)}_grid_thw"
1990 grid_thw = mm_kwargs[grid_thw_key]
1991 if not isinstance(grid_thw, list):
1992 grid_thw = grid_thw.tolist()
1993 return [list(row) for row in grid_thw]
1994
1995 def get_encoder_cudagraph_item_specs(
1996 self,
1997 mm_kwargs: dict[str, Any],
1998 ):
1999 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
2000
2001 m = self.visual.spatial_merge_size
2002 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
2003 return [
2004 EncoderItemSpec(
2005 input_size=t * h * w,
2006 output_tokens=t * (h // m) * (w // m),
2007 )
2008 for t, h, w in grid_thw
2009 ]
2010
2011 def select_encoder_cudagraph_items(
2012 self,
2013 mm_kwargs: dict[str, Any],
2014 indices: list[int],
2015 ) -> dict[str, Any]:
2016 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
2017 pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
2018
2019 if len(indices) == 0:
2020 if self.get_input_modality(mm_kwargs) == "image":
2021 return {
2022 "pixel_values": pixel_values[:0],
2023 "image_grid_thw": [],
2024 }
2025 else:
2026 return {
2027 "pixel_values_videos": pixel_values[:0],
2028 "video_grid_thw": [],
2029 }
2030
2031 # Compute cumulative patch offsets for slicing pixel_values
2032 patches_per_item = [t * h * w for t, h, w in grid_thw]
2033 cum_patches = [0]
2034 for p in patches_per_item:
2035 cum_patches.append(cum_patches[-1] + p)
2036
2037 selected_pv = torch.cat(
2038 [pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
2039 )
2040 selected_grid = [grid_thw[i] for i in indices]
2041
2042 if self.get_input_modality(mm_kwargs) == "image":
2043 return {
2044 "pixel_values": selected_pv,
2045 "image_grid_thw": selected_grid,
2046 }
2047 else:
2048 return {
2049 "pixel_values_videos": selected_pv,
2050 "video_grid_thw": selected_grid,
2051 }
2052
2053 def prepare_encoder_cudagraph_capture_inputs(
2054 self,
2055 token_budget: int,
2056 max_batch_size: int,
2057 max_frames_per_batch: int,
2058 device: torch.device,
2059 dtype: torch.dtype,
2060 path: str = "default",
2061 axis_keys: tuple[Hashable, ...] | None = None,
2062 ):
2063 from vllm.v1.worker.encoder_cudagraph_defs import (
2064 EncoderCudaGraphCaptureInputs,
2065 )
2066
2067 spatial_merge_size = self.visual.spatial_merge_size
2068 per_mm_item_output = token_budget // max_batch_size
2069
2070 frames_per_item = max_frames_per_batch // max_batch_size
2071 if frames_per_item > 1:
2072 # Build the capture grid using a video-format layout so that
2073 # cu_seqlens is sized for video replays from the start.
2074 # cu_seqlens has one entry per attention sequence (one per frame),
2075 # so using T > 1 per item makes the buffer large enough without
2076 # relying solely on padding.
2077 # Ceiling ensures frames_per_item * tokens_per_frame >= per_mm_item_output
2078 # so the pixel_values buffer covers any valid single-item replay.
2079 tokens_per_frame = (
2080 per_mm_item_output + frames_per_item - 1
2081 ) // frames_per_item
2082 # Video-format grid_config (T=frames_per_item).
2083 grid_config = [
2084 [
2085 frames_per_item,
2086 spatial_merge_size,
2087 tokens_per_frame * spatial_merge_size,
2088 ]
2089 for _ in range(max_batch_size)
2090 ]
2091 else:
2092 # Image-format grid_config (T=1).
2093 grid_config = [
2094 [1, spatial_merge_size, per_mm_item_output * spatial_merge_size]
2095 for _ in range(max_batch_size)
2096 ]
2097
2098 # Create dummy pixel_values
2099 patch_embed = self.visual.patch_embed
2100 in_channels = patch_embed.proj.in_channels
2101 patch_size = patch_embed.patch_size
2102 temporal_patch_size = patch_embed.temporal_patch_size
2103 total_patches = sum(t * h * w for t, h, w in grid_config)
2104 flattened_patch_size = (
2105 in_channels * temporal_patch_size * patch_size * patch_size
2106 )
2107 dummy_pixel_values = torch.zeros(
2108 total_patches,
2109 flattened_patch_size,
2110 device=device,
2111 dtype=self.visual.input_norm.input_dtype or dtype,
2112 )
2113
2114 # Override max_seqlen with a safe upper bound for capture.
2115 # max_seqlen.item() gets baked into the CUDA graph (not replayed),
2116 # so the capture value must cover any replay scenario.
2117 # Worst case: 1 item consuming the full budget ->
2118 # seq_len = token_budget * spatial_merge_size^2.
2119 metadata = self.visual.prepare_encoder_metadata(
2120 grid_config,
2121 max_batch_size=max_batch_size,
2122 max_frames_per_batch=max_frames_per_batch,
2123 max_seqlen_override=token_budget * (spatial_merge_size**2),
2124 device=device,
2125 )
2126
2127 # Just use image-modality dummy input_buffer for capturing, since it's also
2128 # compatible for video inputs (has the same shape: [num_patches, C*T*P*P]).
2129 values = metadata | {
2130 "pixel_values": dummy_pixel_values,
2131 }
2132
2133 return EncoderCudaGraphCaptureInputs(
2134 values=values,
2135 )
2136
2137 def prepare_encoder_cudagraph_replay_buffers(
2138 self,
2139 mm_kwargs: dict[str, Any],
2140 max_batch_size: int,
2141 max_frames_per_batch: int,
2142 path: str = "default",
2143 ):
2144 modality = self.get_input_modality(mm_kwargs)
2145 grid_thw_list = self._get_grid_thw_by_modality(mm_kwargs)
2146
2147 if modality == "image":
2148 metadata = self.visual.prepare_encoder_metadata(
2149 grid_thw_list,
2150 max_batch_size=max_batch_size,
2151 )
2152 elif modality == "video":
2153 metadata = self.visual.prepare_encoder_metadata(
2154 grid_thw_list,
2155 max_frames_per_batch=max_frames_per_batch,
2156 )
2157 else:
2158 raise AssertionError("This line should be unreachable.")
2159
2160 values = metadata | {
2161 "pixel_values": self._get_pixel_values_by_modality(mm_kwargs),
2162 }
2163 return EncoderCudaGraphReplayBuffers(values=values)
2164
2165 def encoder_cudagraph_forward(
2166 self,
2167 values: dict[str, torch.Tensor],
2168 path: str = "default",
2169 ) -> torch.Tensor:
2170 pixel_values = values.pop("pixel_values")
2171 metadata = values
2172 return self.visual(pixel_values, None, encoder_metadata=metadata)
2173
2174 def encoder_eager_forward(
2175 self,
2176 mm_kwargs: dict[str, Any],
2177 path: str = "default",
2178 ) -> torch.Tensor:
2179 pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
2180 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
2181 return self.visual(pixel_values, grid_thw)
2182
2183 def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
2184 mm_input_by_modality: dict[str, Glm4vImageInputs | Glm4vVideoInputs | None] = {}
2185
2186 # Preserve the order of modalities if there are multiple of them
2187 # from the order of kwargs.
2188 for input_key in kwargs:
2189 if (
2190 input_key in ("pixel_values", "image_embeds")
2191 and "image" not in mm_input_by_modality
2192 ):
2193 mm_input_by_modality["image"] = self._parse_and_validate_image_input(
2194 **kwargs
2195 )
2196 if (
2197 input_key in ("pixel_values_videos", "video_embeds")
2198 and "video" not in mm_input_by_modality
2199 ):
2200 mm_input_by_modality["video"] = self._parse_and_validate_video_input(
2201 **kwargs
2202 )
2203 return mm_input_by_modality
2204
2205 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
2206 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
2207 if not mm_input_by_modality:
2208 return None
2209
2210 # The result multimodal_embeddings is tuple of tensors, with each
2211 # tensor corresponding to a multimodal data item (image or video).
2212 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
2213
2214 # NOTE: It is important to iterate over the keys in this dictionary
2215 # to preserve the order of the modalities.
2216 for modality in mm_input_by_modality:
2217 multimodal_input = mm_input_by_modality[modality]
2218 if modality == "image":
2219 assert isinstance(
2220 multimodal_input,
2221 (Glm4vImagePixelInputs, Glm4vImageEmbeddingInputs),
2222 )
2223 image_embeddings = self._process_image_input(multimodal_input)
2224 multimodal_embeddings += tuple(image_embeddings)
2225 if modality == "video":
2226 assert isinstance(
2227 multimodal_input,
2228 (Glm4vVideoPixelInputs, Glm4vVideoEmbeddingInputs),
2229 )
2230 video_embeddings = self._process_video_input(multimodal_input)
2231 multimodal_embeddings += tuple(video_embeddings)
2232 return multimodal_embeddings
2233
2234 def iter_mm_grid_thw(
2235 self, mm_features: list[MultiModalFeatureSpec]
2236 ) -> Iterator[tuple[int, int, int, int]]:
2237 hf_config = self.config
2238 spatial_merge_size = hf_config.vision_config.spatial_merge_size
2239 for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
2240 embed_ranges = mm_feature.mm_position.extract_embeds_range()
2241 if mm_feature.modality == "image":
2242 feature_data = mm_feature.data
2243 assert feature_data is not None
2244 grid_item = feature_data.get("image_grid_thw")
2245 assert grid_item is not None
2246 grid_data = grid_item.data
2247 assert isinstance(grid_data, torch.Tensor)
2248 t, h, w = grid_data.tolist()
2249 assert t == 1, f"Image must have 1 frame, got {t}"
2250 assert len(embed_ranges) == 1
2251 offset, end = embed_ranges[0]
2252 assert end - offset + 1 == h * w // spatial_merge_size**2
2253 yield offset, t, h // spatial_merge_size, w // spatial_merge_size
2254 elif mm_feature.modality == "video":
2255 feature_data = mm_feature.data
2256 assert feature_data is not None
2257 grid_item = feature_data.get("video_grid_thw")
2258 assert grid_item is not None
2259 grid_data = grid_item.data
2260 assert isinstance(grid_data, torch.Tensor)
2261 t, h, w = grid_data.tolist()
2262 llm_grid_h = h // spatial_merge_size
2263 llm_grid_w = w // spatial_merge_size
2264 num_tokens_per_frame = llm_grid_h * llm_grid_w
2265
2266 if len(embed_ranges) == t:
2267 for offset, end in embed_ranges:
2268 assert end - offset + 1 == num_tokens_per_frame
2269 yield offset, 1, llm_grid_h, llm_grid_w
2270 else:
2271 offset = mm_feature.mm_position.offset
2272 yield offset, t, llm_grid_h, llm_grid_w
2273 else:
2274 raise ValueError(f"Unsupported modality: {mm_feature.modality}")
2275
2276 def get_mrope_input_positions(
2277 self,
2278 input_tokens: list[int],
2279 mm_features: list[MultiModalFeatureSpec],
2280 ) -> tuple[torch.Tensor, int]:
2281 llm_pos_ids_list: list = []
2282 st = 0
2283 for (
2284 offset,
2285 llm_grid_t,
2286 llm_grid_h,
2287 llm_grid_w,
2288 ) in self.iter_mm_grid_thw(mm_features):
2289 text_len = offset - st
2290 st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
2291 llm_pos_ids_list.append(
2292 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
2293 )
2294 grid_indices = np.indices((llm_grid_t, llm_grid_h, llm_grid_w)).reshape(
2295 3, -1
2296 )
2297 llm_pos_ids_list.append(grid_indices + text_len + st_idx)
2298 st = offset + llm_grid_t * llm_grid_h * llm_grid_w
2299
2300 if st < len(input_tokens):
2301 text_len = len(input_tokens) - st
2302 st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
2303 llm_pos_ids_list.append(
2304 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
2305 )
2306
2307 llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
2308 mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item()
2309 return torch.from_numpy(llm_positions), mrope_position_delta
2310
2311 def forward(
2312 self,
2313 input_ids: torch.Tensor | None,
2314 positions: torch.Tensor,
2315 intermediate_tensors: IntermediateTensors | None = None,
2316 inputs_embeds: torch.Tensor | None = None,
2317 **kwargs: object,
2318 ) -> torch.Tensor | IntermediateTensors:
2319 """Run forward pass for GLM-4V.
2320
2321 Args:
2322 input_ids: Flattened (concatenated) input_ids corresponding to a
2323 batch.
2324 positions: Flattened (concatenated) position ids corresponding to a
2325 batch.
2326 **NOTE**: If mrope is enabled (default setting for GLM-4V
2327 opensource models), the shape will be `(3, seq_len)`,
2328 otherwise it will be `(seq_len,).
2329 intermediate_tensors: Optional intermediate tensors for pipeline
2330 parallelism.
2331 inputs_embeds: Optional pre-computed input embeddings.
2332 **kwargs: Additional keyword arguments.
2333
2334 """
2335 if intermediate_tensors is not None:
2336 inputs_embeds = None
2337
2338 hidden_states = self.language_model.model(
2339 input_ids=input_ids,
2340 positions=positions,
2341 intermediate_tensors=intermediate_tensors,
2342 inputs_embeds=inputs_embeds,
2343 )
2344 return hidden_states
2345
2346 def compute_logits(
2347 self,
2348 hidden_states: torch.Tensor,
2349 ) -> torch.Tensor | None:
2350 return self.language_model.compute_logits(hidden_states)
2351
2352 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
2353 loader = AutoWeightsLoader(self)
2354 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
2355
2356 def get_mm_mapping(self) -> MultiModelKeys:
2357 """Get the module prefix in multimodal models."""
2358 return MultiModelKeys.from_string_field(
2359 language_model="language_model.model",
2360 connector="visual.merger.",
2361 tower_model="visual.",
2362 )
2363
2364 def get_mm_lora_token_counts(
2365 self,
2366 *,
2367 modality: str,
2368 mm_kwargs: MultiModalKwargsItem | None,
2369 num_mm_embeds: int,
2370 ) -> tuple[int, int | None]:
2371 del modality, mm_kwargs
2372 merge_size = self.config.vision_config.spatial_merge_size
2373 return num_mm_embeds * (merge_size**2), num_mm_embeds
2374
2375
2376 @MULTIMODAL_REGISTRY.register_processor(
2377 Glm4vMultiModalProcessor,
2378 info=Glm4vProcessingInfo,
2379 dummy_inputs=Glm4vDummyInputsBuilder,
2380 )
2381 class Glm4vMoeForConditionalGeneration(Glm4vForConditionalGeneration):
2382 packed_modules_mapping = {
2383 "qkv_proj": [
2384 "q_proj",
2385 "k_proj",
2386 "v_proj",
2387 ],
2388 "gate_up_proj": [
2389 "gate_proj",
2390 "up_proj",
2391 ],
2392 }