vllm-project--vllm
1325 行
48 KiB
Python
1325 行
48 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Inference-only MiniCPM-V 4.6 model (MiniCPMV4_6ForConditionalGeneration)."""
|
|
|
|
from collections.abc import Iterable, Mapping
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image as PILImage
|
|
from torch import nn
|
|
from transformers import MiniCPMV4_6Config
|
|
|
|
from vllm.config import VllmConfig
|
|
from vllm.distributed import get_tensor_model_parallel_world_size
|
|
from vllm.model_executor.layers.activation import get_act_fn
|
|
from vllm.model_executor.layers.attention import MMEncoderAttention
|
|
from vllm.model_executor.layers.linear import (
|
|
QKVParallelLinear,
|
|
RowParallelLinear,
|
|
)
|
|
from vllm.model_executor.layers.mamba.mamba_utils import (
|
|
MambaStateCopyFuncCalculator,
|
|
MambaStateDtypeCalculator,
|
|
MambaStateShapeCalculator,
|
|
)
|
|
from vllm.model_executor.layers.quantization import QuantizationConfig
|
|
from vllm.multimodal import MULTIMODAL_REGISTRY
|
|
from vllm.multimodal.inputs import (
|
|
MultiModalFeatureSpec,
|
|
MultiModalFieldConfig,
|
|
NestedTensors,
|
|
)
|
|
from vllm.multimodal.parse import ImageProcessorItems, ImageSize, VideoProcessorItems
|
|
from vllm.multimodal.processing.processor import (
|
|
PromptReplacement,
|
|
PromptUpdateDetails,
|
|
ResolvedPromptUpdate,
|
|
_seq2text,
|
|
)
|
|
from vllm.sequence import IntermediateTensors
|
|
|
|
from .idefics2_vision_model import Idefics2VisionTransformer
|
|
from .interfaces import (
|
|
HasInnerState,
|
|
IsHybrid,
|
|
MultiModalEmbeddings,
|
|
SupportsLoRA,
|
|
SupportsMRoPE,
|
|
SupportsMultiModal,
|
|
SupportsPP,
|
|
_require_is_multimodal,
|
|
)
|
|
from .minicpmv import (
|
|
MiniCPMVDummyInputsBuilder,
|
|
MiniCPMVImageEmbeddingInputs,
|
|
MiniCPMVImageEmbeddingItems,
|
|
MiniCPMVImagePixelInputs,
|
|
MiniCPMVMultiModalProcessor,
|
|
MiniCPMVProcessingInfo,
|
|
MiniCPMVVideoEmbeddingItems,
|
|
)
|
|
from .module_mapping import MultiModelKeys
|
|
from .qwen3_5 import Qwen3_5ForCausalLM
|
|
from .utils import (
|
|
AutoWeightsLoader,
|
|
WeightsMapper,
|
|
_merge_multimodal_embeddings,
|
|
flatten_bn,
|
|
maybe_prefix,
|
|
)
|
|
from .vision import is_vit_use_data_parallel
|
|
|
|
|
|
def _minicpmv4_6_field_config(hf_inputs: Mapping[str, torch.Tensor]):
|
|
fields = dict(
|
|
pixel_values=MultiModalFieldConfig.batched("image"),
|
|
tgt_sizes=MultiModalFieldConfig.batched("image"),
|
|
image_embeds=MultiModalFieldConfig.batched("image"),
|
|
video_pixel_values=MultiModalFieldConfig.batched("video"),
|
|
video_image_sizes=MultiModalFieldConfig.batched("video"),
|
|
video_tgt_sizes=MultiModalFieldConfig.batched("video"),
|
|
video_embeds=MultiModalFieldConfig.batched("video"),
|
|
)
|
|
if "use_vit_merger" in hf_inputs:
|
|
fields["use_vit_merger"] = MultiModalFieldConfig.batched("image")
|
|
return fields
|
|
|
|
|
|
class MiniCPMV4_6MultiModalProcessor(MiniCPMVMultiModalProcessor):
|
|
def _resolve_downsample_mode(
|
|
self,
|
|
mm_kwargs: Mapping[str, object],
|
|
) -> str:
|
|
ds = mm_kwargs.get("downsample_mode")
|
|
if ds is not None:
|
|
return str(ds)
|
|
return self.info._get_downsample_mode()
|
|
|
|
def get_image_prompt_texts(
|
|
self,
|
|
image_size,
|
|
image_idx: int = 0,
|
|
downsample_mode: str | None = None,
|
|
) -> str:
|
|
return self.info.get_slice_image_placeholder(
|
|
image_size,
|
|
image_idx=image_idx,
|
|
downsample_mode=downsample_mode,
|
|
)
|
|
|
|
def get_video_prompt_texts(
|
|
self,
|
|
image_size,
|
|
num_frames: int,
|
|
downsample_mode: str | None = None,
|
|
video_idx: int = 0,
|
|
) -> str:
|
|
# Match transformers v5.7+ MiniCPMV4_6Processor video formatting:
|
|
# <image_id>{video_idx}</image_id>(<image>VIDEO*src</image>
|
|
# <slice>VIDEO*patch</slice>...)*num_frames
|
|
# Crucially the visual token inside each frame is ``<|video_pad|>``
|
|
# (tokenizer.video_token), NOT ``<|image_pad|>`` — they share the same
|
|
# embedding-injection role but the language model is conditioned on
|
|
# which one is used. Using image_token for video silently produces
|
|
# garbage descriptions.
|
|
info = self.info
|
|
grids, source_tokens, patch_tokens = info._compute_visual_tokens(
|
|
image_size,
|
|
max_slice_nums=info.get_video_max_slice_num(),
|
|
downsample_mode=downsample_mode,
|
|
)
|
|
tokenizer = info.get_tokenizer()
|
|
video_token = getattr(tokenizer, "video_token", "<|video_pad|>")
|
|
image_start = getattr(tokenizer, "image_start_token", "<image>")
|
|
image_end = getattr(tokenizer, "image_end_token", "</image>")
|
|
slice_start = getattr(tokenizer, "slice_start_token", "<slice>")
|
|
slice_end = getattr(tokenizer, "slice_end_token", "</slice>")
|
|
id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
|
|
id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
|
|
|
|
per_frame = image_start + video_token * source_tokens + image_end
|
|
if grids[0] > 0 and grids[1] > 0 and patch_tokens > 0:
|
|
slice_ph = slice_start + video_token * patch_tokens + slice_end
|
|
rows = [slice_ph * grids[1] for _ in range(grids[0])]
|
|
per_frame += "\n".join(rows)
|
|
|
|
body = per_frame * num_frames
|
|
return f"{id_start}{video_idx}{id_end}" + body
|
|
|
|
def process_images(
|
|
self,
|
|
mm_data: Mapping[str, object],
|
|
mm_kwargs: Mapping[str, object],
|
|
tok_kwargs: Mapping[str, object],
|
|
) -> Mapping[str, NestedTensors]:
|
|
if (images := mm_data.get("images")) is None:
|
|
return {}
|
|
|
|
mm_items = self.info.parse_mm_data({"image": images}, validate=False)
|
|
parsed_images = mm_items.get_items(
|
|
"image", (MiniCPMVImageEmbeddingItems, ImageProcessorItems)
|
|
)
|
|
|
|
if isinstance(parsed_images, MiniCPMVImageEmbeddingItems):
|
|
return {}
|
|
|
|
# transformers v5.7+ MiniCPMV4_6ImageProcessor returns
|
|
# `pixel_values` (1, C, P, sum_W) where all slices are fused along W
|
|
# (NaViT-style), and `target_sizes` (n_slices, 2). vLLM expects each
|
|
# image entry to be a 4D tensor (n_slices, C, P, L_max_padded).
|
|
n_images = len(parsed_images)
|
|
image_processor = self.info.get_image_processor()
|
|
patch_size = image_processor.patch_size
|
|
per_image_pixel_values: list[torch.Tensor] = []
|
|
per_image_tgt_sizes: list[torch.Tensor] = []
|
|
for image in parsed_images:
|
|
ip_out = image_processor([image], **mm_kwargs)
|
|
pv = ip_out["pixel_values"] # (1, C, P, sum_W)
|
|
ts = ip_out["target_sizes"] # (n_slices, 2)
|
|
if pv.ndim == 4 and pv.shape[0] == 1:
|
|
pv = pv.squeeze(0) # (C, P, sum_W)
|
|
ts_long = ts.to(torch.long)
|
|
split_widths = (ts_long[:, 0] * ts_long[:, 1] * patch_size).tolist()
|
|
slices = torch.split(pv, split_widths, dim=-1)
|
|
n_slices = len(slices)
|
|
l_max = max(s.shape[-1] for s in slices)
|
|
out = torch.zeros(
|
|
n_slices,
|
|
pv.shape[0],
|
|
pv.shape[1],
|
|
l_max,
|
|
dtype=pv.dtype,
|
|
device=pv.device,
|
|
)
|
|
for i, s in enumerate(slices):
|
|
out[i, :, :, : s.shape[-1]] = s
|
|
per_image_pixel_values.append(out)
|
|
per_image_tgt_sizes.append(ts_long)
|
|
|
|
image_inputs: dict = {
|
|
"pixel_values": per_image_pixel_values,
|
|
"tgt_sizes": per_image_tgt_sizes,
|
|
}
|
|
|
|
ds_mode = self._resolve_downsample_mode(mm_kwargs)
|
|
insert_layer_id = getattr(
|
|
self.info.get_hf_config(),
|
|
"insert_layer_id",
|
|
-1,
|
|
)
|
|
merger_flag = ds_mode != "4x" and insert_layer_id >= 0
|
|
image_inputs["use_vit_merger"] = [
|
|
torch.tensor([merger_flag], dtype=torch.bool) for _ in range(n_images)
|
|
]
|
|
return image_inputs
|
|
|
|
def process_videos(
|
|
self,
|
|
mm_data: Mapping[str, object],
|
|
mm_kwargs: Mapping[str, object],
|
|
tok_kwargs: Mapping[str, object],
|
|
) -> Mapping[str, NestedTensors]:
|
|
if (videos := mm_data.get("videos")) is None:
|
|
return {}
|
|
|
|
mm_items = self.info.parse_mm_data({"video": videos}, validate=False)
|
|
parsed_videos = mm_items.get_items(
|
|
"video", (MiniCPMVVideoEmbeddingItems, VideoProcessorItems)
|
|
)
|
|
|
|
if isinstance(parsed_videos, MiniCPMVVideoEmbeddingItems):
|
|
return {}
|
|
|
|
# Treat each video as a sequence of frames. The transformers v5.7+
|
|
# `MiniCPMV4_6ImageProcessor` returns NaViT-style fused `pixel_values`;
|
|
# we run it per-frame, split the slices, then re-pack each video into
|
|
# a single 4D tensor (sum_slices, C, P, L_max_video).
|
|
image_processor = self.info.get_image_processor()
|
|
patch_size = image_processor.patch_size
|
|
video_max_slice = self.info.get_video_max_slice_num()
|
|
video_mm_kwargs = {**mm_kwargs, "max_slice_nums": video_max_slice}
|
|
|
|
per_video_pixel_values: list[torch.Tensor] = []
|
|
per_video_tgt_sizes: list[torch.Tensor] = []
|
|
per_video_image_sizes: list[torch.Tensor] = []
|
|
|
|
for video in parsed_videos:
|
|
# video is iterable of frames (PIL Image or numpy array).
|
|
all_slices: list[torch.Tensor] = []
|
|
ts_list: list[torch.Tensor] = []
|
|
frame_sizes: list[torch.Tensor] = []
|
|
for frame in video:
|
|
# Record per-frame (W, H) for video_image_sizes so that
|
|
# get_video_prompt_texts can consume a consistent frame size.
|
|
if isinstance(frame, PILImage.Image):
|
|
w, h = frame.size
|
|
elif isinstance(frame, np.ndarray):
|
|
if frame.ndim == 3 and frame.shape[-1] in (1, 3, 4):
|
|
# HWC (e.g. from np.array(PIL.Image))
|
|
h, w = frame.shape[0], frame.shape[1]
|
|
else:
|
|
# CHW
|
|
_, h, w = frame.shape
|
|
elif isinstance(frame, torch.Tensor):
|
|
if frame.ndim == 3 and frame.shape[-1] in (1, 3, 4):
|
|
h, w = frame.shape[0], frame.shape[1]
|
|
else:
|
|
_, h, w = frame.shape
|
|
else:
|
|
raise TypeError(f"Unsupported frame type: {type(frame)}")
|
|
frame_sizes.append(torch.tensor([w, h], dtype=torch.long, device="cpu"))
|
|
|
|
ip_out = image_processor([frame], **video_mm_kwargs)
|
|
pv = ip_out["pixel_values"] # (1, C, P, sum_W)
|
|
ts = ip_out["target_sizes"] # (n_slices, 2)
|
|
if pv.ndim == 4 and pv.shape[0] == 1:
|
|
pv = pv.squeeze(0) # (C, P, sum_W)
|
|
ts_long = ts.to(torch.long)
|
|
split_widths = (ts_long[:, 0] * ts_long[:, 1] * patch_size).tolist()
|
|
slices = torch.split(pv, split_widths, dim=-1)
|
|
all_slices.extend(slices)
|
|
ts_list.append(ts_long)
|
|
|
|
if not all_slices:
|
|
continue
|
|
|
|
l_max = max(s.shape[-1] for s in all_slices)
|
|
n_total = len(all_slices)
|
|
C, P = all_slices[0].shape[0], all_slices[0].shape[1]
|
|
out = torch.zeros(
|
|
n_total,
|
|
C,
|
|
P,
|
|
l_max,
|
|
dtype=all_slices[0].dtype,
|
|
device=all_slices[0].device,
|
|
)
|
|
for i, s in enumerate(all_slices):
|
|
out[i, :, :, : s.shape[-1]] = s
|
|
|
|
per_video_pixel_values.append(out)
|
|
per_video_tgt_sizes.append(torch.cat(ts_list, dim=0))
|
|
per_video_image_sizes.append(torch.stack(frame_sizes))
|
|
|
|
if not per_video_pixel_values:
|
|
return {}
|
|
|
|
return {
|
|
"video_pixel_values": per_video_pixel_values,
|
|
"video_tgt_sizes": per_video_tgt_sizes,
|
|
"video_image_sizes": per_video_image_sizes,
|
|
}
|
|
|
|
def _get_prompt_updates(
|
|
self,
|
|
mm_items,
|
|
hf_processor_mm_kwargs: Mapping[str, object],
|
|
out_mm_kwargs,
|
|
):
|
|
ds_mode = self._resolve_downsample_mode(hf_processor_mm_kwargs)
|
|
|
|
placeholders = [
|
|
("image", self.info.image_pattern),
|
|
("video", self.info.video_pattern),
|
|
]
|
|
tokenizer = self.info.get_tokenizer()
|
|
additional_placeholders = []
|
|
for modality, pattern in placeholders:
|
|
sub_pattern = tokenizer.decode(
|
|
tokenizer.encode(pattern, add_special_tokens=False)
|
|
)
|
|
if sub_pattern != pattern:
|
|
additional_placeholders.append((modality, sub_pattern))
|
|
placeholders += additional_placeholders
|
|
|
|
# The 4.6 chat_template emits `<|image_pad|>` / `<|video_pad|>` rather
|
|
# than `<unk>`, so use those tokens as the embedding selector.
|
|
image_embed_text = getattr(tokenizer, "image_token", "<|image_pad|>")
|
|
video_embed_text = getattr(tokenizer, "video_token", "<|video_pad|>")
|
|
|
|
def get_image_replacement(item_idx: int):
|
|
images = mm_items.get_items(
|
|
"image",
|
|
(MiniCPMVImageEmbeddingItems, ImageProcessorItems),
|
|
)
|
|
image_size = images.get_image_size(item_idx)
|
|
return PromptUpdateDetails.select_text(
|
|
self.get_image_prompt_texts(
|
|
image_size,
|
|
item_idx,
|
|
downsample_mode=ds_mode,
|
|
),
|
|
image_embed_text,
|
|
)
|
|
|
|
def get_video_replacement(item_idx: int):
|
|
# Prefer video_image_sizes from processed data so that the
|
|
# placeholder count is driven by the same frame sizes that the
|
|
# vision tower will actually consume.
|
|
video_mm_kwargs = out_mm_kwargs.get("video")
|
|
if video_mm_kwargs is not None and item_idx < len(video_mm_kwargs):
|
|
video_item = video_mm_kwargs[item_idx]
|
|
image_sizes_elem = video_item.get("video_image_sizes")
|
|
if image_sizes_elem is not None and image_sizes_elem.data is not None:
|
|
# image_sizes_elem.data: (num_frames, 2) – each row is [W, H]
|
|
image_sizes = image_sizes_elem.data
|
|
num_frames = image_sizes.shape[0]
|
|
frame_size = ImageSize(
|
|
width=int(image_sizes[0, 0].item()),
|
|
height=int(image_sizes[0, 1].item()),
|
|
)
|
|
return PromptUpdateDetails.select_text(
|
|
self.get_video_prompt_texts(
|
|
frame_size,
|
|
num_frames,
|
|
downsample_mode=ds_mode,
|
|
video_idx=item_idx,
|
|
),
|
|
video_embed_text,
|
|
)
|
|
|
|
videos = mm_items.get_items(
|
|
"video",
|
|
(MiniCPMVVideoEmbeddingItems, VideoProcessorItems),
|
|
)
|
|
frame_size = videos.get_frame_size(item_idx)
|
|
num_frames = videos.get_num_frames(item_idx)
|
|
return PromptUpdateDetails.select_text(
|
|
self.get_video_prompt_texts(
|
|
frame_size,
|
|
num_frames,
|
|
downsample_mode=ds_mode,
|
|
video_idx=item_idx,
|
|
),
|
|
video_embed_text,
|
|
)
|
|
|
|
get_replacement = {
|
|
"image": get_image_replacement,
|
|
"video": get_video_replacement,
|
|
}
|
|
|
|
return [
|
|
PromptReplacement(
|
|
modality=modality,
|
|
target=pattern,
|
|
replacement=get_replacement[modality],
|
|
)
|
|
for modality, pattern in placeholders
|
|
]
|
|
|
|
def _recompute_cached_prompt_update(
|
|
self, cached_update: ResolvedPromptUpdate, new_item_idx: int
|
|
) -> ResolvedPromptUpdate:
|
|
new_update = super()._recompute_cached_prompt_update(
|
|
cached_update, new_item_idx
|
|
)
|
|
# MiniCPM-V 4.6 prefixes video placeholders with `<image_id>{idx}</image_id>`
|
|
# (the base class only rewrites the image modality).
|
|
if cached_update.modality == "video":
|
|
tokenizer = self.info.get_tokenizer()
|
|
id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
|
|
id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
|
|
video_token = getattr(tokenizer, "video_token", "<|video_pad|>")
|
|
|
|
text = _seq2text(tokenizer, cached_update.content.full)
|
|
prev_item_idx = cached_update.item_idx
|
|
|
|
new_update = new_update.with_content(
|
|
PromptUpdateDetails.select_text(
|
|
text.replace(
|
|
f"{id_start}{prev_item_idx}{id_end}",
|
|
f"{id_start}{new_item_idx}{id_end}",
|
|
1,
|
|
),
|
|
video_token,
|
|
)
|
|
)
|
|
return new_update
|
|
|
|
def _get_mm_fields_config(
|
|
self,
|
|
hf_inputs,
|
|
hf_processor_mm_kwargs: Mapping[str, object],
|
|
) -> Mapping[str, MultiModalFieldConfig]:
|
|
return _minicpmv4_6_field_config(hf_inputs)
|
|
|
|
|
|
class MiniCPMV4_6ProcessingInfo(MiniCPMVProcessingInfo):
|
|
# transformers v5.7+ chat_template emits these as image/video placeholders.
|
|
image_pattern = "<|image_pad|>"
|
|
video_pattern = "<|video_pad|>"
|
|
|
|
def get_hf_config(self):
|
|
return self.ctx.get_hf_config()
|
|
|
|
def get_hf_processor(self, **kwargs: object):
|
|
# MiniCPM-V 4.6 keeps the native transformers MiniCPMV4_6Processor:
|
|
# this model has its own image/video handling and prompt-update logic
|
|
# below, so it does not need (and is incompatible with) the vendored
|
|
# MiniCPMVProcessor used by 2.x/4.0/4.5, whose __init__ assumes a
|
|
# legacy `image_processor.version` attribute that 4.6 no longer has.
|
|
hf_processor = self.ctx.get_hf_processor(**kwargs)
|
|
|
|
# NumPy arrays are considered as Iterable but not Sequence in
|
|
# https://github.com/huggingface/transformers/blob/main/src/transformers/image_transforms.py#L428
|
|
image_processor = getattr(hf_processor, "image_processor", None)
|
|
if image_processor is not None:
|
|
# transformers v5+ renamed `mean`/`std` -> `image_mean`/`image_std`
|
|
for attr in ("mean", "std", "image_mean", "image_std"):
|
|
val = getattr(image_processor, attr, None)
|
|
if isinstance(val, np.ndarray):
|
|
setattr(image_processor, attr, val.tolist())
|
|
|
|
return hf_processor
|
|
|
|
def _get_expected_hidden_size(self) -> int:
|
|
config = self.get_hf_config()
|
|
if hasattr(config, "text_config") and config.text_config is not None:
|
|
return config.text_config.hidden_size
|
|
return config.hidden_size
|
|
|
|
def get_model_version(self):
|
|
return (4, 6)
|
|
|
|
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
|
return {"image": None, "video": None}
|
|
|
|
def get_image_max_slice_num(self) -> int:
|
|
config = self.get_hf_config()
|
|
if hasattr(config, "slice_config") and config.slice_config is not None:
|
|
return getattr(config.slice_config, "max_slice_nums", 9)
|
|
return getattr(config, "max_slice_nums", 9)
|
|
|
|
def get_video_max_slice_num(self) -> int:
|
|
# Override the base class default of 1: transformers v5.7+
|
|
# `MiniCPMV4_6VideoProcessor` keeps the same max_slice_nums (default 9)
|
|
# as the image processor so that high-res frames get sliced.
|
|
try:
|
|
hf_processor = self.get_hf_processor()
|
|
video_processor = getattr(hf_processor, "video_processor", None)
|
|
if video_processor is not None:
|
|
return int(getattr(video_processor, "max_slice_nums", 9))
|
|
except Exception:
|
|
pass
|
|
return self.get_image_max_slice_num()
|
|
|
|
def _get_downsample_mode(
|
|
self,
|
|
downsample_mode: str | None = None,
|
|
) -> str:
|
|
if downsample_mode is not None:
|
|
return downsample_mode
|
|
image_processor = self.get_image_processor()
|
|
return getattr(image_processor, "downsample_mode", "16x")
|
|
|
|
def _compute_visual_tokens(
|
|
self,
|
|
image_size,
|
|
max_slice_nums: int | None = None,
|
|
downsample_mode: str | None = None,
|
|
) -> tuple[list[int], int, int]:
|
|
"""Compute grid, source_image_visual_tokens and patch_visual_tokens.
|
|
|
|
Args:
|
|
downsample_mode: ``"16x"`` (default, full merge) or ``"4x"``
|
|
(skip vit_merger, 4x more visual tokens).
|
|
|
|
Returns:
|
|
(grids, source_image_visual_tokens, patch_visual_tokens)
|
|
grids is [0, 0] when no slicing occurs.
|
|
"""
|
|
image_processor = self.get_image_processor()
|
|
if max_slice_nums is None:
|
|
max_slice_nums = image_processor.max_slice_nums
|
|
|
|
patch_size = image_processor.patch_size
|
|
scale_res = image_processor.scale_resolution
|
|
downsample_mode = self._get_downsample_mode(downsample_mode)
|
|
token_divisor = 4 if downsample_mode == "4x" else 16
|
|
|
|
# vLLM ImageSize is (width, height); transformers expects (height, width)
|
|
hf_image_size = (image_size.height, image_size.width)
|
|
|
|
# transformers v5.7+ requires `scale_resolution` arg
|
|
try:
|
|
grids = image_processor.get_sliced_grid(
|
|
hf_image_size,
|
|
max_slice_nums,
|
|
scale_res,
|
|
)
|
|
except TypeError:
|
|
grids = image_processor.get_sliced_grid(
|
|
hf_image_size,
|
|
max_slice_nums,
|
|
)
|
|
|
|
if grids is None:
|
|
best_size = image_processor.find_best_resize(
|
|
hf_image_size,
|
|
scale_res,
|
|
patch_size,
|
|
allow_upscale=True,
|
|
)
|
|
source_tokens = (
|
|
best_size[0] * best_size[1] // (patch_size * patch_size * token_divisor)
|
|
)
|
|
return [0, 0], source_tokens, 0
|
|
|
|
best_resize = image_processor.find_best_resize(
|
|
hf_image_size,
|
|
scale_res,
|
|
patch_size,
|
|
)
|
|
source_tokens = (
|
|
best_resize[0] * best_resize[1] // (patch_size * patch_size * token_divisor)
|
|
)
|
|
refine_size = image_processor.get_refine_size(
|
|
hf_image_size,
|
|
grids,
|
|
scale_res,
|
|
patch_size,
|
|
allow_upscale=True,
|
|
)
|
|
patch_w = refine_size[0] // grids[0]
|
|
patch_h = refine_size[1] // grids[1]
|
|
patch_tokens = patch_w * patch_h // (patch_size * patch_size * token_divisor)
|
|
return grids, source_tokens, patch_tokens
|
|
|
|
def get_slice_image_placeholder(
|
|
self,
|
|
image_size,
|
|
image_idx: int = 0,
|
|
max_slice_nums: int | None = None,
|
|
use_image_id: bool = True,
|
|
downsample_mode: str | None = None,
|
|
) -> str:
|
|
grids, source_tokens, patch_tokens = self._compute_visual_tokens(
|
|
image_size,
|
|
max_slice_nums,
|
|
downsample_mode=downsample_mode,
|
|
)
|
|
image_processor = self.get_image_processor()
|
|
# transformers v5.7+ removed `get_slice_image_placeholder` from the
|
|
# image_processor and moved the logic into MiniCPMV4_6Processor.
|
|
# Replicate it here using tokenizer special tokens.
|
|
if hasattr(image_processor, "get_slice_image_placeholder"):
|
|
return image_processor.get_slice_image_placeholder(
|
|
grids,
|
|
image_idx=image_idx,
|
|
max_slice_nums=max_slice_nums,
|
|
use_image_id=use_image_id,
|
|
source_image_visual_tokens=source_tokens,
|
|
patch_visual_tokens=patch_tokens,
|
|
)
|
|
tokenizer = self.get_tokenizer()
|
|
image_token = getattr(tokenizer, "image_token", "<|image_pad|>")
|
|
image_start = getattr(tokenizer, "image_start_token", "<image>")
|
|
image_end = getattr(tokenizer, "image_end_token", "</image>")
|
|
slice_start = getattr(tokenizer, "slice_start_token", "<slice>")
|
|
slice_end = getattr(tokenizer, "slice_end_token", "</slice>")
|
|
id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
|
|
id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
|
|
|
|
placeholder = image_start + image_token * source_tokens + image_end
|
|
if use_image_id:
|
|
placeholder = f"{id_start}{image_idx}{id_end}" + placeholder
|
|
|
|
num_rows, num_cols = grids[0], grids[1]
|
|
if num_cols > 0 and num_rows > 0 and patch_tokens > 0:
|
|
slice_ph = slice_start + image_token * patch_tokens + slice_end
|
|
slices = [slice_ph * num_cols for _ in range(num_rows)]
|
|
placeholder += "\n".join(slices)
|
|
return placeholder
|
|
|
|
def get_num_image_tokens(
|
|
self,
|
|
image_size,
|
|
max_slice_nums: int | None = None,
|
|
downsample_mode: str | None = None,
|
|
) -> int:
|
|
grids, source_tokens, patch_tokens = self._compute_visual_tokens(
|
|
image_size,
|
|
max_slice_nums,
|
|
downsample_mode=downsample_mode,
|
|
)
|
|
return source_tokens + grids[0] * grids[1] * patch_tokens
|
|
|
|
|
|
class MiniCPMV4_6ViTWindowAttentionSelfAttn(nn.Module):
|
|
hf_to_vllm_mapper = WeightsMapper(
|
|
orig_to_new_stacked={
|
|
"q_proj": ("qkv_proj", "q"),
|
|
"k_proj": ("qkv_proj", "k"),
|
|
"v_proj": ("qkv_proj", "v"),
|
|
}
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
config,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
):
|
|
super().__init__()
|
|
use_data_parallel = is_vit_use_data_parallel()
|
|
self.embed_dim = config.hidden_size
|
|
self.num_heads = config.num_attention_heads
|
|
self.head_dim = self.embed_dim // self.num_heads
|
|
self.scale = self.head_dim**-0.5
|
|
|
|
tp_size = 1 if use_data_parallel else get_tensor_model_parallel_world_size()
|
|
assert self.num_heads % tp_size == 0
|
|
self.num_heads_per_partition = self.num_heads // tp_size
|
|
|
|
self.qkv_proj = QKVParallelLinear(
|
|
self.embed_dim,
|
|
self.head_dim,
|
|
self.num_heads,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.qkv_proj",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
self.out_proj = RowParallelLinear(
|
|
self.embed_dim,
|
|
self.embed_dim,
|
|
bias=True,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.out_proj",
|
|
disable_tp=use_data_parallel,
|
|
)
|
|
self.attn = MMEncoderAttention(
|
|
self.num_heads_per_partition,
|
|
self.head_dim,
|
|
self.scale,
|
|
prefix=f"{prefix}.attn",
|
|
)
|
|
|
|
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
|
qkv, _ = self.qkv_proj(hidden_states)
|
|
q, k, v = qkv.chunk(3, dim=-1)
|
|
attn_out = self.attn(q, k, v)
|
|
out, _ = self.out_proj(attn_out)
|
|
return out
|
|
|
|
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 MiniCPMV4_6ViTWindowAttentionMerger(nn.Module):
|
|
def __init__(
|
|
self,
|
|
config,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
):
|
|
super().__init__()
|
|
self.window_kernel_size = (2, 2)
|
|
self.embed_dim = config.hidden_size
|
|
|
|
self.self_attn = MiniCPMV4_6ViTWindowAttentionSelfAttn(
|
|
config,
|
|
quant_config=quant_config,
|
|
prefix=f"{prefix}.self_attn",
|
|
)
|
|
self.layer_norm1 = nn.LayerNorm(
|
|
self.embed_dim,
|
|
eps=config.layer_norm_eps,
|
|
)
|
|
|
|
hidden_4x = self.embed_dim * 4
|
|
inter_4x = config.intermediate_size * 4
|
|
|
|
self.pre_norm = nn.LayerNorm(hidden_4x, eps=config.layer_norm_eps)
|
|
self.linear_1 = nn.Linear(hidden_4x, inter_4x, bias=True)
|
|
self.act = get_act_fn("gelu_pytorch_tanh")
|
|
self.linear_2 = nn.Linear(inter_4x, self.embed_dim, bias=True)
|
|
|
|
def _apply_window_attention(
|
|
self,
|
|
valid_states: torch.Tensor,
|
|
H: int,
|
|
W: int,
|
|
) -> torch.Tensor:
|
|
D = valid_states.shape[-1]
|
|
wh, ww = self.window_kernel_size
|
|
nh, nw = H // wh, W // ww
|
|
num_windows = nh * nw
|
|
|
|
x = valid_states.view(H, W, D)
|
|
x = x.view(nh, wh, nw, ww, D).permute(0, 2, 1, 3, 4).contiguous()
|
|
x = x.view(num_windows, wh * ww, D)
|
|
|
|
x = self.self_attn(x)
|
|
|
|
x = x.view(nh, nw, wh, ww, D).permute(0, 2, 1, 3, 4).contiguous()
|
|
return x.view(H * W, D)
|
|
|
|
def _apply_mlp_downsample(
|
|
self,
|
|
valid_states: torch.Tensor,
|
|
H: int,
|
|
W: int,
|
|
) -> torch.Tensor:
|
|
D = valid_states.shape[-1]
|
|
wh, ww = self.window_kernel_size
|
|
nh, nw = H // wh, W // ww
|
|
|
|
x = valid_states.view(H, W, D)
|
|
x = x.view(nh, wh, nw, ww, D).permute(0, 2, 1, 3, 4).contiguous()
|
|
|
|
residual = x.reshape(nh * nw, wh * ww, D).mean(dim=1)
|
|
x = x.reshape(nh * nw, wh * ww * D)
|
|
|
|
x = self.pre_norm(x)
|
|
x = self.linear_1(x)
|
|
x = self.act(x)
|
|
x = self.linear_2(x)
|
|
return x + residual
|
|
|
|
def forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
tgt_sizes: torch.Tensor,
|
|
attention_mask: torch.Tensor | None,
|
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
|
|
B, _L, D = hidden_states.shape
|
|
device = hidden_states.device
|
|
dtype = hidden_states.dtype
|
|
|
|
all_merged = []
|
|
new_tgt_sizes = torch.zeros_like(tgt_sizes)
|
|
|
|
for b in range(B):
|
|
H, W = tgt_sizes[b].tolist()
|
|
hs = hidden_states[b, : H * W, :]
|
|
|
|
residual = hs
|
|
hs = self.layer_norm1(hs)
|
|
hs = residual + self._apply_window_attention(hs, H, W)
|
|
|
|
wh, ww = self.window_kernel_size
|
|
new_H, new_W = H // wh, W // ww
|
|
all_merged.append(self._apply_mlp_downsample(hs, H, W))
|
|
new_tgt_sizes[b] = torch.tensor(
|
|
[new_H, new_W],
|
|
device=device,
|
|
dtype=tgt_sizes.dtype,
|
|
)
|
|
|
|
new_num_patches = new_tgt_sizes[:, 0] * new_tgt_sizes[:, 1]
|
|
new_max_patches = int(new_num_patches.max().item())
|
|
new_hidden = torch.zeros(
|
|
B,
|
|
new_max_patches,
|
|
D,
|
|
device=device,
|
|
dtype=dtype,
|
|
)
|
|
for b, merged in enumerate(all_merged):
|
|
new_hidden[b, : merged.shape[0], :] = merged
|
|
|
|
# Build new attention mask after spatial downsampling
|
|
new_attention_mask: torch.Tensor | None = None
|
|
if attention_mask is not None:
|
|
mask = torch.zeros(
|
|
B,
|
|
new_max_patches,
|
|
dtype=torch.bool,
|
|
device=device,
|
|
)
|
|
for b in range(B):
|
|
mask[b, : int(new_num_patches[b].item())] = True
|
|
min_val = torch.finfo(dtype).min
|
|
new_attention_mask = (~mask).to(dtype=dtype) * min_val
|
|
new_attention_mask = new_attention_mask[:, None, None, :]
|
|
|
|
return new_hidden, new_tgt_sizes, new_attention_mask
|
|
|
|
|
|
class MiniCPMV4_6DownsampleMLP(nn.Module):
|
|
"""Match HF (transformers v5.7+) parameter naming: pre_norm/linear_1/
|
|
act/linear_2 (instead of pre_norm + Sequential(mlp.0/mlp.2))."""
|
|
|
|
def __init__(
|
|
self,
|
|
hidden_size: int,
|
|
llm_embed_dim: int,
|
|
merge_kernel_size: tuple[int, int] = (2, 2),
|
|
):
|
|
super().__init__()
|
|
self.merge_kernel_size = merge_kernel_size
|
|
self.hidden_size = hidden_size * merge_kernel_size[0] * merge_kernel_size[1]
|
|
self.pre_norm = nn.LayerNorm(self.hidden_size, eps=1e-6)
|
|
self.linear_1 = nn.Linear(self.hidden_size, self.hidden_size, bias=True)
|
|
self.act = get_act_fn("gelu")
|
|
self.linear_2 = nn.Linear(self.hidden_size, llm_embed_dim, bias=True)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
x = self.pre_norm(x)
|
|
x = self.linear_1(x)
|
|
x = self.act(x)
|
|
x = self.linear_2(x)
|
|
return x
|
|
|
|
|
|
class MiniCPMV4_6Merger(nn.Module):
|
|
def __init__(
|
|
self,
|
|
hidden_size: int,
|
|
llm_embed_dim: int,
|
|
merge_kernel_size: tuple[int, int] = (2, 2),
|
|
times: int = 1,
|
|
):
|
|
super().__init__()
|
|
self.merge_kernel_size = merge_kernel_size
|
|
self.times = times
|
|
self.mlp = nn.ModuleList(
|
|
[
|
|
MiniCPMV4_6DownsampleMLP(
|
|
hidden_size,
|
|
llm_embed_dim if i == times - 1 else hidden_size,
|
|
merge_kernel_size,
|
|
)
|
|
for i in range(times)
|
|
]
|
|
)
|
|
|
|
def forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
tgt_sizes: torch.Tensor,
|
|
) -> list[torch.Tensor]:
|
|
"""
|
|
Args:
|
|
hidden_states: (B, max_patches, D) padded batch.
|
|
tgt_sizes: (B, 2) actual (H, W) per sample.
|
|
"""
|
|
m1, m2 = self.merge_kernel_size
|
|
results = []
|
|
|
|
for b in range(len(tgt_sizes)):
|
|
h, w = tgt_sizes[b].tolist()
|
|
n_patches = h * w
|
|
hs = hidden_states[b, :n_patches, :]
|
|
|
|
hs = hs.reshape(h // m1, m1, w // m2, m2, -1)
|
|
hs = hs.permute(0, 2, 1, 3, 4).reshape(
|
|
(h // m1) * (w // m2),
|
|
m1 * m2 * hs.shape[-1],
|
|
)
|
|
hs = self.mlp[0](hs)
|
|
|
|
if self.times > 1:
|
|
cur_h, cur_w = h // m1, w // m2
|
|
for t in range(1, self.times):
|
|
cur_h, cur_w = cur_h // m1, cur_w // m2
|
|
hs = hs.reshape(cur_h, m1, cur_w, m2, -1)
|
|
hs = hs.permute(0, 2, 1, 3, 4).reshape(
|
|
cur_h * cur_w,
|
|
m1 * m2 * hs.shape[-1],
|
|
)
|
|
hs = self.mlp[t](hs)
|
|
|
|
results.append(hs)
|
|
|
|
return results
|
|
|
|
|
|
@MULTIMODAL_REGISTRY.register_processor(
|
|
MiniCPMV4_6MultiModalProcessor,
|
|
info=MiniCPMV4_6ProcessingInfo,
|
|
dummy_inputs=MiniCPMVDummyInputsBuilder,
|
|
)
|
|
class MiniCPMV4_6ForConditionalGeneration(
|
|
nn.Module,
|
|
SupportsMultiModal,
|
|
SupportsLoRA,
|
|
SupportsPP,
|
|
HasInnerState,
|
|
IsHybrid,
|
|
SupportsMRoPE,
|
|
):
|
|
supports_encoder_tp_data = True
|
|
|
|
hf_to_vllm_mapper = WeightsMapper(
|
|
orig_to_new_prefix={
|
|
# transformers v5.7+ uses `vision_tower` and nests `vit_merger`
|
|
# inside it. Order matters: more specific prefix must come first.
|
|
"model.vision_tower.vit_merger.": "vit_merger.",
|
|
"model.vision_tower.": "vpm.",
|
|
"model.vpm.": "vpm.",
|
|
"model.vit_merger.": "vit_merger.",
|
|
"model.merger.": "merger.",
|
|
"model.language_model.": "language_model.model.",
|
|
"lm_head.": "language_model.lm_head.",
|
|
}
|
|
)
|
|
|
|
packed_modules_mapping = {
|
|
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
|
|
"gate_up_proj": ["gate_proj", "up_proj"],
|
|
"in_proj_qkvz": ["in_proj_qkv", "in_proj_z"],
|
|
"in_proj_ba": ["in_proj_b", "in_proj_a"],
|
|
}
|
|
|
|
@classmethod
|
|
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
|
# transformers v5.7+ chat_template uses these tokens.
|
|
if modality.startswith("image"):
|
|
return "<|image_pad|>"
|
|
if modality.startswith("video"):
|
|
return "<|video_pad|>"
|
|
raise ValueError("Only image or video modality is supported")
|
|
|
|
def get_mrope_input_positions(
|
|
self,
|
|
input_tokens: list[int],
|
|
mm_features: list["MultiModalFeatureSpec"],
|
|
) -> tuple[torch.Tensor, int]:
|
|
"""MiniCPM-V uses embedding injection for vision, not spatial M-RoPE.
|
|
|
|
All tokens (text and vision placeholders) get identical sequential
|
|
positions duplicated across the 3 M-RoPE channels expected by the
|
|
Qwen3.5 backbone.
|
|
"""
|
|
seq_len = len(input_tokens)
|
|
positions = torch.arange(seq_len).unsqueeze(0).expand(3, -1)
|
|
return positions, 0
|
|
|
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
|
super().__init__()
|
|
config: MiniCPMV4_6Config = vllm_config.model_config.hf_config
|
|
quant_config = vllm_config.quant_config
|
|
multimodal_config = vllm_config.model_config.multimodal_config
|
|
|
|
self.config = config
|
|
self.multimodal_config = multimodal_config
|
|
self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
|
|
|
|
# --- Vision tower ---
|
|
with self._mark_tower_model(vllm_config, {"image"}):
|
|
self.vpm = Idefics2VisionTransformer(
|
|
config.vision_config,
|
|
quant_config=quant_config,
|
|
apply_encoder_attention_mask=True,
|
|
prefix=maybe_prefix(prefix, "vpm"),
|
|
)
|
|
if config.drop_vision_last_layer:
|
|
self.vpm.encoder.layers = self.vpm.encoder.layers[:-1]
|
|
|
|
self.vit_merger = MiniCPMV4_6ViTWindowAttentionMerger(
|
|
config.vision_config,
|
|
quant_config=quant_config,
|
|
prefix=maybe_prefix(prefix, "vit_merger"),
|
|
)
|
|
self.merger = MiniCPMV4_6Merger(
|
|
hidden_size=config.vision_config.hidden_size,
|
|
llm_embed_dim=config.text_config.hidden_size,
|
|
)
|
|
|
|
# --- Language model ---
|
|
# Temporarily swap top-level model_type so that Qwen3_5ForCausalLM
|
|
# picks up the expected text config when introspecting the hf config.
|
|
with self._mark_language_model(vllm_config):
|
|
saved_model_type = config.model_type
|
|
config.model_type = "qwen3_5_text"
|
|
try:
|
|
self.language_model = Qwen3_5ForCausalLM(
|
|
vllm_config=vllm_config,
|
|
prefix=maybe_prefix(prefix, "language_model"),
|
|
)
|
|
finally:
|
|
config.model_type = saved_model_type
|
|
|
|
self.make_empty_intermediate_tensors = (
|
|
self.language_model.make_empty_intermediate_tensors
|
|
)
|
|
|
|
# ----- Multimodal parsing -----
|
|
|
|
def _parse_and_validate_vision_input(
|
|
self,
|
|
**kwargs: object,
|
|
) -> MiniCPMVImagePixelInputs | MiniCPMVImageEmbeddingInputs | None:
|
|
pixel_values = kwargs.pop("pixel_values", None)
|
|
image_embeds = kwargs.pop("image_embeds", None)
|
|
|
|
if pixel_values is None and image_embeds is None:
|
|
return None
|
|
|
|
if image_embeds is not None:
|
|
return MiniCPMVImageEmbeddingInputs(
|
|
type="image_embeds",
|
|
image_embeds=image_embeds,
|
|
)
|
|
|
|
tgt_sizes = kwargs.pop("tgt_sizes")
|
|
num_slices_flat = torch.tensor([len(ps) for ps in pixel_values])
|
|
pixel_values_flat = flatten_bn(pixel_values)
|
|
tgt_sizes_flat = flatten_bn(tgt_sizes, concat=True)
|
|
|
|
return MiniCPMVImagePixelInputs(
|
|
type="pixel_values",
|
|
pixel_values=pixel_values_flat,
|
|
tgt_sizes=tgt_sizes_flat,
|
|
num_slices=num_slices_flat,
|
|
)
|
|
|
|
# ----- Vision forward -----
|
|
|
|
def get_vision_hidden_states(
|
|
self,
|
|
data: MiniCPMVImagePixelInputs,
|
|
downsample_mode: str | None = None,
|
|
) -> list[torch.Tensor]:
|
|
pixel_values = data["pixel_values"]
|
|
tgt_sizes = data["tgt_sizes"]
|
|
|
|
B = len(pixel_values)
|
|
P = pixel_values[0].shape[-2]
|
|
L = max(item.shape[-1] for item in pixel_values)
|
|
device = pixel_values[0].device
|
|
target_dtype = self.vpm.embeddings.patch_embedding.weight.dtype
|
|
|
|
all_pixel_values = torch.zeros(
|
|
B,
|
|
3,
|
|
P,
|
|
L,
|
|
dtype=target_dtype,
|
|
device=device,
|
|
)
|
|
for i, pv in enumerate(pixel_values):
|
|
all_pixel_values[i, ..., : pv.shape[-1]] = pv.to(target_dtype)
|
|
|
|
num_patches = tgt_sizes.prod(-1)
|
|
max_patches = int(num_patches.max().item())
|
|
patch_attn_mask = torch.zeros(
|
|
B,
|
|
max_patches,
|
|
dtype=torch.bool,
|
|
device=device,
|
|
)
|
|
for i in range(B):
|
|
patch_attn_mask[i, : num_patches[i]] = True
|
|
|
|
hidden_states = self.vpm.embeddings(
|
|
all_pixel_values,
|
|
patch_attention_mask=patch_attn_mask.unsqueeze(1),
|
|
tgt_sizes=tgt_sizes,
|
|
)
|
|
|
|
if torch.any(~patch_attn_mask):
|
|
mask_dtype = hidden_states.dtype
|
|
min_val = torch.finfo(mask_dtype).min
|
|
attention_mask = (~patch_attn_mask).to(dtype=mask_dtype) * min_val
|
|
attention_mask = attention_mask[:, None, None, :]
|
|
else:
|
|
attention_mask = None
|
|
|
|
# Encoder layers with mid-encoder merger injection
|
|
insert_layer_id = getattr(self.config, "insert_layer_id", -1)
|
|
if downsample_mode is None:
|
|
downsample_mode = getattr(self.config, "downsample_mode", "16x")
|
|
use_vit_merger = downsample_mode != "4x" and insert_layer_id >= 0
|
|
|
|
for layer in self.vpm.encoder.layers[: insert_layer_id + 1]:
|
|
hidden_states = layer(hidden_states, attention_mask=attention_mask)
|
|
|
|
if use_vit_merger:
|
|
hidden_states, tgt_sizes, attention_mask = self.vit_merger(
|
|
hidden_states,
|
|
tgt_sizes,
|
|
attention_mask,
|
|
)
|
|
|
|
for layer in self.vpm.encoder.layers[insert_layer_id + 1 :]:
|
|
hidden_states = layer(hidden_states, attention_mask=attention_mask)
|
|
|
|
# 4. Post layernorm
|
|
hidden_states = self.vpm.post_layernorm(hidden_states)
|
|
|
|
# 5. MLP merger → list of per-slice tensors
|
|
return self.merger(hidden_states, tgt_sizes)
|
|
|
|
def _process_vision_input(self, image_input, use_vit_merger=None):
|
|
if image_input["type"] == "image_embeds":
|
|
return image_input["image_embeds"]
|
|
|
|
downsample_mode = None
|
|
if use_vit_merger is not None:
|
|
downsample_mode = "16x" if use_vit_merger else "4x"
|
|
image_features = self.get_vision_hidden_states(
|
|
image_input,
|
|
downsample_mode=downsample_mode,
|
|
)
|
|
num_slices = image_input["num_slices"]
|
|
results = []
|
|
idx = 0
|
|
for n in num_slices.tolist():
|
|
group = image_features[idx : idx + n]
|
|
results.append(torch.cat(group, dim=0))
|
|
idx += n
|
|
return results
|
|
|
|
# ----- Multimodal embedding interface -----
|
|
|
|
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
|
|
use_vit_merger_tensors = kwargs.pop("use_vit_merger", None)
|
|
use_vit_merger = None
|
|
if use_vit_merger_tensors is not None:
|
|
if isinstance(use_vit_merger_tensors, torch.Tensor):
|
|
use_vit_merger = bool(use_vit_merger_tensors.any().item())
|
|
elif isinstance(use_vit_merger_tensors, list | tuple):
|
|
use_vit_merger = any(
|
|
bool(t.any().item()) if isinstance(t, torch.Tensor) else bool(t)
|
|
for t in use_vit_merger_tensors
|
|
)
|
|
|
|
# Split kwargs into image / video buckets (videos are processed via
|
|
# the same vision pipeline; their fields just carry a ``video_`` prefix).
|
|
image_kwargs = {
|
|
k: v
|
|
for k, v in kwargs.items()
|
|
if k in ("pixel_values", "image_embeds", "tgt_sizes")
|
|
}
|
|
video_kwargs = {
|
|
k.removeprefix("video_"): v
|
|
for k, v in kwargs.items()
|
|
if k.startswith("video_")
|
|
}
|
|
|
|
multimodal_embeddings: tuple[torch.Tensor, ...] = ()
|
|
|
|
if (
|
|
image_kwargs.get("pixel_values") is not None
|
|
or image_kwargs.get("image_embeds") is not None
|
|
):
|
|
image_input = self._parse_and_validate_vision_input(**image_kwargs)
|
|
if image_input is not None:
|
|
multimodal_embeddings += tuple(
|
|
self._process_vision_input(
|
|
image_input,
|
|
use_vit_merger=use_vit_merger,
|
|
)
|
|
)
|
|
|
|
if (
|
|
video_kwargs.get("pixel_values") is not None
|
|
or video_kwargs.get("image_embeds") is not None
|
|
):
|
|
video_input = self._parse_and_validate_vision_input(**video_kwargs)
|
|
if video_input is not None:
|
|
multimodal_embeddings += tuple(
|
|
self._process_vision_input(
|
|
video_input,
|
|
use_vit_merger=use_vit_merger,
|
|
)
|
|
)
|
|
|
|
if not multimodal_embeddings:
|
|
return []
|
|
return multimodal_embeddings
|
|
|
|
def embed_input_ids(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
multimodal_embeddings: MultiModalEmbeddings | None = None,
|
|
*,
|
|
is_multimodal: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
inputs_embeds = self._embed_text_input_ids(
|
|
input_ids,
|
|
self.language_model.embed_input_ids,
|
|
is_multimodal=is_multimodal,
|
|
)
|
|
if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
|
|
return inputs_embeds
|
|
|
|
is_multimodal = _require_is_multimodal(is_multimodal)
|
|
return _merge_multimodal_embeddings(
|
|
inputs_embeds=inputs_embeds,
|
|
multimodal_embeddings=multimodal_embeddings,
|
|
is_multimodal=is_multimodal,
|
|
)
|
|
|
|
# ----- Forward / Logits -----
|
|
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor | None,
|
|
positions: torch.Tensor,
|
|
intermediate_tensors: IntermediateTensors | None = None,
|
|
inputs_embeds: torch.Tensor | None = None,
|
|
**kwargs: Any,
|
|
) -> torch.Tensor:
|
|
if intermediate_tensors is not None:
|
|
inputs_embeds = None
|
|
|
|
return self.language_model.model(
|
|
input_ids=input_ids,
|
|
positions=positions,
|
|
intermediate_tensors=intermediate_tensors,
|
|
inputs_embeds=inputs_embeds,
|
|
)
|
|
|
|
def compute_logits(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
) -> torch.Tensor | None:
|
|
return self.language_model.compute_logits(hidden_states)
|
|
|
|
# ----- Weight loading -----
|
|
|
|
def load_weights(
|
|
self,
|
|
weights: Iterable[tuple[str, torch.Tensor]],
|
|
) -> set[str]:
|
|
loader = AutoWeightsLoader(self, skip_prefixes=["mtp."])
|
|
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
|
|
|
def get_mm_mapping(self) -> MultiModelKeys:
|
|
return MultiModelKeys.from_string_field(
|
|
language_model="language_model",
|
|
connector=["vit_merger", "merger"],
|
|
tower_model="vpm",
|
|
)
|
|
|
|
# ----- Mamba / Hybrid state helpers (same as Qwen3.5 VLM) -----
|
|
|
|
@classmethod
|
|
def get_mamba_state_dtype_from_config(cls, vllm_config):
|
|
return MambaStateDtypeCalculator.gated_delta_net_state_dtype(
|
|
vllm_config.model_config.dtype,
|
|
vllm_config.cache_config.mamba_cache_dtype,
|
|
vllm_config.cache_config.mamba_ssm_cache_dtype,
|
|
)
|
|
|
|
@classmethod
|
|
def get_mamba_state_shape_from_config(cls, vllm_config):
|
|
parallel_config = vllm_config.parallel_config
|
|
hf_config = vllm_config.model_config.hf_text_config
|
|
tp_size = parallel_config.tensor_parallel_size
|
|
num_spec = (
|
|
vllm_config.speculative_config.num_speculative_tokens
|
|
if vllm_config.speculative_config
|
|
else 0
|
|
)
|
|
return MambaStateShapeCalculator.gated_delta_net_state_shape(
|
|
tp_size,
|
|
hf_config.linear_num_key_heads,
|
|
hf_config.linear_num_value_heads,
|
|
hf_config.linear_key_head_dim,
|
|
hf_config.linear_value_head_dim,
|
|
hf_config.linear_conv_kernel_dim,
|
|
num_spec,
|
|
)
|
|
|
|
@classmethod
|
|
def get_mamba_state_copy_func(cls):
|
|
return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()
|