2059 lines
75 KiB
Python
2059 lines
75 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
# Adapted from
|
|
# https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2_5_vl/modeling_qwen2_5_vl.py
|
|
# Copyright 2025 The vLLM team.
|
|
# Copyright 2025 The Qwen Team.
|
|
# Copyright 2025 The HuggingFace Inc. team.
|
|
# All rights reserved.
|
|
#
|
|
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
|
|
# and OPT implementations in this library. It has been modified from its
|
|
# original forms to accommodate minor architectural differences compared
|
|
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
"""Inference-only Qwen2.5-VL model compatible with HuggingFace weights."""
|
|
|
|
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
|
|
from functools import lru_cache, partial
|
|
from typing import Annotated, Any, Literal, TypeAlias
|
|
|
|
import einops
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn as nn
|
|
import torch.nn.functional as F
|
|
from transformers import BatchFeature
|
|
from transformers.models.qwen2_5_vl import Qwen2_5_VLProcessor
|
|
from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import (
|
|
Qwen2_5_VLConfig,
|
|
Qwen2_5_VLVisionConfig,
|
|
)
|
|
|
|
from vllm.compilation.decorators import (
|
|
should_torch_compile_mm_encoder,
|
|
support_torch_compile,
|
|
)
|
|
from vllm.config import VllmConfig
|
|
from vllm.distributed import parallel_state
|
|
from vllm.distributed import utils as dist_utils
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.layers.activation import get_act_and_mul_fn
|
|
from vllm.model_executor.layers.attention import MMEncoderAttention
|
|
from vllm.model_executor.layers.conv import Conv3dLayer
|
|
from vllm.model_executor.layers.layernorm import RMSNorm
|
|
from vllm.model_executor.layers.linear import (
|
|
ColumnParallelLinear,
|
|
MergedColumnParallelLinear,
|
|
QKVParallelLinear,
|
|
RowParallelLinear,
|
|
)
|
|
from vllm.model_executor.layers.quantization import QuantizationConfig
|
|
from vllm.model_executor.layers.rotary_embedding import get_rope
|
|
from vllm.model_executor.layers.rotary_embedding.common import (
|
|
ApplyRotaryEmb,
|
|
)
|
|
from vllm.model_executor.models.module_mapping import MultiModelKeys
|
|
from vllm.multimodal import MULTIMODAL_REGISTRY
|
|
from vllm.multimodal.evs import (
|
|
compute_mrope_for_media,
|
|
compute_retained_tokens_count,
|
|
compute_retention_mask,
|
|
recompute_mrope_positions,
|
|
)
|
|
from vllm.multimodal.inputs import (
|
|
MultiModalFeatureSpec,
|
|
MultiModalFieldConfig,
|
|
MultiModalKwargsItems,
|
|
)
|
|
from vllm.multimodal.parse import MultiModalDataItems
|
|
from vllm.multimodal.processing import PromptReplacement, PromptUpdate
|
|
from vllm.platforms import current_platform
|
|
from vllm.sequence import IntermediateTensors
|
|
from vllm.utils.tensor_schema import TensorSchema, TensorShape
|
|
from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
|
|
from vllm.v1.attention.backends.registry import AttentionBackendEnum
|
|
from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
|
|
|
|
from .interfaces import (
|
|
MultiModalEmbeddings,
|
|
SupportsEagle,
|
|
SupportsEagle3,
|
|
SupportsEncoderCudaGraph,
|
|
SupportsLoRA,
|
|
SupportsMRoPE,
|
|
SupportsMultiModal,
|
|
SupportsMultiModalPruning,
|
|
SupportsPP,
|
|
SupportsQuant,
|
|
)
|
|
from .qwen2_vl import Qwen2VLDummyInputsBuilder as Qwen2_5_VLDummyInputsBuilder
|
|
from .qwen2_vl import (
|
|
Qwen2VLMultiModalProcessor,
|
|
Qwen2VLProcessingInfo,
|
|
)
|
|
from .utils import (
|
|
AutoWeightsLoader,
|
|
WeightsMapper,
|
|
cast_overflow_tensors,
|
|
init_vllm_registered_model,
|
|
maybe_prefix,
|
|
)
|
|
from .vision import (
|
|
get_fp8_padded_hidden_size,
|
|
get_vit_attn_backend,
|
|
is_vit_use_data_parallel,
|
|
run_dp_sharded_mrope_vision_model,
|
|
)
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def _pad_cumulative_seqlens_buffer(
|
|
dst: torch.Tensor,
|
|
src: torch.Tensor,
|
|
) -> None:
|
|
n = src.shape[0]
|
|
dst.zero_()
|
|
dst[:n].copy_(src)
|
|
if n < dst.shape[0]:
|
|
dst[n:] = src[-1]
|
|
|
|
|
|
def _pad_flashinfer_cu_seqlens_buffer(
|
|
dst: torch.Tensor,
|
|
src: torch.Tensor,
|
|
) -> None:
|
|
src_mid = src.shape[0] // 2
|
|
dst_mid = dst.shape[0] // 2
|
|
assert src_mid <= dst_mid, (
|
|
f"FlashInfer cu_seqlens replay buffer is larger than capture buffer: "
|
|
f"src_section={src_mid}, dst_section={dst_mid}"
|
|
)
|
|
|
|
dst.zero_()
|
|
dst[:src_mid].copy_(src[:src_mid])
|
|
if src_mid < dst_mid:
|
|
dst[src_mid:dst_mid] = src[src_mid - 1]
|
|
|
|
dst[dst_mid : dst_mid + src_mid].copy_(src[src_mid:])
|
|
if dst_mid + src_mid < dst.shape[0]:
|
|
dst[dst_mid + src_mid :] = src[-1]
|
|
|
|
|
|
# === Vision Inputs === #
|
|
|
|
|
|
class Qwen2_5_VLImagePixelInputs(TensorSchema):
|
|
"""
|
|
Dimensions:
|
|
- np: Number of patches
|
|
- ni: Number of images
|
|
- cps: Number of channels * patch_size * patch_size
|
|
|
|
Historical context:
|
|
- pixel_values shape: (num_patches, num_channels * patch_size *
|
|
patch_size)
|
|
- image_grid_thw shape: (num_images, 3) in (grid_t, grid_h, grid_w)
|
|
format.
|
|
"""
|
|
|
|
type: Literal["pixel_values"]
|
|
|
|
pixel_values: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("np", "cps"),
|
|
]
|
|
|
|
image_grid_thw: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("ni", 3),
|
|
]
|
|
|
|
|
|
class Qwen2_5_VLImageEmbeddingInputs(TensorSchema):
|
|
"""
|
|
Dimensions:
|
|
- nf: Number of image features
|
|
- hs: Hidden size
|
|
- ni: Number of images
|
|
|
|
Historical context:
|
|
- image_embeds shape: (num_image_features, hidden_size)
|
|
- num_image_features varies based on the number and resolution of the
|
|
images.
|
|
- hidden_size must match the hidden size of language model backbone.
|
|
- image_grid_thw shape: (num_images, 3) in (grid_t, grid_h, grid_w)
|
|
format
|
|
"""
|
|
|
|
type: Literal["image_embeds"]
|
|
|
|
image_embeds: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("nf", "hs"),
|
|
]
|
|
|
|
image_grid_thw: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("ni", 3),
|
|
]
|
|
|
|
|
|
Qwen2_5_VLImageInputs: TypeAlias = (
|
|
Qwen2_5_VLImagePixelInputs | Qwen2_5_VLImageEmbeddingInputs
|
|
)
|
|
|
|
|
|
class Qwen2_5_VLVideoPixelInputs(TensorSchema):
|
|
"""
|
|
Dimensions:
|
|
- np: Number of patches
|
|
- nv: Number of videos
|
|
- ctps: Number of channels * temporal_patch_size * patch_size *
|
|
patch_size
|
|
|
|
Historical context:
|
|
- pixel_values_videos shape: (num_patches, num_channels *
|
|
temporal_patch_size * patch_size * patch_size)
|
|
- video_grid_thw shape: (num_videos, 3) in (grid_t, grid_h, grid_w)
|
|
format
|
|
- second_per_grid_ts: The video time interval (in seconds) for each
|
|
grid along the temporal dimension in the 3D position IDs. Returned
|
|
when `videos` is not `None`.
|
|
- timestamps: List of timestamp values (in seconds) for each frame
|
|
after merging. Length equals the temporal dimension after merging.
|
|
"""
|
|
|
|
type: Literal["pixel_values_videos"]
|
|
|
|
pixel_values_videos: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("np", "ctps"),
|
|
]
|
|
|
|
video_grid_thw: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("nv", 3),
|
|
]
|
|
|
|
second_per_grid_ts: Annotated[
|
|
torch.Tensor | None,
|
|
TensorShape("nv"),
|
|
]
|
|
|
|
timestamps: list[list[float]] | None = None
|
|
|
|
|
|
class Qwen2_5_VLVideoEmbeddingInputs(TensorSchema):
|
|
"""
|
|
Dimensions:
|
|
- nf: Number of video features
|
|
- hs: Hidden size
|
|
- nv: Number of videos
|
|
|
|
Historical context:
|
|
- video_embeds shape: (num_video_features, hidden_size)
|
|
- num_video_features varies based on the number and resolution of the
|
|
videos.
|
|
- hidden_size must match the hidden size of language model backbone.
|
|
- video_grid_thw shape: (num_videos, 3) in (grid_t, grid_h, grid_w)
|
|
format
|
|
- second_per_grid_ts: The video time interval (in seconds) for each
|
|
grid along the temporal dimension in the 3D position IDs. Returned
|
|
when `videos` is not `None`.
|
|
- timestamps: List of timestamp values (in seconds) for each frame
|
|
after merging. Length equals the temporal dimension after merging.
|
|
"""
|
|
|
|
type: Literal["video_embeds"]
|
|
|
|
video_embeds: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("nf", "hs"),
|
|
]
|
|
|
|
video_grid_thw: Annotated[
|
|
torch.Tensor,
|
|
TensorShape("nv", 3),
|
|
]
|
|
|
|
second_per_grid_ts: Annotated[
|
|
torch.Tensor | None,
|
|
TensorShape("nv"),
|
|
] = None
|
|
timestamps: list[list[float]] | None = None
|
|
|
|
|
|
Qwen2_5_VLVideoInputs: TypeAlias = (
|
|
Qwen2_5_VLVideoPixelInputs | Qwen2_5_VLVideoEmbeddingInputs
|
|
)
|
|
|
|
# === Vision Encoder === #
|
|
|
|
|
|
class Qwen2_5_VisionMLP(nn.Module):
|
|
def __init__(
|
|
self,
|
|
in_features: int,
|
|
hidden_features: int,
|
|
bias: bool = False,
|
|
act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
):
|
|
super().__init__()
|
|
use_data_parallel = is_vit_use_data_parallel()
|
|
self.gate_up_proj = MergedColumnParallelLinear(
|
|
input_size=in_features,
|
|
output_sizes=[hidden_features] * 2, # [gate_proj, up_proj]
|
|
bias=bias,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.gate_up_proj",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
|
|
self.down_proj = RowParallelLinear(
|
|
hidden_features,
|
|
in_features,
|
|
bias=bias,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.down_proj",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
self.act_fn = act_fn
|
|
|
|
def forward(self, x: torch.Tensor):
|
|
gate_up, _ = self.gate_up_proj(x)
|
|
x = self.act_fn(gate_up)
|
|
x_down, _ = self.down_proj(x)
|
|
return x_down
|
|
|
|
|
|
class Qwen2_5_VisionAttention(nn.Module):
|
|
def __init__(
|
|
self,
|
|
embed_dim: int,
|
|
num_heads: int,
|
|
projection_size: int,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__()
|
|
# Per attention head and per partition values.
|
|
use_data_parallel = is_vit_use_data_parallel()
|
|
self.tp_size = (
|
|
1
|
|
if use_data_parallel
|
|
else parallel_state.get_tensor_model_parallel_world_size()
|
|
)
|
|
self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
|
|
self.hidden_size_per_attention_head = dist_utils.divide(
|
|
projection_size, num_heads
|
|
)
|
|
self.num_attention_heads_per_partition = dist_utils.divide(
|
|
num_heads, self.tp_size
|
|
)
|
|
|
|
self.qkv = QKVParallelLinear(
|
|
hidden_size=embed_dim,
|
|
head_size=self.hidden_size_per_attention_head,
|
|
total_num_heads=num_heads,
|
|
total_num_kv_heads=num_heads,
|
|
bias=True,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.qkv",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
|
|
self.proj = RowParallelLinear(
|
|
input_size=projection_size,
|
|
output_size=embed_dim,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.proj",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
|
|
self.attn = MMEncoderAttention(
|
|
num_heads=self.num_attention_heads_per_partition,
|
|
head_size=self.hidden_size_per_attention_head,
|
|
scale=self.hidden_size_per_attention_head**-0.5,
|
|
prefix=f"{prefix}.attn",
|
|
)
|
|
|
|
self.apply_rotary_emb = ApplyRotaryEmb(enforce_enable=True)
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
cu_seqlens: torch.Tensor,
|
|
rotary_pos_emb_cos: torch.Tensor,
|
|
rotary_pos_emb_sin: torch.Tensor,
|
|
max_seqlen: torch.Tensor, # Only used for Flash Attention
|
|
# Only used for FlashInfer CuDNN backend.
|
|
sequence_lengths: torch.Tensor | None,
|
|
) -> torch.Tensor:
|
|
# [s, b, c] --> [s, b, head * 3 * head_dim]
|
|
x, _ = self.qkv(x)
|
|
seq_len, batch_size, _ = x.shape
|
|
|
|
qkv = einops.rearrange(
|
|
x,
|
|
"s b (three head head_dim) -> b s three head head_dim",
|
|
three=3,
|
|
head=self.num_attention_heads_per_partition,
|
|
)
|
|
|
|
if rotary_pos_emb_cos is not None and rotary_pos_emb_sin is not None:
|
|
qk, v = qkv[:, :, :2], qkv[:, :, 2]
|
|
|
|
qk_reshaped = einops.rearrange(
|
|
qk, "b s two head head_dim -> (two b) s head head_dim", two=2
|
|
)
|
|
qk_reshaped = qk_reshaped.contiguous()
|
|
qk_rotated = self.apply_rotary_emb(
|
|
qk_reshaped,
|
|
rotary_pos_emb_cos,
|
|
rotary_pos_emb_sin,
|
|
)
|
|
qk_rotated = qk_rotated.view(
|
|
2,
|
|
batch_size,
|
|
seq_len,
|
|
self.num_attention_heads_per_partition,
|
|
self.hidden_size_per_attention_head,
|
|
)
|
|
q, k = qk_rotated.unbind(dim=0)
|
|
else:
|
|
q, k, v = qkv.unbind(dim=2)
|
|
|
|
context_layer = self.attn(
|
|
query=q,
|
|
key=k,
|
|
value=v,
|
|
cu_seqlens=cu_seqlens,
|
|
max_seqlen=max_seqlen,
|
|
sequence_lengths=sequence_lengths,
|
|
)
|
|
|
|
context_layer = einops.rearrange(
|
|
context_layer, "b s h d -> s b (h d)", b=batch_size
|
|
).contiguous()
|
|
|
|
output, _ = self.proj(context_layer)
|
|
return output
|
|
|
|
|
|
@support_torch_compile(
|
|
dynamic_arg_dims={
|
|
"x": 0,
|
|
"cu_seqlens": 0,
|
|
"sequence_lengths": 0,
|
|
"rotary_pos_emb_cos": 0,
|
|
"rotary_pos_emb_sin": 0,
|
|
},
|
|
enable_if=should_torch_compile_mm_encoder,
|
|
is_encoder=True,
|
|
)
|
|
class Qwen2_5_VisionBlock(nn.Module):
|
|
def __init__(
|
|
self,
|
|
dim: int,
|
|
num_heads: int,
|
|
mlp_hidden_dim: int,
|
|
act_fn: Callable[[torch.Tensor], torch.Tensor] = F.silu,
|
|
norm_layer: Callable[[int], nn.Module] | None = None,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__()
|
|
if norm_layer is None:
|
|
norm_layer = partial(nn.LayerNorm, eps=1e-6)
|
|
self.norm1 = norm_layer(dim)
|
|
self.norm2 = norm_layer(dim)
|
|
self.attn = Qwen2_5_VisionAttention(
|
|
embed_dim=dim,
|
|
num_heads=num_heads,
|
|
projection_size=dim,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.attn",
|
|
)
|
|
self.mlp = Qwen2_5_VisionMLP(
|
|
dim,
|
|
mlp_hidden_dim,
|
|
act_fn=act_fn,
|
|
bias=True,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.mlp",
|
|
)
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
cu_seqlens: torch.Tensor,
|
|
rotary_pos_emb_cos: torch.Tensor,
|
|
rotary_pos_emb_sin: torch.Tensor,
|
|
max_seqlen: torch.Tensor, # Only used for Flash Attention
|
|
# Only used for FlashInfer CuDNN backend.
|
|
sequence_lengths: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
x_attn = self.attn(
|
|
self.norm1(x),
|
|
cu_seqlens=cu_seqlens,
|
|
rotary_pos_emb_cos=rotary_pos_emb_cos,
|
|
rotary_pos_emb_sin=rotary_pos_emb_sin,
|
|
max_seqlen=max_seqlen,
|
|
sequence_lengths=sequence_lengths,
|
|
)
|
|
x_fused_norm, residual = self.norm2(x, residual=x_attn)
|
|
x = residual + self.mlp(x_fused_norm)
|
|
return x
|
|
|
|
|
|
@support_torch_compile(
|
|
dynamic_arg_dims={
|
|
"x": 0,
|
|
},
|
|
enable_if=should_torch_compile_mm_encoder,
|
|
is_encoder=True,
|
|
)
|
|
class Qwen2_5_VisionPatchEmbed(nn.Module):
|
|
def __init__(
|
|
self,
|
|
patch_size: int = 14,
|
|
temporal_patch_size: int = 2,
|
|
in_channels: int = 3,
|
|
hidden_size: int = 1152,
|
|
) -> None:
|
|
super().__init__()
|
|
self.patch_size = patch_size
|
|
self.temporal_patch_size = temporal_patch_size
|
|
self.hidden_size = hidden_size
|
|
|
|
kernel_size = (temporal_patch_size, patch_size, patch_size)
|
|
self.proj = Conv3dLayer(
|
|
in_channels,
|
|
hidden_size,
|
|
kernel_size=kernel_size,
|
|
stride=kernel_size,
|
|
bias=False,
|
|
)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
L, C = x.shape
|
|
x = x.view(L, -1, self.temporal_patch_size, self.patch_size, self.patch_size)
|
|
x = self.proj(x).view(L, self.hidden_size)
|
|
return x
|
|
|
|
|
|
@support_torch_compile(
|
|
dynamic_arg_dims={
|
|
"x": 0,
|
|
},
|
|
enable_if=should_torch_compile_mm_encoder,
|
|
is_encoder=True,
|
|
)
|
|
class Qwen2_5_VisionPatchMerger(nn.Module):
|
|
def __init__(
|
|
self,
|
|
d_model: int,
|
|
context_dim: int,
|
|
norm_layer: Callable[[int], nn.Module] | None = None,
|
|
spatial_merge_size: int = 2,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__()
|
|
use_data_parallel = is_vit_use_data_parallel()
|
|
self.hidden_size = context_dim * (spatial_merge_size**2)
|
|
if norm_layer is None:
|
|
norm_layer = partial(nn.LayerNorm, eps=1e-6)
|
|
self.ln_q = norm_layer(context_dim)
|
|
|
|
self.mlp = nn.Sequential(
|
|
ColumnParallelLinear(
|
|
self.hidden_size,
|
|
self.hidden_size,
|
|
bias=True,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.mlp.0",
|
|
return_bias=False,
|
|
disable_tp=use_data_parallel,
|
|
),
|
|
nn.GELU(),
|
|
RowParallelLinear(
|
|
self.hidden_size,
|
|
d_model,
|
|
bias=True,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.mlp.2",
|
|
return_bias=False,
|
|
disable_tp=use_data_parallel,
|
|
),
|
|
)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
x = self.ln_q(x)
|
|
x = x.view(-1, self.hidden_size)
|
|
out = self.mlp(x)
|
|
return out
|
|
|
|
|
|
class Qwen2_5_VisionTransformer(nn.Module):
|
|
hf_to_vllm_mapper = WeightsMapper(
|
|
orig_to_new_stacked={
|
|
".attn.q.": (".attn.qkv.", "q"),
|
|
".attn.k.": (".attn.qkv.", "k"),
|
|
".attn.v.": (".attn.qkv.", "v"),
|
|
".mlp.gate_proj.": (".mlp.gate_up_proj.", 0),
|
|
".mlp.up_proj.": (".mlp.gate_up_proj.", 1),
|
|
}
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
vision_config: Qwen2_5_VLVisionConfig,
|
|
norm_eps: float = 1e-6,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__()
|
|
|
|
patch_size = vision_config.patch_size
|
|
temporal_patch_size = vision_config.temporal_patch_size
|
|
in_channels = vision_config.in_channels
|
|
depth = vision_config.depth
|
|
self.hidden_size = vision_config.hidden_size
|
|
self.num_heads = vision_config.num_heads
|
|
self.out_hidden_size = vision_config.out_hidden_size
|
|
|
|
# args for get_window_index_thw
|
|
self.window_size = vision_config.window_size
|
|
self.patch_size = vision_config.patch_size
|
|
self.spatial_merge_size = vision_config.spatial_merge_size
|
|
self.fullatt_block_indexes = vision_config.fullatt_block_indexes
|
|
self.spatial_merge_unit = self.spatial_merge_size**2
|
|
use_data_parallel = is_vit_use_data_parallel()
|
|
self.tp_size = (
|
|
1
|
|
if use_data_parallel
|
|
else parallel_state.get_tensor_model_parallel_world_size()
|
|
)
|
|
self.patch_embed = Qwen2_5_VisionPatchEmbed(
|
|
patch_size=patch_size,
|
|
temporal_patch_size=temporal_patch_size,
|
|
in_channels=in_channels,
|
|
hidden_size=self.hidden_size,
|
|
)
|
|
|
|
norm_layer = partial(RMSNorm, eps=norm_eps)
|
|
head_dim = self.hidden_size // self.num_heads
|
|
# FP8 attention: Q/K/V become independent contiguous tensors after
|
|
# quantization, so FlashInfer cu_seqlens uses uniform stride.
|
|
self.fp8_padded_hidden_size = get_fp8_padded_hidden_size(
|
|
self.num_heads, head_dim
|
|
)
|
|
self.rotary_pos_emb = get_rope(
|
|
head_size=head_dim,
|
|
max_position=8192,
|
|
is_neox_style=True,
|
|
rope_parameters={"partial_rotary_factor": 0.5},
|
|
)
|
|
|
|
self.attn_backend = get_vit_attn_backend(
|
|
head_size=head_dim,
|
|
dtype=torch.get_default_dtype(),
|
|
)
|
|
|
|
self.blocks = nn.ModuleList(
|
|
[
|
|
Qwen2_5_VisionBlock(
|
|
dim=self.hidden_size,
|
|
num_heads=self.num_heads,
|
|
mlp_hidden_dim=vision_config.intermediate_size,
|
|
act_fn=get_act_and_mul_fn(
|
|
vision_config.hidden_act,
|
|
compile_native=not current_platform.is_rocm(),
|
|
),
|
|
norm_layer=norm_layer,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.blocks.{layer_idx}",
|
|
)
|
|
for layer_idx in range(depth)
|
|
]
|
|
)
|
|
|
|
self.merger = Qwen2_5_VisionPatchMerger(
|
|
d_model=vision_config.out_hidden_size,
|
|
context_dim=self.hidden_size,
|
|
norm_layer=norm_layer,
|
|
spatial_merge_size=self.spatial_merge_size,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.merger",
|
|
)
|
|
|
|
@property
|
|
def dtype(self) -> torch.dtype:
|
|
return self.patch_embed.proj.weight.dtype
|
|
|
|
@property
|
|
def device(self) -> torch.device:
|
|
return self.patch_embed.proj.weight.device
|
|
|
|
def rotary_pos_emb_thw(self, t, h, w):
|
|
hpos_ids = torch.arange(h).unsqueeze(1).expand(-1, w)
|
|
wpos_ids = torch.arange(w).unsqueeze(0).expand(h, -1)
|
|
hpos_ids = (
|
|
hpos_ids.reshape(
|
|
h // self.spatial_merge_size,
|
|
self.spatial_merge_size,
|
|
w // self.spatial_merge_size,
|
|
self.spatial_merge_size,
|
|
)
|
|
.permute(0, 2, 1, 3)
|
|
.flatten()
|
|
)
|
|
wpos_ids = (
|
|
wpos_ids.reshape(
|
|
h // self.spatial_merge_size,
|
|
self.spatial_merge_size,
|
|
w // self.spatial_merge_size,
|
|
self.spatial_merge_size,
|
|
)
|
|
.permute(0, 2, 1, 3)
|
|
.flatten()
|
|
)
|
|
pos_ids = torch.stack([hpos_ids, wpos_ids], dim=-1).repeat(t, 1)
|
|
max_size = max(h, w)
|
|
|
|
# Use pre-computed cos_sin_cache from RotaryEmbedding
|
|
cos, sin = self.rotary_pos_emb.get_cos_sin(max_size)
|
|
|
|
pos_ids = pos_ids.to(cos.device, non_blocking=True)
|
|
cos_combined = cos[pos_ids].flatten(1)
|
|
sin_combined = sin[pos_ids].flatten(1)
|
|
|
|
cos_combined = cos_combined.reshape(
|
|
cos_combined.shape[0] // self.spatial_merge_unit,
|
|
self.spatial_merge_unit,
|
|
-1,
|
|
)
|
|
sin_combined = sin_combined.reshape(
|
|
sin_combined.shape[0] // self.spatial_merge_unit,
|
|
self.spatial_merge_unit,
|
|
-1,
|
|
)
|
|
|
|
return cos_combined, sin_combined
|
|
|
|
def get_window_index_thw(self, grid_t, grid_h, grid_w):
|
|
vit_merger_window_size = (
|
|
self.window_size // self.spatial_merge_size // self.patch_size
|
|
)
|
|
|
|
llm_grid_h = grid_h // self.spatial_merge_size
|
|
llm_grid_w = grid_w // self.spatial_merge_size
|
|
index = torch.arange(grid_t * llm_grid_h * llm_grid_w).reshape(
|
|
grid_t, llm_grid_h, llm_grid_w
|
|
)
|
|
pad_h = vit_merger_window_size - llm_grid_h % vit_merger_window_size
|
|
pad_w = vit_merger_window_size - llm_grid_w % vit_merger_window_size
|
|
num_windows_h = (llm_grid_h + pad_h) // vit_merger_window_size
|
|
num_windows_w = (llm_grid_w + pad_w) // vit_merger_window_size
|
|
index_padded = F.pad(index, (0, pad_w, 0, pad_h), "constant", -100)
|
|
index_padded = index_padded.reshape(
|
|
grid_t,
|
|
num_windows_h,
|
|
vit_merger_window_size,
|
|
num_windows_w,
|
|
vit_merger_window_size,
|
|
)
|
|
index_padded = index_padded.permute(0, 1, 3, 2, 4).reshape(
|
|
grid_t,
|
|
num_windows_h * num_windows_w,
|
|
vit_merger_window_size,
|
|
vit_merger_window_size,
|
|
)
|
|
seqlens = (index_padded != -100).sum([2, 3]).reshape(-1)
|
|
index_padded = index_padded.reshape(-1)
|
|
index_new = index_padded[index_padded != -100]
|
|
cu_seqlens_tmp = seqlens.cumsum(0) * self.spatial_merge_unit
|
|
cu_seqlens_tmp = cu_seqlens_tmp.to(dtype=torch.int32)
|
|
cu_seqlens_tmp = torch.unique_consecutive(cu_seqlens_tmp)
|
|
|
|
return index_new, cu_seqlens_tmp
|
|
|
|
@lru_cache(maxsize=1024) # noqa: B019
|
|
def get_rope_by_thw(self, t, h, w):
|
|
window_index_thw, cu_seqlens_window_thw = self.get_window_index_thw(t, h, w)
|
|
cos_thw, sin_thw = self.rotary_pos_emb_thw(t, h, w)
|
|
|
|
window_index_thw_dev = window_index_thw.to(cos_thw.device, non_blocking=True)
|
|
cos_thw = cos_thw[window_index_thw_dev, :, :]
|
|
cos_thw = cos_thw.flatten(start_dim=0, end_dim=1)
|
|
sin_thw = sin_thw[window_index_thw_dev, :, :]
|
|
sin_thw = sin_thw.flatten(start_dim=0, end_dim=1)
|
|
|
|
cu_seqlens_thw = torch.repeat_interleave(
|
|
torch.tensor([h * w], dtype=torch.int32), t
|
|
)
|
|
return (
|
|
cos_thw,
|
|
sin_thw,
|
|
window_index_thw,
|
|
cu_seqlens_window_thw,
|
|
cu_seqlens_thw,
|
|
)
|
|
|
|
def compute_attn_mask_seqlen(
|
|
self,
|
|
cu_seqlens: torch.Tensor,
|
|
) -> torch.Tensor:
|
|
max_seqlen = torch.zeros([], device=cu_seqlens.device)
|
|
if self.attn_backend in {
|
|
AttentionBackendEnum.FLASH_ATTN,
|
|
AttentionBackendEnum.ROCM_AITER_FA,
|
|
AttentionBackendEnum.TRITON_ATTN,
|
|
}:
|
|
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
|
|
return max_seqlen
|
|
|
|
@staticmethod
|
|
def invert_permutation(perm: torch.Tensor) -> torch.Tensor:
|
|
# building the inverse permutation in O(n) time
|
|
inv = torch.empty_like(perm, pin_memory=PIN_MEMORY)
|
|
inv[perm] = torch.arange(perm.numel(), device=perm.device, dtype=perm.dtype)
|
|
return inv
|
|
|
|
def get_encoder_cudagraph_max_window_seqs(
|
|
self,
|
|
token_budget: int,
|
|
max_batch_size: int,
|
|
max_frames_per_batch: int,
|
|
) -> int:
|
|
# token_budget is an upper bound on the total number of merged vision
|
|
# tokens replayed by this encoder CUDA graph. cu_window_seqlens, however,
|
|
# is sized by the number of window-attention sequences (non-empty local
|
|
# windows), not by the number of tokens. Using max_num_batched_tokens as
|
|
# this sequence count can over-pad cu_window_seqlens and make FlashAttention
|
|
# launch thousands of empty CTAs during replay.
|
|
vit_merger_window_size = (
|
|
self.window_size // self.spatial_merge_size // self.patch_size
|
|
)
|
|
max_sequence_units = max(max_batch_size, max_frames_per_batch)
|
|
|
|
# Each local window covers vit_merger_window_size tokens along one merged
|
|
# spatial axis. The largest number of non-empty windows for a fixed token
|
|
# budget comes from a thin strip that advances along only one axis, so
|
|
# ceil(token_budget / window_side) is a safe geometry-driven bound. Multiple
|
|
# images or video frames can fragment that strip at item/frame boundaries,
|
|
# so add max_sequence_units to cover one extra partial window per sequence.
|
|
max_strip_windows = (
|
|
token_budget + vit_merger_window_size - 1
|
|
) // vit_merger_window_size
|
|
|
|
# A non-empty window must contain at least one merged vision token, so the
|
|
# number of window sequences can never exceed token_budget. This final
|
|
# clamp keeps the bound tight for tiny budgets while remaining safe.
|
|
return min(token_budget, max_sequence_units + max_strip_windows)
|
|
|
|
def prepare_encoder_metadata(
|
|
self,
|
|
grid_thw: list[list[int]],
|
|
*,
|
|
max_batch_size: int | None = None,
|
|
max_frames_per_batch: int | None = None,
|
|
max_window_seqs_per_batch: int | None = None,
|
|
max_seqlen_override: int | None = None,
|
|
max_seqlen_window_override: int | None = None,
|
|
device: torch.device | None = None,
|
|
) -> dict[str, torch.Tensor]:
|
|
"""Compute encoder metadata from grid_thw.
|
|
|
|
Shared by the eager forward path, CUDA graph capture, and
|
|
CUDA graph replay to avoid duplicated implementation.
|
|
|
|
Args:
|
|
grid_thw: Grid configurations as list of [t, h, w].
|
|
max_batch_size: If set, pad cu_seqlens to this size
|
|
(needed for CUDA graph capture/replay).
|
|
max_frames_per_batch: If set, overrides max_batch_size for
|
|
cu_seqlens padding. For video inputs each item contributes
|
|
T attention sequences (frames); this sizes the buffer to
|
|
the total frame budget so video replays never overflow.
|
|
max_window_seqs_per_batch: If set, pad cu_window_seqlens to this
|
|
number of window sequences. This keeps cu_window_seqlens shape
|
|
stable across capture/replay for CUDA graph safety.
|
|
max_seqlen_override: If set, use this value for max_seqlen
|
|
instead of computing from cu_seqlens (needed for CUDA
|
|
graph capture to cover worst-case replay scenarios).
|
|
max_seqlen_window_override: If set, use this value for
|
|
window-attention max_seqlen instead of computing from
|
|
cu_window_seqlens (needed for CUDA graph capture to
|
|
cover worst-case replay scenarios).
|
|
device: Device to place tensors on. Defaults to self.device.
|
|
"""
|
|
|
|
if device is None:
|
|
device = self.device
|
|
metadata: dict[str, torch.Tensor] = {}
|
|
|
|
# patchify
|
|
rotary_pos_emb_cos = []
|
|
rotary_pos_emb_sin = []
|
|
window_index: list = []
|
|
cu_window_seqlens: list = [torch.tensor([0], dtype=torch.int32)]
|
|
cu_seqlens: list = []
|
|
|
|
window_index_id = 0
|
|
cu_window_seqlens_last = 0
|
|
for t, h, w in grid_thw:
|
|
t, h, w = int(t), int(h), int(w)
|
|
llm_h = h // self.spatial_merge_size
|
|
llm_w = w // self.spatial_merge_size
|
|
|
|
(
|
|
cos_thw,
|
|
sin_thw,
|
|
window_index_thw,
|
|
cu_seqlens_window_thw,
|
|
cu_seqlens_thw,
|
|
) = self.get_rope_by_thw(t, h, w)
|
|
|
|
window_index.append(window_index_thw + window_index_id)
|
|
window_index_id += t * llm_h * llm_w
|
|
|
|
cu_seqlens_window_thw = cu_seqlens_window_thw + cu_window_seqlens_last
|
|
cu_window_seqlens_last = cu_seqlens_window_thw[-1]
|
|
cu_window_seqlens.append(cu_seqlens_window_thw)
|
|
|
|
rotary_pos_emb_cos.append(cos_thw)
|
|
rotary_pos_emb_sin.append(sin_thw)
|
|
|
|
cu_seqlens.append(cu_seqlens_thw)
|
|
|
|
rotary_pos_emb_cos = torch.cat(rotary_pos_emb_cos)
|
|
rotary_pos_emb_sin = torch.cat(rotary_pos_emb_sin)
|
|
window_index = torch.cat(window_index)
|
|
# compute reverse indices
|
|
reverse_indices = self.invert_permutation(window_index)
|
|
cu_window_seqlens = torch.cat(cu_window_seqlens)
|
|
cu_window_seqlens = torch.unique_consecutive(cu_window_seqlens)
|
|
cu_seqlens = torch.cat(cu_seqlens)
|
|
cu_seqlens = torch.cumsum(cu_seqlens, dim=0, dtype=torch.int32)
|
|
cu_seqlens = F.pad(cu_seqlens, (1, 0), "constant", 0)
|
|
|
|
# Pad cu_seqlens to the required number of sequences.
|
|
# For videos each item contributes T frames = T attention sequences,
|
|
# so the total can exceed max_batch_size. max_frames_per_batch
|
|
# overrides the pad target when set.
|
|
pad_to = (
|
|
max_frames_per_batch if max_frames_per_batch is not None else max_batch_size
|
|
)
|
|
if pad_to is not None:
|
|
num_seqs = len(cu_seqlens) - 1
|
|
if num_seqs < pad_to:
|
|
cu_seqlens = torch.cat(
|
|
(
|
|
cu_seqlens,
|
|
torch.full(
|
|
(pad_to - num_seqs,),
|
|
cu_seqlens[-1],
|
|
dtype=cu_seqlens.dtype,
|
|
device=cu_seqlens.device,
|
|
),
|
|
)
|
|
)
|
|
|
|
# Pad cu_window_seqlens to a stable number of window sequences.
|
|
# Like cu_seqlens, we repeat the last cumulative offset so padded
|
|
# entries represent empty sequences.
|
|
if max_window_seqs_per_batch is not None:
|
|
num_window_seqs = len(cu_window_seqlens) - 1
|
|
if num_window_seqs < max_window_seqs_per_batch:
|
|
cu_window_seqlens = torch.cat(
|
|
(
|
|
cu_window_seqlens,
|
|
torch.full(
|
|
(max_window_seqs_per_batch - num_window_seqs,),
|
|
cu_window_seqlens[-1],
|
|
dtype=cu_window_seqlens.dtype,
|
|
device=cu_window_seqlens.device,
|
|
),
|
|
)
|
|
)
|
|
|
|
cu_seqlens_np = cu_seqlens.cpu().numpy()
|
|
cu_window_seqlens_np = cu_window_seqlens.cpu().numpy()
|
|
|
|
# FlashInfer needs the real per-sequence lengths in addition to
|
|
# cu_seqlens. For other backends this returns None and is ignored.
|
|
sequence_lengths_full = MMEncoderAttention.maybe_compute_seq_lens(
|
|
self.attn_backend, cu_seqlens_np, device
|
|
)
|
|
sequence_lengths_window = MMEncoderAttention.maybe_compute_seq_lens(
|
|
self.attn_backend, cu_window_seqlens_np, device
|
|
)
|
|
|
|
# Pre-compute max sequence lengths for window/full attention. FlashInfer
|
|
# buckets this value for cuDNN graph reuse; other backends keep the exact
|
|
# maximum. Keep the scalar on CPU because attention wrappers call .item().
|
|
if max_seqlen_override is None:
|
|
max_seqlen_full_val = MMEncoderAttention.compute_max_seqlen(
|
|
self.attn_backend, cu_seqlens_np
|
|
)
|
|
else:
|
|
max_seqlen_full_val = max_seqlen_override
|
|
max_seqlen_full = torch.tensor(max_seqlen_full_val, dtype=torch.int32)
|
|
if max_seqlen_window_override is None:
|
|
max_seqlen_window_val = MMEncoderAttention.compute_max_seqlen(
|
|
self.attn_backend, cu_window_seqlens_np
|
|
)
|
|
else:
|
|
max_seqlen_window_val = max_seqlen_window_override
|
|
max_seqlen_window = torch.tensor(max_seqlen_window_val, dtype=torch.int32)
|
|
|
|
# FlashInfer uses backend-specific cu_seqlens offsets into the flattened
|
|
# Q/K/O and V buffers. Other backends receive the original cumulative
|
|
# token offsets unchanged.
|
|
cu_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
|
|
self.attn_backend,
|
|
cu_seqlens_np,
|
|
self.hidden_size,
|
|
self.tp_size,
|
|
device,
|
|
fp8_padded_hidden_size=self.fp8_padded_hidden_size,
|
|
)
|
|
cu_window_seqlens = MMEncoderAttention.maybe_recompute_cu_seqlens(
|
|
self.attn_backend,
|
|
cu_window_seqlens_np,
|
|
self.hidden_size,
|
|
self.tp_size,
|
|
device,
|
|
fp8_padded_hidden_size=self.fp8_padded_hidden_size,
|
|
)
|
|
rotary_pos_emb_cos = rotary_pos_emb_cos.to(device=device, non_blocking=True)
|
|
rotary_pos_emb_sin = rotary_pos_emb_sin.to(device=device, non_blocking=True)
|
|
window_index = window_index.to(device=device, non_blocking=True)
|
|
reverse_indices = reverse_indices.to(device=device, non_blocking=True)
|
|
|
|
metadata["rotary_pos_emb_cos"] = rotary_pos_emb_cos
|
|
metadata["rotary_pos_emb_sin"] = rotary_pos_emb_sin
|
|
metadata["window_index"] = window_index
|
|
metadata["reverse_indices"] = reverse_indices
|
|
metadata["cu_seqlens"] = cu_seqlens
|
|
metadata["cu_window_seqlens"] = cu_window_seqlens
|
|
metadata["max_seqlen_full"] = max_seqlen_full
|
|
metadata["max_seqlen_window"] = max_seqlen_window
|
|
metadata["sequence_lengths_full"] = sequence_lengths_full
|
|
metadata["sequence_lengths_window"] = sequence_lengths_window
|
|
|
|
return metadata
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
grid_thw: list[list[int]] | None,
|
|
*,
|
|
encoder_metadata: dict[str, torch.Tensor] | None = None,
|
|
) -> torch.Tensor:
|
|
hidden_states = x.to(device=self.device, dtype=self.dtype)
|
|
hidden_states = self.patch_embed(hidden_states)
|
|
|
|
seq_len = hidden_states.shape[0]
|
|
if encoder_metadata is None:
|
|
encoder_metadata = self.prepare_encoder_metadata(grid_thw)
|
|
|
|
rotary_pos_emb_cos = encoder_metadata["rotary_pos_emb_cos"]
|
|
rotary_pos_emb_sin = encoder_metadata["rotary_pos_emb_sin"]
|
|
window_index = encoder_metadata["window_index"]
|
|
reverse_indices = encoder_metadata["reverse_indices"]
|
|
cu_seqlens = encoder_metadata["cu_seqlens"]
|
|
cu_window_seqlens = encoder_metadata["cu_window_seqlens"]
|
|
max_seqlen_full = encoder_metadata["max_seqlen_full"]
|
|
max_seqlen_window = encoder_metadata["max_seqlen_window"]
|
|
sequence_lengths_full = encoder_metadata.get("sequence_lengths_full")
|
|
sequence_lengths_window = encoder_metadata.get("sequence_lengths_window")
|
|
|
|
hidden_states = hidden_states.reshape(
|
|
seq_len // self.spatial_merge_unit, self.spatial_merge_unit, -1
|
|
)
|
|
hidden_states = hidden_states[window_index, :, :]
|
|
hidden_states = hidden_states.reshape(seq_len, -1)
|
|
|
|
hidden_states = hidden_states.unsqueeze(1)
|
|
|
|
for layer_num, blk in enumerate(self.blocks):
|
|
if layer_num in self.fullatt_block_indexes:
|
|
cu_seqlens_now = cu_seqlens
|
|
max_seqlen_now = max_seqlen_full
|
|
sequence_lengths_now = sequence_lengths_full
|
|
else:
|
|
cu_seqlens_now = cu_window_seqlens
|
|
max_seqlen_now = max_seqlen_window
|
|
sequence_lengths_now = sequence_lengths_window
|
|
|
|
hidden_states = blk(
|
|
hidden_states,
|
|
cu_seqlens=cu_seqlens_now,
|
|
rotary_pos_emb_cos=rotary_pos_emb_cos,
|
|
rotary_pos_emb_sin=rotary_pos_emb_sin,
|
|
max_seqlen=max_seqlen_now,
|
|
sequence_lengths=sequence_lengths_now,
|
|
)
|
|
|
|
# For Qwen2.5-VL-3B, float16 will overflow at last block
|
|
# for long visual tokens sequences.
|
|
if hidden_states.dtype == torch.float16:
|
|
hidden_states = cast_overflow_tensors(hidden_states)
|
|
|
|
# adapter
|
|
hidden_states = self.merger(hidden_states)
|
|
hidden_states = hidden_states[reverse_indices, :]
|
|
return hidden_states
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
loader = AutoWeightsLoader(self)
|
|
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
|
|
|
|
|
class Qwen2_5_VLProcessingInfo(Qwen2VLProcessingInfo):
|
|
def get_hf_config(self):
|
|
return self.ctx.get_hf_config(Qwen2_5_VLConfig)
|
|
|
|
def get_hf_processor(self, **kwargs: object) -> Qwen2_5_VLProcessor:
|
|
return self.ctx.get_hf_processor(
|
|
Qwen2_5_VLProcessor,
|
|
use_fast=kwargs.pop("use_fast", True),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
class Qwen2_5_VLMultiModalProcessor(Qwen2VLMultiModalProcessor):
|
|
def _get_mm_fields_config(
|
|
self,
|
|
hf_inputs: BatchFeature,
|
|
hf_processor_mm_kwargs: Mapping[str, object],
|
|
) -> Mapping[str, MultiModalFieldConfig]:
|
|
return dict(
|
|
**super()._get_mm_fields_config(hf_inputs, hf_processor_mm_kwargs),
|
|
second_per_grid_ts=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
|
|
)
|
|
|
|
def _call_hf_processor(
|
|
self,
|
|
prompt: str,
|
|
mm_data: Mapping[str, object],
|
|
mm_kwargs: Mapping[str, object],
|
|
tok_kwargs: Mapping[str, object],
|
|
) -> BatchFeature:
|
|
# Override to use the text path instead of token path to use the
|
|
# video-specific logic in processing_qwen2_5_vl.py
|
|
return super()._call_hf_processor(prompt, mm_data, mm_kwargs, tok_kwargs)
|
|
|
|
def _get_prompt_updates(
|
|
self,
|
|
mm_items: MultiModalDataItems,
|
|
hf_processor_mm_kwargs: Mapping[str, Any],
|
|
out_mm_kwargs: MultiModalKwargsItems,
|
|
) -> Sequence[PromptUpdate]:
|
|
hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
|
|
image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
|
|
tokenizer = self.info.get_tokenizer()
|
|
vocab = tokenizer.get_vocab()
|
|
|
|
placeholder = {
|
|
"image": vocab[hf_processor.image_token],
|
|
"video": vocab[hf_processor.video_token],
|
|
}
|
|
|
|
merge_length = image_processor.merge_size**2
|
|
|
|
def get_replacement_qwen2vl(item_idx: int, modality: str):
|
|
out_item = out_mm_kwargs[modality][item_idx]
|
|
grid_thw = out_item[f"{modality}_grid_thw"].data
|
|
assert isinstance(grid_thw, torch.Tensor)
|
|
|
|
num_tokens = int(grid_thw.prod()) // merge_length
|
|
|
|
# EVS-specific code
|
|
video_pruning_rate = self.info.ctx.get_mm_config().video_pruning_rate
|
|
if (
|
|
modality == "video"
|
|
and video_pruning_rate is not None
|
|
and video_pruning_rate > 0.0
|
|
):
|
|
T, H, W = map(int, grid_thw)
|
|
tokens_per_frame = (H // image_processor.merge_size) * (
|
|
W // image_processor.merge_size
|
|
)
|
|
num_tokens = compute_retained_tokens_count(
|
|
tokens_per_frame,
|
|
T,
|
|
video_pruning_rate,
|
|
)
|
|
# End of EVS-specific code
|
|
|
|
return [placeholder[modality]] * num_tokens
|
|
|
|
return [
|
|
PromptReplacement(
|
|
modality=modality,
|
|
target=[placeholder[modality]],
|
|
replacement=partial(get_replacement_qwen2vl, modality=modality),
|
|
)
|
|
for modality in ("image", "video")
|
|
]
|
|
|
|
|
|
@MULTIMODAL_REGISTRY.register_processor(
|
|
Qwen2_5_VLMultiModalProcessor,
|
|
info=Qwen2_5_VLProcessingInfo,
|
|
dummy_inputs=Qwen2_5_VLDummyInputsBuilder,
|
|
)
|
|
class Qwen2_5_VLForConditionalGeneration(
|
|
nn.Module,
|
|
SupportsMultiModal,
|
|
SupportsEncoderCudaGraph,
|
|
SupportsLoRA,
|
|
SupportsPP,
|
|
SupportsQuant,
|
|
SupportsEagle,
|
|
SupportsEagle3,
|
|
SupportsMultiModalPruning,
|
|
SupportsMRoPE,
|
|
):
|
|
packed_modules_mapping = {
|
|
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
|
|
"gate_up_proj": ["gate_proj", "up_proj"],
|
|
"qkv": ["qkv"], # For vision tower's already-packed QKV
|
|
}
|
|
|
|
# To ensure correct weight loading and mapping.
|
|
hf_to_vllm_mapper = WeightsMapper(
|
|
orig_to_new_prefix={
|
|
# mapping for new names in checkpoint saved after transformers v4.52
|
|
"model.language_model.": "language_model.model.",
|
|
"model.visual.": "visual.",
|
|
# mapping for original checkpoint
|
|
"lm_head.": "language_model.lm_head.",
|
|
"model.": "language_model.model.",
|
|
}
|
|
)
|
|
|
|
supports_encoder_tp_data = True
|
|
|
|
def iter_mm_grid_thw(
|
|
self, mm_features: list[MultiModalFeatureSpec]
|
|
) -> Iterator[tuple[int, int, int, int, float]]:
|
|
"""
|
|
Iterate over multimodal features and yield grid information.
|
|
|
|
Args:
|
|
mm_features: List of multimodal feature specifications
|
|
|
|
Yields:
|
|
Tuple of (offset, grid_t, grid_h, grid_w, t_factor) for each frame/image
|
|
"""
|
|
spatial_merge_size = self.config.vision_config.spatial_merge_size
|
|
tokens_per_second = getattr(self.config.vision_config, "tokens_per_second", 1.0)
|
|
for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
|
|
offset = mm_feature.mm_position.offset
|
|
if mm_feature.modality == "image":
|
|
t, h, w = mm_feature.data["image_grid_thw"].data.tolist()
|
|
assert t == 1, f"Image must have 1 frame, got {t}"
|
|
yield offset, 1, h // spatial_merge_size, w // spatial_merge_size, 1.0
|
|
elif mm_feature.modality == "video":
|
|
t, h, w = mm_feature.data["video_grid_thw"].data.tolist()
|
|
second_per_grid_ts = 1.0
|
|
if mm_feature.data.get("second_per_grid_ts", None):
|
|
second_per_grid_ts = mm_feature.data[
|
|
"second_per_grid_ts"
|
|
].data.item()
|
|
t_factor = second_per_grid_ts * tokens_per_second
|
|
yield (
|
|
offset,
|
|
t,
|
|
h // spatial_merge_size,
|
|
w // spatial_merge_size,
|
|
t_factor,
|
|
)
|
|
else:
|
|
raise ValueError(f"Unsupported modality: {mm_feature.modality}")
|
|
|
|
def get_mrope_input_positions(
|
|
self,
|
|
input_tokens: list[int],
|
|
mm_features: list[MultiModalFeatureSpec],
|
|
) -> tuple[torch.Tensor, int]:
|
|
llm_pos_ids_list: list = []
|
|
st = 0
|
|
|
|
for (
|
|
offset,
|
|
llm_grid_t,
|
|
llm_grid_h,
|
|
llm_grid_w,
|
|
t_factor,
|
|
) in self.iter_mm_grid_thw(mm_features):
|
|
text_len = offset - st
|
|
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
|
|
llm_pos_ids_list.append(
|
|
np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
|
|
)
|
|
|
|
grid_indices = np.indices((llm_grid_t, llm_grid_h, llm_grid_w))
|
|
if t_factor != 1.0:
|
|
grid_indices[0] = (grid_indices[0] * t_factor).astype(np.int64)
|
|
llm_pos_ids_list.append(grid_indices.reshape(3, -1) + text_len + st_idx)
|
|
st = offset + llm_grid_t * llm_grid_h * llm_grid_w
|
|
|
|
if st < len(input_tokens):
|
|
st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
|
|
text_len = len(input_tokens) - st
|
|
llm_pos_ids_list.append(
|
|
np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
|
|
)
|
|
|
|
llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
|
|
mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item()
|
|
|
|
return torch.from_numpy(llm_positions), mrope_position_delta
|
|
|
|
@classmethod
|
|
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
|
if modality.startswith("image"):
|
|
return "<|vision_start|><|image_pad|><|vision_end|>"
|
|
if modality.startswith("video"):
|
|
return "<|vision_start|><|video_pad|><|vision_end|>"
|
|
|
|
raise ValueError("Only image or video modality is supported")
|
|
|
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
|
super().__init__()
|
|
config: Qwen2_5_VLConfig = vllm_config.model_config.hf_config
|
|
multimodal_config = vllm_config.model_config.multimodal_config
|
|
|
|
self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
|
|
self.config = config
|
|
self.model_config = vllm_config.model_config
|
|
self.vllm_config = vllm_config
|
|
self.multimodal_config = multimodal_config
|
|
self.video_pruning_rate = multimodal_config.video_pruning_rate
|
|
self.is_multimodal_pruning_enabled = (
|
|
multimodal_config.is_multimodal_pruning_enabled()
|
|
)
|
|
|
|
with self._mark_tower_model(vllm_config, {"image", "video"}):
|
|
self.visual = Qwen2_5_VisionTransformer(
|
|
vision_config=config.vision_config,
|
|
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
|
|
quant_config=self.quant_config,
|
|
prefix=maybe_prefix(prefix, "visual"),
|
|
)
|
|
|
|
with self._mark_language_model(vllm_config):
|
|
self.language_model = init_vllm_registered_model(
|
|
vllm_config=vllm_config,
|
|
prefix=maybe_prefix(prefix, "language_model"),
|
|
architectures=["Qwen2ForCausalLM"],
|
|
)
|
|
|
|
self.make_empty_intermediate_tensors = (
|
|
self.language_model.make_empty_intermediate_tensors
|
|
)
|
|
|
|
def _parse_and_validate_image_input(
|
|
self, **kwargs: object
|
|
) -> Qwen2_5_VLImageInputs | None:
|
|
pixel_values = kwargs.pop("pixel_values", None)
|
|
image_embeds = kwargs.pop("image_embeds", None)
|
|
image_grid_thw = kwargs.pop("image_grid_thw", None)
|
|
|
|
if pixel_values is None and image_embeds is None:
|
|
return None
|
|
|
|
if pixel_values is not None:
|
|
return Qwen2_5_VLImagePixelInputs(
|
|
type="pixel_values",
|
|
pixel_values=pixel_values,
|
|
image_grid_thw=image_grid_thw,
|
|
)
|
|
|
|
if image_embeds is not None:
|
|
return Qwen2_5_VLImageEmbeddingInputs(
|
|
type="image_embeds",
|
|
image_embeds=image_embeds,
|
|
image_grid_thw=image_grid_thw,
|
|
)
|
|
|
|
def _parse_and_validate_video_input(
|
|
self, **kwargs: object
|
|
) -> Qwen2_5_VLVideoInputs | None:
|
|
pixel_values_videos = kwargs.pop("pixel_values_videos", None)
|
|
video_embeds = kwargs.pop("video_embeds", None)
|
|
video_grid_thw = kwargs.pop("video_grid_thw", None)
|
|
second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
|
|
|
|
if pixel_values_videos is None and video_embeds is None:
|
|
return None
|
|
|
|
if pixel_values_videos is not None:
|
|
return Qwen2_5_VLVideoPixelInputs(
|
|
type="pixel_values_videos",
|
|
pixel_values_videos=pixel_values_videos,
|
|
video_grid_thw=video_grid_thw,
|
|
second_per_grid_ts=second_per_grid_ts,
|
|
)
|
|
|
|
if video_embeds is not None:
|
|
return Qwen2_5_VLVideoEmbeddingInputs(
|
|
type="video_embeds",
|
|
video_embeds=video_embeds,
|
|
video_grid_thw=video_grid_thw,
|
|
second_per_grid_ts=second_per_grid_ts,
|
|
)
|
|
|
|
def _process_image_input(
|
|
self, image_input: Qwen2_5_VLImageInputs
|
|
) -> tuple[torch.Tensor, ...]:
|
|
grid_thw = image_input["image_grid_thw"]
|
|
assert grid_thw.ndim == 2
|
|
grid_thw_list = grid_thw.tolist()
|
|
|
|
if image_input["type"] == "image_embeds":
|
|
image_embeds = image_input["image_embeds"].type(self.visual.dtype)
|
|
else:
|
|
pixel_values = image_input["pixel_values"]
|
|
if self.use_data_parallel:
|
|
return run_dp_sharded_mrope_vision_model(
|
|
self.visual, pixel_values, grid_thw_list, rope_type="rope_3d"
|
|
)
|
|
else:
|
|
image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
|
|
|
|
# Split concatenated embeddings for each image item.
|
|
merge_size = self.visual.spatial_merge_size
|
|
sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
|
|
return image_embeds.split(sizes)
|
|
|
|
def _postprocess_image_embeds_evs(
|
|
self,
|
|
image_embeds_split: tuple[torch.Tensor, ...],
|
|
image_input: Qwen2_5_VLImageInputs,
|
|
) -> tuple[torch.Tensor, ...]:
|
|
"""
|
|
Append mrope positions for each for images.
|
|
This is necessary to recover correct mrope
|
|
positions after video pruning
|
|
|
|
Args:
|
|
image_embeds_split: Tuple of image embeddings for
|
|
each image item.
|
|
image_input: Image input data.
|
|
|
|
Returns:
|
|
Tuple of image embeddings for each image item.
|
|
Resulting embeddings will have extra 4 channels for
|
|
computed mrope positions.
|
|
"""
|
|
merge_size = self.visual.spatial_merge_size
|
|
grid_thw = image_input["image_grid_thw"]
|
|
grid_thw_list = grid_thw.tolist()
|
|
image_embeds_out = []
|
|
for emb, size in zip(image_embeds_split, grid_thw_list):
|
|
positions = compute_mrope_for_media(size, merge_size).to(
|
|
emb.device, non_blocking=True
|
|
)
|
|
emb = torch.cat([emb, positions], dim=1)
|
|
image_embeds_out.append(emb)
|
|
image_embeds_split = image_embeds_out
|
|
return tuple(image_embeds_split)
|
|
|
|
def _process_video_input(
|
|
self, video_input: Qwen2_5_VLVideoInputs
|
|
) -> tuple[torch.Tensor, ...]:
|
|
grid_thw = video_input["video_grid_thw"]
|
|
assert grid_thw.ndim == 2
|
|
grid_thw_list = grid_thw.tolist()
|
|
|
|
if video_input["type"] == "video_embeds":
|
|
video_embeds = video_input["video_embeds"].type(self.visual.dtype)
|
|
else:
|
|
pixel_values_videos = video_input["pixel_values_videos"]
|
|
if self.use_data_parallel:
|
|
return run_dp_sharded_mrope_vision_model(
|
|
self.visual,
|
|
pixel_values_videos,
|
|
grid_thw_list,
|
|
rope_type="rope_3d",
|
|
)
|
|
else:
|
|
video_embeds = self.visual(pixel_values_videos, grid_thw=grid_thw_list)
|
|
|
|
# Split concatenated embeddings for each video item.
|
|
merge_size = self.visual.spatial_merge_size
|
|
sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
|
|
return video_embeds.split(sizes)
|
|
|
|
def _postprocess_video_embeds_evs(
|
|
self,
|
|
video_embeds_split: tuple[torch.Tensor, ...],
|
|
video_input: Qwen2_5_VLVideoInputs,
|
|
) -> tuple[torch.Tensor, ...]:
|
|
"""
|
|
Prunes video embeddings via Efficient Video Sampling (EVS)
|
|
and then appends mrope positions for each retained embeddings
|
|
|
|
Args:
|
|
video_embeds_split: Tuple of video embeddings for each video item.
|
|
video_input: Video input data.
|
|
|
|
Returns:
|
|
Tuple of video embeddings for each video item.
|
|
Resulting embeddings will have extra 4 channels for
|
|
computed mrope positions.
|
|
"""
|
|
grid_thw = video_input["video_grid_thw"]
|
|
assert grid_thw.ndim == 2
|
|
grid_thw_list = grid_thw.tolist()
|
|
merge_size = self.visual.spatial_merge_size
|
|
|
|
# Cast to long to match the original code
|
|
# https://github.com/huggingface/transformers/blob/41980ce93e775f6c88500c51c8db7946fc6a2add/src/transformers/models/qwen2_5_vl/modular_qwen2_5_vl.py#L491 # noqa
|
|
second_per_grid_ts = video_input.get("second_per_grid_ts")
|
|
if second_per_grid_ts is None:
|
|
raise ValueError(
|
|
"second_per_grid_ts is required when video_pruning_rate > 0 "
|
|
"is enabled for video inputs, including the video_embeds path."
|
|
)
|
|
second_per_grid_ts = second_per_grid_ts.long()
|
|
tokens_per_second = self.config.vision_config.tokens_per_second
|
|
|
|
video_embeds_out = []
|
|
for emb, size, video_second_per_grid_t in zip(
|
|
video_embeds_split, grid_thw_list, second_per_grid_ts
|
|
):
|
|
# For each video, we compute retention mask using EVS
|
|
retention_mask = compute_retention_mask(
|
|
emb,
|
|
size,
|
|
spatial_merge_size=self.visual.spatial_merge_size,
|
|
q=self.video_pruning_rate,
|
|
)
|
|
positions = compute_mrope_for_media(
|
|
size,
|
|
merge_size,
|
|
tokens_per_second=tokens_per_second,
|
|
video_second_per_grid=video_second_per_grid_t.item(),
|
|
).to(emb.device, non_blocking=True)
|
|
|
|
emb = emb[retention_mask]
|
|
positions = positions[retention_mask]
|
|
emb = torch.cat([emb, positions], dim=1)
|
|
video_embeds_out.append(emb)
|
|
return tuple(video_embeds_out)
|
|
|
|
def recompute_mrope_positions(
|
|
self,
|
|
input_ids: list[int] | torch.Tensor,
|
|
multimodal_embeddings: Sequence[torch.Tensor],
|
|
mrope_positions: torch.LongTensor,
|
|
num_computed_tokens: int,
|
|
) -> tuple[Sequence[torch.Tensor], torch.Tensor, int]:
|
|
"""
|
|
Update part of input mrope positions (starting with
|
|
num_computed_tokens index). Original mrope_positions are computed
|
|
for unpruned sequence and becomes incorrect once pruning occurs,
|
|
so once we prune media tokens we should reflect this in the
|
|
mrope_positions before we feed it to LLM.
|
|
|
|
Args:
|
|
input_ids: (N,) All input tokens of the prompt (Containing
|
|
entire sequence).
|
|
multimodal_embeddings: Tuple of multimodal embeddings.
|
|
mrope_positions: Existing mrope positions (3, N) for entire
|
|
sequence
|
|
num_computed_tokens: A number of computed tokens so far.
|
|
|
|
Returns:
|
|
Tuple of (multimodal_embeddings, mrope_positions,
|
|
mrope_position_delta).
|
|
"""
|
|
image_token_id = self.config.image_token_id
|
|
video_token_id = self.config.video_token_id
|
|
vision_start_token_id = self.config.vision_start_token_id
|
|
|
|
# Device
|
|
device = (
|
|
multimodal_embeddings[0].device
|
|
if len(multimodal_embeddings)
|
|
else mrope_positions.device
|
|
)
|
|
|
|
# Tensors. input_ids may already be a (device-side) tensor.
|
|
if isinstance(input_ids, torch.Tensor):
|
|
assert input_ids.device == device
|
|
input_ids_t = input_ids.to(torch.long)
|
|
else:
|
|
input_ids_t = async_tensor_h2d(input_ids, dtype=torch.long, device=device)
|
|
|
|
mm_embeddings_out = [mm[:, :-4] for mm in multimodal_embeddings]
|
|
mm_embeddings_pos = [
|
|
mm[:, -4:].permute(1, 0).long() for mm in multimodal_embeddings
|
|
]
|
|
|
|
positions, mrope_positions_delta = recompute_mrope_positions(
|
|
input_ids_t,
|
|
mm_embeddings_pos,
|
|
mrope_positions,
|
|
num_computed_tokens,
|
|
vision_start_token_id,
|
|
image_token_id,
|
|
video_token_id,
|
|
)
|
|
|
|
return mm_embeddings_out, positions, mrope_positions_delta
|
|
|
|
def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
|
|
mm_input_by_modality = {}
|
|
|
|
# Preserve the order of modalities if there are multiple of them
|
|
# from the order of kwargs.
|
|
for input_key in kwargs:
|
|
if (
|
|
input_key in ("pixel_values", "image_embeds")
|
|
and "image" not in mm_input_by_modality
|
|
):
|
|
mm_input_by_modality["image"] = self._parse_and_validate_image_input(
|
|
**kwargs
|
|
)
|
|
if (
|
|
input_key in ("pixel_values_videos", "video_embeds")
|
|
and "video" not in mm_input_by_modality
|
|
):
|
|
mm_input_by_modality["video"] = self._parse_and_validate_video_input(
|
|
**kwargs
|
|
)
|
|
return mm_input_by_modality
|
|
|
|
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
|
|
mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
|
|
if not mm_input_by_modality:
|
|
return []
|
|
|
|
# The result multimodal_embeddings is tuple of tensors, with each
|
|
# tensor correspoending to a multimodal data item (image or video).
|
|
multimodal_embeddings: tuple[torch.Tensor, ...] = ()
|
|
|
|
# NOTE: It is important to iterate over the keys in this dictionary
|
|
# to preserve the order of the modalities.
|
|
for modality in mm_input_by_modality:
|
|
multimodal_input = mm_input_by_modality[modality]
|
|
if modality == "image":
|
|
image_embeddings = self._process_image_input(multimodal_input)
|
|
if self.is_multimodal_pruning_enabled:
|
|
image_embeddings = self._postprocess_image_embeds_evs(
|
|
image_embeddings, multimodal_input
|
|
)
|
|
multimodal_embeddings += tuple(image_embeddings)
|
|
if modality == "video":
|
|
video_embeddings = self._process_video_input(multimodal_input)
|
|
if self.is_multimodal_pruning_enabled:
|
|
video_embeddings = self._postprocess_video_embeds_evs(
|
|
video_embeddings, multimodal_input
|
|
)
|
|
multimodal_embeddings += tuple(video_embeddings)
|
|
return multimodal_embeddings
|
|
|
|
# -- SupportsEncoderCudaGraph protocol methods --
|
|
|
|
def get_encoder_cudagraph_config(self):
|
|
from vllm.v1.worker.encoder_cudagraph_defs import (
|
|
EncoderCudaGraphConfig,
|
|
)
|
|
|
|
# NOTE: With EVS pruning enabled, multimodal embeddings are post-processed
|
|
# (append positions for image and prune+append positions for video) in
|
|
# embed_multimodal(). The encoder CUDA graph path bypasses that postprocess
|
|
# hook, so disable CUDA graph for all modalities to avoid inconsistent
|
|
# embedding formats between eager and cudagraph paths.
|
|
modalities = [] if self.is_multimodal_pruning_enabled else ["image", "video"]
|
|
|
|
max_frames = self.get_max_frames_per_video() if "video" in modalities else 1
|
|
cu_seqlens_padding = (
|
|
_pad_flashinfer_cu_seqlens_buffer
|
|
if self.visual.attn_backend == AttentionBackendEnum.FLASHINFER
|
|
else _pad_cumulative_seqlens_buffer
|
|
)
|
|
return EncoderCudaGraphConfig(
|
|
modalities=modalities,
|
|
buffer_keys=[
|
|
"pixel_values",
|
|
"rotary_pos_emb_cos",
|
|
"rotary_pos_emb_sin",
|
|
"window_index",
|
|
"reverse_indices",
|
|
"cu_seqlens",
|
|
"cu_window_seqlens",
|
|
"max_seqlen_full",
|
|
"max_seqlen_window",
|
|
"sequence_lengths_full",
|
|
"sequence_lengths_window",
|
|
],
|
|
padding_logics={
|
|
"cu_seqlens": cu_seqlens_padding,
|
|
"cu_window_seqlens": cu_seqlens_padding,
|
|
},
|
|
out_hidden_size=self.visual.out_hidden_size,
|
|
max_frames_per_video=max_frames,
|
|
)
|
|
|
|
def get_input_modality(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
) -> str:
|
|
if "image_grid_thw" in mm_kwargs:
|
|
return "image"
|
|
elif "video_grid_thw" in mm_kwargs:
|
|
return "video"
|
|
raise AssertionError("This line should be unreachable.")
|
|
|
|
def get_max_frames_per_video(self) -> int:
|
|
mm_registry = MULTIMODAL_REGISTRY
|
|
info = mm_registry.get_processing_info(self.model_config)
|
|
max_frames_per_video = info.get_num_frames_with_most_features(
|
|
seq_len=self.model_config.max_model_len,
|
|
mm_counts={"video": self.multimodal_config.get_limit_per_prompt("video")},
|
|
)
|
|
return max_frames_per_video
|
|
|
|
def get_encoder_cudagraph_budget_range(
|
|
self,
|
|
vllm_config: VllmConfig,
|
|
) -> tuple[int, int]:
|
|
# Min: estimated smallest possible encoder input.
|
|
# 224x224 image → 16x16 patches (patch_size=14)
|
|
# spatial_merge_size=2 → 8x8 = 64 tokens
|
|
min_budget = 64
|
|
# Max: capped by max_num_batched_tokens
|
|
max_budget = min(
|
|
vllm_config.scheduler_config.max_num_batched_tokens,
|
|
self.model_config.max_model_len,
|
|
)
|
|
return (min_budget, max_budget)
|
|
|
|
def _get_pixel_values_by_modality(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
) -> torch.Tensor:
|
|
if self.get_input_modality(mm_kwargs) == "image":
|
|
pixel_values = mm_kwargs["pixel_values"]
|
|
else:
|
|
pixel_values = mm_kwargs["pixel_values_videos"]
|
|
return pixel_values
|
|
|
|
def _get_grid_thw_by_modality(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
) -> list[tuple[int, int, int]]:
|
|
grid_thw_key = f"{self.get_input_modality(mm_kwargs)}_grid_thw"
|
|
grid_thw = mm_kwargs[grid_thw_key]
|
|
if not isinstance(grid_thw, list):
|
|
grid_thw = grid_thw.tolist()
|
|
return grid_thw
|
|
|
|
def get_encoder_cudagraph_item_specs(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
):
|
|
from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
|
|
|
|
m = self.visual.spatial_merge_size
|
|
grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
|
|
return [
|
|
EncoderItemSpec(
|
|
input_size=t * h * w,
|
|
output_tokens=t * (h // m) * (w // m),
|
|
)
|
|
for t, h, w in grid_thw
|
|
]
|
|
|
|
def select_encoder_cudagraph_items(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
indices: list[int],
|
|
) -> dict[str, Any]:
|
|
grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
|
|
pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
|
|
|
|
if len(indices) == 0:
|
|
if self.get_input_modality(mm_kwargs) == "image":
|
|
return {
|
|
"pixel_values": pixel_values[:0],
|
|
"image_grid_thw": [],
|
|
}
|
|
elif self.get_input_modality(mm_kwargs) == "video":
|
|
return {
|
|
"pixel_values_videos": pixel_values[:0],
|
|
"video_grid_thw": [],
|
|
}
|
|
else:
|
|
raise AssertionError("This line should be unreachable.")
|
|
|
|
# Compute cumulative patch offsets for slicing pixel_values
|
|
patches_per_item = [t * h * w for t, h, w in grid_thw]
|
|
cum_patches = [0]
|
|
for p in patches_per_item:
|
|
cum_patches.append(cum_patches[-1] + p)
|
|
|
|
selected_pv = torch.cat(
|
|
[pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
|
|
)
|
|
selected_grid = [grid_thw[i] for i in indices]
|
|
|
|
if self.get_input_modality(mm_kwargs) == "image":
|
|
return {
|
|
"pixel_values": selected_pv,
|
|
"image_grid_thw": selected_grid,
|
|
}
|
|
elif self.get_input_modality(mm_kwargs) == "video":
|
|
return {
|
|
"pixel_values_videos": selected_pv,
|
|
"video_grid_thw": selected_grid,
|
|
}
|
|
else:
|
|
raise AssertionError("This line should be unreachable.")
|
|
|
|
def prepare_encoder_cudagraph_capture_inputs(
|
|
self,
|
|
token_budget: int,
|
|
max_batch_size: int,
|
|
max_frames_per_batch: int,
|
|
device: torch.device,
|
|
dtype: torch.dtype,
|
|
path: str = "default",
|
|
):
|
|
from vllm.v1.worker.encoder_cudagraph_defs import (
|
|
EncoderCudaGraphCaptureInputs,
|
|
)
|
|
|
|
spatial_merge_size = self.visual.spatial_merge_size
|
|
max_window_seqs_per_batch = self.visual.get_encoder_cudagraph_max_window_seqs(
|
|
token_budget, max_batch_size, max_frames_per_batch
|
|
)
|
|
# Use ceil here (not floor) so total captured capacity is never smaller
|
|
# than token_budget when token_budget is not divisible by max_batch_size
|
|
# (e.g., 324 budget with max_batch_size=8). Floor under-allocates
|
|
# input_buffer and can fail replay copy for valid single-item batches.
|
|
per_mm_item_output = (token_budget + max_batch_size - 1) // max_batch_size
|
|
|
|
frames_per_item = max_frames_per_batch // max_batch_size
|
|
if frames_per_item > 1:
|
|
# Build the capture grid using a video-format layout so that
|
|
# cu_seqlens is sized for video replays from the start.
|
|
# cu_seqlens has one entry per attention sequence (one per frame),
|
|
# so using T > 1 per item makes the buffer large enough without
|
|
# relying solely on padding.
|
|
# Ceiling ensures frames_per_item * tokens_per_frame >= per_mm_item_output
|
|
# so the pixel_values buffer covers any valid single-item replay.
|
|
tokens_per_frame = (
|
|
per_mm_item_output + frames_per_item - 1
|
|
) // frames_per_item
|
|
# Video-format grid_config (T=frames_per_item).
|
|
grid_config = [
|
|
[
|
|
frames_per_item,
|
|
spatial_merge_size,
|
|
tokens_per_frame * spatial_merge_size,
|
|
]
|
|
for _ in range(max_batch_size)
|
|
]
|
|
else:
|
|
# Image-format grid_config (T=1).
|
|
grid_config = [
|
|
[1, spatial_merge_size, per_mm_item_output * spatial_merge_size]
|
|
for _ in range(max_batch_size)
|
|
]
|
|
|
|
# Create dummy pixel_values
|
|
patch_embed = self.visual.patch_embed
|
|
in_channels = patch_embed.proj.in_channels
|
|
patch_size = patch_embed.patch_size
|
|
temporal_patch_size = patch_embed.temporal_patch_size
|
|
total_patches = sum(t * h * w for t, h, w in grid_config)
|
|
flattened_patch_size = (
|
|
in_channels * temporal_patch_size * patch_size * patch_size
|
|
)
|
|
dummy_pixel_values = torch.randn(
|
|
total_patches, flattened_patch_size, device=device, dtype=dtype
|
|
)
|
|
|
|
# Override max_seqlen with a safe upper bound for capture.
|
|
# max_seqlen.item() gets baked into the CUDA graph (not replayed),
|
|
# so the capture value must cover any replay scenario.
|
|
# Worst case: 1 item consuming the full budget ->
|
|
# seq_len = token_budget * spatial_merge_size^2.
|
|
# For window-attention, each local window is bounded by fixed geometry:
|
|
# (window_size / patch_size / spatial_merge_size)^2 windows in merged
|
|
# token space, multiplied by spatial_merge_size^2 to map back to the
|
|
# unmerged sequence length used by attention kernels.
|
|
vit_merger_window_size = (
|
|
self.visual.window_size
|
|
// self.visual.spatial_merge_size
|
|
// self.visual.patch_size
|
|
)
|
|
max_seqlen_window_override = vit_merger_window_size**2 * (spatial_merge_size**2)
|
|
metadata = self.visual.prepare_encoder_metadata(
|
|
grid_config,
|
|
max_batch_size=max_batch_size,
|
|
max_frames_per_batch=max_frames_per_batch,
|
|
max_window_seqs_per_batch=max_window_seqs_per_batch,
|
|
max_seqlen_override=token_budget * (spatial_merge_size**2),
|
|
max_seqlen_window_override=max_seqlen_window_override,
|
|
device=device,
|
|
)
|
|
|
|
# Just use image-modality dummy input_buffer for capturing, since it's also
|
|
# compatible for video inputs (has the same shape: [num_patches, C*T*P*P]).
|
|
values = metadata | {
|
|
"pixel_values": dummy_pixel_values,
|
|
}
|
|
|
|
return EncoderCudaGraphCaptureInputs(
|
|
values=values,
|
|
)
|
|
|
|
def prepare_encoder_cudagraph_replay_buffers(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
max_batch_size: int,
|
|
max_frames_per_batch: int,
|
|
path: str = "default",
|
|
):
|
|
modality = self.get_input_modality(mm_kwargs)
|
|
grid_thw_list = self._get_grid_thw_by_modality(mm_kwargs)
|
|
|
|
# Keep replay metadata sized to the actual batch. The captured buffers
|
|
# may be larger, but EncoderCudaGraphManager fills the remaining
|
|
# cu*_seqlens entries with the last cumulative offset to represent empty
|
|
# sequences. Padding cu_window_seqlens here would require a static upper
|
|
# bound and can over-pad window attention into many empty FlashAttention
|
|
# CTAs.
|
|
if modality == "image":
|
|
metadata = self.visual.prepare_encoder_metadata(
|
|
grid_thw_list,
|
|
max_batch_size=max_batch_size,
|
|
)
|
|
elif modality == "video":
|
|
metadata = self.visual.prepare_encoder_metadata(
|
|
grid_thw_list,
|
|
max_frames_per_batch=max_frames_per_batch,
|
|
)
|
|
else:
|
|
raise AssertionError("This line should be unreachable.")
|
|
|
|
values = metadata | {
|
|
"pixel_values": self._get_pixel_values_by_modality(mm_kwargs),
|
|
}
|
|
return EncoderCudaGraphReplayBuffers(values=values)
|
|
|
|
def encoder_cudagraph_forward(
|
|
self,
|
|
values: dict[str, torch.Tensor],
|
|
path: str = "default",
|
|
) -> torch.Tensor:
|
|
pixel_values = values.pop("pixel_values")
|
|
metadata = values
|
|
return self.visual(pixel_values, None, encoder_metadata=metadata)
|
|
|
|
def encoder_eager_forward(
|
|
self,
|
|
mm_kwargs: dict[str, Any],
|
|
path: str = "default",
|
|
) -> torch.Tensor:
|
|
pixel_values = self._get_pixel_values_by_modality(mm_kwargs)
|
|
grid_thw = self._get_grid_thw_by_modality(mm_kwargs)
|
|
return self.visual(pixel_values, grid_thw)
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor | None,
|
|
positions: torch.Tensor,
|
|
intermediate_tensors: IntermediateTensors | None = None,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
**kwargs: object,
|
|
) -> torch.Tensor | IntermediateTensors:
|
|
"""Run forward pass for Qwen2.5-VL.
|
|
|
|
Args:
|
|
input_ids: Flattened (concatenated) input_ids corresponding to a
|
|
batch.
|
|
positions: Flattened (concatenated) position ids corresponding to a
|
|
batch. **NOTE**: If mrope is enabled (default setting for
|
|
Qwen2.5-VL opensource models), the shape will be `(3, seq_len)`,
|
|
otherwise it will be `(seq_len,).
|
|
"""
|
|
|
|
if intermediate_tensors is not None:
|
|
inputs_embeds = None
|
|
|
|
hidden_states = self.language_model.model(
|
|
input_ids=input_ids,
|
|
positions=positions,
|
|
intermediate_tensors=intermediate_tensors,
|
|
inputs_embeds=inputs_embeds,
|
|
)
|
|
return hidden_states
|
|
|
|
def compute_logits(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
) -> torch.Tensor | None:
|
|
return self.language_model.compute_logits(hidden_states)
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
|
loader = AutoWeightsLoader(self)
|
|
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
|
|
|
def get_mm_mapping(self) -> MultiModelKeys:
|
|
"""
|
|
Get the module prefix in multimodal models
|
|
"""
|
|
return MultiModelKeys.from_string_field(
|
|
language_model="language_model",
|
|
connector="visual.merger.",
|
|
tower_model="visual.",
|
|
)
|
|
|
|
def get_num_mm_encoder_tokens(
|
|
self,
|
|
num_image_tokens: int,
|
|
) -> int:
|
|
hf_config = self.config
|
|
vision_config = hf_config.vision_config
|
|
merge_size = vision_config.spatial_merge_size
|
|
|
|
return num_image_tokens * merge_size**2
|
|
|
|
def get_num_mm_connector_tokens(
|
|
self,
|
|
num_vision_tokens: int,
|
|
) -> int:
|
|
hf_config = self.config
|
|
vision_config = hf_config.vision_config
|
|
merge_size = vision_config.spatial_merge_size
|
|
return num_vision_tokens // merge_size**2
|