main
py 609 lines 21.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4 # Adapted from https://huggingface.co/moonshotai/Kimi-VL-A3B-Instruct/blob/main/modeling_kimi_vl.py
5 # Copyright 2025 The Moonshot AI Team, DeepSeek-AI, and HuggingFace Inc. team. All rights reserved.
6 #
7 # The code is based on llava (llava/modeling_llava.py) and DeepSeek-V3 (DeepSeek-V3/modeling_deepseek.py), but modified for KimiVL.
8 #
9 # Licensing Information:
10 # - Code derived from llava (llava/modeling_llava.py) and DeepSeek-V3 (DeepSeek-V3/modeling_deepseek.py) is licensed under the Apache License, Version 2.0.
11 # - Other parts of the code are licensed under the MIT License.
12 #
13 # Apache License, Version 2.0:
14 # Licensed under the Apache License, Version 2.0 (the "License");
15 # you may not use this file except in compliance with the License.
16 # You may obtain a copy of the License at
17 #
18 # http://www.apache.org/licenses/LICENSE-2.0
19 #
20 # Unless required by applicable law or agreed to in writing, software
21 # distributed under the License is distributed on an "AS IS" BASIS,
22 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 # See the License for the specific language governing permissions and
24 # limitations under the License.
25 #
26 # MIT License:
27 # Permission is hereby granted, free of charge, to any person obtaining a copy
28 # of this software and associated documentation files (the "Software"), to deal
29 # in the Software without restriction, including without limitation the rights
30 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31 # copies of the Software, and to permit persons to whom the Software is
32 # furnished to do so, subject to the following conditions:
33 #
34 # The above copyright notice and this permission notice shall be included in all
35 # copies or substantial portions of the Software.
36 #
37 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43 # SOFTWARE.
44
45 import math
46 from collections.abc import Hashable, Iterable, Mapping, Sequence
47 from dataclasses import dataclass
48 from typing import Annotated, Any, Literal
49
50 import torch
51 from torch import nn
52 from transformers import BatchFeature
53 from transformers.activations import GELUActivation
54
55 from vllm.config import VllmConfig
56 from vllm.config.multimodal import MultiModalDummyOptions
57 from vllm.inputs import MultiModalDataDict
58 from vllm.model_executor.layers.linear import ReplicatedLinear
59 from vllm.model_executor.models.interfaces import (
60 SupportsEncoderCudaGraph,
61 SupportsMultiModal,
62 SupportsPP,
63 )
64 from vllm.model_executor.models.moonvit import MoonVitPretrainedModel
65 from vllm.multimodal import MULTIMODAL_REGISTRY
66 from vllm.multimodal.inputs import (
67 MultiModalFieldConfig,
68 MultiModalKwargsItems,
69 NestedTensors,
70 )
71 from vllm.multimodal.parse import (
72 ImageEmbeddingItems,
73 ImageProcessorItems,
74 MultiModalDataItems,
75 )
76 from vllm.multimodal.processing import (
77 BaseDummyInputsBuilder,
78 BaseMultiModalProcessor,
79 BaseProcessingInfo,
80 PromptReplacement,
81 PromptUpdate,
82 )
83 from vllm.sequence import IntermediateTensors
84 from vllm.transformers_utils.configs.kimi_vl import KimiVLConfig, MoonViTConfig
85 from vllm.utils.tensor_schema import TensorSchema, TensorShape
86 from vllm.v1.worker.encoder_cudagraph_defs import EncoderCudaGraphReplayBuffers
87
88 from .utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
89 from .vision import is_vit_use_data_parallel, run_dp_sharded_mrope_vision_model
90
91
92 # For dummy input only
93 @dataclass
94 class MaxImageTokenMeta:
95 width: int = 1024
96 height: int = 1024
97
98
99 class KimiVLMultiModalProjector(nn.Module):
100 def __init__(
101 self,
102 config: KimiVLConfig,
103 prefix: str = "",
104 ):
105 super().__init__()
106 self.use_data_parallel = is_vit_use_data_parallel()
107
108 self.hidden_size = (
109 config.vision_config.hidden_size
110 * config.vision_config.merge_kernel_size[0]
111 * config.vision_config.merge_kernel_size[1]
112 )
113
114 self.pre_norm = torch.nn.LayerNorm(config.vision_config.hidden_size, eps=1e-5)
115 self.linear_1 = ReplicatedLinear(
116 self.hidden_size,
117 self.hidden_size,
118 bias=True,
119 prefix=maybe_prefix(prefix, "linear_1"),
120 )
121 self.linear_2 = ReplicatedLinear(
122 self.hidden_size,
123 config.text_config.hidden_size,
124 bias=True,
125 prefix=maybe_prefix(prefix, "linear_2"),
126 )
127 self.act = GELUActivation()
128
129 def forward(self, image_features: torch.Tensor) -> torch.Tensor:
130 hidden_states = self.pre_norm(image_features).view(-1, self.hidden_size)
131 hidden_states, _ = self.linear_1(hidden_states)
132 hidden_states = self.act(hidden_states)
133 hidden_states, _ = self.linear_2(hidden_states)
134 return hidden_states
135
136
137 class KimiVLImagePixelInputs(TensorSchema):
138 """Dimensions:
139 - nc: Number of channels
140 - np: Number of patches
141 - ps: Patch size
142 - ni: Number of images
143 """
144
145 type: Literal["pixel_values"] = "pixel_values"
146
147 pixel_values: Annotated[
148 torch.Tensor | list[torch.Tensor],
149 TensorShape("np", 3, "ps", "ps"),
150 ]
151
152 image_grid_hws: Annotated[torch.Tensor, TensorShape("ni", 2)]
153
154
155 # TODO: support embeds too
156 # We only support pixel input for kimi-vl now
157 KimiVLImageInputs = KimiVLImagePixelInputs
158
159
160 class KimiVLProcessingInfo(BaseProcessingInfo):
161 def get_hf_config(self):
162 return self.ctx.get_hf_config(KimiVLConfig)
163
164 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
165 return {"image": None}
166
167 def get_num_image_tokens(
168 self,
169 *,
170 image_width: int,
171 image_height: int,
172 ) -> int:
173 hf_processor = self.get_hf_processor()
174 patch_size = hf_processor.image_processor.patch_size
175 kernel_size = hf_processor.image_processor.merge_kernel_size
176 in_token_limit = hf_processor.image_processor.in_token_limit
177 height = image_height
178 width = image_width
179 assert isinstance(height, int), f"height must be int, current height {height}"
180 assert isinstance(width, int), f"width must be int, current width {width}"
181 assert kernel_size is not None, "kernel_size must be specified"
182
183 if (width // patch_size) * (height // patch_size) > in_token_limit:
184 scale = math.sqrt(
185 in_token_limit / ((width // patch_size) * (height // patch_size))
186 )
187 new_w, new_h = int(width * scale), int(height * scale)
188 width, height = new_w, new_h
189
190 kernel_height, kernel_width = kernel_size
191
192 pad_height = (
193 kernel_height * patch_size - height % (kernel_height * patch_size)
194 ) % (kernel_height * patch_size)
195 pad_width = (
196 kernel_width * patch_size - width % (kernel_width * patch_size)
197 ) % (kernel_width * patch_size)
198
199 # Calculate new dimensions after padding and patching
200 token_height = (height + pad_height) // (kernel_size[0] * patch_size)
201 token_width = (width + pad_width) // (kernel_size[1] * patch_size)
202 return int(token_height * token_width)
203
204 @property
205 def image_token_id(self) -> int:
206 return self.get_hf_config().media_placeholder_token_id
207
208
209 class KimiVLDummyInputsBuilder(BaseDummyInputsBuilder[KimiVLProcessingInfo]):
210 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
211 num_images = mm_counts.get("image", 0)
212
213 processor = self.info.get_hf_processor()
214 image_token = processor.image_token
215
216 return image_token * num_images
217
218 def get_dummy_mm_data(
219 self,
220 seq_len: int,
221 mm_counts: Mapping[str, int],
222 mm_options: MultiModalDummyOptions,
223 ) -> MultiModalDataDict:
224 return {
225 "image": self._get_dummy_images(
226 width=MaxImageTokenMeta.width,
227 height=MaxImageTokenMeta.height,
228 num_images=mm_counts.get("image", 0),
229 overrides=mm_options.get("image"),
230 )
231 }
232
233
234 class KimiVLMultiModalProcessor(BaseMultiModalProcessor[KimiVLProcessingInfo]):
235 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
236 return self.dummy_inputs.get_dummy_text(mm_counts)
237
238 def _get_mm_fields_config(
239 self,
240 hf_inputs: BatchFeature,
241 hf_processor_mm_kwargs: Mapping[str, object],
242 ) -> Mapping[str, MultiModalFieldConfig]:
243 image_grid_hws = hf_inputs.get("image_grid_hws", torch.empty((0, 2)))
244 image_grid_sizes = image_grid_hws.prod(-1)
245
246 # pixel_values is merged as a single large tensor
247 # image_grid_hws is shapes for each subtensor in pixel_values
248 return dict(
249 pixel_values=MultiModalFieldConfig.flat_from_sizes(
250 "image", image_grid_sizes
251 ),
252 image_grid_hws=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
253 )
254
255 def _get_prompt_updates(
256 self,
257 mm_items: MultiModalDataItems,
258 hf_processor_mm_kwargs: Mapping[str, Any],
259 out_mm_kwargs: MultiModalKwargsItems,
260 ) -> Sequence[PromptUpdate]:
261 image_token_id = self.info.image_token_id
262
263 def get_replacement(item_idx: int):
264 images = mm_items.get_items(
265 "image", (ImageEmbeddingItems, ImageProcessorItems)
266 )
267
268 if isinstance(images, ImageEmbeddingItems):
269 num_image_tokens = images.get_feature_size(item_idx)
270 else:
271 assert isinstance(images, ImageProcessorItems)
272 image_size = images.get_image_size(item_idx)
273 num_image_tokens = self.info.get_num_image_tokens(
274 image_width=image_size.width,
275 image_height=image_size.height,
276 )
277
278 return [image_token_id] * num_image_tokens
279
280 return [
281 PromptReplacement(
282 modality="image",
283 target=[image_token_id],
284 replacement=get_replacement,
285 ),
286 ]
287
288
289 @MULTIMODAL_REGISTRY.register_processor(
290 KimiVLMultiModalProcessor,
291 info=KimiVLProcessingInfo,
292 dummy_inputs=KimiVLDummyInputsBuilder,
293 )
294 class KimiVLForConditionalGeneration(
295 nn.Module, SupportsMultiModal, SupportsEncoderCudaGraph, SupportsPP
296 ):
297 supports_encoder_tp_data = True
298
299 @classmethod
300 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
301 if modality.startswith("image"):
302 return "<|media_start|>image<|media_content|><|media_pad|><|media_end|>"
303
304 raise ValueError("Only image modality is supported")
305
306 def __init__(
307 self,
308 vllm_config: VllmConfig,
309 prefix: str = "",
310 ) -> None:
311 super().__init__()
312 model_config = vllm_config.model_config
313 config: KimiVLConfig = model_config.hf_config
314 quant_config = vllm_config.quant_config
315
316 self.config = config
317 self.quant_config = quant_config
318
319 assert isinstance(config.vision_config, MoonViTConfig)
320 self.use_data_parallel = (
321 model_config.get_multimodal_config().mm_encoder_tp_mode == "data"
322 )
323 self.hidden_size = config.text_config.hidden_size
324
325 with self._mark_tower_model(vllm_config, "image"):
326 self.vision_tower = MoonVitPretrainedModel(
327 config.vision_config,
328 prefix=maybe_prefix(prefix, "vision_tower"),
329 )
330 self.multi_modal_projector = KimiVLMultiModalProjector(
331 config=config,
332 prefix=maybe_prefix(prefix, "multi_modal_projector"),
333 )
334
335 with self._mark_language_model(vllm_config):
336 self.language_model = init_vllm_registered_model(
337 vllm_config=vllm_config,
338 hf_config=config.text_config,
339 prefix=maybe_prefix(prefix, "language_model"),
340 architectures=["DeepseekV2ForCausalLM"],
341 )
342
343 self.make_empty_intermediate_tensors = (
344 self.language_model.make_empty_intermediate_tensors
345 )
346
347 self.media_placeholder: int = self.config.media_placeholder_token_id
348
349 self.model_config = model_config
350
351 # -- SupportsEncoderCudaGraph protocol methods --
352
353 def get_encoder_cudagraph_config(self):
354 from vllm.v1.worker.encoder_cudagraph_defs import (
355 EncoderCudaGraphConfig,
356 )
357
358 return EncoderCudaGraphConfig(
359 modalities=["image"],
360 buffer_keys=[
361 "pixel_values",
362 "pos_embeds",
363 "rope_freqs_cis",
364 "cu_seqlens",
365 "max_seqlen",
366 "merge_gather_idx",
367 ],
368 out_hidden_size=self.hidden_size,
369 )
370
371 def get_encoder_cudagraph_budget_range(
372 self,
373 vllm_config,
374 ) -> tuple[int, int]:
375 # Min: estimated smallest possible encoder input.
376 # 224x224 image with patch_size=14 -> 16x16 patches, then merge
377 # kernel (2,2) -> 8x8 = 64 output tokens.
378 min_budget = 64
379 max_budget = min(
380 vllm_config.scheduler_config.max_num_batched_tokens,
381 self.model_config.max_model_len,
382 )
383 return (min_budget, max_budget)
384
385 def _get_grid_hws(
386 self,
387 mm_kwargs: dict[str, Any],
388 ) -> list[tuple[int, int]]:
389 grid_hws = mm_kwargs["image_grid_hws"]
390 if not isinstance(grid_hws, list):
391 grid_hws = grid_hws.tolist()
392 return grid_hws
393
394 def get_encoder_cudagraph_item_specs(
395 self,
396 mm_kwargs: dict[str, Any],
397 ):
398 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
399
400 kh, kw = self.config.vision_config.merge_kernel_size
401 return [
402 EncoderItemSpec(
403 input_size=h * w,
404 output_tokens=(h // kh) * (w // kw),
405 )
406 for h, w in self._get_grid_hws(mm_kwargs)
407 ]
408
409 def select_encoder_cudagraph_items(
410 self,
411 mm_kwargs: dict[str, Any],
412 indices: list[int],
413 ) -> dict[str, Any]:
414 grid_hws = self._get_grid_hws(mm_kwargs)
415 pixel_values = mm_kwargs["pixel_values"]
416
417 if len(indices) == 0:
418 return {
419 "pixel_values": pixel_values[:0],
420 "image_grid_hws": pixel_values.new_zeros((0, 2), dtype=torch.long),
421 }
422
423 patches_per_item = [h * w for h, w in grid_hws]
424 cum_patches = [0]
425 for p in patches_per_item:
426 cum_patches.append(cum_patches[-1] + p)
427
428 selected_pv = torch.cat(
429 [pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
430 )
431 selected_grid = torch.tensor(
432 [grid_hws[i] for i in indices],
433 dtype=torch.long,
434 device=pixel_values.device,
435 )
436 return {
437 "pixel_values": selected_pv,
438 "image_grid_hws": selected_grid,
439 }
440
441 def prepare_encoder_cudagraph_capture_inputs(
442 self,
443 token_budget: int,
444 max_batch_size: int,
445 max_frames_per_batch: int,
446 device: torch.device,
447 dtype: torch.dtype,
448 path: str = "default",
449 axis_keys: tuple[Hashable, ...] | None = None,
450 ):
451 from vllm.v1.worker.encoder_cudagraph_defs import (
452 EncoderCudaGraphCaptureInputs,
453 )
454
455 kh, kw = self.config.vision_config.merge_kernel_size
456 # Ceil so the buffer fits the worst case of one item using the full
457 # budget. Floor under-allocates when budget is not a multiple of
458 # max_batch_size.
459 per_mm_item_output = (token_budget + max_batch_size - 1) // max_batch_size
460
461 # Shape the synthetic grid so neither dimension exceeds Rope2DPosEmb's
462 # precomputed range. Pack as wide a row as fits, then add rows.
463 rope = self.vision_tower.encoder.rope_2d
464 max_wo = rope.max_width // kw
465 wo = min(per_mm_item_output, max_wo)
466 ho = (per_mm_item_output + wo - 1) // wo
467 assert ho * kh <= rope.max_height, (
468 f"per_mm_item_output={per_mm_item_output} exceeds RoPE grid capacity "
469 f"(max {(rope.max_height // kh) * (rope.max_width // kw)} tokens)"
470 )
471 grid_hws_list = [(ho * kh, wo * kw) for _ in range(max_batch_size)]
472
473 patch_size: int | tuple[int, int] = self.config.vision_config.patch_size
474 if isinstance(patch_size, int):
475 patch_size = (patch_size, patch_size)
476
477 total_patches = sum(h * w for h, w in grid_hws_list)
478 in_channels = 3
479 dummy_pixel_values = torch.randn(
480 total_patches,
481 in_channels,
482 patch_size[0],
483 patch_size[1],
484 device=device,
485 dtype=dtype,
486 )
487
488 buffers = self.vision_tower.prepare_encoder_metadata(
489 grid_hws_list,
490 max_batch_size=max_batch_size,
491 max_seqlen_override=token_budget,
492 device=device,
493 )
494 values = buffers | {"pixel_values": dummy_pixel_values}
495
496 return EncoderCudaGraphCaptureInputs(values=values)
497
498 def prepare_encoder_cudagraph_replay_buffers(
499 self,
500 mm_kwargs: dict[str, Any],
501 max_batch_size: int,
502 max_frames_per_batch: int,
503 path: str = "default",
504 ):
505 grid_hws_list = self._get_grid_hws(mm_kwargs)
506 buffers = self.vision_tower.prepare_encoder_metadata(
507 grid_hws_list,
508 max_batch_size=max_batch_size,
509 device=mm_kwargs["pixel_values"].device,
510 )
511 values = buffers | {"pixel_values": mm_kwargs["pixel_values"]}
512 return EncoderCudaGraphReplayBuffers(values=values)
513
514 def encoder_cudagraph_forward(
515 self,
516 values: dict[str, torch.Tensor],
517 path: str = "default",
518 ) -> torch.Tensor:
519 pixel_values = values.pop("pixel_values")
520 metadata = values
521 image_features = self.vision_tower(
522 pixel_values, grid_hw=None, encoder_metadata=metadata
523 )
524 return self.multi_modal_projector(image_features)
525
526 def encoder_eager_forward(
527 self,
528 mm_kwargs: dict[str, Any],
529 path: str = "default",
530 ) -> torch.Tensor:
531 pixel_values = mm_kwargs["pixel_values"]
532 image_grid_hws = mm_kwargs["image_grid_hws"]
533 image_features = self.vision_tower(pixel_values, image_grid_hws)
534 return self.multi_modal_projector(torch.cat(image_features))
535
536 def _parse_and_validate_image_input(
537 self, **kwargs: object
538 ) -> KimiVLImageInputs | None:
539 # image input type must be pixel values now
540 pixel_values = kwargs.pop("pixel_values", None)
541 image_grid_hws = kwargs.pop("image_grid_hws", None)
542
543 if pixel_values is None:
544 return None
545
546 return KimiVLImagePixelInputs(
547 type="pixel_values",
548 pixel_values=pixel_values,
549 image_grid_hws=image_grid_hws,
550 )
551
552 # perform vt on processored pixel_values
553 @torch.inference_mode()
554 def _process_image_pixels(self, inputs: KimiVLImagePixelInputs) -> torch.Tensor:
555 pixel_values = inputs["pixel_values"]
556 image_grid_hws = inputs["image_grid_hws"]
557 if self.use_data_parallel:
558 return run_dp_sharded_mrope_vision_model(
559 self.vision_tower,
560 pixel_values,
561 image_grid_hws.tolist(),
562 rope_type="rope_2d",
563 )
564 else:
565 return self.vision_tower(pixel_values, image_grid_hws)
566
567 def _process_image_input(self, image_input: KimiVLImageInputs) -> torch.Tensor:
568 assert image_input["type"] == "pixel_values"
569 image_features = self._process_image_pixels(image_input)
570 assert isinstance(image_features, (list, tuple))
571 lengths = [x.shape[0] for x in image_features]
572 return self.multi_modal_projector(torch.cat(image_features)).split(lengths)
573
574 def embed_multimodal(self, **kwargs: object) -> NestedTensors | None:
575 # Validate the multimodal input keyword arguments
576 image_input = self._parse_and_validate_image_input(**kwargs)
577 if image_input is None:
578 return None
579
580 # Run multimodal inputs through encoder and projector
581 vision_embeddings = self._process_image_input(image_input)
582 return vision_embeddings
583
584 def forward(
585 self,
586 input_ids: torch.Tensor | None,
587 positions: torch.Tensor,
588 intermediate_tensors: IntermediateTensors | None = None,
589 inputs_embeds: torch.Tensor | None = None,
590 **kwargs: object,
591 ) -> IntermediateTensors:
592 if intermediate_tensors is not None:
593 inputs_embeds = None
594
595 hidden_states = self.language_model(
596 input_ids=input_ids,
597 positions=positions,
598 intermediate_tensors=intermediate_tensors,
599 inputs_embeds=inputs_embeds,
600 )
601
602 return hidden_states
603
604 def compute_logits(self, hidden_states: torch.Tensor, **kwargs) -> torch.Tensor:
605 return self.language_model.compute_logits(hidden_states)
606
607 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
608 loader = AutoWeightsLoader(self)
609 return loader.load_weights(weights)