main
py 934 lines 35.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """vLLM implementation of Granite 4 Vision.
4
5 Uses GraniteForCausalLM as the language backbone with SigLIP vision encoder
6 and deepstack feature injection via WindowQFormer projectors.
7
8 LoRA support: use --enable-lora --default-mm-loras for LM-only LoRA adapters.
9 """
10
11 import math
12 from collections.abc import Iterable, Mapping
13 from fractions import Fraction
14 from itertools import islice
15
16 import torch
17 import torch.nn as nn
18 from transformers import BatchFeature
19 from transformers.models.blip_2.configuration_blip_2 import Blip2QFormerConfig
20 from transformers.models.llava_next.modeling_llava_next import (
21 get_anyres_image_grid_shape,
22 image_size_to_num_patches,
23 unpad_image,
24 )
25
26 from vllm.compilation.decorators import support_torch_compile
27 from vllm.config import CacheConfig, VllmConfig
28 from vllm.distributed.parallel_state import get_pp_group
29 from vllm.logger import init_logger
30 from vllm.model_executor.layers.logits_processor import LogitsProcessor
31 from vllm.model_executor.layers.quantization import QuantizationConfig
32 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
33 from vllm.model_executor.models.granite import GraniteForCausalLM, GraniteModel
34 from vllm.model_executor.models.interfaces import (
35 MultiModalEmbeddings,
36 SupportsLoRA,
37 SupportsMultiModal,
38 SupportsPP,
39 )
40 from vllm.model_executor.models.llava import LlavaDummyInputsBuilder
41 from vllm.model_executor.models.llava_next import (
42 BaseLlavaNextMultiModalProcessor,
43 LlavaNextImageEmbeddingInputs,
44 LlavaNextImageInputs,
45 LlavaNextImagePixelInputs,
46 LlavaNextProcessingInfo,
47 )
48 from vllm.model_executor.models.module_mapping import MultiModelKeys
49 from vllm.model_executor.models.siglip import SiglipVisionModel
50 from vllm.model_executor.models.utils import (
51 AutoWeightsLoader,
52 PPMissingLayer,
53 WeightsMapper,
54 maybe_prefix,
55 )
56 from vllm.multimodal import MULTIMODAL_REGISTRY
57 from vllm.multimodal.inputs import MultiModalFieldConfig
58 from vllm.sequence import IntermediateTensors
59
60 from .blip2 import Blip2QFormerModel
61
62 logger = init_logger(__name__)
63
64
65 # ---------------------------------------------------------------------------
66 # Downsampler modules (translated from HF downsampling.py)
67 # ---------------------------------------------------------------------------
68
69
70 class InterpolateDownsampler:
71 """Spatial downsampling via area interpolation."""
72
73 def __init__(self, config, mode="area"):
74 self.orig_image_side = (
75 config.vision_config.image_size // config.vision_config.patch_size
76 )
77 self.new_image_side = int(
78 self.orig_image_side * Fraction(config.downsample_rate)
79 )
80 self.mode = mode
81
82 def __call__(self, image_features: torch.Tensor) -> torch.Tensor:
83 batch_size, _, dim = image_features.size()
84 up_shape = [batch_size, self.orig_image_side, self.orig_image_side, dim]
85 large = image_features.view(up_shape).permute(0, 3, 1, 2)
86 small = torch.nn.functional.interpolate(
87 large,
88 size=(self.new_image_side, self.new_image_side),
89 mode=self.mode,
90 )
91 return small.permute(0, 2, 3, 1).flatten(1, 2)
92
93
94 class SpatialOffsetDownsampler:
95 """Sample one position from each 2x2 block (offset 0-3 = TL/TR/BL/BR)."""
96
97 def __init__(self, config, offset: int = 0):
98 self.orig_image_side = (
99 config.vision_config.image_size // config.vision_config.patch_size
100 )
101 self.new_image_side = self.orig_image_side // 2
102 offsets = [(0, 0), (0, 1), (1, 0), (1, 1)]
103 self.offset_h, self.offset_w = offsets[offset]
104
105 def __call__(self, image_features: torch.Tensor) -> torch.Tensor:
106 B, _, C = image_features.shape
107 features_2d = image_features.reshape(
108 B, self.orig_image_side, self.orig_image_side, C
109 )
110 n = self.new_image_side
111 blocks = features_2d.reshape(B, n, 2, n, 2, C)
112 sampled = blocks[:, :, self.offset_h, :, self.offset_w, :]
113 return sampled.reshape(B, -1, C)
114
115
116 class WindowQFormerDownsampler(nn.Module):
117 """Window-based QFormer downsampler (matches HF downsampling.py exactly)."""
118
119 def __init__(
120 self,
121 config,
122 quant_config: QuantizationConfig | None = None,
123 cache_config: CacheConfig | None = None,
124 spatial_offset: int | None = None,
125 prefix: str = "",
126 ):
127 super().__init__()
128 llm_hidden_size = config.text_config.hidden_size
129 vision_hidden_size = config.vision_config.hidden_size
130
131 self.dropout = nn.Dropout(config.projector_dropout)
132
133 self.downsampler: SpatialOffsetDownsampler | InterpolateDownsampler
134 if spatial_offset is not None:
135 self.downsampler = SpatialOffsetDownsampler(config, offset=spatial_offset)
136 else:
137 self.downsampler = InterpolateDownsampler(config)
138
139 qformer_config = Blip2QFormerConfig(
140 hidden_size=vision_hidden_size,
141 num_attention_heads=vision_hidden_size // 64,
142 intermediate_size=3072,
143 num_hidden_layers=1,
144 encoder_hidden_size=vision_hidden_size,
145 cross_attention_frequency=1,
146 max_position_embeddings=2048,
147 use_qformer_text_input=False,
148 )
149 self.qformer = Blip2QFormerModel(
150 qformer_config,
151 quant_config=quant_config,
152 cache_config=cache_config,
153 prefix=maybe_prefix(prefix, "qformer"),
154 )
155
156 self.image_side = (
157 config.vision_config.image_size // config.vision_config.patch_size
158 )
159 q, w = config.downsample_rate.split("/")
160 self.query_side, self.window_side = int(q), int(w)
161 self.query_length = self.query_side**2
162
163 embed_std = 1 / math.sqrt(vision_hidden_size)
164 self.norm = nn.LayerNorm(vision_hidden_size, eps=1e-6)
165 self.query = nn.Parameter(
166 torch.randn(1, self.query_length, vision_hidden_size) * embed_std
167 )
168 self.image_positions = nn.Parameter(
169 torch.randn(1, self.window_side**2, vision_hidden_size) * embed_std
170 )
171 self.out_linear = nn.Linear(vision_hidden_size, llm_hidden_size, bias=True)
172
173 def _win(self, x: torch.Tensor, side: int, win: int) -> torch.Tensor:
174 """(B, side*side, C) → (B*n*n, win*win, C) where n=side//win."""
175 B, _, C = x.shape
176 n = side // win
177 return (
178 x.view(B, side, side, C)
179 .view(B, n, win, n, win, C)
180 .transpose(2, 3)
181 .flatten(0, 2)
182 .flatten(1, 2)
183 )
184
185 def _unwin(self, xw: torch.Tensor, n: int, win: int) -> torch.Tensor:
186 """(B*n*n, win*win, C) → (B, (n*win)^2, C)."""
187 Bnn, _, C = xw.shape
188 B = Bnn // (n * n)
189 side = n * win
190 return (
191 xw.view(B, n, n, win, win, C)
192 .transpose(2, 3)
193 .contiguous()
194 .view(B, side, side, C)
195 .flatten(1, 2)
196 )
197
198 def forward(self, image_features: torch.Tensor) -> torch.Tensor:
199 B, HW, C = image_features.shape
200 assert self.image_side * self.image_side == HW
201 n = self.image_side // self.window_side
202
203 image_features = self.norm(image_features)
204 enc = self._win(image_features, self.image_side, self.window_side)
205
206 downsampled = self.downsampler(image_features)
207 new_side = n * self.query_side
208 downsampled_w = self._win(downsampled, new_side, self.query_side)
209
210 query_embeds = self.query + downsampled_w
211 encoder_embeds = self.dropout(enc + self.image_positions)
212 out_w = self.qformer(
213 query_embeds=query_embeds,
214 encoder_hidden_states=encoder_embeds,
215 )
216
217 out = self._unwin(out_w, n=n, win=self.query_side)
218 out = self.dropout(out)
219 return self.out_linear(out)
220
221
222 # ---------------------------------------------------------------------------
223 # LLM subclasses with deepstack injection in the layer loop
224 # ---------------------------------------------------------------------------
225
226
227 @support_torch_compile(
228 dynamic_arg_dims={
229 "input_ids": 0,
230 "positions": 0,
231 "intermediate_tensors": 0,
232 "inputs_embeds": 0,
233 "deepstack_input_embeds": 0,
234 }
235 )
236 class Granite4VisionLLMModel(GraniteModel):
237 """GraniteModel with deepstack feature injection in the layer loop."""
238
239 def forward(
240 self,
241 input_ids: torch.Tensor | None,
242 positions: torch.Tensor,
243 intermediate_tensors: IntermediateTensors | None = None,
244 inputs_embeds: torch.Tensor | None = None,
245 deepstack_input_embeds: IntermediateTensors | None = None,
246 ) -> torch.Tensor | IntermediateTensors:
247 if get_pp_group().is_first_rank:
248 if inputs_embeds is not None:
249 hidden_states = inputs_embeds
250 else:
251 hidden_states = self.embed_input_ids(input_ids)
252 hidden_states = hidden_states * self.config.embedding_multiplier
253 else:
254 assert intermediate_tensors is not None
255 hidden_states = intermediate_tensors["hidden_states"]
256 # Recover deepstack features forwarded from the previous PP rank.
257 if deepstack_input_embeds is None:
258 ds_keys = [
259 k for k in intermediate_tensors.tensors if k.startswith("ds_")
260 ]
261 if ds_keys:
262 deepstack_input_embeds = IntermediateTensors(
263 {k: intermediate_tensors[k] for k in ds_keys}
264 )
265
266 for layer_idx, layer in islice(
267 enumerate(self.layers), self.start_layer, self.end_layer
268 ):
269 if deepstack_input_embeds is not None:
270 key = f"ds_{layer_idx}"
271 if key in deepstack_input_embeds.tensors:
272 feat = deepstack_input_embeds[key]
273 # Resize to match hidden_states in case of CUDA graph padding
274 num_tokens = hidden_states.size(0)
275 buf_len = feat.shape[0]
276 if buf_len != num_tokens:
277 feat = torch.nn.functional.pad(
278 feat[:num_tokens],
279 (0, 0, 0, max(0, num_tokens - buf_len)),
280 )
281 hidden_states = hidden_states + feat
282 hidden_states = layer(positions, hidden_states)
283
284 if not get_pp_group().is_last_rank:
285 # Forward hidden_states and any deepstack features for later ranks.
286 it = {"hidden_states": hidden_states}
287 if deepstack_input_embeds is not None:
288 remaining = {
289 k: v
290 for k, v in deepstack_input_embeds.tensors.items()
291 if int(k.split("_")[1]) >= self.end_layer
292 }
293 it.update(remaining)
294 return IntermediateTensors(it)
295
296 hidden_states = self.norm(hidden_states)
297 return hidden_states
298
299
300 class Granite4VisionLLMForCausalLM(GraniteForCausalLM):
301 """GraniteForCausalLM backed by Granite4VisionLLMModel."""
302
303 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
304 nn.Module.__init__(self)
305 config = vllm_config.model_config.hf_config
306 quant_config = vllm_config.quant_config
307 self.config = config
308 self.quant_config = quant_config
309 self.model = Granite4VisionLLMModel(
310 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
311 )
312 if get_pp_group().is_last_rank:
313 self.lm_head = ParallelLMHead(
314 config.vocab_size,
315 config.hidden_size,
316 quant_config=quant_config,
317 prefix=maybe_prefix(prefix, "lm_head"),
318 )
319 if config.tie_word_embeddings:
320 self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
321 logit_scale = getattr(config, "logit_scale", 1.0)
322 if hasattr(config, "logits_scaling"):
323 logit_scale /= config.logits_scaling
324 self.logits_processor = LogitsProcessor(
325 config.vocab_size, scale=logit_scale
326 )
327 else:
328 self.lm_head = PPMissingLayer()
329
330 def make_empty_intermediate_tensors(
331 self, batch_size: int, dtype: torch.dtype, device: torch.device
332 ) -> IntermediateTensors:
333 tensors = super().make_empty_intermediate_tensors(batch_size, dtype, device)
334 # Include deepstack buffers so non-first PP ranks receive them.
335 # _ds_layer_indices is set directly on this instance by the outer model.
336 for llm_layer in getattr(self, "_ds_layer_indices", []):
337 tensors.tensors[f"ds_{llm_layer}"] = torch.zeros(
338 (batch_size, self.config.hidden_size), dtype=dtype, device=device
339 )
340 return tensors
341
342
343 # ---------------------------------------------------------------------------
344 # Processing info / processor (reuses LlavaNext patterns)
345 # ---------------------------------------------------------------------------
346
347
348 class Granite4VisionProcessingInfo(LlavaNextProcessingInfo):
349 def get_hf_config(self):
350 return self.ctx.get_hf_config()
351
352 def get_hf_processor(self, **kwargs):
353 return self.ctx.get_hf_processor(**kwargs)
354
355 def get_num_image_tokens(
356 self,
357 *,
358 image_width: int,
359 image_height: int,
360 ) -> int:
361 hf_config = self.get_hf_config()
362 vision_encoder_info = self.get_vision_encoder_info()
363
364 # After QFormer downsampling, patch grid is scaled by downsample_rate
365 ds_rate = Fraction(hf_config.downsample_rate)
366 patch_grid = vision_encoder_info.get_patch_grid_length() # 24 for 384/16
367 downsampled_grid = int(patch_grid * ds_rate) # 12 for rate 4/8
368
369 # Base feature: downsampled_grid^2
370 base_feature_size = downsampled_grid * downsampled_grid
371
372 num_patch_height, num_patch_width = get_anyres_image_grid_shape(
373 image_size=(image_height, image_width),
374 grid_pinpoints=hf_config.image_grid_pinpoints,
375 patch_size=vision_encoder_info.get_image_size(),
376 )
377
378 (
379 unpadded_feature_size,
380 newline_feature_size,
381 ) = self._get_num_unpadded_features(
382 original_height=image_height,
383 original_width=image_width,
384 npatches=downsampled_grid,
385 num_patch_height=num_patch_height,
386 num_patch_width=num_patch_width,
387 )
388
389 return unpadded_feature_size + newline_feature_size + base_feature_size
390
391
392 class Granite4VisionMultiModalProcessor(
393 BaseLlavaNextMultiModalProcessor[Granite4VisionProcessingInfo]
394 ):
395 def _get_mm_fields_config(
396 self,
397 hf_inputs: BatchFeature,
398 hf_processor_mm_kwargs: Mapping[str, object],
399 ) -> Mapping[str, MultiModalFieldConfig]:
400 return dict(
401 pixel_values=MultiModalFieldConfig.batched("image"),
402 image_sizes=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
403 )
404
405
406 # ---------------------------------------------------------------------------
407 # Top-level model
408 # ---------------------------------------------------------------------------
409
410
411 @MULTIMODAL_REGISTRY.register_processor(
412 Granite4VisionMultiModalProcessor,
413 info=Granite4VisionProcessingInfo,
414 dummy_inputs=LlavaDummyInputsBuilder,
415 )
416 class Granite4VisionForConditionalGeneration(
417 nn.Module, SupportsLoRA, SupportsMultiModal, SupportsPP
418 ):
419 """vLLM implementation of Granite 4 Vision.
420
421 Architecture:
422 - SigLIP vision tower -> WindowQFormerDownsampler projectors
423 - Deepstack: 4 vision layers projected and injected at 4 LLM layers
424 - Spatial: 4 offset groups from last vision layer injected at 4 more LLM layers
425 - Granite language backbone with embedding_multiplier
426 - logits_scaling via LogitsProcessor
427
428 The outer model runs the LLM layer loop directly (like HF does) to inject
429 deepstack features. This avoids wrapping the inner model and keeps weight
430 loading simple.
431
432 LoRA support:
433 - Full merge: --hf-overrides '{"adapter_path": "path/to/lora"}' merges
434 LM-only LoRA deltas at load time (W += scaling * B @ A).
435 - Native LoRA: --enable-lora --default-mm-loras '{"image": "path/to/lora"}'
436 lets vLLM runtime serve LM LoRA per-request.
437 Both modes expect a LM-only adapter (no modules_to_save).
438 """
439
440 # LoRA class attributes (matches GraniteForCausalLM)
441 packed_modules_mapping = {
442 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
443 "gate_up_proj": ["gate_proj", "up_proj"],
444 }
445 embedding_modules = {}
446
447 # Weight mapping: HF checkpoint -> vLLM parameter names
448 # HF: model.language_model.layers.0...
449 # vLLM: language_model.model.layers.0...
450 # (because GraniteForCausalLM.model = GraniteModel)
451 hf_to_vllm_mapper = WeightsMapper(
452 orig_to_new_prefix={
453 "model.language_model.": "language_model.model.",
454 "model.layerwise_projectors.": "layerwise_projectors.",
455 "model.spatial_projectors.": "spatial_projectors.",
456 "model.image_newline": "image_newline",
457 "model.vision_tower.": "vision_tower.",
458 "lm_head.": "language_model.lm_head.",
459 }
460 )
461
462 @classmethod
463 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
464 if modality.startswith("image"):
465 return "<image>"
466 raise ValueError(f"Only image modality is supported, got {modality}")
467
468 def get_mm_mapping(self) -> MultiModelKeys:
469 return MultiModelKeys.from_string_field(
470 language_model="language_model",
471 connector=["layerwise_projectors", "spatial_projectors"],
472 tower_model="vision_tower",
473 )
474
475 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
476 super().__init__()
477
478 config = vllm_config.model_config.hf_config
479 quant_config = vllm_config.quant_config
480 self.config = config
481 self.vllm_config = vllm_config
482
483 # ----- Vision tower + projectors (marked as tower) -----
484 with self._mark_tower_model(vllm_config, "image"):
485 # Do NOT use init_vision_tower_for_llava here — it truncates the
486 # encoder to vision_feature_layer depth. Deepstack needs ALL hidden
487 # states (deepstack_layer_map uses negative indices into the full
488 # encoder output list).
489 self.vision_tower = SiglipVisionModel(
490 config.vision_config,
491 quant_config=quant_config,
492 require_post_norm=False,
493 prefix=maybe_prefix(prefix, "vision_tower"),
494 )
495
496 # image_newline parameter
497 if config.use_image_newline_parameter:
498 self.image_newline = nn.Parameter(
499 torch.empty(config.text_config.hidden_size)
500 )
501 else:
502 self.image_newline = None
503
504 cache_config = vllm_config.cache_config
505
506 # Deepstack projectors: one per (vision_layer, llm_layer) pair
507 self.layerwise_projectors = nn.ModuleList(
508 [
509 WindowQFormerDownsampler(
510 config,
511 quant_config=quant_config,
512 cache_config=cache_config,
513 prefix=maybe_prefix(prefix, f"layerwise_projectors.{i}"),
514 )
515 for i in range(len(config.deepstack_layer_map))
516 ]
517 )
518
519 # Spatial projectors: 4 offset groups
520 self.spatial_projectors = None
521 if config.use_spatial_sampling:
522 self.spatial_projectors = nn.ModuleList(
523 [
524 WindowQFormerDownsampler(
525 config,
526 quant_config=quant_config,
527 cache_config=cache_config,
528 spatial_offset=i,
529 prefix=maybe_prefix(prefix, f"spatial_projectors.{i}"),
530 )
531 for i in range(4)
532 ]
533 )
534
535 # ----- Language model (marked as LM) -----
536 with self._mark_language_model(vllm_config):
537 self.language_model = Granite4VisionLLMForCausalLM(
538 vllm_config=vllm_config.with_hf_config(config.text_config),
539 prefix=maybe_prefix(prefix, "language_model"),
540 )
541
542 self.make_empty_intermediate_tensors = (
543 self.language_model.make_empty_intermediate_tensors
544 )
545
546 # Store config values we need
547 self._deepstack_layer_map = config.deepstack_layer_map # [[-19, 9], ...]
548 self._use_spatial_sampling = getattr(config, "use_spatial_sampling", False)
549 self._spatial_vision_layer = getattr(config, "spatial_vision_layer", -1)
550 self._spatial_target_layers = getattr(config, "spatial_target_layers", [])
551 self._vision_feature_select_strategy = getattr(
552 config, "vision_feature_select_strategy", "full"
553 )
554 self._downsample_rate = Fraction(config.downsample_rate)
555
556 # Ordered list of LLM layer indices for each deepstack level.
557 # Pre-populated from config so it's available during CUDA graph capture
558 # (before any embed_multimodal call).
559 self._ds_layer_indices: list[int] = [
560 llm_layer for _, llm_layer in config.deepstack_layer_map
561 ] + list(getattr(config, "spatial_target_layers", []))
562
563 # Share ds_layer_indices with the LLM causal model so
564 # make_empty_intermediate_tensors includes the correct keys
565 # (its self.config is text_config, no deepstack_layer_map).
566 self.language_model._ds_layer_indices = self._ds_layer_indices
567
568 # Pre-allocated persistent GPU buffers for deepstack features.
569 # Written via .copy_() in embed_input_ids(), read by forward() via a
570 # slice. Because the buffer address is fixed, CUDA graph replay sees
571 # the updated values written just before each prefill.
572 # Shape: (max_num_batched_tokens, lm_hidden_size) per level.
573 n_layerwise = len(config.deepstack_layer_map)
574 n_spatial = len(getattr(config, "spatial_target_layers", []))
575 num_ds_levels = n_layerwise + n_spatial
576 lm_hidden = config.text_config.hidden_size
577 max_tokens = vllm_config.scheduler_config.max_num_batched_tokens
578 # Allocated on CPU first; moved to GPU in embed_input_ids on first use.
579 self._ds_buffers: list[torch.Tensor] = [
580 torch.zeros(max_tokens, lm_hidden) for _ in range(num_ds_levels)
581 ]
582 self._ds_num_tokens: int = 0 # tokens written in last embed_input_ids call
583
584 # ----- Vision feature extraction -----
585
586 def _get_vision_hidden_states(
587 self, pixel_values: torch.Tensor
588 ) -> list[torch.Tensor]:
589 """Run vision tower and return all hidden states (including input embeddings).
590
591 Uses SiglipEncoder's built-in return_all_hidden_states support.
592 Returns list[Tensor] where index 0 = embeddings, index i = after layer i-1.
593 """
594 vt = self.vision_tower
595 vm = vt.vision_model if hasattr(vt, "vision_model") else vt
596
597 hidden_states = vm.embeddings(pixel_values)
598 all_hidden_states = vm.encoder(
599 inputs_embeds=hidden_states,
600 return_all_hidden_states=True,
601 )
602 return all_hidden_states
603
604 def _pack_and_unpad_image_features(
605 self,
606 image_features: list[torch.Tensor] | tuple[torch.Tensor, ...],
607 image_sizes: torch.Tensor,
608 ) -> list[torch.Tensor]:
609 """Reshape, unpad, and pack image features.
610
611 Matches HF Granite4VisionModel.pack_and_unpad_image_features exactly.
612 """
613 config = self.config
614 ds_rate = self._downsample_rate
615 new_image_features = []
616
617 for image_idx, image_feature in enumerate(image_features):
618 if image_feature.shape[0] > 1:
619 # Multi-patch: first is base, rest are high-res
620 base_image_feature = image_feature[0]
621 image_feature = image_feature[1:]
622
623 height = width = (
624 config.vision_config.image_size // config.vision_config.patch_size
625 )
626 # After QFormer downsampling
627 height = int(height * ds_rate)
628 width = int(width * ds_rate)
629
630 num_patch_height, num_patch_width = get_anyres_image_grid_shape(
631 image_sizes[image_idx],
632 config.image_grid_pinpoints,
633 config.vision_config.image_size,
634 )
635
636 image_feature = image_feature.view(
637 num_patch_height, num_patch_width, height, width, -1
638 )
639 image_feature = (
640 image_feature.permute(4, 0, 2, 1, 3)
641 .contiguous()
642 .flatten(1, 2)
643 .flatten(2, 3)
644 )
645 image_feature = unpad_image(image_feature, image_sizes[image_idx])
646
647 if self.image_newline is not None:
648 image_feature = torch.cat(
649 (
650 image_feature,
651 self.image_newline[:, None, None]
652 .expand(*image_feature.shape[:-1], 1)
653 .to(image_feature.device, image_feature.dtype),
654 ),
655 dim=-1,
656 )
657
658 image_feature = image_feature.flatten(1, 2).transpose(0, 1)
659 image_feature = torch.cat((base_image_feature, image_feature), dim=0)
660 else:
661 image_feature = image_feature[0]
662 if self.image_newline is not None:
663 image_feature = torch.cat(
664 (image_feature, self.image_newline[None].to(image_feature)),
665 dim=0,
666 )
667
668 new_image_features.append(image_feature)
669
670 return new_image_features
671
672 def _get_all_layer_features(
673 self,
674 pixel_values: torch.Tensor,
675 image_sizes: torch.Tensor,
676 ) -> tuple[list[int], list[torch.Tensor]]:
677 """Extract deepstack + spatial features for all levels.
678
679 Returns:
680 llm_layer_indices: ordered list of target LLM layer indices
681 per_image_packed: one tensor per image, shape
682 (num_tokens_i, lm_hidden_size * num_levels),
683 all levels packed on dim=-1.
684
685 Packing on dim=-1 means the framework's token-level slicing for
686 chunked prefill preserves all levels intact.
687
688 """
689 select_strategy = self._vision_feature_select_strategy
690
691 image_num_patches = [
692 image_size_to_num_patches(
693 image_size=imsize,
694 grid_pinpoints=self.config.image_grid_pinpoints,
695 patch_size=self.config.vision_config.image_size,
696 )
697 for imsize in image_sizes
698 ]
699
700 if pixel_values.dim() == 5:
701 pixel_values = torch.cat(
702 [pv[:np_] for pv, np_ in zip(pixel_values, image_num_patches)],
703 dim=0,
704 )
705
706 all_hidden_states = self._get_vision_hidden_states(pixel_values)
707
708 # Collect per-level: (llm_layer, [per_image_tensor, ...])
709 levels: list[tuple[int, list[torch.Tensor]]] = []
710
711 for proj_idx, (vision_layer, llm_layer) in enumerate(self._deepstack_layer_map):
712 selected = all_hidden_states[vision_layer]
713 if select_strategy == "default":
714 selected = selected[:, 1:]
715 projected = self.layerwise_projectors[proj_idx](selected)
716 per_image = self._pack_and_unpad_image_features(
717 torch.split(projected, image_num_patches, dim=0), image_sizes
718 )
719 levels.append((llm_layer, per_image))
720
721 if self._use_spatial_sampling and self.spatial_projectors is not None:
722 spatial_hidden = all_hidden_states[self._spatial_vision_layer]
723 if select_strategy == "default":
724 spatial_hidden = spatial_hidden[:, 1:]
725 for group_idx, llm_layer in enumerate(self._spatial_target_layers):
726 projected = self.spatial_projectors[group_idx](spatial_hidden)
727 per_image = self._pack_and_unpad_image_features(
728 torch.split(projected, image_num_patches, dim=0), image_sizes
729 )
730 levels.append((llm_layer, per_image))
731
732 llm_layer_indices = [llm_layer for llm_layer, _ in levels]
733 num_images = len(image_sizes)
734 per_image_packed = [
735 torch.cat([levels[lvl][1][img] for lvl in range(len(levels))], dim=-1)
736 for img in range(num_images)
737 ]
738
739 return llm_layer_indices, per_image_packed
740
741 # ----- Multimodal interface -----
742
743 def _parse_and_validate_image_input(
744 self, **kwargs: object
745 ) -> LlavaNextImageInputs | None:
746 pixel_values = kwargs.pop("pixel_values", None)
747 image_sizes = kwargs.pop("image_sizes", None)
748 image_embeds = kwargs.pop("image_embeds", None)
749
750 if pixel_values is None and image_embeds is None:
751 return None
752
753 if pixel_values is not None:
754 expected_h = expected_w = self.config.vision_config.image_size
755 return LlavaNextImagePixelInputs(
756 type="pixel_values",
757 pixel_values=pixel_values,
758 image_sizes=image_sizes,
759 resolve_bindings={"h": expected_h, "w": expected_w},
760 )
761
762 if image_embeds is not None:
763 return LlavaNextImageEmbeddingInputs(
764 type="image_embeds",
765 data=image_embeds,
766 )
767
768 raise AssertionError("Unreachable")
769
770 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
771 """Run vision tower and return per-image packed feature tensors.
772
773 Each returned tensor has shape (num_tokens_i, lm_hidden_size * num_levels)
774 with all deepstack levels packed on dim=-1. The framework caches these
775 tensors and slices along dim=0 for chunked prefill — all levels survive
776 intact because slicing is token-wise, not feature-wise.
777
778 embed_input_ids() splits the packed tensor back into per-level buffers.
779 """
780 image_input = self._parse_and_validate_image_input(**kwargs)
781 if image_input is None:
782 return []
783
784 if image_input["type"] == "image_embeds":
785 return [image_input["data"]]
786
787 pixel_values = image_input["pixel_values"]
788 image_sizes = image_input.get("image_sizes")
789
790 if isinstance(pixel_values, list):
791 pixel_values = torch.cat(pixel_values, dim=0)
792
793 llm_layer_indices, per_image_packed = self._get_all_layer_features(
794 pixel_values, image_sizes
795 )
796 self._ds_layer_indices = llm_layer_indices
797 return per_image_packed
798
799 def embed_input_ids(
800 self,
801 input_ids: torch.Tensor,
802 multimodal_embeddings: MultiModalEmbeddings | None = None,
803 *,
804 is_multimodal: torch.Tensor | None = None,
805 handle_oov_mm_token: bool = True,
806 ) -> torch.Tensor:
807 """Merge text and vision embeddings, apply embedding_multiplier.
808
809 HF flow:
810 1. inputs_embeds = embed_tokens(input_ids)
811 2. inputs_embeds.masked_fill(vision_mask, 0.0)
812 3. hidden_states = inputs_embeds * embedding_multiplier
813 4. layer loop injects deepstack features at target layers
814
815 multimodal_embeddings contains packed tensors from embed_multimodal():
816 shape (num_tokens_i, lm_hidden_size * num_levels). We split on dim=-1
817 to get per-level features, build batch-sized buffers (zero at text
818 positions), and store in self._ds_features for forward().
819 """
820 lm_inner = self.language_model.model
821
822 has_vision = (
823 multimodal_embeddings is not None
824 and is_multimodal is not None
825 and len(multimodal_embeddings) > 0
826 and is_multimodal.any()
827 )
828
829 if not has_vision:
830 self._ds_num_tokens = 0
831 embeds = lm_inner.embed_input_ids(input_ids)
832 return embeds * lm_inner.config.embedding_multiplier
833
834 # 1. Text embeddings
835 text_embeds = lm_inner.embed_input_ids(input_ids)
836
837 # 2. Zero image positions (matches HF masked_fill(vision_mask, 0.0))
838 text_embeds[is_multimodal] = 0.0
839
840 # 3. Apply embedding_multiplier
841 inputs_embeds = text_embeds * lm_inner.config.embedding_multiplier
842
843 # 4. Split packed tensors into per-level features and build buffers.
844 # multimodal_embeddings is a list of per-image packed tensors
845 # (possibly a chunk slice from the framework's encoder cache).
846 # Concatenate along token dim → (total_mm_tokens, lm_h * num_levels).
847 N, lm_h = inputs_embeds.shape
848 assert multimodal_embeddings is not None
849 all_packed = torch.cat(
850 [t.to(dtype=inputs_embeds.dtype) for t in multimodal_embeddings],
851 dim=0,
852 )
853 level_features = all_packed.split(lm_h, dim=-1) # num_levels tensors
854
855 # Ensure persistent buffers are on the right device/dtype (first call).
856 buf0 = self._ds_buffers[0]
857 if buf0.device != inputs_embeds.device or buf0.dtype != inputs_embeds.dtype:
858 self._ds_buffers = [
859 b.to(device=inputs_embeds.device, dtype=inputs_embeds.dtype)
860 for b in self._ds_buffers
861 ]
862
863 for level_idx in range(len(self._ds_layer_indices)):
864 target = self._ds_buffers[level_idx][:N]
865 target.zero_()
866 target[is_multimodal] = level_features[level_idx]
867
868 self._ds_num_tokens = N
869 return inputs_embeds
870
871 # ----- Forward -----
872
873 def forward(
874 self,
875 input_ids: torch.Tensor,
876 positions: torch.Tensor,
877 intermediate_tensors: IntermediateTensors | None = None,
878 inputs_embeds: torch.Tensor | None = None,
879 **kwargs: object,
880 ) -> torch.Tensor | IntermediateTensors:
881 if intermediate_tensors is not None:
882 inputs_embeds = None
883
884 # Build IntermediateTensors from pre-allocated persistent buffers.
885 # Always pass deepstack when inputs_embeds is non-None (prefill path),
886 # including during CUDA graph capture (buffers are zero → no-op injection).
887 # This ensures the graph captures the injection code path.
888 if (
889 inputs_embeds is not None
890 and get_pp_group().is_first_rank
891 and self._ds_layer_indices
892 ):
893 n = inputs_embeds.size(0)
894 ds: IntermediateTensors | None = IntermediateTensors(
895 {
896 f"ds_{llm_layer}": self._ds_buffers[lvl][:n]
897 for lvl, llm_layer in enumerate(self._ds_layer_indices)
898 }
899 )
900 else:
901 ds = None
902
903 hidden_states = self.language_model.model(
904 input_ids=input_ids,
905 positions=positions,
906 intermediate_tensors=intermediate_tensors,
907 inputs_embeds=inputs_embeds,
908 deepstack_input_embeds=ds,
909 )
910
911 # Clear buffers after use so stale features don't leak into the next request.
912 if (
913 inputs_embeds is not None
914 and get_pp_group().is_first_rank
915 and self._ds_num_tokens > 0
916 ):
917 n = self._ds_num_tokens
918 for buf in self._ds_buffers:
919 buf[:n].zero_()
920 self._ds_num_tokens = 0
921
922 return hidden_states
923
924 def compute_logits(
925 self,
926 hidden_states: torch.Tensor,
927 ) -> torch.Tensor | None:
928 # GraniteForCausalLM.compute_logits uses
929 # LogitsProcessor(scale=1/logits_scaling)
930 return self.language_model.compute_logits(hidden_states)
931
932 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
933 loader = AutoWeightsLoader(self)
934 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)