A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 15, 08:30 UTC
[VLM] Initialize video input support for InternVL models (#18499)
Signed-off-by: Isotr0py <2037008807@qq.com> Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
Isotr0py committed
May 25, 2025 at 12:51 UTC
75f81750f3a9071b23f7f2d5c9f9f1c2cd0091b1
10 files changed
+596
-62
docs/models/supported_models.md
+4
-1
@@ -527,7 +527,7 @@ Specified using `--task generate`.
527
| `GraniteSpeechForConditionalGeneration` | Granite Speech | T + A | `ibm-granite/granite-speech-3.3-8b` | ✅︎ | ✅︎ | ✅︎ |
528
| `H2OVLChatModel` | H2OVL | T + I<sup>E+</sup> | `h2oai/h2ovl-mississippi-800m`, `h2oai/h2ovl-mississippi-2b`, etc. | ✅︎ | ✅︎\* | |
529
| `Idefics3ForConditionalGeneration` | Idefics3 | T + I | `HuggingFaceM4/Idefics3-8B-Llama3` etc. | ✅︎ | ✅︎ | |
530
-| `InternVLChatModel` | InternVL 3.0, InternVideo 2.5, InternVL 2.5, Mono-InternVL, InternVL 2.0 | T + I<sup>E+</sup> | `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/Mono-InternVL-2B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ | |
530
+| `InternVLChatModel` | InternVL 3.0, InternVideo 2.5, InternVL 2.5, Mono-InternVL, InternVL 2.0 | T + I<sup>E+</sup> + (V<sup>E+</sup>) | `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/Mono-InternVL-2B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ | |
531
| `KimiVLForConditionalGeneration` | Kimi-VL-A3B-Instruct, Kimi-VL-A3B-Thinking | T + I<sup>+</sup> | `moonshotai/Kimi-VL-A3B-Instruct`, `moonshotai/Kimi-VL-A3B-Thinking` | ✅︎ | | |
532
| `Llama4ForConditionalGeneration` | Llama 4 | T + I<sup>+</sup> | `meta-llama/Llama-4-Scout-17B-16E-Instruct`, `meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8`, `meta-llama/Llama-4-Maverick-17B-128E-Instruct`, etc. | ✅︎ | ✅︎ | |
533
| `LlavaForConditionalGeneration` | LLaVA-1.5 | T + I<sup>E+</sup> | `llava-hf/llava-1.5-7b-hf`, `TIGER-Lab/Mantis-8B-siglip-llama3` (see note), etc. | ✅︎ | ✅︎ | |
@@ -577,6 +577,9 @@ Specified using `--task generate`.
577
578
This limitation exists because the model's mixed attention pattern (bidirectional for images, causal otherwise) is not yet supported by vLLM's attention backends.
579
580
+!!! note
581
+ Only `InternVLChatModel` with Qwen2.5 text backbone (`OpenGVLab/InternVL3-2B`, `OpenGVLab/InternVL2.5-1B` etc) has video inputs support currently.
582
+
583
!!! note
584
`h2oai/h2ovl-mississippi-2b` will be available in V1 once we support head size 80.
585
examples/offline_inference/vision_language.py
+11
-4
@@ -330,22 +330,26 @@ def run_smolvlm(questions: list[str], modality: str) -> ModelRequestData:
330
331
# InternVL
332
def run_internvl(questions: list[str], modality: str) -> ModelRequestData:
333
- assert modality == "image"
333
335
- model_name = "OpenGVLab/InternVL2-2B"
334
+ model_name = "OpenGVLab/InternVL3-2B"
335
336
engine_args = EngineArgs(
337
model=model_name,
338
trust_remote_code=True,
340
- max_model_len=4096,
339
+ max_model_len=8192,
340
limit_mm_per_prompt={modality: 1},
341
)
342
343
+ if modality == "image":
344
+ placeholder = "<image>"
345
+ elif modality == "video":
346
+ placeholder = "<video>"
347
+
348
tokenizer = AutoTokenizer.from_pretrained(model_name,
349
trust_remote_code=True)
350
messages = [[{
351
'role': 'user',
348
- 'content': f"<image>\n{question}"
352
+ 'content': f"{placeholder}\n{question}"
353
}] for question in questions]
354
prompts = tokenizer.apply_chat_template(messages,
355
tokenize=False,
@@ -357,6 +361,9 @@ def run_internvl(questions: list[str], modality: str) -> ModelRequestData:
361
# https://huggingface.co/OpenGVLab/InternVL2-2B/blob/main/conversation.py
362
stop_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>", "<|end|>"]
363
stop_token_ids = [tokenizer.convert_tokens_to_ids(i) for i in stop_tokens]
364
+ stop_token_ids = [
365
+ token_id for token_id in stop_token_ids if token_id is not None
366
+ ]
367
368
return ModelRequestData(
369
engine_args=engine_args,
tests/models/multimodal/generation/test_common.py
+11
@@ -349,6 +349,17 @@ VLM_TEST_SETTINGS = {
349
use_tokenizer_eos=True,
350
patch_hf_runner=model_utils.internvl_patch_hf_runner,
351
),
352
+ "intern_vl-video": VLMTestInfo(
353
+ models=[
354
+ "OpenGVLab/InternVL3-1B",
355
+ ],
356
+ test_type=VLMTestType.VIDEO,
357
+ prompt_formatter=lambda img_prompt: f"<|im_start|>User\n{img_prompt}<|im_end|>\n<|im_start|>Assistant\n", # noqa: E501
358
+ video_idx_to_prompt=lambda idx: "<video>",
359
+ max_model_len=8192,
360
+ use_tokenizer_eos=True,
361
+ patch_hf_runner=model_utils.internvl_patch_hf_runner,
362
+ ),
363
"kimi_vl": VLMTestInfo(
364
models=["moonshotai/Kimi-VL-A3B-Instruct"],
365
test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
tests/models/multimodal/generation/vlm_utils/model_utils.py
+66
-20
@@ -7,6 +7,8 @@ import types
7
from pathlib import PosixPath
8
from typing import Optional, Union
9
10
+import numpy as np
11
+import numpy.typing as npt
12
import regex as re
13
import torch
14
from PIL.Image import Image
@@ -495,30 +497,74 @@ def internvl_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
497
self.max_num = self.config.max_dynamic_patch
498
self.image_size = self.vision_config.image_size
499
498
- def __call__(self, text: str, images: Union[Image, list[Image]],
499
- **kwargs):
500
+ def __call__(
501
+ self,
502
+ text: str,
503
+ images: Union[Image, list[Image]] = None,
504
+ videos: Union[npt.NDArray, list[npt.NDArray]] = None,
505
+ **kwargs,
506
+ ):
507
from vllm.model_executor.models.internvl import (
508
IMG_CONTEXT, IMG_END, IMG_START,
502
- image_to_pixel_values_internvl)
509
+ image_to_pixel_values_internvl, video_to_pixel_values_internvl)
510
images = [images] if isinstance(images, Image) else images
504
- pixel_values = [
505
- image_to_pixel_values_internvl(
506
- image,
507
- input_size=self.image_size,
508
- min_num=self.min_num,
509
- max_num=self.max_num,
510
- use_thumbnail=self.use_thumbnail,
511
- ) for image in images
512
- ]
513
- num_patches_list = [
514
- pixel_value.shape[0] for pixel_value in pixel_values
515
- ]
511
+ videos = [videos] if isinstance(videos, np.ndarray) else videos
512
+ if images is not None:
513
+ pixel_values_images = [
514
+ image_to_pixel_values_internvl(
515
+ image,
516
+ input_size=self.image_size,
517
+ min_num=self.min_num,
518
+ max_num=self.max_num,
519
+ use_thumbnail=self.use_thumbnail,
520
+ ) for image in images
521
+ ]
522
+ num_patches_images = [
523
+ pixel_value.shape[0] for pixel_value in pixel_values_images
524
+ ]
525
+ else:
526
+ pixel_values_images, num_patches_images = [], []
527
+
528
+ if videos is not None:
529
+ pixel_values_videos = [
530
+ video_to_pixel_values_internvl(
531
+ video,
532
+ input_size=self.image_size,
533
+ min_num=1,
534
+ max_num=1,
535
+ use_thumbnail=False,
536
+ ) for video in videos
537
+ ]
538
+ num_patches_videos = [
539
+ pixel_value.shape[0] for pixel_value in pixel_values_videos
540
+ ]
541
+ else:
542
+ pixel_values_videos, num_patches_videos = [], []
543
+
544
+ pixel_values = []
545
+ while ("<image>" in text) or ("<video>" in text):
546
+ image_index = text.find("<image>")
547
+ video_index = text.find("<video>")
548
+ if image_index == -1 or (video_index > -1
549
+ and video_index < image_index):
550
+ num_patches = num_patches_videos.pop(0)
551
+ pixel_values.append(pixel_values_videos.pop(0))
552
+ context_tokens = IMG_START + \
553
+ IMG_CONTEXT * self.num_image_token + IMG_END
554
+ video_tokens = ''.join([
555
+ f'Frame{i+1}: {context_tokens}'
556
+ for i in range(num_patches)
557
+ ])
558
+ text = text.replace('<video>', video_tokens, 1)
559
+ else:
560
+ num_patches = num_patches_images.pop(0)
561
+ pixel_values.append(pixel_values_images.pop(0))
562
+ context_tokens = IMG_CONTEXT * self.num_image_token \
563
+ * num_patches
564
+ image_tokens = IMG_START + context_tokens + IMG_END
565
+ text = text.replace('<image>', image_tokens, 1)
566
pixel_values = torch.cat(pixel_values, dim=0)
517
- for num_patches in num_patches_list:
518
- context_tokens = IMG_CONTEXT * self.num_image_token \
519
- * num_patches
520
- image_tokens = IMG_START + context_tokens + IMG_END
521
- text = text.replace('<image>', image_tokens, 1)
567
+
568
prompt = self.tokenizer(text, return_tensors="pt")
569
prompt.update({"pixel_values": pixel_values})
570
return prompt
tests/models/multimodal/processing/test_common.py
+1
@@ -258,6 +258,7 @@ def _test_processing_correctness_mistral(
258
"ibm-granite/granite-speech-3.3-8b",
259
"h2oai/h2ovl-mississippi-800m",
260
"OpenGVLab/InternVL2-1B",
261
+ "OpenGVLab/InternVL3-1B",
262
"HuggingFaceM4/Idefics3-8B-Llama3",
263
"HuggingFaceTB/SmolVLM2-2.2B-Instruct",
264
"moonshotai/Kimi-VL-A3B-Instruct",
tests/models/registry.py
+2
-1
@@ -334,7 +334,8 @@ _MULTIMODAL_EXAMPLE_MODELS = {
334
max_transformers_version="4.48", # noqa: E501
335
transformers_version_reason="HF model is not compatible."), # noqa: E501
336
"InternVLChatModel": _HfExamplesInfo("OpenGVLab/InternVL2-1B",
337
- extras={"2B": "OpenGVLab/InternVL2-2B"}, # noqa: E501
337
+ extras={"2B": "OpenGVLab/InternVL2-2B",
338
+ "3.0": "OpenGVLab/InternVL3-1B"}, # noqa: E501
339
trust_remote_code=True),
340
"Idefics3ForConditionalGeneration": _HfExamplesInfo("HuggingFaceM4/Idefics3-8B-Llama3", # noqa: E501
341
{"tiny": "HuggingFaceTB/SmolVLM-256M-Instruct"}), # noqa: E501
vllm/entrypoints/chat_utils.py
+2
@@ -556,6 +556,8 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
556
return "(<audio>./</audio>)"
557
raise TypeError(f"Unknown model type: {model_type}")
558
elif modality == "video":
559
+ if model_type == "internvl_chat":
560
+ return "<video>"
561
if model_type in ("qwen2_vl", "qwen2_5_vl"):
562
return "<|vision_start|><|video_pad|><|vision_end|>"
563
if model_type == "qwen2_5_omni":
vllm/model_executor/models/h2ovl.py
+6
-5
@@ -25,9 +25,10 @@ from vllm.transformers_utils.tokenizer import AnyTokenizer
25
26
from .intern_vit import InternVisionModel
27
from .internvl import (IMG_CONTEXT, IMG_END, IMG_START,
28
+ BaseInternVLDummyInputsBuilder,
29
+ BaseInternVLMultiModalProcessor,
30
BaseInternVLProcessingInfo, BaseInternVLProcessor,
29
- InternVLChatModel, InternVLDummyInputsBuilder,
30
- InternVLMultiModalProcessor, build_transform,
31
+ InternVLChatModel, build_transform,
32
find_closest_aspect_ratio, get_internvl_target_ratios)
33
34
@@ -430,8 +431,8 @@ class H2OVLProcessingInfo(BaseInternVLProcessingInfo):
431
)
432
433
433
-class H2OVLMultiModalProcessor(InternVLMultiModalProcessor[H2OVLProcessingInfo]
434
- ):
434
+class H2OVLMultiModalProcessor(
435
+ BaseInternVLMultiModalProcessor[H2OVLProcessingInfo]):
436
437
def _get_prompt_updates(
438
self,
@@ -514,7 +515,7 @@ class H2OVLMultiModalProcessor(InternVLMultiModalProcessor[H2OVLProcessingInfo]
515
@MULTIMODAL_REGISTRY.register_processor(
516
H2OVLMultiModalProcessor,
517
info=H2OVLProcessingInfo,
517
- dummy_inputs=InternVLDummyInputsBuilder)
518
+ dummy_inputs=BaseInternVLDummyInputsBuilder)
519
class H2OVLChatModel(InternVLChatModel):
520
521
def _init_vision_model(
vllm/model_executor/models/internvl.py
+485
-26
@@ -8,8 +8,9 @@
8
# --------------------------------------------------------
9
from abc import ABC, abstractmethod
10
from collections.abc import Iterable, Mapping, Sequence
11
-from typing import Literal, Optional, TypedDict, TypeVar, Union
11
+from typing import Any, Literal, Optional, TypedDict, TypeVar, Union
12
13
+import numpy.typing as npt
14
import torch
15
import torch.nn as nn
16
import torchvision.transforms as T
@@ -74,6 +75,33 @@ InternVLImageInputs = Union[InternVLImagePixelInputs,
75
InternVLImageEmbeddingInputs]
76
77
78
+class InternVLVideoPixelInputs(TypedDict):
79
+ type: Literal["pixel_values_videos"]
80
+ pixel_values_flat: torch.Tensor
81
+ """
82
+ Shape:
83
+ `(batch_size * num_video * num_frames, num_channels, height, width)`
84
+ """
85
+
86
+ num_patches: torch.Tensor
87
+ """Shape: `(batch_size * num_images)`"""
88
+
89
+
90
+class InternVLVideoEmbeddingInputs(TypedDict):
91
+ type: Literal["video_embeds"]
92
+ data: Union[torch.Tensor, list[torch.Tensor]]
93
+ """
94
+ A tensor of shape `(num_videos, total_video_feature_size, hidden_size)`
95
+ or a list of tensors of shape `(total_video_feature_size, hidden_size)`
96
+
97
+ `hidden_size` must match the hidden size of language model backbone.
98
+ """
99
+
100
+
101
+InternVLVideoInputs = Union[InternVLVideoPixelInputs,
102
+ InternVLVideoEmbeddingInputs]
103
+
104
+
105
# adapted from https://huggingface.co/OpenGVLab/InternVL2-1B
106
def build_transform(input_size: int):
107
MEAN, STD = IMAGENET_MEAN, IMAGENET_STD
@@ -231,6 +259,33 @@ def image_to_pixel_values_internvl(
259
return pixel_values
260
261
262
+# adapted from https://huggingface.co/OpenGVLab/InternVL2-1B
263
+def video_to_pixel_values_internvl(
264
+ video: npt.NDArray,
265
+ *,
266
+ input_size: int,
267
+ min_num: int,
268
+ max_num: int,
269
+ use_thumbnail: bool,
270
+) -> torch.Tensor:
271
+ target_ratios = get_internvl_target_ratios(min_num, max_num)
272
+
273
+ transform = build_transform(input_size=input_size)
274
+ frames_list = list[Image.Image]()
275
+ for frame in video:
276
+ pil_frame = dynamic_preprocess_internvl(
277
+ Image.fromarray(frame, mode="RGB"),
278
+ target_ratios=target_ratios,
279
+ image_size=input_size,
280
+ use_thumbnail=use_thumbnail,
281
+ )
282
+ assert len(pil_frame) == 1
283
+ frames_list.extend(pil_frame)
284
+
285
+ pixel_values = torch.stack([transform(image) for image in frames_list])
286
+ return pixel_values
287
+
288
+
289
class BaseInternVLProcessor(ABC):
290
"""
291
This model doesn't define its own HF processor,
@@ -375,24 +430,14 @@ class BaseInternVLProcessor(ABC):
430
) for image in images
431
]
432
378
- def __call__(
433
+ def _preprocess_image(
434
self,
380
- text: Optional[Union[str, list[str]]] = None,
381
- images: Optional[Union[Image.Image, list[Image.Image]]] = None,
435
+ text: list[str],
436
+ images: list[Image.Image],
437
min_dynamic_patch: Optional[int] = None,
438
max_dynamic_patch: Optional[int] = None,
439
dynamic_image_size: Optional[bool] = None,
385
- return_tensors: Optional[Union[str, TensorType]] = None,
386
- ) -> Mapping[str, NestedTensors]:
387
- if text is None:
388
- text = []
389
- if not isinstance(text, list):
390
- text = [text]
391
- if images is None:
392
- images = []
393
- if not isinstance(images, list):
394
- images = [images]
395
-
440
+ ) -> tuple[list[str], dict[str, torch.Tensor]]:
441
if len(images) == 0:
442
image_inputs = {}
443
else:
@@ -415,6 +460,34 @@ class BaseInternVLProcessor(ABC):
460
461
image_repl = self.get_image_repl(feature_size, num_patches)
462
text = [t.replace('<image>', image_repl.full, 1) for t in text]
463
+ return text, image_inputs
464
+
465
+ def _make_batch_input(self,
466
+ input_item: Optional[Union[Any, list[Any]]] = None):
467
+ if input_item is None:
468
+ input_item = []
469
+ if not isinstance(input_item, list):
470
+ input_item = [input_item]
471
+ return input_item
472
+
473
+ def __call__(
474
+ self,
475
+ text: Optional[Union[str, list[str]]] = None,
476
+ images: Optional[Union[Image.Image, list[Image.Image]]] = None,
477
+ min_dynamic_patch: Optional[int] = None,
478
+ max_dynamic_patch: Optional[int] = None,
479
+ dynamic_image_size: Optional[bool] = None,
480
+ return_tensors: Optional[Union[str, TensorType]] = None,
481
+ ) -> Mapping[str, NestedTensors]:
482
+ text, images = [self._make_batch_input(x) for x in (text, images)]
483
+
484
+ text, image_inputs = self._preprocess_image(
485
+ text=text,
486
+ images=images,
487
+ min_dynamic_patch=min_dynamic_patch,
488
+ max_dynamic_patch=max_dynamic_patch,
489
+ dynamic_image_size=dynamic_image_size,
490
+ )
491
492
text_inputs = self.tokenizer(text)
493
@@ -425,11 +498,133 @@ class BaseInternVLProcessor(ABC):
498
499
500
class InternVLProcessor(BaseInternVLProcessor):
501
+ """
502
+ HF Processor for InternVLChatModel with extended video processing logic.
503
+
504
+ Code for video processing is adapted from video example:
505
+ https://huggingface.co/OpenGVLab/InternVL3-1B#inference-with-transformers
506
+ """
507
+
508
+ def __init__(
509
+ self,
510
+ config: PretrainedConfig,
511
+ tokenizer: AnyTokenizer,
512
+ *,
513
+ min_dynamic_patch: Optional[int] = None,
514
+ max_dynamic_patch: Optional[int] = None,
515
+ dynamic_image_size: Optional[bool] = None,
516
+ video_token: Optional[str] = None,
517
+ ) -> None:
518
+ super().__init__(
519
+ config=config,
520
+ tokenizer=tokenizer,
521
+ min_dynamic_patch=min_dynamic_patch,
522
+ max_dynamic_patch=max_dynamic_patch,
523
+ dynamic_image_size=dynamic_image_size,
524
+ )
525
+ # add extra video token for video processing
526
+ self.video_token = video_token
527
528
@property
529
def image_token_id(self) -> int:
530
return self.tokenizer.get_vocab()[IMG_CONTEXT]
531
532
+ @property
533
+ def video_token_id(self) -> Optional[int]:
534
+ if self.video_token is None:
535
+ return None
536
+ return self.tokenizer.get_vocab().get(self.video_token, None)
537
+
538
+ @property
539
+ def supports_video(self) -> bool:
540
+ return self.video_token_id is not None
541
+
542
+ def _videos_to_pixel_values_lst(
543
+ self,
544
+ videos: list[npt.NDArray],
545
+ dynamic_image_size: Optional[bool] = None,
546
+ ) -> list[torch.Tensor]:
547
+ min_num, max_num = self.resolve_min_max_num(
548
+ min_dynamic_patch=1,
549
+ max_dynamic_patch=1,
550
+ dynamic_image_size=dynamic_image_size,
551
+ use_thumbnail=False, # Applied in image_to_pixel_values
552
+ )
553
+
554
+ return [
555
+ video_to_pixel_values_internvl(
556
+ video,
557
+ input_size=self.image_size,
558
+ min_num=min_num,
559
+ max_num=max_num,
560
+ use_thumbnail=False,
561
+ ) for video in videos
562
+ ]
563
+
564
+ def _preprocess_video(
565
+ self,
566
+ text: list[str],
567
+ videos: list[npt.NDArray],
568
+ dynamic_image_size: Optional[bool] = None,
569
+ ):
570
+ if len(videos) == 0 or not self.supports_video:
571
+ video_inputs = {}
572
+ else:
573
+ pixel_values_lst_video = self._videos_to_pixel_values_lst(
574
+ videos,
575
+ dynamic_image_size=dynamic_image_size,
576
+ )
577
+ video_inputs: dict[str, NestedTensors] = {
578
+ "pixel_values_flat_video":
579
+ torch.cat(pixel_values_lst_video),
580
+ "video_num_patches":
581
+ torch.tensor([len(item) for item in pixel_values_lst_video]),
582
+ }
583
+
584
+ for pixel_values in pixel_values_lst_video:
585
+ num_patches = pixel_values.shape[0]
586
+
587
+ video_repl = self.get_video_repl(self.num_image_token,
588
+ num_patches, self.video_token)
589
+ text = [t.replace('<video>', video_repl.full, 1) for t in text]
590
+ return text, video_inputs
591
+
592
+ def __call__(
593
+ self,
594
+ text: Optional[Union[str, list[str]]] = None,
595
+ images: Optional[Union[Image.Image, list[Image.Image]]] = None,
596
+ videos: Optional[Union[npt.NDArray, list[npt.NDArray]]] = None,
597
+ min_dynamic_patch: Optional[int] = None,
598
+ max_dynamic_patch: Optional[int] = None,
599
+ dynamic_image_size: Optional[bool] = None,
600
+ return_tensors: Optional[Union[str, TensorType]] = None,
601
+ ) -> Mapping[str, NestedTensors]:
602
+ text, images, videos = [
603
+ self._make_batch_input(x) for x in (text, images, videos)
604
+ ]
605
+
606
+ text, image_inputs = self._preprocess_image(
607
+ text=text,
608
+ images=images,
609
+ min_dynamic_patch=min_dynamic_patch,
610
+ max_dynamic_patch=max_dynamic_patch,
611
+ dynamic_image_size=dynamic_image_size,
612
+ )
613
+
614
+ text, video_inputs = self._preprocess_video(
615
+ text=text,
616
+ videos=videos,
617
+ dynamic_image_size=dynamic_image_size,
618
+ )
619
+
620
+ text_inputs = self.tokenizer(text)
621
+
622
+ return {
623
+ **BatchEncoding(text_inputs, tensor_type=return_tensors),
624
+ **image_inputs,
625
+ **video_inputs,
626
+ }
627
+
628
def get_image_repl(
629
self,
630
feature_size: int,
@@ -440,8 +635,24 @@ class InternVLProcessor(BaseInternVLProcessor):
635
636
return PromptUpdateDetails.select_text(repl_full, IMG_CONTEXT)
637
638
+ def get_video_repl(
639
+ self,
640
+ feature_size: int,
641
+ num_patches: Optional[int] = None,
642
+ video_context_token: str = IMG_CONTEXT,
643
+ ) -> PromptUpdateDetails[str]:
644
+ repl_features = video_context_token * self.num_image_token
645
+ repl_features_with_sep = IMG_START + repl_features + IMG_END
646
+ # num_patches is equal to num_frames
647
+ repl_full = ''.join([
648
+ f'Frame{i+1}: {repl_features_with_sep}' for i in range(num_patches)
649
+ ])
650
+
651
+ return PromptUpdateDetails.select_text(repl_full, video_context_token)
652
+
653
654
class BaseInternVLProcessingInfo(BaseProcessingInfo):
655
+ """Basic image-only ProcessingInfo for InternVL-style models."""
656
657
@abstractmethod
658
def get_hf_processor(
@@ -497,11 +708,22 @@ class BaseInternVLProcessingInfo(BaseProcessingInfo):
708
709
return largest_feature_pinpoint
710
711
+ def get_max_image_tokens(self) -> int:
712
+ processor = self.get_hf_processor()
713
+ target_width, target_height = self.get_image_size_with_most_features()
714
+
715
+ return self.get_num_image_tokens(
716
+ image_width=target_width,
717
+ image_height=target_height,
718
+ processor=processor,
719
+ )
720
+
721
722
_I = TypeVar("_I", bound=BaseInternVLProcessingInfo)
723
724
504
-class InternVLDummyInputsBuilder(BaseDummyInputsBuilder[_I]):
725
+class BaseInternVLDummyInputsBuilder(BaseDummyInputsBuilder[_I]):
726
+ """Basic image-only DummyInputsBuilder for InternVL-style models."""
727
728
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
729
num_images = mm_counts.get("image", 0)
@@ -525,7 +747,8 @@ class InternVLDummyInputsBuilder(BaseDummyInputsBuilder[_I]):
747
}
748
749
528
-class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
750
+class BaseInternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
751
+ """ Basic image-only MultiModalProcessor for InternVL-style models."""
752
753
def _call_hf_processor(
754
self,
@@ -614,6 +837,38 @@ class InternVLMultiModalProcessor(BaseMultiModalProcessor[_I]):
837
838
839
class InternVLProcessingInfo(BaseInternVLProcessingInfo):
840
+ """InternVL ProcessingInfo extended for video processing"""
841
+
842
+ @property
843
+ def supports_video(self):
844
+ return self.get_hf_processor().supports_video
845
+
846
+ def get_supported_mm_limits(self):
847
+ video_limit = {"video": None} if self.supports_video else {}
848
+ return {**super().get_supported_mm_limits(), **video_limit}
849
+
850
+ def get_video_token(self) -> Optional[str]:
851
+ text_model_type = self.get_hf_config().get_text_config().model_type
852
+ if text_model_type == "qwen2":
853
+ return "<|video_pad|>"
854
+ return None
855
+
856
+ def get_num_frames_with_most_features(
857
+ self,
858
+ seq_len: int,
859
+ mm_counts: Mapping[str, int],
860
+ ) -> int:
861
+ max_images = mm_counts.get("image", 0)
862
+ max_videos = mm_counts.get("video", 0)
863
+
864
+ processor = self.get_hf_processor()
865
+
866
+ max_image_tokens = self.get_max_image_tokens() * max_images
867
+ max_total_frames = (seq_len -
868
+ max_image_tokens) // processor.num_image_token
869
+ max_frames_per_video = max_total_frames // max(max_videos, 1)
870
+
871
+ return max(max_frames_per_video, 1)
872
873
def get_hf_processor(
874
self,
@@ -630,6 +885,8 @@ class InternVLProcessingInfo(BaseInternVLProcessingInfo):
885
if dynamic_image_size is not None:
886
kwargs["dynamic_image_size"] = dynamic_image_size
887
888
+ kwargs["video_token"] = self.get_video_token()
889
+
890
return self.ctx.init_processor(
891
InternVLProcessor,
892
config=self.get_hf_config(),
@@ -638,6 +895,121 @@ class InternVLProcessingInfo(BaseInternVLProcessingInfo):
895
)
896
897
898
+class InternVLDummyInputsBuilder(
899
+ BaseInternVLDummyInputsBuilder[InternVLProcessingInfo]):
900
+ """InternVL DummyInputsBuilder extended for video support"""
901
+
902
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
903
+ num_videos = mm_counts.get("video", 0)
904
+
905
+ return super().get_dummy_text(mm_counts) + "<video>" * num_videos
906
+
907
+ def get_dummy_mm_data(
908
+ self,
909
+ seq_len: int,
910
+ mm_counts: Mapping[str, int],
911
+ ) -> MultiModalDataDict:
912
+ dummy_image = super().get_dummy_mm_data(seq_len=seq_len,
913
+ mm_counts=mm_counts)
914
+ if self.info.supports_video:
915
+ config = self.info.get_hf_config()
916
+ image_size: int = config.vision_config.image_size
917
+ target_num_frames = \
918
+ self.info.get_num_frames_with_most_features(seq_len, mm_counts)
919
+ num_videos = mm_counts.get("video", 0)
920
+ dummy_video = {
921
+ "video":
922
+ self._get_dummy_videos(width=image_size,
923
+ height=image_size,
924
+ num_frames=target_num_frames,
925
+ num_videos=num_videos)
926
+ }
927
+ else:
928
+ dummy_video = {}
929
+ return {**dummy_image, **dummy_video}
930
+
931
+
932
+class InternVLMultiModalProcessor(
933
+ BaseInternVLMultiModalProcessor[InternVLProcessingInfo]):
934
+ """InternVL MultiModalProcessor extended for video support"""
935
+
936
+ def _call_hf_processor(
937
+ self,
938
+ prompt: str,
939
+ mm_data: Mapping[str, object],
940
+ mm_kwargs: Mapping[str, object],
941
+ ) -> Mapping[str, NestedTensors]:
942
+ processed_outputs = super()._call_hf_processor(prompt, mm_data,
943
+ mm_kwargs)
944
+
945
+ hf_processor = self.info.get_hf_processor(**mm_kwargs)
946
+ if self.info.supports_video and (
947
+ video_token_id := hf_processor.video_token_id) is not None:
948
+ processed_outputs["video_token_id"] = torch.tensor(video_token_id)
949
+ return processed_outputs
950
+
951
+ def _get_mm_fields_config(
952
+ self,
953
+ hf_inputs: Mapping[str, NestedTensors],
954
+ hf_processor_mm_kwargs: Mapping[str, object],
955
+ ) -> Mapping[str, MultiModalFieldConfig]:
956
+ image_fields = super()._get_mm_fields_config(hf_inputs,
957
+ hf_processor_mm_kwargs)
958
+ if self.info.supports_video:
959
+ video_num_patches = hf_inputs.get("video_num_patches",
960
+ torch.empty(0))
961
+ num_videos = len(video_num_patches)
962
+ video_fields = dict(
963
+ pixel_values_flat_video=MultiModalFieldConfig.flat_from_sizes(
964
+ "video", video_num_patches),
965
+ video_num_patches=MultiModalFieldConfig.batched("video"),
966
+ video_token_id=MultiModalFieldConfig.shared(
967
+ "video", num_videos),
968
+ )
969
+ else:
970
+ video_fields = {}
971
+
972
+ return image_fields | video_fields
973
+
974
+ def _get_prompt_updates(
975
+ self,
976
+ mm_items: MultiModalDataItems,
977
+ hf_processor_mm_kwargs: Mapping[str, object],
978
+ out_mm_kwargs: MultiModalKwargs,
979
+ ) -> Sequence[PromptUpdate]:
980
+ prompt_repl: list[PromptUpdate] = super()._get_prompt_updates(
981
+ mm_items, hf_processor_mm_kwargs, out_mm_kwargs)
982
+
983
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
984
+
985
+ if "video_num_patches" in out_mm_kwargs:
986
+ video_num_patches = out_mm_kwargs["video_num_patches"]
987
+ assert isinstance(video_num_patches, torch.Tensor)
988
+ video_num_patches = video_num_patches.tolist()
989
+ else:
990
+ video_num_patches = []
991
+
992
+ def get_video_replacement_internvl(item_idx: int):
993
+ feature_size = hf_processor.num_image_token
994
+ num_patches = video_num_patches[item_idx]
995
+ if num_patches is not None:
996
+ assert isinstance(num_patches, int)
997
+
998
+ return hf_processor.get_video_repl(
999
+ feature_size,
1000
+ num_patches,
1001
+ video_context_token=hf_processor.video_token)
1002
+
1003
+ if self.info.supports_video:
1004
+ prompt_repl.append(
1005
+ PromptReplacement(
1006
+ modality="video",
1007
+ target="<video>",
1008
+ replacement=get_video_replacement_internvl,
1009
+ ))
1010
+ return prompt_repl
1011
+
1012
+
1013
@MULTIMODAL_REGISTRY.register_processor(
1014
InternVLMultiModalProcessor,
1015
info=InternVLProcessingInfo,
@@ -681,6 +1053,8 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1053
self.mlp1 = self._init_mlp1(config)
1054
1055
self.img_context_token_id = None
1056
+ self.video_context_token_id = None
1057
+
1058
self.visual_token_mask = None
1059
self.make_empty_intermediate_tensors = (
1060
self.language_model.make_empty_intermediate_tensors)
@@ -825,10 +1199,55 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1199
1200
raise AssertionError("This line should be unreachable.")
1201
1202
+ def _parse_and_validate_video_input(
1203
+ self, **kwargs: object) -> Optional[InternVLVideoPixelInputs]:
1204
+ pixel_values_flat_video = kwargs.pop("pixel_values_flat_video", None)
1205
+ video_num_patches = kwargs.pop("video_num_patches", None)
1206
+ video_embeds = kwargs.pop("image_embeds", None)
1207
+
1208
+ if pixel_values_flat_video is None and video_embeds is None:
1209
+ return None
1210
+
1211
+ if video_embeds is not None:
1212
+ if not isinstance(video_embeds, (torch.Tensor, list)):
1213
+ raise ValueError("Incorrect type of video embeddings. "
1214
+ f"Got type: {type(video_embeds)}")
1215
+
1216
+ return InternVLImageEmbeddingInputs(
1217
+ type="video_embeds",
1218
+ data=flatten_bn(video_embeds),
1219
+ )
1220
+
1221
+ video_token_id = kwargs["video_token_id"]
1222
+ assert isinstance(video_token_id, torch.Tensor)
1223
+ self.video_context_token_id = video_token_id.flatten().unique().item()
1224
+
1225
+ if pixel_values_flat_video is not None:
1226
+ if not isinstance(pixel_values_flat_video, (torch.Tensor, list)):
1227
+ raise ValueError("Incorrect type of pixel values. "
1228
+ f"Got type: {type(pixel_values_flat_video)}")
1229
+
1230
+ if not isinstance(video_num_patches, (torch.Tensor, list)):
1231
+ raise ValueError("Incorrect type of image_num_patches. "
1232
+ f"Got type: {type(video_num_patches)}")
1233
+
1234
+ pixel_values_flat_video = flatten_bn(pixel_values_flat_video,
1235
+ concat=True)
1236
+ video_num_patches = flatten_bn(video_num_patches, concat=True)
1237
+
1238
+ return InternVLVideoPixelInputs(
1239
+ type="pixel_values_videos",
1240
+ pixel_values_flat=self._validate_pixel_values(
1241
+ pixel_values_flat_video),
1242
+ num_patches=video_num_patches,
1243
+ )
1244
+
1245
+ raise AssertionError("This line should be unreachable.")
1246
+
1247
def _process_image_input(
1248
self,
830
- image_input: InternVLImageInputs,
831
- ) -> Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor, ...]]:
1249
+ image_input: Union[InternVLImageInputs, InternVLVideoPixelInputs],
1250
+ ) -> tuple[torch.Tensor, ...]:
1251
if image_input["type"] == "image_embeds":
1252
return image_input["data"]
1253
@@ -840,8 +1259,8 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1259
1260
# Only one image in the current batch
1261
if len(num_patches) == 1:
843
- return image_embeds.view(
844
- -1, self.config.text_config.hidden_size).unsqueeze(0)
1262
+ return (image_embeds.view(-1,
1263
+ self.config.text_config.hidden_size), )
1264
1265
# NOTE: Image embeddings are split into separate tensors for each image
1266
# by the size of each embedding.
@@ -853,8 +1272,26 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1272
]
1273
return image_embeds.split(image_feature_sizes)
1274
1275
+ def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1276
+ modalities = {}
1277
+
1278
+ # Preserve the order of modalities if there are multiple of them
1279
+ # from the order of kwargs.
1280
+ for input_key in kwargs:
1281
+ if input_key in ("pixel_values_flat",
1282
+ "image_embeds") and "images" not in modalities:
1283
+ modalities["images"] = self._parse_and_validate_image_input(
1284
+ **kwargs)
1285
+ if input_key in ("pixel_values_flat_video",
1286
+ ) and "videos" not in modalities:
1287
+ modalities["videos"] = self._parse_and_validate_video_input(
1288
+ **kwargs)
1289
+
1290
+ return modalities
1291
+
1292
def _set_visual_token_mask(self, input_ids: torch.Tensor) -> None:
1293
if self.is_mono:
1294
+ assert self.img_context_token_id is not None
1295
self.visual_token_mask = (
1296
input_ids == self.img_context_token_id).reshape(-1, 1)
1297
else:
@@ -865,11 +1302,28 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1302
1303
def get_multimodal_embeddings(
1304
self, **kwargs: object) -> Optional[MultiModalEmbeddings]:
868
- image_input = self._parse_and_validate_image_input(**kwargs)
869
- if image_input is None:
1305
+
1306
+ modalities = self._parse_and_validate_multimodal_inputs(**kwargs)
1307
+ if not modalities:
1308
return None
1309
872
- return self._process_image_input(image_input)
1310
+ # The result multimodal_embeddings is tuple of tensors, with each
1311
+ # tensor correspoending to a multimodal data item (image or video).
1312
+ multimodal_embeddings: tuple[torch.Tensor, ...] = ()
1313
+
1314
+ # NOTE: It is important to iterate over the keys in this dictionary
1315
+ # to preserve the order of the modalities.
1316
+ for modality in modalities:
1317
+ if modality == "images":
1318
+ image_input = modalities["images"]
1319
+ vision_embeddings = self._process_image_input(image_input)
1320
+ multimodal_embeddings += vision_embeddings
1321
+ if modality == "videos":
1322
+ video_input = modalities["videos"]
1323
+ video_embeddings = self._process_image_input(video_input)
1324
+ multimodal_embeddings += video_embeddings
1325
+
1326
+ return multimodal_embeddings
1327
1328
def get_input_embeddings(
1329
self,
@@ -878,13 +1332,18 @@ class InternVLChatModel(nn.Module, SupportsMultiModal, SupportsPP):
1332
) -> torch.Tensor:
1333
inputs_embeds = self.language_model.get_input_embeddings(input_ids)
1334
if multimodal_embeddings is not None:
881
- assert self.img_context_token_id is not None
1335
+ context_token_ids = [
1336
+ token_id for token_id in (self.img_context_token_id,
1337
+ self.video_context_token_id)
1338
+ if token_id is not None
1339
+ ]
1340
+ assert len(context_token_ids) >= 1
1341
self._set_visual_token_mask(input_ids)
1342
inputs_embeds = merge_multimodal_embeddings(
1343
input_ids,
1344
inputs_embeds,
1345
multimodal_embeddings,
887
- self.img_context_token_id,
1346
+ context_token_ids,
1347
)
1348
return inputs_embeds
1349
vllm/model_executor/models/nvlm_d.py
+8
-5
@@ -22,9 +22,10 @@ from vllm.multimodal.processing import (PromptReplacement, PromptUpdate,
22
PromptUpdateDetails)
23
24
from .intern_vit import InternVisionModel
25
-from .internvl import (BaseInternVLProcessingInfo, BaseInternVLProcessor,
26
- InternVLChatModel, InternVLDummyInputsBuilder,
27
- InternVLMultiModalProcessor)
25
+from .internvl import (BaseInternVLDummyInputsBuilder,
26
+ BaseInternVLMultiModalProcessor,
27
+ BaseInternVLProcessingInfo, BaseInternVLProcessor,
28
+ InternVLChatModel)
29
30
IMG_PAD = "<|vision_pad|>"
31
@@ -84,7 +85,8 @@ class NVLMProcessingInfo(BaseInternVLProcessingInfo):
85
)
86
87
87
-class NVLMDummyInputsBuilder(InternVLDummyInputsBuilder[NVLMProcessingInfo]):
88
+class NVLMDummyInputsBuilder(BaseInternVLDummyInputsBuilder[NVLMProcessingInfo]
89
+ ):
90
91
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
92
num_images = mm_counts.get("image", 0)
@@ -110,7 +112,8 @@ class NVLMDummyInputsBuilder(InternVLDummyInputsBuilder[NVLMProcessingInfo]):
112
}
113
114
113
-class NVLMMultiModalProcessor(InternVLMultiModalProcessor[NVLMProcessingInfo]):
115
+class NVLMMultiModalProcessor(
116
+ BaseInternVLMultiModalProcessor[NVLMProcessingInfo]):
117
118
def _get_prompt_updates(
119
self,