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 14, 05:30 UTC
[Model]: support KeyeVL-1_5-8B (#23838)
Signed-off-by: wangruitao <wangruitao@kuaishou.com> Co-authored-by: wangruitao <wangruitao@kuaishou.com>
Kwai-Keye committed
Sep 1, 2025 at 18:50 UTC
7c8271cd1e85670bd60c058441e1a224421dea01
9 files changed
+980
-135
docs/models/supported_models.md
+2
-1
@@ -634,7 +634,8 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
634
| `InternS1ForConditionalGeneration` | Intern-S1 | T + I<sup>E+</sup> + V<sup>E+</sup> | `internlm/Intern-S1`, etc. | ✅︎ | ✅︎ | ✅︎ |
635
| `InternVLChatModel` | InternVL 3.5, 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_5-14B`, `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/Mono-InternVL-2B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ | ✅︎ |
636
| `InternVLForConditionalGeneration` | InternVL 3.0 (HF format) | T + I<sup>E+</sup> + V<sup>E+</sup> | `OpenGVLab/InternVL3-1B-hf`, etc. | ✅︎ | ✅︎ | ✅︎ |
637
-| `KeyeForConditionalGeneration` | Keye-VL-8B-Preview | T + I<sup>E+</sup> + V<sup>E+</sup> | `Kwai-Keye/Keye-VL-8B-Preview` | | | ✅︎ |
637
+| `KeyeForConditionalGeneration` | Keye-VL-8B-Preview | T + I<sup>E+</sup> + V<sup>E+</sup> | `Kwai-Keye/Keye-VL-8B-Preview` | ✅︎ | ✅︎ | ✅︎ |
638
+| `KeyeVL1_5ForConditionalGeneration` | Keye-VL-1_5-8B | T + I<sup>E+</sup> + V<sup>E+</sup> | `Kwai-Keye/Keye-VL-1_5-8B` | ✅︎ | ✅︎ | ✅︎ |
639
| `KimiVLForConditionalGeneration` | Kimi-VL-A3B-Instruct, Kimi-VL-A3B-Thinking | T + I<sup>+</sup> | `moonshotai/Kimi-VL-A3B-Instruct`, `moonshotai/Kimi-VL-A3B-Thinking` | | ✅︎ | ✅︎ |
640
| `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. | | ✅︎ | ✅︎ |
641
| `Llama_Nemotron_Nano_VL` | Llama Nemotron Nano VL | T + I<sup>E+</sup> | `nvidia/Llama-3.1-Nemotron-Nano-VL-8B-V1` | ✅︎ | ✅︎ | ✅︎ |
examples/offline_inference/vision_language.py
+32
@@ -683,6 +683,37 @@ def run_keye_vl(questions: list[str], modality: str) -> ModelRequestData:
683
)
684
685
686
+# Keye-VL-1.5
687
+def run_keye_vl1_5(questions: list[str], modality: str) -> ModelRequestData:
688
+ model_name = "Kwai-Keye/Keye-VL-1.5-8B"
689
+
690
+ engine_args = EngineArgs(
691
+ model=model_name,
692
+ max_model_len=8192,
693
+ trust_remote_code=True,
694
+ limit_mm_per_prompt={modality: 1},
695
+ )
696
+
697
+ if modality == "image":
698
+ placeholder = "<|image_pad|>"
699
+ elif modality == "video":
700
+ placeholder = "<|video_pad|>"
701
+
702
+ prompts = [
703
+ (
704
+ f"<|im_start|>user\n<|vision_start|>{placeholder}<|vision_end|>"
705
+ f"{question}<|im_end|>\n"
706
+ "<|im_start|>assistant\n"
707
+ )
708
+ for question in questions
709
+ ]
710
+
711
+ return ModelRequestData(
712
+ engine_args=engine_args,
713
+ prompts=prompts,
714
+ )
715
+
716
+
717
# Kimi-VL
718
def run_kimi_vl(questions: list[str], modality: str) -> ModelRequestData:
719
assert modality == "image"
@@ -1648,6 +1679,7 @@ model_example_map = {
1679
"interns1": run_interns1,
1680
"internvl_chat": run_internvl,
1681
"keye_vl": run_keye_vl,
1682
+ "keye_vl1_5": run_keye_vl1_5,
1683
"kimi_vl": run_kimi_vl,
1684
"llama4": run_llama4,
1685
"llava": run_llava,
examples/offline_inference/vision_language_multi_image.py
+38
@@ -542,6 +542,43 @@ def load_keye_vl(question: str, image_urls: list[str]) -> ModelRequestData:
542
)
543
544
545
+def load_keye_vl1_5(question: str, image_urls: list[str]) -> ModelRequestData:
546
+ model_name = "Kwai-Keye/Keye-VL-1_5-8B"
547
+
548
+ engine_args = EngineArgs(
549
+ model=model_name,
550
+ trust_remote_code=True,
551
+ max_model_len=8192,
552
+ max_num_seqs=5,
553
+ limit_mm_per_prompt={"image": len(image_urls)},
554
+ )
555
+
556
+ placeholders = [{"type": "image", "image": url} for url in image_urls]
557
+ messages = [
558
+ {
559
+ "role": "user",
560
+ "content": [
561
+ *placeholders,
562
+ {"type": "text", "text": question},
563
+ ],
564
+ },
565
+ ]
566
+
567
+ processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)
568
+
569
+ prompt = processor.apply_chat_template(
570
+ messages, tokenize=False, add_generation_prompt=True
571
+ )
572
+
573
+ image_data = [fetch_image(url) for url in image_urls]
574
+
575
+ return ModelRequestData(
576
+ engine_args=engine_args,
577
+ prompt=prompt,
578
+ image_data=image_data,
579
+ )
580
+
581
+
582
def load_kimi_vl(question: str, image_urls: list[str]) -> ModelRequestData:
583
model_name = "moonshotai/Kimi-VL-A3B-Instruct"
584
@@ -1209,6 +1246,7 @@ model_example_map = {
1246
"interns1": load_interns1,
1247
"internvl_chat": load_internvl,
1248
"keye_vl": load_keye_vl,
1249
+ "keye_vl1_5": load_keye_vl1_5,
1250
"kimi_vl": load_kimi_vl,
1251
"llama4": load_llama4,
1252
"llava": load_llava,
tests/models/multimodal/processing/test_common.py
+1
@@ -293,6 +293,7 @@ def _test_processing_correctness_one(
293
"OpenGVLab/InternVL3_5-GPT-OSS-20B-A4B-Preview",
294
"OpenGVLab/InternVL3_5-30B-A3B",
295
"Kwai-Keye/Keye-VL-8B-Preview",
296
+ "Kwai-Keye/Keye-VL-1_5-8B",
297
"moonshotai/Kimi-VL-A3B-Instruct",
298
"meta-llama/Llama-4-Scout-17B-16E-Instruct",
299
"llava-hf/llava-1.5-7b-hf",
tests/models/registry.py
+2
@@ -438,6 +438,8 @@ _MULTIMODAL_EXAMPLE_MODELS = {
438
"InternVLForConditionalGeneration": _HfExamplesInfo("OpenGVLab/InternVL3-1B-hf"), # noqa: E501
439
"KeyeForConditionalGeneration": _HfExamplesInfo("Kwai-Keye/Keye-VL-8B-Preview", # noqa: E501
440
trust_remote_code=True),
441
+ "KeyeVL1_5ForConditionalGeneration": _HfExamplesInfo("Kwai-Keye/Keye-VL-1_5-8B", # noqa: E501
442
+ trust_remote_code=True),
443
"KimiVLForConditionalGeneration": _HfExamplesInfo("moonshotai/Kimi-VL-A3B-Instruct", # noqa: E501
444
extras={"thinking": "moonshotai/Kimi-VL-A3B-Thinking"}, # noqa: E501
445
trust_remote_code=True),
vllm/model_executor/layers/rotary_embedding/mrope.py
+129
@@ -402,6 +402,15 @@ class MRotaryEmbedding(RotaryEmbedding):
402
context_len=context_len,
403
seq_len=seq_len,
404
)
405
+ elif "KeyeVL1_5" in hf_config.model_type:
406
+ return cls._keye_get_input_positions_tensor(
407
+ input_tokens=input_tokens,
408
+ hf_config=hf_config,
409
+ image_grid_thw=image_grid_thw,
410
+ video_grid_thw=video_grid_thw,
411
+ context_len=context_len,
412
+ seq_len=seq_len,
413
+ )
414
else:
415
return cls._vl_get_input_positions_tensor(
416
input_tokens=input_tokens,
@@ -636,6 +645,126 @@ class MRotaryEmbedding(RotaryEmbedding):
645
len(input_tokens)).item()
646
return llm_positions, mrope_position_delta
647
648
+ @classmethod
649
+ def _keye_get_input_positions_tensor(
650
+ cls,
651
+ input_tokens: list[int],
652
+ hf_config: PretrainedConfig,
653
+ image_grid_thw: Union[list[list[int]], torch.Tensor],
654
+ video_grid_thw: Union[list[list[int]], torch.Tensor],
655
+ context_len: int = 0,
656
+ seq_len: Optional[int] = None,
657
+ ) -> tuple[torch.Tensor, int]:
658
+ if isinstance(video_grid_thw, list) and len(video_grid_thw) > 0:
659
+ video_grid_thw = video_grid_thw[0]
660
+ """Get mrope input positions and delta value (Keye series)."""
661
+
662
+ def split_thw(
663
+ grid_thw: Union[torch.Tensor, list[int]]) -> list[list[int]]:
664
+ """
665
+ Split grid_thw along the t dimension.
666
+
667
+ Args:
668
+ grid_thw: shape [N, 3] tensor or nested list of [t, h, w].
669
+
670
+ Returns:
671
+ List of [1, h, w] rows, repeated t times for each original row.
672
+ """
673
+
674
+ if isinstance(grid_thw, list):
675
+ grid_thw = torch.tensor(grid_thw, dtype=torch.long)
676
+
677
+ if grid_thw.numel() == 0:
678
+ return []
679
+
680
+ t, hw = grid_thw[:, 0], grid_thw[:, 1:]
681
+ ones = torch.ones_like(hw[:, :1]) # [N,1]
682
+ out = torch.cat([ones, hw], dim=1).repeat_interleave(t, dim=0)
683
+ return out.tolist()
684
+
685
+ video_grid_thw = split_thw(video_grid_thw)
686
+
687
+ image_token_id = hf_config.image_token_id
688
+ video_token_id = hf_config.video_token_id
689
+ spatial_merge_size = hf_config.vision_config.spatial_merge_size
690
+
691
+ image_nums = len(image_grid_thw)
692
+ frame_nums = len(video_grid_thw)
693
+ llm_pos_ids_list: list = []
694
+
695
+ st = 0
696
+ remain_images, remain_frames = image_nums, frame_nums
697
+
698
+ image_index, video_index = 0, 0
699
+ for _ in range(image_nums + frame_nums):
700
+ if remain_images > 0:
701
+ try:
702
+ ed_image = input_tokens.index(image_token_id, st)
703
+ except ValueError:
704
+ ed_image = len(input_tokens) + 1
705
+ else:
706
+ ed_image = len(input_tokens) + 1
707
+ if remain_frames > 0:
708
+ try:
709
+ ed_video = input_tokens.index(video_token_id, st)
710
+ except ValueError:
711
+ ed_video = len(input_tokens) + 1
712
+ else:
713
+ ed_video = len(input_tokens) + 1
714
+
715
+ if ed_image < ed_video:
716
+ t, h, w = (
717
+ image_grid_thw[image_index][0],
718
+ image_grid_thw[image_index][1],
719
+ image_grid_thw[image_index][2],
720
+ )
721
+ image_index += 1
722
+ remain_images -= 1
723
+ ed = ed_image
724
+ else:
725
+ t, h, w = (
726
+ video_grid_thw[video_index][0],
727
+ video_grid_thw[video_index][1],
728
+ video_grid_thw[video_index][2],
729
+ )
730
+ video_index += 1
731
+ remain_frames -= 1
732
+ ed = ed_video
733
+
734
+ llm_grid_t, llm_grid_h, llm_grid_w = \
735
+ t, h // spatial_merge_size, w // spatial_merge_size
736
+ text_len = ed - st
737
+
738
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(
739
+ llm_pos_ids_list) > 0 else 0
740
+ llm_pos_ids_list.append(
741
+ torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
742
+
743
+ t_index = (torch.arange(llm_grid_t).view(-1, 1).expand(
744
+ -1, llm_grid_h * llm_grid_w)).long().flatten()
745
+
746
+ h_index = torch.arange(llm_grid_h).view(1, -1, 1).expand(
747
+ llm_grid_t, -1, llm_grid_w).flatten()
748
+ w_index = torch.arange(llm_grid_w).view(1, 1, -1).expand(
749
+ llm_grid_t, llm_grid_h, -1).flatten()
750
+ llm_pos_ids_list.append(
751
+ torch.stack([t_index, h_index, w_index]) + text_len + st_idx)
752
+ st = ed + llm_grid_t * llm_grid_h * llm_grid_w
753
+
754
+ if st < len(input_tokens):
755
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(
756
+ llm_pos_ids_list) > 0 else 0
757
+ text_len = len(input_tokens) - st
758
+ llm_pos_ids_list.append(
759
+ torch.arange(text_len).view(1, -1).expand(3, -1) + st_idx)
760
+
761
+ llm_positions = torch.cat(llm_pos_ids_list, dim=1).reshape(3, -1)
762
+ mrope_position_delta = (llm_positions.max() + 1 -
763
+ len(input_tokens)).item()
764
+ llm_positions = llm_positions[:, context_len:seq_len]
765
+
766
+ return llm_positions, mrope_position_delta
767
+
768
@classmethod
769
def _vl_get_input_positions_tensor(
770
cls,
vllm/model_executor/models/keye.py
+174
-134
@@ -1,9 +1,10 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import math
4
+from abc import abstractmethod
5
from collections.abc import Iterable, Mapping, Sequence
6
from functools import partial
6
-from typing import Annotated, Any, Literal, Optional, Union
7
+from typing import Annotated, Any, Literal, Optional, TypeVar, Union
8
9
import numpy as np
10
import torch
@@ -57,16 +58,13 @@ from .vision import get_vit_attn_backend
58
59
logger = init_logger(__name__)
60
60
-_MAX_FRAMES_PER_VIDEO = 16
61
-_MAX_IMAGE_SIZE = 9999999
62
-
61
62
def smart_resize(
63
height: int,
64
width: int,
67
- factor: int = 28,
68
- min_pixels: int = 28 * 28 * 130,
69
- max_pixels: int = 28 * 28 * 1280,
65
+ factor: int,
66
+ min_pixels: int,
67
+ max_pixels: int,
68
):
69
if height < factor:
70
logger.warning(
@@ -887,9 +885,9 @@ class Projector(nn.Module):
885
886
def forward(
887
self,
890
- image_features: torch.Tensor,
888
+ image_features: Union[torch.Tensor, list[torch.Tensor]],
889
image_grid_thw: list[tuple[int, int, int]],
892
- ) -> torch.Tensor:
890
+ ) -> Union[torch.Tensor, list[torch.Tensor]]:
891
m1, m2 = self.merge_kernel_size
892
if isinstance(image_features, (list, tuple)):
893
processed_features = list()
@@ -986,6 +984,12 @@ class KeyeMultiModalDataParser(MultiModalDataParser):
984
985
class KeyeProcessingInfo(BaseProcessingInfo):
986
987
+ def get_max_image_size(self) -> int:
988
+ return 9999999 #_MAX_IMAGE_SIZE
989
+
990
+ def get_max_frame_per_video(self) -> int:
991
+ return 16 #_MAX_FRAMES_PER_VIDEO
992
+
993
def get_image_processor(self, **kwargs: object):
994
return self.get_hf_processor(**kwargs).image_processor
995
@@ -1077,8 +1081,8 @@ class KeyeProcessingInfo(BaseProcessingInfo):
1081
1082
def get_image_size_with_most_features(self, ) -> ImageSize:
1083
max_image_size, _ = self._get_vision_info(
1080
- image_width=_MAX_IMAGE_SIZE,
1081
- image_height=_MAX_IMAGE_SIZE,
1084
+ image_width=self.get_max_image_size(),
1085
+ image_height=self.get_max_image_size(),
1086
image_processor=None,
1087
)
1088
return max_image_size
@@ -1123,7 +1127,7 @@ class KeyeProcessingInfo(BaseProcessingInfo):
1127
max_image_tokens)
1128
max_frames_per_video = min(
1129
max_total_frames // max(max_videos, 1),
1126
- _MAX_FRAMES_PER_VIDEO,
1130
+ self.get_max_frame_per_video(),
1131
)
1132
1133
return max(max_frames_per_video, 1)
@@ -1139,7 +1143,10 @@ class KeyeProcessingInfo(BaseProcessingInfo):
1143
)
1144
1145
1142
-class KeyeDummyInputsBuilder(BaseDummyInputsBuilder[KeyeProcessingInfo]):
1146
+_I = TypeVar("_I", bound=KeyeProcessingInfo)
1147
+
1148
+
1149
+class KeyeBaseDummyInputsBuilder(BaseDummyInputsBuilder[_I]):
1150
1151
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1152
num_images = mm_counts.get("image", 0)
@@ -1183,6 +1190,10 @@ class KeyeDummyInputsBuilder(BaseDummyInputsBuilder[KeyeProcessingInfo]):
1190
return mm_data
1191
1192
1193
+class KeyeDummyInputsBuilder(KeyeBaseDummyInputsBuilder[KeyeProcessingInfo]):
1194
+ ...
1195
+
1196
+
1197
class KeyeMultiModalProcessor(BaseMultiModalProcessor[KeyeProcessingInfo]):
1198
1199
def _get_data_parser(self) -> MultiModalDataParser:
@@ -1231,13 +1242,7 @@ class KeyeMultiModalProcessor(BaseMultiModalProcessor[KeyeProcessingInfo]):
1242
return _keye_field_config(hf_inputs)
1243
1244
1234
-@MULTIMODAL_REGISTRY.register_processor(
1235
- KeyeMultiModalProcessor,
1236
- info=KeyeProcessingInfo,
1237
- dummy_inputs=KeyeDummyInputsBuilder,
1238
-)
1239
-class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1240
- SupportsPP):
1245
+class BaseKeyeModule(nn.Module):
1246
packed_modules_mapping = {
1247
"qkv_proj": [
1248
"q_proj",
@@ -1264,6 +1269,11 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1269
1270
raise ValueError("Only image or video modality is supported")
1271
1272
+ def _maybe_ignore_quant_config(self, quant_config: QuantizationConfig):
1273
+ if isinstance(quant_config, (GPTQConfig, GPTQMarlinConfig)):
1274
+ return None
1275
+ return quant_config
1276
+
1277
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1278
super().__init__()
1279
config: PretrainedConfig = vllm_config.model_config.hf_config
@@ -1278,7 +1288,8 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1288
quant_config=self._maybe_ignore_quant_config(quant_config),
1289
prefix=maybe_prefix(prefix, "visual"),
1290
)
1281
- self.mlp_AR = Projector(
1291
+
1292
+ self.mlp_AR = self._build_projector(
1293
config,
1294
config.vision_config,
1295
quant_config=self._maybe_ignore_quant_config(quant_config),
@@ -1294,102 +1305,16 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1305
self.make_empty_intermediate_tensors = (
1306
self.language_model.make_empty_intermediate_tensors)
1307
1297
- def _maybe_ignore_quant_config(self, quant_config: QuantizationConfig):
1298
- if isinstance(quant_config, (GPTQConfig, GPTQMarlinConfig)):
1299
- return None
1300
- return quant_config
1301
-
1302
- def _validate_and_reshape_mm_tensor(self, mm_input: NestedTensors,
1303
- name: str) -> torch.Tensor:
1304
- if not isinstance(mm_input, (torch.Tensor, list)):
1305
- raise ValueError(f"Incorrect type of {name}. "
1306
- f"Got type: {type(mm_input)}")
1307
- if isinstance(mm_input, torch.Tensor):
1308
- if mm_input.ndim == 2:
1309
- return mm_input
1310
- if mm_input.ndim == 5:
1311
- return mm_input
1312
- if mm_input.ndim != 3:
1313
- raise ValueError(f"{name} should be 2D or batched 3D tensor. "
1314
- f"Got ndim: {mm_input.ndim} "
1315
- f"(shape={mm_input.shape})")
1316
- return torch.concat(list(mm_input))
1317
- elif is_list_of(mm_input, torch.Tensor):
1318
- if all(p.dim() == 4 for p in mm_input) or all(p.dim() == 2
1319
- for p in mm_input):
1320
- return mm_input
1321
- return torch.concat(list(mm_input))
1322
-
1323
- def _parse_and_validate_image_input(
1324
- self, **kwargs: object) -> Optional[KeyeImageInputs]:
1325
- pixel_values = kwargs.pop("pixel_values", None)
1326
- image_embeds = kwargs.pop("image_embeds", None)
1327
- image_grid_thw = kwargs.pop("image_grid_thw", None)
1328
-
1329
- if pixel_values is None and image_embeds is None:
1330
- return None
1331
-
1332
- if pixel_values is not None:
1333
- pixel_values = self._validate_and_reshape_mm_tensor(
1334
- pixel_values, "image pixel values")
1335
- image_grid_thw = self._validate_and_reshape_mm_tensor(
1336
- image_grid_thw, "image grid_thw")
1337
-
1338
- return KeyeImagePixelInputs(
1339
- type="pixel_values",
1340
- pixel_values=pixel_values,
1341
- image_grid_thw=image_grid_thw,
1342
- )
1343
-
1344
- if image_embeds is not None:
1345
- image_embeds = self._validate_and_reshape_mm_tensor(
1346
- image_embeds, "image embeds")
1347
- image_grid_thw = self._validate_and_reshape_mm_tensor(
1348
- image_grid_thw, "image grid_thw")
1349
-
1350
- return KeyeImageEmbeddingInputs(
1351
- type="image_embeds",
1352
- image_embeds=image_embeds,
1353
- image_grid_thw=image_grid_thw,
1354
- )
1355
-
1356
- def _parse_and_validate_video_input(
1357
- self, **kwargs: object) -> Optional[KeyeVideoInputs]:
1358
- pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1359
- video_embeds = kwargs.pop("video_embeds", None)
1360
- video_grid_thw = kwargs.pop("video_grid_thw", None)
1361
-
1362
- if pixel_values_videos is None and video_embeds is None:
1363
- return None
1364
-
1365
- if pixel_values_videos is not None:
1366
- pixel_values_videos = self._validate_and_reshape_mm_tensor(
1367
- pixel_values_videos,
1368
- "video pixel values",
1369
- )
1370
- video_grid_thw = self._validate_and_reshape_mm_tensor(
1371
- video_grid_thw, "video grid_thw")
1372
-
1373
- return KeyeVideoPixelInputs(
1374
- type="pixel_values_videos",
1375
- pixel_values_videos=pixel_values_videos,
1376
- video_grid_thw=video_grid_thw,
1377
- )
1378
-
1379
- if video_embeds is not None:
1380
- video_embeds = self._validate_and_reshape_mm_tensor(
1381
- video_embeds, "video embeds")
1382
- video_grid_thw = self._validate_and_reshape_mm_tensor(
1383
- video_grid_thw, "video grid_thw")
1384
-
1385
- return KeyeVideoEmbeddingInputs(
1386
- type="video_embeds",
1387
- video_embeds=video_embeds,
1388
- video_grid_thw=video_grid_thw,
1389
- )
1308
+ @abstractmethod
1309
+ def _build_projector(self,
1310
+ text_config: PretrainedConfig,
1311
+ vision_config: PretrainedConfig,
1312
+ quant_config: Optional[QuantizationConfig] = None,
1313
+ prefix: str = "") -> nn.Module:
1314
+ raise ValueError("Need projector")
1315
1391
- def _process_image_input(
1392
- self, image_input: KeyeImageInputs) -> tuple[torch.Tensor, ...]:
1316
+ def _process_image_input(self,
1317
+ image_input: Any) -> tuple[torch.Tensor, ...]:
1318
siglip_position_ids = list()
1319
image_grid_hws = list()
1320
sample_indices = list()
@@ -1434,18 +1359,20 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1359
image_embeds = tuple(self.mlp_AR(image_embeds, image_grid_thw))
1360
return image_embeds
1361
1437
- def _process_video_input(
1438
- self, video_input: KeyeVideoInputs) -> tuple[torch.Tensor, ...]:
1362
+ def _process_video_embeds(
1363
+ self,
1364
+ video_type: Literal["video_embeds", "pixel_values_videos"],
1365
+ video_grid_thw: list[torch.Tensor],
1366
+ pixel_values_videos: Optional[torch.Tensor] = None
1367
+ ) -> Union[torch.Tensor, list[torch.Tensor]]:
1368
siglip_position_ids = list()
1369
video_grid_hws = list()
1370
sample_indices = list()
1371
cu_seqlens = [0]
1372
1444
- video_grid_thw = video_input["video_grid_thw"]
1373
assert video_grid_thw.ndim == 2
1446
-
1447
- for idx, thaw in enumerate(video_grid_thw):
1448
- thw_tuple = tuple(thaw.detach().cpu().numpy().tolist())
1374
+ for idx, sub_thw in enumerate(video_grid_thw):
1375
+ thw_tuple = tuple(sub_thw.detach().cpu().numpy().tolist())
1376
numel = np.prod(thw_tuple)
1377
1378
video_grid_hws.append(thw_tuple)
@@ -1455,12 +1382,11 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1382
dtype=torch.int64))
1383
cu_seqlens.append(cu_seqlens[-1] + numel)
1384
1458
- if video_input["type"] == "video_embeds":
1385
+ if video_type == "video_embeds":
1386
raise ValueError(
1387
"Video embeddings are not supported for this processing path.")
1388
else:
1462
- pixel_values_videos = video_input["pixel_values_videos"].type(
1463
- self.visual.dtype)
1389
+ pixel_values_videos = pixel_values_videos.type(self.visual.dtype)
1390
siglip_position_ids = torch.concat(siglip_position_ids, dim=0).to(
1391
pixel_values_videos.device)
1392
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32).to(
@@ -1479,7 +1405,7 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1405
use_rope=True,
1406
window_size=-1,
1407
)
1482
- video_embeds = tuple(self.mlp_AR(video_embeds, video_grid_thw))
1408
+ video_embeds = self.mlp_AR(video_embeds, video_grid_thw)
1409
return video_embeds
1410
1411
def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
@@ -1541,8 +1467,8 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1467
def get_input_embeddings_v0(
1468
self,
1469
input_ids: torch.Tensor,
1544
- image_input: Optional[KeyeImagePixelInputs] = None,
1545
- video_input: Optional[KeyeVideoPixelInputs] = None,
1470
+ image_input: Optional[Any] = None,
1471
+ video_input: Optional[Any] = None,
1472
) -> torch.Tensor:
1473
inputs_embeds = self.get_input_embeddings(input_ids)
1474
if image_input is not None:
@@ -1572,7 +1498,7 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1498
inputs_embeds: Optional[torch.Tensor] = None,
1499
**kwargs: object,
1500
) -> Union[torch.Tensor, IntermediateTensors]:
1575
- """Run forward pass for Qwen2-VL.
1501
+ """Run forward pass for Keye-VL.
1502
1503
Args:
1504
input_ids: Flattened (concatenated) input_ids corresponding to a
@@ -1591,14 +1517,12 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1517
video_grid_thw: Tensor `(n_videos, 3)` of video 3D grid in LLM.
1518
`None` if no videos are passed.
1519
"""
1594
-
1520
if intermediate_tensors is not None:
1521
inputs_embeds = None
1522
1523
elif inputs_embeds is None:
1524
image_input = self._parse_and_validate_image_input(**kwargs)
1525
video_input = self._parse_and_validate_video_input(**kwargs)
1601
-
1526
if image_input is None and video_input is None:
1527
inputs_embeds = None
1528
else:
@@ -1619,6 +1543,7 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1543
intermediate_tensors=intermediate_tensors,
1544
inputs_embeds=inputs_embeds,
1545
)
1546
+
1547
return hidden_states
1548
1549
def compute_logits(
@@ -1631,7 +1556,6 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1556
1557
def load_weights(self, weights: Iterable[tuple[str,
1558
torch.Tensor]]) -> set[str]:
1634
-
1559
loader = AutoWeightsLoader(self)
1560
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1561
@@ -1639,6 +1563,122 @@ class KeyeForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLoRA,
1563
"""Get the module prefix in multimodal models."""
1564
return MultiModelKeys.from_string_field(
1565
language_model="language_model",
1642
- connector="visual.",
1643
- tower_model="mlp_AR.",
1566
+ connector="mlp_AR.",
1567
+ tower_model="visual.",
1568
)
1569
+
1570
+
1571
+@MULTIMODAL_REGISTRY.register_processor(
1572
+ KeyeMultiModalProcessor,
1573
+ info=KeyeProcessingInfo,
1574
+ dummy_inputs=KeyeDummyInputsBuilder,
1575
+)
1576
+class KeyeForConditionalGeneration(BaseKeyeModule, SupportsMultiModal,
1577
+ SupportsLoRA, SupportsPP):
1578
+
1579
+ def _build_projector(self,
1580
+ text_config: PretrainedConfig,
1581
+ vision_config: PretrainedConfig,
1582
+ quant_config: Optional[QuantizationConfig] = None,
1583
+ prefix: str = "") -> nn.Module:
1584
+ return Projector(text_config, vision_config, quant_config, prefix)
1585
+
1586
+ def _validate_and_reshape_mm_tensor(
1587
+ self, mm_input: NestedTensors,
1588
+ name: str) -> Union[torch.Tensor, list[torch.Tensor]]:
1589
+ if not isinstance(mm_input, (torch.Tensor, list)):
1590
+ raise ValueError(f"Incorrect type of {name}. "
1591
+ f"Got type: {type(mm_input)}")
1592
+ if isinstance(mm_input, torch.Tensor):
1593
+ if mm_input.ndim == 2:
1594
+ return mm_input
1595
+ if mm_input.ndim == 5:
1596
+ return mm_input
1597
+ if mm_input.ndim != 3:
1598
+ raise ValueError(f"{name} should be 2D or batched 3D tensor. "
1599
+ f"Got ndim: {mm_input.ndim} "
1600
+ f"(shape={mm_input.shape})")
1601
+ return torch.concat(list(mm_input))
1602
+ elif is_list_of(mm_input, torch.Tensor):
1603
+ if all(p.dim() == 4 for p in mm_input) or all(p.dim() == 2
1604
+ for p in mm_input):
1605
+ return mm_input
1606
+ return torch.concat(list(mm_input))
1607
+
1608
+ def _parse_and_validate_image_input(
1609
+ self, **kwargs: object) -> Optional[KeyeImageInputs]:
1610
+ pixel_values = kwargs.pop("pixel_values", None)
1611
+ image_embeds = kwargs.pop("image_embeds", None)
1612
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
1613
+
1614
+ if pixel_values is None and image_embeds is None:
1615
+ return None
1616
+
1617
+ if pixel_values is not None:
1618
+ pixel_values = self._validate_and_reshape_mm_tensor(
1619
+ pixel_values, "image pixel values")
1620
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1621
+ image_grid_thw, "image grid_thw")
1622
+
1623
+ return KeyeImagePixelInputs(
1624
+ type="pixel_values",
1625
+ pixel_values=pixel_values,
1626
+ image_grid_thw=image_grid_thw,
1627
+ )
1628
+
1629
+ if image_embeds is not None:
1630
+ image_embeds = self._validate_and_reshape_mm_tensor(
1631
+ image_embeds, "image embeds")
1632
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
1633
+ image_grid_thw, "image grid_thw")
1634
+
1635
+ return KeyeImageEmbeddingInputs(
1636
+ type="image_embeds",
1637
+ image_embeds=image_embeds,
1638
+ image_grid_thw=image_grid_thw,
1639
+ )
1640
+
1641
+ def _parse_and_validate_video_input(
1642
+ self, **kwargs: object) -> Optional[KeyeVideoInputs]:
1643
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1644
+ video_embeds = kwargs.pop("video_embeds", None)
1645
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
1646
+
1647
+ if pixel_values_videos is None and video_embeds is None:
1648
+ return None
1649
+
1650
+ if pixel_values_videos is not None:
1651
+ pixel_values_videos = self._validate_and_reshape_mm_tensor(
1652
+ pixel_values_videos,
1653
+ "video pixel values",
1654
+ )
1655
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1656
+ video_grid_thw, "video grid_thw")
1657
+
1658
+ return KeyeVideoPixelInputs(
1659
+ type="pixel_values_videos",
1660
+ pixel_values_videos=pixel_values_videos,
1661
+ video_grid_thw=video_grid_thw,
1662
+ )
1663
+
1664
+ if video_embeds is not None:
1665
+ video_embeds = self._validate_and_reshape_mm_tensor(
1666
+ video_embeds, "video embeds")
1667
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
1668
+ video_grid_thw, "video grid_thw")
1669
+
1670
+ return KeyeVideoEmbeddingInputs(
1671
+ type="video_embeds",
1672
+ video_embeds=video_embeds,
1673
+ video_grid_thw=video_grid_thw,
1674
+ )
1675
+
1676
+ def _process_video_input(
1677
+ self, video_input: KeyeVideoInputs) -> tuple[torch.Tensor, ...]:
1678
+ video_type = video_input["type"]
1679
+ video_grid_thw = video_input["video_grid_thw"]
1680
+ pixel_values_videos = video_input.get("pixel_values_videos", None)
1681
+
1682
+ return tuple(
1683
+ self._process_video_embeds(video_type, video_grid_thw,
1684
+ pixel_values_videos))
vllm/model_executor/models/keye_vl1_5.py
new
+601
@@ -0,0 +1,601 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import itertools
4
+from collections.abc import Mapping, Sequence
5
+from functools import partial
6
+from typing import Annotated, Any, Literal, Optional, Union
7
+
8
+import numpy as np
9
+import torch
10
+import torch.nn as nn
11
+from einops import rearrange
12
+from transformers import PretrainedConfig
13
+from transformers.activations import GELUActivation
14
+from transformers.feature_extraction_utils import BatchFeature
15
+
16
+from vllm.config import VllmConfig
17
+from vllm.logger import init_logger
18
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
19
+ RowParallelLinear)
20
+from vllm.model_executor.layers.quantization import QuantizationConfig
21
+from vllm.multimodal import MULTIMODAL_REGISTRY, NestedTensors
22
+from vllm.multimodal.inputs import (ImageItem, ModalityData,
23
+ MultiModalFieldConfig,
24
+ MultiModalKwargsItems, VideoItem)
25
+from vllm.multimodal.parse import (DictEmbeddingItems, ModalityDataItems,
26
+ MultiModalDataItems, MultiModalDataParser)
27
+from vllm.multimodal.processing import (PromptReplacement, PromptUpdate,
28
+ PromptUpdateDetails)
29
+from vllm.utils.tensor_schema import TensorSchema, TensorShape
30
+
31
+from .interfaces import SupportsLoRA, SupportsMultiModal, SupportsPP
32
+from .keye import (BaseKeyeModule, BaseMultiModalProcessor,
33
+ KeyeBaseDummyInputsBuilder, KeyeProcessingInfo)
34
+
35
+logger = init_logger(__name__)
36
+
37
+
38
+def split_thw(grid_thw: torch.Tensor) -> torch.Tensor:
39
+ """
40
+ Split grid_thw in t dimension.
41
+
42
+ Args:
43
+ grid_thw: [N, 3] tensor of [t, h, w]
44
+
45
+ Returns:
46
+ [Σt, 3] tensor where each row is [1, h, w]
47
+
48
+ Example:
49
+ >>> grid_thw = torch.tensor([[2, 3, 4], [1, 5, 6]])
50
+ >>> split_thw(grid_thw)
51
+ tensor([[1, 3, 4],
52
+ [1, 3, 4],
53
+ [1, 5, 6]])
54
+ """
55
+ t = grid_thw[:, 0]
56
+ h_w = grid_thw[:, 1:]
57
+ ones = torch.ones_like(h_w[:, :1])
58
+ return torch.cat([ones, h_w], dim=1).repeat_interleave(t, dim=0)
59
+
60
+
61
+def get_num_patches(grid_thw: torch.Tensor, num_frames: Union[list[int],
62
+ torch.Tensor]):
63
+ """
64
+ Return num_patches per video.
65
+
66
+ Args:
67
+ t: tensor with shape [N, ...] where each item is a list/tensor
68
+ cu_seqlens: list indicating the boundaries of groups
69
+
70
+ Returns:
71
+ list of ints representing the sum of products for each group
72
+
73
+ Examples:
74
+ >>> # Suppose there are 2 videos with a total of 3 grids
75
+ >>> grid_thw = torch.tensor([[2, 2, 2], # grid 0: 2*2*2=8 patches
76
+ ... [2, 2, 2], # grid 1: 2*2*2=8 patches
77
+ ... [1, 1, 1]]) # grid 2: 1*1*1=1 patches
78
+ >>> num_frames = [2, 1] # The first video contains 2 grids,
79
+ the second contains 1 grid.
80
+ >>> get_num_patches(grid_thw, num_frames)
81
+ tensor([16, 1]) # Total patches for first video: 8+8=16,
82
+ second video: 1.
83
+ """
84
+
85
+ assert len(grid_thw.shape) == 2
86
+ if isinstance(num_frames, torch.Tensor):
87
+ num_frames = num_frames.clone().tolist()
88
+
89
+ num_grids_per_frame = grid_thw.prod(dim=1)
90
+ start_idx_per_video = [0, *itertools.accumulate(num_frames)]
91
+ num_patches = [
92
+ num_grids_per_frame[start_idx_per_video[i]:start_idx_per_video[i + 1]].
93
+ sum() for i in range(len(num_frames))
94
+ ]
95
+ return torch.stack(num_patches) if num_patches else torch.zeros(
96
+ 0, dtype=grid_thw.dtype, device=grid_thw.device)
97
+
98
+
99
+class KeyeVL1_5ImagePixelInputs(TensorSchema):
100
+ """
101
+ Dimensions:
102
+ - b: Batch size
103
+ - np: Number of patches
104
+ - c: Number of channels
105
+ - ps: Patch size
106
+ - ni: Number of images
107
+ - g: Grid dimensions (3 for t, h, w)
108
+ """
109
+ type: Literal["pixel_values"]
110
+
111
+ pixel_values: Annotated[
112
+ torch.Tensor,
113
+ TensorShape("np", 3, "ps", "ps", dynamic_dims={"np"})]
114
+
115
+ image_grid_thw: Annotated[torch.Tensor, TensorShape("ni", 3)]
116
+
117
+
118
+class KeyeVL1_5ImageEmbeddingInputs(TensorSchema):
119
+ """
120
+ Dimensions:
121
+ - nf: Number of image features
122
+ - hs: Hidden size (must match the hidden size of language model
123
+ backbone)
124
+ - ni: Number of images
125
+ - g: Grid dimensions (3 for t, h, w)
126
+ """
127
+ type: Literal["image_embeds"]
128
+ image_embeds: Annotated[torch.Tensor, TensorShape("nf", "hs")]
129
+ image_grid_thw: Annotated[torch.Tensor, TensorShape("ni", 3)]
130
+
131
+
132
+KeyeVL1_5ImageInputs = Union[KeyeVL1_5ImagePixelInputs,
133
+ KeyeVL1_5ImageEmbeddingInputs]
134
+
135
+
136
+class KeyeVL1_5VideoPixelInputs(TensorSchema):
137
+ """
138
+ Dimensions:
139
+ - b: Batch size
140
+ - np: Number of patches
141
+ - c: Number of channels
142
+ - ps: Patch size
143
+ - ni: Number of images
144
+ - g: Grid dimensions (3 for t, h, w)
145
+ """
146
+ type: Literal["pixel_values_videos"]
147
+ pixel_values_videos: Annotated[
148
+ torch.Tensor,
149
+ TensorShape("np", 3, "ps", "ps", dynamic_dims={"np"})]
150
+ video_grid_thw: Annotated[torch.Tensor, TensorShape("nv", 3)]
151
+
152
+ num_frames: torch.Tensor
153
+
154
+
155
+class KeyeVL1_5VideoEmbeddingInputs(TensorSchema):
156
+ """
157
+ Dimensions:
158
+ - nf: Number of video features
159
+ - hs: Hidden size (must match the hidden size of language model
160
+ backbone)
161
+ - nv: Number of videos
162
+ - g: Grid dimensions (3 for t, h, w)
163
+ """
164
+ type: Literal["video_embeds"]
165
+ video_embeds: Annotated[torch.Tensor, TensorShape("nf", "hs")]
166
+ video_grid_thw: Annotated[torch.Tensor, TensorShape("nv", 3)]
167
+ num_frames: torch.Tensor
168
+
169
+
170
+KeyeVL1_5VideoInputs = Union[KeyeVL1_5VideoPixelInputs,
171
+ KeyeVL1_5VideoEmbeddingInputs]
172
+
173
+
174
+class KeyeVL1_5Projector(nn.Module):
175
+
176
+ def __init__(
177
+ self,
178
+ text_config: PretrainedConfig,
179
+ vision_config: PretrainedConfig,
180
+ quant_config: Optional[QuantizationConfig] = None,
181
+ prefix: str = "",
182
+ ):
183
+ super().__init__()
184
+ self.text_config = text_config
185
+ self.vision_config = vision_config
186
+ self.merge_kernel_size = (2, 2)
187
+
188
+ self.hidden_size = (self.vision_config.hidden_size *
189
+ self.merge_kernel_size[0] *
190
+ self.merge_kernel_size[1])
191
+
192
+ self.pre_norm = torch.nn.LayerNorm(self.hidden_size, eps=1e-05)
193
+ self.act = GELUActivation()
194
+
195
+ self.linear_1 = ColumnParallelLinear(
196
+ self.hidden_size,
197
+ self.hidden_size,
198
+ bias=True,
199
+ quant_config=quant_config,
200
+ prefix=f"{prefix}.linear_1",
201
+ )
202
+ self.linear_2 = RowParallelLinear(
203
+ self.hidden_size,
204
+ self.text_config.hidden_size,
205
+ bias=True,
206
+ quant_config=quant_config,
207
+ prefix=f"{prefix}.linear_2",
208
+ )
209
+
210
+ def forward(
211
+ self,
212
+ image_features: Union[torch.Tensor, tuple[torch.Tensor],
213
+ list[torch.Tensor]],
214
+ image_grid_thw: list[tuple[int, int, int]],
215
+ ) -> Union[torch.Tensor, list[torch.Tensor]]:
216
+ m1, m2 = self.merge_kernel_size
217
+ if isinstance(image_features, (list, tuple)):
218
+ processed_features = list()
219
+ for image_feature, image_grid in zip(image_features,
220
+ image_grid_thw):
221
+ t, h, w = image_grid
222
+ image_feature = rearrange(
223
+ image_feature,
224
+ "(t h p1 w p2) d -> (t h w) (p1 p2 d)",
225
+ t=t,
226
+ h=h // m1,
227
+ p1=m1,
228
+ w=w // m2,
229
+ p2=m2,
230
+ )
231
+ image_feature = self.pre_norm(image_feature)
232
+ hidden_states, _ = self.linear_1(image_feature)
233
+ hidden_states = self.act(hidden_states)
234
+ hidden_states, _ = self.linear_2(hidden_states)
235
+ processed_features.append(hidden_states)
236
+
237
+ return processed_features
238
+
239
+ dims = image_features.shape[:-1]
240
+ dim = image_features.shape[-1]
241
+ image_features = image_features.view(np.prod(dims), dim)
242
+ hidden_states = self.pre_norm(image_features.view(
243
+ -1, self.hidden_size))
244
+ hidden_states = self.linear_1(hidden_states)
245
+ hidden_states = self.act(hidden_states)
246
+ hidden_states = self.linear_2(hidden_states)
247
+
248
+ return hidden_states.view(*dims, -1)
249
+
250
+
251
+class KeyeVL1_5ProcessingInfo(KeyeProcessingInfo):
252
+
253
+ def get_max_frame_per_video(self) -> int:
254
+ return 2048
255
+
256
+ def get_supported_mm_limits(self, ) -> Mapping[str, Optional[int]]:
257
+ return {"image": None, "video": 1}
258
+
259
+
260
+def _keye_field_config(hf_inputs: Mapping[str, torch.Tensor], ):
261
+ image_grid_thw = hf_inputs.get("image_grid_thw",
262
+ torch.empty((0, 3), dtype=torch.int64))
263
+ image_grid_sizes = image_grid_thw.prod(-1)
264
+
265
+ video_grid_thw = hf_inputs.get("video_grid_thw",
266
+ torch.empty((0, 3), dtype=torch.int64))
267
+ video_grid_thw = split_thw(video_grid_thw)
268
+ num_frames = hf_inputs.get("num_frames",
269
+ video_grid_thw[:, 0]).clone().tolist()
270
+
271
+ video_num_patches = get_num_patches(video_grid_thw, num_frames)
272
+
273
+ video_num_grids = []
274
+ if len(num_frames) > 0:
275
+ i = 0
276
+ j = 1
277
+ cur_frames = num_frames[i]
278
+ for t, _, _ in video_grid_thw.tolist():
279
+ cur_frames -= t
280
+ if cur_frames == 0:
281
+ video_num_grids.append(j)
282
+ i += 1
283
+ if i < len(num_frames):
284
+ cur_frames = num_frames[i]
285
+ j = 1
286
+ else:
287
+ j += 1
288
+ video_num_grids = torch.tensor(video_num_grids)
289
+ return dict(pixel_values=MultiModalFieldConfig.flat_from_sizes(
290
+ "image", image_grid_sizes),
291
+ image_embeds=MultiModalFieldConfig.flat_from_sizes(
292
+ "image", image_grid_sizes),
293
+ image_grid_thw=MultiModalFieldConfig.batched("image"),
294
+ pixel_values_videos=MultiModalFieldConfig.flat_from_sizes(
295
+ "video", video_num_patches),
296
+ video_embeds=MultiModalFieldConfig.flat_from_sizes(
297
+ "video", video_num_patches),
298
+ video_grid_thw=MultiModalFieldConfig.flat_from_sizes(
299
+ "video", video_num_grids),
300
+ num_frames=MultiModalFieldConfig.batched("video"))
301
+
302
+
303
+class KeyeVL1_5MultiModalDataParser(MultiModalDataParser):
304
+
305
+ def _parse_image_data(
306
+ self,
307
+ data: Union[dict[str, torch.Tensor], ModalityData[ImageItem]],
308
+ ) -> ModalityDataItems[Any, Any]:
309
+ if isinstance(data, dict):
310
+ return DictEmbeddingItems(
311
+ data,
312
+ modality="image",
313
+ required_fields={
314
+ "image_embeds",
315
+ "image_grid_thw",
316
+ },
317
+ fields_factory=_keye_field_config,
318
+ )
319
+
320
+ return super()._parse_image_data(data)
321
+
322
+ def _parse_video_data(
323
+ self,
324
+ data: Union[dict[str, torch.Tensor], ModalityData[VideoItem]],
325
+ ) -> ModalityDataItems[Any, Any]:
326
+ if isinstance(data, dict):
327
+ return DictEmbeddingItems(
328
+ data,
329
+ modality="video",
330
+ required_fields={
331
+ "video_embeds",
332
+ "video_grid_thw",
333
+ },
334
+ fields_factory=_keye_field_config,
335
+ )
336
+
337
+ return super()._parse_video_data(data)
338
+
339
+
340
+class KeyeVL1_5MultiModalProcessor(
341
+ BaseMultiModalProcessor[KeyeVL1_5ProcessingInfo]):
342
+
343
+ def _get_data_parser(self) -> MultiModalDataParser:
344
+ return KeyeVL1_5MultiModalDataParser()
345
+
346
+ def _get_prompt_updates(
347
+ self,
348
+ mm_items: MultiModalDataItems,
349
+ hf_processor_mm_kwargs: Mapping[str, Any],
350
+ out_mm_kwargs: MultiModalKwargsItems,
351
+ ) -> Sequence[PromptUpdate]:
352
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
353
+ image_processor = self.info.get_image_processor(
354
+ **hf_processor_mm_kwargs)
355
+ tokenizer = self.info.get_tokenizer()
356
+ vocab = tokenizer.get_vocab()
357
+ image_token_id = vocab[hf_processor.image_token]
358
+ video_token_id = vocab[hf_processor.video_token]
359
+ placeholder = {"image": image_token_id, "video": video_token_id}
360
+ merge_length = image_processor.merge_size**2
361
+
362
+ out_mm_kwargs_data = out_mm_kwargs.get_data()
363
+ frame_types: list[torch.Tensor] = \
364
+ hf_processor_mm_kwargs.get("frame_types", None)
365
+ timestamps: list[torch.Tensor] = \
366
+ hf_processor_mm_kwargs.get("timestamps", None)
367
+ num_videos = mm_items.get_count("video", strict=False)
368
+
369
+ if frame_types is None:
370
+ frame_types = [None] * num_videos
371
+ assert len(frame_types) == num_videos, \
372
+ f"Number of frame_types={len(frame_types)} " \
373
+ f"doesn't equal to number of videos={num_videos}"
374
+ if timestamps is None:
375
+ timestamps = [None] * num_videos
376
+ assert len(timestamps) == num_videos, \
377
+ f"Number of timestamps={len(timestamps)} " \
378
+ f"doesn't equal to number of videos={num_videos}"
379
+
380
+ video_grid_thw = out_mm_kwargs_data.get(
381
+ 'video_grid_thw', torch.empty((0, 3), dtype=torch.int64))
382
+ num_frames = out_mm_kwargs_data.get(
383
+ 'num_frames', torch.tensor([], dtype=torch.int64))
384
+
385
+ assert len(num_frames) == num_videos, \
386
+ f"Size of num_frames={len(num_frames)} " \
387
+ f"doesn't equal to number of videos={num_videos}"
388
+
389
+ video_grid_hws = split_thw(video_grid_thw)
390
+ assert int(num_frames.sum().tolist()) == video_grid_hws.shape[0], (
391
+ f"The first dimension of `video_grid_hws`={video_grid_hws.shape[0]}"
392
+ f"doesn't equal to num of frames.")
393
+
394
+ cu_seqlens = torch.cumsum(torch.tensor([0] + num_frames.tolist()),
395
+ dim=-1)
396
+
397
+ def get_replacement_keye(item_idx: int, modality: str):
398
+ """
399
+ Args:
400
+ item_idx(int): The item index of modality to replace
401
+ modality(str): The modality
402
+ """
403
+ if modality == "image":
404
+ out_item = out_mm_kwargs[modality][item_idx]
405
+ grid_thw = out_item[f"{modality}_grid_thw"].data
406
+ assert isinstance(grid_thw, torch.Tensor)
407
+
408
+ num_tokens = int(grid_thw.prod()) // merge_length
409
+ return [image_token_id] * num_tokens
410
+ elif modality == "video":
411
+ placeholders = []
412
+ video_timestamps = timestamps[item_idx]
413
+ video_frame_types = frame_types[item_idx]
414
+ grid_thw = video_grid_hws[
415
+ cu_seqlens[item_idx]:cu_seqlens[item_idx + 1]]
416
+
417
+ nframes = grid_thw.shape[0]
418
+
419
+ if video_timestamps is None:
420
+ video_timestamps = [""] * nframes
421
+ else:
422
+ video_timestamps = [
423
+ format(ts, ".1f") for ts in video_timestamps
424
+ ]
425
+
426
+ if video_frame_types is None:
427
+ video_frame_types = [0] * nframes
428
+ for i, sub_thw in enumerate(grid_thw):
429
+ s = f"{hf_processor.frame_token}{video_timestamps[i]}"
430
+ if video_frame_types[i] == 1:
431
+ s += hf_processor.fast_start
432
+ placeholders.extend(tokenizer.encode(s))
433
+ num_frame_tokens = int(sub_thw.prod()) // merge_length
434
+ placeholders.extend([video_token_id] * num_frame_tokens)
435
+ if video_frame_types[i] == 1:
436
+ placeholders.append(vocab[hf_processor.fast_end])
437
+
438
+ return PromptUpdateDetails.select_token_id(
439
+ placeholders, embed_token_id=video_token_id)
440
+ else:
441
+ raise ValueError(f"Unsupported modality {modality}")
442
+
443
+ return [
444
+ PromptReplacement(
445
+ modality=modality,
446
+ target=[placeholder[modality]],
447
+ replacement=partial(get_replacement_keye, modality=modality),
448
+ ) for modality in ("image", "video")
449
+ ]
450
+
451
+ def _get_mm_fields_config(
452
+ self,
453
+ hf_inputs: BatchFeature,
454
+ hf_processor_mm_kwargs: Mapping[str, object],
455
+ ) -> Mapping[str, MultiModalFieldConfig]:
456
+ return _keye_field_config(hf_inputs)
457
+
458
+
459
+class KeyeVL1_5DummyInputsBuilder(
460
+ KeyeBaseDummyInputsBuilder[KeyeVL1_5ProcessingInfo]):
461
+ ...
462
+
463
+
464
+@MULTIMODAL_REGISTRY.register_processor(
465
+ KeyeVL1_5MultiModalProcessor,
466
+ info=KeyeVL1_5ProcessingInfo,
467
+ dummy_inputs=KeyeVL1_5DummyInputsBuilder,
468
+)
469
+class KeyeVL1_5ForConditionalGeneration(BaseKeyeModule, SupportsMultiModal,
470
+ SupportsLoRA, SupportsPP):
471
+
472
+ def _build_projector(self,
473
+ text_config: PretrainedConfig,
474
+ vision_config: PretrainedConfig,
475
+ quant_config: Optional[QuantizationConfig] = None,
476
+ prefix: str = "") -> nn.Module:
477
+ return KeyeVL1_5Projector(text_config, vision_config, quant_config,
478
+ prefix)
479
+
480
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
481
+ config: PretrainedConfig = vllm_config.model_config.hf_config
482
+ self.merge_size = config.vision_config.spatial_merge_size
483
+ super().__init__(vllm_config=vllm_config, prefix=prefix)
484
+
485
+ def _validate_and_reshape_mm_tensor(self, mm_input: NestedTensors,
486
+ expected_dim: int, name: str):
487
+ if not isinstance(mm_input, (torch.Tensor, list)):
488
+ raise ValueError(f"Incorrect type of {name}. "
489
+ f"Got type: {type(mm_input)}")
490
+ if isinstance(mm_input, torch.Tensor):
491
+ if mm_input.ndim == expected_dim:
492
+ return mm_input
493
+ elif mm_input.ndim == expected_dim + 1:
494
+ return torch.concat(list(mm_input))
495
+ else:
496
+ raise ValueError(
497
+ f"{name} should be {expected_dim}D or "
498
+ f"batched {expected_dim}D tensor."
499
+ f"Got ndim: {mm_input.ndim} (shape={mm_input.shape})")
500
+ else:
501
+ return torch.concat(list(mm_input))
502
+
503
+ def _parse_and_validate_image_input(
504
+ self, **kwargs: object) -> Optional[KeyeVL1_5ImageInputs]:
505
+ pixel_values = kwargs.pop("pixel_values", None)
506
+ image_embeds = kwargs.pop("image_embeds", None)
507
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
508
+
509
+ if pixel_values is None and image_embeds is None:
510
+ return None
511
+
512
+ if pixel_values is not None:
513
+ pixel_values = self._validate_and_reshape_mm_tensor(
514
+ pixel_values, expected_dim=4, name="image pixel values")
515
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
516
+ image_grid_thw, expected_dim=2, name="image grid_thw")
517
+
518
+ return KeyeVL1_5ImagePixelInputs(
519
+ type="pixel_values",
520
+ pixel_values=pixel_values,
521
+ image_grid_thw=image_grid_thw,
522
+ )
523
+
524
+ if image_embeds is not None:
525
+ image_embeds = self._validate_and_reshape_mm_tensor(
526
+ image_embeds, expected_dim=2, name="image embeds")
527
+ image_grid_thw = self._validate_and_reshape_mm_tensor(
528
+ image_grid_thw, expected_dim=2, name="image grid_thw")
529
+
530
+ return KeyeVL1_5ImageEmbeddingInputs(
531
+ type="image_embeds",
532
+ image_embeds=image_embeds,
533
+ image_grid_thw=image_grid_thw,
534
+ )
535
+
536
+ def _parse_and_validate_video_input(
537
+ self, **kwargs: object) -> Optional[KeyeVL1_5VideoInputs]:
538
+ pixel_values_videos = kwargs.pop("pixel_values_videos", None)
539
+ video_embeds = kwargs.pop("video_embeds", None)
540
+ video_grid_thw = kwargs.pop("video_grid_thw", None)
541
+ num_frames = kwargs.pop("num_frames", None)
542
+
543
+ if pixel_values_videos is None and video_embeds is None:
544
+ return None
545
+
546
+ if pixel_values_videos is not None:
547
+ pixel_values_videos = self._validate_and_reshape_mm_tensor(
548
+ pixel_values_videos,
549
+ expected_dim=4,
550
+ name="video pixel values",
551
+ )
552
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
553
+ video_grid_thw, expected_dim=2, name="video grid_thw")
554
+
555
+ num_frames = self._validate_and_reshape_mm_tensor(
556
+ num_frames, expected_dim=1, name="video num frames")
557
+
558
+ return KeyeVL1_5VideoPixelInputs(
559
+ type="pixel_values_videos",
560
+ pixel_values_videos=pixel_values_videos,
561
+ video_grid_thw=video_grid_thw,
562
+ num_frames=num_frames)
563
+
564
+ if video_embeds is not None:
565
+ video_embeds = self._validate_and_reshape_mm_tensor(
566
+ video_embeds, expected_dim=2, name="video embeds")
567
+ video_grid_thw = self._validate_and_reshape_mm_tensor(
568
+ video_grid_thw, expected_dim=2, name="video grid_thw")
569
+
570
+ return KeyeVL1_5VideoEmbeddingInputs(type="video_embeds",
571
+ video_embeds=video_embeds,
572
+ video_grid_thw=video_grid_thw,
573
+ num_frames=num_frames)
574
+
575
+ def _process_video_input(
576
+ self,
577
+ video_input: KeyeVL1_5VideoInputs) -> tuple[torch.Tensor, ...]:
578
+ video_type = video_input["type"]
579
+ video_grid_thw = split_thw(video_input["video_grid_thw"])
580
+ pixel_values_videos = video_input.get("pixel_values_videos", None)
581
+
582
+ video_embeds = self._process_video_embeds(video_type, video_grid_thw,
583
+ pixel_values_videos)
584
+ video_embeds = torch.concat(video_embeds, dim=0)
585
+
586
+ num_frames = video_input["num_frames"].clone().tolist()
587
+
588
+ num_patches = get_num_patches(video_grid_thw, num_frames).tolist()
589
+
590
+ patch_cu_seqlens = torch.cumsum(
591
+ torch.tensor([0] + num_patches).detach().clone(), dim=-1)
592
+ patch_cu_seqlens = torch.div(patch_cu_seqlens,
593
+ self.merge_size**2,
594
+ rounding_mode="floor")
595
+
596
+ new_video_embeds = []
597
+ for idx in range(patch_cu_seqlens.shape[0] - 1):
598
+ start = patch_cu_seqlens[idx]
599
+ end = patch_cu_seqlens[idx + 1]
600
+ new_video_embeds.append(video_embeds[start:end])
601
+ return tuple(new_video_embeds)
vllm/model_executor/models/registry.py
+1
@@ -227,6 +227,7 @@ _MULTIMODAL_MODELS = {
227
"Idefics3ForConditionalGeneration":("idefics3","Idefics3ForConditionalGeneration"),
228
"SmolVLMForConditionalGeneration": ("smolvlm","SmolVLMForConditionalGeneration"), # noqa: E501
229
"KeyeForConditionalGeneration": ("keye", "KeyeForConditionalGeneration"),
230
+ "KeyeVL1_5ForConditionalGeneration": ("keye_vl1_5", "KeyeVL1_5ForConditionalGeneration"), # noqa: E501
231
"RForConditionalGeneration": ("rvl", "RForConditionalGeneration"),
232
"KimiVLForConditionalGeneration": ("kimi_vl", "KimiVLForConditionalGeneration"), # noqa: E501
233
"Llama_Nemotron_Nano_VL": ("nemotron_vl", "LlamaNemotronVLChatModel"),