main
py 1,342 lines 48.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only MiniCPM-V 4.6 model (MiniCPMV4_6ForConditionalGeneration)."""
4
5 from collections.abc import Iterable, Mapping
6 from typing import Any
7
8 import numpy as np
9 import torch
10 from PIL import Image as PILImage
11 from torch import nn
12 from transformers import MiniCPMV4_6Config
13
14 from vllm.config import VllmConfig
15 from vllm.distributed import get_tensor_model_parallel_world_size
16 from vllm.model_executor.layers.activation import get_act_fn
17 from vllm.model_executor.layers.attention import MMEncoderAttention
18 from vllm.model_executor.layers.linear import (
19 QKVParallelLinear,
20 RowParallelLinear,
21 )
22 from vllm.model_executor.layers.mamba.mamba_utils import (
23 MambaStateCopyFuncCalculator,
24 MambaStateDtypeCalculator,
25 MambaStateShapeCalculator,
26 )
27 from vllm.model_executor.layers.quantization import QuantizationConfig
28 from vllm.multimodal import MULTIMODAL_REGISTRY
29 from vllm.multimodal.inputs import (
30 MultiModalFeatureSpec,
31 MultiModalFieldConfig,
32 NestedTensors,
33 )
34 from vllm.multimodal.parse import ImageProcessorItems, ImageSize, VideoProcessorItems
35 from vllm.multimodal.processing.processor import (
36 PromptReplacement,
37 PromptUpdateDetails,
38 ResolvedPromptUpdate,
39 cached_encode,
40 )
41 from vllm.sequence import IntermediateTensors
42
43 from .idefics2_vision_model import Idefics2VisionTransformer
44 from .interfaces import (
45 HasInnerState,
46 IsHybrid,
47 MultiModalEmbeddings,
48 SupportsLoRA,
49 SupportsMRoPE,
50 SupportsMultiModal,
51 SupportsPP,
52 _require_is_multimodal,
53 )
54 from .minicpmv import (
55 MiniCPMVDummyInputsBuilder,
56 MiniCPMVImageEmbeddingInputs,
57 MiniCPMVImageEmbeddingItems,
58 MiniCPMVImagePixelInputs,
59 MiniCPMVMultiModalProcessor,
60 MiniCPMVProcessingInfo,
61 MiniCPMVVideoEmbeddingItems,
62 _image_kwargs_from_video,
63 )
64 from .module_mapping import MultiModelKeys
65 from .qwen3_5 import Qwen3_5ForCausalLM
66 from .utils import (
67 AutoWeightsLoader,
68 WeightsMapper,
69 _merge_multimodal_embeddings,
70 flatten_bn,
71 maybe_prefix,
72 )
73 from .vision import is_vit_use_data_parallel
74
75
76 def _minicpmv4_6_field_config(hf_inputs: Mapping[str, torch.Tensor]):
77 fields = dict(
78 pixel_values=MultiModalFieldConfig.batched("image"),
79 tgt_sizes=MultiModalFieldConfig.batched("image"),
80 image_embeds=MultiModalFieldConfig.batched("image"),
81 video_pixel_values=MultiModalFieldConfig.batched("video"),
82 video_image_sizes=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
83 video_tgt_sizes=MultiModalFieldConfig.batched("video"),
84 video_embeds=MultiModalFieldConfig.batched("video"),
85 )
86 if "use_vit_merger" in hf_inputs:
87 fields["use_vit_merger"] = MultiModalFieldConfig.batched(
88 "image", keep_on_cpu=True
89 )
90 return fields
91
92
93 class MiniCPMV4_6MultiModalProcessor(MiniCPMVMultiModalProcessor):
94 def _resolve_downsample_mode(
95 self,
96 mm_kwargs: Mapping[str, object],
97 ) -> str:
98 ds = mm_kwargs.get("downsample_mode")
99 if ds is not None:
100 return str(ds)
101 info = self.info
102 assert isinstance(info, MiniCPMV4_6ProcessingInfo)
103 return info._get_downsample_mode()
104
105 def get_image_prompt_texts(
106 self,
107 image_size,
108 image_idx: int = 0,
109 downsample_mode: str | None = None,
110 ) -> str:
111 info = self.info
112 assert isinstance(info, MiniCPMV4_6ProcessingInfo)
113 return info.get_slice_image_placeholder(
114 image_size,
115 image_idx=image_idx,
116 downsample_mode=downsample_mode,
117 )
118
119 def get_video_prompt_texts(
120 self,
121 image_size,
122 num_frames: int,
123 downsample_mode: str | None = None,
124 video_idx: int = 0,
125 ) -> str:
126 # Match transformers v5.7+ MiniCPMV4_6Processor video formatting:
127 # <image_id>{video_idx}</image_id>(<image>VIDEO*src</image>
128 # <slice>VIDEO*patch</slice>...)*num_frames
129 # Crucially the visual token inside each frame is ``<|video_pad|>``
130 # (tokenizer.video_token), NOT ``<|image_pad|>`` — they share the same
131 # embedding-injection role but the language model is conditioned on
132 # which one is used. Using image_token for video silently produces
133 # garbage descriptions.
134 info = self.info
135 assert isinstance(info, MiniCPMV4_6ProcessingInfo)
136 grids, source_tokens, patch_tokens = info._compute_visual_tokens(
137 image_size,
138 max_slice_nums=info.get_video_max_slice_num(),
139 downsample_mode=downsample_mode,
140 )
141 tokenizer = info.get_tokenizer()
142 video_token = getattr(tokenizer, "video_token", "<|video_pad|>")
143 image_start = getattr(tokenizer, "image_start_token", "<image>")
144 image_end = getattr(tokenizer, "image_end_token", "</image>")
145 slice_start = getattr(tokenizer, "slice_start_token", "<slice>")
146 slice_end = getattr(tokenizer, "slice_end_token", "</slice>")
147 id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
148 id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
149
150 per_frame = image_start + video_token * source_tokens + image_end
151 if grids[0] > 0 and grids[1] > 0 and patch_tokens > 0:
152 slice_ph = slice_start + video_token * patch_tokens + slice_end
153 rows = [slice_ph * grids[1] for _ in range(grids[0])]
154 per_frame += "\n".join(rows)
155
156 body = per_frame * num_frames
157 return f"{id_start}{video_idx}{id_end}" + body
158
159 def process_images(
160 self,
161 mm_data: Mapping[str, object],
162 mm_kwargs: Mapping[str, object],
163 ) -> Mapping[str, NestedTensors]:
164 if (images := mm_data.get("images")) is None:
165 return {}
166
167 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
168 parsed_images = mm_items.get_items(
169 "image", (MiniCPMVImageEmbeddingItems, ImageProcessorItems)
170 )
171
172 if isinstance(parsed_images, MiniCPMVImageEmbeddingItems):
173 return {}
174
175 # transformers v5.7+ MiniCPMV4_6ImageProcessor returns
176 # `pixel_values` (1, C, P, sum_W) where all slices are fused along W
177 # (NaViT-style), and `target_sizes` (n_slices, 2). vLLM expects each
178 # image entry to be a 4D tensor (n_slices, C, P, L_max_padded).
179 n_images = len(parsed_images)
180 image_processor = self.info.get_image_processor()
181 patch_size = image_processor.patch_size
182 per_image_pixel_values: list[torch.Tensor] = []
183 per_image_tgt_sizes: list[torch.Tensor] = []
184 for image in parsed_images:
185 ip_out = image_processor([image], **mm_kwargs)
186 pv = ip_out["pixel_values"] # (1, C, P, sum_W)
187 ts = ip_out["target_sizes"] # (n_slices, 2)
188 if pv.ndim == 4 and pv.shape[0] == 1:
189 pv = pv.squeeze(0) # (C, P, sum_W)
190 ts_long = ts.to(torch.long)
191 split_widths = (ts_long[:, 0] * ts_long[:, 1] * patch_size).tolist()
192 slices = torch.split(pv, split_widths, dim=-1)
193 n_slices = len(slices)
194 l_max = max(s.shape[-1] for s in slices)
195 out = torch.zeros(
196 n_slices,
197 pv.shape[0],
198 pv.shape[1],
199 l_max,
200 dtype=pv.dtype,
201 device=pv.device,
202 )
203 for i, s in enumerate(slices):
204 out[i, :, :, : s.shape[-1]] = s
205 per_image_pixel_values.append(out)
206 per_image_tgt_sizes.append(ts_long)
207
208 image_inputs: dict = {
209 "pixel_values": per_image_pixel_values,
210 "tgt_sizes": per_image_tgt_sizes,
211 }
212
213 ds_mode = self._resolve_downsample_mode(mm_kwargs)
214 insert_layer_id = getattr(
215 self.info.get_hf_config(),
216 "insert_layer_id",
217 -1,
218 )
219 merger_flag = ds_mode != "4x" and insert_layer_id >= 0
220 image_inputs["use_vit_merger"] = [
221 torch.tensor([merger_flag], dtype=torch.bool) for _ in range(n_images)
222 ]
223 return image_inputs
224
225 def process_videos(
226 self,
227 mm_data: Mapping[str, object],
228 mm_kwargs: Mapping[str, object],
229 ) -> Mapping[str, NestedTensors]:
230 if (videos := mm_data.get("videos")) is None:
231 return {}
232
233 mm_items = self.info.parse_mm_data({"video": videos}, validate=False)
234 parsed_videos = mm_items.get_items(
235 "video", (MiniCPMVVideoEmbeddingItems, VideoProcessorItems)
236 )
237
238 if isinstance(parsed_videos, MiniCPMVVideoEmbeddingItems):
239 return {}
240
241 # Treat each video as a sequence of frames. The transformers v5.7+
242 # `MiniCPMV4_6ImageProcessor` returns NaViT-style fused `pixel_values`;
243 # we run it per-frame, split the slices, then re-pack each video into
244 # a single 4D tensor (sum_slices, C, P, L_max_video).
245 image_processor = self.info.get_image_processor()
246 patch_size = image_processor.patch_size
247 video_max_slice = self.info.get_video_max_slice_num()
248 video_mm_kwargs = {**mm_kwargs, "max_slice_nums": video_max_slice}
249
250 per_video_pixel_values: list[torch.Tensor] = []
251 per_video_tgt_sizes: list[torch.Tensor] = []
252 per_video_image_sizes: list[torch.Tensor] = []
253
254 for video in parsed_videos:
255 # video is iterable of frames (PIL Image or numpy array).
256 all_slices: list[torch.Tensor] = []
257 ts_list: list[torch.Tensor] = []
258 frame_sizes: list[torch.Tensor] = []
259 for frame in video:
260 # Record per-frame (W, H) for video_image_sizes so that
261 # get_video_prompt_texts can consume a consistent frame size.
262 if isinstance(frame, PILImage.Image):
263 w, h = frame.size
264 elif isinstance(frame, np.ndarray):
265 if frame.ndim == 3 and frame.shape[-1] in (1, 3, 4):
266 # HWC (e.g. from np.array(PIL.Image))
267 h, w = frame.shape[0], frame.shape[1]
268 else:
269 # CHW
270 _, h, w = frame.shape
271 elif isinstance(frame, torch.Tensor):
272 if frame.ndim == 3 and frame.shape[-1] in (1, 3, 4):
273 h, w = frame.shape[0], frame.shape[1]
274 else:
275 _, h, w = frame.shape
276 else:
277 raise TypeError(f"Unsupported frame type: {type(frame)}")
278 frame_sizes.append(torch.tensor([w, h], dtype=torch.long, device="cpu"))
279
280 ip_out = image_processor([frame], **video_mm_kwargs)
281 pv = ip_out["pixel_values"] # (1, C, P, sum_W)
282 ts = ip_out["target_sizes"] # (n_slices, 2)
283 if pv.ndim == 4 and pv.shape[0] == 1:
284 pv = pv.squeeze(0) # (C, P, sum_W)
285 ts_long = ts.to(torch.long)
286 split_widths = (ts_long[:, 0] * ts_long[:, 1] * patch_size).tolist()
287 slices = torch.split(pv, split_widths, dim=-1)
288 all_slices.extend(slices)
289 ts_list.append(ts_long)
290
291 if not all_slices:
292 continue
293
294 l_max = max(s.shape[-1] for s in all_slices)
295 n_total = len(all_slices)
296 C, P = all_slices[0].shape[0], all_slices[0].shape[1]
297 out = torch.zeros(
298 n_total,
299 C,
300 P,
301 l_max,
302 dtype=all_slices[0].dtype,
303 device=all_slices[0].device,
304 )
305 for i, s in enumerate(all_slices):
306 out[i, :, :, : s.shape[-1]] = s
307
308 per_video_pixel_values.append(out)
309 per_video_tgt_sizes.append(torch.cat(ts_list, dim=0))
310 per_video_image_sizes.append(torch.stack(frame_sizes))
311
312 if not per_video_pixel_values:
313 return {}
314
315 return {
316 "video_pixel_values": per_video_pixel_values,
317 "video_tgt_sizes": per_video_tgt_sizes,
318 "video_image_sizes": per_video_image_sizes,
319 }
320
321 def _get_prompt_updates(
322 self,
323 mm_items,
324 hf_processor_mm_kwargs: Mapping[str, object],
325 out_mm_kwargs,
326 ):
327 ds_mode = self._resolve_downsample_mode(hf_processor_mm_kwargs)
328
329 placeholders = [
330 ("image", self.info.image_pattern),
331 ("video", self.info.video_pattern),
332 ]
333 tokenizer = self.info.get_tokenizer()
334 additional_placeholders = []
335 for modality, pattern in placeholders:
336 sub_pattern = tokenizer.decode(
337 cached_encode(tokenizer, pattern, add_special_tokens=False)
338 )
339 if sub_pattern != pattern:
340 additional_placeholders.append((modality, sub_pattern))
341 placeholders += additional_placeholders
342
343 # The 4.6 chat_template emits `<|image_pad|>` / `<|video_pad|>` rather
344 # than `<unk>`, so use those tokens as the embedding selector.
345 image_embed_text = getattr(tokenizer, "image_token", "<|image_pad|>")
346 video_embed_text = getattr(tokenizer, "video_token", "<|video_pad|>")
347 vocab = tokenizer.get_vocab()
348 image_embed_ids = [vocab[image_embed_text]]
349 video_embed_ids = [vocab[video_embed_text]]
350
351 def get_image_replacement(item_idx: int):
352 images = mm_items.get_items(
353 "image",
354 (MiniCPMVImageEmbeddingItems, ImageProcessorItems),
355 )
356 image_size = images.get_image_size(item_idx)
357 return PromptUpdateDetails.select_token_ids(
358 cached_encode(
359 tokenizer,
360 self.get_image_prompt_texts(
361 image_size,
362 item_idx,
363 downsample_mode=ds_mode,
364 ),
365 add_special_tokens=False,
366 ),
367 image_embed_ids,
368 )
369
370 def get_video_replacement(item_idx: int):
371 # Prefer video_image_sizes from processed data so that the
372 # placeholder count is driven by the same frame sizes that the
373 # vision tower will actually consume.
374 video_mm_kwargs = out_mm_kwargs.get("video")
375 if video_mm_kwargs is not None and item_idx < len(video_mm_kwargs):
376 video_item = video_mm_kwargs[item_idx]
377 image_sizes_elem = video_item.get("video_image_sizes")
378 if image_sizes_elem is not None and image_sizes_elem.data is not None:
379 # image_sizes_elem.data: (num_frames, 2) – each row is [W, H]
380 image_sizes = image_sizes_elem.data
381 num_frames = image_sizes.shape[0]
382 frame_size = ImageSize(
383 width=int(image_sizes[0, 0].item()),
384 height=int(image_sizes[0, 1].item()),
385 )
386 return PromptUpdateDetails.select_token_ids(
387 cached_encode(
388 tokenizer,
389 self.get_video_prompt_texts(
390 frame_size,
391 num_frames,
392 downsample_mode=ds_mode,
393 video_idx=item_idx,
394 ),
395 add_special_tokens=False,
396 ),
397 video_embed_ids,
398 )
399
400 videos = mm_items.get_items(
401 "video",
402 (MiniCPMVVideoEmbeddingItems, VideoProcessorItems),
403 )
404 frame_size = videos.get_frame_size(item_idx)
405 num_frames = videos.get_num_frames(item_idx)
406 return PromptUpdateDetails.select_token_ids(
407 cached_encode(
408 tokenizer,
409 self.get_video_prompt_texts(
410 frame_size,
411 num_frames,
412 downsample_mode=ds_mode,
413 video_idx=item_idx,
414 ),
415 add_special_tokens=False,
416 ),
417 video_embed_ids,
418 )
419
420 get_replacement = {
421 "image": get_image_replacement,
422 "video": get_video_replacement,
423 }
424
425 return [
426 PromptReplacement(
427 modality=modality,
428 target=cached_encode(tokenizer, pattern, add_special_tokens=False),
429 replacement=get_replacement[modality],
430 )
431 for modality, pattern in placeholders
432 ]
433
434 def _recompute_cached_prompt_update(
435 self, cached_update: ResolvedPromptUpdate, new_item_idx: int
436 ) -> ResolvedPromptUpdate:
437 new_update = super()._recompute_cached_prompt_update(
438 cached_update, new_item_idx
439 )
440 # MiniCPM-V 4.6 prefixes video placeholders with `<image_id>{idx}</image_id>`
441 # (the base class only rewrites the image modality).
442 if cached_update.modality == "video":
443 tokenizer = self.info.get_tokenizer()
444 id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
445 id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
446 video_token = getattr(tokenizer, "video_token", "<|video_pad|>")
447
448 text = tokenizer.decode(cached_update.content.full)
449 prev_item_idx = cached_update.item_idx
450
451 new_update = new_update.with_content(
452 PromptUpdateDetails.select_token_ids(
453 cached_encode(
454 tokenizer,
455 text.replace(
456 f"{id_start}{prev_item_idx}{id_end}",
457 f"{id_start}{new_item_idx}{id_end}",
458 1,
459 ),
460 add_special_tokens=False,
461 ),
462 cached_encode(tokenizer, video_token, add_special_tokens=False),
463 )
464 )
465 return new_update
466
467 def _get_mm_fields_config(
468 self,
469 hf_inputs,
470 hf_processor_mm_kwargs: Mapping[str, object],
471 ) -> Mapping[str, MultiModalFieldConfig]:
472 return _minicpmv4_6_field_config(hf_inputs)
473
474
475 class MiniCPMV4_6ProcessingInfo(MiniCPMVProcessingInfo):
476 # transformers v5.7+ chat_template emits these as image/video placeholders.
477 image_pattern = "<|image_pad|>"
478 video_pattern = "<|video_pad|>"
479
480 def get_hf_config(self):
481 return self.ctx.get_hf_config()
482
483 def get_hf_processor(self, **kwargs: object):
484 # MiniCPM-V 4.6 keeps the native transformers MiniCPMV4_6Processor:
485 # this model has its own image/video handling and prompt-update logic
486 # below, so it does not need (and is incompatible with) the vendored
487 # MiniCPMVProcessor used by 2.x/4.0/4.5, whose __init__ assumes a
488 # legacy `image_processor.version` attribute that 4.6 no longer has.
489 hf_processor = self.ctx.get_hf_processor(**kwargs)
490
491 # NumPy arrays are considered as Iterable but not Sequence in
492 # https://github.com/huggingface/transformers/blob/main/src/transformers/image_transforms.py#L428
493 image_processor = getattr(hf_processor, "image_processor", None)
494 if image_processor is not None:
495 # transformers v5+ renamed `mean`/`std` -> `image_mean`/`image_std`
496 for attr in ("mean", "std", "image_mean", "image_std"):
497 val = getattr(image_processor, attr, None)
498 if isinstance(val, np.ndarray):
499 setattr(image_processor, attr, val.tolist())
500
501 return hf_processor
502
503 def _get_expected_hidden_size(self) -> int:
504 config = self.get_hf_config()
505 if hasattr(config, "text_config") and config.text_config is not None:
506 return config.text_config.hidden_size
507 return config.hidden_size
508
509 def get_model_version(self):
510 return (4, 6)
511
512 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
513 return {"image": None, "video": None}
514
515 def get_image_max_slice_num(self) -> int:
516 config = self.get_hf_config()
517 if hasattr(config, "slice_config") and config.slice_config is not None:
518 return getattr(config.slice_config, "max_slice_nums", 9)
519 return getattr(config, "max_slice_nums", 9)
520
521 def get_video_max_slice_num(self) -> int:
522 # Override the base class default of 1: transformers v5.7+
523 # `MiniCPMV4_6VideoProcessor` keeps the same max_slice_nums (default 9)
524 # as the image processor so that high-res frames get sliced.
525 try:
526 hf_processor = self.get_hf_processor()
527 video_processor = getattr(hf_processor, "video_processor", None)
528 if video_processor is not None:
529 return int(getattr(video_processor, "max_slice_nums", 9))
530 except Exception:
531 pass
532 return self.get_image_max_slice_num()
533
534 def _get_downsample_mode(
535 self,
536 downsample_mode: str | None = None,
537 ) -> str:
538 if downsample_mode is not None:
539 return downsample_mode
540 image_processor = self.get_image_processor()
541 return getattr(image_processor, "downsample_mode", "16x")
542
543 def _compute_visual_tokens(
544 self,
545 image_size: ImageSize,
546 max_slice_nums: int | None = None,
547 downsample_mode: str | None = None,
548 ) -> tuple[list[int], int, int]:
549 """Compute grid, source_image_visual_tokens and patch_visual_tokens.
550
551 Args:
552 image_size: Size of the source image.
553 max_slice_nums: Maximum number of slices, or None for the default.
554 downsample_mode: ``"16x"`` (default, full merge) or ``"4x"``
555 (skip vit_merger, 4x more visual tokens).
556
557 Returns:
558 (grids, source_image_visual_tokens, patch_visual_tokens)
559 grids is [0, 0] when no slicing occurs.
560
561 """
562 image_processor = self.get_image_processor()
563 if max_slice_nums is None:
564 max_slice_nums = image_processor.max_slice_nums
565
566 patch_size = image_processor.patch_size
567 scale_res = image_processor.scale_resolution
568 downsample_mode = self._get_downsample_mode(downsample_mode)
569 token_divisor = 4 if downsample_mode == "4x" else 16
570
571 # vLLM ImageSize is (width, height); transformers expects (height, width)
572 hf_image_size = (image_size.height, image_size.width)
573
574 # transformers v5.7+ requires `scale_resolution` arg
575 try:
576 grids = image_processor.get_sliced_grid(
577 hf_image_size,
578 max_slice_nums,
579 scale_res,
580 )
581 except TypeError:
582 grids = image_processor.get_sliced_grid(
583 hf_image_size,
584 max_slice_nums,
585 )
586
587 if grids is None:
588 best_size = image_processor.find_best_resize(
589 hf_image_size,
590 scale_res,
591 patch_size,
592 allow_upscale=True,
593 )
594 source_tokens = (
595 best_size[0] * best_size[1] // (patch_size * patch_size * token_divisor)
596 )
597 return [0, 0], source_tokens, 0
598
599 best_resize = image_processor.find_best_resize(
600 hf_image_size,
601 scale_res,
602 patch_size,
603 )
604 source_tokens = (
605 best_resize[0] * best_resize[1] // (patch_size * patch_size * token_divisor)
606 )
607 refine_size = image_processor.get_refine_size(
608 hf_image_size,
609 grids,
610 scale_res,
611 patch_size,
612 allow_upscale=True,
613 )
614 patch_w = refine_size[0] // grids[0]
615 patch_h = refine_size[1] // grids[1]
616 patch_tokens = patch_w * patch_h // (patch_size * patch_size * token_divisor)
617 return grids, source_tokens, patch_tokens
618
619 def get_slice_image_placeholder(
620 self,
621 image_size,
622 image_idx: int = 0,
623 max_slice_nums: int | None = None,
624 use_image_id: bool = True,
625 downsample_mode: str | None = None,
626 ) -> str:
627 grids, source_tokens, patch_tokens = self._compute_visual_tokens(
628 image_size,
629 max_slice_nums,
630 downsample_mode=downsample_mode,
631 )
632 image_processor = self.get_image_processor()
633 # transformers v5.7+ removed `get_slice_image_placeholder` from the
634 # image_processor and moved the logic into MiniCPMV4_6Processor.
635 # Replicate it here using tokenizer special tokens.
636 if hasattr(image_processor, "get_slice_image_placeholder"):
637 return image_processor.get_slice_image_placeholder(
638 grids,
639 image_idx=image_idx,
640 max_slice_nums=max_slice_nums,
641 use_image_id=use_image_id,
642 source_image_visual_tokens=source_tokens,
643 patch_visual_tokens=patch_tokens,
644 )
645 tokenizer = self.get_tokenizer()
646 image_token = getattr(tokenizer, "image_token", "<|image_pad|>")
647 image_start = getattr(tokenizer, "image_start_token", "<image>")
648 image_end = getattr(tokenizer, "image_end_token", "</image>")
649 slice_start = getattr(tokenizer, "slice_start_token", "<slice>")
650 slice_end = getattr(tokenizer, "slice_end_token", "</slice>")
651 id_start = getattr(tokenizer, "image_id_start_token", "<image_id>")
652 id_end = getattr(tokenizer, "image_id_end_token", "</image_id>")
653
654 placeholder = image_start + image_token * source_tokens + image_end
655 if use_image_id:
656 placeholder = f"{id_start}{image_idx}{id_end}" + placeholder
657
658 num_rows, num_cols = grids[0], grids[1]
659 if num_cols > 0 and num_rows > 0 and patch_tokens > 0:
660 slice_ph = slice_start + image_token * patch_tokens + slice_end
661 slices = [slice_ph * num_cols for _ in range(num_rows)]
662 placeholder += "\n".join(slices)
663 return placeholder
664
665 def get_num_image_tokens(
666 self,
667 image_size,
668 max_slice_nums: int | None = None,
669 downsample_mode: str | None = None,
670 ) -> int:
671 grids, source_tokens, patch_tokens = self._compute_visual_tokens(
672 image_size,
673 max_slice_nums,
674 downsample_mode=downsample_mode,
675 )
676 return source_tokens + grids[0] * grids[1] * patch_tokens
677
678
679 class MiniCPMV4_6ViTWindowAttentionSelfAttn(nn.Module):
680 def __init__(
681 self,
682 config,
683 quant_config: QuantizationConfig | None = None,
684 prefix: str = "",
685 ):
686 super().__init__()
687 use_data_parallel = is_vit_use_data_parallel()
688 self.embed_dim = config.hidden_size
689 self.num_heads = config.num_attention_heads
690 self.head_dim = self.embed_dim // self.num_heads
691 self.scale = self.head_dim**-0.5
692
693 tp_size = 1 if use_data_parallel else get_tensor_model_parallel_world_size()
694 assert self.num_heads % tp_size == 0
695 self.num_heads_per_partition = self.num_heads // tp_size
696
697 self.qkv_proj = QKVParallelLinear(
698 self.embed_dim,
699 self.head_dim,
700 self.num_heads,
701 quant_config=quant_config,
702 prefix=f"{prefix}.qkv_proj",
703 disable_tp=use_data_parallel,
704 )
705 self.out_proj = RowParallelLinear(
706 self.embed_dim,
707 self.embed_dim,
708 bias=True,
709 quant_config=quant_config,
710 prefix=f"{prefix}.out_proj",
711 disable_tp=use_data_parallel,
712 )
713 self.attn = MMEncoderAttention(
714 self.num_heads_per_partition,
715 self.head_dim,
716 self.scale,
717 prefix=f"{prefix}.attn",
718 )
719
720 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
721 qkv, _ = self.qkv_proj(hidden_states)
722 q, k, v = qkv.chunk(3, dim=-1)
723 attn_out = self.attn(q, k, v)
724 out, _ = self.out_proj(attn_out)
725 return out
726
727
728 class MiniCPMV4_6ViTWindowAttentionMerger(nn.Module):
729 def __init__(
730 self,
731 config,
732 quant_config: QuantizationConfig | None = None,
733 prefix: str = "",
734 ):
735 super().__init__()
736 self.window_kernel_size = (2, 2)
737 self.embed_dim = config.hidden_size
738
739 self.self_attn = MiniCPMV4_6ViTWindowAttentionSelfAttn(
740 config,
741 quant_config=quant_config,
742 prefix=f"{prefix}.self_attn",
743 )
744 self.layer_norm1 = nn.LayerNorm(
745 self.embed_dim,
746 eps=config.layer_norm_eps,
747 )
748
749 hidden_4x = self.embed_dim * 4
750 inter_4x = config.intermediate_size * 4
751
752 self.pre_norm = nn.LayerNorm(hidden_4x, eps=config.layer_norm_eps)
753 self.linear_1 = nn.Linear(hidden_4x, inter_4x, bias=True)
754 self.act = get_act_fn("gelu_pytorch_tanh")
755 self.linear_2 = nn.Linear(inter_4x, self.embed_dim, bias=True)
756
757 def _apply_window_attention(
758 self,
759 valid_states: torch.Tensor,
760 H: int,
761 W: int,
762 ) -> torch.Tensor:
763 D = valid_states.shape[-1]
764 wh, ww = self.window_kernel_size
765 nh, nw = H // wh, W // ww
766 num_windows = nh * nw
767
768 x = valid_states.view(H, W, D)
769 x = x.view(nh, wh, nw, ww, D).permute(0, 2, 1, 3, 4).contiguous()
770 x = x.view(num_windows, wh * ww, D)
771
772 x = self.self_attn(x)
773
774 x = x.view(nh, nw, wh, ww, D).permute(0, 2, 1, 3, 4).contiguous()
775 return x.view(H * W, D)
776
777 def _apply_mlp_downsample(
778 self,
779 valid_states: torch.Tensor,
780 H: int,
781 W: int,
782 ) -> torch.Tensor:
783 D = valid_states.shape[-1]
784 wh, ww = self.window_kernel_size
785 nh, nw = H // wh, W // ww
786
787 x = valid_states.view(H, W, D)
788 x = x.view(nh, wh, nw, ww, D).permute(0, 2, 1, 3, 4).contiguous()
789
790 residual = x.reshape(nh * nw, wh * ww, D).mean(dim=1)
791 x = x.reshape(nh * nw, wh * ww * D)
792
793 x = self.pre_norm(x)
794 x = self.linear_1(x)
795 x = self.act(x)
796 x = self.linear_2(x)
797 return x + residual
798
799 def forward(
800 self,
801 hidden_states: torch.Tensor,
802 tgt_sizes: torch.Tensor,
803 attention_mask: torch.Tensor | None,
804 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
805 B, _L, D = hidden_states.shape
806 device = hidden_states.device
807 dtype = hidden_states.dtype
808
809 all_merged = []
810 new_tgt_sizes = torch.zeros_like(tgt_sizes)
811
812 for b in range(B):
813 H, W = tgt_sizes[b].tolist()
814 hs = hidden_states[b, : H * W, :]
815
816 residual = hs
817 hs = self.layer_norm1(hs)
818 hs = residual + self._apply_window_attention(hs, H, W)
819
820 wh, ww = self.window_kernel_size
821 new_H, new_W = H // wh, W // ww
822 all_merged.append(self._apply_mlp_downsample(hs, H, W))
823 new_tgt_sizes[b] = torch.tensor(
824 [new_H, new_W],
825 device=device,
826 dtype=tgt_sizes.dtype,
827 )
828
829 new_num_patches = new_tgt_sizes[:, 0] * new_tgt_sizes[:, 1]
830 new_max_patches = int(new_num_patches.max().item())
831 new_hidden = torch.zeros(
832 B,
833 new_max_patches,
834 D,
835 device=device,
836 dtype=dtype,
837 )
838 for b, merged in enumerate(all_merged):
839 new_hidden[b, : merged.shape[0], :] = merged
840
841 # Build new attention mask after spatial downsampling
842 new_attention_mask: torch.Tensor | None = None
843 if attention_mask is not None:
844 mask = torch.zeros(
845 B,
846 new_max_patches,
847 dtype=torch.bool,
848 device=device,
849 )
850 for b in range(B):
851 mask[b, : int(new_num_patches[b].item())] = True
852 min_val = torch.finfo(dtype).min
853 new_attention_mask = (~mask).to(dtype=dtype) * min_val
854 new_attention_mask = new_attention_mask[:, None, None, :]
855
856 return new_hidden, new_tgt_sizes, new_attention_mask
857
858
859 class MiniCPMV4_6DownsampleMLP(nn.Module):
860 """Match HF (transformers v5.7+) parameter naming: pre_norm/linear_1/
861 act/linear_2 (instead of pre_norm + Sequential(mlp.0/mlp.2))."""
862
863 def __init__(
864 self,
865 hidden_size: int,
866 llm_embed_dim: int,
867 merge_kernel_size: tuple[int, int] = (2, 2),
868 ):
869 super().__init__()
870 self.merge_kernel_size = merge_kernel_size
871 self.hidden_size = hidden_size * merge_kernel_size[0] * merge_kernel_size[1]
872 self.pre_norm = nn.LayerNorm(self.hidden_size, eps=1e-6)
873 self.linear_1 = nn.Linear(self.hidden_size, self.hidden_size, bias=True)
874 self.act = get_act_fn("gelu")
875 self.linear_2 = nn.Linear(self.hidden_size, llm_embed_dim, bias=True)
876
877 def forward(self, x: torch.Tensor) -> torch.Tensor:
878 x = self.pre_norm(x)
879 x = self.linear_1(x)
880 x = self.act(x)
881 x = self.linear_2(x)
882 return x
883
884
885 class MiniCPMV4_6Merger(nn.Module):
886 def __init__(
887 self,
888 hidden_size: int,
889 llm_embed_dim: int,
890 merge_kernel_size: tuple[int, int] = (2, 2),
891 times: int = 1,
892 ):
893 super().__init__()
894 self.merge_kernel_size = merge_kernel_size
895 self.times = times
896 self.mlp = nn.ModuleList(
897 [
898 MiniCPMV4_6DownsampleMLP(
899 hidden_size,
900 llm_embed_dim if i == times - 1 else hidden_size,
901 merge_kernel_size,
902 )
903 for i in range(times)
904 ]
905 )
906
907 def forward(
908 self,
909 hidden_states: torch.Tensor,
910 tgt_sizes: torch.Tensor,
911 ) -> list[torch.Tensor]:
912 """Args:
913 hidden_states: (B, max_patches, D) padded batch.
914 tgt_sizes: (B, 2) actual (H, W) per sample.
915
916 """
917 m1, m2 = self.merge_kernel_size
918 results = []
919
920 for b in range(len(tgt_sizes)):
921 h, w = tgt_sizes[b].tolist()
922 n_patches = h * w
923 hs = hidden_states[b, :n_patches, :]
924
925 hs = hs.reshape(h // m1, m1, w // m2, m2, -1)
926 hs = hs.permute(0, 2, 1, 3, 4).reshape(
927 (h // m1) * (w // m2),
928 m1 * m2 * hs.shape[-1],
929 )
930 hs = self.mlp[0](hs)
931
932 if self.times > 1:
933 cur_h, cur_w = h // m1, w // m2
934 for t in range(1, self.times):
935 cur_h, cur_w = cur_h // m1, cur_w // m2
936 hs = hs.reshape(cur_h, m1, cur_w, m2, -1)
937 hs = hs.permute(0, 2, 1, 3, 4).reshape(
938 cur_h * cur_w,
939 m1 * m2 * hs.shape[-1],
940 )
941 hs = self.mlp[t](hs)
942
943 results.append(hs)
944
945 return results
946
947
948 @MULTIMODAL_REGISTRY.register_processor(
949 MiniCPMV4_6MultiModalProcessor,
950 info=MiniCPMV4_6ProcessingInfo,
951 dummy_inputs=MiniCPMVDummyInputsBuilder,
952 )
953 class MiniCPMV4_6ForConditionalGeneration(
954 nn.Module,
955 SupportsMultiModal,
956 SupportsLoRA,
957 SupportsPP,
958 HasInnerState,
959 IsHybrid,
960 SupportsMRoPE,
961 ):
962 supports_encoder_tp_data = True
963
964 hf_to_vllm_mapper = WeightsMapper(
965 orig_to_new_stacked={
966 ".q_proj": (".qkv_proj", "q"),
967 ".k_proj": (".qkv_proj", "k"),
968 ".v_proj": (".qkv_proj", "v"),
969 },
970 orig_to_new_prefix={
971 # transformers v5.7+ uses `vision_tower` and nests `vit_merger`
972 # inside it. Order matters: more specific prefix must come first.
973 "model.vision_tower.vit_merger.": "vit_merger.",
974 "model.vision_tower.": "vpm.",
975 "model.vpm.": "vpm.",
976 "model.vit_merger.": "vit_merger.",
977 "model.merger.": "merger.",
978 "model.language_model.": "language_model.model.",
979 "lm_head.": "language_model.lm_head.",
980 "mtp.": None,
981 },
982 )
983
984 packed_modules_mapping = {
985 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
986 "gate_up_proj": ["gate_proj", "up_proj"],
987 "in_proj_qkvz": ["in_proj_qkv", "in_proj_z"],
988 "in_proj_ba": ["in_proj_b", "in_proj_a"],
989 }
990
991 @classmethod
992 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
993 # transformers v5.7+ chat_template uses these tokens.
994 if modality.startswith("image"):
995 return "<|image_pad|>"
996 if modality.startswith("video"):
997 return "<|video_pad|>"
998 raise ValueError("Only image or video modality is supported")
999
1000 def get_mrope_input_positions(
1001 self,
1002 input_tokens: list[int],
1003 mm_features: list["MultiModalFeatureSpec"],
1004 ) -> tuple[torch.Tensor, int]:
1005 """MiniCPM-V uses embedding injection for vision, not spatial M-RoPE.
1006
1007 All tokens (text and vision placeholders) get identical sequential
1008 positions duplicated across the 3 M-RoPE channels expected by the
1009 Qwen3.5 backbone.
1010 """
1011 seq_len = len(input_tokens)
1012 positions = torch.arange(seq_len).unsqueeze(0).expand(3, -1)
1013 return positions, 0
1014
1015 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1016 super().__init__()
1017 config: MiniCPMV4_6Config = vllm_config.model_config.hf_config
1018 quant_config = vllm_config.quant_config
1019 multimodal_config = vllm_config.model_config.multimodal_config
1020 assert multimodal_config is not None
1021
1022 self.config = config
1023 self.multimodal_config = multimodal_config
1024 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1025
1026 # --- Vision tower ---
1027 with self._mark_tower_model(vllm_config, {"image"}):
1028 self.vpm = Idefics2VisionTransformer(
1029 config.vision_config,
1030 quant_config=quant_config,
1031 apply_encoder_attention_mask=True,
1032 prefix=maybe_prefix(prefix, "vpm"),
1033 )
1034 if config.drop_vision_last_layer:
1035 self.vpm.encoder.layers = self.vpm.encoder.layers[:-1]
1036
1037 self.vit_merger = MiniCPMV4_6ViTWindowAttentionMerger(
1038 config.vision_config,
1039 quant_config=quant_config,
1040 prefix=maybe_prefix(prefix, "vit_merger"),
1041 )
1042 self.merger = MiniCPMV4_6Merger(
1043 hidden_size=config.vision_config.hidden_size,
1044 llm_embed_dim=config.text_config.hidden_size,
1045 )
1046
1047 # --- Language model ---
1048 # Temporarily swap top-level model_type so that Qwen3_5ForCausalLM
1049 # picks up the expected text config when introspecting the hf config.
1050 with self._mark_language_model(vllm_config):
1051 saved_model_type = config.model_type
1052 config.model_type = "qwen3_5_text"
1053 try:
1054 self.language_model = Qwen3_5ForCausalLM(
1055 vllm_config=vllm_config,
1056 prefix=maybe_prefix(prefix, "language_model"),
1057 )
1058 finally:
1059 config.model_type = saved_model_type
1060
1061 self.make_empty_intermediate_tensors = (
1062 self.language_model.make_empty_intermediate_tensors
1063 )
1064
1065 # ----- Multimodal parsing -----
1066
1067 def _parse_and_validate_vision_input(
1068 self,
1069 **kwargs: object,
1070 ) -> MiniCPMVImagePixelInputs | MiniCPMVImageEmbeddingInputs | None:
1071 pixel_values = kwargs.pop("pixel_values", None)
1072 image_embeds = kwargs.pop("image_embeds", None)
1073
1074 if pixel_values is None and image_embeds is None:
1075 return None
1076
1077 if image_embeds is not None:
1078 return MiniCPMVImageEmbeddingInputs(
1079 type="image_embeds",
1080 image_embeds=image_embeds,
1081 )
1082
1083 assert isinstance(pixel_values, torch.Tensor | list)
1084 tgt_sizes = kwargs.pop("tgt_sizes")
1085 num_slices_flat = torch.tensor([len(ps) for ps in pixel_values])
1086 pixel_values_flat = flatten_bn(pixel_values)
1087 tgt_sizes_flat = flatten_bn(tgt_sizes, concat=True)
1088
1089 return MiniCPMVImagePixelInputs(
1090 type="pixel_values",
1091 pixel_values=pixel_values_flat,
1092 tgt_sizes=tgt_sizes_flat,
1093 num_slices=num_slices_flat,
1094 )
1095
1096 # ----- Vision forward -----
1097
1098 def get_vision_hidden_states(
1099 self,
1100 data: MiniCPMVImagePixelInputs,
1101 downsample_mode: str | None = None,
1102 ) -> list[torch.Tensor]:
1103 pixel_values = data["pixel_values"]
1104 tgt_sizes = data["tgt_sizes"]
1105
1106 B = len(pixel_values)
1107 P = pixel_values[0].shape[-2]
1108 L = max(item.shape[-1] for item in pixel_values)
1109 device = pixel_values[0].device
1110 target_dtype = self.vpm.embeddings.patch_embedding.weight.dtype
1111
1112 all_pixel_values = torch.zeros(
1113 B,
1114 3,
1115 P,
1116 L,
1117 dtype=target_dtype,
1118 device=device,
1119 )
1120 for i, pv in enumerate(pixel_values):
1121 all_pixel_values[i, ..., : pv.shape[-1]] = pv.to(target_dtype)
1122
1123 num_patches = tgt_sizes.prod(-1)
1124 max_patches = int(num_patches.max().item())
1125 patch_attn_mask = torch.zeros(
1126 B,
1127 max_patches,
1128 dtype=torch.bool,
1129 device=device,
1130 )
1131 for i in range(B):
1132 patch_attn_mask[i, : num_patches[i]] = True
1133
1134 hidden_states = self.vpm.embeddings(
1135 all_pixel_values,
1136 patch_attention_mask=patch_attn_mask.unsqueeze(1),
1137 tgt_sizes=tgt_sizes,
1138 )
1139
1140 if torch.any(~patch_attn_mask):
1141 mask_dtype = hidden_states.dtype
1142 min_val = torch.finfo(mask_dtype).min
1143 attention_mask = (~patch_attn_mask).to(dtype=mask_dtype) * min_val
1144 attention_mask = attention_mask[:, None, None, :]
1145 else:
1146 attention_mask = None
1147
1148 # Encoder layers with mid-encoder merger injection
1149 insert_layer_id = getattr(self.config, "insert_layer_id", -1)
1150 if downsample_mode is None:
1151 downsample_mode = getattr(self.config, "downsample_mode", "16x")
1152 use_vit_merger = downsample_mode != "4x" and insert_layer_id >= 0
1153
1154 for layer in self.vpm.encoder.layers[: insert_layer_id + 1]:
1155 hidden_states = layer(hidden_states, attention_mask=attention_mask)
1156
1157 if use_vit_merger:
1158 hidden_states, tgt_sizes, attention_mask = self.vit_merger(
1159 hidden_states,
1160 tgt_sizes,
1161 attention_mask,
1162 )
1163
1164 for layer in self.vpm.encoder.layers[insert_layer_id + 1 :]:
1165 hidden_states = layer(hidden_states, attention_mask=attention_mask)
1166
1167 # 4. Post layernorm
1168 hidden_states = self.vpm.post_layernorm(hidden_states)
1169
1170 # 5. MLP merger → list of per-slice tensors
1171 return self.merger(hidden_states, tgt_sizes)
1172
1173 def _process_vision_input(self, image_input, use_vit_merger=None):
1174 if image_input["type"] == "image_embeds":
1175 return image_input["image_embeds"]
1176
1177 downsample_mode = None
1178 if use_vit_merger is not None:
1179 downsample_mode = "16x" if use_vit_merger else "4x"
1180 image_features = self.get_vision_hidden_states(
1181 image_input,
1182 downsample_mode=downsample_mode,
1183 )
1184 num_slices = image_input["num_slices"]
1185 results = []
1186 idx = 0
1187 for n in num_slices.tolist():
1188 group = image_features[idx : idx + n]
1189 results.append(torch.cat(group, dim=0))
1190 idx += n
1191 return results
1192
1193 # ----- Multimodal embedding interface -----
1194
1195 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
1196 use_vit_merger_tensors = kwargs.pop("use_vit_merger", None)
1197 use_vit_merger = None
1198 if use_vit_merger_tensors is not None:
1199 if isinstance(use_vit_merger_tensors, torch.Tensor):
1200 use_vit_merger = bool(use_vit_merger_tensors.any().item())
1201 elif isinstance(use_vit_merger_tensors, list | tuple):
1202 use_vit_merger = any(
1203 bool(t.any().item()) if isinstance(t, torch.Tensor) else bool(t)
1204 for t in use_vit_merger_tensors
1205 )
1206
1207 image_kwargs = {
1208 k: v
1209 for k, v in kwargs.items()
1210 if k in ("pixel_values", "image_embeds", "tgt_sizes")
1211 }
1212 video_kwargs = _image_kwargs_from_video(kwargs)
1213
1214 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1215
1216 if (
1217 image_kwargs.get("pixel_values") is not None
1218 or image_kwargs.get("image_embeds") is not None
1219 ):
1220 image_input = self._parse_and_validate_vision_input(**image_kwargs)
1221 if image_input is not None:
1222 multimodal_embeddings += tuple(
1223 self._process_vision_input(
1224 image_input,
1225 use_vit_merger=use_vit_merger,
1226 )
1227 )
1228
1229 if (
1230 video_kwargs.get("pixel_values") is not None
1231 or video_kwargs.get("image_embeds") is not None
1232 ):
1233 video_input = self._parse_and_validate_vision_input(**video_kwargs)
1234 if video_input is not None:
1235 multimodal_embeddings += tuple(
1236 self._process_vision_input(
1237 video_input,
1238 use_vit_merger=use_vit_merger,
1239 )
1240 )
1241
1242 if not multimodal_embeddings:
1243 return []
1244 return multimodal_embeddings
1245
1246 def embed_input_ids(
1247 self,
1248 input_ids: torch.Tensor,
1249 multimodal_embeddings: MultiModalEmbeddings | None = None,
1250 *,
1251 is_multimodal: torch.Tensor | None = None,
1252 ) -> torch.Tensor:
1253 inputs_embeds = self._embed_text_input_ids(
1254 input_ids,
1255 self.language_model.embed_input_ids,
1256 is_multimodal=is_multimodal,
1257 )
1258 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
1259 return inputs_embeds
1260
1261 is_multimodal = _require_is_multimodal(is_multimodal)
1262 return _merge_multimodal_embeddings(
1263 inputs_embeds=inputs_embeds,
1264 multimodal_embeddings=multimodal_embeddings,
1265 is_multimodal=is_multimodal,
1266 )
1267
1268 # ----- Forward / Logits -----
1269
1270 def forward(
1271 self,
1272 input_ids: torch.Tensor | None,
1273 positions: torch.Tensor,
1274 intermediate_tensors: IntermediateTensors | None = None,
1275 inputs_embeds: torch.Tensor | None = None,
1276 **kwargs: Any,
1277 ) -> torch.Tensor:
1278 if intermediate_tensors is not None:
1279 inputs_embeds = None
1280
1281 return self.language_model.model(
1282 input_ids=input_ids,
1283 positions=positions,
1284 intermediate_tensors=intermediate_tensors,
1285 inputs_embeds=inputs_embeds,
1286 )
1287
1288 def compute_logits(
1289 self,
1290 hidden_states: torch.Tensor,
1291 ) -> torch.Tensor | None:
1292 return self.language_model.compute_logits(hidden_states)
1293
1294 # ----- Weight loading -----
1295
1296 def load_weights(
1297 self,
1298 weights: Iterable[tuple[str, torch.Tensor]],
1299 ) -> set[str]:
1300 loader = AutoWeightsLoader(self)
1301 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1302
1303 def get_mm_mapping(self) -> MultiModelKeys:
1304 return MultiModelKeys.from_string_field(
1305 language_model="language_model",
1306 connector=["vit_merger", "merger"],
1307 tower_model="vpm",
1308 )
1309
1310 # ----- Mamba / Hybrid state helpers (same as Qwen3.5 VLM) -----
1311
1312 @classmethod
1313 def get_mamba_state_dtype_from_config(cls, vllm_config):
1314 return MambaStateDtypeCalculator.gated_delta_net_state_dtype(
1315 vllm_config.model_config.dtype,
1316 vllm_config.cache_config.mamba_cache_dtype,
1317 vllm_config.cache_config.mamba_ssm_cache_dtype,
1318 )
1319
1320 @classmethod
1321 def get_mamba_state_shape_from_config(cls, vllm_config):
1322 parallel_config = vllm_config.parallel_config
1323 hf_config = vllm_config.model_config.hf_text_config
1324 tp_size = parallel_config.tensor_parallel_size
1325 num_spec = (
1326 vllm_config.speculative_config.num_speculative_tokens
1327 if vllm_config.speculative_config
1328 else 0
1329 )
1330 return MambaStateShapeCalculator.gated_delta_net_state_shape(
1331 tp_size,
1332 hf_config.linear_num_key_heads,
1333 hf_config.linear_num_value_heads,
1334 hf_config.linear_key_head_dim,
1335 hf_config.linear_value_head_dim,
1336 hf_config.linear_conv_kernel_dim,
1337 num_spec,
1338 )
1339
1340 @classmethod
1341 def get_mamba_state_copy_func(cls):
1342 return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()