| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | # Copyright 2025 The vLLM team. |
| 5 | # Copyright 2025 The Qwen Team. |
| 6 | # Copyright 2025 The HuggingFace Inc. team. |
| 7 | # All rights reserved. |
| 8 | # |
| 9 | # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX |
| 10 | # and OPT implementations in this library. It has been modified from its |
| 11 | # original forms to accommodate minor architectural differences compared |
| 12 | # to GPT-NeoX and OPT used by the Meta AI team that trained the model. |
| 13 | # |
| 14 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 15 | # you may not use this file except in compliance with the License. |
| 16 | # You may obtain a copy of the License at |
| 17 | # |
| 18 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 19 | # |
| 20 | # Unless required by applicable law or agreed to in writing, software |
| 21 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 22 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 23 | # See the License for the specific language governing permissions and |
| 24 | # limitations under the License. |
| 25 | """Inference-only Qwen3VL model compatible with HuggingFace weights.""" |
| 26 | |
| 27 | from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence |
| 28 | from functools import lru_cache, partial |
| 29 | from itertools import islice |
| 30 | from typing import Any, Literal |
| 31 | |
| 32 | import numpy as np |
| 33 | import torch |
| 34 | import torch.nn as nn |
| 35 | import torch.nn.functional as F |
| 36 | from transformers import BatchFeature, ProcessorMixin |
| 37 | from transformers.models.qwen2_vl import Qwen2VLImageProcessor |
| 38 | from transformers.models.qwen2_vl.image_processing_qwen2_vl import ( |
| 39 | smart_resize as image_smart_resize, |
| 40 | ) |
| 41 | from transformers.models.qwen3_vl import Qwen3VLProcessor, Qwen3VLVideoProcessor |
| 42 | from transformers.models.qwen3_vl.configuration_qwen3_vl import ( |
| 43 | Qwen3VLConfig, |
| 44 | Qwen3VLVisionConfig, |
| 45 | ) |
| 46 | from transformers.models.qwen3_vl.video_processing_qwen3_vl import ( |
| 47 | smart_resize as video_smart_resize, |
| 48 | ) |
| 49 | from transformers.video_utils import VideoMetadata |
| 50 | |
| 51 | from vllm.compilation.decorators import ( |
| 52 | should_torch_compile_mm_encoder, |
| 53 | support_torch_compile, |
| 54 | ) |
| 55 | from vllm.config import VllmConfig |
| 56 | from vllm.config.multimodal import ( |
| 57 | MultiModalConfig, |
| 58 | MultiModalDummyOptions, |
| 59 | VideoDummyOptions, |
| 60 | VideoPruningMethod, |
| 61 | ) |
| 62 | from vllm.distributed import get_pp_group, parallel_state |
| 63 | from vllm.inputs import MultiModalDataDict |
| 64 | from vllm.logger import init_logger |
| 65 | from vllm.model_executor.layers.activation import _ACTIVATION_REGISTRY |
| 66 | from vllm.model_executor.layers.attention.mm_encoder_attention import ( |
| 67 | MMEncoderAttention, |
| 68 | ) |
| 69 | from vllm.model_executor.layers.conv import Conv3dLayer |
| 70 | from vllm.model_executor.layers.fusion.mm_input_norm import ( |
| 71 | IdentityInputNorm, |
| 72 | build_mm_input_norm, |
| 73 | ) |
| 74 | from vllm.model_executor.layers.linear import ( |
| 75 | ColumnParallelLinear, |
| 76 | RowParallelLinear, |
| 77 | ) |
| 78 | from vllm.model_executor.layers.logits_processor import LogitsProcessor |
| 79 | from vllm.model_executor.layers.quantization import QuantizationConfig |
| 80 | from vllm.model_executor.layers.rotary_embedding import get_rope |
| 81 | from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead |
| 82 | from vllm.model_executor.models.module_mapping import MultiModelKeys |
| 83 | from vllm.multimodal import MULTIMODAL_REGISTRY |
| 84 | from vllm.multimodal.inputs import ( |
| 85 | MultiModalFeatureSpec, |
| 86 | MultiModalFieldConfig, |
| 87 | MultiModalFieldElem, |
| 88 | MultiModalKwargsItem, |
| 89 | MultiModalKwargsItems, |
| 90 | PlaceholderRange, |
| 91 | VideoItem, |
| 92 | ) |
| 93 | from vllm.multimodal.parse import ImageSize, MultiModalDataItems |
| 94 | from vllm.multimodal.processing import ( |
| 95 | BaseDummyInputsBuilder, |
| 96 | BaseMultiModalProcessor, |
| 97 | PromptReplacement, |
| 98 | PromptUpdate, |
| 99 | PromptUpdateDetails, |
| 100 | ) |
| 101 | from vllm.multimodal.video_prune.evs import ( |
| 102 | compute_mrope_for_media, |
| 103 | compute_retained_tokens_count, |
| 104 | compute_retention_mask, |
| 105 | recompute_mrope_positions, |
| 106 | ) |
| 107 | from vllm.multimodal.video_prune.vidcom2 import ( |
| 108 | compute_retained_tokens_count as vidcom2_compute_retained_tokens_count, |
| 109 | ) |
| 110 | from vllm.multimodal.video_prune.vidcom2 import ( |
| 111 | compute_retention_mask as vidcom2_compute_retention_mask, |
| 112 | ) |
| 113 | from vllm.sequence import IntermediateTensors |
| 114 | from vllm.tokenizers.protocol import TokenizerLike |
| 115 | from vllm.tokenizers.registry import cached_tokenizer_from_config |
| 116 | from vllm.triton_utils import HAS_TRITON, tl, triton |
| 117 | from vllm.utils.cache import LRUCache |
| 118 | from vllm.utils.collection_utils import is_list_of |
| 119 | from vllm.utils.math_utils import cdiv, round_up |
| 120 | from vllm.utils.torch_utils import PIN_MEMORY |
| 121 | from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers |
| 122 | |
| 123 | from ...utils.gpu_sync_debug import gpu_sync_allowed |
| 124 | from ...utils.torch_utils import async_tensor_h2d |
| 125 | from .interfaces import ( |
| 126 | MultiModalEmbeddings, |
| 127 | SupportsEagle, |
| 128 | SupportsEagle3, |
| 129 | SupportsEncoderCudaGraph, |
| 130 | SupportsLoRA, |
| 131 | SupportsMRoPE, |
| 132 | SupportsMultiModal, |
| 133 | SupportsMultiModalPruning, |
| 134 | SupportsPP, |
| 135 | _require_is_multimodal, |
| 136 | ) |
| 137 | from .qwen2_5_vl import ( |
| 138 | Qwen2_5_VisionAttention, |
| 139 | Qwen2_5_VLImageEmbeddingInputs, |
| 140 | Qwen2_5_VLImageInputs, |
| 141 | Qwen2_5_VLImagePixelInputs, |
| 142 | Qwen2_5_VLVideoEmbeddingInputs, |
| 143 | Qwen2_5_VLVideoInputs, |
| 144 | Qwen2_5_VLVideoPixelInputs, |
| 145 | ) |
| 146 | from .qwen2_vl import ( |
| 147 | Qwen2VLMultiModalDataParser, |
| 148 | Qwen2VLProcessingInfo, |
| 149 | _create_qwen2vl_field_factory, |
| 150 | ) |
| 151 | from .qwen3 import Qwen3ForCausalLM, Qwen3Model |
| 152 | from .utils import ( |
| 153 | AutoWeightsLoader, |
| 154 | PPMissingLayer, |
| 155 | StageMissingLayer, |
| 156 | WeightsMapper, |
| 157 | _merge_multimodal_embeddings, |
| 158 | maybe_prefix, |
| 159 | ) |
| 160 | from .vision import ( |
| 161 | get_fp8_padded_hidden_size, |
| 162 | get_vit_attn_backend, |
| 163 | is_vit_use_data_parallel, |
| 164 | run_dp_sharded_mrope_vision_model, |
| 165 | ) |
| 166 | |
| 167 | logger = init_logger(__name__) |
| 168 | |
| 169 | # We use 2048 dummy video frames that would generate vision embeddings |
| 170 | # of the maximum size. |
| 171 | DUMMY_VIDEO_NUM_FRAMES = 2048 |
| 172 | |
| 173 | # --------------------------------------------------------------------------- |
| 174 | # Triton kernel: fused bilinear position-embedding interpolation |
| 175 | # --------------------------------------------------------------------------- |
| 176 | # Replaces many small eager-mode CUDA kernels with a single launch. |
| 177 | # The spatial-merge reorder is baked into the index math so the output |
| 178 | # is ready to be added to the patch embeddings directly. |
| 179 | # --------------------------------------------------------------------------- |
| 180 | |
| 181 | if HAS_TRITON: |
| 182 | |
| 183 | @triton.jit |
| 184 | def _bilinear_pos_embed_kernel( |
| 185 | embed_ptr, |
| 186 | output_ptr, |
| 187 | H, |
| 188 | W, |
| 189 | h_scale, |
| 190 | w_scale, |
| 191 | NUM_GRID: tl.constexpr, |
| 192 | M_SIZE: tl.constexpr, |
| 193 | HIDDEN_DIM: tl.constexpr, |
| 194 | BLOCK_D: tl.constexpr, |
| 195 | ): |
| 196 | """Fused bilinear pos-embed interpolation with spatial-merge reorder.""" |
| 197 | pid = tl.program_id(0) |
| 198 | total_spatial = H * W |
| 199 | spatial_idx = pid % total_spatial |
| 200 | |
| 201 | num_blocks_w = W // M_SIZE |
| 202 | block_idx = spatial_idx // (M_SIZE * M_SIZE) |
| 203 | local_idx = spatial_idx % (M_SIZE * M_SIZE) |
| 204 | br = block_idx // num_blocks_w |
| 205 | bc = block_idx % num_blocks_w |
| 206 | lr = local_idx // M_SIZE |
| 207 | lc = local_idx % M_SIZE |
| 208 | row = br * M_SIZE + lr |
| 209 | col = bc * M_SIZE + lc |
| 210 | |
| 211 | h_frac = row.to(tl.float32) * h_scale |
| 212 | w_frac = col.to(tl.float32) * w_scale |
| 213 | |
| 214 | hf = tl.math.floor(h_frac).to(tl.int32) |
| 215 | wf = tl.math.floor(w_frac).to(tl.int32) |
| 216 | hc = tl.minimum(hf + 1, NUM_GRID - 1) |
| 217 | wc = tl.minimum(wf + 1, NUM_GRID - 1) |
| 218 | |
| 219 | dh = h_frac - hf.to(tl.float32) |
| 220 | dw = w_frac - wf.to(tl.float32) |
| 221 | w11 = dh * dw |
| 222 | w10 = dh - w11 |
| 223 | w01 = dw - w11 |
| 224 | w00 = 1.0 - dh - w01 |
| 225 | |
| 226 | off00 = (hf * NUM_GRID + wf) * HIDDEN_DIM |
| 227 | off01 = (hf * NUM_GRID + wc) * HIDDEN_DIM |
| 228 | off10 = (hc * NUM_GRID + wf) * HIDDEN_DIM |
| 229 | off11 = (hc * NUM_GRID + wc) * HIDDEN_DIM |
| 230 | out_off = pid * HIDDEN_DIM |
| 231 | |
| 232 | # Cast weights to output dtype so the multiply-accumulate stays |
| 233 | # in the same precision as the native PyTorch implementation. |
| 234 | out_dtype = output_ptr.dtype.element_ty |
| 235 | w00_c = w00.to(out_dtype) |
| 236 | w01_c = w01.to(out_dtype) |
| 237 | w10_c = w10.to(out_dtype) |
| 238 | w11_c = w11.to(out_dtype) |
| 239 | |
| 240 | for d in tl.range(0, HIDDEN_DIM, BLOCK_D): |
| 241 | cols = d + tl.arange(0, BLOCK_D) |
| 242 | mask = cols < HIDDEN_DIM |
| 243 | |
| 244 | e00 = tl.load(embed_ptr + off00 + cols, mask=mask) |
| 245 | e01 = tl.load(embed_ptr + off01 + cols, mask=mask) |
| 246 | e10 = tl.load(embed_ptr + off10 + cols, mask=mask) |
| 247 | e11 = tl.load(embed_ptr + off11 + cols, mask=mask) |
| 248 | |
| 249 | val = w00_c * e00 + w01_c * e01 + w10_c * e10 + w11_c * e11 |
| 250 | |
| 251 | tl.store(output_ptr + out_off + cols, val, mask=mask) |
| 252 | |
| 253 | def triton_pos_embed_interpolate( |
| 254 | embed_weight: torch.Tensor, |
| 255 | t: int, |
| 256 | h: int, |
| 257 | w: int, |
| 258 | num_grid_per_side: int, |
| 259 | m_size: int, |
| 260 | dtype: torch.dtype, |
| 261 | ) -> torch.Tensor: |
| 262 | """Launch the fused Triton kernel for one (t,h,w) grid. |
| 263 | |
| 264 | Returns a tensor of shape ``(t * h * w, hidden_dim)`` with the |
| 265 | bilinearly-interpolated position embeddings in spatial-merge order. |
| 266 | """ |
| 267 | assert h % m_size == 0 and w % m_size == 0, ( |
| 268 | f"h={h} and w={w} must be divisible by m_size={m_size}" |
| 269 | ) |
| 270 | hidden_dim = embed_weight.shape[1] |
| 271 | total_out = t * h * w |
| 272 | output = torch.empty( |
| 273 | total_out, |
| 274 | hidden_dim, |
| 275 | device=embed_weight.device, |
| 276 | dtype=dtype, |
| 277 | ) |
| 278 | |
| 279 | h_scale = float(num_grid_per_side - 1) / float(h - 1) if h > 1 else 0.0 |
| 280 | w_scale = float(num_grid_per_side - 1) / float(w - 1) if w > 1 else 0.0 |
| 281 | |
| 282 | BLOCK_D = triton.next_power_of_2(hidden_dim) |
| 283 | |
| 284 | _bilinear_pos_embed_kernel[(total_out,)]( |
| 285 | embed_weight, |
| 286 | output, |
| 287 | h, |
| 288 | w, |
| 289 | h_scale, |
| 290 | w_scale, |
| 291 | num_grid_per_side, |
| 292 | m_size, |
| 293 | hidden_dim, |
| 294 | BLOCK_D, |
| 295 | ) |
| 296 | return output |
| 297 | |
| 298 | |
| 299 | def pos_embed_interpolate_native( |
| 300 | embed_weight: torch.Tensor, |
| 301 | t: int, |
| 302 | h: int, |
| 303 | w: int, |
| 304 | num_grid_per_side: int, |
| 305 | m_size: int, |
| 306 | dtype: torch.dtype, |
| 307 | ) -> torch.Tensor: |
| 308 | """Eager PyTorch bilinear position-embedding interpolation. |
| 309 | |
| 310 | Returns a tensor of shape ``(t * h * w, hidden_dim)`` with the |
| 311 | bilinearly-interpolated position embeddings in spatial-merge order. |
| 312 | """ |
| 313 | assert h % m_size == 0 and w % m_size == 0, ( |
| 314 | f"h={h} and w={w} must be divisible by m_size={m_size}" |
| 315 | ) |
| 316 | hidden_dim = embed_weight.shape[1] |
| 317 | device = embed_weight.device |
| 318 | |
| 319 | h_idxs = torch.linspace( |
| 320 | 0, |
| 321 | num_grid_per_side - 1, |
| 322 | h, |
| 323 | dtype=torch.float32, |
| 324 | device=device, |
| 325 | ) |
| 326 | w_idxs = torch.linspace( |
| 327 | 0, |
| 328 | num_grid_per_side - 1, |
| 329 | w, |
| 330 | dtype=torch.float32, |
| 331 | device=device, |
| 332 | ) |
| 333 | |
| 334 | h_floor = h_idxs.to(torch.long) |
| 335 | w_floor = w_idxs.to(torch.long) |
| 336 | h_ceil = torch.clamp(h_floor + 1, max=num_grid_per_side - 1) |
| 337 | w_ceil = torch.clamp(w_floor + 1, max=num_grid_per_side - 1) |
| 338 | |
| 339 | dh = h_idxs - h_floor |
| 340 | dw = w_idxs - w_floor |
| 341 | |
| 342 | dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij") |
| 343 | h_floor_grid, w_floor_grid = torch.meshgrid(h_floor, w_floor, indexing="ij") |
| 344 | h_ceil_grid, w_ceil_grid = torch.meshgrid(h_ceil, w_ceil, indexing="ij") |
| 345 | |
| 346 | w11 = dh_grid * dw_grid |
| 347 | w10 = dh_grid - w11 |
| 348 | w01 = dw_grid - w11 |
| 349 | w00 = 1 - dh_grid - w01 |
| 350 | |
| 351 | h_grid = torch.stack([h_floor_grid, h_floor_grid, h_ceil_grid, h_ceil_grid]) |
| 352 | w_grid = torch.stack([w_floor_grid, w_ceil_grid, w_floor_grid, w_ceil_grid]) |
| 353 | h_grid_idx = h_grid * num_grid_per_side |
| 354 | |
| 355 | indices = (h_grid_idx + w_grid).reshape(4, -1) |
| 356 | weights = torch.stack([w00, w01, w10, w11], dim=0).reshape(4, -1, 1) |
| 357 | weights = weights.to(dtype=dtype) |
| 358 | |
| 359 | embeds = embed_weight[indices] |
| 360 | embeds *= weights |
| 361 | combined = embeds.sum(dim=0) |
| 362 | |
| 363 | combined = combined.reshape(h // m_size, m_size, w // m_size, m_size, hidden_dim) |
| 364 | combined = combined.permute(0, 2, 1, 3, 4).reshape(1, -1, hidden_dim) |
| 365 | repeated = combined.expand(t, -1, -1).reshape(-1, hidden_dim) |
| 366 | return repeated.to(dtype=dtype) |
| 367 | |
| 368 | |
| 369 | @support_torch_compile( |
| 370 | dynamic_arg_dims={ |
| 371 | "x": 0, |
| 372 | }, |
| 373 | enable_if=should_torch_compile_mm_encoder, |
| 374 | is_encoder=True, |
| 375 | ) |
| 376 | class Qwen3_VisionPatchEmbed(nn.Module): |
| 377 | def __init__( |
| 378 | self, |
| 379 | patch_size: int = 14, |
| 380 | temporal_patch_size: int = 2, |
| 381 | in_channels: int = 3, |
| 382 | hidden_size: int = 1152, |
| 383 | ) -> None: |
| 384 | super().__init__() |
| 385 | self.patch_size = patch_size |
| 386 | self.temporal_patch_size = temporal_patch_size |
| 387 | self.hidden_size = hidden_size |
| 388 | |
| 389 | kernel_size = (temporal_patch_size, patch_size, patch_size) |
| 390 | self.proj = Conv3dLayer( |
| 391 | in_channels, |
| 392 | hidden_size, |
| 393 | kernel_size=kernel_size, |
| 394 | stride=kernel_size, |
| 395 | bias=True, |
| 396 | ) |
| 397 | |
| 398 | def forward(self, x: torch.Tensor) -> torch.Tensor: |
| 399 | L, C = x.shape |
| 400 | x = x.view(L, -1, self.temporal_patch_size, self.patch_size, self.patch_size) |
| 401 | x = self.proj(x).view(L, self.hidden_size) |
| 402 | return x |
| 403 | |
| 404 | |
| 405 | class Qwen3_VisionMLP(nn.Module): |
| 406 | def __init__( |
| 407 | self, |
| 408 | in_features: int, |
| 409 | hidden_features: int, |
| 410 | bias: bool = False, |
| 411 | act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu, |
| 412 | quant_config: QuantizationConfig | None = None, |
| 413 | prefix: str = "", |
| 414 | ): |
| 415 | super().__init__() |
| 416 | use_data_parallel = is_vit_use_data_parallel() |
| 417 | self.linear_fc1 = ColumnParallelLinear( |
| 418 | in_features, |
| 419 | hidden_features, |
| 420 | bias=bias, |
| 421 | quant_config=quant_config, |
| 422 | return_bias=False, |
| 423 | prefix=f"{prefix}.linear_fc1", |
| 424 | disable_tp=use_data_parallel, |
| 425 | ) |
| 426 | self.linear_fc2 = RowParallelLinear( |
| 427 | hidden_features, |
| 428 | in_features, |
| 429 | bias=bias, |
| 430 | quant_config=quant_config, |
| 431 | return_bias=False, |
| 432 | prefix=f"{prefix}.linear_fc2", |
| 433 | disable_tp=use_data_parallel, |
| 434 | ) |
| 435 | self.act_fn = act_fn |
| 436 | |
| 437 | def forward(self, x: torch.Tensor): |
| 438 | mlp_output = self.linear_fc2(self.act_fn(self.linear_fc1(x))) |
| 439 | return mlp_output |
| 440 | |
| 441 | |
| 442 | @support_torch_compile( |
| 443 | dynamic_arg_dims={ |
| 444 | "x": 0, |
| 445 | "cu_seqlens": 0, |
| 446 | "rotary_pos_emb_cos": 0, |
| 447 | "rotary_pos_emb_sin": 0, |
| 448 | "sequence_lengths": 0, |
| 449 | }, |
| 450 | enable_if=should_torch_compile_mm_encoder, |
| 451 | is_encoder=True, |
| 452 | ) |
| 453 | class Qwen3_VisionBlock(nn.Module): |
| 454 | def __init__( |
| 455 | self, |
| 456 | dim: int, |
| 457 | num_heads: int, |
| 458 | mlp_hidden_dim: int, |
| 459 | act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu, |
| 460 | norm_layer: Callable[[int], nn.Module] | None = None, |
| 461 | quant_config: QuantizationConfig | None = None, |
| 462 | prefix: str = "", |
| 463 | ) -> None: |
| 464 | super().__init__() |
| 465 | if norm_layer is None: |
| 466 | norm_layer = partial(nn.LayerNorm, eps=1e-6) |
| 467 | self.norm1 = norm_layer(dim) |
| 468 | self.norm2 = norm_layer(dim) |
| 469 | self.attn = Qwen2_5_VisionAttention( |
| 470 | embed_dim=dim, |
| 471 | num_heads=num_heads, |
| 472 | projection_size=dim, |
| 473 | quant_config=quant_config, |
| 474 | prefix=f"{prefix}.attn", |
| 475 | ) |
| 476 | self.mlp = Qwen3_VisionMLP( |
| 477 | dim, |
| 478 | mlp_hidden_dim, |
| 479 | act_fn=act_fn, |
| 480 | bias=True, |
| 481 | quant_config=quant_config, |
| 482 | prefix=f"{prefix}.mlp", |
| 483 | ) |
| 484 | |
| 485 | def forward( |
| 486 | self, |
| 487 | x: torch.Tensor, |
| 488 | cu_seqlens: torch.Tensor, |
| 489 | rotary_pos_emb_cos: torch.Tensor, |
| 490 | rotary_pos_emb_sin: torch.Tensor, |
| 491 | max_seqlen: torch.Tensor, # Only used for Flash Attention |
| 492 | sequence_lengths: torch.Tensor, # Only used for FlashInfer CuDNN backend |
| 493 | ) -> torch.Tensor: |
| 494 | x = x + self.attn( |
| 495 | self.norm1(x), |
| 496 | cu_seqlens=cu_seqlens, |
| 497 | rotary_pos_emb_cos=rotary_pos_emb_cos, |
| 498 | rotary_pos_emb_sin=rotary_pos_emb_sin, |
| 499 | max_seqlen=max_seqlen, |
| 500 | sequence_lengths=sequence_lengths, |
| 501 | ) |
| 502 | |
| 503 | x = x + self.mlp(self.norm2(x)) |
| 504 | return x |
| 505 | |
| 506 | |
| 507 | class Qwen3_VisionPatchMerger(nn.Module): |
| 508 | def __init__( |
| 509 | self, |
| 510 | d_model: int, |
| 511 | context_dim: int, |
| 512 | norm_layer: Callable[[int], nn.Module] | None = None, |
| 513 | spatial_merge_size: int = 2, |
| 514 | use_postshuffle_norm: bool = False, |
| 515 | quant_config: QuantizationConfig | None = None, |
| 516 | prefix: str = "", |
| 517 | ) -> None: |
| 518 | super().__init__() |
| 519 | use_data_parallel = is_vit_use_data_parallel() |
| 520 | self.hidden_size = context_dim * (spatial_merge_size**2) |
| 521 | |
| 522 | self.use_postshuffle_norm = use_postshuffle_norm |
| 523 | if self.use_postshuffle_norm: |
| 524 | context_dim = self.hidden_size |
| 525 | |
| 526 | if norm_layer is None: |
| 527 | norm_layer = partial(nn.LayerNorm, eps=1e-6) |
| 528 | self.norm = norm_layer(context_dim) |
| 529 | self.linear_fc1 = ColumnParallelLinear( |
| 530 | self.hidden_size, |
| 531 | self.hidden_size, |
| 532 | bias=True, |
| 533 | quant_config=quant_config, |
| 534 | prefix=f"{prefix}.linear_fc1", |
| 535 | disable_tp=use_data_parallel, |
| 536 | ) |
| 537 | self.act_fn = nn.GELU() |
| 538 | self.linear_fc2 = RowParallelLinear( |
| 539 | self.hidden_size, |
| 540 | d_model, |
| 541 | bias=True, |
| 542 | quant_config=quant_config, |
| 543 | prefix=f"{prefix}.linear_fc2", |
| 544 | disable_tp=use_data_parallel, |
| 545 | ) |
| 546 | |
| 547 | def forward(self, x: torch.Tensor) -> torch.Tensor: |
| 548 | if self.use_postshuffle_norm: |
| 549 | x = self.norm(x.view(-1, self.hidden_size)) |
| 550 | else: |
| 551 | x = self.norm(x).view(-1, self.hidden_size) |
| 552 | |
| 553 | x_parallel, _ = self.linear_fc1(x) |
| 554 | x_parallel = self.act_fn(x_parallel) |
| 555 | out, _ = self.linear_fc2(x_parallel) |
| 556 | return out |
| 557 | |
| 558 | |
| 559 | class Qwen3_VisionTransformer(nn.Module): |
| 560 | hf_to_vllm_mapper = WeightsMapper( |
| 561 | orig_to_new_stacked={ |
| 562 | "attn.q.": ("attn.qkv.", "q"), |
| 563 | "attn.k.": ("attn.qkv.", "k"), |
| 564 | "attn.v.": ("attn.qkv.", "v"), |
| 565 | } |
| 566 | ) |
| 567 | |
| 568 | def __init__( |
| 569 | self, |
| 570 | vision_config: Qwen3VLVisionConfig, |
| 571 | norm_eps: float = 1e-6, |
| 572 | quant_config: QuantizationConfig | None = None, |
| 573 | input_norm: nn.Module | None = None, |
| 574 | prefix: str = "", |
| 575 | ) -> None: |
| 576 | super().__init__() |
| 577 | self.hidden_size = vision_config.hidden_size |
| 578 | self.num_heads = vision_config.num_heads |
| 579 | self.num_position_embeddings = vision_config.num_position_embeddings |
| 580 | self.patch_size = vision_config.patch_size |
| 581 | self.spatial_merge_size = vision_config.spatial_merge_size |
| 582 | self.spatial_merge_unit = self.spatial_merge_size**2 |
| 583 | self.temporal_patch_size = vision_config.temporal_patch_size |
| 584 | self.deepstack_visual_indexes = ( |
| 585 | vision_config.deepstack_visual_indexes |
| 586 | if hasattr(vision_config, "deepstack_visual_indexes") |
| 587 | else [] |
| 588 | ) |
| 589 | self.num_grid_per_side = int(self.num_position_embeddings**0.5) |
| 590 | self._rot_pos_ids_cache: LRUCache[tuple[int, int, int], torch.Tensor] = ( |
| 591 | LRUCache(capacity=1024) |
| 592 | ) |
| 593 | |
| 594 | use_data_parallel = is_vit_use_data_parallel() |
| 595 | self.tp_size = ( |
| 596 | 1 |
| 597 | if use_data_parallel |
| 598 | else parallel_state.get_tensor_model_parallel_world_size() |
| 599 | ) |
| 600 | |
| 601 | # NOTE: This is used for creating empty tensor for all_gather for |
| 602 | # DP ViT. Here out_hidden_size is enlarged due to deepstack |
| 603 | self.out_hidden_size = vision_config.out_hidden_size * ( |
| 604 | 1 + len(self.deepstack_visual_indexes) |
| 605 | ) |
| 606 | |
| 607 | self.patch_embed = Qwen3_VisionPatchEmbed( |
| 608 | patch_size=self.patch_size, |
| 609 | temporal_patch_size=self.temporal_patch_size, |
| 610 | in_channels=vision_config.in_channels, |
| 611 | hidden_size=self.hidden_size, |
| 612 | ) |
| 613 | self.input_norm = input_norm if input_norm is not None else IdentityInputNorm() |
| 614 | |
| 615 | self.pos_embed = nn.Embedding(self.num_position_embeddings, self.hidden_size) |
| 616 | |
| 617 | norm_layer = partial(nn.LayerNorm, eps=norm_eps) |
| 618 | head_dim = self.hidden_size // self.num_heads |
| 619 | |
| 620 | # FP8 attention: Q/K/V become independent contiguous tensors |
| 621 | # after quantization, so cu_seqlens uses uniform stride (no 3x V). |
| 622 | self.fp8_padded_hidden_size = get_fp8_padded_hidden_size( |
| 623 | self.num_heads, head_dim |
| 624 | ) |
| 625 | |
| 626 | self.rotary_pos_emb = get_rope( |
| 627 | head_size=head_dim, |
| 628 | max_position=8192, |
| 629 | is_neox_style=True, |
| 630 | rope_parameters={"partial_rotary_factor": 0.5}, |
| 631 | ) |
| 632 | |
| 633 | self.merger = Qwen3_VisionPatchMerger( |
| 634 | d_model=vision_config.out_hidden_size, |
| 635 | context_dim=self.hidden_size, |
| 636 | norm_layer=norm_layer, |
| 637 | spatial_merge_size=self.spatial_merge_size, |
| 638 | quant_config=quant_config, |
| 639 | prefix=f"{prefix}.merger", |
| 640 | ) |
| 641 | |
| 642 | self.deepstack_merger_list = nn.ModuleList( |
| 643 | [ |
| 644 | Qwen3_VisionPatchMerger( |
| 645 | d_model=vision_config.out_hidden_size, |
| 646 | context_dim=self.hidden_size, |
| 647 | spatial_merge_size=self.spatial_merge_size, |
| 648 | use_postshuffle_norm=True, |
| 649 | norm_layer=norm_layer, |
| 650 | quant_config=quant_config, |
| 651 | prefix=f"{prefix}.deepstack_merger_list.{layer_idx}", |
| 652 | ) |
| 653 | for layer_idx in range(len(self.deepstack_visual_indexes)) |
| 654 | ] |
| 655 | ) |
| 656 | |
| 657 | self.attn_backend = get_vit_attn_backend( |
| 658 | head_size=head_dim, |
| 659 | dtype=torch.get_default_dtype(), |
| 660 | ) |
| 661 | |
| 662 | self.blocks = nn.ModuleList( |
| 663 | [ |
| 664 | Qwen3_VisionBlock( |
| 665 | dim=self.hidden_size, |
| 666 | num_heads=self.num_heads, |
| 667 | mlp_hidden_dim=vision_config.intermediate_size, |
| 668 | act_fn=_ACTIVATION_REGISTRY[vision_config.hidden_act], |
| 669 | norm_layer=norm_layer, |
| 670 | quant_config=quant_config, |
| 671 | prefix=f"{prefix}.blocks.{layer_idx}", |
| 672 | ) |
| 673 | for layer_idx in range(vision_config.depth) |
| 674 | ] |
| 675 | ) |
| 676 | |
| 677 | @property |
| 678 | def dtype(self) -> torch.dtype: |
| 679 | return self.patch_embed.proj.weight.dtype |
| 680 | |
| 681 | @property |
| 682 | def device(self) -> torch.device: |
| 683 | return self.patch_embed.proj.weight.device |
| 684 | |
| 685 | def rot_pos_ids(self, h: int, w: int, spatial_merge_size: int) -> torch.Tensor: |
| 686 | cache_key = (h, w, spatial_merge_size) |
| 687 | if cache_key in self._rot_pos_ids_cache: |
| 688 | return self._rot_pos_ids_cache[cache_key] |
| 689 | |
| 690 | hpos_ids = np.broadcast_to(np.arange(h).reshape(h, 1), (h, w)) |
| 691 | h_div = h // spatial_merge_size |
| 692 | w_div = w // spatial_merge_size |
| 693 | hpos_ids = hpos_ids.reshape( |
| 694 | h_div, |
| 695 | spatial_merge_size, |
| 696 | w_div, |
| 697 | spatial_merge_size, |
| 698 | ) |
| 699 | hpos_ids = hpos_ids.transpose(0, 2, 1, 3) |
| 700 | hpos_ids = hpos_ids.flatten() |
| 701 | |
| 702 | wpos_ids = np.broadcast_to(np.arange(w).reshape(1, w), (h, w)) |
| 703 | wpos_ids = wpos_ids.reshape( |
| 704 | h_div, |
| 705 | spatial_merge_size, |
| 706 | w_div, |
| 707 | spatial_merge_size, |
| 708 | ) |
| 709 | wpos_ids = wpos_ids.transpose(0, 2, 1, 3) |
| 710 | wpos_ids = wpos_ids.flatten() |
| 711 | |
| 712 | result = torch.from_numpy(np.stack([hpos_ids, wpos_ids], axis=-1)) |
| 713 | self._rot_pos_ids_cache[cache_key] = result |
| 714 | return result |
| 715 | |
| 716 | def rot_pos_emb(self, grid_thw: Sequence[Sequence[int]]): |
| 717 | max_grid_size = max(max(h, w) for _, h, w in grid_thw) |
| 718 | pos_ids = [ |
| 719 | self.rot_pos_ids(h, w, self.spatial_merge_size) |
| 720 | if t == 1 |
| 721 | else self.rot_pos_ids(h, w, self.spatial_merge_size).repeat(t, 1) |
| 722 | for t, h, w in grid_thw |
| 723 | ] |
| 724 | num_pos = sum(p.shape[0] for p in pos_ids) |
| 725 | pinned = torch.empty( |
| 726 | (num_pos, pos_ids[0].shape[1]), |
| 727 | dtype=pos_ids[0].dtype, |
| 728 | pin_memory=PIN_MEMORY, |
| 729 | ) |
| 730 | pos_ids = torch.cat(pos_ids, dim=0, out=pinned).to( |
| 731 | self.device, non_blocking=True |
| 732 | ) |
| 733 | |
| 734 | # Use pre-computed cos_sin_cache from RotaryEmbedding |
| 735 | cos, sin = self.rotary_pos_emb.get_cos_sin(max_grid_size) |
| 736 | |
| 737 | cos_combined = cos[pos_ids].flatten(1) |
| 738 | sin_combined = sin[pos_ids].flatten(1) |
| 739 | |
| 740 | return cos_combined, sin_combined |
| 741 | |
| 742 | def fast_pos_embed_interpolate( |
| 743 | self, grid_thw: Sequence[Sequence[int]] |
| 744 | ) -> torch.Tensor: |
| 745 | interpolate_fn = ( |
| 746 | triton_pos_embed_interpolate if HAS_TRITON else pos_embed_interpolate_native |
| 747 | ) |
| 748 | outputs = [] |
| 749 | for t, h, w in grid_thw: |
| 750 | outputs.append( |
| 751 | interpolate_fn( |
| 752 | self.pos_embed.weight, |
| 753 | t, |
| 754 | h, |
| 755 | w, |
| 756 | self.num_grid_per_side, |
| 757 | self.spatial_merge_size, |
| 758 | self.dtype, |
| 759 | ) |
| 760 | ) |
| 761 | return torch.cat(outputs, dim=0) |
| 762 | |
| 763 | def prepare_encoder_metadata( |
| 764 | self, |
| 765 | grid_thw_list: Sequence[Sequence[int]], |
| 766 | *, |
| 767 | max_batch_size: int | None = None, |
| 768 | max_frames_per_batch: int | None = None, |
| 769 | max_seqlen_override: int | None = None, |
| 770 | device: torch.device | None = None, |
| 771 | ) -> dict[str, torch.Tensor | None]: |
| 772 | """Compute encoder metadata from grid_thw_list. |
| 773 | |
| 774 | Shared by the eager forward path, CUDA graph capture, and |
| 775 | CUDA graph replay to avoid duplicated implementation. |
| 776 | |
| 777 | Args: |
| 778 | grid_thw_list: Grid configurations as list of [t, h, w]. |
| 779 | max_batch_size: If set, pad cu_seqlens to this size |
| 780 | (needed for CUDA graph capture/replay). |
| 781 | max_frames_per_batch: If set, overrides max_batch_size for |
| 782 | cu_seqlens padding. For video inputs each item contributes |
| 783 | T attention sequences (frames); this sizes the buffer to |
| 784 | the total frame budget so video replays never overflow. |
| 785 | max_seqlen_override: If set, use this value for max_seqlen |
| 786 | instead of computing from cu_seqlens (needed for CUDA |
| 787 | graph capture to cover worst-case replay scenarios). |
| 788 | device: Device to place tensors on. Defaults to self.device. |
| 789 | |
| 790 | """ |
| 791 | if device is None: |
| 792 | device = self.device |
| 793 | |
| 794 | metadata: dict[str, torch.Tensor | None] = {} |
| 795 | |
| 796 | # Positional embeddings |
| 797 | metadata["pos_embeds"] = self.fast_pos_embed_interpolate(grid_thw_list) |
| 798 | rotary_cos, rotary_sin = self.rot_pos_emb(grid_thw_list) |
| 799 | metadata["rotary_pos_emb_cos"] = rotary_cos |
| 800 | metadata["rotary_pos_emb_sin"] = rotary_sin |
| 801 | |
| 802 | # cu_seqlens from grid_thw |
| 803 | grid_thw_np = np.array(grid_thw_list, dtype=np.int32) |
| 804 | patches_per_frame = grid_thw_np[:, 1] * grid_thw_np[:, 2] |
| 805 | cu_seqlens = np.repeat(patches_per_frame, grid_thw_np[:, 0]).cumsum( |
| 806 | dtype=np.int32 |
| 807 | ) |
| 808 | cu_seqlens = np.concatenate([np.zeros(1, dtype=np.int32), cu_seqlens]) |
| 809 | |
| 810 | # Pad cu_seqlens to the required number of sequences. |
| 811 | # For videos each item contributes T frames = T attention sequences, |
| 812 | # so the total can exceed max_batch_size. max_frames_per_batch |
| 813 | # overrides the pad target when set. |
| 814 | pad_to = ( |
| 815 | max_frames_per_batch if max_frames_per_batch is not None else max_batch_size |
| 816 | ) |
| 817 | if pad_to is not None: |
| 818 | num_seqs = len(cu_seqlens) - 1 |
| 819 | if num_seqs < pad_to: |
| 820 | cu_seqlens = np.concatenate( |
| 821 | [ |
| 822 | cu_seqlens, |
| 823 | np.full( |
| 824 | pad_to - num_seqs, |
| 825 | cu_seqlens[-1], |
| 826 | dtype=np.int32, |
| 827 | ), |
| 828 | ] |
| 829 | ) |
| 830 | |
| 831 | # sequence_lengths (backend-specific) |
| 832 | metadata["sequence_lengths"] = MMEncoderAttention.maybe_compute_seq_lens( |
| 833 | self.attn_backend, cu_seqlens, device |
| 834 | ) |
| 835 | |
| 836 | # max_seqlen |
| 837 | if max_seqlen_override is not None: |
| 838 | max_seqlen_val = max_seqlen_override |
| 839 | else: |
| 840 | max_seqlen_val = MMEncoderAttention.compute_max_seqlen( |
| 841 | self.attn_backend, cu_seqlens |
| 842 | ) |
| 843 | # Keep max_seqlen on CPU: attention wrappers call .item() on it, |
| 844 | # and having it on GPU would capture a wasteful D2H copy in CUDA |
| 845 | # graphs without changing behavior (the scalar is baked at capture). |
| 846 | metadata["max_seqlen"] = torch.tensor(max_seqlen_val, dtype=torch.int32) |
| 847 | |
| 848 | # Recompute cu_seqlens (backend-specific transformation) |
| 849 | metadata["cu_seqlens"] = MMEncoderAttention.maybe_recompute_cu_seqlens( |
| 850 | self.attn_backend, |
| 851 | cu_seqlens, |
| 852 | self.hidden_size, |
| 853 | self.tp_size, |
| 854 | device, |
| 855 | fp8_padded_hidden_size=self.fp8_padded_hidden_size, |
| 856 | ) |
| 857 | |
| 858 | return metadata |
| 859 | |
| 860 | def forward( |
| 861 | self, |
| 862 | x: torch.Tensor, |
| 863 | grid_thw: torch.Tensor | list[list[int]], |
| 864 | *, |
| 865 | encoder_metadata: dict[str, torch.Tensor] | None = None, |
| 866 | ) -> torch.Tensor: |
| 867 | hidden_states = self.input_norm( |
| 868 | x.to(device=self.device, non_blocking=True), self.dtype |
| 869 | ) |
| 870 | hidden_states = self.patch_embed(hidden_states) |
| 871 | |
| 872 | if encoder_metadata is None: |
| 873 | if isinstance(grid_thw, list): |
| 874 | grid_thw_list = grid_thw |
| 875 | else: |
| 876 | grid_thw_list = grid_thw.tolist() |
| 877 | encoder_metadata = self.prepare_encoder_metadata(grid_thw_list) |
| 878 | |
| 879 | pos_embeds = encoder_metadata["pos_embeds"] |
| 880 | hidden_states = hidden_states + pos_embeds |
| 881 | hidden_states = hidden_states.unsqueeze(1) |
| 882 | |
| 883 | deepstack_feature_lists = [] |
| 884 | for layer_num, blk in enumerate(self.blocks): |
| 885 | hidden_states = blk( |
| 886 | hidden_states, |
| 887 | cu_seqlens=encoder_metadata["cu_seqlens"], |
| 888 | rotary_pos_emb_cos=encoder_metadata["rotary_pos_emb_cos"], |
| 889 | rotary_pos_emb_sin=encoder_metadata["rotary_pos_emb_sin"], |
| 890 | max_seqlen=encoder_metadata["max_seqlen"], |
| 891 | sequence_lengths=encoder_metadata.get("sequence_lengths"), |
| 892 | ) |
| 893 | if layer_num in self.deepstack_visual_indexes: |
| 894 | deepstack_merger_idx = self.deepstack_visual_indexes.index(layer_num) |
| 895 | deepstack_feature = self.deepstack_merger_list[deepstack_merger_idx]( |
| 896 | hidden_states |
| 897 | ) |
| 898 | deepstack_feature_lists.append(deepstack_feature) |
| 899 | hidden_states = self.merger(hidden_states) |
| 900 | hidden_states = torch.cat( |
| 901 | [hidden_states] + deepstack_feature_lists, dim=1 |
| 902 | ) # [seq_len, hidden_size * (1 + depth_of_deepstack)] |
| 903 | return hidden_states |
| 904 | |
| 905 | def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: |
| 906 | loader = AutoWeightsLoader(self) |
| 907 | return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper) |
| 908 | |
| 909 | |
| 910 | class Qwen3VLMultiModalDataParser(Qwen2VLMultiModalDataParser): |
| 911 | # Timestamps are part of the prompt replacement (each frame group's |
| 912 | # "<X.X seconds>" text), so they size the placeholder range and must |
| 913 | # travel with the grid when embeddings are delivered out of band. |
| 914 | embedding_fields = { |
| 915 | **Qwen2VLMultiModalDataParser.embedding_fields, |
| 916 | "video": { |
| 917 | **Qwen2VLMultiModalDataParser.embedding_fields["video"], |
| 918 | "timestamps": "metadata", |
| 919 | }, |
| 920 | } |
| 921 | |
| 922 | |
| 923 | class Qwen3VLProcessingInfo(Qwen2VLProcessingInfo): |
| 924 | def get_hf_config(self): |
| 925 | return self.ctx.get_hf_config(Qwen3VLConfig) |
| 926 | |
| 927 | def get_hf_processor(self, **kwargs: object) -> Qwen3VLProcessor: |
| 928 | return self.ctx.get_hf_processor( |
| 929 | Qwen3VLProcessor, |
| 930 | **kwargs, |
| 931 | ) |
| 932 | |
| 933 | def get_image_processor(self, **kwargs: object) -> Qwen2VLImageProcessor: |
| 934 | return self.get_hf_processor(**kwargs).image_processor |
| 935 | |
| 936 | def get_video_processor(self, **kwargs: object) -> Qwen3VLVideoProcessor: |
| 937 | return self.get_hf_processor(**kwargs).video_processor |
| 938 | |
| 939 | def get_data_parser(self): |
| 940 | return Qwen3VLMultiModalDataParser( |
| 941 | self.get_hf_config().vision_config.spatial_merge_size, |
| 942 | video_needs_metadata=True, |
| 943 | expected_hidden_size=self._get_expected_hidden_size(), |
| 944 | allow_missing_mm_embeddings=self.allow_missing_mm_embeddings, |
| 945 | ) |
| 946 | |
| 947 | def get_supported_mm_processor_kwargs(self) -> dict[str, set[str]]: |
| 948 | supported = super().get_supported_mm_processor_kwargs() |
| 949 | |
| 950 | # vLLM has historically accepted `min_pixels` / `max_pixels` for |
| 951 | # Qwen3-VL video. Include them in the routing schema even though |
| 952 | # Qwen3VLVideoProcessor only accepts the equivalent `size`; they are |
| 953 | # folded into `size` before calling HF. |
| 954 | supported["videos_kwargs"].update({"min_pixels", "max_pixels"}) |
| 955 | return supported |
| 956 | |
| 957 | def _get_vision_info( |
| 958 | self, |
| 959 | *, |
| 960 | image_width: int, |
| 961 | image_height: int, |
| 962 | num_frames: int = 2, |
| 963 | do_resize: bool = True, |
| 964 | image_processor: Qwen2VLImageProcessor | Qwen3VLVideoProcessor, |
| 965 | mm_kwargs: Mapping[str, object], |
| 966 | modality: Literal["image", "video"] | None = None, |
| 967 | ) -> tuple[ImageSize, int]: |
| 968 | is_video = isinstance(image_processor, Qwen3VLVideoProcessor) |
| 969 | |
| 970 | hf_config = self.get_hf_config() |
| 971 | vision_config = hf_config.vision_config |
| 972 | patch_size = vision_config.patch_size |
| 973 | merge_size = vision_config.spatial_merge_size |
| 974 | temporal_patch_size = vision_config.temporal_patch_size |
| 975 | |
| 976 | if modality is None: |
| 977 | modality = "video" if is_video else "image" |
| 978 | |
| 979 | merged_mm_kwargs = self._merge_and_resolve_mm_processor_kwargs(mm_kwargs) |
| 980 | scope = "videos_kwargs" if modality == "video" else "images_kwargs" |
| 981 | scoped_mm_kwargs = merged_mm_kwargs.get(scope, {}) |
| 982 | size = self._get_vision_size( |
| 983 | scoped_mm_kwargs, |
| 984 | default_size=image_processor.size, |
| 985 | ) |
| 986 | |
| 987 | if do_resize: |
| 988 | if is_video: |
| 989 | smart_resize = video_smart_resize |
| 990 | extra_kwargs = { |
| 991 | "num_frames": num_frames, |
| 992 | "temporal_factor": temporal_patch_size, |
| 993 | } |
| 994 | else: |
| 995 | smart_resize = image_smart_resize |
| 996 | extra_kwargs = {} |
| 997 | |
| 998 | resized_height, resized_width = smart_resize( |
| 999 | height=image_height, |
| 1000 | width=image_width, |
| 1001 | factor=patch_size * merge_size, |
| 1002 | min_pixels=size["shortest_edge"], |
| 1003 | max_pixels=size["longest_edge"], |
| 1004 | **extra_kwargs, |
| 1005 | ) |
| 1006 | preprocessed_size = ImageSize(width=resized_width, height=resized_height) |
| 1007 | else: |
| 1008 | preprocessed_size = ImageSize(width=image_width, height=image_height) |
| 1009 | |
| 1010 | padded_num_frames = round_up(num_frames, temporal_patch_size) |
| 1011 | |
| 1012 | grid_t = max(padded_num_frames // temporal_patch_size, 1) |
| 1013 | grid_h = preprocessed_size.height // patch_size |
| 1014 | grid_w = preprocessed_size.width // patch_size |
| 1015 | |
| 1016 | num_patches = grid_t * grid_h * grid_w |
| 1017 | num_vision_tokens = num_patches // (merge_size**2) |
| 1018 | |
| 1019 | return preprocessed_size, num_vision_tokens |
| 1020 | |
| 1021 | def _get_max_video_frames(self, max_tokens: int, start_num_frames: int = 2) -> int: |
| 1022 | return super()._get_max_video_frames( |
| 1023 | max_tokens, start_num_frames=start_num_frames |
| 1024 | ) |
| 1025 | |
| 1026 | def get_num_frames_with_most_features( |
| 1027 | self, |
| 1028 | seq_len: int, |
| 1029 | mm_counts: Mapping[str, int], |
| 1030 | max_frames_per_video: int = DUMMY_VIDEO_NUM_FRAMES, |
| 1031 | ) -> int: |
| 1032 | return super().get_num_frames_with_most_features( |
| 1033 | seq_len, mm_counts, max_frames_per_video=max_frames_per_video |
| 1034 | ) |
| 1035 | |
| 1036 | def get_max_video_tokens( |
| 1037 | self, |
| 1038 | seq_len: int, |
| 1039 | mm_counts: Mapping[str, int], |
| 1040 | ) -> int: |
| 1041 | video_processor = self.get_video_processor() |
| 1042 | |
| 1043 | merged_mm_kwargs = self._merge_and_resolve_mm_processor_kwargs({}) |
| 1044 | video_mm_kwargs = merged_mm_kwargs.get("videos_kwargs", {}) |
| 1045 | video_size = self._get_vision_size( |
| 1046 | video_mm_kwargs, |
| 1047 | default_size=video_processor.size, |
| 1048 | ) |
| 1049 | temporal_patch_size = video_mm_kwargs.get( |
| 1050 | "temporal_patch_size", video_processor.temporal_patch_size |
| 1051 | ) |
| 1052 | |
| 1053 | # video_max_pixels contains the temporal compression factor, |
| 1054 | # so we divide by 2 to get the maximum number of image pixels. |
| 1055 | video_max_pixels = video_size["longest_edge"] |
| 1056 | target_width, target_height = self.get_image_size_with_most_features( |
| 1057 | max_pixels=video_max_pixels // temporal_patch_size |
| 1058 | ) |
| 1059 | num_video_soft_tokens = self.get_num_video_tokens( |
| 1060 | image_width=target_width, |
| 1061 | image_height=target_height, |
| 1062 | num_frames=2, |
| 1063 | image_processor=video_processor, |
| 1064 | mm_kwargs={}, |
| 1065 | ) |
| 1066 | return num_video_soft_tokens |
| 1067 | |
| 1068 | def _calculate_timestamps( |
| 1069 | self, indices: list[int] | torch.Tensor, video_fps: float, merge_size: int |
| 1070 | ): |
| 1071 | if not isinstance(indices, list): |
| 1072 | indices = indices.tolist() |
| 1073 | if len(indices) % merge_size != 0: |
| 1074 | # don't update metadata's frames_indices directly |
| 1075 | indices = indices + [indices[-1]] * (merge_size - len(indices) % merge_size) |
| 1076 | timestamps = [idx / video_fps for idx in indices] |
| 1077 | timestamps = [ |
| 1078 | (timestamps[i] + timestamps[i + merge_size - 1]) / 2 |
| 1079 | for i in range(0, len(timestamps), merge_size) |
| 1080 | ] |
| 1081 | return timestamps |
| 1082 | |
| 1083 | def _get_video_second_idx( |
| 1084 | self, |
| 1085 | metadata: dict[str, Any], |
| 1086 | do_sample_frames: bool | None = None, |
| 1087 | sampled_fps: float | None = None, |
| 1088 | sampled_num_frames: int | None = None, |
| 1089 | ) -> list[int]: |
| 1090 | video_processor = self.get_video_processor() |
| 1091 | temporal_patch_size = video_processor.temporal_patch_size |
| 1092 | indices = metadata["frames_indices"] |
| 1093 | |
| 1094 | # metadata["fps"] refers to the true fps of the input video. |
| 1095 | video_fps = metadata["fps"] |
| 1096 | if do_sample_frames is None: |
| 1097 | do_sample_frames = metadata.get("do_sample_frames", False) |
| 1098 | |
| 1099 | # If video frames are sampled in HF processor (instead of vLLM |
| 1100 | # video loader), we need to re-calculate the indices from original |
| 1101 | # metadata. |
| 1102 | if do_sample_frames: |
| 1103 | total_num_frames = metadata["total_num_frames"] |
| 1104 | |
| 1105 | # When num_frames is explicitly provided, use it directly |
| 1106 | # instead of computing from fps. This mirrors the behavior of |
| 1107 | # HF's Qwen3VLVideoProcessor.sample_frames where num_frames |
| 1108 | # and fps are mutually exclusive. |
| 1109 | if sampled_num_frames is not None: |
| 1110 | num_frames = sampled_num_frames |
| 1111 | else: |
| 1112 | # here video_fps is the fps of the sampled video, and |
| 1113 | # metadata["fps"] refers to the fps of the original video. |
| 1114 | sampled_fps = sampled_fps if sampled_fps else video_processor.fps |
| 1115 | num_frames = int(total_num_frames / metadata["fps"] * sampled_fps) |
| 1116 | |
| 1117 | num_frames = min( |
| 1118 | min( |
| 1119 | max(num_frames, video_processor.min_frames), |
| 1120 | video_processor.max_frames, |
| 1121 | ), |
| 1122 | total_num_frames, |
| 1123 | ) |
| 1124 | indices = ( |
| 1125 | np.linspace(0, total_num_frames - 1, num_frames) |
| 1126 | .round() |
| 1127 | .astype(int) |
| 1128 | .tolist() |
| 1129 | ) |
| 1130 | timestamps = self._calculate_timestamps(indices, video_fps, temporal_patch_size) |
| 1131 | return timestamps |
| 1132 | |
| 1133 | |
| 1134 | class Qwen3VLDummyInputsBuilder(BaseDummyInputsBuilder[Qwen3VLProcessingInfo]): |
| 1135 | def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str: |
| 1136 | num_images = mm_counts.get("image", 0) |
| 1137 | num_videos = mm_counts.get("video", 0) |
| 1138 | |
| 1139 | image_token = "<|vision_start|><|image_pad|><|vision_end|>" |
| 1140 | video_token = "<|vision_start|><|video_pad|><|vision_end|>" |
| 1141 | |
| 1142 | return image_token * num_images + video_token * num_videos |
| 1143 | |
| 1144 | def get_dummy_mm_data( |
| 1145 | self, |
| 1146 | seq_len: int, |
| 1147 | mm_counts: Mapping[str, int], |
| 1148 | mm_options: MultiModalDummyOptions, |
| 1149 | ) -> MultiModalDataDict: |
| 1150 | video_overrides = mm_options.get("video") |
| 1151 | |
| 1152 | target_image_width, target_image_height = ( |
| 1153 | self.info.get_image_size_with_most_features() |
| 1154 | ) |
| 1155 | |
| 1156 | # treat videos as special images |
| 1157 | target_num_frames = 2 |
| 1158 | if video_overrides: |
| 1159 | num_frames_override = video_overrides.num_frames |
| 1160 | if num_frames_override: |
| 1161 | if num_frames_override > target_num_frames: |
| 1162 | logger.warning( |
| 1163 | "video.num_frames override (%d) exceeds model's " |
| 1164 | "maximum number of frames (%d), will be ignored", |
| 1165 | num_frames_override, |
| 1166 | target_num_frames, |
| 1167 | ) |
| 1168 | if num_frames_override < 2: |
| 1169 | logger.warning( |
| 1170 | "video.num_frames override (%d) cannot be less " |
| 1171 | "than 2, will be ignored", |
| 1172 | num_frames_override, |
| 1173 | ) |
| 1174 | target_num_frames = min(target_num_frames, num_frames_override) |
| 1175 | target_num_frames = max(target_num_frames, 2) |
| 1176 | |
| 1177 | video_processor = self.info.get_video_processor() |
| 1178 | |
| 1179 | merged_mm_kwargs = self.info._merge_and_resolve_mm_processor_kwargs({}) |
| 1180 | video_mm_kwargs = merged_mm_kwargs.get("videos_kwargs", {}) |
| 1181 | video_size = self.info._get_vision_size( |
| 1182 | video_mm_kwargs, |
| 1183 | default_size=video_processor.size, |
| 1184 | ) |
| 1185 | temporal_patch_size = video_mm_kwargs.get( |
| 1186 | "temporal_patch_size", video_processor.temporal_patch_size |
| 1187 | ) |
| 1188 | |
| 1189 | # video_max_pixels contains the temporal compression factor, |
| 1190 | # so we divide by 2 to get the maximum number of image pixels. |
| 1191 | video_max_pixels = video_size["longest_edge"] |
| 1192 | |
| 1193 | # With the HF processor's per-frame pixel cap enabled |
| 1194 | # (cap_pixels_per_frame, huggingface/transformers#48071), a 2-frame |
| 1195 | # dummy is processed at only 2 * cap pixels, so memory profiling |
| 1196 | # underestimates the largest possible item: a fully sampled video |
| 1197 | # still reaches the whole longest_edge budget. Spread the budget |
| 1198 | # over enough frames that the cap is not binding for the dummy. |
| 1199 | # This takes precedence over a shrinking num_frames override, which |
| 1200 | # would otherwise reintroduce the under-profiling. |
| 1201 | if video_mm_kwargs.get("cap_pixels_per_frame"): |
| 1202 | max_video_tokens = video_mm_kwargs.get( |
| 1203 | "max_video_tokens", |
| 1204 | getattr(video_processor, "max_video_tokens", 768), |
| 1205 | ) |
| 1206 | patch_size = video_mm_kwargs.get("patch_size", video_processor.patch_size) |
| 1207 | merge_size = video_mm_kwargs.get("merge_size", video_processor.merge_size) |
| 1208 | per_frame_cap = max_video_tokens * (patch_size * merge_size) ** 2 |
| 1209 | target_num_frames = max( |
| 1210 | target_num_frames, |
| 1211 | min(video_processor.max_frames, cdiv(video_max_pixels, per_frame_cap)), |
| 1212 | ) |
| 1213 | |
| 1214 | target_video_width, target_video_height = ( |
| 1215 | self.info.get_image_size_with_most_features( |
| 1216 | max_pixels=video_max_pixels // temporal_patch_size |
| 1217 | ) |
| 1218 | ) |
| 1219 | target_video_size, _ = self.info._get_vision_info( |
| 1220 | image_width=target_video_width, |
| 1221 | image_height=target_video_height, |
| 1222 | num_frames=target_num_frames, |
| 1223 | image_processor=video_processor, |
| 1224 | mm_kwargs={}, |
| 1225 | ) |
| 1226 | # NOTE: we need to do this check here since Qwen3-VL resizes video |
| 1227 | # frames depending on how many frames there are. |
| 1228 | target_video_width, target_video_height = ( |
| 1229 | target_video_size.width, |
| 1230 | target_video_size.height, |
| 1231 | ) |
| 1232 | if video_overrides: |
| 1233 | width_override = video_overrides.width |
| 1234 | if width_override: |
| 1235 | if width_override > target_video_width: |
| 1236 | logger.warning( |
| 1237 | "video.width override (%d) exceeds model's " |
| 1238 | "maximum width (%d), will be ignored", |
| 1239 | width_override, |
| 1240 | target_video_width, |
| 1241 | ) |
| 1242 | target_video_width = min(target_video_width, width_override) |
| 1243 | height_override = video_overrides.height |
| 1244 | if height_override: |
| 1245 | if height_override > target_video_height: |
| 1246 | logger.warning( |
| 1247 | "video.height override (%d) exceeds model's " |
| 1248 | "maximum height (%d), will be ignored", |
| 1249 | height_override, |
| 1250 | target_video_height, |
| 1251 | ) |
| 1252 | target_video_height = min(target_video_height, height_override) |
| 1253 | |
| 1254 | return { |
| 1255 | "image": self._get_dummy_images( |
| 1256 | width=target_image_width, |
| 1257 | height=target_image_height, |
| 1258 | num_images=mm_counts.get("image", 0), |
| 1259 | overrides=mm_options.get("image"), |
| 1260 | ), |
| 1261 | "video": self._get_dummy_videos( |
| 1262 | width=target_video_width, |
| 1263 | height=target_video_height, |
| 1264 | num_frames=target_num_frames, |
| 1265 | num_videos=mm_counts.get("video", 0), |
| 1266 | ), |
| 1267 | } |
| 1268 | |
| 1269 | def _get_dummy_videos( |
| 1270 | self, |
| 1271 | *, |
| 1272 | width: int, |
| 1273 | height: int, |
| 1274 | num_frames: int, |
| 1275 | num_videos: int, |
| 1276 | overrides: VideoDummyOptions | None = None, |
| 1277 | ) -> list[VideoItem]: |
| 1278 | videos = super()._get_dummy_videos( |
| 1279 | width=width, |
| 1280 | height=height, |
| 1281 | num_frames=num_frames, |
| 1282 | num_videos=num_videos, |
| 1283 | overrides=overrides, |
| 1284 | ) |
| 1285 | videos = [v.copy() for v in videos] |
| 1286 | |
| 1287 | video_items: list[VideoItem] = [] |
| 1288 | for video in videos: |
| 1289 | video_num_frames = video.shape[0] |
| 1290 | video_metadata = { |
| 1291 | "fps": 2.0, |
| 1292 | "duration": video_num_frames / 2.0, |
| 1293 | "total_num_frames": video_num_frames, |
| 1294 | "frames_indices": list(range(video_num_frames)), |
| 1295 | "video_backend": "opencv", |
| 1296 | "do_sample_frames": False, |
| 1297 | } |
| 1298 | video_items.append((video, video_metadata)) |
| 1299 | |
| 1300 | return video_items |
| 1301 | |
| 1302 | |
| 1303 | def _replace_video_token_placeholders( |
| 1304 | prompt_ids: list[int], |
| 1305 | target: list[int], |
| 1306 | replacements: list[list[int]], |
| 1307 | ) -> list[int]: |
| 1308 | """Replace each video placeholder with its expanded sequence. |
| 1309 | |
| 1310 | Args: |
| 1311 | prompt_ids: Token IDs of the original (unexpanded) prompt. |
| 1312 | target: Token-ID sequence to search for. New-style |
| 1313 | (transformers>=5.10) processors expand only the bare |
| 1314 | `[video_pad_id]`; older ones expand the full |
| 1315 | `[vision_start_id, video_pad_id, vision_end_id]` triplet. |
| 1316 | replacements: Per-video expanded token sequences, in prompt order. |
| 1317 | |
| 1318 | Returns: |
| 1319 | Token IDs with every placeholder replaced. |
| 1320 | |
| 1321 | """ |
| 1322 | result: list[int] = [] |
| 1323 | repl_idx = 0 |
| 1324 | i = 0 |
| 1325 | n = len(prompt_ids) |
| 1326 | k = len(target) |
| 1327 | num_repl = len(replacements) |
| 1328 | |
| 1329 | while i < n: |
| 1330 | if prompt_ids[i : i + k] == target: |
| 1331 | result.extend(replacements[repl_idx]) |
| 1332 | repl_idx += 1 |
| 1333 | i += k |
| 1334 | else: |
| 1335 | result.append(prompt_ids[i]) |
| 1336 | i += 1 |
| 1337 | |
| 1338 | assert repl_idx == num_repl, ( |
| 1339 | f"Found {repl_idx} video placeholders but expected {num_repl}" |
| 1340 | ) |
| 1341 | return result |
| 1342 | |
| 1343 | |
| 1344 | class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo]): |
| 1345 | @staticmethod |
| 1346 | def _expands_only_video_token(hf_processor: ProcessorMixin) -> bool: |
| 1347 | """Transformers>=5.10 processors override `replace_video_token` |
| 1348 | to expand only the bare video token, keeping the prompt's outer |
| 1349 | `<|vision_start|>`/`<|vision_end|>` markers.""" |
| 1350 | mixin_impl = getattr(ProcessorMixin, "replace_video_token", None) |
| 1351 | proc_impl = getattr(type(hf_processor), "replace_video_token", None) |
| 1352 | return proc_impl is not None and proc_impl is not mixin_impl |
| 1353 | |
| 1354 | def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str: |
| 1355 | return self.dummy_inputs.get_dummy_text(mm_counts) |
| 1356 | |
| 1357 | def _apply_hf_processor_main( |
| 1358 | self, |
| 1359 | mm_items: MultiModalDataItems, |
| 1360 | hf_kwargs: Mapping[str, object], |
| 1361 | ) -> BatchFeature: |
| 1362 | hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs( |
| 1363 | mm_items, hf_kwargs |
| 1364 | ) |
| 1365 | |
| 1366 | if not hf_data: |
| 1367 | return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data) |
| 1368 | |
| 1369 | # Merge and resolve the multimodal processor kwargs once so every HF |
| 1370 | # call below starts from the same engine/request state and scoped form. |
| 1371 | merged_mm_kwargs = self.info._merge_and_resolve_mm_processor_kwargs(hf_kwargs) |
| 1372 | |
| 1373 | if "images_kwargs" in merged_mm_kwargs: |
| 1374 | image_scoped_kwargs = dict(merged_mm_kwargs["images_kwargs"]) |
| 1375 | |
| 1376 | # Qwen2VLImageProcessor requires both `size` edges. If a partial size |
| 1377 | # override is passed, fill missing edges from the processor defaults |
| 1378 | # before any HF call. |
| 1379 | if image_scoped_kwargs.keys() & {"size", "min_pixels", "max_pixels"}: |
| 1380 | default_image_size = self.info.get_image_processor().size |
| 1381 | image_scoped_kwargs["size"] = self.info._get_vision_size( |
| 1382 | image_scoped_kwargs, |
| 1383 | default_size=default_image_size, |
| 1384 | ) |
| 1385 | |
| 1386 | merged_mm_kwargs["images_kwargs"] = image_scoped_kwargs |
| 1387 | |
| 1388 | if "videos_kwargs" in merged_mm_kwargs: |
| 1389 | video_scoped_kwargs = dict(merged_mm_kwargs["videos_kwargs"]) |
| 1390 | |
| 1391 | # Qwen3VLVideoProcessor requires both `size` edges. If a partial size |
| 1392 | # override is passed, fill missing edges from the processor defaults |
| 1393 | # before any HF call. |
| 1394 | if video_scoped_kwargs.keys() & {"size", "min_pixels", "max_pixels"}: |
| 1395 | default_video_size = self.info.get_video_processor().size |
| 1396 | video_scoped_kwargs["size"] = self.info._get_vision_size( |
| 1397 | video_scoped_kwargs, |
| 1398 | default_size=default_video_size, |
| 1399 | ) |
| 1400 | |
| 1401 | # Qwen3VLVideoProcessor does not accept vLLM's min/max pixel |
| 1402 | # compatibility kwargs. They have already been folded into `size`, |
| 1403 | # so remove them before any HF call, including calls that contain |
| 1404 | # only text/images. |
| 1405 | video_scoped_kwargs.pop("min_pixels", None) |
| 1406 | video_scoped_kwargs.pop("max_pixels", None) |
| 1407 | |
| 1408 | merged_mm_kwargs["videos_kwargs"] = video_scoped_kwargs |
| 1409 | |
| 1410 | # Separate video processing from image processing. Because the videos |
| 1411 | # are processed into several image patches |
| 1412 | video_input_ids_lst: list[list[int]] = [] |
| 1413 | if videos := hf_data.pop("videos", []): |
| 1414 | assert isinstance(videos, list) |
| 1415 | video_grid_thw_lst = [] |
| 1416 | pixel_values_videos_lst = [] |
| 1417 | timestamps_per_video = [] |
| 1418 | |
| 1419 | hf_config = self.info.get_hf_config() |
| 1420 | tokenizer = self.info.get_tokenizer() |
| 1421 | merge_size = hf_config.vision_config.spatial_merge_size |
| 1422 | pruning_spec = self.info.ctx.get_mm_config().get_video_pruning_spec() |
| 1423 | vision_start_token_id = hf_config.vision_start_token_id |
| 1424 | vision_end_token_id = hf_config.vision_end_token_id |
| 1425 | video_token_id = hf_config.video_token_id |
| 1426 | |
| 1427 | for item_idx, item in enumerate(videos): |
| 1428 | video_array, metadata = item |
| 1429 | |
| 1430 | # NOTE: @JJJYmmm new attr metadata.frames_indices indicates |
| 1431 | # the sampled frames indices of pre-sampled videos, which is |
| 1432 | # used to calculate the timestamps. Make sure that |
| 1433 | # do_sample_frames in mm_kwargs is false for presampled videos. |
| 1434 | |
| 1435 | # The root copy is shallow, so give each item its own video |
| 1436 | # scope before modifying per-video values below. |
| 1437 | video_mm_kwargs = dict(merged_mm_kwargs) |
| 1438 | video_scoped_kwargs = dict(video_mm_kwargs.get("videos_kwargs", {})) |
| 1439 | video_mm_kwargs["videos_kwargs"] = video_scoped_kwargs |
| 1440 | |
| 1441 | sampled_fps = video_scoped_kwargs.get("fps") |
| 1442 | if is_list_of(sampled_fps, float): |
| 1443 | video_scoped_kwargs["fps"] = sampled_fps[item_idx] |
| 1444 | sampled_num_frames = video_scoped_kwargs.get("num_frames") |
| 1445 | if is_list_of(sampled_num_frames, int): |
| 1446 | video_scoped_kwargs["num_frames"] = sampled_num_frames[item_idx] |
| 1447 | if "do_sample_frames" not in video_scoped_kwargs: |
| 1448 | # qwen_vl_utils already has "do_sample_frames" in |
| 1449 | # mm_kwargs, don't overwrite it. |
| 1450 | video_scoped_kwargs["do_sample_frames"] = metadata.get( |
| 1451 | "do_sample_frames", False |
| 1452 | ) |
| 1453 | |
| 1454 | metadata = VideoMetadata( |
| 1455 | **{k: metadata[k] for k in metadata if k != "do_sample_frames"} |
| 1456 | ) |
| 1457 | |
| 1458 | # Compute timestamps here where we have access to metadata |
| 1459 | timestamps = self.info._get_video_second_idx( |
| 1460 | metadata=metadata, |
| 1461 | do_sample_frames=video_scoped_kwargs["do_sample_frames"], |
| 1462 | sampled_fps=video_scoped_kwargs.get("fps"), |
| 1463 | sampled_num_frames=video_scoped_kwargs.get("num_frames"), |
| 1464 | ) |
| 1465 | timestamps_per_video.append(timestamps) |
| 1466 | |
| 1467 | video_mm_data = dict() |
| 1468 | video_mm_data["videos"] = [[video_array]] |
| 1469 | video_mm_data["video_metadata"] = [[metadata]] |
| 1470 | |
| 1471 | # When num_frames is specified, explicitly set fps=None |
| 1472 | # to prevent HF's BaseVideoProcessor.preprocess() from |
| 1473 | # filling in the class default (fps=2) via setdefault(), |
| 1474 | # which would conflict with num_frames (mutually exclusive). |
| 1475 | if ( |
| 1476 | "num_frames" in video_scoped_kwargs |
| 1477 | and "fps" not in video_scoped_kwargs |
| 1478 | ): |
| 1479 | video_scoped_kwargs["fps"] = None |
| 1480 | |
| 1481 | video_outputs = self.info.ctx.call_hf_processor( |
| 1482 | self.info.get_hf_processor(**video_mm_kwargs), |
| 1483 | dict( |
| 1484 | text="<|vision_start|><|video_pad|><|vision_end|>", |
| 1485 | **video_mm_data, |
| 1486 | ), |
| 1487 | video_mm_kwargs, |
| 1488 | ) |
| 1489 | |
| 1490 | # Discard HF output input_ids — we use get_video_repl below |
| 1491 | # to generate the correct (EVS-adjusted) token sequence. |
| 1492 | video_outputs.pop("input_ids", None) |
| 1493 | |
| 1494 | video_grid_thw = video_outputs["video_grid_thw"] |
| 1495 | num_frames = int(video_grid_thw[0, 0]) |
| 1496 | tokens_per_frame_base = int(video_grid_thw[0, 1:].prod()) // ( |
| 1497 | merge_size**2 |
| 1498 | ) |
| 1499 | |
| 1500 | # Apply video pruning (EVS or VidCom2) if enabled. |
| 1501 | if pruning_spec is not None: |
| 1502 | method, prune_q = pruning_spec |
| 1503 | count_fn = ( |
| 1504 | vidcom2_compute_retained_tokens_count |
| 1505 | if method == "vidcom2" |
| 1506 | else compute_retained_tokens_count |
| 1507 | ) |
| 1508 | num_tokens = count_fn( |
| 1509 | tokens_per_frame=tokens_per_frame_base, |
| 1510 | num_frames=num_frames, |
| 1511 | q=prune_q, |
| 1512 | ) |
| 1513 | tokens_per_frame = [num_tokens] + [0] * (num_frames - 1) |
| 1514 | select_token_id = False |
| 1515 | else: |
| 1516 | tokens_per_frame = [tokens_per_frame_base] * num_frames |
| 1517 | select_token_id = True |
| 1518 | |
| 1519 | video_repl = Qwen3VLMultiModalProcessor.get_video_repl( |
| 1520 | tokens_per_frame=tokens_per_frame, |
| 1521 | timestamps=timestamps, |
| 1522 | tokenizer=tokenizer, |
| 1523 | vision_start_token_id=vision_start_token_id, |
| 1524 | vision_end_token_id=vision_end_token_id, |
| 1525 | video_token_id=video_token_id, |
| 1526 | select_token_id=select_token_id, |
| 1527 | ) |
| 1528 | video_input_ids_lst.append(list(video_repl.full)) |
| 1529 | |
| 1530 | video_grid_thw_lst.append(video_outputs["video_grid_thw"]) |
| 1531 | pixel_values_videos_lst.append(video_outputs["pixel_values_videos"]) |
| 1532 | video_outputs = dict( |
| 1533 | pixel_values_videos=torch.cat(pixel_values_videos_lst), |
| 1534 | video_grid_thw=torch.cat(video_grid_thw_lst), |
| 1535 | timestamps=timestamps_per_video, |
| 1536 | ) |
| 1537 | else: |
| 1538 | video_outputs = dict() |
| 1539 | |
| 1540 | # The root copy is shallow, so copy the video scope before removing |
| 1541 | # per-video kwargs to avoid mutating `merged_mm_kwargs`. |
| 1542 | non_video_mm_kwargs = dict(merged_mm_kwargs) |
| 1543 | if "videos_kwargs" in non_video_mm_kwargs: |
| 1544 | non_video_scoped_kwargs = dict(non_video_mm_kwargs["videos_kwargs"]) |
| 1545 | non_video_scoped_kwargs.pop("fps", None) |
| 1546 | non_video_scoped_kwargs.pop("num_frames", None) |
| 1547 | non_video_mm_kwargs["videos_kwargs"] = non_video_scoped_kwargs |
| 1548 | processed_data = self.info.ctx.call_hf_processor( |
| 1549 | self.info.get_hf_processor(**non_video_mm_kwargs), |
| 1550 | hf_data, |
| 1551 | non_video_mm_kwargs, |
| 1552 | ) |
| 1553 | |
| 1554 | # Replace each placeholder with pre-computed video tokens. |
| 1555 | if video_input_ids_lst: |
| 1556 | hf_config = self.info.get_hf_config() |
| 1557 | if self._expands_only_video_token(self.info.get_hf_processor()): |
| 1558 | video_target = [hf_config.video_token_id] |
| 1559 | else: |
| 1560 | video_target = [ |
| 1561 | hf_config.vision_start_token_id, |
| 1562 | hf_config.video_token_id, |
| 1563 | hf_config.vision_end_token_id, |
| 1564 | ] |
| 1565 | input_ids = processed_data.pop("input_ids") |
| 1566 | if not isinstance(input_ids, list): |
| 1567 | input_ids = input_ids.tolist() |
| 1568 | (prompt_ids,) = input_ids |
| 1569 | expanded_ids = _replace_video_token_placeholders( |
| 1570 | prompt_ids, video_target, video_input_ids_lst |
| 1571 | ) |
| 1572 | processed_data["input_ids"] = [expanded_ids] |
| 1573 | |
| 1574 | processed_data.update(video_outputs) |
| 1575 | return self._finalize_hf_mm_data( |
| 1576 | hf_data, hf_kwargs, passthrough_data, processed_data |
| 1577 | ) |
| 1578 | |
| 1579 | def _get_mm_fields_config( |
| 1580 | self, |
| 1581 | hf_inputs: BatchFeature, |
| 1582 | hf_processor_mm_kwargs: Mapping[str, object], |
| 1583 | ) -> Mapping[str, MultiModalFieldConfig]: |
| 1584 | return _create_qwen2vl_field_factory( |
| 1585 | self.info.get_hf_config().vision_config.spatial_merge_size |
| 1586 | )(hf_inputs) |
| 1587 | |
| 1588 | def _get_prompt_updates( |
| 1589 | self, |
| 1590 | mm_items: MultiModalDataItems, |
| 1591 | hf_processor_mm_kwargs: Mapping[str, Any], |
| 1592 | out_mm_kwargs: MultiModalKwargsItems, |
| 1593 | ) -> Sequence[PromptUpdate]: |
| 1594 | hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs) |
| 1595 | image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs) |
| 1596 | tokenizer = self.info.get_tokenizer() |
| 1597 | hf_config = self.info.get_hf_config() |
| 1598 | |
| 1599 | video_token_id = hf_config.video_token_id |
| 1600 | vision_start_token_id = hf_config.vision_start_token_id |
| 1601 | vision_end_token_id = hf_config.vision_end_token_id |
| 1602 | |
| 1603 | merge_length = image_processor.merge_size**2 |
| 1604 | |
| 1605 | def get_image_replacement_qwen3vl(item_idx: int): |
| 1606 | out_item = out_mm_kwargs["image"][item_idx] |
| 1607 | grid_thw = out_item["image_grid_thw"].data |
| 1608 | assert isinstance(grid_thw, torch.Tensor) |
| 1609 | |
| 1610 | num_tokens = int(grid_thw.prod()) // merge_length |
| 1611 | return [hf_processor.image_token_id] * num_tokens |
| 1612 | |
| 1613 | def get_video_replacement_qwen3vl(item_idx: int): |
| 1614 | out_item = out_mm_kwargs["video"][item_idx] |
| 1615 | grid_thw = out_item["video_grid_thw"].data |
| 1616 | assert isinstance(grid_thw, torch.Tensor) |
| 1617 | |
| 1618 | sampled_fps = hf_processor_mm_kwargs.get("fps") |
| 1619 | if is_list_of(sampled_fps, float): |
| 1620 | sampled_fps = sampled_fps[item_idx] |
| 1621 | |
| 1622 | timestamps = out_item["timestamps"].data |
| 1623 | assert isinstance(timestamps, (list, torch.Tensor)) |
| 1624 | assert len(timestamps) == grid_thw[0], ( |
| 1625 | f"The timestamps length({len(timestamps)}) should be equal " |
| 1626 | f"video length ({grid_thw[0]})." |
| 1627 | ) |
| 1628 | |
| 1629 | # Compute tokens per frame, with EVS / VidCom2 support |
| 1630 | num_frames = int(grid_thw[0]) |
| 1631 | tokens_per_frame_base = int(grid_thw[1:].prod()) // merge_length |
| 1632 | |
| 1633 | pruning_spec = self.info.ctx.get_mm_config().get_video_pruning_spec() |
| 1634 | if pruning_spec is not None: |
| 1635 | method, prune_q = pruning_spec |
| 1636 | count_fn = ( |
| 1637 | vidcom2_compute_retained_tokens_count |
| 1638 | if method == "vidcom2" |
| 1639 | else compute_retained_tokens_count |
| 1640 | ) |
| 1641 | num_tokens = count_fn( |
| 1642 | tokens_per_frame=tokens_per_frame_base, |
| 1643 | num_frames=num_frames, |
| 1644 | q=prune_q, |
| 1645 | ) |
| 1646 | tokens_per_frame = [num_tokens] + [0] * (num_frames - 1) |
| 1647 | select_token_id = False |
| 1648 | else: |
| 1649 | tokens_per_frame = [tokens_per_frame_base] * num_frames |
| 1650 | select_token_id = True |
| 1651 | |
| 1652 | return Qwen3VLMultiModalProcessor.get_video_repl( |
| 1653 | tokens_per_frame=tokens_per_frame, |
| 1654 | timestamps=timestamps, |
| 1655 | tokenizer=tokenizer, |
| 1656 | vision_start_token_id=vision_start_token_id, |
| 1657 | vision_end_token_id=vision_end_token_id, |
| 1658 | video_token_id=video_token_id, |
| 1659 | select_token_id=select_token_id, |
| 1660 | ) |
| 1661 | |
| 1662 | if self._expands_only_video_token(hf_processor): |
| 1663 | # transformers>=5.10 expands only the bare video_token |
| 1664 | video_target = [video_token_id] |
| 1665 | else: |
| 1666 | # Old-style processors expand the full placeholder |
| 1667 | video_target = [ |
| 1668 | vision_start_token_id, |
| 1669 | video_token_id, |
| 1670 | vision_end_token_id, |
| 1671 | ] |
| 1672 | |
| 1673 | return [ |
| 1674 | PromptReplacement( |
| 1675 | modality="image", |
| 1676 | target=[hf_processor.image_token_id], |
| 1677 | replacement=get_image_replacement_qwen3vl, |
| 1678 | ), |
| 1679 | PromptReplacement( |
| 1680 | modality="video", |
| 1681 | target=video_target, |
| 1682 | replacement=get_video_replacement_qwen3vl, |
| 1683 | ), |
| 1684 | ] |
| 1685 | |
| 1686 | @staticmethod |
| 1687 | def get_video_repl( |
| 1688 | *, |
| 1689 | tokens_per_frame: list[int], |
| 1690 | timestamps: Sequence[float] | torch.Tensor, |
| 1691 | tokenizer: TokenizerLike, |
| 1692 | vision_start_token_id: int, |
| 1693 | vision_end_token_id: int, |
| 1694 | video_token_id: int, |
| 1695 | select_token_id: bool = False, |
| 1696 | ) -> PromptUpdateDetails: |
| 1697 | """Build prompt replacement for a video in Qwen3VL format. |
| 1698 | |
| 1699 | The replacement structure for each frame is: |
| 1700 | timestamp_tokens + vision_start_token + video_tokens + vision_end_token |
| 1701 | |
| 1702 | Args: |
| 1703 | tokens_per_frame: Number of video tokens per frame (can vary per frame for |
| 1704 | EVS). |
| 1705 | timestamps: List of timestamps in seconds for each frame |
| 1706 | tokenizer: Tokenizer to encode timestamp strings |
| 1707 | vision_start_token_id: Token ID for vision start marker |
| 1708 | vision_end_token_id: Token ID for vision end marker |
| 1709 | video_token_id: Token ID for video content |
| 1710 | select_token_id: Whether to return token ids instead of text |
| 1711 | |
| 1712 | Returns: |
| 1713 | PromptUpdateDetails with full token sequence |
| 1714 | |
| 1715 | """ |
| 1716 | assert len(timestamps) == len(tokens_per_frame), ( |
| 1717 | "timestamps and tokens_per_frame must have the same length" |
| 1718 | ) |
| 1719 | |
| 1720 | # Tokenize timestamp strings independently to avoid tokenizer merging |
| 1721 | # tokens across boundaries. |
| 1722 | timestamp_token_ids = [ |
| 1723 | tokenizer.encode(f"<{timestamp:.1f} seconds>", add_special_tokens=False) |
| 1724 | for timestamp in timestamps |
| 1725 | ] |
| 1726 | |
| 1727 | # Build the full token sequence |
| 1728 | all_token_ids = [] |
| 1729 | for frame_timestamp_ids, num_tokens in zip( |
| 1730 | timestamp_token_ids, tokens_per_frame |
| 1731 | ): |
| 1732 | # Add timestamp tokens |
| 1733 | all_token_ids.extend(frame_timestamp_ids) |
| 1734 | |
| 1735 | # Add vision tokens: vision_start + video_tokens + vision_end |
| 1736 | all_token_ids.append(vision_start_token_id) |
| 1737 | all_token_ids.extend([video_token_id] * num_tokens) |
| 1738 | all_token_ids.append(vision_end_token_id) |
| 1739 | |
| 1740 | if select_token_id: |
| 1741 | return PromptUpdateDetails.select_token_id(all_token_ids, video_token_id) |
| 1742 | |
| 1743 | # NOTE: we use `from_seq` instead of `select_token_id` because we want all |
| 1744 | # tokens in the placeholder to be initially marked as candidates. Then |
| 1745 | # in `get_input_embeddings``, we refine the mask to only replace |
| 1746 | # `video_token_id` / `image_token_id`` positions with video/image embeddings, |
| 1747 | # keeping text embeddings for timestamps and structural tokens. |
| 1748 | return PromptUpdateDetails.from_seq(all_token_ids) |
| 1749 | |
| 1750 | |
| 1751 | @support_torch_compile( |
| 1752 | dynamic_arg_dims={ |
| 1753 | "input_ids": 0, |
| 1754 | # positions is of shape (3, seq_len) if mrope is enabled for qwen2-vl, |
| 1755 | # otherwise (seq_len, ). |
| 1756 | "positions": -1, |
| 1757 | "intermediate_tensors": 0, |
| 1758 | "inputs_embeds": 0, |
| 1759 | # the same shape as input_embeds |
| 1760 | "deepstack_input_embeds": 0, |
| 1761 | } |
| 1762 | ) |
| 1763 | class Qwen3LLMModel(Qwen3Model): |
| 1764 | def forward( |
| 1765 | self, |
| 1766 | input_ids: torch.Tensor | None, |
| 1767 | positions: torch.Tensor, |
| 1768 | intermediate_tensors: IntermediateTensors | None = None, |
| 1769 | inputs_embeds: torch.Tensor | None = None, |
| 1770 | # args for deepstack |
| 1771 | deepstack_input_embeds: IntermediateTensors | None = None, |
| 1772 | ) -> torch.Tensor | IntermediateTensors: |
| 1773 | if get_pp_group().is_first_rank: |
| 1774 | if inputs_embeds is not None: |
| 1775 | hidden_states = inputs_embeds |
| 1776 | else: |
| 1777 | hidden_states = self.embed_input_ids(input_ids) |
| 1778 | residual = None |
| 1779 | else: |
| 1780 | assert intermediate_tensors is not None |
| 1781 | hidden_states = intermediate_tensors["hidden_states"] |
| 1782 | residual = intermediate_tensors["residual"] |
| 1783 | |
| 1784 | aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual) |
| 1785 | for layer_idx, layer in islice( |
| 1786 | enumerate(self.layers), self.start_layer, self.end_layer |
| 1787 | ): |
| 1788 | hidden_states, residual = layer( |
| 1789 | positions, |
| 1790 | hidden_states, |
| 1791 | residual, |
| 1792 | ) |
| 1793 | |
| 1794 | if deepstack_input_embeds is not None and layer_idx in range( |
| 1795 | 0, len(deepstack_input_embeds) |
| 1796 | ): |
| 1797 | hidden_states = ( |
| 1798 | hidden_states |
| 1799 | + deepstack_input_embeds[f"deepstack_input_embeds_{layer_idx}"] |
| 1800 | ) |
| 1801 | self._maybe_add_hidden_state( |
| 1802 | aux_hidden_states, layer_idx + 1, hidden_states, residual |
| 1803 | ) |
| 1804 | |
| 1805 | if not get_pp_group().is_last_rank: |
| 1806 | return IntermediateTensors( |
| 1807 | {"hidden_states": hidden_states, "residual": residual} |
| 1808 | ) |
| 1809 | hidden_states, _ = self.norm(hidden_states, residual) |
| 1810 | |
| 1811 | if len(aux_hidden_states) > 0: |
| 1812 | return hidden_states, aux_hidden_states |
| 1813 | return hidden_states |
| 1814 | |
| 1815 | |
| 1816 | class Qwen3LLMForCausalLM(Qwen3ForCausalLM): |
| 1817 | def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""): |
| 1818 | super(Qwen3ForCausalLM, self).__init__() |
| 1819 | config = vllm_config.model_config.hf_config |
| 1820 | quant_config = vllm_config.quant_config |
| 1821 | |
| 1822 | self.config = config |
| 1823 | |
| 1824 | self.quant_config = quant_config |
| 1825 | self.model = Qwen3LLMModel( |
| 1826 | vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") |
| 1827 | ) |
| 1828 | |
| 1829 | if get_pp_group().is_last_rank: |
| 1830 | self.lm_head = ParallelLMHead( |
| 1831 | config.vocab_size, |
| 1832 | config.hidden_size, |
| 1833 | quant_config=quant_config, |
| 1834 | prefix="lm_head", |
| 1835 | ) |
| 1836 | if config.tie_word_embeddings: |
| 1837 | self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens) |
| 1838 | else: |
| 1839 | self.lm_head = PPMissingLayer() |
| 1840 | |
| 1841 | self.logits_processor = LogitsProcessor(config.vocab_size) |
| 1842 | |
| 1843 | self.make_empty_intermediate_tensors = ( |
| 1844 | self.model.make_empty_intermediate_tensors |
| 1845 | ) |
| 1846 | |
| 1847 | |
| 1848 | @MULTIMODAL_REGISTRY.register_processor( |
| 1849 | Qwen3VLMultiModalProcessor, |
| 1850 | info=Qwen3VLProcessingInfo, |
| 1851 | dummy_inputs=Qwen3VLDummyInputsBuilder, |
| 1852 | ) |
| 1853 | class Qwen3VLForConditionalGeneration( |
| 1854 | nn.Module, |
| 1855 | SupportsMultiModal, |
| 1856 | SupportsEncoderCudaGraph, |
| 1857 | SupportsLoRA, |
| 1858 | SupportsPP, |
| 1859 | SupportsMRoPE, |
| 1860 | SupportsEagle, |
| 1861 | SupportsEagle3, |
| 1862 | SupportsMultiModalPruning, |
| 1863 | ): |
| 1864 | packed_modules_mapping: dict[str, list[str]] = { |
| 1865 | "qkv_proj": [ |
| 1866 | "q_proj", |
| 1867 | "k_proj", |
| 1868 | "v_proj", |
| 1869 | ], |
| 1870 | "gate_up_proj": [ |
| 1871 | "gate_proj", |
| 1872 | "up_proj", |
| 1873 | ], |
| 1874 | "qkv": ["qkv"], # For vision tower's already-packed QKV |
| 1875 | } |
| 1876 | |
| 1877 | supports_encoder_tp_data = True |
| 1878 | supports_tower_connector_lora = True |
| 1879 | supports_mm_device_do_normalize = True |
| 1880 | |
| 1881 | supported_video_pruning_methods = ("evs", "vidcom2") |
| 1882 | |
| 1883 | # To ensure correct weight loading and mapping. |
| 1884 | hf_to_vllm_mapper = WeightsMapper( |
| 1885 | orig_to_new_prefix={ |
| 1886 | "model.visual.": "visual.", |
| 1887 | "lm_head.": "language_model.lm_head.", |
| 1888 | "model.language_model.": "language_model.model.", |
| 1889 | } |
| 1890 | ) |
| 1891 | |
| 1892 | @classmethod |
| 1893 | def get_placeholder_str(cls, modality: str, i: int) -> str | None: |
| 1894 | if modality.startswith("image"): |
| 1895 | return "<|vision_start|><|image_pad|><|vision_end|>" |
| 1896 | if modality.startswith("video"): |
| 1897 | return "<|vision_start|><|video_pad|><|vision_end|>" |
| 1898 | |
| 1899 | raise ValueError("Only image or video modality is supported") |
| 1900 | |
| 1901 | def _init_video_pruning(self, multimodal_config: MultiModalConfig) -> None: |
| 1902 | pruning_spec = multimodal_config.get_video_pruning_spec() |
| 1903 | if pruning_spec is None: |
| 1904 | self.video_pruning_method: VideoPruningMethod | None = None |
| 1905 | self.video_pruning_rate = multimodal_config.video_pruning_rate |
| 1906 | else: |
| 1907 | self.video_pruning_method, self.video_pruning_rate = pruning_spec |
| 1908 | self.is_multimodal_pruning_enabled = ( |
| 1909 | multimodal_config.is_multimodal_pruning_enabled() |
| 1910 | ) |
| 1911 | |
| 1912 | def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"): |
| 1913 | super().__init__() |
| 1914 | config: Qwen3VLConfig = vllm_config.model_config.hf_config |
| 1915 | quant_config = vllm_config.quant_config |
| 1916 | multimodal_config = vllm_config.model_config.get_multimodal_config() |
| 1917 | |
| 1918 | self.config = config |
| 1919 | self.model_config = vllm_config.model_config |
| 1920 | self._tokenizer = cached_tokenizer_from_config(vllm_config.model_config) |
| 1921 | self.multimodal_config = multimodal_config |
| 1922 | self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data" |
| 1923 | self._init_video_pruning(multimodal_config) |
| 1924 | |
| 1925 | with self._mark_tower_model(vllm_config, {"image", "video"}): |
| 1926 | self.visual = Qwen3_VisionTransformer( |
| 1927 | config.vision_config, |
| 1928 | norm_eps=getattr(config, "rms_norm_eps", 1e-6), |
| 1929 | quant_config=quant_config, |
| 1930 | input_norm=build_mm_input_norm(self.model_config), |
| 1931 | prefix=maybe_prefix(prefix, "visual"), |
| 1932 | ) |
| 1933 | |
| 1934 | self.use_deepstack = hasattr( |
| 1935 | config.vision_config, "deepstack_visual_indexes" |
| 1936 | ) and not isinstance(self.visual, StageMissingLayer) |
| 1937 | self.deepstack_num_level = ( |
| 1938 | len(config.vision_config.deepstack_visual_indexes) |
| 1939 | if self.use_deepstack |
| 1940 | else 0 |
| 1941 | ) |
| 1942 | self.visual_dim = config.vision_config.out_hidden_size |
| 1943 | self.multiscale_dim = self.visual_dim * self.deepstack_num_level |
| 1944 | |
| 1945 | if self.use_deepstack: |
| 1946 | self.deepstack_input_embeds = [ |
| 1947 | torch.zeros( |
| 1948 | vllm_config.scheduler_config.max_num_batched_tokens, |
| 1949 | config.text_config.hidden_size, |
| 1950 | ) |
| 1951 | for _ in range(self.deepstack_num_level) |
| 1952 | ] |
| 1953 | # Tracks the valid token span currently stored in the buffer. |
| 1954 | # Zero means there is no active deepstack payload to consume. |
| 1955 | self.deepstack_input_embeds_num_tokens = 0 |
| 1956 | |
| 1957 | with self._mark_language_model(vllm_config): |
| 1958 | self.language_model = Qwen3LLMForCausalLM( |
| 1959 | vllm_config=vllm_config.with_hf_config( |
| 1960 | config.text_config, |
| 1961 | architectures=["Qwen3ForCausalLM"], |
| 1962 | ), |
| 1963 | prefix=maybe_prefix(prefix, "language_model"), |
| 1964 | ) |
| 1965 | |
| 1966 | if not get_pp_group().is_first_rank and hasattr( |
| 1967 | config.vision_config, "deepstack_visual_indexes" |
| 1968 | ): |
| 1969 | assert self.language_model.model.start_layer >= len( |
| 1970 | config.vision_config.deepstack_visual_indexes |
| 1971 | ), ( |
| 1972 | "start_layer should be greater than or equal to " |
| 1973 | "len(deepstack_visual_indexes)" |
| 1974 | ) |
| 1975 | |
| 1976 | self.make_empty_intermediate_tensors = ( |
| 1977 | self.language_model.make_empty_intermediate_tensors |
| 1978 | ) |
| 1979 | |
| 1980 | def _get_deepstack_input_embeds( |
| 1981 | self, |
| 1982 | num_tokens: int, |
| 1983 | ) -> IntermediateTensors | None: |
| 1984 | if not getattr(self, "deepstack_input_embeds", None): |
| 1985 | return None # If vision tower is skipped |
| 1986 | if num_tokens > self.deepstack_input_embeds[0].size(0): |
| 1987 | self._resize_deepstack_input_embeds(num_tokens) |
| 1988 | |
| 1989 | # get deepstack_input_embeds from buffer, and clear the buffer |
| 1990 | return IntermediateTensors( |
| 1991 | { |
| 1992 | f"deepstack_input_embeds_{idx}": self.deepstack_input_embeds[idx][ |
| 1993 | :num_tokens |
| 1994 | ] |
| 1995 | for idx in range(self.deepstack_num_level) |
| 1996 | } |
| 1997 | ) |
| 1998 | |
| 1999 | def _resize_deepstack_input_embeds(self, num_tokens: int) -> None: |
| 2000 | self.deepstack_input_embeds = [ |
| 2001 | torch.zeros( |
| 2002 | num_tokens, |
| 2003 | self.config.text_config.hidden_size, |
| 2004 | device=self.deepstack_input_embeds[0].device, |
| 2005 | dtype=self.deepstack_input_embeds[0].dtype, |
| 2006 | ) |
| 2007 | for _ in range(self.deepstack_num_level) |
| 2008 | ] |
| 2009 | |
| 2010 | def _set_deepstack_input_embeds(self, deepstack_input_embeds: torch.Tensor) -> None: |
| 2011 | if not getattr(self, "deepstack_input_embeds", None): |
| 2012 | return |
| 2013 | |
| 2014 | # set deepstack_input_embeds to buffer |
| 2015 | num_tokens = deepstack_input_embeds.size(1) |
| 2016 | if num_tokens > self.deepstack_input_embeds[0].size(0): |
| 2017 | self._resize_deepstack_input_embeds(num_tokens) |
| 2018 | for idx in range(self.deepstack_num_level): |
| 2019 | self.deepstack_input_embeds[idx][:num_tokens].copy_( |
| 2020 | deepstack_input_embeds[idx] |
| 2021 | ) |
| 2022 | self.deepstack_input_embeds_num_tokens = num_tokens |
| 2023 | |
| 2024 | def _clear_deepstack_input_embeds(self, num_tokens: int) -> None: |
| 2025 | if not getattr(self, "deepstack_input_embeds", None): |
| 2026 | return |
| 2027 | if getattr(self, "deepstack_input_embeds_num_tokens", 0) == 0: |
| 2028 | return |
| 2029 | |
| 2030 | # clear deepstack_input_embeds in buffer |
| 2031 | if num_tokens > 0: |
| 2032 | for idx in range(self.deepstack_num_level): |
| 2033 | self.deepstack_input_embeds[idx][:num_tokens].zero_() |
| 2034 | self.deepstack_input_embeds_num_tokens = 0 |
| 2035 | |
| 2036 | # -- SupportsEncoderCudaGraph protocol methods -- |
| 2037 | |
| 2038 | def get_encoder_cudagraph_config(self): |
| 2039 | from vllm.v1.worker.encoder_cudagraph_defs import ( |
| 2040 | EncoderCudaGraphConfig, |
| 2041 | ) |
| 2042 | |
| 2043 | # When video pruning is enabled, embed_multimodal post-processes both |
| 2044 | # image and video embeddings (mrope positions are appended for image, |
| 2045 | # prune+append for video). The encoder CUDA graph path bypasses that |
| 2046 | # post-process, producing inconsistent embedding formats vs eager. So |
| 2047 | # disable CUDA graph for all modalities when pruning is on. |
| 2048 | modalities = [] if self.is_multimodal_pruning_enabled else ["image", "video"] |
| 2049 | |
| 2050 | # Compute max_frames_per_video for budget sizing. |
| 2051 | max_frames = self.get_max_frames_per_video() if "video" in modalities else 1 |
| 2052 | |
| 2053 | return EncoderCudaGraphConfig( |
| 2054 | modalities=modalities, |
| 2055 | buffer_keys=[ |
| 2056 | "pixel_values", |
| 2057 | "pos_embeds", |
| 2058 | "rotary_pos_emb_cos", |
| 2059 | "rotary_pos_emb_sin", |
| 2060 | "cu_seqlens", |
| 2061 | "max_seqlen", |
| 2062 | "sequence_lengths", |
| 2063 | ], |
| 2064 | out_hidden_size=self.visual.out_hidden_size, |
| 2065 | max_frames_per_video=max_frames, |
| 2066 | ) |
| 2067 | |
| 2068 | def get_input_modality( |
| 2069 | self, |
| 2070 | mm_kwargs: dict[str, Any], |
| 2071 | ) -> str: |
| 2072 | if "image_grid_thw" in mm_kwargs: |
| 2073 | return "image" |
| 2074 | elif "video_grid_thw" in mm_kwargs: |
| 2075 | return "video" |
| 2076 | raise AssertionError("This line should be unreachable.") |
| 2077 | |
| 2078 | def get_max_frames_per_video(self) -> int: |
| 2079 | mm_registry = MULTIMODAL_REGISTRY |
| 2080 | info = mm_registry.get_processing_info(self.model_config) |
| 2081 | assert isinstance(info, Qwen3VLProcessingInfo) |
| 2082 | max_frames_per_video = info.get_num_frames_with_most_features( |
| 2083 | seq_len=self.model_config.max_model_len, |
| 2084 | mm_counts={"video": self.multimodal_config.get_limit_per_prompt("video")}, |
| 2085 | ) |
| 2086 | return max_frames_per_video |
| 2087 | |
| 2088 | def get_encoder_cudagraph_budget_range( |
| 2089 | self, |
| 2090 | vllm_config, |
| 2091 | ) -> tuple[int, int]: |
| 2092 | # Min: estimated smallest possible encoder input. |
| 2093 | # 224x224 image → 16x16 patches (patch_size=14) |
| 2094 | # spatial_merge_size=2 → 8x8 = 64 tokens |
| 2095 | min_budget = 64 |
| 2096 | # Max: capped by max_num_batched_tokens |
| 2097 | # TODO(shen-shanshan): the max_budget auto-infer needs to be optimized later. |
| 2098 | max_budget = min( |
| 2099 | vllm_config.scheduler_config.max_num_batched_tokens, |
| 2100 | self.model_config.max_model_len, |
| 2101 | ) |
| 2102 | return (min_budget, max_budget) |
| 2103 | |
| 2104 | def _get_pixel_values_by_modality( |
| 2105 | self, |
| 2106 | mm_kwargs: dict[str, Any], |
| 2107 | ) -> torch.Tensor: |
| 2108 | modality = self.get_input_modality(mm_kwargs) |
| 2109 | if modality == "image": |
| 2110 | return mm_kwargs["pixel_values"] |
| 2111 | elif modality == "video": |
| 2112 | return mm_kwargs["pixel_values_videos"] |
| 2113 | raise AssertionError("This line should be unreachable.") |
| 2114 | |
| 2115 | def _get_grid_thw_by_modality( |
| 2116 | self, |
| 2117 | mm_kwargs: dict[str, Any], |
| 2118 | ) -> list[tuple[int, int, int]]: |
| 2119 | grid_thw_key = f"{self.get_input_modality(mm_kwargs)}_grid_thw" |
| 2120 | grid_thw = mm_kwargs[grid_thw_key] |
| 2121 | if not isinstance(grid_thw, list): |
| 2122 | grid_thw = grid_thw.tolist() |
| 2123 | return grid_thw |
| 2124 | |
| 2125 | def get_encoder_cudagraph_item_specs( |
| 2126 | self, |
| 2127 | mm_kwargs: dict[str, Any], |
| 2128 | ): |
| 2129 | from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec |
| 2130 | |
| 2131 | m = self.visual.spatial_merge_size |
| 2132 | grid_thw = self._get_grid_thw_by_modality(mm_kwargs) |
| 2133 | return [ |
| 2134 | EncoderItemSpec( |
| 2135 | input_size=t * h * w, |
| 2136 | output_tokens=t * (h // m) * (w // m), |
| 2137 | ) |
| 2138 | for t, h, w in grid_thw |
| 2139 | ] |
| 2140 | |
| 2141 | def select_encoder_cudagraph_items( |
| 2142 | self, |
| 2143 | mm_kwargs: dict[str, Any], |
| 2144 | indices: list[int], |
| 2145 | ) -> dict[str, Any]: |
| 2146 | grid_thw = self._get_grid_thw_by_modality(mm_kwargs) |
| 2147 | pixel_values = self._get_pixel_values_by_modality(mm_kwargs) |
| 2148 | |
| 2149 | if len(indices) == 0: |
| 2150 | if self.get_input_modality(mm_kwargs) == "image": |
| 2151 | return { |
| 2152 | "pixel_values": pixel_values[:0], |
| 2153 | "image_grid_thw": [], |
| 2154 | } |
| 2155 | else: |
| 2156 | return { |
| 2157 | "pixel_values_videos": pixel_values[:0], |
| 2158 | "video_grid_thw": [], |
| 2159 | } |
| 2160 | |
| 2161 | # Compute cumulative patch offsets for slicing pixel_values |
| 2162 | patches_per_item = [t * h * w for t, h, w in grid_thw] |
| 2163 | cum_patches = [0] |
| 2164 | for p in patches_per_item: |
| 2165 | cum_patches.append(cum_patches[-1] + p) |
| 2166 | |
| 2167 | selected_pv = torch.cat( |
| 2168 | [pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices] |
| 2169 | ) |
| 2170 | selected_grid = [grid_thw[i] for i in indices] |
| 2171 | |
| 2172 | if self.get_input_modality(mm_kwargs) == "image": |
| 2173 | return { |
| 2174 | "pixel_values": selected_pv, |
| 2175 | "image_grid_thw": selected_grid, |
| 2176 | } |
| 2177 | else: |
| 2178 | return { |
| 2179 | "pixel_values_videos": selected_pv, |
| 2180 | "video_grid_thw": selected_grid, |
| 2181 | } |
| 2182 | |
| 2183 | def prepare_encoder_cudagraph_capture_inputs( |
| 2184 | self, |
| 2185 | token_budget: int, |
| 2186 | max_batch_size: int, |
| 2187 | max_frames_per_batch: int, |
| 2188 | device: torch.device, |
| 2189 | dtype: torch.dtype, |
| 2190 | path: str = "default", |
| 2191 | axis_keys: tuple[Hashable, ...] | None = None, |
| 2192 | ): |
| 2193 | from vllm.v1.worker.encoder_cudagraph_defs import ( |
| 2194 | EncoderCudaGraphCaptureInputs, |
| 2195 | ) |
| 2196 | |
| 2197 | spatial_merge_size = self.visual.spatial_merge_size |
| 2198 | # Ceil so the buffer fits the worst case of one item using the full |
| 2199 | # budget. Floor under-allocates when budget is not a multiple of |
| 2200 | # max_batch_size. |
| 2201 | per_mm_item_output = (token_budget + max_batch_size - 1) // max_batch_size |
| 2202 | |
| 2203 | frames_per_item = max_frames_per_batch // max_batch_size |
| 2204 | if frames_per_item > 1: |
| 2205 | # Build the capture grid using a video-format layout so that |
| 2206 | # cu_seqlens is sized for video replays from the start. |
| 2207 | # cu_seqlens has one entry per attention sequence (one per frame), |
| 2208 | # so using T > 1 per item makes the buffer large enough without |
| 2209 | # relying solely on padding. |
| 2210 | # Ceiling ensures frames_per_item * tokens_per_frame >= per_mm_item_output |
| 2211 | # so the pixel_values buffer covers any valid single-item replay. |
| 2212 | tokens_per_frame = ( |
| 2213 | per_mm_item_output + frames_per_item - 1 |
| 2214 | ) // frames_per_item |
| 2215 | # Video-format grid_config (T=frames_per_item). |
| 2216 | grid_config = [ |
| 2217 | [ |
| 2218 | frames_per_item, |
| 2219 | spatial_merge_size, |
| 2220 | tokens_per_frame * spatial_merge_size, |
| 2221 | ] |
| 2222 | for _ in range(max_batch_size) |
| 2223 | ] |
| 2224 | else: |
| 2225 | # Image-format grid_config (T=1). |
| 2226 | grid_config = [ |
| 2227 | [1, spatial_merge_size, per_mm_item_output * spatial_merge_size] |
| 2228 | for _ in range(max_batch_size) |
| 2229 | ] |
| 2230 | |
| 2231 | # Create dummy pixel_values |
| 2232 | patch_embed = self.visual.patch_embed |
| 2233 | in_channels = patch_embed.proj.in_channels |
| 2234 | patch_size = patch_embed.patch_size |
| 2235 | temporal_patch_size = patch_embed.temporal_patch_size |
| 2236 | total_patches = sum(t * h * w for t, h, w in grid_config) |
| 2237 | flattened_patch_size = ( |
| 2238 | in_channels * temporal_patch_size * patch_size * patch_size |
| 2239 | ) |
| 2240 | dummy_pixel_values = torch.zeros( |
| 2241 | total_patches, |
| 2242 | flattened_patch_size, |
| 2243 | device=device, |
| 2244 | dtype=self.visual.input_norm.input_dtype or dtype, |
| 2245 | ) |
| 2246 | |
| 2247 | # Override max_seqlen with a safe upper bound for capture. |
| 2248 | # max_seqlen.item() gets baked into the CUDA graph (not replayed), |
| 2249 | # so the capture value must cover any replay scenario. |
| 2250 | # Worst case: 1 item consuming the full budget -> |
| 2251 | # seq_len = token_budget * spatial_merge_size^2. |
| 2252 | metadata = self.visual.prepare_encoder_metadata( |
| 2253 | grid_config, |
| 2254 | max_batch_size=max_batch_size, |
| 2255 | max_frames_per_batch=max_frames_per_batch, |
| 2256 | max_seqlen_override=token_budget * (spatial_merge_size**2), |
| 2257 | device=device, |
| 2258 | ) |
| 2259 | |
| 2260 | # Just use image-modality dummy input_buffer for capturing, since it's also |
| 2261 | # compatible for video inputs (has the same shape: [num_patches, C*T*P*P]). |
| 2262 | values = metadata | { |
| 2263 | "pixel_values": dummy_pixel_values, |
| 2264 | } |
| 2265 | |
| 2266 | return EncoderCudaGraphCaptureInputs( |
| 2267 | values=values, |
| 2268 | ) |
| 2269 | |
| 2270 | def prepare_encoder_cudagraph_replay_buffers( |
| 2271 | self, |
| 2272 | mm_kwargs: dict[str, Any], |
| 2273 | max_batch_size: int, |
| 2274 | max_frames_per_batch: int, |
| 2275 | path: str = "default", |
| 2276 | ): |
| 2277 | modality = self.get_input_modality(mm_kwargs) |
| 2278 | grid_thw_list = self._get_grid_thw_by_modality(mm_kwargs) |
| 2279 | |
| 2280 | if modality == "image": |
| 2281 | metadata = self.visual.prepare_encoder_metadata( |
| 2282 | grid_thw_list, |
| 2283 | max_batch_size=max_batch_size, |
| 2284 | ) |
| 2285 | elif modality == "video": |
| 2286 | metadata = self.visual.prepare_encoder_metadata( |
| 2287 | grid_thw_list, |
| 2288 | max_frames_per_batch=max_frames_per_batch, |
| 2289 | ) |
| 2290 | else: |
| 2291 | raise AssertionError("This line should be unreachable.") |
| 2292 | |
| 2293 | values = metadata | { |
| 2294 | "pixel_values": self._get_pixel_values_by_modality(mm_kwargs), |
| 2295 | } |
| 2296 | return EncoderCudaGraphReplayBuffers(values=values) |
| 2297 | |
| 2298 | def encoder_cudagraph_forward( |
| 2299 | self, |
| 2300 | values: dict[str, torch.Tensor], |
| 2301 | path: str = "default", |
| 2302 | ) -> torch.Tensor: |
| 2303 | pixel_values = values.pop("pixel_values") |
| 2304 | metadata = values |
| 2305 | return self.visual(pixel_values, None, encoder_metadata=metadata) |
| 2306 | |
| 2307 | def encoder_eager_forward( |
| 2308 | self, |
| 2309 | mm_kwargs: dict[str, Any], |
| 2310 | path: str = "default", |
| 2311 | ) -> torch.Tensor: |
| 2312 | pixel_values = self._get_pixel_values_by_modality(mm_kwargs) |
| 2313 | grid_thw = self._get_grid_thw_by_modality(mm_kwargs) |
| 2314 | return self.visual(pixel_values, grid_thw) |
| 2315 | |
| 2316 | def _parse_and_validate_image_input( |
| 2317 | self, **kwargs: object |
| 2318 | ) -> Qwen2_5_VLImageInputs | None: |
| 2319 | pixel_values = kwargs.pop("pixel_values", None) |
| 2320 | image_embeds = kwargs.pop("image_embeds", None) |
| 2321 | image_grid_thw = kwargs.pop("image_grid_thw", None) |
| 2322 | |
| 2323 | if pixel_values is None and image_embeds is None: |
| 2324 | return None |
| 2325 | |
| 2326 | if pixel_values is not None: |
| 2327 | return Qwen2_5_VLImagePixelInputs( |
| 2328 | type="pixel_values", |
| 2329 | pixel_values=pixel_values, |
| 2330 | image_grid_thw=image_grid_thw, |
| 2331 | ) |
| 2332 | |
| 2333 | else: |
| 2334 | return Qwen2_5_VLImageEmbeddingInputs( |
| 2335 | type="image_embeds", |
| 2336 | image_embeds=image_embeds, |
| 2337 | image_grid_thw=image_grid_thw, |
| 2338 | ) |
| 2339 | |
| 2340 | def _parse_and_validate_video_input( |
| 2341 | self, **kwargs: object |
| 2342 | ) -> Qwen2_5_VLVideoInputs | None: |
| 2343 | pixel_values_videos = kwargs.pop("pixel_values_videos", None) |
| 2344 | video_embeds = kwargs.pop("video_embeds", None) |
| 2345 | video_grid_thw = kwargs.pop("video_grid_thw", None) |
| 2346 | second_per_grid_ts = kwargs.pop("second_per_grid_ts", None) |
| 2347 | timestamps = kwargs.pop("timestamps", None) |
| 2348 | |
| 2349 | if pixel_values_videos is None and video_embeds is None: |
| 2350 | return None |
| 2351 | |
| 2352 | if pixel_values_videos is not None: |
| 2353 | return Qwen2_5_VLVideoPixelInputs( |
| 2354 | type="pixel_values_videos", |
| 2355 | pixel_values_videos=pixel_values_videos, |
| 2356 | video_grid_thw=video_grid_thw, |
| 2357 | second_per_grid_ts=second_per_grid_ts, |
| 2358 | timestamps=timestamps, |
| 2359 | ) |
| 2360 | |
| 2361 | else: |
| 2362 | return Qwen2_5_VLVideoEmbeddingInputs( |
| 2363 | type="video_embeds", |
| 2364 | video_embeds=video_embeds, |
| 2365 | video_grid_thw=video_grid_thw, |
| 2366 | timestamps=timestamps, |
| 2367 | ) |
| 2368 | |
| 2369 | def _process_image_input( |
| 2370 | self, image_input: Qwen2_5_VLImageInputs |
| 2371 | ) -> tuple[torch.Tensor, ...]: |
| 2372 | grid_thw = image_input["image_grid_thw"] |
| 2373 | assert grid_thw.ndim == 2 |
| 2374 | |
| 2375 | if image_input["type"] == "image_embeds": |
| 2376 | image_embeds = image_input["image_embeds"].type(self.visual.dtype) |
| 2377 | else: |
| 2378 | pixel_values = image_input["pixel_values"] |
| 2379 | if self.use_data_parallel: |
| 2380 | return run_dp_sharded_mrope_vision_model( |
| 2381 | self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d" |
| 2382 | ) |
| 2383 | else: |
| 2384 | image_embeds = self.visual(pixel_values, grid_thw=grid_thw) |
| 2385 | |
| 2386 | # Split concatenated embeddings for each image item. |
| 2387 | merge_size = self.visual.spatial_merge_size |
| 2388 | sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist() |
| 2389 | return image_embeds.split(sizes) |
| 2390 | |
| 2391 | def _process_video_input( |
| 2392 | self, video_input: Qwen2_5_VLVideoInputs |
| 2393 | ) -> tuple[torch.Tensor, ...]: |
| 2394 | grid_thw = video_input["video_grid_thw"] |
| 2395 | assert grid_thw.ndim == 2 |
| 2396 | |
| 2397 | if video_input["type"] == "video_embeds": |
| 2398 | video_embeds = video_input["video_embeds"].type(self.visual.dtype) |
| 2399 | else: |
| 2400 | pixel_values_videos = video_input["pixel_values_videos"] |
| 2401 | if self.use_data_parallel: |
| 2402 | grid_thw_list = grid_thw.tolist() |
| 2403 | return run_dp_sharded_mrope_vision_model( |
| 2404 | self.visual, pixel_values_videos, grid_thw_list, rope_type="rope_3d" |
| 2405 | ) |
| 2406 | else: |
| 2407 | video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw) |
| 2408 | |
| 2409 | # Split concatenated embeddings for each video item. |
| 2410 | merge_size = self.visual.spatial_merge_size |
| 2411 | sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist() |
| 2412 | return video_embeds.split(sizes) |
| 2413 | |
| 2414 | def _postprocess_image_embeds_evs( |
| 2415 | self, |
| 2416 | image_embeds_split: tuple[torch.Tensor, ...], |
| 2417 | image_input: Qwen2_5_VLImageInputs, |
| 2418 | ) -> tuple[torch.Tensor, ...]: |
| 2419 | """Append mrope positions for each for images. |
| 2420 | This is necessary to recover correct mrope |
| 2421 | positions after video pruning |
| 2422 | |
| 2423 | Args: |
| 2424 | image_embeds_split: Tuple of image embeddings for |
| 2425 | each image item. |
| 2426 | image_input: Image input data. |
| 2427 | |
| 2428 | Returns: |
| 2429 | Tuple of image embeddings for each image item. |
| 2430 | Resulting embeddings will have extra 5 channels for |
| 2431 | computed mrope positions, consistent with video embeddings. |
| 2432 | |
| 2433 | """ |
| 2434 | if self.is_multimodal_pruning_enabled: |
| 2435 | merge_size = self.visual.spatial_merge_size |
| 2436 | grid_thw = image_input["image_grid_thw"] |
| 2437 | grid_thw_list = grid_thw.tolist() |
| 2438 | image_embeds_out = [] |
| 2439 | for emb, size in zip(image_embeds_split, grid_thw_list): |
| 2440 | positions = async_tensor_h2d( |
| 2441 | compute_mrope_for_media(size, merge_size), emb.device |
| 2442 | ) |
| 2443 | positions = torch.cat( |
| 2444 | [ |
| 2445 | positions, |
| 2446 | torch.zeros_like( |
| 2447 | positions[:, 0:1] |
| 2448 | ), # Dummy extra fifth channel |
| 2449 | ], |
| 2450 | dim=1, |
| 2451 | ) |
| 2452 | emb = torch.cat([emb, positions], dim=1) |
| 2453 | image_embeds_out.append(emb) |
| 2454 | image_embeds_split = tuple(image_embeds_out) |
| 2455 | return image_embeds_split |
| 2456 | |
| 2457 | def _postprocess_video_embeds_evs( |
| 2458 | self, |
| 2459 | video_embeds_split: tuple[torch.Tensor, ...], |
| 2460 | video_input: Qwen2_5_VLVideoInputs, |
| 2461 | ) -> tuple[torch.Tensor, ...]: |
| 2462 | """Prunes video embeddings via Efficient Video Sampling (EVS) |
| 2463 | and then appends mrope positions for each retained embeddings |
| 2464 | |
| 2465 | Args: |
| 2466 | video_embeds_split: Tuple of video embeddings for each video item. |
| 2467 | video_input: Video input data. |
| 2468 | |
| 2469 | Returns: |
| 2470 | Tuple of video embeddings for each video item. |
| 2471 | Resulting embeddings will have extra 5 channels for computed mrope |
| 2472 | positions, and whether the index corresponds to a video embedding. |
| 2473 | |
| 2474 | """ |
| 2475 | grid_thw = video_input["video_grid_thw"] |
| 2476 | assert grid_thw.ndim == 2 |
| 2477 | grid_thw_list = grid_thw.tolist() |
| 2478 | merge_size = self.visual.spatial_merge_size |
| 2479 | |
| 2480 | # Apply EVS to each video. |
| 2481 | video_embeds_out = [] |
| 2482 | assert video_input.timestamps is not None |
| 2483 | for video_idx, (emb, size) in enumerate(zip(video_embeds_split, grid_thw_list)): |
| 2484 | # Compute positions. |
| 2485 | timestamps = video_input.timestamps[video_idx] |
| 2486 | num_frames = len(timestamps) |
| 2487 | |
| 2488 | t, h, w = size |
| 2489 | if self.is_multimodal_pruning_enabled: |
| 2490 | assert self.video_pruning_rate is not None |
| 2491 | # Compute the retention mask for each video (EVS or VidCom2). |
| 2492 | if self.video_pruning_method == "vidcom2": |
| 2493 | mask_fn = vidcom2_compute_retention_mask |
| 2494 | else: |
| 2495 | mask_fn = compute_retention_mask |
| 2496 | retention_mask = mask_fn( |
| 2497 | emb, |
| 2498 | size, |
| 2499 | spatial_merge_size=self.visual.spatial_merge_size, |
| 2500 | q=self.video_pruning_rate, |
| 2501 | ) |
| 2502 | |
| 2503 | with gpu_sync_allowed(): |
| 2504 | # Apply retention mask. |
| 2505 | emb = emb[retention_mask] |
| 2506 | |
| 2507 | # Calculate the actual number of retained tokens per frame. |
| 2508 | num_frames, rows, cols = t, h // merge_size, w // merge_size |
| 2509 | retention_mask_thw = retention_mask.reshape(num_frames, rows, cols) |
| 2510 | num_tokens_per_frame = ( |
| 2511 | retention_mask_thw.sum(dim=(1, 2)).long().tolist() |
| 2512 | ) |
| 2513 | else: |
| 2514 | feature_size = emb.shape[0] // num_frames |
| 2515 | num_tokens_per_frame = [feature_size] * num_frames |
| 2516 | retention_mask = None |
| 2517 | |
| 2518 | emb = self._create_final_video_embeddings( |
| 2519 | video_embeddings=emb, |
| 2520 | num_tokens_per_frame=num_tokens_per_frame, |
| 2521 | timestamps=timestamps, |
| 2522 | video_grid_thw=size, |
| 2523 | retention_mask=retention_mask, |
| 2524 | ) |
| 2525 | |
| 2526 | video_embeds_out.append(emb) |
| 2527 | |
| 2528 | return tuple(video_embeds_out) |
| 2529 | |
| 2530 | def _create_final_video_embeddings( |
| 2531 | self, |
| 2532 | video_embeddings: torch.Tensor, |
| 2533 | num_tokens_per_frame: list[int], |
| 2534 | timestamps: list[float], |
| 2535 | video_grid_thw: list[int], |
| 2536 | retention_mask: torch.Tensor, |
| 2537 | ) -> torch.Tensor: |
| 2538 | """Create final embeddings that combine video embeddings with |
| 2539 | text embeddings of indicator tokens. |
| 2540 | |
| 2541 | These final embeddings contain: |
| 2542 | - Actual video embeddings in positions corresponding to video content |
| 2543 | - Text embeddings for indicator tokens (<img>, </img>, and |
| 2544 | frame separation text) in their respective positions |
| 2545 | |
| 2546 | These embeddings will replace the placeholder embeddings to create |
| 2547 | input_embeds for the LLM. |
| 2548 | """ |
| 2549 | device = video_embeddings.device |
| 2550 | |
| 2551 | # Generate video replacement token IDs using get_video_repl |
| 2552 | # This tokenizes each frame separator independently, then uses pre-tokenized |
| 2553 | # special tokens to ensure consistent tokenization regardless of |
| 2554 | # num_tokens_per_frame values. |
| 2555 | video_repl = Qwen3VLMultiModalProcessor.get_video_repl( |
| 2556 | tokens_per_frame=num_tokens_per_frame, |
| 2557 | tokenizer=self._tokenizer, |
| 2558 | timestamps=timestamps, |
| 2559 | vision_start_token_id=self.config.vision_start_token_id, |
| 2560 | vision_end_token_id=self.config.vision_end_token_id, |
| 2561 | video_token_id=self.config.video_token_id, |
| 2562 | select_token_id=self.is_multimodal_pruning_enabled, |
| 2563 | ) |
| 2564 | |
| 2565 | repl_token_ids = torch.tensor(video_repl.full, device=device) |
| 2566 | embed_token_id = _cached_tensor(self.config.video_token_id, device=device) |
| 2567 | is_video_embed = torch.isin(repl_token_ids, embed_token_id) |
| 2568 | |
| 2569 | # Get text embeddings for indicator tokens (has only `visual_dim``). |
| 2570 | text_embeddings = self.get_language_model().embed_input_ids(repl_token_ids) |
| 2571 | |
| 2572 | if self.use_deepstack: |
| 2573 | ( |
| 2574 | deepstack_input_embeds, |
| 2575 | multimodal_embeddings, |
| 2576 | ) = self._compute_deepstack_embeds( |
| 2577 | inputs_embeds=text_embeddings, |
| 2578 | multimodal_embeddings=[video_embeddings], |
| 2579 | is_multimodal=is_video_embed, |
| 2580 | ) |
| 2581 | else: |
| 2582 | deepstack_input_embeds = None |
| 2583 | multimodal_embeddings = [video_embeddings] |
| 2584 | |
| 2585 | merged_embeddings = _merge_multimodal_embeddings( |
| 2586 | inputs_embeds=text_embeddings, |
| 2587 | multimodal_embeddings=multimodal_embeddings, |
| 2588 | is_multimodal=is_video_embed, |
| 2589 | ) |
| 2590 | |
| 2591 | to_concat = [merged_embeddings] |
| 2592 | if deepstack_input_embeds is not None: |
| 2593 | to_concat.append( |
| 2594 | deepstack_input_embeds.permute(1, 0, 2).reshape( |
| 2595 | deepstack_input_embeds.shape[1], -1 |
| 2596 | ) |
| 2597 | ) |
| 2598 | |
| 2599 | expanded_positions = None |
| 2600 | if self.is_multimodal_pruning_enabled: |
| 2601 | is_vision_start = repl_token_ids.eq(self.config.vision_start_token_id) |
| 2602 | expanded_positions = self._get_expanded_positions( |
| 2603 | device=merged_embeddings.device, |
| 2604 | seq_len=merged_embeddings.shape[0], |
| 2605 | video_grid_thw=video_grid_thw, |
| 2606 | num_tokens_per_frame=num_tokens_per_frame, |
| 2607 | timestamps=timestamps, |
| 2608 | is_video_embed=is_video_embed, |
| 2609 | is_vision_start=is_vision_start, |
| 2610 | retention_mask=retention_mask, |
| 2611 | ) |
| 2612 | to_concat.append(expanded_positions) |
| 2613 | |
| 2614 | final_video_embeddings = torch.cat(to_concat, dim=-1) |
| 2615 | |
| 2616 | return final_video_embeddings |
| 2617 | |
| 2618 | def _get_expanded_positions( |
| 2619 | self, |
| 2620 | device, |
| 2621 | seq_len, |
| 2622 | video_grid_thw, |
| 2623 | num_tokens_per_frame, |
| 2624 | timestamps, |
| 2625 | is_video_embed, |
| 2626 | is_vision_start, |
| 2627 | retention_mask, |
| 2628 | ): |
| 2629 | embed_token_id = _cached_tensor(self.config.video_token_id, device=device) |
| 2630 | |
| 2631 | # Expand positions to match the full sequence length |
| 2632 | # (includes both video tokens and indicator tokens) |
| 2633 | # Shape: [full_length, 5] where positions are filled for video tokens |
| 2634 | # and zeros for indicator tokens. |
| 2635 | # Channel 3 flags VISION_START tokens so that |
| 2636 | # recompute_mrope_positions can reliably count timestamp tokens |
| 2637 | # (even when early frames have all video tokens pruned). |
| 2638 | # Channel 4 flags video-embedding tokens. |
| 2639 | expanded_positions = torch.zeros( |
| 2640 | seq_len, |
| 2641 | 5, # [t_index, h_index, w_index, is_vision_start, is_video] |
| 2642 | device=device, |
| 2643 | dtype=torch.long, |
| 2644 | ) |
| 2645 | _, h, w = video_grid_thw |
| 2646 | merge_size = self.visual.spatial_merge_size |
| 2647 | num_frames = len(num_tokens_per_frame) |
| 2648 | unpruned_token_ids = Qwen3VLMultiModalProcessor.get_video_repl( |
| 2649 | tokens_per_frame=[(h // merge_size) * (w // merge_size)] * num_frames, |
| 2650 | tokenizer=self._tokenizer, |
| 2651 | timestamps=timestamps, |
| 2652 | vision_start_token_id=self.config.vision_start_token_id, |
| 2653 | vision_end_token_id=self.config.vision_end_token_id, |
| 2654 | video_token_id=self.config.video_token_id, |
| 2655 | ).full |
| 2656 | unpruned_token_ids_tensor = torch.tensor(unpruned_token_ids, device=device) |
| 2657 | mm_feature = MultiModalFeatureSpec( |
| 2658 | data=MultiModalKwargsItem( |
| 2659 | { |
| 2660 | "video_grid_thw": MultiModalFieldElem( |
| 2661 | data=torch.tensor(video_grid_thw), |
| 2662 | field=MultiModalFieldConfig.batched("video").field, |
| 2663 | ), |
| 2664 | } |
| 2665 | ), |
| 2666 | modality="video", |
| 2667 | identifier="DUMMY", |
| 2668 | mm_position=PlaceholderRange(offset=0, length=len(unpruned_token_ids)), |
| 2669 | ) |
| 2670 | original_mrope_cpu = self.get_mrope_input_positions( |
| 2671 | input_tokens=unpruned_token_ids, |
| 2672 | mm_features=[mm_feature], |
| 2673 | )[0] |
| 2674 | original_mrope = async_tensor_h2d(original_mrope_cpu, device).permute(1, 0) |
| 2675 | full_is_video_embed = unpruned_token_ids_tensor == embed_token_id |
| 2676 | |
| 2677 | with gpu_sync_allowed(): |
| 2678 | expanded_positions[is_video_embed, :3] = original_mrope[ |
| 2679 | full_is_video_embed |
| 2680 | ][retention_mask] |
| 2681 | expanded_positions[~is_video_embed, :3] = original_mrope[ |
| 2682 | ~full_is_video_embed |
| 2683 | ] |
| 2684 | expanded_positions[..., 3] = is_vision_start |
| 2685 | expanded_positions[..., 4] = is_video_embed |
| 2686 | |
| 2687 | return expanded_positions |
| 2688 | |
| 2689 | def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict: |
| 2690 | mm_input_by_modality: dict[ |
| 2691 | str, Qwen2_5_VLImageInputs | Qwen2_5_VLVideoInputs | None |
| 2692 | ] = {} |
| 2693 | for input_key in kwargs: |
| 2694 | if ( |
| 2695 | input_key in ("pixel_values", "image_embeds") |
| 2696 | and "image" not in mm_input_by_modality |
| 2697 | ): |
| 2698 | mm_input_by_modality["image"] = self._parse_and_validate_image_input( |
| 2699 | **kwargs |
| 2700 | ) |
| 2701 | if ( |
| 2702 | input_key in ("pixel_values_videos", "video_embeds") |
| 2703 | and "video" not in mm_input_by_modality |
| 2704 | ): |
| 2705 | mm_input_by_modality["video"] = self._parse_and_validate_video_input( |
| 2706 | **kwargs |
| 2707 | ) |
| 2708 | return mm_input_by_modality |
| 2709 | |
| 2710 | @staticmethod |
| 2711 | def _iter_mm_grid_hw( |
| 2712 | input_tokens: list[int], |
| 2713 | mm_features: list[MultiModalFeatureSpec], |
| 2714 | video_token_id: int, |
| 2715 | vision_start_token_id: int, |
| 2716 | vision_end_token_id: int, |
| 2717 | spatial_merge_size: int, |
| 2718 | ) -> Iterator[tuple[int, int, int, int]]: |
| 2719 | """Iterate over multimodal features and yield position info. |
| 2720 | |
| 2721 | Args: |
| 2722 | input_tokens: List of token IDs in the input sequence. |
| 2723 | mm_features: List of multimodal feature specifications containing |
| 2724 | image/video data and position information. |
| 2725 | video_token_id: Token ID used for video tokens. |
| 2726 | vision_start_token_id: Token ID marking the start of a vision sequence. |
| 2727 | vision_end_token_id: Token ID marking the end of a vision sequence. |
| 2728 | spatial_merge_size: Size of the spatial merge operation used to |
| 2729 | compute logical grid dimensions from the original feature grid. |
| 2730 | |
| 2731 | Yields: |
| 2732 | offset: Position of the first video/image token in the sequence. |
| 2733 | llm_grid_h: Logical grid height (may not match actual token count with EVS). |
| 2734 | llm_grid_w: Logical grid width (may not match actual token count with EVS). |
| 2735 | actual_num_tokens: Actual number of video/image tokens in the placeholder. |
| 2736 | |
| 2737 | """ |
| 2738 | for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset): |
| 2739 | offset = mm_feature.mm_position.offset |
| 2740 | data = mm_feature.data |
| 2741 | assert data is not None |
| 2742 | if mm_feature.modality == "image": |
| 2743 | grid = data["image_grid_thw"].data |
| 2744 | assert isinstance(grid, torch.Tensor) |
| 2745 | t, h, w = grid.tolist() |
| 2746 | assert t == 1, f"Image must have 1 frame, got {t}" |
| 2747 | llm_grid_h = h // spatial_merge_size |
| 2748 | llm_grid_w = w // spatial_merge_size |
| 2749 | yield offset, llm_grid_h, llm_grid_w, llm_grid_h * llm_grid_w |
| 2750 | elif mm_feature.modality == "video": |
| 2751 | grid = data["video_grid_thw"].data |
| 2752 | assert isinstance(grid, torch.Tensor) |
| 2753 | t, h, w = grid.tolist() |
| 2754 | llm_grid_h = h // spatial_merge_size |
| 2755 | llm_grid_w = w // spatial_merge_size |
| 2756 | |
| 2757 | for _ in range(t): |
| 2758 | # When EVS is enabled, some frames may have 0 video tokens in the |
| 2759 | # placeholder. We use `vision_start_token_id` to locate each frame |
| 2760 | # since it is always present for every frame. |
| 2761 | # We then look for the first `video_token_id` after |
| 2762 | # `vision_start_token_id` and before `vision_end_token_id`. |
| 2763 | offset = input_tokens.index(vision_start_token_id, offset) |
| 2764 | vision_end_offset = input_tokens.index(vision_end_token_id, offset) |
| 2765 | |
| 2766 | try: |
| 2767 | actual_num_tokens = 0 |
| 2768 | video_offset = input_tokens.index( |
| 2769 | video_token_id, offset, vision_end_offset |
| 2770 | ) |
| 2771 | # NOTE: looking at the |
| 2772 | # `Qwen3VLMultiModalProcessor.get_video_repl` code, we can |
| 2773 | # see that we can use the below formula to get the token |
| 2774 | # count, since everything in between `video_offset` and |
| 2775 | # `vision_end_offset` is populated as `video_token_id`. |
| 2776 | # This saves us from manually counting the number tokens |
| 2777 | # that match `video_token_id` in between. |
| 2778 | actual_num_tokens += vision_end_offset - video_offset |
| 2779 | except ValueError: |
| 2780 | # No `video_token_id` in this frame (EVS with 0 tokens for |
| 2781 | # this frame) -> use `offset + 1`` to move past |
| 2782 | # `vision_start_token_id`. |
| 2783 | video_offset = offset + 1 |
| 2784 | |
| 2785 | yield video_offset, llm_grid_h, llm_grid_w, actual_num_tokens |
| 2786 | # Move offset past this frame for next iteration. |
| 2787 | offset = vision_end_offset + 1 |
| 2788 | else: |
| 2789 | raise ValueError(f"Unsupported modality: {mm_feature.modality}") |
| 2790 | |
| 2791 | def get_mrope_input_positions( |
| 2792 | self, |
| 2793 | input_tokens: list[int], |
| 2794 | mm_features: list[MultiModalFeatureSpec], |
| 2795 | ) -> tuple[torch.Tensor, int]: |
| 2796 | return self._get_mrope_input_positions( |
| 2797 | input_tokens=input_tokens, |
| 2798 | mm_features=mm_features, |
| 2799 | config=self.config, |
| 2800 | ) |
| 2801 | |
| 2802 | @staticmethod |
| 2803 | def _get_mrope_input_positions( |
| 2804 | input_tokens: list[int], |
| 2805 | mm_features: list[MultiModalFeatureSpec], |
| 2806 | config: Qwen3VLConfig, |
| 2807 | ): |
| 2808 | llm_pos_ids_list: list[np.ndarray] = [] |
| 2809 | st = 0 |
| 2810 | for ( |
| 2811 | offset, |
| 2812 | llm_grid_h, |
| 2813 | llm_grid_w, |
| 2814 | actual_num_tokens, |
| 2815 | ) in Qwen3VLForConditionalGeneration._iter_mm_grid_hw( |
| 2816 | input_tokens, |
| 2817 | mm_features, |
| 2818 | video_token_id=config.video_token_id, |
| 2819 | vision_start_token_id=config.vision_start_token_id, |
| 2820 | vision_end_token_id=config.vision_end_token_id, |
| 2821 | spatial_merge_size=config.vision_config.spatial_merge_size, |
| 2822 | ): |
| 2823 | # Skip frames with 0 tokens (EVS placeholder with tokens lumped elsewhere) |
| 2824 | if actual_num_tokens == 0: |
| 2825 | continue |
| 2826 | |
| 2827 | text_len = offset - st |
| 2828 | st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 |
| 2829 | llm_pos_ids_list.append( |
| 2830 | np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx |
| 2831 | ) |
| 2832 | |
| 2833 | # Check if this is a "lumped placeholder" (all tokens from multiple frames |
| 2834 | # assigned to the 0-th frame - see |
| 2835 | # `Qwen3VLMultiModalProcessor.get_video_repl`. |
| 2836 | expected_tokens_per_frame = llm_grid_h * llm_grid_w |
| 2837 | if actual_num_tokens > expected_tokens_per_frame: |
| 2838 | # Lumped placeholder: create grid positions for all "logical" frames |
| 2839 | # represented. |
| 2840 | num_logical_frames = actual_num_tokens // expected_tokens_per_frame |
| 2841 | remainder = actual_num_tokens % expected_tokens_per_frame |
| 2842 | |
| 2843 | # Create positions for complete frames. |
| 2844 | for _ in range(num_logical_frames): |
| 2845 | grid_indices = np.indices((1, llm_grid_h, llm_grid_w)).reshape( |
| 2846 | 3, -1 |
| 2847 | ) |
| 2848 | llm_pos_ids_list.append(grid_indices + text_len + st_idx) |
| 2849 | st_idx = llm_pos_ids_list[-1].max() + 1 |
| 2850 | text_len = 0 # No text between frames within the lump |
| 2851 | |
| 2852 | # Handle remainder tokens if any (partial frame). |
| 2853 | # NOTE: this should never be the case. Should we have an assert? |
| 2854 | if remainder > 0: |
| 2855 | # Create a partial grid - take first 'remainder' positions |
| 2856 | full_grid = np.indices((1, llm_grid_h, llm_grid_w)).reshape(3, -1) |
| 2857 | grid_indices = full_grid[:, :remainder] |
| 2858 | llm_pos_ids_list.append(grid_indices + text_len + st_idx) |
| 2859 | else: |
| 2860 | # Normal case: frame has exactly the expected tokens (after actual EVS |
| 2861 | # pruning). |
| 2862 | grid_indices = np.indices((1, llm_grid_h, llm_grid_w)).reshape(3, -1) |
| 2863 | llm_pos_ids_list.append(grid_indices + text_len + st_idx) |
| 2864 | |
| 2865 | st = offset + actual_num_tokens |
| 2866 | |
| 2867 | if st < len(input_tokens): |
| 2868 | st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0 |
| 2869 | text_len = len(input_tokens) - st |
| 2870 | llm_pos_ids_list.append( |
| 2871 | np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx |
| 2872 | ) |
| 2873 | |
| 2874 | llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1) |
| 2875 | mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item() |
| 2876 | return torch.from_numpy(llm_positions), mrope_position_delta |
| 2877 | |
| 2878 | def recompute_mrope_positions( |
| 2879 | self, |
| 2880 | input_ids: list[int] | torch.Tensor, |
| 2881 | multimodal_embeddings: Sequence[torch.Tensor], |
| 2882 | mrope_positions: torch.LongTensor, |
| 2883 | num_computed_tokens: int, |
| 2884 | ) -> tuple[Sequence[torch.Tensor], torch.Tensor, int]: |
| 2885 | """Update part of input mrope positions (starting with |
| 2886 | num_computed_tokens index). Original mrope_positions are computed |
| 2887 | for unpruned sequence and becomes incorrect once pruning occurs, |
| 2888 | so once we prune media tokens we should reflect this in the |
| 2889 | mrope_positions before we feed it to LLM. |
| 2890 | |
| 2891 | Args: |
| 2892 | input_ids: (N,) All input tokens of the prompt containing |
| 2893 | entire sequence. |
| 2894 | multimodal_embeddings: Sequence of multimodal embeddings that |
| 2895 | fits into the prefill chunk that is being processed. |
| 2896 | mrope_positions: Existing mrope positions (3, N) for entire |
| 2897 | sequence |
| 2898 | num_computed_tokens: A number of computed tokens so far. |
| 2899 | |
| 2900 | Returns: |
| 2901 | Tuple of (multimodal_embeddings, mrope_positions, |
| 2902 | mrope_position_delta). |
| 2903 | |
| 2904 | """ |
| 2905 | return self._recompute_mrope_positions( |
| 2906 | input_ids=input_ids, |
| 2907 | multimodal_embeddings=multimodal_embeddings, |
| 2908 | mrope_positions=mrope_positions, |
| 2909 | num_computed_tokens=num_computed_tokens, |
| 2910 | image_token_id=self.config.image_token_id, |
| 2911 | video_token_id=self.config.video_token_id, |
| 2912 | vision_start_token_id=self.config.vision_start_token_id, |
| 2913 | ) |
| 2914 | |
| 2915 | @staticmethod |
| 2916 | def _recompute_mrope_positions( |
| 2917 | input_ids: list[int] | torch.Tensor, |
| 2918 | multimodal_embeddings: Sequence[torch.Tensor], |
| 2919 | mrope_positions: torch.LongTensor, |
| 2920 | num_computed_tokens: int, |
| 2921 | vision_start_token_id: int, |
| 2922 | image_token_id: int, |
| 2923 | video_token_id: int, |
| 2924 | ) -> tuple[Sequence[torch.Tensor], torch.Tensor, int]: |
| 2925 | # Device |
| 2926 | device = ( |
| 2927 | multimodal_embeddings[0].device |
| 2928 | if len(multimodal_embeddings) |
| 2929 | else mrope_positions.device |
| 2930 | ) |
| 2931 | |
| 2932 | # Tensors. input_ids may already be a (device-side) tensor. |
| 2933 | if isinstance(input_ids, torch.Tensor): |
| 2934 | assert input_ids.device == device |
| 2935 | input_ids_t = input_ids.to(dtype=torch.long) |
| 2936 | else: |
| 2937 | input_ids_t = async_tensor_h2d(input_ids, device=device, dtype=torch.long) |
| 2938 | |
| 2939 | mm_embeddings_out = [] |
| 2940 | mm_embeddings_pos = [] |
| 2941 | # Strip position information from embeddings (last 5 channels) |
| 2942 | # For Qwen3 VL, handle potentially empty frames (from unpacking) |
| 2943 | for mm in multimodal_embeddings: |
| 2944 | if mm.shape[0] > 0: # Only process non-empty frames |
| 2945 | mm_embeddings_out.append(mm[:, :-5]) |
| 2946 | mm_embeddings_pos.append(mm[:, -5:].permute(1, 0).long()) |
| 2947 | else: |
| 2948 | # Empty frame - keep as is |
| 2949 | mm_embeddings_out.append(mm) |
| 2950 | # Create empty position tensor with correct shape |
| 2951 | mm_embeddings_pos.append( |
| 2952 | torch.empty(5, 0, device=device, dtype=torch.long) |
| 2953 | ) |
| 2954 | |
| 2955 | with gpu_sync_allowed(): |
| 2956 | positions, mrope_positions_delta = recompute_mrope_positions( |
| 2957 | input_ids_t, |
| 2958 | mm_embeddings_pos, |
| 2959 | mrope_positions, |
| 2960 | num_computed_tokens, |
| 2961 | vision_start_token_id, |
| 2962 | image_token_id, |
| 2963 | video_token_id, |
| 2964 | ) |
| 2965 | |
| 2966 | return mm_embeddings_out, positions, mrope_positions_delta |
| 2967 | |
| 2968 | def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None: |
| 2969 | mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs) |
| 2970 | if not mm_input_by_modality: |
| 2971 | return None |
| 2972 | |
| 2973 | # The result multimodal_embeddings is tuple of tensors, with each |
| 2974 | # tensor corresponding to a multimodal data item (image or video). |
| 2975 | multimodal_embeddings: list[torch.Tensor] = [] |
| 2976 | |
| 2977 | # NOTE: It is important to iterate over the keys in this dictionary |
| 2978 | # to preserve the order of the modalities. |
| 2979 | for modality in mm_input_by_modality: |
| 2980 | multimodal_input = mm_input_by_modality[modality] |
| 2981 | if modality == "image": |
| 2982 | image_embeddings = self._process_image_input(multimodal_input) |
| 2983 | image_embeddings = self._postprocess_image_embeds_evs( |
| 2984 | image_embeddings, multimodal_input |
| 2985 | ) |
| 2986 | multimodal_embeddings.extend(image_embeddings) |
| 2987 | if modality == "video": |
| 2988 | video_embeddings = self._process_video_input(multimodal_input) |
| 2989 | if self.is_multimodal_pruning_enabled: |
| 2990 | video_embeddings = self._postprocess_video_embeds_evs( |
| 2991 | video_embeddings, multimodal_input |
| 2992 | ) |
| 2993 | multimodal_embeddings.extend(video_embeddings) |
| 2994 | |
| 2995 | embeddings_tuple = tuple(multimodal_embeddings) |
| 2996 | return embeddings_tuple |
| 2997 | |
| 2998 | def _compute_deepstack_embeds( |
| 2999 | self, |
| 3000 | inputs_embeds: torch.Tensor, |
| 3001 | multimodal_embeddings: MultiModalEmbeddings, |
| 3002 | is_multimodal: torch.Tensor, |
| 3003 | ) -> tuple[torch.Tensor, MultiModalEmbeddings]: |
| 3004 | visual_lens = [len(x) for x in multimodal_embeddings] |
| 3005 | multimodal_embeddings_cat = torch.cat(multimodal_embeddings, dim=0) |
| 3006 | |
| 3007 | ( |
| 3008 | multimodal_embeddings_main, |
| 3009 | multimodal_embeddings_multiscale, |
| 3010 | ) = torch.split( |
| 3011 | multimodal_embeddings_cat, |
| 3012 | [self.visual_dim, self.multiscale_dim], |
| 3013 | dim=-1, |
| 3014 | ) |
| 3015 | |
| 3016 | multimodal_embeddings = torch.split( |
| 3017 | multimodal_embeddings_main, visual_lens, dim=0 |
| 3018 | ) |
| 3019 | multimodal_embeddings_multiscale = torch.split( |
| 3020 | multimodal_embeddings_multiscale, visual_lens, dim=0 |
| 3021 | ) |
| 3022 | |
| 3023 | deepstack_input_embeds = inputs_embeds.new_zeros( |
| 3024 | inputs_embeds.size(0), self.deepstack_num_level * inputs_embeds.size(1) |
| 3025 | ) |
| 3026 | |
| 3027 | deepstack_input_embeds = _merge_multimodal_embeddings( |
| 3028 | inputs_embeds=deepstack_input_embeds, |
| 3029 | multimodal_embeddings=multimodal_embeddings_multiscale, |
| 3030 | is_multimodal=is_multimodal, |
| 3031 | ) |
| 3032 | deepstack_input_embeds = deepstack_input_embeds.view( |
| 3033 | inputs_embeds.shape[0], self.deepstack_num_level, self.visual_dim |
| 3034 | ) |
| 3035 | deepstack_input_embeds = deepstack_input_embeds.permute(1, 0, 2) |
| 3036 | |
| 3037 | return deepstack_input_embeds, multimodal_embeddings |
| 3038 | |
| 3039 | def embed_input_ids( |
| 3040 | self, |
| 3041 | input_ids: torch.Tensor, |
| 3042 | multimodal_embeddings: MultiModalEmbeddings | None = None, |
| 3043 | *, |
| 3044 | is_multimodal: torch.Tensor | None = None, |
| 3045 | ) -> torch.Tensor: |
| 3046 | inputs_embeds = self._embed_text_input_ids( |
| 3047 | input_ids, |
| 3048 | self.language_model.embed_input_ids, |
| 3049 | is_multimodal=is_multimodal, |
| 3050 | ) |
| 3051 | |
| 3052 | if multimodal_embeddings is None or len(multimodal_embeddings) == 0: |
| 3053 | return inputs_embeds |
| 3054 | |
| 3055 | is_multimodal = _require_is_multimodal(is_multimodal) |
| 3056 | |
| 3057 | if self.use_deepstack: |
| 3058 | ( |
| 3059 | deepstack_input_embeds, |
| 3060 | multimodal_embeddings, |
| 3061 | ) = self._compute_deepstack_embeds( |
| 3062 | inputs_embeds=inputs_embeds, |
| 3063 | multimodal_embeddings=multimodal_embeddings, |
| 3064 | is_multimodal=is_multimodal, |
| 3065 | ) |
| 3066 | else: |
| 3067 | deepstack_input_embeds = None |
| 3068 | |
| 3069 | inputs_embeds = _merge_multimodal_embeddings( |
| 3070 | inputs_embeds=inputs_embeds, |
| 3071 | multimodal_embeddings=multimodal_embeddings, |
| 3072 | is_multimodal=is_multimodal, |
| 3073 | ) |
| 3074 | |
| 3075 | if deepstack_input_embeds is not None: |
| 3076 | self._set_deepstack_input_embeds(deepstack_input_embeds) |
| 3077 | |
| 3078 | return inputs_embeds |
| 3079 | |
| 3080 | def forward( |
| 3081 | self, |
| 3082 | input_ids: torch.Tensor | None, |
| 3083 | positions: torch.Tensor, |
| 3084 | intermediate_tensors: IntermediateTensors | None = None, |
| 3085 | inputs_embeds: torch.Tensor | None = None, |
| 3086 | **kwargs: object, |
| 3087 | ) -> torch.Tensor | IntermediateTensors: |
| 3088 | """Run forward pass for Qwen3VL. |
| 3089 | |
| 3090 | Args: |
| 3091 | input_ids: Flattened (concatenated) input_ids corresponding to a |
| 3092 | batch. |
| 3093 | positions: Flattened (concatenated) position ids corresponding to a |
| 3094 | batch. |
| 3095 | **NOTE**: If mrope is enabled (default setting for Qwen3VL |
| 3096 | opensource models), the shape will be `(3, seq_len)`, |
| 3097 | otherwise it will be `(seq_len,). |
| 3098 | intermediate_tensors: Intermediate tensors from previous pipeline |
| 3099 | stages. |
| 3100 | inputs_embeds: Pre-computed input embeddings. |
| 3101 | **kwargs: Additional keyword arguments including: |
| 3102 | - pixel_values: Pixel values to be fed to a model. |
| 3103 | `None` if no images are passed. |
| 3104 | - image_grid_thw: Tensor `(n_images, 3)` of image 3D grid in |
| 3105 | LLM. `None` if no images are passed. |
| 3106 | - pixel_values_videos: Pixel values of videos to be fed to a |
| 3107 | model. `None` if no videos are passed. |
| 3108 | - video_grid_thw: Tensor `(n_videos, 3)` of video 3D grid in |
| 3109 | LLM. `None` if no videos are passed. |
| 3110 | |
| 3111 | """ |
| 3112 | if intermediate_tensors is not None: |
| 3113 | inputs_embeds = None |
| 3114 | |
| 3115 | if inputs_embeds is not None and get_pp_group().is_first_rank: |
| 3116 | deepstack_input_embeds = self._get_deepstack_input_embeds( |
| 3117 | inputs_embeds.size(0) |
| 3118 | ) |
| 3119 | else: |
| 3120 | deepstack_input_embeds = None |
| 3121 | |
| 3122 | hidden_states = self.language_model.model( |
| 3123 | input_ids=input_ids, |
| 3124 | positions=positions, |
| 3125 | intermediate_tensors=intermediate_tensors, |
| 3126 | inputs_embeds=inputs_embeds, |
| 3127 | # args for deepstack |
| 3128 | deepstack_input_embeds=deepstack_input_embeds, |
| 3129 | ) |
| 3130 | |
| 3131 | if inputs_embeds is not None and get_pp_group().is_first_rank: |
| 3132 | self._clear_deepstack_input_embeds(inputs_embeds.size(0)) |
| 3133 | |
| 3134 | return hidden_states |
| 3135 | |
| 3136 | def compute_logits( |
| 3137 | self, |
| 3138 | hidden_states: torch.Tensor, |
| 3139 | ) -> torch.Tensor | None: |
| 3140 | return self.language_model.compute_logits(hidden_states) |
| 3141 | |
| 3142 | def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: |
| 3143 | loader = AutoWeightsLoader(self) |
| 3144 | return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper) |
| 3145 | |
| 3146 | def get_mm_mapping(self) -> MultiModelKeys: |
| 3147 | """Get the module prefix in multimodal models.""" |
| 3148 | return MultiModelKeys.from_string_field( |
| 3149 | language_model="language_model", |
| 3150 | connector=["visual.merger", "visual.deepstack_merger_list"], |
| 3151 | tower_model="visual.", |
| 3152 | ) |
| 3153 | |
| 3154 | def get_mm_lora_token_counts( |
| 3155 | self, |
| 3156 | *, |
| 3157 | modality: str, |
| 3158 | mm_kwargs: MultiModalKwargsItem | None, |
| 3159 | num_mm_embeds: int, |
| 3160 | ) -> tuple[int, int | None]: |
| 3161 | del modality, mm_kwargs |
| 3162 | hf_config = self.config |
| 3163 | vision_config = hf_config.vision_config |
| 3164 | merge_size = vision_config.spatial_merge_size |
| 3165 | return num_mm_embeds * merge_size**2, num_mm_embeds |
| 3166 | |
| 3167 | |
| 3168 | @lru_cache |
| 3169 | def _cached_tensor(x, device) -> torch.Tensor: |
| 3170 | return torch.tensor(x, device=device) |