main
py 768 lines 27.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Iterable, Mapping, Sequence
4 from functools import partial
5 from typing import Annotated, Any, Literal, TypeAlias, cast
6
7 import numpy as np
8 import regex as re
9 import torch
10 from einops import rearrange
11 from PIL import Image
12 from timm.layers import LayerNorm2d
13 from timm.layers.pos_embed import resample_abs_pos_embed
14 from timm.models.regnet import RegStage
15 from torch import nn
16 from transformers import BatchFeature
17 from transformers.modeling_outputs import BaseModelOutput
18 from transformers.models.qwen2_vl.configuration_qwen2_vl import Qwen2VLVisionConfig
19
20 from vllm.config import VllmConfig
21 from vllm.config.multimodal import MultiModalDummyOptions
22 from vllm.inputs import MultiModalDataDict
23 from vllm.logger import init_logger
24 from vllm.multimodal import MULTIMODAL_REGISTRY
25 from vllm.multimodal.inputs import (
26 MultiModalFieldConfig,
27 MultiModalKwargsItems,
28 )
29 from vllm.multimodal.parse import ImageSize, MultiModalDataItems
30 from vllm.multimodal.processing import (
31 BaseDummyInputsBuilder,
32 BaseMultiModalProcessor,
33 BaseProcessingInfo,
34 PromptReplacement,
35 PromptUpdate,
36 cached_encode,
37 )
38 from vllm.sequence import IntermediateTensors
39 from vllm.utils.import_utils import resolve_obj_by_qualname
40 from vllm.utils.tensor_schema import TensorSchema, TensorShape
41 from vllm.utils.torch_utils import async_tensor_h2d
42
43 from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
44 from .qwen2_vl import Qwen2VisionTransformer
45 from .utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
46
47 logger = init_logger(__name__)
48
49
50 class KananaVImagePixelInputs(TensorSchema):
51 """Dimensions:
52 - np: The total number of patches over all images in the batch
53 - cps: Number of channels * patch_size * patch_size
54 - ni: Number of images
55 """
56
57 type: Literal["pixel_values"]
58
59 pixel_values: Annotated[
60 torch.Tensor,
61 TensorShape("np", "cps"),
62 ]
63
64 vision_grid_thw: Annotated[
65 torch.Tensor,
66 TensorShape("ni", 3),
67 ]
68
69
70 KananaVImageInputs: TypeAlias = KananaVImagePixelInputs
71
72
73 def build_pos_embeds(
74 config: Qwen2VLVisionConfig,
75 num_input_tokens: int,
76 vision_hidden_size: int,
77 ) -> nn.Parameter | None:
78 """Build positional embeddings for the visual encoder output."""
79 if config.pos_emb:
80 pos_emb = nn.Parameter(torch.zeros(1, num_input_tokens, vision_hidden_size))
81 nn.init.trunc_normal_(pos_emb, mean=0.0, std=0.02)
82 else:
83 pos_emb = None
84
85 return pos_emb
86
87
88 def build_mlp(
89 depth: int,
90 hidden_size: int,
91 output_hidden_size: int,
92 ) -> nn.Sequential:
93 """Simple SiLU-activated MLP used as a projector readout."""
94 layers = [nn.Linear(hidden_size, output_hidden_size)]
95 for _ in range(1, depth):
96 layers.append(nn.SiLU())
97 layers.append(nn.Linear(output_hidden_size, output_hidden_size))
98 return nn.Sequential(*layers)
99
100
101 class PatchMerge(nn.Module):
102 """Merge neighboring patches spatially to reduce resolution."""
103
104 def __init__(self, merge_size: int) -> None:
105 super().__init__()
106 self.merge_size = merge_size
107
108 def forward(
109 self,
110 x: torch.Tensor,
111 channel_last: bool = False,
112 ) -> torch.Tensor:
113 """Merge patches by `merge_size x merge_size`."""
114 if channel_last:
115 x = rearrange(x, "B H W D -> B D H W")
116 _, _, H, W = x.shape
117 merged_x = rearrange(
118 x,
119 "B D (H h2) (W w2) -> B (D h2 w2) H W",
120 h2=self.merge_size,
121 w2=self.merge_size,
122 )
123 return merged_x
124
125
126 class DynamicCAbstractor(nn.Module):
127 """Dynamic C-Abstractor based on RegNet blocks."""
128
129 def __init__(
130 self,
131 config: Qwen2VLVisionConfig,
132 num_input_tokens: int,
133 ) -> None:
134 super().__init__()
135 assert hasattr(config, "merge_size"), "merge_size must be provided."
136 self.config = config
137 self.merge_size = config.merge_size
138 self.pos_emb_size = config.pos_emb_size
139 if num_input_tokens == -1:
140 num_input_tokens = config.pos_emb_size
141 self.num_input_tokens = num_input_tokens
142 self.pos_emb = build_pos_embeds(
143 config, num_input_tokens, config.encoder_hidden_size
144 )
145 self.build_net()
146
147 def _load_from_state_dict(self, state_dict, *args, **kwargs) -> None:
148 if not state_dict:
149 return
150
151 if self.pos_emb is not None:
152 key_re = re.compile(r"[\w,.]*abstractor[\w,.]*pos_emb")
153 pos_emb_key = None
154 for key in state_dict:
155 if key_re.match(key):
156 pos_emb_key = key
157 break
158
159 assert pos_emb_key is not None
160 # update old ckpt compatible with current code
161 pos_emb = state_dict[pos_emb_key]
162 if pos_emb.size(1) == self.pos_emb.size(1) + 1:
163 # remove obsolete first pos emb (for cls token originally)
164 state_dict[pos_emb_key] = pos_emb[:, 1:]
165
166 super()._load_from_state_dict(state_dict, *args, **kwargs)
167
168 def build_net(self) -> None:
169 encoder_hidden_size = self.config.encoder_hidden_size
170 hidden_size = self.config.hidden_size
171 output_hidden_size = self.config.output_hidden_size
172 depth = self.config.depth
173 mlp_depth = self.config.mlp_depth
174
175 RegBlock = partial(
176 RegStage,
177 stride=1,
178 dilation=1,
179 act_layer=nn.SiLU,
180 norm_layer=LayerNorm2d,
181 )
182
183 s1 = RegBlock(
184 depth,
185 encoder_hidden_size,
186 hidden_size,
187 )
188 sampler = PatchMerge(merge_size=self.merge_size)
189 s2 = RegBlock(
190 depth,
191 self.merge_size**2 * hidden_size,
192 hidden_size,
193 )
194
195 if depth:
196 self.net = nn.ModuleList([s1, sampler, s2])
197 self.readout = build_mlp(mlp_depth, hidden_size, output_hidden_size)
198 else:
199 self.net = sampler
200 self.readout = build_mlp(mlp_depth, encoder_hidden_size, output_hidden_size)
201
202 def forward(
203 self,
204 flattened_visual_embeds: torch.Tensor,
205 grid_thw: torch.Tensor,
206 **unused_kwargs: object,
207 ) -> BaseModelOutput:
208 """Apply the dynamic abstractor over flattened visual embeddings."""
209 n_token_loc = torch.prod(grid_thw, dim=1)
210 split_visual_embeds = torch.split(flattened_visual_embeds, n_token_loc.tolist())
211
212 flattened_visual_embeds = []
213 for _visual_embeds, _grid_thw in zip(split_visual_embeds, grid_thw):
214 T, H, W = _grid_thw
215 assert T == 1, "T must be 1. Video is not supported yet."
216 reshaped_visual_embeds = rearrange(
217 _visual_embeds, "(t h w) d -> 1 t h w d", t=T, h=H, w=W
218 )
219 # remove temporal dim
220 reshaped_visual_embeds = reshaped_visual_embeds[:, 0]
221
222 if self.pos_emb is not None:
223 # interpolate pos emb and add to visual embeds
224 _local_pos_emb = resample_abs_pos_embed(
225 posemb=self.pos_emb,
226 old_size=tuple([int(self.pos_emb_size**0.5)] * 2),
227 new_size=(H, W),
228 num_prefix_tokens=0,
229 )
230 _local_pos_emb = rearrange(
231 _local_pos_emb,
232 "1 (h w) d -> 1 h w d",
233 h=H,
234 w=W,
235 )
236 reshaped_visual_embeds = reshaped_visual_embeds + _local_pos_emb
237
238 reshaped_visual_embeds = self._forward(
239 reshaped_visual_embeds,
240 input_size=(H, W),
241 )
242 flattened_visual_embeds.append(reshaped_visual_embeds)
243 reshaped_visual_embeds = torch.cat(flattened_visual_embeds, dim=0)
244 return BaseModelOutput(last_hidden_state=reshaped_visual_embeds)
245
246 def _forward(
247 self,
248 x: torch.Tensor,
249 input_size: tuple[int, int],
250 ) -> torch.Tensor:
251 h, w = input_size
252 x = rearrange(x, "1 h w d -> 1 d h w", h=h, w=w)
253 if self.config.depth:
254 x = self.net[0](x)
255 x = self.net[1](x)
256 x = self.net[2](x)
257 else:
258 # When depth=0, self.net is a single PatchMerge module
259 x = self.net(x)
260 x = rearrange(x, "1 d h w -> (h w) d")
261 x = self.readout(x)
262 return x
263
264
265 class CustomQwen2VLVE(Qwen2VisionTransformer):
266 """Thin wrapper around the Qwen2-VL used as a vision encoder.
267
268 This mirrors the original HF-based vision encoder used in Kanana-V, but
269 reuses vLLM's optimized `Qwen2VisionTransformer` building blocks.
270 """
271
272 def __init__(self, config: Qwen2VLVisionConfig) -> None:
273 super().__init__(
274 vision_config=config,
275 norm_eps=getattr(config, "rms_norm_eps", 1e-6),
276 quant_config=None,
277 prefix="",
278 )
279
280 # Kanana-V uses its own projector/abstractor instead of the Qwen2
281 # built-in patch merger, so we drop the merger module to keep the
282 # parameter set compatible with the original checkpoint.
283 if hasattr(self, "merger"):
284 del self.merger
285
286 @classmethod
287 def _from_config(cls, config: Qwen2VLVisionConfig) -> "CustomQwen2VLVE":
288 """Drop-in replacement for the HF `_from_config` constructor."""
289 return cls(config)
290
291 def forward( # type: ignore[override]
292 self,
293 pixel_values: torch.Tensor,
294 grid_thw: torch.Tensor,
295 output_hidden_states: bool | None = None,
296 return_dict: bool | None = None,
297 ) -> tuple | BaseModelOutput:
298 """Run the vision transformer and optionally return intermediate states.
299
300 Unlike the base `Qwen2VisionTransformer`, this wrapper exposes the
301 pre-merger patch-level representations and a HF-style `BaseModelOutput`
302 so that the existing projector / abstractor code can be reused.
303 """
304 assert return_dict, "Only return_dict=True is supported."
305
306 # Patchify
307 x = pixel_values.to(device=self.device, dtype=self.dtype)
308 x = self.patch_embed(x) # (num_patches, embed_dim)
309
310 # Prepare grid and rotary embeddings – mirror base implementation.
311 if isinstance(grid_thw, list):
312 grid_thw_list = grid_thw
313 grid_thw_np = np.array(grid_thw, dtype=np.int32)
314 else:
315 grid_thw_list = grid_thw.tolist()
316 grid_thw_np = grid_thw.cpu().numpy()
317
318 rotary_pos_emb_cos, rotary_pos_emb_sin = self.rot_pos_emb(grid_thw_list)
319
320 # Compute cu_seqlens in numpy then move to device, same as base model.
321 cu_seqlens = np.repeat(
322 grid_thw_np[:, 1] * grid_thw_np[:, 2],
323 grid_thw_np[:, 0],
324 ).cumsum(axis=0, dtype=np.int32)
325 cu_seqlens = np.concatenate([np.zeros(1, dtype=np.int32), cu_seqlens])
326 cu_seqlens = async_tensor_h2d(cu_seqlens, device=self.device)
327
328 # Shape to (S, B, D) with batch dimension 1 as expected by the blocks.
329 x = x.unsqueeze(1)
330
331 # Pre-compute seqlens for attention backend.
332 max_seqlen = self.compute_attn_mask_seqlen(cu_seqlens)
333
334 encoder_states: tuple[torch.Tensor, ...] = ()
335
336 for blk in self.blocks:
337 if output_hidden_states:
338 # Store patch-level states (S, D).
339 encoder_states += (x.squeeze(1),)
340
341 x = blk(
342 x,
343 cu_seqlens=cu_seqlens,
344 rotary_pos_emb_cos=rotary_pos_emb_cos,
345 rotary_pos_emb_sin=rotary_pos_emb_sin,
346 max_seqlen=max_seqlen,
347 )
348
349 # Final hidden state at patch level (S, D).
350 hidden_states = x.squeeze(1)
351 all_hidden_states = None
352 if output_hidden_states:
353 all_hidden_states = encoder_states + (hidden_states,)
354
355 if not return_dict:
356 return tuple(v for v in [hidden_states, all_hidden_states] if v is not None)
357 return BaseModelOutput(
358 last_hidden_state=hidden_states,
359 hidden_states=all_hidden_states,
360 )
361
362 def get_num_tokens(self) -> int:
363 # Not used in the current Kanana-V pipeline, kept for API compatibility.
364 return -1
365
366
367 class KananaVProcessingInfo(BaseProcessingInfo):
368 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
369 return {"image": None}
370
371 def get_image_size_with_most_features(self) -> ImageSize:
372 max_image_size, _ = self._get_vision_info(
373 image_width=9999,
374 image_height=9999,
375 num_frames=1,
376 )
377 return max_image_size
378
379 def _get_vision_info(
380 self,
381 *,
382 image_width: int,
383 image_height: int,
384 num_frames: int = 1,
385 do_resize: bool = True,
386 ) -> tuple[ImageSize, int]:
387 image_processor = self.ctx.get_hf_processor().image_processor
388 smart_resize = resolve_obj_by_qualname(
389 f"{type(image_processor).__module__}.smart_resize"
390 )
391
392 hf_config = self.get_hf_config()
393 vision_config = hf_config.vision_config
394 patch_size = vision_config.patch_size
395 merge_size = vision_config.spatial_merge_size
396 temporal_patch_size = vision_config.temporal_patch_size
397
398 if do_resize:
399 resized_height, resized_width = smart_resize(
400 height=image_height,
401 width=image_width,
402 factor=patch_size * merge_size,
403 min_pixels=image_processor.min_pixels,
404 max_pixels=image_processor.max_pixels,
405 )
406 preprocessed_size = ImageSize(width=resized_width, height=resized_height)
407 else:
408 preprocessed_size = ImageSize(width=image_width, height=image_height)
409
410 # NOTE: Frames are padded to be divisible by `temporal_patch_size`
411 # https://github.com/huggingface/transformers/blob/v5.13.0/src/transformers/models/qwen2_vl/video_processing_qwen2_vl.py#L249-L252
412 padded_num_frames = num_frames + (-num_frames % temporal_patch_size)
413
414 grid_t = max(padded_num_frames // temporal_patch_size, 1)
415 grid_h = preprocessed_size.height // patch_size
416 grid_w = preprocessed_size.width // patch_size
417
418 num_patches = grid_t * grid_h * grid_w
419 num_vision_tokens = num_patches // (merge_size**2)
420
421 return preprocessed_size, num_vision_tokens
422
423 def get_mm_max_tokens_per_item(
424 self,
425 seq_len: int,
426 mm_counts: Mapping[str, int],
427 ) -> Mapping[str, int]:
428 target_width, target_height = self.get_image_size_with_most_features()
429 num_vision_tokens = self._get_vision_info(
430 image_width=target_width,
431 image_height=target_height,
432 num_frames=1,
433 )[1]
434 return {"image": num_vision_tokens}
435
436
437 class KananaVDummyInputsBuilder(BaseDummyInputsBuilder[KananaVProcessingInfo]):
438 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
439 num_images = mm_counts.get("image", 0)
440 return "<image>" * num_images
441
442 def get_dummy_mm_data(
443 self,
444 seq_len: int,
445 mm_counts: Mapping[str, int],
446 mm_options: MultiModalDummyOptions,
447 ) -> MultiModalDataDict:
448 num_images = mm_counts.get("image", 0)
449 return {
450 "image": self._get_dummy_images(
451 width=9999, height=9999, num_images=num_images
452 ),
453 }
454
455
456 class KananaVMultiModalProcessor(BaseMultiModalProcessor[KananaVProcessingInfo]):
457 """vLLM multimodal processor for Kanana-V (text + image)."""
458
459 @property
460 def media_token_id(self) -> int:
461 return self.info.get_hf_config().text_config.eos_token_id + 1
462
463 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
464 return self.dummy_inputs.get_dummy_text(mm_counts)
465
466 def _apply_hf_processor_main(
467 self,
468 mm_items: MultiModalDataItems,
469 hf_kwargs: Mapping[str, object],
470 ) -> BatchFeature:
471 """Run the underlying HF processor on text and image data."""
472 hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
473 mm_items, hf_kwargs
474 )
475
476 if not hf_data or not hf_data.get("images", []):
477 return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
478
479 prompt_text = hf_data.pop("text")
480 assert isinstance(prompt_text, str)
481
482 # Images
483 image_inputs = cast(list[Any], hf_data.get("images", []))
484 pixel_sizes = []
485 if not isinstance(image_inputs[0], Image.Image):
486 image_inputs = [Image.fromarray(image) for image in image_inputs]
487
488 image_processor = self.info.get_hf_processor().image_processor
489 processor_output = [image_processor(image) for image in image_inputs]
490 pixel_values = [o["pixel_values"] for o in processor_output]
491 image_meta_list = [o["image_meta"] for o in processor_output]
492 # list of dict -> dict of list
493 image_meta = {k: [d[k] for d in image_meta_list] for k in image_meta_list[0]}
494
495 for pixel_value in pixel_values:
496 pixel_sizes.append(pixel_value.shape[0])
497 # flattened pixel_values for single example (already includes batch dim)
498 pixel_values = torch.concat(pixel_values, dim=0)
499
500 tokenizer = self.info.get_tokenizer()
501 media_token = tokenizer.convert_ids_to_tokens([self.media_token_id])[0]
502 prompt_replaced = prompt_text.replace("<image>", media_token)
503 input_ids = tokenizer.encode(prompt_replaced)
504 input_ids = torch.tensor(input_ids)
505
506 # Ensure HF output is consistent with vLLM prompt-update expectations:
507 # if the HF tokenizer emits exactly 1 placeholder token per image, expand
508 # it to `T*H*W` placeholder tokens per image so placeholder detection works.
509 num_images = len(image_inputs)
510 image_token_thw = torch.tensor(image_meta["image_token_thw"])
511 per_image_token_counts = image_token_thw.prod(dim=1).tolist()
512 expected_total = int(sum(int(x) for x in per_image_token_counts))
513
514 n_placeholders = int((input_ids == self.media_token_id).sum().item())
515 if n_placeholders == num_images and expected_total != num_images:
516 expanded: list[int] = []
517 img_i = 0
518 for tok in input_ids.tolist():
519 if tok == self.media_token_id and img_i < num_images:
520 expanded.extend(
521 [self.media_token_id] * int(per_image_token_counts[img_i])
522 )
523 img_i += 1
524 else:
525 expanded.append(tok)
526 input_ids = input_ids.new_tensor(expanded)
527
528 combined_outputs = dict(
529 # Add batch dimension to input_ids.
530 input_ids=input_ids.unsqueeze(0),
531 pixel_values=pixel_values,
532 vision_grid_thw=torch.tensor(image_meta["vision_grid_thw"]),
533 image_token_thw=torch.tensor(image_meta["image_token_thw"]),
534 pixel_sizes=torch.tensor(pixel_sizes),
535 )
536 processed_data = BatchFeature(combined_outputs, tensor_type="pt")
537 return self._finalize_hf_mm_data(
538 hf_data, hf_kwargs, passthrough_data, processed_data
539 )
540
541 def _get_prompt_updates(
542 self,
543 mm_items: MultiModalDataItems,
544 hf_processor_mm_kwargs: Mapping[str, object],
545 out_mm_kwargs: MultiModalKwargsItems,
546 ) -> Sequence[PromptUpdate]:
547 tokenizer = self.info.get_tokenizer()
548
549 def get_replacement(idx: int) -> list[int]:
550 out_item = out_mm_kwargs["image"][idx]
551 image_token_thw = out_item["image_token_thw"].data
552 assert isinstance(image_token_thw, torch.Tensor)
553
554 num_tokens = int(image_token_thw.prod().item())
555 return [self.media_token_id] * num_tokens
556
557 return [
558 PromptReplacement(
559 modality="image",
560 target=cached_encode(tokenizer, "<image>", add_special_tokens=False),
561 replacement=get_replacement,
562 ),
563 ]
564
565 def _get_mm_fields_config(
566 self,
567 hf_inputs: BatchFeature,
568 hf_processor_mm_kwargs: Mapping[str, object],
569 ) -> Mapping[str, MultiModalFieldConfig]:
570 pixel_sizes = hf_inputs.get("pixel_sizes", torch.empty(0))
571
572 mm_fields_config = dict(
573 pixel_values=MultiModalFieldConfig.flat_from_sizes("image", pixel_sizes),
574 vision_grid_thw=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
575 image_token_thw=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
576 )
577 return mm_fields_config
578
579
580 @MULTIMODAL_REGISTRY.register_processor(
581 KananaVMultiModalProcessor,
582 info=KananaVProcessingInfo,
583 dummy_inputs=KananaVDummyInputsBuilder,
584 )
585 class KananaVForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
586 @classmethod
587 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
588 if modality.startswith("image"):
589 return "<image>"
590 else:
591 raise ValueError(f"Unsupported modality: {modality}")
592
593 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
594 super().__init__()
595
596 config = vllm_config.model_config.hf_config
597 self.config = config
598
599 with self._mark_tower_model(vllm_config, "image"):
600 self.vision_model = CustomQwen2VLVE._from_config(config.vision_config)
601 self.abstractor = DynamicCAbstractor(
602 config.projector_config,
603 num_input_tokens=self.vision_model.get_num_tokens(),
604 )
605
606 with self._mark_language_model(vllm_config):
607 self.language_model = init_vllm_registered_model(
608 vllm_config=vllm_config,
609 hf_config=config.text_config,
610 prefix=maybe_prefix(prefix, "model"),
611 architectures=["LlamaForCausalLM"],
612 )
613
614 self.make_empty_intermediate_tensors = (
615 self.language_model.make_empty_intermediate_tensors
616 )
617
618 def _parse_and_validate_image_input(
619 self, **kwargs: object
620 ) -> KananaVImageInputs | None:
621 pixel_values = kwargs.pop("pixel_values", None)
622 vision_grid_thw = kwargs.pop("vision_grid_thw", None)
623
624 if pixel_values is None:
625 return None
626
627 if vision_grid_thw is None:
628 raise ValueError(
629 "vision_grid_thw is required when pixel_values is provided"
630 )
631
632 # Normalize pixel_values to 2D tensor (num_patches, channels*patch*patch)
633 if isinstance(pixel_values, torch.Tensor):
634 if pixel_values.ndim == 2:
635 pass # Already in expected shape
636 elif pixel_values.ndim == 3:
637 pixel_values = pixel_values.flatten(0, 1)
638 else:
639 raise ValueError(
640 f"pixel_values should be 2D or batched 3D tensor. "
641 f"Got ndim: {pixel_values.ndim} "
642 f"(shape={pixel_values.shape})"
643 )
644 else:
645 pixel_values = torch.concat(pixel_values)
646
647 # Normalize vision_grid_thw to 2D tensor (num_images, 3)
648 if isinstance(vision_grid_thw, torch.Tensor):
649 if vision_grid_thw.ndim == 3:
650 vision_grid_thw = vision_grid_thw.flatten(0, 1)
651 else:
652 vision_grid_thw = torch.concat(vision_grid_thw)
653
654 return KananaVImagePixelInputs(
655 type="pixel_values",
656 pixel_values=pixel_values,
657 vision_grid_thw=vision_grid_thw,
658 )
659
660 def _process_image_input(self, image_input: KananaVImageInputs) -> torch.Tensor:
661 pixel_values = image_input["pixel_values"]
662 vision_grid_thw = image_input["vision_grid_thw"]
663
664 image_metas = {"vision_grid_thw": vision_grid_thw}
665 visual_embeds = self.forward_and_project_vision(pixel_values, image_metas)
666
667 merge_size = self.abstractor.merge_size
668 batch_size = vision_grid_thw.size(0)
669 multi_modal_embeddings: tuple[torch.Tensor, ...] = ()
670 sample_index = 0
671 for i in range(batch_size):
672 t, h, w = (
673 vision_grid_thw[i][0],
674 vision_grid_thw[i][1] // merge_size,
675 vision_grid_thw[i][2] // merge_size,
676 )
677 num_tokens = t * h * w
678 visual_embed = visual_embeds[sample_index : sample_index + num_tokens]
679 multi_modal_embeddings += (visual_embed,)
680 sample_index += num_tokens
681
682 return multi_modal_embeddings
683
684 def _get_visual_feature_at(
685 self,
686 v_output: Sequence[torch.Tensor],
687 layer_index: int | list[int] | tuple[int, ...],
688 ) -> torch.Tensor:
689 if isinstance(layer_index, (list, tuple)):
690 visual_features = torch.stack(v_output, dim=1)[
691 :, layer_index
692 ] # [B, n_scales, L, dim]
693 else:
694 visual_features = v_output[layer_index] # [B, L, dim]
695 return visual_features
696
697 def forward_vision(
698 self,
699 pixel_values: torch.Tensor,
700 image_metas: dict,
701 ) -> torch.Tensor:
702 vision_model_args = {
703 "pixel_values": pixel_values,
704 "return_dict": True,
705 "output_hidden_states": True,
706 "grid_thw": image_metas["vision_grid_thw"],
707 }
708 v_outputs = self.vision_model(**vision_model_args)
709 layer_index = self.config.projector_config.feature_layer_index
710 visual_features = self._get_visual_feature_at(
711 v_outputs.hidden_states, layer_index
712 )
713 return visual_features
714
715 def forward_projector(
716 self,
717 visual_features: torch.Tensor,
718 image_metas: dict,
719 ) -> torch.Tensor:
720 visual_embeds = self.abstractor(
721 visual_features,
722 grid_thw=image_metas["vision_grid_thw"],
723 )["last_hidden_state"]
724 return visual_embeds
725
726 def forward_and_project_vision(
727 self,
728 pixel_values: torch.Tensor,
729 image_metas: dict,
730 ) -> torch.Tensor:
731 assert pixel_values is not None
732 visual_features = self.forward_vision(pixel_values, image_metas=image_metas)
733 visual_embeds = self.forward_projector(visual_features, image_metas=image_metas)
734 return visual_embeds
735
736 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
737 image_input = self._parse_and_validate_image_input(**kwargs)
738 if image_input is None:
739 return []
740
741 return self._process_image_input(image_input)
742
743 def forward(
744 self,
745 input_ids: torch.Tensor | None,
746 positions: torch.Tensor,
747 intermediate_tensors: IntermediateTensors | None = None,
748 inputs_embeds: torch.Tensor | None = None,
749 **kwargs,
750 ):
751 if intermediate_tensors is not None:
752 inputs_embeds = None
753
754 hidden_states = self.language_model(
755 input_ids=input_ids,
756 positions=positions,
757 intermediate_tensors=intermediate_tensors,
758 inputs_embeds=inputs_embeds,
759 )
760
761 return hidden_states
762
763 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
764 return self.language_model.compute_logits(hidden_states)
765
766 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
767 loader = AutoWeightsLoader(self)
768 return loader.load_weights(weights)