main
py 2,144 lines 78 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/qwen2_5_vl/modeling_qwen2_5_vl.py
6 # Copyright 2025 The vLLM team.
7 # Copyright 2025 The Qwen 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 Qwen2.5-VL model compatible with HuggingFace weights."""
28
29 from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence
30 from functools import partial
31 from typing import Annotated, Any, Literal, TypeAlias
32
33 import einops
34 import numpy as np
35 import torch
36 import torch.nn as nn
37 import torch.nn.functional as F
38 from transformers import BatchFeature
39 from transformers.models.qwen2_5_vl import Qwen2_5_VLProcessor
40 from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import (
41 Qwen2_5_VLConfig,
42 Qwen2_5_VLVisionConfig,
43 )
44 from transformers.video_utils import VideoMetadata
45
46 from vllm.compilation.decorators import (
47 should_torch_compile_mm_encoder,
48 support_torch_compile,
49 )
50 from vllm.config import VllmConfig
51 from vllm.config.multimodal import VideoDummyOptions
52 from vllm.distributed import parallel_state
53 from vllm.distributed import utils as dist_utils
54 from vllm.logger import init_logger
55 from vllm.model_executor.layers.activation import get_act_and_mul_fn
56 from vllm.model_executor.layers.attention import MMEncoderAttention
57 from vllm.model_executor.layers.conv import Conv3dLayer
58 from vllm.model_executor.layers.fusion.mm_input_norm import (
59 IdentityInputNorm,
60 build_mm_input_norm,
61 )
62 from vllm.model_executor.layers.layernorm import RMSNorm
63 from vllm.model_executor.layers.linear import (
64 ColumnParallelLinear,
65 MergedColumnParallelLinear,
66 QKVParallelLinear,
67 RowParallelLinear,
68 )
69 from vllm.model_executor.layers.quantization import QuantizationConfig
70 from vllm.model_executor.layers.rotary_embedding import get_rope
71 from vllm.model_executor.layers.rotary_embedding.common import (
72 ApplyRotaryEmb,
73 )
74 from vllm.model_executor.models.module_mapping import MultiModelKeys
75 from vllm.multimodal import MULTIMODAL_REGISTRY
76 from vllm.multimodal.inputs import (
77 MultiModalFeatureSpec,
78 MultiModalFieldConfig,
79 MultiModalKwargsItem,
80 MultiModalKwargsItems,
81 VideoItem,
82 )
83 from vllm.multimodal.parse import MultiModalDataItems
84 from vllm.multimodal.processing import PromptReplacement, PromptUpdate
85 from vllm.multimodal.processing.processor import HFMultiModalInputs
86 from vllm.multimodal.video_prune.evs import (
87 compute_mrope_for_media,
88 compute_retained_tokens_count,
89 compute_retention_mask,
90 recompute_mrope_positions,
91 )
92 from vllm.platforms import current_platform
93 from vllm.sequence import IntermediateTensors
94 from vllm.utils.cache import LRUCache
95 from vllm.utils.tensor_schema import TensorSchema, TensorShape
96 from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
97 from vllm.v1.attention.backends.registry import AttentionBackendEnum
98 from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
99
100 from ...utils.gpu_sync_debug import gpu_sync_allowed
101 from .interfaces import (
102 MultiModalEmbeddings,
103 SupportsEagle,
104 SupportsEagle3,
105 SupportsEncoderCudaGraph,
106 SupportsLoRA,
107 SupportsMRoPE,
108 SupportsMultiModal,
109 SupportsMultiModalPruning,
110 SupportsPP,
111 SupportsQuant,
112 )
113 from .qwen2_vl import (
114 Qwen2VLDummyInputsBuilder,
115 Qwen2VLMultiModalDataParser,
116 Qwen2VLMultiModalProcessor,
117 Qwen2VLProcessingInfo,
118 )
119 from .utils import (
120 AutoWeightsLoader,
121 WeightsMapper,
122 cast_overflow_tensors,
123 init_vllm_registered_model,
124 maybe_prefix,
125 )
126 from .vision import (
127 get_fp8_padded_hidden_size,
128 get_vit_attn_backend,
129 is_vit_use_data_parallel,
130 run_dp_sharded_mrope_vision_model,
131 )
132
133 logger = init_logger(__name__)
134
135
136 def _pad_cumulative_seqlens_buffer(
137 dst: torch.Tensor,
138 src: torch.Tensor,
139 ) -> None:
140 n = src.shape[0]
141 dst.zero_()
142 dst[:n].copy_(src)
143 if n < dst.shape[0]:
144 dst[n:] = src[-1]
145
146
147 def _pad_flashinfer_cu_seqlens_buffer(
148 dst: torch.Tensor,
149 src: torch.Tensor,
150 ) -> None:
151 src_mid = src.shape[0] // 2
152 dst_mid = dst.shape[0] // 2
153 assert src_mid <= dst_mid, (
154 f"FlashInfer cu_seqlens replay buffer is larger than capture buffer: "
155 f"src_section={src_mid}, dst_section={dst_mid}"
156 )
157
158 dst.zero_()
159 dst[:src_mid].copy_(src[:src_mid])
160 if src_mid < dst_mid:
161 dst[src_mid:dst_mid] = src[src_mid - 1]
162
163 dst[dst_mid : dst_mid + src_mid].copy_(src[src_mid:])
164 if dst_mid + src_mid < dst.shape[0]:
165 dst[dst_mid + src_mid :] = src[-1]
166
167
168 # === Vision Inputs === #
169
170
171 class Qwen2_5_VLImagePixelInputs(TensorSchema):
172 """Dimensions:
173 - np: Number of patches
174 - ni: Number of images
175 - cps: Number of channels * patch_size * patch_size
176
177 Historical context:
178 - pixel_values shape: (num_patches, num_channels * patch_size *
179 patch_size)
180 - image_grid_thw shape: (num_images, 3) in (grid_t, grid_h, grid_w)
181 format.
182 """
183
184 type: Literal["pixel_values"]
185
186 pixel_values: Annotated[
187 torch.Tensor,
188 TensorShape("np", "cps"),
189 ]
190
191 image_grid_thw: Annotated[
192 torch.Tensor,
193 TensorShape("ni", 3),
194 ]
195
196
197 class Qwen2_5_VLImageEmbeddingInputs(TensorSchema):
198 """Dimensions:
199 - nf: Number of image features
200 - hs: Hidden size
201 - ni: Number of images
202
203 Historical context:
204 - image_embeds shape: (num_image_features, hidden_size)
205 - num_image_features varies based on the number and resolution of the
206 images.
207 - hidden_size must match the hidden size of language model backbone.
208 - image_grid_thw shape: (num_images, 3) in (grid_t, grid_h, grid_w)
209 format
210 """
211
212 type: Literal["image_embeds"]
213
214 image_embeds: Annotated[
215 torch.Tensor,
216 TensorShape("nf", "hs"),
217 ]
218
219 image_grid_thw: Annotated[
220 torch.Tensor,
221 TensorShape("ni", 3),
222 ]
223
224
225 Qwen2_5_VLImageInputs: TypeAlias = (
226 Qwen2_5_VLImagePixelInputs | Qwen2_5_VLImageEmbeddingInputs
227 )
228
229
230 class Qwen2_5_VLVideoPixelInputs(TensorSchema):
231 """Dimensions:
232 - np: Number of patches
233 - nv: Number of videos
234 - ctps: Number of channels * temporal_patch_size * patch_size *
235 patch_size
236
237 Historical context:
238 - pixel_values_videos shape: (num_patches, num_channels *
239 temporal_patch_size * patch_size * patch_size)
240 - video_grid_thw shape: (num_videos, 3) in (grid_t, grid_h, grid_w)
241 format
242 - second_per_grid_ts: The video time interval (in seconds) for each
243 grid along the temporal dimension in the 3D position IDs. Returned
244 when `videos` is not `None`.
245 - timestamps: List of timestamp values (in seconds) for each frame
246 after merging. Length equals the temporal dimension after merging.
247 """
248
249 type: Literal["pixel_values_videos"]
250
251 pixel_values_videos: Annotated[
252 torch.Tensor,
253 TensorShape("np", "ctps"),
254 ]
255
256 video_grid_thw: Annotated[
257 torch.Tensor,
258 TensorShape("nv", 3),
259 ]
260
261 second_per_grid_ts: Annotated[
262 torch.Tensor | None,
263 TensorShape("nv"),
264 ]
265
266 timestamps: list[list[float]] | None = None
267
268
269 class Qwen2_5_VLVideoEmbeddingInputs(TensorSchema):
270 """Dimensions:
271 - nf: Number of video features
272 - hs: Hidden size
273 - nv: Number of videos
274
275 Historical context:
276 - video_embeds shape: (num_video_features, hidden_size)
277 - num_video_features varies based on the number and resolution of the
278 videos.
279 - hidden_size must match the hidden size of language model backbone.
280 - video_grid_thw shape: (num_videos, 3) in (grid_t, grid_h, grid_w)
281 format
282 - second_per_grid_ts: The video time interval (in seconds) for each
283 grid along the temporal dimension in the 3D position IDs. Returned
284 when `videos` is not `None`.
285 - timestamps: List of timestamp values (in seconds) for each frame
286 after merging. Length equals the temporal dimension after merging.
287 """
288
289 type: Literal["video_embeds"]
290
291 video_embeds: Annotated[
292 torch.Tensor,
293 TensorShape("nf", "hs"),
294 ]
295
296 video_grid_thw: Annotated[
297 torch.Tensor,
298 TensorShape("nv", 3),
299 ]
300
301 second_per_grid_ts: Annotated[
302 torch.Tensor | None,
303 TensorShape("nv"),
304 ] = None
305 timestamps: list[list[float]] | None = None
306
307
308 Qwen2_5_VLVideoInputs: TypeAlias = (
309 Qwen2_5_VLVideoPixelInputs | Qwen2_5_VLVideoEmbeddingInputs
310 )
311
312 # === Vision Encoder === #
313
314
315 class Qwen2_5_VisionMLP(nn.Module):
316 def __init__(
317 self,
318 in_features: int,
319 hidden_features: int,
320 bias: bool = False,
321 act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
322 quant_config: QuantizationConfig | None = None,
323 prefix: str = "",
324 ):
325 super().__init__()
326 use_data_parallel = is_vit_use_data_parallel()
327 self.gate_up_proj = MergedColumnParallelLinear(
328 input_size=in_features,
329 output_sizes=[hidden_features] * 2, # [gate_proj, up_proj]
330 bias=bias,
331 quant_config=quant_config,
332 prefix=f"{prefix}.gate_up_proj",
333 disable_tp=use_data_parallel,
334 )
335
336 self.down_proj = RowParallelLinear(
337 hidden_features,
338 in_features,
339 bias=bias,
340 quant_config=quant_config,
341 prefix=f"{prefix}.down_proj",
342 disable_tp=use_data_parallel,
343 )
344 self.act_fn = act_fn
345
346 def forward(self, x: torch.Tensor):
347 gate_up, _ = self.gate_up_proj(x)
348 x = self.act_fn(gate_up)
349 x_down, _ = self.down_proj(x)
350 return x_down
351
352
353 class Qwen2_5_VisionAttention(nn.Module):
354 def __init__(
355 self,
356 embed_dim: int,
357 num_heads: int,
358 projection_size: int,
359 quant_config: QuantizationConfig | None = None,
360 prefix: str = "",
361 ) -> None:
362 super().__init__()
363 # Per attention head and per partition values.
364 use_data_parallel = is_vit_use_data_parallel()
365 self.tp_size = (
366 1
367 if use_data_parallel
368 else parallel_state.get_tensor_model_parallel_world_size()
369 )
370 self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
371 self.hidden_size_per_attention_head = dist_utils.divide(
372 projection_size, num_heads
373 )
374 self.num_attention_heads_per_partition = dist_utils.divide(
375 num_heads, self.tp_size
376 )
377
378 self.qkv = QKVParallelLinear(
379 hidden_size=embed_dim,
380 head_size=self.hidden_size_per_attention_head,
381 total_num_heads=num_heads,
382 total_num_kv_heads=num_heads,
383 bias=True,
384 quant_config=quant_config,
385 prefix=f"{prefix}.qkv",
386 disable_tp=use_data_parallel,
387 )
388
389 self.proj = RowParallelLinear(
390 input_size=projection_size,
391 output_size=embed_dim,
392 quant_config=quant_config,
393 prefix=f"{prefix}.proj",
394 disable_tp=use_data_parallel,
395 )
396
397 self.attn = MMEncoderAttention(
398 num_heads=self.num_attention_heads_per_partition,
399 head_size=self.hidden_size_per_attention_head,
400 scale=self.hidden_size_per_attention_head**-0.5,
401 prefix=f"{prefix}.attn",
402 )
403
404 self.apply_rotary_emb = ApplyRotaryEmb(enforce_enable=True)
405
406 def forward(
407 self,
408 x: torch.Tensor,
409 cu_seqlens: torch.Tensor,
410 rotary_pos_emb_cos: torch.Tensor,
411 rotary_pos_emb_sin: torch.Tensor,
412 max_seqlen: torch.Tensor, # Only used for Flash Attention
413 # Only used for FlashInfer CuDNN backend.
414 sequence_lengths: torch.Tensor | None,
415 ) -> torch.Tensor:
416 # [s, b, c] --> [s, b, head * 3 * head_dim]
417 x, _ = self.qkv(x)
418 seq_len, batch_size, _ = x.shape
419
420 qkv = einops.rearrange(
421 x,
422 "s b (three head head_dim) -> b s three head head_dim",
423 three=3,
424 head=self.num_attention_heads_per_partition,
425 )
426
427 if rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:
428 qk, v = qkv[:, :, :2], qkv[:, :, 2]
429
430 qk_reshaped = einops.rearrange(
431 qk, "b s two head head_dim -> (two b) s head head_dim", two=2
432 )
433 qk_reshaped = qk_reshaped.contiguous()
434 qk_rotated = self.apply_rotary_emb(
435 qk_reshaped,
436 rotary_pos_emb_cos,
437 rotary_pos_emb_sin,
438 )
439 qk_rotated = qk_rotated.view(
440 2,
441 batch_size,
442 seq_len,
443 self.num_attention_heads_per_partition,
444 self.hidden_size_per_attention_head,
445 )
446 q, k = qk_rotated.unbind(dim=0)
447 else:
448 q, k, v = qkv.unbind(dim=2)
449
450 context_layer = self.attn(
451 query=q,
452 key=k,
453 value=v,
454 cu_seqlens=cu_seqlens,
455 max_seqlen=max_seqlen,
456 sequence_lengths=sequence_lengths,
457 )
458
459 context_layer = einops.rearrange(
460 context_layer, "b s h d -> s b (h d)", b=batch_size
461 ).contiguous()
462
463 output, _ = self.proj(context_layer)
464 return output
465
466
467 @support_torch_compile(
468 dynamic_arg_dims={
469 "x": 0,
470 "cu_seqlens": 0,
471 "sequence_lengths": 0,
472 "rotary_pos_emb_cos": 0,
473 "rotary_pos_emb_sin": 0,
474 },
475 enable_if=should_torch_compile_mm_encoder,
476 is_encoder=True,
477 )
478 class Qwen2_5_VisionBlock(nn.Module):
479 def __init__(
480 self,
481 dim: int,
482 num_heads: int,
483 mlp_hidden_dim: int,
484 act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
485 norm_layer: Callable[[int], nn.Module] | None = None,
486 quant_config: QuantizationConfig | None = None,
487 prefix: str = "",
488 ) -> None:
489 super().__init__()
490 if norm_layer is None:
491 norm_layer = partial(nn.LayerNorm, eps=1e-6)
492 self.norm1 = norm_layer(dim)
493 self.norm2 = norm_layer(dim)
494 self.attn = Qwen2_5_VisionAttention(
495 embed_dim=dim,
496 num_heads=num_heads,
497 projection_size=dim,
498 quant_config=quant_config,
499 prefix=f"{prefix}.attn",
500 )
501 self.mlp = Qwen2_5_VisionMLP(
502 dim,
503 mlp_hidden_dim,
504 act_fn=act_fn,
505 bias=True,
506 quant_config=quant_config,
507 prefix=f"{prefix}.mlp",
508 )
509
510 def forward(
511 self,
512 x: torch.Tensor,
513 cu_seqlens: torch.Tensor,
514 rotary_pos_emb_cos: torch.Tensor,
515 rotary_pos_emb_sin: torch.Tensor,
516 max_seqlen: torch.Tensor, # Only used for Flash Attention
517 # Only used for FlashInfer CuDNN backend.
518 sequence_lengths: torch.Tensor | None = None,
519 ) -> torch.Tensor:
520 x_attn = self.attn(
521 self.norm1(x),
522 cu_seqlens=cu_seqlens,
523 rotary_pos_emb_cos=rotary_pos_emb_cos,
524 rotary_pos_emb_sin=rotary_pos_emb_sin,
525 max_seqlen=max_seqlen,
526 sequence_lengths=sequence_lengths,
527 )
528 x_fused_norm, residual = self.norm2(x, residual=x_attn)
529 x = residual + self.mlp(x_fused_norm)
530 return x
531
532
533 @support_torch_compile(
534 dynamic_arg_dims={
535 "x": 0,
536 },
537 enable_if=should_torch_compile_mm_encoder,
538 is_encoder=True,
539 )
540 class Qwen2_5_VisionPatchEmbed(nn.Module):
541 def __init__(
542 self,
543 patch_size: int = 14,
544 temporal_patch_size: int = 2,
545 in_channels: int = 3,
546 hidden_size: int = 1152,
547 ) -> None:
548 super().__init__()
549 self.patch_size = patch_size
550 self.temporal_patch_size = temporal_patch_size
551 self.hidden_size = hidden_size
552
553 kernel_size = (temporal_patch_size, patch_size, patch_size)
554 self.proj = Conv3dLayer(
555 in_channels,
556 hidden_size,
557 kernel_size=kernel_size,
558 stride=kernel_size,
559 bias=False,
560 )
561
562 def forward(self, x: torch.Tensor) -> torch.Tensor:
563 L, C = x.shape
564 x = x.view(L, -1, self.temporal_patch_size, self.patch_size, self.patch_size)
565 x = self.proj(x).view(L, self.hidden_size)
566 return x
567
568
569 @support_torch_compile(
570 dynamic_arg_dims={
571 "x": 0,
572 },
573 enable_if=should_torch_compile_mm_encoder,
574 is_encoder=True,
575 )
576 class Qwen2_5_VisionPatchMerger(nn.Module):
577 def __init__(
578 self,
579 d_model: int,
580 context_dim: int,
581 norm_layer: Callable[[int], nn.Module] | None = None,
582 spatial_merge_size: int = 2,
583 quant_config: QuantizationConfig | None = None,
584 prefix: str = "",
585 ) -> None:
586 super().__init__()
587 use_data_parallel = is_vit_use_data_parallel()
588 self.hidden_size = context_dim * (spatial_merge_size**2)
589 if norm_layer is None:
590 norm_layer = partial(nn.LayerNorm, eps=1e-6)
591 self.ln_q = norm_layer(context_dim)
592
593 self.mlp = nn.Sequential(
594 ColumnParallelLinear(
595 self.hidden_size,
596 self.hidden_size,
597 bias=True,
598 quant_config=quant_config,
599 prefix=f"{prefix}.mlp.0",
600 return_bias=False,
601 disable_tp=use_data_parallel,
602 ),
603 nn.GELU(),
604 RowParallelLinear(
605 self.hidden_size,
606 d_model,
607 bias=True,
608 quant_config=quant_config,
609 prefix=f"{prefix}.mlp.2",
610 return_bias=False,
611 disable_tp=use_data_parallel,
612 ),
613 )
614
615 def forward(self, x: torch.Tensor) -> torch.Tensor:
616 x = self.ln_q(x)
617 x = x.view(-1, self.hidden_size)
618 out = self.mlp(x)
619 return out
620
621
622 class Qwen2_5_VisionTransformer(nn.Module):
623 hf_to_vllm_mapper = WeightsMapper(
624 orig_to_new_stacked={
625 ".attn.q.": (".attn.qkv.", "q"),
626 ".attn.k.": (".attn.qkv.", "k"),
627 ".attn.v.": (".attn.qkv.", "v"),
628 ".mlp.gate_proj.": (".mlp.gate_up_proj.", 0),
629 ".mlp.up_proj.": (".mlp.gate_up_proj.", 1),
630 }
631 )
632
633 def __init__(
634 self,
635 vision_config: Qwen2_5_VLVisionConfig,
636 norm_eps: float = 1e-6,
637 quant_config: QuantizationConfig | None = None,
638 input_norm: nn.Module | None = 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 self.out_hidden_size = vision_config.out_hidden_size
650
651 # args for get_window_index_thw
652 self.window_size = vision_config.window_size
653 self.patch_size = vision_config.patch_size
654 self.spatial_merge_size = vision_config.spatial_merge_size
655 self.fullatt_block_indexes = vision_config.fullatt_block_indexes
656 self.spatial_merge_unit = self.spatial_merge_size**2
657 self._rope_by_thw_cache: LRUCache[
658 tuple[int, int, int],
659 tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
660 ] = LRUCache(capacity=1024)
661 use_data_parallel = is_vit_use_data_parallel()
662 self.tp_size = (
663 1
664 if use_data_parallel
665 else parallel_state.get_tensor_model_parallel_world_size()
666 )
667 self.patch_embed = Qwen2_5_VisionPatchEmbed(
668 patch_size=patch_size,
669 temporal_patch_size=temporal_patch_size,
670 in_channels=in_channels,
671 hidden_size=self.hidden_size,
672 )
673 self.input_norm = input_norm if input_norm is not None else IdentityInputNorm()
674
675 norm_layer = partial(RMSNorm, eps=norm_eps)
676 head_dim = self.hidden_size // self.num_heads
677 # FP8 attention: Q/K/V become independent contiguous tensors after
678 # quantization, so FlashInfer cu_seqlens uses uniform stride.
679 self.fp8_padded_hidden_size = get_fp8_padded_hidden_size(
680 self.num_heads, head_dim
681 )
682 self.rotary_pos_emb = get_rope(
683 head_size=head_dim,
684 max_position=8192,
685 is_neox_style=True,
686 rope_parameters={"partial_rotary_factor": 0.5},
687 )
688
689 self.attn_backend = get_vit_attn_backend(
690 head_size=head_dim,
691 dtype=torch.get_default_dtype(),
692 )
693
694 self.blocks = nn.ModuleList(
695 [
696 Qwen2_5_VisionBlock(
697 dim=self.hidden_size,
698 num_heads=self.num_heads,
699 mlp_hidden_dim=vision_config.intermediate_size,
700 act_fn=get_act_and_mul_fn(
701 vision_config.hidden_act,
702 compile_native=not current_platform.is_rocm(),
703 ),
704 norm_layer=norm_layer,
705 quant_config=quant_config,
706 prefix=f"{prefix}.blocks.{layer_idx}",
707 )
708 for layer_idx in range(depth)
709 ]
710 )
711
712 self.merger = Qwen2_5_VisionPatchMerger(
713 d_model=vision_config.out_hidden_size,
714 context_dim=self.hidden_size,
715 norm_layer=norm_layer,
716 spatial_merge_size=self.spatial_merge_size,
717 quant_config=quant_config,
718 prefix=f"{prefix}.merger",
719 )
720
721 @property
722 def dtype(self) -> torch.dtype:
723 return self.patch_embed.proj.weight.dtype
724
725 @property
726 def device(self) -> torch.device:
727 return self.patch_embed.proj.weight.device
728
729 def rotary_pos_emb_thw(self, t, h, w):
730 # Use pre-computed cos_sin_cache from RotaryEmbedding
731 cos, sin = self.rotary_pos_emb.get_cos_sin(max(h, w))
732
733 hpos_ids = torch.arange(h, device=cos.device).unsqueeze(1).expand(-1, w)
734 wpos_ids = torch.arange(w, device=cos.device).unsqueeze(0).expand(h, -1)
735 hpos_ids = (
736 hpos_ids.reshape(
737 h // self.spatial_merge_size,
738 self.spatial_merge_size,
739 w // self.spatial_merge_size,
740 self.spatial_merge_size,
741 )
742 .permute(0, 2, 1, 3)
743 .flatten()
744 )
745 wpos_ids = (
746 wpos_ids.reshape(
747 h // self.spatial_merge_size,
748 self.spatial_merge_size,
749 w // self.spatial_merge_size,
750 self.spatial_merge_size,
751 )
752 .permute(0, 2, 1, 3)
753 .flatten()
754 )
755 pos_ids = torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1)
756
757 cos_combined = cos[pos_ids].flatten(1)
758 sin_combined = sin[pos_ids].flatten(1)
759
760 cos_combined = cos_combined.reshape(
761 cos_combined.shape[0] // self.spatial_merge_unit,
762 self.spatial_merge_unit,
763 -1,
764 )
765 sin_combined = sin_combined.reshape(
766 sin_combined.shape[0] // self.spatial_merge_unit,
767 self.spatial_merge_unit,
768 -1,
769 )
770
771 return cos_combined, sin_combined
772
773 def get_window_index_thw(self, grid_t, grid_h, grid_w):
774 vit_merger_window_size = (
775 self.window_size // self.spatial_merge_size // self.patch_size
776 )
777
778 llm_grid_h = grid_h // self.spatial_merge_size
779 llm_grid_w = grid_w // self.spatial_merge_size
780 index = torch.arange(grid_t * llm_grid_h * llm_grid_w).reshape(
781 grid_t, llm_grid_h, llm_grid_w
782 )
783 pad_h = vit_merger_window_size - llm_grid_h % vit_merger_window_size
784 pad_w = vit_merger_window_size - llm_grid_w % vit_merger_window_size
785 num_windows_h = (llm_grid_h + pad_h) // vit_merger_window_size
786 num_windows_w = (llm_grid_w + pad_w) // vit_merger_window_size
787 index_padded = F.pad(index, (0, pad_w, 0, pad_h), "constant", -100)
788 index_padded = index_padded.reshape(
789 grid_t,
790 num_windows_h,
791 vit_merger_window_size,
792 num_windows_w,
793 vit_merger_window_size,
794 )
795 index_padded = index_padded.permute(0, 1, 3, 2, 4).reshape(
796 grid_t,
797 num_windows_h * num_windows_w,
798 vit_merger_window_size,
799 vit_merger_window_size,
800 )
801 seqlens = (index_padded != -100).sum([2, 3]).reshape(-1)
802 index_padded = index_padded.reshape(-1)
803 index_new = index_padded[index_padded != -100]
804 cu_seqlens_tmp = seqlens.cumsum(0) * self.spatial_merge_unit
805 cu_seqlens_tmp = cu_seqlens_tmp.to(dtype=torch.int32)
806 cu_seqlens_tmp = torch.unique_consecutive(cu_seqlens_tmp)
807
808 return index_new, cu_seqlens_tmp
809
810 def get_rope_by_thw(self, t, h, w):
811 cache_key = (t, h, w)
812 if cache_key in self._rope_by_thw_cache:
813 return self._rope_by_thw_cache[cache_key]
814
815 window_index_thw, cu_seqlens_window_thw = self.get_window_index_thw(t, h, w)
816 cos_thw, sin_thw = self.rotary_pos_emb_thw(t, h, w)
817
818 window_index_thw_dev = async_tensor_h2d(window_index_thw, cos_thw.device)
819 cos_thw = cos_thw[window_index_thw_dev, :, :]
820 cos_thw = cos_thw.flatten(start_dim=0, end_dim=1)
821 sin_thw = sin_thw[window_index_thw_dev, :, :]
822 sin_thw = sin_thw.flatten(start_dim=0, end_dim=1)
823
824 cu_seqlens_thw = torch.repeat_interleave(
825 torch.tensor([h * w], dtype=torch.int32), t
826 )
827 result = (
828 cos_thw,
829 sin_thw,
830 window_index_thw,
831 cu_seqlens_window_thw,
832 cu_seqlens_thw,
833 )
834 self._rope_by_thw_cache[cache_key] = result
835 return result
836
837 def compute_attn_mask_seqlen(
838 self,
839 cu_seqlens: torch.Tensor,
840 ) -> torch.Tensor:
841 max_seqlen = torch.zeros([], device=cu_seqlens.device)
842 if self.attn_backend in {
843 AttentionBackendEnum.FLASH_ATTN,
844 AttentionBackendEnum.ROCM_AITER_FA,
845 AttentionBackendEnum.TRITON_ATTN,
846 }:
847 max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
848 return max_seqlen
849
850 @staticmethod
851 def invert_permutation(perm: torch.Tensor) -> torch.Tensor:
852 # building the inverse permutation in O(n) time
853 inv = torch.empty_like(perm, pin_memory=PIN_MEMORY)
854 inv[perm] = torch.arange(perm.numel(), device=perm.device, dtype=perm.dtype)
855 return inv
856
857 def get_encoder_cudagraph_max_window_seqs(
858 self,
859 token_budget: int,
860 max_batch_size: int,
861 max_frames_per_batch: int,
862 ) -> int:
863 # token_budget is an upper bound on the total number of merged vision
864 # tokens replayed by this encoder CUDA graph. cu_window_seqlens, however,
865 # is sized by the number of window-attention sequences (non-empty local
866 # windows), not by the number of tokens. Using max_num_batched_tokens as
867 # this sequence count can over-pad cu_window_seqlens and make FlashAttention
868 # launch thousands of empty CTAs during replay.
869 vit_merger_window_size = (
870 self.window_size // self.spatial_merge_size // self.patch_size
871 )
872 max_sequence_units = max(max_batch_size, max_frames_per_batch)
873
874 # Each local window covers vit_merger_window_size tokens along one merged
875 # spatial axis. The largest number of non-empty windows for a fixed token
876 # budget comes from a thin strip that advances along only one axis, so
877 # ceil(token_budget / window_side) is a safe geometry-driven bound. Multiple
878 # images or video frames can fragment that strip at item/frame boundaries,
879 # so add max_sequence_units to cover one extra partial window per sequence.
880 max_strip_windows = (
881 token_budget + vit_merger_window_size - 1
882 ) // vit_merger_window_size
883
884 # A non-empty window must contain at least one merged vision token, so the
885 # number of window sequences can never exceed token_budget. This final
886 # clamp keeps the bound tight for tiny budgets while remaining safe.
887 return min(token_budget, max_sequence_units + max_strip_windows)
888
889 def prepare_encoder_metadata(
890 self,
891 grid_thw: Sequence[Sequence[int]],
892 *,
893 max_batch_size: int | None = None,
894 max_frames_per_batch: int | None = None,
895 max_window_seqs_per_batch: int | None = None,
896 max_seqlen_override: int | None = None,
897 max_seqlen_window_override: int | None = None,
898 device: torch.device | None = None,
899 ) -> dict[str, torch.Tensor]:
900 """Compute encoder metadata from grid_thw.
901
902 Shared by the eager forward path, CUDA graph capture, and
903 CUDA graph replay to avoid duplicated implementation.
904
905 Args:
906 grid_thw: Grid configurations as list of [t, h, w].
907 max_batch_size: If set, pad cu_seqlens to this size
908 (needed for CUDA graph capture/replay).
909 max_frames_per_batch: If set, overrides max_batch_size for
910 cu_seqlens padding. For video inputs each item contributes
911 T attention sequences (frames); this sizes the buffer to
912 the total frame budget so video replays never overflow.
913 max_window_seqs_per_batch: If set, pad cu_window_seqlens to this
914 number of window sequences. This keeps cu_window_seqlens shape
915 stable across capture/replay for CUDA graph safety.
916 max_seqlen_override: If set, use this value for max_seqlen
917 instead of computing from cu_seqlens (needed for CUDA
918 graph capture to cover worst-case replay scenarios).
919 max_seqlen_window_override: If set, use this value for
920 window-attention max_seqlen instead of computing from
921 cu_window_seqlens (needed for CUDA graph capture to
922 cover worst-case replay scenarios).
923 device: Device to place tensors on. Defaults to self.device.
924
925 """
926 if device is None:
927 device = self.device
928 metadata: dict[str, torch.Tensor] = {}
929
930 # patchify
931 rotary_pos_emb_cos = []
932 rotary_pos_emb_sin = []
933 window_indices: list[torch.Tensor] = []
934 cu_window_seqlens_parts = [torch.tensor([0], dtype=torch.int32)]
935 cu_seqlens_parts: list[torch.Tensor] = []
936
937 window_index_id = 0
938 cu_window_seqlens_last = 0
939 for t, h, w in grid_thw:
940 t, h, w = int(t), int(h), int(w)
941 llm_h = h // self.spatial_merge_size
942 llm_w = w // self.spatial_merge_size
943
944 (
945 cos_thw,
946 sin_thw,
947 window_index_thw,
948 cu_seqlens_window_thw,
949 cu_seqlens_thw,
950 ) = self.get_rope_by_thw(t, h, w)
951
952 window_indices.append(window_index_thw + window_index_id)
953 window_index_id += t * llm_h * llm_w
954
955 cu_seqlens_window_thw = cu_seqlens_window_thw + cu_window_seqlens_last
956 cu_window_seqlens_last = cu_seqlens_window_thw[-1]
957 cu_window_seqlens_parts.append(cu_seqlens_window_thw)
958
959 rotary_pos_emb_cos.append(cos_thw)
960 rotary_pos_emb_sin.append(sin_thw)
961
962 cu_seqlens_parts.append(cu_seqlens_thw)
963
964 rotary_pos_emb_cos = torch.cat(rotary_pos_emb_cos)
965 rotary_pos_emb_sin = torch.cat(rotary_pos_emb_sin)
966 window_index = torch.cat(window_indices)
967 # compute reverse indices
968 reverse_indices = self.invert_permutation(window_index)
969 cu_window_seqlens = torch.cat(cu_window_seqlens_parts)
970 cu_window_seqlens = torch.unique_consecutive(cu_window_seqlens)
971 cu_seqlens = torch.cat(cu_seqlens_parts)
972 cu_seqlens = torch.cumsum(cu_seqlens, dim=0, dtype=torch.int32)
973 cu_seqlens = F.pad(cu_seqlens, (1, 0), "constant", 0)
974
975 # Pad cu_seqlens to the required number of sequences.
976 # For videos each item contributes T frames = T attention sequences,
977 # so the total can exceed max_batch_size. max_frames_per_batch
978 # overrides the pad target when set.
979 pad_to = (
980 max_frames_per_batch if max_frames_per_batch is not None else max_batch_size
981 )
982 if pad_to is not None:
983 num_seqs = len(cu_seqlens) - 1
984 if num_seqs < pad_to:
985 cu_seqlens = torch.cat(
986 (
987 cu_seqlens,
988 torch.full(
989 (pad_to - num_seqs,),
990 cu_seqlens[-1],
991 dtype=cu_seqlens.dtype,
992 device=cu_seqlens.device,
993 ),
994 )
995 )
996
997 # Pad cu_window_seqlens to a stable number of window sequences.
998 # Like cu_seqlens, we repeat the last cumulative offset so padded
999 # entries represent empty sequences.
1000 if max_window_seqs_per_batch is not None:
1001 num_window_seqs = len(cu_window_seqlens) - 1
1002 if num_window_seqs < max_window_seqs_per_batch:
1003 cu_window_seqlens = torch.cat(
1004 (
1005 cu_window_seqlens,
1006 torch.full(
1007 (max_window_seqs_per_batch - num_window_seqs,),
1008 cu_window_seqlens[-1],
1009 dtype=cu_window_seqlens.dtype,
1010 device=cu_window_seqlens.device,
1011 ),
1012 )
1013 )
1014
1015 cu_seqlens_np = cu_seqlens.cpu().numpy()
1016 cu_window_seqlens_np = cu_window_seqlens.cpu().numpy()
1017
1018 # FlashInfer needs the real per-sequence lengths in addition to
1019 # cu_seqlens. For other backends this returns None and is ignored.
1020 sequence_lengths_full = MMEncoderAttention.maybe_compute_seq_lens(
1021 self.attn_backend, cu_seqlens_np, device
1022 )
1023 sequence_lengths_window = MMEncoderAttention.maybe_compute_seq_lens(
1024 self.attn_backend, cu_window_seqlens_np, device
1025 )
1026
1027 # Pre-compute max sequence lengths for window/full attention. FlashInfer
1028 # buckets this value for cuDNN graph reuse; other backends keep the exact
1029 # maximum. Keep the scalar on CPU because attention wrappers call .item().
1030 if max_seqlen_override is None:
1031 max_seqlen_full_val = MMEncoderAttention.compute_max_seqlen(
1032 self.attn_backend, cu_seqlens_np
1033 )
1034 else:
1035 max_seqlen_full_val = max_seqlen_override
1036 max_seqlen_full = torch.tensor(max_seqlen_full_val, dtype=torch.int32)
1037 if max_seqlen_window_override is None:
1038 max_seqlen_window_val = MMEncoderAttention.compute_max_seqlen(
1039 self.attn_backend, cu_window_seqlens_np
1040 )
1041 else:
1042 max_seqlen_window_val = max_seqlen_window_override
1043 max_seqlen_window = torch.tensor(max_seqlen_window_val, dtype=torch.int32)
1044
1045 # FlashInfer uses backend-specific cu_seqlens offsets into the flattened
1046 # Q/K/O and V buffers. Other backends receive the original cumulative
1047 # token offsets unchanged.
1048 cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
1049 self.attn_backend,
1050 cu_seqlens_np,
1051 self.hidden_size,
1052 self.tp_size,
1053 device,
1054 fp8_padded_hidden_size=self.fp8_padded_hidden_size,
1055 )
1056 cu_window_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
1057 self.attn_backend,
1058 cu_window_seqlens_np,
1059 self.hidden_size,
1060 self.tp_size,
1061 device,
1062 fp8_padded_hidden_size=self.fp8_padded_hidden_size,
1063 )
1064 rotary_pos_emb_cos = rotary_pos_emb_cos.to(device=device, non_blocking=True)
1065 rotary_pos_emb_sin = rotary_pos_emb_sin.to(device=device, non_blocking=True)
1066 window_index = async_tensor_h2d(window_index, device)
1067 reverse_indices = reverse_indices.to(device=device, non_blocking=True)
1068
1069 metadata["rotary_pos_emb_cos"] = rotary_pos_emb_cos
1070 metadata["rotary_pos_emb_sin"] = rotary_pos_emb_sin
1071 metadata["window_index"] = window_index
1072 metadata["reverse_indices"] = reverse_indices
1073 metadata["cu_seqlens"] = cu_seqlens
1074 metadata["cu_window_seqlens"] = cu_window_seqlens
1075 metadata["max_seqlen_full"] = max_seqlen_full
1076 metadata["max_seqlen_window"] = max_seqlen_window
1077 metadata["sequence_lengths_full"] = sequence_lengths_full
1078 metadata["sequence_lengths_window"] = sequence_lengths_window
1079
1080 return metadata
1081
1082 def forward(
1083 self,
1084 x: torch.Tensor,
1085 grid_thw: list[list[int]] | None,
1086 *,
1087 encoder_metadata: dict[str, torch.Tensor] | None = None,
1088 ) -> torch.Tensor:
1089 hidden_states = self.input_norm(x.to(device=self.device), self.dtype)
1090 hidden_states = self.patch_embed(hidden_states)
1091
1092 seq_len = hidden_states.shape[0]
1093 if encoder_metadata is None:
1094 assert grid_thw is not None
1095 encoder_metadata = self.prepare_encoder_metadata(grid_thw)
1096
1097 rotary_pos_emb_cos = encoder_metadata["rotary_pos_emb_cos"]
1098 rotary_pos_emb_sin = encoder_metadata["rotary_pos_emb_sin"]
1099 window_index = encoder_metadata["window_index"]
1100 reverse_indices = encoder_metadata["reverse_indices"]
1101 cu_seqlens = encoder_metadata["cu_seqlens"]
1102 cu_window_seqlens = encoder_metadata["cu_window_seqlens"]
1103 max_seqlen_full = encoder_metadata["max_seqlen_full"]
1104 max_seqlen_window = encoder_metadata["max_seqlen_window"]
1105 sequence_lengths_full = encoder_metadata.get("sequence_lengths_full")
1106 sequence_lengths_window = encoder_metadata.get("sequence_lengths_window")
1107
1108 hidden_states = hidden_states.reshape(
1109 seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1
1110 )
1111 hidden_states = hidden_states[window_index, :, :]
1112 hidden_states = hidden_states.reshape(seq_len, -1)
1113
1114 hidden_states = hidden_states.unsqueeze(1)
1115
1116 for layer_num, blk in enumerate(self.blocks):
1117 if layer_num in self.fullatt_block_indexes:
1118 cu_seqlens_now = cu_seqlens
1119 max_seqlen_now = max_seqlen_full
1120 sequence_lengths_now = sequence_lengths_full
1121 else:
1122 cu_seqlens_now = cu_window_seqlens
1123 max_seqlen_now = max_seqlen_window
1124 sequence_lengths_now = sequence_lengths_window
1125
1126 hidden_states = blk(
1127 hidden_states,
1128 cu_seqlens=cu_seqlens_now,
1129 rotary_pos_emb_cos=rotary_pos_emb_cos,
1130 rotary_pos_emb_sin=rotary_pos_emb_sin,
1131 max_seqlen=max_seqlen_now,
1132 sequence_lengths=sequence_lengths_now,
1133 )
1134
1135 # For Qwen2.5-VL-3B, float16 will overflow at last block
1136 # for long visual tokens sequences.
1137 if hidden_states.dtype == torch.float16:
1138 hidden_states = cast_overflow_tensors(hidden_states)
1139
1140 # adapter
1141 hidden_states = self.merger(hidden_states)
1142 hidden_states = hidden_states[reverse_indices, :]
1143 return hidden_states
1144
1145 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1146 loader = AutoWeightsLoader(self)
1147 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1148
1149
1150 class Qwen2_5_VLDummyInputsBuilder(Qwen2VLDummyInputsBuilder):
1151 def _get_dummy_videos(
1152 self,
1153 *,
1154 width: int,
1155 height: int,
1156 num_frames: int,
1157 num_videos: int,
1158 overrides: VideoDummyOptions | None = None,
1159 ) -> list[VideoItem]:
1160 videos = super()._get_dummy_videos(
1161 width=width,
1162 height=height,
1163 num_frames=num_frames,
1164 num_videos=num_videos,
1165 overrides=overrides,
1166 )
1167 videos = [v.copy() for v in videos]
1168
1169 video_items: list[VideoItem] = []
1170 for video in videos:
1171 video_num_frames = video.shape[0]
1172 video_metadata = {
1173 "fps": 2.0,
1174 "duration": video_num_frames / 2.0,
1175 "total_num_frames": video_num_frames,
1176 "frames_indices": list(range(video_num_frames)),
1177 "video_backend": "opencv",
1178 "do_sample_frames": False,
1179 }
1180 video_items.append((video, video_metadata))
1181 return video_items
1182
1183
1184 class Qwen2_5_VLProcessingInfo(Qwen2VLProcessingInfo):
1185 def get_hf_config(self):
1186 return self.ctx.get_hf_config(Qwen2_5_VLConfig)
1187
1188 def get_hf_processor(self, **kwargs: object) -> Qwen2_5_VLProcessor:
1189 return self.ctx.get_hf_processor(
1190 Qwen2_5_VLProcessor,
1191 **kwargs,
1192 )
1193
1194 def get_data_parser(self):
1195 return Qwen2VLMultiModalDataParser(
1196 self.get_hf_config().vision_config.spatial_merge_size,
1197 video_needs_metadata=True,
1198 expected_hidden_size=self._get_expected_hidden_size(),
1199 allow_missing_mm_embeddings=self.allow_missing_mm_embeddings,
1200 )
1201
1202
1203 class Qwen2_5_VLMultiModalProcessor(Qwen2VLMultiModalProcessor):
1204 def _get_mm_fields_config(
1205 self,
1206 hf_inputs: BatchFeature,
1207 hf_processor_mm_kwargs: Mapping[str, object],
1208 ) -> Mapping[str, MultiModalFieldConfig]:
1209 return dict(
1210 **super()._get_mm_fields_config(hf_inputs, hf_processor_mm_kwargs),
1211 second_per_grid_ts=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
1212 )
1213
1214 def _get_hf_mm_inputs(
1215 self,
1216 mm_items: MultiModalDataItems,
1217 hf_kwargs: Mapping[str, object],
1218 ) -> HFMultiModalInputs:
1219 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
1220 hf_data = hf_inputs.hf_data
1221
1222 if videos := hf_data.get("videos"):
1223 assert isinstance(videos, list)
1224 # HF expects video metadata as a separate argument.
1225 hf_data["videos"] = [video for video, _ in videos]
1226 hf_data["video_metadata"] = [
1227 VideoMetadata(
1228 **{k: metadata[k] for k in metadata if k != "do_sample_frames"}
1229 )
1230 for _, metadata in videos
1231 ]
1232 return hf_inputs
1233
1234 def _get_prompt_updates(
1235 self,
1236 mm_items: MultiModalDataItems,
1237 hf_processor_mm_kwargs: Mapping[str, Any],
1238 out_mm_kwargs: MultiModalKwargsItems,
1239 ) -> Sequence[PromptUpdate]:
1240 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
1241 image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
1242 tokenizer = self.info.get_tokenizer()
1243 vocab = tokenizer.get_vocab()
1244
1245 placeholder = {
1246 "image": vocab[hf_processor.image_token],
1247 "video": vocab[hf_processor.video_token],
1248 }
1249
1250 merge_length = image_processor.merge_size**2
1251
1252 def get_replacement_qwen2vl(item_idx: int, modality: str):
1253 out_item = out_mm_kwargs[modality][item_idx]
1254 grid_thw = out_item[f"{modality}_grid_thw"].data
1255 assert isinstance(grid_thw, torch.Tensor)
1256
1257 num_tokens = int(grid_thw.prod()) // merge_length
1258
1259 # EVS-specific code
1260 video_pruning_rate = self.info.ctx.get_mm_config().video_pruning_rate
1261 if (
1262 modality == "video"
1263 and video_pruning_rate is not None
1264 and video_pruning_rate > 0.0
1265 ):
1266 T, H, W = map(int, grid_thw)
1267 tokens_per_frame = (H // image_processor.merge_size) * (
1268 W // image_processor.merge_size
1269 )
1270 num_tokens = compute_retained_tokens_count(
1271 tokens_per_frame,
1272 T,
1273 video_pruning_rate,
1274 )
1275 # End of EVS-specific code
1276
1277 return [placeholder[modality]] * num_tokens
1278
1279 return [
1280 PromptReplacement(
1281 modality=modality,
1282 target=[placeholder[modality]],
1283 replacement=partial(get_replacement_qwen2vl, modality=modality),
1284 )
1285 for modality in ("image", "video")
1286 ]
1287
1288
1289 @MULTIMODAL_REGISTRY.register_processor(
1290 Qwen2_5_VLMultiModalProcessor,
1291 info=Qwen2_5_VLProcessingInfo,
1292 dummy_inputs=Qwen2_5_VLDummyInputsBuilder,
1293 )
1294 class Qwen2_5_VLForConditionalGeneration(
1295 nn.Module,
1296 SupportsMultiModal,
1297 SupportsEncoderCudaGraph,
1298 SupportsLoRA,
1299 SupportsPP,
1300 SupportsQuant,
1301 SupportsEagle,
1302 SupportsEagle3,
1303 SupportsMultiModalPruning,
1304 SupportsMRoPE,
1305 ):
1306 packed_modules_mapping = {
1307 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
1308 "gate_up_proj": ["gate_proj", "up_proj"],
1309 "qkv": ["qkv"], # For vision tower's already-packed QKV
1310 }
1311
1312 # To ensure correct weight loading and mapping.
1313 hf_to_vllm_mapper = WeightsMapper(
1314 orig_to_new_prefix={
1315 # mapping for new names in checkpoint saved after transformers v4.52
1316 "model.language_model.": "language_model.model.",
1317 "model.visual.": "visual.",
1318 # mapping for original checkpoint
1319 "lm_head.": "language_model.lm_head.",
1320 "model.": "language_model.model.",
1321 }
1322 )
1323
1324 supports_encoder_tp_data = True
1325 supports_mm_device_do_normalize = True
1326 supports_tower_connector_lora = True
1327
1328 def iter_mm_grid_thw(
1329 self, mm_features: list[MultiModalFeatureSpec]
1330 ) -> Iterator[tuple[int, int, int, int, float]]:
1331 """Iterate over multimodal features and yield grid information.
1332
1333 Args:
1334 mm_features: List of multimodal feature specifications
1335
1336 Yields:
1337 Tuple of (offset, grid_t, grid_h, grid_w, t_factor) for each frame/image
1338
1339 """
1340 spatial_merge_size = self.config.vision_config.spatial_merge_size
1341 tokens_per_second = getattr(self.config.vision_config, "tokens_per_second", 1.0)
1342 for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
1343 offset = mm_feature.mm_position.offset
1344 data = mm_feature.data
1345 assert data is not None
1346 if mm_feature.modality == "image":
1347 grid = data["image_grid_thw"].data
1348 assert isinstance(grid, torch.Tensor)
1349 t, h, w = grid.tolist()
1350 assert t == 1, f"Image must have 1 frame, got {t}"
1351 yield offset, 1, h // spatial_merge_size, w // spatial_merge_size, 1.0
1352 elif mm_feature.modality == "video":
1353 grid = data["video_grid_thw"].data
1354 assert isinstance(grid, torch.Tensor)
1355 t, h, w = grid.tolist()
1356 second_per_grid_ts = 1.0
1357 if seconds := data.get("second_per_grid_ts"):
1358 assert isinstance(seconds.data, torch.Tensor)
1359 second_per_grid_ts = seconds.data.item()
1360 t_factor = second_per_grid_ts * tokens_per_second
1361 yield (
1362 offset,
1363 t,
1364 h // spatial_merge_size,
1365 w // spatial_merge_size,
1366 t_factor,
1367 )
1368 else:
1369 raise ValueError(f"Unsupported modality: {mm_feature.modality}")
1370
1371 def get_mrope_input_positions(
1372 self,
1373 input_tokens: list[int],
1374 mm_features: list[MultiModalFeatureSpec],
1375 ) -> tuple[torch.Tensor, int]:
1376 llm_pos_ids_list: list = []
1377 st = 0
1378
1379 for (
1380 offset,
1381 llm_grid_t,
1382 llm_grid_h,
1383 llm_grid_w,
1384 t_factor,
1385 ) in self.iter_mm_grid_thw(mm_features):
1386 text_len = offset - st
1387 st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
1388 llm_pos_ids_list.append(
1389 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
1390 )
1391
1392 grid_indices = np.indices((llm_grid_t, llm_grid_h, llm_grid_w))
1393 if t_factor != 1.0:
1394 grid_indices[0] = (grid_indices[0] * t_factor).astype(np.int64)
1395 llm_pos_ids_list.append(grid_indices.reshape(3, -1) + text_len + st_idx)
1396 st = offset + llm_grid_t * llm_grid_h * llm_grid_w
1397
1398 if st < len(input_tokens):
1399 st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
1400 text_len = len(input_tokens) - st
1401 llm_pos_ids_list.append(
1402 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
1403 )
1404
1405 llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
1406 mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item()
1407
1408 return torch.from_numpy(llm_positions), mrope_position_delta
1409
1410 @classmethod
1411 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1412 if modality.startswith("image"):
1413 return "<|vision_start|><|image_pad|><|vision_end|>"
1414 if modality.startswith("video"):
1415 return "<|vision_start|><|video_pad|><|vision_end|>"
1416
1417 raise ValueError("Only image or video modality is supported")
1418
1419 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1420 super().__init__()
1421 config: Qwen2_5_VLConfig = vllm_config.model_config.hf_config
1422 multimodal_config = vllm_config.model_config.get_multimodal_config()
1423
1424 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1425 self.config = config
1426 self.model_config = vllm_config.model_config
1427 self.vllm_config = vllm_config
1428 self.multimodal_config = multimodal_config
1429 self.video_pruning_rate = multimodal_config.video_pruning_rate
1430 self.is_multimodal_pruning_enabled = (
1431 multimodal_config.is_multimodal_pruning_enabled()
1432 )
1433
1434 with self._mark_tower_model(vllm_config, {"image", "video"}):
1435 self.visual = Qwen2_5_VisionTransformer(
1436 vision_config=config.vision_config,
1437 norm_eps=getattr(config, "rms_norm_eps", 1e-6),
1438 quant_config=self.quant_config,
1439 input_norm=build_mm_input_norm(self.model_config),
1440 prefix=maybe_prefix(prefix, "visual"),
1441 )
1442
1443 with self._mark_language_model(vllm_config):
1444 self.language_model = init_vllm_registered_model(
1445 vllm_config=vllm_config,
1446 prefix=maybe_prefix(prefix, "language_model"),
1447 architectures=["Qwen2ForCausalLM"],
1448 )
1449
1450 self.make_empty_intermediate_tensors = (
1451 self.language_model.make_empty_intermediate_tensors
1452 )
1453
1454 def _parse_and_validate_image_input(
1455 self, **kwargs: object
1456 ) -> Qwen2_5_VLImageInputs | None:
1457 pixel_values = kwargs.pop("pixel_values", None)
1458 image_embeds = kwargs.pop("image_embeds", None)
1459 image_grid_thw = kwargs.pop("image_grid_thw", None)
1460
1461 if pixel_values is None and image_embeds is None:
1462 return None
1463
1464 if pixel_values is not None:
1465 return Qwen2_5_VLImagePixelInputs(
1466 type="pixel_values",
1467 pixel_values=pixel_values,
1468 image_grid_thw=image_grid_thw,
1469 )
1470
1471 else:
1472 return Qwen2_5_VLImageEmbeddingInputs(
1473 type="image_embeds",
1474 image_embeds=image_embeds,
1475 image_grid_thw=image_grid_thw,
1476 )
1477
1478 def _parse_and_validate_video_input(
1479 self, **kwargs: object
1480 ) -> Qwen2_5_VLVideoInputs | None:
1481 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1482 video_embeds = kwargs.pop("video_embeds", None)
1483 video_grid_thw = kwargs.pop("video_grid_thw", None)
1484 second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
1485
1486 if pixel_values_videos is None and video_embeds is None:
1487 return None
1488
1489 if pixel_values_videos is not None:
1490 return Qwen2_5_VLVideoPixelInputs(
1491 type="pixel_values_videos",
1492 pixel_values_videos=pixel_values_videos,
1493 video_grid_thw=video_grid_thw,
1494 second_per_grid_ts=second_per_grid_ts,
1495 )
1496
1497 else:
1498 return Qwen2_5_VLVideoEmbeddingInputs(
1499 type="video_embeds",
1500 video_embeds=video_embeds,
1501 video_grid_thw=video_grid_thw,
1502 second_per_grid_ts=second_per_grid_ts,
1503 )
1504
1505 def _process_image_input(
1506 self, image_input: Qwen2_5_VLImageInputs
1507 ) -> tuple[torch.Tensor, ...]:
1508 grid_thw = image_input["image_grid_thw"]
1509 assert grid_thw.ndim == 2
1510 grid_thw_list = grid_thw.tolist()
1511
1512 if image_input["type"] == "image_embeds":
1513 image_embeds = image_input["image_embeds"].type(self.visual.dtype)
1514 else:
1515 pixel_values = image_input["pixel_values"]
1516
1517 if self.use_data_parallel:
1518 return run_dp_sharded_mrope_vision_model(
1519 self.visual,
1520 pixel_values,
1521 grid_thw_list,
1522 rope_type="rope_3d",
1523 )
1524 else:
1525 image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
1526
1527 # Split concatenated embeddings for each image item.
1528 merge_size = self.visual.spatial_merge_size
1529 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1530 return image_embeds.split(sizes)
1531
1532 def _postprocess_image_embeds_evs(
1533 self,
1534 image_embeds_split: tuple[torch.Tensor, ...],
1535 image_input: Qwen2_5_VLImageInputs,
1536 ) -> tuple[torch.Tensor, ...]:
1537 """Append mrope positions for each for images.
1538 This is necessary to recover correct mrope
1539 positions after video pruning
1540
1541 Args:
1542 image_embeds_split: Tuple of image embeddings for
1543 each image item.
1544 image_input: Image input data.
1545
1546 Returns:
1547 Tuple of image embeddings for each image item.
1548 Resulting embeddings will have extra 4 channels for
1549 computed mrope positions.
1550
1551 """
1552 merge_size = self.visual.spatial_merge_size
1553 grid_thw = image_input["image_grid_thw"]
1554 grid_thw_list = grid_thw.tolist()
1555 image_embeds_out = []
1556 for emb, size in zip(image_embeds_split, grid_thw_list):
1557 positions = async_tensor_h2d(
1558 compute_mrope_for_media(size, merge_size), emb.device
1559 )
1560 emb = torch.cat([emb, positions], dim=1)
1561 image_embeds_out.append(emb)
1562 return tuple(image_embeds_out)
1563
1564 def _process_video_input(
1565 self, video_input: Qwen2_5_VLVideoInputs
1566 ) -> tuple[torch.Tensor, ...]:
1567 grid_thw = video_input["video_grid_thw"]
1568 assert grid_thw.ndim == 2
1569 grid_thw_list = grid_thw.tolist()
1570
1571 if video_input["type"] == "video_embeds":
1572 video_embeds = video_input["video_embeds"].type(self.visual.dtype)
1573 else:
1574 pixel_values_videos = video_input["pixel_values_videos"]
1575
1576 if self.use_data_parallel:
1577 return run_dp_sharded_mrope_vision_model(
1578 self.visual,
1579 pixel_values_videos,
1580 grid_thw_list,
1581 rope_type="rope_3d",
1582 )
1583 else:
1584 video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw_list)
1585
1586 # Split concatenated embeddings for each video item.
1587 merge_size = self.visual.spatial_merge_size
1588 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
1589 return video_embeds.split(sizes)
1590
1591 def _postprocess_video_embeds_evs(
1592 self,
1593 video_embeds_split: tuple[torch.Tensor, ...],
1594 video_input: Qwen2_5_VLVideoInputs,
1595 ) -> tuple[torch.Tensor, ...]:
1596 """Prunes video embeddings via Efficient Video Sampling (EVS)
1597 and then appends mrope positions for each retained embeddings
1598
1599 Args:
1600 video_embeds_split: Tuple of video embeddings for each video item.
1601 video_input: Video input data.
1602
1603 Returns:
1604 Tuple of video embeddings for each video item.
1605 Resulting embeddings will have extra 4 channels for
1606 computed mrope positions.
1607
1608 """
1609 grid_thw = video_input["video_grid_thw"]
1610 assert grid_thw.ndim == 2
1611 grid_thw_list = grid_thw.tolist()
1612 merge_size = self.visual.spatial_merge_size
1613
1614 # Cast to long to match the original code
1615 # https://github.com/huggingface/transformers/blob/41980ce93e775f6c88500c51c8db7946fc6a2add/src/transformers/models/qwen2_5_vl/modular_qwen2_5_vl.py#L491 # noqa
1616 second_per_grid_ts = video_input.get("second_per_grid_ts")
1617 if second_per_grid_ts is None:
1618 raise ValueError(
1619 "second_per_grid_ts is required when video_pruning_rate > 0 "
1620 "is enabled for video inputs, including the video_embeds path."
1621 )
1622 second_per_grid_ts = second_per_grid_ts.long()
1623 tokens_per_second = self.config.vision_config.tokens_per_second
1624
1625 video_embeds_out = []
1626 assert self.video_pruning_rate is not None
1627 for emb, size, video_second_per_grid_t in zip(
1628 video_embeds_split, grid_thw_list, second_per_grid_ts
1629 ):
1630 # For each video, we compute retention mask using EVS
1631 retention_mask = compute_retention_mask(
1632 emb,
1633 size,
1634 spatial_merge_size=self.visual.spatial_merge_size,
1635 q=self.video_pruning_rate,
1636 )
1637 positions_cpu = compute_mrope_for_media(
1638 size,
1639 merge_size,
1640 tokens_per_second=tokens_per_second,
1641 video_second_per_grid=video_second_per_grid_t.item(),
1642 )
1643 positions = async_tensor_h2d(positions_cpu, emb.device)
1644
1645 with gpu_sync_allowed():
1646 emb = emb[retention_mask]
1647 positions = positions[retention_mask]
1648 emb = torch.cat([emb, positions], dim=1)
1649 video_embeds_out.append(emb)
1650 return tuple(video_embeds_out)
1651
1652 def recompute_mrope_positions(
1653 self,
1654 input_ids: list[int] | torch.Tensor,
1655 multimodal_embeddings: Sequence[torch.Tensor],
1656 mrope_positions: torch.LongTensor,
1657 num_computed_tokens: int,
1658 ) -> tuple[Sequence[torch.Tensor], torch.Tensor, int]:
1659 """Update part of input mrope positions (starting with
1660 num_computed_tokens index). Original mrope_positions are computed
1661 for unpruned sequence and becomes incorrect once pruning occurs,
1662 so once we prune media tokens we should reflect this in the
1663 mrope_positions before we feed it to LLM.
1664
1665 Args:
1666 input_ids: (N,) All input tokens of the prompt (Containing
1667 entire sequence).
1668 multimodal_embeddings: Tuple of multimodal embeddings.
1669 mrope_positions: Existing mrope positions (3, N) for entire
1670 sequence
1671 num_computed_tokens: A number of computed tokens so far.
1672
1673 Returns:
1674 Tuple of (multimodal_embeddings, mrope_positions,
1675 mrope_position_delta).
1676
1677 """
1678 image_token_id = self.config.image_token_id
1679 video_token_id = self.config.video_token_id
1680 vision_start_token_id = self.config.vision_start_token_id
1681
1682 # Device
1683 device = (
1684 multimodal_embeddings[0].device
1685 if len(multimodal_embeddings)
1686 else mrope_positions.device
1687 )
1688
1689 # Tensors. input_ids may already be a (device-side) tensor.
1690 if isinstance(input_ids, torch.Tensor):
1691 assert input_ids.device == device
1692 input_ids_t = input_ids.to(torch.long)
1693 else:
1694 input_ids_t = async_tensor_h2d(input_ids, dtype=torch.long, device=device)
1695
1696 mm_embeddings_out = [mm[:, :-4] for mm in multimodal_embeddings]
1697 mm_embeddings_pos = [
1698 mm[:, -4:].permute(1, 0).long() for mm in multimodal_embeddings
1699 ]
1700
1701 with gpu_sync_allowed():
1702 positions, mrope_positions_delta = recompute_mrope_positions(
1703 input_ids_t,
1704 mm_embeddings_pos,
1705 mrope_positions,
1706 num_computed_tokens,
1707 vision_start_token_id,
1708 image_token_id,
1709 video_token_id,
1710 )
1711
1712 return mm_embeddings_out, positions, mrope_positions_delta
1713
1714 def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1715 mm_input_by_modality: dict[
1716 str, Qwen2_5_VLImageInputs | Qwen2_5_VLVideoInputs | None
1717 ] = {}
1718
1719 # Preserve the order of modalities if there are multiple of them
1720 # from the order of kwargs.
1721 for input_key in kwargs:
1722 if (
1723 input_key in ("pixel_values", "image_embeds")
1724 and "image" not in mm_input_by_modality
1725 ):
1726 mm_input_by_modality["image"] = self._parse_and_validate_image_input(
1727 **kwargs
1728 )
1729 if (
1730 input_key in ("pixel_values_videos", "video_embeds")
1731 and "video" not in mm_input_by_modality
1732 ):
1733 mm_input_by_modality["video"] = self._parse_and_validate_video_input(
1734 **kwargs
1735 )
1736 return mm_input_by_modality
1737
1738 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
1739 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
1740 if not mm_input_by_modality:
1741 return []
1742
1743 # The result multimodal_embeddings is tuple of tensors, with each
1744 # tensor correspoending to a multimodal data item (image or video).
1745 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1746
1747 # NOTE: It is important to iterate over the keys in this dictionary
1748 # to preserve the order of the modalities.
1749 for modality in mm_input_by_modality:
1750 multimodal_input = mm_input_by_modality[modality]
1751 if modality == "image":
1752 image_embeddings = self._process_image_input(multimodal_input)
1753 if self.is_multimodal_pruning_enabled:
1754 image_embeddings = self._postprocess_image_embeds_evs(
1755 image_embeddings, multimodal_input
1756 )
1757 multimodal_embeddings += tuple(image_embeddings)
1758 if modality == "video":
1759 video_embeddings = self._process_video_input(multimodal_input)
1760 if self.is_multimodal_pruning_enabled:
1761 video_embeddings = self._postprocess_video_embeds_evs(
1762 video_embeddings, multimodal_input
1763 )
1764 multimodal_embeddings += tuple(video_embeddings)
1765 return multimodal_embeddings
1766
1767 # -- SupportsEncoderCudaGraph protocol methods --
1768
1769 def get_encoder_cudagraph_config(self):
1770 from vllm.v1.worker.encoder_cudagraph_defs import (
1771 EncoderCudaGraphConfig,
1772 )
1773
1774 # NOTE: With EVS pruning enabled, multimodal embeddings are post-processed
1775 # (append positions for image and prune+append positions for video) in
1776 # embed_multimodal(). The encoder CUDA graph path bypasses that postprocess
1777 # hook, so disable CUDA graph for all modalities to avoid inconsistent
1778 # embedding formats between eager and cudagraph paths.
1779 modalities = [] if self.is_multimodal_pruning_enabled else ["image", "video"]
1780
1781 max_frames = self.get_max_frames_per_video() if "video" in modalities else 1
1782 cu_seqlens_padding = (
1783 _pad_flashinfer_cu_seqlens_buffer
1784 if self.visual.attn_backend == AttentionBackendEnum.FLASHINFER
1785 else _pad_cumulative_seqlens_buffer
1786 )
1787 return EncoderCudaGraphConfig(
1788 modalities=modalities,
1789 buffer_keys=[
1790 "pixel_values",
1791 "rotary_pos_emb_cos",
1792 "rotary_pos_emb_sin",
1793 "window_index",
1794 "reverse_indices",
1795 "cu_seqlens",
1796 "cu_window_seqlens",
1797 "max_seqlen_full",
1798 "max_seqlen_window",
1799 "sequence_lengths_full",
1800 "sequence_lengths_window",
1801 ],
1802 padding_logics={
1803 "cu_seqlens": cu_seqlens_padding,
1804 "cu_window_seqlens": cu_seqlens_padding,
1805 },
1806 out_hidden_size=self.visual.out_hidden_size,
1807 max_frames_per_video=max_frames,
1808 )
1809
1810 def get_input_modality(
1811 self,
1812 mm_kwargs: dict[str, Any],
1813 ) -> str:
1814 if "image_grid_thw" in mm_kwargs:
1815 return "image"
1816 elif "video_grid_thw" in mm_kwargs:
1817 return "video"
1818 raise AssertionError("This line should be unreachable.")
1819
1820 def get_max_frames_per_video(self) -> int:
1821 mm_registry = MULTIMODAL_REGISTRY
1822 info = mm_registry.get_processing_info(self.model_config)
1823 assert isinstance(info, Qwen2VLProcessingInfo)
1824 max_frames_per_video = info.get_num_frames_with_most_features(
1825 seq_len=self.model_config.max_model_len,
1826 mm_counts={"video": self.multimodal_config.get_limit_per_prompt("video")},
1827 )
1828 return max_frames_per_video
1829
1830 def get_encoder_cudagraph_budget_range(
1831 self,
1832 vllm_config: VllmConfig,
1833 ) -> tuple[int, int]:
1834 # Min: estimated smallest possible encoder input.
1835 # 224x224 image → 16x16 patches (patch_size=14)
1836 # spatial_merge_size=2 → 8x8 = 64 tokens
1837 min_budget = 64
1838 # Max: capped by max_num_batched_tokens
1839 max_budget = min(
1840 vllm_config.scheduler_config.max_num_batched_tokens,
1841 self.model_config.max_model_len,
1842 )
1843 return (min_budget, max_budget)
1844
1845 def _get_pixel_values_by_modality(
1846 self,
1847 mm_kwargs: dict[str, Any],
1848 ) -> torch.Tensor:
1849 if self.get_input_modality(mm_kwargs) == "image":
1850 pixel_values = mm_kwargs["pixel_values"]
1851 else:
1852 pixel_values = mm_kwargs["pixel_values_videos"]
1853 return pixel_values
1854
1855 def _get_grid_thw_by_modality(
1856 self,
1857 mm_kwargs: dict[str, Any],
1858 ) -> list[tuple[int, int, int]]:
1859 grid_thw_key = f"{self.get_input_modality(mm_kwargs)}_grid_thw"
1860 grid_thw = mm_kwargs[grid_thw_key]
1861 if not isinstance(grid_thw, list):
1862 grid_thw = grid_thw.tolist()
1863 return grid_thw
1864
1865 def get_encoder_cudagraph_item_specs(
1866 self,
1867 mm_kwargs: dict[str, Any],
1868 ):
1869 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
1870
1871 m = self.visual.spatial_merge_size
1872 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
1873 return [
1874 EncoderItemSpec(
1875 input_size=t * h * w,
1876 output_tokens=t * (h // m) * (w // m),
1877 )
1878 for t, h, w in grid_thw
1879 ]
1880
1881 def select_encoder_cudagraph_items(
1882 self,
1883 mm_kwargs: dict[str, Any],
1884 indices: list[int],
1885 ) -> dict[str, Any]:
1886 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
1887 pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
1888
1889 if len(indices) == 0:
1890 if self.get_input_modality(mm_kwargs) == "image":
1891 return {
1892 "pixel_values": pixel_values[:0],
1893 "image_grid_thw": [],
1894 }
1895 elif self.get_input_modality(mm_kwargs) == "video":
1896 return {
1897 "pixel_values_videos": pixel_values[:0],
1898 "video_grid_thw": [],
1899 }
1900 else:
1901 raise AssertionError("This line should be unreachable.")
1902
1903 # Compute cumulative patch offsets for slicing pixel_values
1904 patches_per_item = [t * h * w for t, h, w in grid_thw]
1905 cum_patches = [0]
1906 for p in patches_per_item:
1907 cum_patches.append(cum_patches[-1] + p)
1908
1909 selected_pv = torch.cat(
1910 [pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
1911 )
1912 selected_grid = [grid_thw[i] for i in indices]
1913
1914 if self.get_input_modality(mm_kwargs) == "image":
1915 return {
1916 "pixel_values": selected_pv,
1917 "image_grid_thw": selected_grid,
1918 }
1919 elif self.get_input_modality(mm_kwargs) == "video":
1920 return {
1921 "pixel_values_videos": selected_pv,
1922 "video_grid_thw": selected_grid,
1923 }
1924 else:
1925 raise AssertionError("This line should be unreachable.")
1926
1927 def prepare_encoder_cudagraph_capture_inputs(
1928 self,
1929 token_budget: int,
1930 max_batch_size: int,
1931 max_frames_per_batch: int,
1932 device: torch.device,
1933 dtype: torch.dtype,
1934 path: str = "default",
1935 axis_keys: tuple[Hashable, ...] | None = None,
1936 ):
1937 from vllm.v1.worker.encoder_cudagraph_defs import (
1938 EncoderCudaGraphCaptureInputs,
1939 )
1940
1941 spatial_merge_size = self.visual.spatial_merge_size
1942 max_window_seqs_per_batch = self.visual.get_encoder_cudagraph_max_window_seqs(
1943 token_budget, max_batch_size, max_frames_per_batch
1944 )
1945 # Use ceil here (not floor) so total captured capacity is never smaller
1946 # than token_budget when token_budget is not divisible by max_batch_size
1947 # (e.g., 324 budget with max_batch_size=8). Floor under-allocates
1948 # input_buffer and can fail replay copy for valid single-item batches.
1949 per_mm_item_output = (token_budget + max_batch_size - 1) // max_batch_size
1950
1951 frames_per_item = max_frames_per_batch // max_batch_size
1952 if frames_per_item > 1:
1953 # Build the capture grid using a video-format layout so that
1954 # cu_seqlens is sized for video replays from the start.
1955 # cu_seqlens has one entry per attention sequence (one per frame),
1956 # so using T > 1 per item makes the buffer large enough without
1957 # relying solely on padding.
1958 # Ceiling ensures frames_per_item * tokens_per_frame >= per_mm_item_output
1959 # so the pixel_values buffer covers any valid single-item replay.
1960 tokens_per_frame = (
1961 per_mm_item_output + frames_per_item - 1
1962 ) // frames_per_item
1963 # Video-format grid_config (T=frames_per_item).
1964 grid_config = [
1965 [
1966 frames_per_item,
1967 spatial_merge_size,
1968 tokens_per_frame * spatial_merge_size,
1969 ]
1970 for _ in range(max_batch_size)
1971 ]
1972 else:
1973 # Image-format grid_config (T=1).
1974 grid_config = [
1975 [1, spatial_merge_size, per_mm_item_output * spatial_merge_size]
1976 for _ in range(max_batch_size)
1977 ]
1978
1979 # Create dummy pixel_values; uint8 when normalization is fused
1980 # on-device. Contents are overwritten before every replay.
1981 patch_embed = self.visual.patch_embed
1982 in_channels = patch_embed.proj.in_channels
1983 patch_size = patch_embed.patch_size
1984 temporal_patch_size = patch_embed.temporal_patch_size
1985 total_patches = sum(t * h * w for t, h, w in grid_config)
1986 flattened_patch_size = (
1987 in_channels * temporal_patch_size * patch_size * patch_size
1988 )
1989 dummy_pixel_values = torch.zeros(
1990 total_patches,
1991 flattened_patch_size,
1992 device=device,
1993 dtype=self.visual.input_norm.input_dtype or dtype,
1994 )
1995
1996 # Override max_seqlen with a safe upper bound for capture.
1997 # max_seqlen.item() gets baked into the CUDA graph (not replayed),
1998 # so the capture value must cover any replay scenario.
1999 # Worst case: 1 item consuming the full budget ->
2000 # seq_len = token_budget * spatial_merge_size^2.
2001 # For window-attention, each local window is bounded by fixed geometry:
2002 # (window_size / patch_size / spatial_merge_size)^2 windows in merged
2003 # token space, multiplied by spatial_merge_size^2 to map back to the
2004 # unmerged sequence length used by attention kernels.
2005 vit_merger_window_size = (
2006 self.visual.window_size
2007 // self.visual.spatial_merge_size
2008 // self.visual.patch_size
2009 )
2010 max_seqlen_window_override = vit_merger_window_size**2 * (spatial_merge_size**2)
2011 metadata = self.visual.prepare_encoder_metadata(
2012 grid_config,
2013 max_batch_size=max_batch_size,
2014 max_frames_per_batch=max_frames_per_batch,
2015 max_window_seqs_per_batch=max_window_seqs_per_batch,
2016 max_seqlen_override=token_budget * (spatial_merge_size**2),
2017 max_seqlen_window_override=max_seqlen_window_override,
2018 device=device,
2019 )
2020
2021 # Just use image-modality dummy input_buffer for capturing, since it's also
2022 # compatible for video inputs (has the same shape: [num_patches, C*T*P*P]).
2023 values = metadata | {
2024 "pixel_values": dummy_pixel_values,
2025 }
2026
2027 return EncoderCudaGraphCaptureInputs(
2028 values=values,
2029 )
2030
2031 def prepare_encoder_cudagraph_replay_buffers(
2032 self,
2033 mm_kwargs: dict[str, Any],
2034 max_batch_size: int,
2035 max_frames_per_batch: int,
2036 path: str = "default",
2037 ):
2038 modality = self.get_input_modality(mm_kwargs)
2039 grid_thw_list = self._get_grid_thw_by_modality(mm_kwargs)
2040
2041 # Keep replay metadata sized to the actual batch. The captured buffers
2042 # may be larger, but EncoderCudaGraphManager fills the remaining
2043 # cu*_seqlens entries with the last cumulative offset to represent empty
2044 # sequences. Padding cu_window_seqlens here would require a static upper
2045 # bound and can over-pad window attention into many empty FlashAttention
2046 # CTAs.
2047 if modality == "image":
2048 metadata = self.visual.prepare_encoder_metadata(
2049 grid_thw_list,
2050 max_batch_size=max_batch_size,
2051 )
2052 elif modality == "video":
2053 metadata = self.visual.prepare_encoder_metadata(
2054 grid_thw_list,
2055 max_frames_per_batch=max_frames_per_batch,
2056 )
2057 else:
2058 raise AssertionError("This line should be unreachable.")
2059
2060 values = metadata | {
2061 "pixel_values": self._get_pixel_values_by_modality(mm_kwargs),
2062 }
2063 return EncoderCudaGraphReplayBuffers(values=values)
2064
2065 def encoder_cudagraph_forward(
2066 self,
2067 values: dict[str, torch.Tensor],
2068 path: str = "default",
2069 ) -> torch.Tensor:
2070 pixel_values = values.pop("pixel_values")
2071 metadata = values
2072 return self.visual(pixel_values, None, encoder_metadata=metadata)
2073
2074 def encoder_eager_forward(
2075 self,
2076 mm_kwargs: dict[str, Any],
2077 path: str = "default",
2078 ) -> torch.Tensor:
2079 pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
2080 grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
2081 return self.visual(pixel_values, grid_thw)
2082
2083 def forward(
2084 self,
2085 input_ids: torch.Tensor | None,
2086 positions: torch.Tensor,
2087 intermediate_tensors: IntermediateTensors | None = None,
2088 inputs_embeds: torch.Tensor | None = None,
2089 **kwargs: object,
2090 ) -> torch.Tensor | IntermediateTensors:
2091 """Run forward pass for Qwen2.5-VL.
2092
2093 Args:
2094 input_ids: Flattened (concatenated) input_ids corresponding to a
2095 batch.
2096 positions: Flattened (concatenated) position ids corresponding to a
2097 batch. **NOTE**: If mrope is enabled (default setting for
2098 Qwen2.5-VL opensource models), the shape will be `(3, seq_len)`,
2099 otherwise it will be `(seq_len,).
2100 intermediate_tensors: Intermediate tensors from prior forward pass.
2101 inputs_embeds: Optional tensor of input embeddings.
2102
2103 """
2104 if intermediate_tensors is not None:
2105 inputs_embeds = None
2106
2107 hidden_states = self.language_model.model(
2108 input_ids=input_ids,
2109 positions=positions,
2110 intermediate_tensors=intermediate_tensors,
2111 inputs_embeds=inputs_embeds,
2112 )
2113 return hidden_states
2114
2115 def compute_logits(
2116 self,
2117 hidden_states: torch.Tensor,
2118 ) -> torch.Tensor | None:
2119 return self.language_model.compute_logits(hidden_states)
2120
2121 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
2122 loader = AutoWeightsLoader(self)
2123 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
2124
2125 def get_mm_mapping(self) -> MultiModelKeys:
2126 """Get the module prefix in multimodal models."""
2127 return MultiModelKeys.from_string_field(
2128 language_model="language_model",
2129 connector="visual.merger.",
2130 tower_model="visual.",
2131 )
2132
2133 def get_mm_lora_token_counts(
2134 self,
2135 *,
2136 modality: str,
2137 mm_kwargs: MultiModalKwargsItem | None,
2138 num_mm_embeds: int,
2139 ) -> tuple[int, int | None]:
2140 del modality, mm_kwargs
2141 hf_config = self.config
2142 vision_config = hf_config.vision_config
2143 merge_size = vision_config.spatial_merge_size
2144 return num_mm_embeds * merge_size**2, num_mm_embeds