main
py 1,018 lines 35.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from __future__ import annotations
4
5 from collections.abc import Iterable, Iterator, Mapping, Sequence
6 from typing import Annotated, Any
7
8 import numpy as np
9 import torch
10 import torch.nn as nn
11 import torch.nn.functional as F
12 from einops import rearrange
13 from transformers.image_processing_utils import BatchFeature
14
15 from vllm.config import ModelConfig, VllmConfig
16 from vllm.config.multimodal import MultiModalDummyOptions
17 from vllm.distributed import parallel_state
18 from vllm.distributed import utils as dist_utils
19 from vllm.inputs import MultiModalDataDict
20 from vllm.model_executor.layers.attention import MMEncoderAttention
21 from vllm.model_executor.layers.linear import (
22 ColumnParallelLinear,
23 QKVParallelLinear,
24 ReplicatedLinear,
25 RowParallelLinear,
26 )
27 from vllm.model_executor.layers.quantization import QuantizationConfig
28 from vllm.model_executor.models.interfaces import (
29 MultiModalEmbeddings,
30 SupportsLoRA,
31 SupportsMRoPE,
32 SupportsMultiModal,
33 SupportsPP,
34 )
35 from vllm.model_executor.models.module_mapping import MultiModelKeys
36 from vllm.model_executor.models.siglip import SiglipMLP
37 from vllm.model_executor.models.utils import (
38 AutoWeightsLoader,
39 WeightsMapper,
40 init_vllm_registered_model,
41 maybe_prefix,
42 )
43 from vllm.multimodal import MULTIMODAL_REGISTRY
44 from vllm.multimodal.inputs import (
45 MultiModalFeatureSpec,
46 MultiModalFieldConfig,
47 MultiModalKwargsItems,
48 )
49 from vllm.multimodal.parse import ImageSize, MultiModalDataItems
50 from vllm.multimodal.processing import (
51 BaseDummyInputsBuilder,
52 BaseMultiModalProcessor,
53 BaseProcessingInfo,
54 PromptReplacement,
55 PromptUpdate,
56 PromptUpdateDetails,
57 cached_encode,
58 )
59 from vllm.sequence import IntermediateTensors
60 from vllm.tokenizers import cached_tokenizer_from_config
61 from vllm.transformers_utils.config import patch_rope_parameters
62 from vllm.transformers_utils.configs.isaac import (
63 IsaacConfig,
64 PixelShuffleSiglip2VisionConfig,
65 )
66 from vllm.transformers_utils.processors.isaac import (
67 IsaacImageProcessor,
68 IsaacProcessor,
69 get_image_size_for_max_num_patches,
70 )
71 from vllm.utils.tensor_schema import TensorSchema, TensorShape
72 from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
73
74 from .vision import is_vit_use_data_parallel
75
76
77 def create_cumulative_seq_lengths(
78 seq_sizes: torch.Tensor, device: torch.device
79 ) -> tuple[torch.Tensor, torch.Tensor]:
80 """Create cumulative sequence lengths for variable-length attention."""
81 cu_seqlens = torch.zeros(len(seq_sizes) + 1, dtype=torch.int32, device=device)
82 cu_seqlens[1:] = seq_sizes.cumsum(0)
83 max_seqlen = (
84 seq_sizes.max()
85 if len(seq_sizes) > 0
86 else torch.tensor(0, dtype=torch.int32, device=device)
87 )
88 return cu_seqlens, max_seqlen
89
90
91 class Siglip2VariableSequenceEmbeddings(nn.Module):
92 def __init__(self, config: PixelShuffleSiglip2VisionConfig):
93 super().__init__()
94 self.config = config
95 self.embed_dim = config.hidden_size
96 self.patch_size = config.patch_size
97
98 self.patch_embedding = ReplicatedLinear(
99 input_size=config.num_channels * self.patch_size * self.patch_size,
100 output_size=self.embed_dim,
101 return_bias=False,
102 )
103
104 self.num_patches = config.num_patches
105 self.position_embedding_size = int(self.num_patches**0.5)
106 self.position_embedding = nn.Embedding(self.num_patches, self.embed_dim)
107
108 def positional_embeddings(
109 self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
110 ) -> torch.Tensor:
111 # Prepare positional embeddings grid: (1, embed_dim, h, w)
112 positional_embeddings = (
113 self.position_embedding.weight.reshape(
114 self.position_embedding_size, self.position_embedding_size, -1
115 )
116 .permute(2, 0, 1)
117 .unsqueeze(0)
118 )
119
120 _seq_patches, _seq_sizes, spatial_shapes = packed_seq_patches
121 pos_embeds_list = []
122 mode = "bilinear"
123 align_corners = False
124 antialias = True
125 for spatial_shape in spatial_shapes:
126 height, width = int(spatial_shape[0]), int(spatial_shape[1])
127 # Guard to ensure height and width are positive for torch.compile
128 if height > 0 and width > 0:
129 resized_pos_embed = F.interpolate(
130 positional_embeddings,
131 size=(height, width),
132 mode=mode,
133 align_corners=align_corners,
134 antialias=antialias,
135 )
136 # Reshape from (1, embed_dim, height, width) to
137 # (height*width, embed_dim)
138 resized_pos_embed = resized_pos_embed.reshape(
139 self.embed_dim, height * width
140 ).transpose(0, 1)
141 else:
142 # Fallback - should never happen in practice
143 resized_pos_embed = positional_embeddings.reshape(
144 self.embed_dim,
145 self.position_embedding_size * self.position_embedding_size,
146 ).transpose(0, 1)[: height * width]
147 pos_embeds_list.append(resized_pos_embed)
148
149 # Concatenate all positional embeddings along the sequence dimension
150 pos_embeds = torch.cat(pos_embeds_list, dim=0)
151 return pos_embeds
152
153 def forward(
154 self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
155 ):
156 seq_patches, _seq_sizes, _spatial_shapes = packed_seq_patches
157
158 target_weight = self.patch_embedding.weight
159 seq_patches = seq_patches.to(
160 device=target_weight.device, dtype=target_weight.dtype
161 )
162 patch_embeds = self.patch_embedding(seq_patches)
163 pos_embeds = self.positional_embeddings(packed_seq_patches)
164
165 # Flatten patch embeddings to match positional embeddings format
166 if patch_embeds.dim() == 3:
167 patch_embeds = patch_embeds.view(-1, patch_embeds.size(-1))
168
169 # Add positional embeddings to patch embeddings
170 embeddings = patch_embeds + pos_embeds
171 return embeddings
172
173
174 def create_pixel_shuffle_index_map(
175 seq_sizes: torch.Tensor,
176 token_grids: torch.Tensor,
177 scale_factor: int = 1,
178 device: torch.device | None = None,
179 ) -> torch.Tensor:
180 """Build a gather-index map that tells us, for every *output* token after
181 pixel-shuffle, which `scale_factor**2` *input* tokens are being merged.
182
183 Args:
184 ----
185 seq_sizes : (num_images,) - #patches in each image (row-major order)
186 token_grids : (num_images,2) - (height, width) for every image
187 scale_factor : spatial down-scale factor (≥2)
188 device : (optional) overrides `seq_sizes.device`
189
190 Returns:
191 -------
192 gather_idx : (new_total_seq_len, scale_factor**2) int64 tensor.
193 gather_idx[i, j] is the *flat* index into the *original*
194 packed sequence for the j-th sub-patch that forms the
195 i-th output token.
196
197 """
198 if device is None:
199 device = seq_sizes.device
200
201 r = int(scale_factor)
202 if r < 2:
203 raise ValueError("`scale_factor` must be ≥ 2")
204
205 # Safety: all spatial dims must be divisible by r
206 # Cannot run under torch compile fullgraph mode hence
207 if not torch.compiler.is_compiling() and not (
208 (token_grids[:, 0] % r == 0).all() and (token_grids[:, 1] % r == 0).all()
209 ):
210 raise AssertionError(
211 "Every (H,W) in `token_grids` must be divisible by "
212 f"scale_factor={r}, got {token_grids.tolist()}"
213 )
214
215 gather_chunks: list[torch.Tensor] = []
216 tok_offset = 0
217
218 for seq_len, (h, w) in zip(seq_sizes.tolist(), token_grids.tolist(), strict=False):
219 # Build the (H, W) grid of flat indices for this image
220 grid = torch.arange(seq_len, device=device, dtype=torch.int64) + tok_offset
221 grid = grid.view(h, w) # (H, W)
222
223 # -------- identical ordering to your fixed-res routine --------
224 # Step 1: split width into blocks of r
225 grid = grid.view(h, w // r, r) # (H, W/r, r)
226 # Step 2: now split height into blocks of r
227 grid = grid.view(h // r, r, w // r, r) # (H/r, r, W/r, r)
228 # Step 3: final permutation to (H/r, W/r, r, r)
229 grid = grid.permute(0, 2, 1, 3).contiguous() # (H/r, W/r, r, r)
230 # Step 4: each (r, r) block forms one output token
231 gather_chunks.append(grid.reshape(-1, r * r)) # (H*W / r², r²)
232
233 tok_offset += seq_len
234
235 # Concatenate over all images in the packed batch
236 gather_idx = torch.cat(gather_chunks, dim=0) # (Σ_i HᵢWᵢ/r², r²)
237 return gather_idx
238
239
240 def pixel_shuffle_varlen(
241 x: torch.Tensor,
242 token_grids: torch.Tensor,
243 scale_factor: int = 1,
244 ) -> torch.Tensor:
245 r"""Apply pixel shuffle to a packed vision sequence without unpacking per image.
246
247 Args:
248 x (`torch.Tensor`):
249 Concatenated vision embeddings. Accepts `(seq_len, hidden_size)` or
250 `(1, seq_len, hidden_size)` shapes produced by stacking image
251 patches.
252 token_grids (`torch.Tensor`):
253 Integer tensor of shape `(num_images, 2)` whose rows give the
254 `(height, width)` patch grid sizes corresponding to each image
255 segment inside `x`.
256 scale_factor (`int`, *optional*, defaults to 1):
257 Spatial down-sampling factor specific to pixel shuffle. Values
258 greater than one merge `scale_factor**2` neighboring patches into a
259 single embedding channel-group.
260
261 Returns:
262 `torch.Tensor`: Pixel-shuffled embeddings with shape matching the input
263 convention: `(seq_len, hidden_size * scale_factor**2)` when the input
264 was 2D, or `(1, seq_len, hidden_size * scale_factor**2)` if the
265 singleton batch dimension was present.
266
267 Raises:
268 ValueError: If more than one batch item is provided.
269
270 """
271 keep_batch_dim = x.dim() == 3
272 if keep_batch_dim:
273 if x.size(0) != 1:
274 raise AssertionError("Packed sequence is expected to have batch_size == 1")
275 x_ = x.squeeze(0) # (seq, embed)
276 else:
277 x_ = x # (seq, embed)
278
279 embed_dim = x_.size(-1)
280 r = int(scale_factor)
281
282 # Calculate seq_sizes from token_grids
283 seq_sizes = torch.prod(token_grids, dim=-1)
284
285 # Build index map and gather in one go
286 gather_idx = create_pixel_shuffle_index_map(
287 seq_sizes=seq_sizes,
288 token_grids=token_grids,
289 scale_factor=r,
290 device=x_.device,
291 ) # (new_seq, r²)
292
293 # Gather → (new_seq, r², embed_dim)
294 gathered = x_[gather_idx] # fancy indexing keeps gradient
295
296 # Merge the r² group dimension into channels to finish the shuffle
297 out = gathered.reshape(gathered.size(0), embed_dim * r * r)
298
299 # Restore batch dimension if needed
300 if keep_batch_dim:
301 out = out.unsqueeze(0)
302 return out
303
304
305 # ============================================================================
306 # Configuration
307 # ============================================================================
308
309
310 class IsaacProcessingInfo(BaseProcessingInfo):
311 def get_hf_config(self) -> IsaacConfig:
312 if hasattr(self.ctx, "get_hf_config"):
313 original_config = self.ctx.get_hf_config()
314 # Map HF config parameters to our vLLM config parameters
315 return IsaacConfig(
316 # Vision parameters - map from HF names
317 vision_config=getattr(original_config, "vision_config", None),
318 vision_patch_size=getattr(original_config, "video_patch_size", 16),
319 vision_max_num_patches=getattr(
320 original_config, "vision_max_num_patches", 256
321 ),
322 vision_min_num_patches=getattr(
323 original_config, "vision_min_num_patches", None
324 ),
325 pixel_shuffle_scale=getattr(original_config, "pixel_shuffle_scale", 1),
326 max_sequence_length=getattr(
327 original_config, "max_sequence_length", 16384
328 ),
329 vision_token=getattr(original_config, "vision_token", "<image>"),
330 vision_attn_implementation=getattr(
331 original_config, "vision_attn_implementation", None
332 ),
333 )
334 return IsaacConfig()
335
336 def get_image_processor(self, **kwargs) -> IsaacImageProcessor:
337 return IsaacImageProcessor(**kwargs)
338
339 def get_hf_processor(self, **kwargs) -> IsaacProcessor:
340 hf_config = self.get_hf_config()
341
342 return IsaacProcessor(
343 tokenizer=self.get_tokenizer(),
344 image_processor=self.get_image_processor(**kwargs),
345 image_token=hf_config.vision_token,
346 )
347
348 def get_image_size_with_most_features(self) -> ImageSize:
349 hf_config = self.get_hf_config()
350 # Get target dimensions
351 target_height, target_width = get_image_size_for_max_num_patches(
352 9999999,
353 9999999,
354 hf_config.video_patch_size,
355 hf_config.vision_max_num_patches,
356 min_num_patches=hf_config.vision_min_num_patches,
357 pixel_shuffle_scale=hf_config.pixel_shuffle_scale,
358 )
359 return ImageSize(width=target_width, height=target_height)
360
361 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
362 return {"image": None}
363
364 def get_mm_max_tokens_per_item(
365 self,
366 seq_len: int,
367 mm_counts: Mapping[str, int],
368 ) -> Mapping[str, int]:
369 hf_config = self.get_hf_config()
370 num_vision_tokens = hf_config.vision_max_num_patches // (
371 hf_config.pixel_shuffle_scale**2
372 )
373 return {"image": num_vision_tokens}
374
375
376 class IsaacDummyInputsBuilder(BaseDummyInputsBuilder[IsaacProcessingInfo]):
377 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
378 num_images = mm_counts.get("image", 0)
379
380 hf_processor = self.info.get_hf_processor()
381 image_token: str = hf_processor.image_token
382
383 return image_token * num_images
384
385 def get_dummy_mm_data(
386 self,
387 seq_len: int,
388 mm_counts: Mapping[str, int],
389 mm_options: MultiModalDummyOptions,
390 ) -> MultiModalDataDict:
391 target_width, target_height = self.info.get_image_size_with_most_features()
392
393 return {
394 "image": self._get_dummy_images(
395 width=target_width,
396 height=target_height,
397 num_images=mm_counts.get("image", 0),
398 overrides=mm_options.get("image"),
399 ),
400 }
401
402
403 class IsaacImagePixelInputs(TensorSchema):
404 """Schema for validating Isaac image inputs.
405
406 Dimensions:
407 - np: Number of patches
408 - d: Patch dimension
409 - ni: Number of images
410
411 The schema enforces:
412 - pixel_values must be 2D: (num_patches, patch_dim)
413 - image_grid_thw must be 2D: (num_images, 3)
414 where 3 represents [T, H, W]
415 """
416
417 pixel_values: Annotated[
418 torch.Tensor,
419 TensorShape("np", "d"),
420 ]
421
422 image_grid_thw: Annotated[
423 torch.Tensor,
424 TensorShape("ni", 3),
425 ]
426
427
428 class IsaacMultiModalProcessor(BaseMultiModalProcessor):
429 def _get_mm_fields_config(
430 self,
431 hf_inputs: BatchFeature,
432 hf_processor_mm_kwargs: Mapping[str, object],
433 ) -> Mapping[str, MultiModalFieldConfig]:
434 # Configure multimodal fields for Isaac model
435 image_grid_thw = hf_inputs.get("image_grid_thw", torch.empty((0, 3)))
436 image_grid_sizes = image_grid_thw.prod(-1)
437
438 return {
439 "pixel_values": MultiModalFieldConfig.flat_from_sizes(
440 "image", image_grid_sizes
441 ),
442 "image_grid_thw": MultiModalFieldConfig.batched("image", keep_on_cpu=True),
443 }
444
445 def _get_prompt_updates(
446 self,
447 mm_items: MultiModalDataItems,
448 hf_processor_mm_kwargs: Mapping[str, Any],
449 out_mm_kwargs: MultiModalKwargsItems,
450 ) -> Sequence[PromptUpdate]:
451 image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
452 tokenizer = self.info.get_tokenizer()
453 image_pad_token_ids = cached_encode(
454 tokenizer, "<|image_pad|>", add_special_tokens=False
455 )
456
457 pixel_shuffle_scale = getattr(image_processor, "pixel_shuffle_scale", 2)
458 merge_length = pixel_shuffle_scale**2
459
460 def get_replacement_isaac(item_idx: int):
461 out_item = out_mm_kwargs["image"][item_idx]
462 grid_thw = out_item["image_grid_thw"].data
463 assert isinstance(grid_thw, torch.Tensor)
464
465 feature_size = int(grid_thw.prod()) // merge_length
466 repl_full = image_pad_token_ids * feature_size
467 return PromptUpdateDetails.select_token_ids(repl_full, image_pad_token_ids)
468
469 return [
470 PromptReplacement(
471 modality="image",
472 target=cached_encode(tokenizer, "<image>", add_special_tokens=False),
473 replacement=get_replacement_isaac,
474 )
475 ]
476
477
478 class Siglip2VisionAttention(nn.Module):
479 def __init__(
480 self,
481 config: PixelShuffleSiglip2VisionConfig,
482 quant_config: QuantizationConfig | None = None,
483 *,
484 prefix: str = "",
485 ) -> None:
486 super().__init__()
487
488 use_data_parallel = is_vit_use_data_parallel()
489 self.tp_size = (
490 1
491 if use_data_parallel
492 else parallel_state.get_tensor_model_parallel_world_size()
493 )
494 self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
495 self.hidden_size_per_attention_head = dist_utils.divide(
496 config.hidden_size, config.num_attention_heads
497 )
498 self.num_attention_heads_per_partition = dist_utils.divide(
499 config.num_attention_heads, self.tp_size
500 )
501
502 self.qkv_proj = QKVParallelLinear(
503 hidden_size=config.hidden_size,
504 head_size=self.hidden_size_per_attention_head,
505 total_num_heads=config.num_attention_heads,
506 total_num_kv_heads=config.num_attention_heads,
507 bias=True,
508 quant_config=quant_config,
509 prefix=f"{prefix}.qkv_proj",
510 disable_tp=use_data_parallel,
511 )
512 self.out_proj = RowParallelLinear(
513 input_size=config.hidden_size,
514 output_size=config.hidden_size,
515 quant_config=quant_config,
516 prefix=f"{prefix}.out_proj",
517 disable_tp=use_data_parallel,
518 )
519
520 self.attn = MMEncoderAttention(
521 num_heads=self.num_attention_heads_per_partition,
522 head_size=self.hidden_size_per_attention_head,
523 scale=self.hidden_size_per_attention_head**-0.5,
524 prefix=f"{prefix}.attn",
525 )
526
527 def split_qkv(self, qkv: torch.Tensor) -> tuple[torch.Tensor, ...]:
528 seq_len, bs, _ = qkv.shape
529 q, k, v = qkv.chunk(3, dim=2)
530 new_shape = (
531 seq_len,
532 bs,
533 self.num_attention_heads_per_partition,
534 self.hidden_size_per_attention_head,
535 )
536 q, k, v = (x.view(*new_shape) for x in (q, k, v))
537 return q, k, v
538
539 def forward(
540 self,
541 hidden_states: torch.Tensor,
542 *,
543 cu_seqlens: torch.Tensor,
544 max_seqlen: torch.Tensor | None,
545 ) -> torch.Tensor:
546 batch_size, _, _ = hidden_states.shape
547 if batch_size != 1:
548 raise ValueError("packed variable-length attention expects batch_size=1")
549 x = rearrange(hidden_states, "b s d -> s b d")
550 x, _ = self.qkv_proj(x)
551 q, k, v = self.split_qkv(x)
552 q, k, v = (rearrange(t, "s b h d -> b s h d") for t in (q, k, v))
553
554 context_layer = self.attn(
555 query=q,
556 key=k,
557 value=v,
558 cu_seqlens=cu_seqlens,
559 max_seqlen=max_seqlen,
560 )
561 context_layer = rearrange(context_layer, "b s h d -> s b (h d)").contiguous()
562
563 output, _ = self.out_proj(context_layer)
564 output = rearrange(output, "s b d -> b s d")
565 return output
566
567
568 class Siglip2EncoderLayer(nn.Module):
569 def __init__(
570 self,
571 config: PixelShuffleSiglip2VisionConfig,
572 quant_config: QuantizationConfig | None = None,
573 *,
574 prefix: str = "",
575 ) -> None:
576 super().__init__()
577 self.embed_dim = config.hidden_size
578 self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
579 self.self_attn = Siglip2VisionAttention(
580 config,
581 quant_config=quant_config,
582 prefix=f"{prefix}.self_attn",
583 )
584 self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
585 self.mlp = SiglipMLP(
586 config,
587 quant_config=quant_config,
588 prefix=f"{prefix}.mlp",
589 )
590
591 def forward(
592 self,
593 hidden_states: torch.Tensor,
594 *,
595 cu_seqlens: torch.Tensor,
596 max_seqlen: torch.Tensor | None,
597 ) -> torch.Tensor:
598 residual = hidden_states
599
600 hidden_states = self.layer_norm1(hidden_states)
601 hidden_states = self.self_attn(
602 hidden_states=hidden_states,
603 cu_seqlens=cu_seqlens,
604 max_seqlen=max_seqlen,
605 )
606 hidden_states = residual + hidden_states
607
608 residual = hidden_states
609 hidden_states = self.layer_norm2(hidden_states)
610 hidden_states = self.mlp(hidden_states)
611 hidden_states = residual + hidden_states
612
613 return hidden_states
614
615
616 class Siglip2Encoder(nn.Module):
617 def __init__(
618 self,
619 config: PixelShuffleSiglip2VisionConfig,
620 quant_config: QuantizationConfig | None = None,
621 *,
622 prefix: str = "",
623 ) -> None:
624 super().__init__()
625 self.config = config
626 self.layers = nn.ModuleList(
627 [
628 Siglip2EncoderLayer(
629 config,
630 quant_config=quant_config,
631 prefix=f"{prefix}.layers.{layer_idx}",
632 )
633 for layer_idx in range(config.num_hidden_layers)
634 ]
635 )
636
637 def forward(
638 self,
639 inputs_embeds: torch.Tensor,
640 *,
641 cu_seqlens: torch.Tensor | None = None,
642 max_seqlen: torch.Tensor | None = None,
643 ) -> torch.Tensor:
644 hidden_states = inputs_embeds
645 for encoder_layer in self.layers:
646 hidden_states = encoder_layer(
647 hidden_states,
648 cu_seqlens=cu_seqlens,
649 max_seqlen=max_seqlen,
650 )
651 return hidden_states
652
653
654 class Siglip2VisionTransformer(nn.Module):
655 hf_to_vllm_mapper = WeightsMapper(
656 orig_to_new_stacked={
657 ".q_proj": (".qkv_proj", "q"),
658 ".k_proj": (".qkv_proj", "k"),
659 ".v_proj": (".qkv_proj", "v"),
660 }
661 )
662
663 def __init__(
664 self,
665 config: PixelShuffleSiglip2VisionConfig,
666 quant_config: QuantizationConfig | None = None,
667 prefix: str = "",
668 ):
669 super().__init__()
670 self.config = config
671 self.quant_config = quant_config
672 embed_dim = config.hidden_size
673
674 self.embeddings = Siglip2VariableSequenceEmbeddings(config)
675 self.pixel_shuffle_scale_factor = config.pixel_shuffle_scale_factor
676 self.encoder = Siglip2Encoder(
677 config,
678 quant_config=quant_config,
679 prefix=f"{prefix}.encoder",
680 )
681 self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
682
683 def forward(
684 self,
685 packed_seq_patches: tuple[torch.Tensor, torch.Tensor],
686 ) -> torch.Tensor:
687 r"""spatial_shapes (`torch.LongTensor` of shape `(batch_size, 2)`):
688 Tensor containing the spatial dimensions (height, width)
689 of the input images.
690 """
691 seq_patches, token_grids = packed_seq_patches
692 seq_sizes = torch.prod(token_grids, dim=-1)
693
694 # Get embeddings from packed sequence
695 hidden_states = self.embeddings((seq_patches, seq_sizes, token_grids))
696
697 # Add a pseudo batch dimension for the encoder
698 hidden_states = hidden_states.unsqueeze(0)
699
700 cu_seqlens, max_seqlen = create_cumulative_seq_lengths(
701 seq_sizes, hidden_states.device
702 )
703
704 hidden_states = self.encoder(
705 inputs_embeds=hidden_states,
706 cu_seqlens=cu_seqlens,
707 max_seqlen=max_seqlen,
708 )
709 hidden_states = self.post_layernorm(hidden_states)
710
711 if self.pixel_shuffle_scale_factor > 1:
712 hidden_states = pixel_shuffle_varlen(
713 x=hidden_states,
714 token_grids=token_grids,
715 scale_factor=self.pixel_shuffle_scale_factor,
716 )
717 # Remove the pseudo batch dimension we added earlier
718 hidden_states = hidden_states.squeeze(0)
719
720 # return last_hidden_state
721 return hidden_states
722
723 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
724 loader = AutoWeightsLoader(self)
725 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
726
727
728 def _resolve_vision_token_id(model_config: ModelConfig, vision_token: str) -> int:
729 tokenizer = cached_tokenizer_from_config(model_config)
730 assert tokenizer is not None
731 return tokenizer.encode(vision_token, add_special_tokens=False)[0]
732
733
734 class IsaacVisionEmbedding(nn.Module):
735 def __init__(
736 self,
737 vision_cfg: PixelShuffleSiglip2VisionConfig,
738 hidden_dim: int,
739 output_dim: int,
740 quant_config: QuantizationConfig | None = None,
741 prefix: str = "",
742 ):
743 super().__init__()
744 self.transformer = Siglip2VisionTransformer(
745 vision_cfg,
746 quant_config=quant_config,
747 prefix=maybe_prefix(prefix, "0"),
748 )
749 self.linear_fc1 = ColumnParallelLinear(
750 hidden_dim,
751 4 * hidden_dim,
752 bias=False,
753 quant_config=quant_config,
754 prefix=maybe_prefix(prefix, "1"),
755 return_bias=False,
756 )
757 self.act = nn.SiLU()
758 self.linear_fc2 = RowParallelLinear(
759 4 * hidden_dim,
760 output_dim,
761 bias=False,
762 quant_config=quant_config,
763 prefix=maybe_prefix(prefix, "3"),
764 return_bias=False,
765 )
766
767 def forward(
768 self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor]
769 ) -> torch.Tensor:
770 hidden_states = self.transformer(packed_seq_patches)
771 hidden_states = self.linear_fc1(hidden_states)
772 hidden_states = self.act(hidden_states)
773 hidden_states = self.linear_fc2(hidden_states)
774 return hidden_states
775
776
777 @MULTIMODAL_REGISTRY.register_processor(
778 IsaacMultiModalProcessor,
779 info=IsaacProcessingInfo,
780 dummy_inputs=IsaacDummyInputsBuilder,
781 )
782 class IsaacForConditionalGeneration(
783 nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP, SupportsMRoPE
784 ):
785 packed_modules_mapping = {
786 "qkv_proj": [
787 "q_proj",
788 "k_proj",
789 "v_proj",
790 ],
791 "gate_up_proj": [
792 "gate_proj",
793 "up_proj",
794 ],
795 }
796
797 supports_encoder_tp_data = True
798
799 # To ensure correct weight loading and mapping.
800 hf_to_vllm_mapper = WeightsMapper(
801 orig_to_new_prefix={
802 "lm_head.": "language_model.lm_head.",
803 "model.text_model.lm_head.": "language_model.lm_head.",
804 "model.text_model.": "language_model.model.",
805 "model.vision_embedding.0": "vision_embedding.transformer",
806 "model.vision_embedding.1": "vision_embedding.linear_fc1",
807 "model.vision_embedding.2": "vision_embedding.act",
808 "model.vision_embedding.3": "vision_embedding.linear_fc2",
809 "model.vision_embedding.": "vision_embedding.",
810 "model.lm_head.": "language_model.lm_head.",
811 "model.": "language_model.model.",
812 }
813 )
814
815 @classmethod
816 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
817 if modality.startswith("image"):
818 return "<image>"
819
820 raise ValueError("Only image modality is supported")
821
822 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
823 super().__init__()
824 config: IsaacConfig = vllm_config.model_config.hf_config
825 quant_config = vllm_config.quant_config
826 self.config = config
827
828 head_dim = config.head_dim
829 calculated_mrope_section = [
830 head_dim // 4, # 2x more for temporal dim
831 head_dim // 8,
832 head_dim // 8,
833 ]
834
835 self.vision_token_id = _resolve_vision_token_id(
836 vllm_config.model_config, config.vision_token
837 )
838 config.image_token_id = self.vision_token_id
839
840 text_cfg = getattr(config, "text_config", None)
841 target_cfg = (
842 text_cfg
843 if text_cfg is not None and not isinstance(text_cfg, dict)
844 else config
845 )
846
847 rope_scaling = getattr(target_cfg, "rope_scaling", None)
848 if rope_scaling is None and target_cfg is config:
849 rope_scaling = getattr(config, "_rope_scaling", None)
850
851 patch_rope_parameters(target_cfg)
852 rope_parameters = target_cfg.rope_parameters
853 rope_parameters["mrope_section"] = calculated_mrope_section
854 if rope_scaling is not None and "mrope_interleaved" in rope_scaling:
855 rope_parameters.setdefault(
856 "mrope_interleaved", rope_scaling["mrope_interleaved"]
857 )
858 target_cfg.rope_parameters = rope_parameters
859
860 with self._mark_language_model(vllm_config):
861 self.language_model = init_vllm_registered_model(
862 vllm_config=vllm_config,
863 architectures=["Qwen3ForCausalLM"],
864 prefix=maybe_prefix(prefix, "language_model"),
865 )
866
867 self.make_empty_intermediate_tensors = (
868 self.language_model.make_empty_intermediate_tensors
869 )
870
871 vision_cfg = config.vision_config
872 if vision_cfg is None:
873 raise ValueError("IsaacConfig should always have vision_config")
874 attn_impl = (
875 config.vision_attn_implementation
876 if config.vision_attn_implementation is not None
877 else getattr(config, "_attn_implementation", None)
878 )
879 if attn_impl is not None:
880 vision_cfg._attn_implementation = attn_impl
881
882 hidden_dim = vision_cfg.hidden_size * (vision_cfg.pixel_shuffle_scale_factor**2)
883
884 with self._mark_tower_model(vllm_config, "image"):
885 self.vision_embedding = IsaacVisionEmbedding(
886 vision_cfg=vision_cfg,
887 hidden_dim=hidden_dim,
888 output_dim=config.hidden_size,
889 quant_config=quant_config,
890 prefix=maybe_prefix(prefix, "vision_embedding"),
891 )
892
893 def iter_mm_grid_hw(
894 self, input_tokens: list[int], mm_features: list[MultiModalFeatureSpec]
895 ) -> Iterator[tuple[int, int, int]]:
896 spatial_merge_size = self.config.vision_config.pixel_shuffle_scale_factor
897 for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
898 offset = mm_feature.mm_position.offset
899 if mm_feature.modality == "image":
900 mm_data = mm_feature.data
901 assert mm_data is not None
902 grid_thw = mm_data["image_grid_thw"].data
903 assert isinstance(grid_thw, torch.Tensor)
904 t, h, w = grid_thw.tolist()
905 assert t == 1, f"Image must have 1 frame, got {t}"
906 yield offset, h // spatial_merge_size, w // spatial_merge_size
907 else:
908 raise ValueError(f"Unsupported modality: {mm_feature.modality}")
909
910 def get_mrope_input_positions(
911 self,
912 input_tokens: list[int],
913 mm_features: list[MultiModalFeatureSpec],
914 ) -> tuple[torch.Tensor, int]:
915 llm_pos_ids_list: list[np.ndarray] = []
916 st = 0
917 for offset, llm_grid_h, llm_grid_w in self.iter_mm_grid_hw(
918 input_tokens, mm_features
919 ):
920 text_len = offset - st
921 st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
922 llm_pos_ids_list.append(
923 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
924 )
925
926 grid_indices = np.indices((1, llm_grid_h, llm_grid_w)).reshape(3, -1)
927 grid_indices[0, :] = grid_indices[0, :] + text_len + st_idx
928 llm_pos_ids_list.append(grid_indices)
929 st = offset + llm_grid_h * llm_grid_w
930
931 if st < len(input_tokens):
932 st_idx = llm_pos_ids_list[-1][0, -1] + 1 if len(llm_pos_ids_list) > 0 else 0
933 text_len = len(input_tokens) - st
934 llm_pos_ids_list.append(
935 np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
936 )
937
938 llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
939 mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item()
940
941 return torch.from_numpy(llm_positions), mrope_position_delta
942
943 def _parse_and_validate_image_input(
944 self, **kwargs: object
945 ) -> IsaacImagePixelInputs | None:
946 pixel_values = kwargs.get("pixel_values")
947 image_grid_thw = kwargs.get("image_grid_thw")
948 if pixel_values is None or image_grid_thw is None:
949 return None
950
951 # TensorSchema will automatically validate shapes on initialization
952 return IsaacImagePixelInputs(
953 pixel_values=pixel_values,
954 image_grid_thw=image_grid_thw,
955 )
956
957 def _process_image_input(
958 self,
959 image_input: IsaacImagePixelInputs,
960 ) -> tuple[torch.Tensor, ...]:
961 pixel_values = image_input["pixel_values"]
962 image_grid_thw = image_input["image_grid_thw"]
963 if pixel_values.numel() == 0:
964 return ()
965
966 device = next(self.language_model.parameters()).device
967 dtype = self.vision_embedding.linear_fc1.weight.dtype
968 pixel_values = pixel_values.to(device=device, dtype=dtype)
969 # The [:, 1:3] column slice isn't densely laid out, so stage it with a
970 # single copy straight into pinned memory to keep the H2D non-blocking.
971 spatial_grids_cpu = torch.empty(
972 (image_grid_thw.shape[0], 2),
973 dtype=image_grid_thw.dtype,
974 pin_memory=PIN_MEMORY,
975 ).copy_(image_grid_thw[:, 1:3])
976 spatial_grids = async_tensor_h2d(spatial_grids_cpu, device, dtype=torch.int32)
977
978 vision_embeddings = self.vision_embedding((pixel_values, spatial_grids))
979 merge_size = self.config.vision_config.pixel_shuffle_scale_factor
980 sizes = spatial_grids.prod(-1) // (merge_size * merge_size)
981 return tuple(vision_embeddings.split(sizes.tolist()))
982
983 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
984 image_input = self._parse_and_validate_image_input(**kwargs)
985 if image_input is None:
986 return ()
987 return self._process_image_input(image_input)
988
989 def forward(
990 self,
991 input_ids: torch.Tensor | None,
992 positions: torch.Tensor,
993 intermediate_tensors: IntermediateTensors | None = None,
994 inputs_embeds: torch.Tensor | None = None,
995 **kwargs: object,
996 ) -> torch.Tensor | IntermediateTensors:
997 return self.language_model(
998 input_ids=input_ids,
999 positions=positions,
1000 intermediate_tensors=intermediate_tensors,
1001 inputs_embeds=inputs_embeds,
1002 **kwargs,
1003 )
1004
1005 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
1006 return self.language_model.compute_logits(hidden_states)
1007
1008 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1009 loader = AutoWeightsLoader(self)
1010 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1011
1012 def get_mm_mapping(self) -> MultiModelKeys:
1013 """Get the module prefix in multimodal models."""
1014 return MultiModelKeys.from_string_field(
1015 language_model="language_model",
1016 connector="vision_embedding.linear_fc2", # The final linear layer
1017 tower_model="vision_embedding",
1018 )