A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 10, 22:45 UTC
Feature/isaac 0.1 (#28367)
Signed-off-by: oscardev256 <42308241+oscardev256@users.noreply.github.com> Signed-off-by: Oscar Gonzalez <ogonzal6@alumni.jh.edu> Signed-off-by: Yang <lymailforjob@gmail.com> Co-authored-by: Yang <lymailforjob@gmail.com>
oscardev256 committed
Dec 25, 2025 at 21:49 UTC
b7165d53c689ed11f3f9a984a4b7f45accbc211f
11 files changed
+1800
-2
docs/models/supported_models.md
+1
@@ -688,6 +688,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
688
| `H2OVLChatModel` | H2OVL | T + I<sup>E+</sup> | `h2oai/h2ovl-mississippi-800m`, `h2oai/h2ovl-mississippi-2b`, etc. | | ✅︎ |
689
| `HunYuanVLForConditionalGeneration` | HunyuanOCR | T + I<sup>E+</sup> | `tencent/HunyuanOCR`, etc. | ✅︎ | ✅︎ |
690
| `Idefics3ForConditionalGeneration` | Idefics3 | T + I | `HuggingFaceM4/Idefics3-8B-Llama3`, etc. | ✅︎ | |
691
+| `IsaacForConditionalGeneration` | Isaac | T + I<sup>+</sup> | `PerceptronAI/Isaac-0.1` | ✅︎ | ✅︎ |
692
| `InternS1ForConditionalGeneration` | Intern-S1 | T + I<sup>E+</sup> + V<sup>E+</sup> | `internlm/Intern-S1`, `internlm/Intern-S1-mini`, etc. | ✅︎ | ✅︎ |
693
| `InternVLChatModel` | InternVL 3.5, InternVL 3.0, InternVideo 2.5, InternVL 2.5, Mono-InternVL, InternVL 2.0 | T + I<sup>E+</sup> + (V<sup>E+</sup>) | `OpenGVLab/InternVL3_5-14B`, `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/Mono-InternVL-2B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ |
694
| `InternVLForConditionalGeneration` | InternVL 3.0 (HF format) | T + I<sup>E+</sup> + V<sup>E+</sup> | `OpenGVLab/InternVL3-1B-hf`, etc. | ✅︎ | ✅︎ |
requirements/test.in
+2
@@ -56,3 +56,5 @@ pydantic>=2.12 # 2.11 leads to error on python 3.13
56
decord==0.6.0
57
terratorch @ git+https://github.com/IBM/terratorch.git@1.1.rc3 # required for PrithviMAE test
58
gpt-oss >= 0.0.7; python_version > '3.11'
59
+
60
+perceptron # required for isaac test
requirements/test.txt
+19
-2
@@ -135,6 +135,7 @@ cloudpickle==3.1.1
135
# via mlflow-skinny
136
colorama==0.4.6
137
# via
138
+ # perceptron
139
# sacrebleu
140
# schemathesis
141
# tqdm-multiprocess
@@ -302,6 +303,8 @@ h11==0.14.0
303
# via
304
# httpcore
305
# uvicorn
306
+h2==4.3.0
307
+ # via httpx
308
h5py==3.13.0
309
# via terratorch
310
harfile==0.3.0
@@ -310,6 +313,8 @@ hf-xet==1.1.7
313
# via huggingface-hub
314
hiredis==3.0.0
315
# via tensorizer
316
+hpack==4.1.0
317
+ # via h2
318
html2text==2025.4.15
319
# via gpt-oss
320
httpcore==1.0.6
@@ -317,6 +322,7 @@ httpcore==1.0.6
322
httpx==0.27.2
323
# via
324
# -r requirements/test.in
325
+ # perceptron
326
# schemathesis
327
huggingface-hub==0.34.3
328
# via
@@ -338,6 +344,8 @@ hydra-core==1.3.2
344
# via
345
# lightly
346
# lightning
347
+hyperframe==6.1.0
348
+ # via h2
349
hypothesis==6.131.0
350
# via
351
# hypothesis-graphql
@@ -549,6 +557,7 @@ numpy==1.26.4
557
# pandas
558
# patsy
559
# peft
560
+ # perceptron
561
# pycocotools
562
# pyogrio
563
# rasterio
@@ -702,6 +711,8 @@ peft==0.16.0
711
# via
712
# -r requirements/test.in
713
# lm-eval
714
+perceptron==0.1.4
715
+ # via -r requirements/test.in
716
pillow==10.4.0
717
# via
718
# genai-perf
@@ -709,6 +720,7 @@ pillow==10.4.0
720
# lightly-utils
721
# matplotlib
722
# mistral-common
723
+ # perceptron
724
# scikit-image
725
# segmentation-models-pytorch
726
# sentence-transformers
@@ -952,6 +964,7 @@ rich==13.9.4
964
# genai-perf
965
# lightning
966
# mteb
967
+ # perceptron
968
# typer
969
rioxarray==0.19.0
970
# via terratorch
@@ -1024,7 +1037,9 @@ shapely==2.1.1
1037
# geopandas
1038
# torchgeo
1039
shellingham==1.5.4
1027
- # via typer
1040
+ # via
1041
+ # perceptron
1042
+ # typer
1043
six==1.16.0
1044
# via
1045
# junit-xml
@@ -1218,7 +1233,9 @@ typepy==1.3.2
1233
# pytablewriter
1234
# tabledata
1235
typer==0.15.2
1221
- # via fastsafetensors
1236
+ # via
1237
+ # fastsafetensors
1238
+ # perceptron
1239
types-python-dateutil==2.9.0.20241206
1240
# via arrow
1241
typeshed-client==2.8.2
tests/models/multimodal/generation/test_common.py
+25
@@ -529,6 +529,31 @@ VLM_TEST_SETTINGS = {
529
use_tokenizer_eos=True,
530
auto_cls=AutoModelForImageTextToText,
531
),
532
+ "isaac": VLMTestInfo(
533
+ models=["PerceptronAI/Isaac-0.1"],
534
+ test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
535
+ prompt_formatter=lambda img_prompt: (
536
+ f"<|im_start|>User\n{img_prompt}<|im_end|>\n<|im_start|>assistant\n"
537
+ ),
538
+ img_idx_to_prompt=lambda idx: "<image>",
539
+ single_image_prompts=IMAGE_ASSETS.prompts(
540
+ {
541
+ "stop_sign": "<vlm_image>Please describe the image shortly.",
542
+ "cherry_blossom": "<vlm_image>Please infer the season with reason.",
543
+ }
544
+ ),
545
+ multi_image_prompt=(
546
+ "Picture 1: <vlm_image>\n"
547
+ "Picture 2: <vlm_image>\n"
548
+ "Describe these two images with one paragraph respectively."
549
+ ),
550
+ enforce_eager=False,
551
+ max_model_len=4096,
552
+ max_num_seqs=2,
553
+ hf_model_kwargs={"device_map": "auto"},
554
+ patch_hf_runner=model_utils.isaac_patch_hf_runner,
555
+ image_size_factors=[(0.25,), (0.25, 0.25, 0.25), (0.25, 0.2, 0.15)],
556
+ ),
557
"kimi_vl": VLMTestInfo(
558
models=["moonshotai/Kimi-VL-A3B-Instruct"],
559
test_type=(VLMTestType.IMAGE, VLMTestType.MULTI_IMAGE),
tests/models/multimodal/generation/vlm_utils/model_utils.py
+177
@@ -522,6 +522,183 @@ def h2ovl_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
522
return hf_model
523
524
525
+def isaac_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
526
+ """Patch HF runner for Isaac:
527
+ 1) Move processor outputs to model device
528
+ 2) Ensure IsaacModel.forward returns hidden_states
529
+ for compatibility with hidden_states_to_seq_logprobs()
530
+ """
531
+
532
+ from perceptron.tensorstream import TextType
533
+ from perceptron.tensorstream.ops import compute_mrope_pos_tensor, modality_mask
534
+ from transformers.modeling_outputs import BaseModelOutputWithPast
535
+
536
+ def compute_position_ids_input_ids(input_ids: torch.Tensor) -> torch.Tensor:
537
+ """
538
+ Create 3D positional indices for token input.
539
+ """
540
+ batch_size, seq_length = input_ids.shape
541
+ position_ids = torch.arange(seq_length, device=input_ids.device)
542
+ position_ids = position_ids.view(1, -1).expand(batch_size, -1)
543
+ position_ids = position_ids.unsqueeze(2).expand(-1, -1, 3) # Add 3D for MRoPE
544
+ return position_ids
545
+
546
+ model_device = next(hf_model.model.parameters()).device
547
+
548
+ # ----------------------------
549
+ # 1) Patch processor: move BatchFeature input_ids and TensorStream to model device
550
+ # ----------------------------
551
+ original_processor = hf_model.processor
552
+
553
+ def patched_processor(*args, **kwargs):
554
+ result = original_processor(*args, **kwargs)
555
+ for k, v in result.data.items():
556
+ result[k] = v.to(model_device)
557
+ return result
558
+
559
+ hf_model.processor = patched_processor
560
+
561
+ tokenizer = AutoTokenizer.from_pretrained(
562
+ hf_model.model_name, trust_remote_code=True
563
+ )
564
+
565
+ original_generate = hf_model.model.generate
566
+
567
+ def patched_generate(*args, **kwargs):
568
+ kwargs["pad_token_id"] = tokenizer.eos_token_id
569
+ kwargs["eos_token_id"] = tokenizer.eos_token_id
570
+ return original_generate(*args, **kwargs)
571
+
572
+ hf_model.model.generate = patched_generate
573
+
574
+ # ----------------------------
575
+ # 2) Patch IsaacModel.forward: add hidden_states to the output
576
+ # ----------------------------
577
+ isaac_model = hf_model.model.model
578
+
579
+ def patched_forward(
580
+ self,
581
+ input_ids=None,
582
+ tensor_stream=None,
583
+ attention_mask=None,
584
+ position_ids=None,
585
+ modality_tensor=None,
586
+ past_key_values=None,
587
+ inputs_embeds=None,
588
+ use_cache=None,
589
+ output_hidden_states=None,
590
+ return_dict=None,
591
+ cache_position=None,
592
+ **kwargs,
593
+ ):
594
+ """
595
+ Forward pass with MRoPE position embeddings.
596
+ Computes position embeddings once and passes them through all layers.
597
+ """
598
+ output_hidden_states = (
599
+ output_hidden_states
600
+ if output_hidden_states is not None
601
+ else self.config.output_hidden_states
602
+ )
603
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
604
+ return_dict = (
605
+ return_dict if return_dict is not None else self.config.use_return_dict
606
+ )
607
+
608
+ # Get inputs
609
+ if tensor_stream is not None and inputs_embeds is not None:
610
+ raise ValueError("You cannot specify both tensor_stream and inputs_embeds")
611
+ elif tensor_stream is not None:
612
+ # Embed TensorStream directly
613
+ inputs_embeds = self.embed_stream(tensor_stream)
614
+ # Create modality tensor if not provided
615
+ if modality_tensor is None:
616
+ modality_tensor = modality_mask(tensor_stream)
617
+ elif input_ids is not None and inputs_embeds is not None:
618
+ raise ValueError(
619
+ "You cannot specify both input_ids and inputs_embeds at the same time"
620
+ )
621
+ elif input_ids is not None:
622
+ inputs_embeds = self.embed_tokens(input_ids)
623
+ # Create text modality tensor if not provided
624
+ if modality_tensor is None:
625
+ batch_size, seq_length = input_ids.shape
626
+ modality_tensor = torch.full(
627
+ (batch_size, seq_length),
628
+ TextType.text.value,
629
+ device=input_ids.device,
630
+ dtype=torch.long,
631
+ )
632
+ elif inputs_embeds is None:
633
+ raise ValueError(
634
+ "You have to specify either tensor_stream, input_ids or inputs_embeds"
635
+ )
636
+
637
+ # Create default position_ids if not provided
638
+ if position_ids is None:
639
+ if tensor_stream is not None:
640
+ position_ids = compute_mrope_pos_tensor(tensor_stream) # (B,L,3)
641
+ else:
642
+ position_ids = compute_position_ids_input_ids(input_ids)
643
+
644
+ # Compute MRoPE position embeddings if we have custom rotary_emb
645
+ cos, sin = self.rotary_emb(position_ids, modality_tensor)
646
+ cos = cos.to(inputs_embeds.dtype)
647
+ sin = sin.to(inputs_embeds.dtype)
648
+
649
+ # Prepare attention mask
650
+ if attention_mask is not None:
651
+ attention_mask = self._update_causal_mask(
652
+ attention_mask, inputs_embeds, cache_position, past_key_values, False
653
+ )
654
+
655
+ # Initialize and collect hidden states
656
+ hidden_states = inputs_embeds
657
+ hidden_states_list: list[torch.Tensor] = []
658
+
659
+ if output_hidden_states:
660
+ hidden_states_list.append(hidden_states)
661
+
662
+ for decoder_layer in self.layers:
663
+ layer_outputs = decoder_layer(
664
+ hidden_states,
665
+ attention_mask=attention_mask,
666
+ position_ids=position_ids,
667
+ past_key_value=past_key_values,
668
+ use_cache=use_cache,
669
+ cache_position=cache_position,
670
+ position_embeddings=(cos, sin),
671
+ **kwargs,
672
+ )
673
+
674
+ hidden_states = (
675
+ layer_outputs[0] if isinstance(layer_outputs, tuple) else layer_outputs
676
+ )
677
+
678
+ if output_hidden_states:
679
+ hidden_states_list.append(hidden_states)
680
+
681
+ # Final layer norm
682
+ hidden_states = self.norm(hidden_states)
683
+
684
+ if output_hidden_states:
685
+ hidden_states_list.append(hidden_states)
686
+
687
+ # Convert to tuple or None
688
+ all_hidden_states = tuple(hidden_states_list) if output_hidden_states else None
689
+
690
+ # Include hiden_states for compatibility with hidden_states_to_seq_logprobs()
691
+ return BaseModelOutputWithPast(
692
+ last_hidden_state=hidden_states,
693
+ past_key_values=past_key_values,
694
+ hidden_states=all_hidden_states,
695
+ )
696
+
697
+ isaac_model.forward = types.MethodType(patched_forward, isaac_model)
698
+
699
+ return hf_model
700
+
701
+
702
def skyworkr1v_patch_hf_runner(hf_model: HfRunner) -> HfRunner:
703
"""Patches and returns an instance of the HfRunner to use for SkyworkR1V."""
704
tests/models/registry.py
+4
@@ -662,6 +662,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
662
"HuggingFaceM4/Idefics3-8B-Llama3",
663
extras={"tiny": "HuggingFaceTB/SmolVLM-256M-Instruct"},
664
),
665
+ "IsaacForConditionalGeneration": _HfExamplesInfo(
666
+ "PerceptronAI/Isaac-0.1",
667
+ trust_remote_code=True,
668
+ ),
669
"InternS1ForConditionalGeneration": _HfExamplesInfo(
670
"internlm/Intern-S1", trust_remote_code=True
671
),
vllm/model_executor/models/isaac.py
new
+1480
@@ -0,0 +1,1480 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from __future__ import annotations
4
+
5
+import math
6
+from collections.abc import Iterable, Iterator, Mapping, Sequence
7
+from typing import Annotated, Any
8
+
9
+import numpy as np
10
+import PIL.Image
11
+import torch
12
+import torch.nn as nn
13
+import torch.nn.functional as F
14
+from einops import rearrange
15
+from transformers.image_processing_utils import BatchFeature
16
+from transformers.tokenization_utils import TensorType
17
+from typing_extensions import TypedDict, Unpack
18
+
19
+from vllm.attention.layers.mm_encoder_attention import MMEncoderAttention
20
+from vllm.config import MultiModalConfig, VllmConfig
21
+from vllm.config.model import ModelConfig
22
+from vllm.distributed import parallel_state
23
+from vllm.distributed import utils as dist_utils
24
+from vllm.model_executor.layers.linear import (
25
+ ColumnParallelLinear,
26
+ QKVParallelLinear,
27
+ ReplicatedLinear,
28
+ RowParallelLinear,
29
+)
30
+from vllm.model_executor.layers.quantization import QuantizationConfig
31
+from vllm.model_executor.model_loader.weight_utils import (
32
+ default_weight_loader,
33
+)
34
+from vllm.model_executor.models.interfaces import (
35
+ MultiModalEmbeddings,
36
+ SupportsLoRA,
37
+ SupportsMRoPE,
38
+ SupportsMultiModal,
39
+ SupportsPP,
40
+)
41
+from vllm.model_executor.models.module_mapping import MultiModelKeys
42
+from vllm.model_executor.models.siglip import SiglipMLP
43
+from vllm.model_executor.models.utils import (
44
+ AutoWeightsLoader,
45
+ WeightsMapper,
46
+ init_vllm_registered_model,
47
+ maybe_prefix,
48
+)
49
+from vllm.multimodal import MULTIMODAL_REGISTRY
50
+from vllm.multimodal.inputs import (
51
+ MultiModalDataDict,
52
+ MultiModalFeatureSpec,
53
+ MultiModalFieldConfig,
54
+ MultiModalKwargsItems,
55
+)
56
+from vllm.multimodal.parse import ImageSize, MultiModalDataItems
57
+from vllm.multimodal.processing import (
58
+ BaseMultiModalProcessor,
59
+ BaseProcessingInfo,
60
+ PromptReplacement,
61
+ PromptUpdate,
62
+ PromptUpdateDetails,
63
+)
64
+from vllm.multimodal.profiling import BaseDummyInputsBuilder
65
+from vllm.sequence import IntermediateTensors
66
+from vllm.tokenizers import get_tokenizer
67
+from vllm.tokenizers.hf import get_cached_tokenizer
68
+from vllm.transformers_utils.configs import (
69
+ IsaacConfig,
70
+ PixelShuffleSiglip2VisionConfig,
71
+)
72
+from vllm.utils.tensor_schema import TensorSchema, TensorShape
73
+
74
+
75
+def create_cumulative_seq_lengths(
76
+ seq_sizes: torch.Tensor, device: torch.device
77
+) -> tuple[torch.Tensor, torch.Tensor]:
78
+ """Create cumulative sequence lengths for variable-length attention."""
79
+ cu_seqlens = torch.zeros(len(seq_sizes) + 1, dtype=torch.int32, device=device)
80
+ cu_seqlens[1:] = seq_sizes.cumsum(0)
81
+ max_seqlen = (
82
+ seq_sizes.max()
83
+ if len(seq_sizes) > 0
84
+ else torch.tensor(0, dtype=torch.int32, device=device)
85
+ )
86
+ return cu_seqlens, max_seqlen
87
+
88
+
89
+class Siglip2VariableSequenceEmbeddings(nn.Module):
90
+ def __init__(self, config: PixelShuffleSiglip2VisionConfig):
91
+ super().__init__()
92
+ self.config = config
93
+ self.embed_dim = config.hidden_size
94
+ self.patch_size = config.patch_size
95
+
96
+ self.patch_embedding = ReplicatedLinear(
97
+ input_size=config.num_channels * self.patch_size * self.patch_size,
98
+ output_size=self.embed_dim,
99
+ return_bias=False,
100
+ )
101
+
102
+ self.num_patches = config.num_patches
103
+ self.position_embedding_size = int(self.num_patches**0.5)
104
+ self.position_embedding = nn.Embedding(self.num_patches, self.embed_dim)
105
+
106
+ def positional_embeddings(
107
+ self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
108
+ ) -> torch.Tensor:
109
+ # Prepare positional embeddings grid: (1, embed_dim, h, w)
110
+ positional_embeddings = (
111
+ self.position_embedding.weight.reshape(
112
+ self.position_embedding_size, self.position_embedding_size, -1
113
+ )
114
+ .permute(2, 0, 1)
115
+ .unsqueeze(0)
116
+ )
117
+
118
+ _seq_patches, _seq_sizes, spatial_shapes = packed_seq_patches
119
+ pos_embeds_list = []
120
+ mode = "bilinear"
121
+ align_corners = False
122
+ antialias = True
123
+ for spatial_shape in spatial_shapes:
124
+ height, width = int(spatial_shape[0]), int(spatial_shape[1])
125
+ # Guard to ensure height and width are positive for torch.compile
126
+ if height > 0 and width > 0:
127
+ resized_pos_embed = F.interpolate(
128
+ positional_embeddings,
129
+ size=(height, width),
130
+ mode=mode,
131
+ align_corners=align_corners,
132
+ antialias=antialias,
133
+ )
134
+ # Reshape from (1, embed_dim, height, width) to
135
+ # (height*width, embed_dim)
136
+ resized_pos_embed = resized_pos_embed.reshape(
137
+ self.embed_dim, height * width
138
+ ).transpose(0, 1)
139
+ else:
140
+ # Fallback - should never happen in practice
141
+ resized_pos_embed = positional_embeddings.reshape(
142
+ self.embed_dim,
143
+ self.position_embedding_size * self.position_embedding_size,
144
+ ).transpose(0, 1)[: height * width]
145
+ pos_embeds_list.append(resized_pos_embed)
146
+
147
+ # Concatenate all positional embeddings along the sequence dimension
148
+ pos_embeds = torch.cat(pos_embeds_list, dim=0)
149
+ return pos_embeds
150
+
151
+ def forward(
152
+ self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
153
+ ):
154
+ seq_patches, _seq_sizes, _spatial_shapes = packed_seq_patches
155
+
156
+ target_weight = self.patch_embedding.weight
157
+ seq_patches = seq_patches.to(
158
+ device=target_weight.device, dtype=target_weight.dtype
159
+ )
160
+ patch_embeds = self.patch_embedding(seq_patches)
161
+ pos_embeds = self.positional_embeddings(packed_seq_patches)
162
+
163
+ # Flatten patch embeddings to match positional embeddings format
164
+ if patch_embeds.dim() == 3:
165
+ patch_embeds = patch_embeds.view(-1, patch_embeds.size(-1))
166
+
167
+ # Add positional embeddings to patch embeddings
168
+ embeddings = patch_embeds + pos_embeds
169
+ return embeddings
170
+
171
+
172
+def create_pixel_shuffle_index_map(
173
+ seq_sizes: torch.Tensor,
174
+ token_grids: torch.Tensor,
175
+ scale_factor: int = 1,
176
+ device: torch.device | None = None,
177
+) -> torch.Tensor:
178
+ """
179
+ Build a gather-index map that tells us, for every *output* token after
180
+ pixel-shuffle, which `scale_factor**2` *input* tokens are being merged.
181
+
182
+ Args
183
+ ----
184
+ seq_sizes : (num_images,) - #patches in each image (row-major order)
185
+ token_grids : (num_images,2) - (height, width) for every image
186
+ scale_factor : spatial down-scale factor (≥2)
187
+ device : (optional) overrides `seq_sizes.device`
188
+
189
+ Returns
190
+ -------
191
+ gather_idx : (new_total_seq_len, scale_factor**2) int64 tensor.
192
+ gather_idx[i, j] is the *flat* index into the *original*
193
+ packed sequence for the j-th sub-patch that forms the
194
+ i-th output token.
195
+ """
196
+ if device is None:
197
+ device = seq_sizes.device
198
+
199
+ r = int(scale_factor)
200
+ if r < 2:
201
+ raise ValueError("`scale_factor` must be ≥ 2")
202
+
203
+ # Safety: all spatial dims must be divisible by r
204
+ # Cannot run under torch compile fullgraph mode hence
205
+ if not torch.compiler.is_compiling() and not (
206
+ (token_grids[:, 0] % r == 0).all() and (token_grids[:, 1] % r == 0).all()
207
+ ):
208
+ raise AssertionError(
209
+ "Every (H,W) in `token_grids` must be divisible by "
210
+ f"scale_factor={r}, got {token_grids.tolist()}"
211
+ )
212
+
213
+ gather_chunks: list[torch.Tensor] = []
214
+ tok_offset = 0
215
+
216
+ for seq_len, (h, w) in zip(seq_sizes.tolist(), token_grids.tolist(), strict=False):
217
+ # Build the (H, W) grid of flat indices for this image
218
+ grid = torch.arange(seq_len, device=device, dtype=torch.int64) + tok_offset
219
+ grid = grid.view(h, w) # (H, W)
220
+
221
+ # -------- identical ordering to your fixed-res routine --------
222
+ # Step 1: split width into blocks of r
223
+ grid = grid.view(h, w // r, r) # (H, W/r, r)
224
+ # Step 2: now split height into blocks of r
225
+ grid = grid.view(h // r, r, w // r, r) # (H/r, r, W/r, r)
226
+ # Step 3: final permutation to (H/r, W/r, r, r)
227
+ grid = grid.permute(0, 2, 1, 3).contiguous() # (H/r, W/r, r, r)
228
+ # Step 4: each (r, r) block forms one output token
229
+ gather_chunks.append(grid.reshape(-1, r * r)) # (H*W / r², r²)
230
+
231
+ tok_offset += seq_len
232
+
233
+ # Concatenate over all images in the packed batch
234
+ gather_idx = torch.cat(gather_chunks, dim=0) # (Σ_i HᵢWᵢ/r², r²)
235
+ return gather_idx
236
+
237
+
238
+def pixel_shuffle_varlen(
239
+ x: torch.Tensor,
240
+ token_grids: torch.Tensor,
241
+ scale_factor: int = 1,
242
+) -> torch.Tensor:
243
+ r"""Apply pixel shuffle to a packed vision sequence without unpacking per image.
244
+
245
+ Args:
246
+ x (`torch.Tensor`):
247
+ Concatenated vision embeddings. Accepts `(seq_len, hidden_size)` or
248
+ `(1, seq_len, hidden_size)` shapes produced by stacking image
249
+ patches.
250
+ token_grids (`torch.Tensor`):
251
+ Integer tensor of shape `(num_images, 2)` whose rows give the
252
+ `(height, width)` patch grid sizes corresponding to each image
253
+ segment inside `x`.
254
+ scale_factor (`int`, *optional*, defaults to 1):
255
+ Spatial down-sampling factor specific to pixel shuffle. Values
256
+ greater than one merge `scale_factor**2` neighboring patches into a
257
+ single embedding channel-group.
258
+
259
+ Returns:
260
+ `torch.Tensor`: Pixel-shuffled embeddings with shape matching the input
261
+ convention: `(seq_len, hidden_size * scale_factor**2)` when the input
262
+ was 2D, or `(1, seq_len, hidden_size * scale_factor**2)` if the
263
+ singleton batch dimension was present.
264
+
265
+ Raises:
266
+ ValueError: If more than one batch item is provided.
267
+ """
268
+ keep_batch_dim = x.dim() == 3
269
+ if keep_batch_dim:
270
+ if x.size(0) != 1:
271
+ raise AssertionError("Packed sequence is expected to have batch_size == 1")
272
+ x_ = x.squeeze(0) # (seq, embed)
273
+ else:
274
+ x_ = x # (seq, embed)
275
+
276
+ embed_dim = x_.size(-1)
277
+ r = int(scale_factor)
278
+
279
+ # Calculate seq_sizes from token_grids
280
+ seq_sizes = torch.prod(token_grids, dim=-1)
281
+
282
+ # Build index map and gather in one go
283
+ gather_idx = create_pixel_shuffle_index_map(
284
+ seq_sizes=seq_sizes,
285
+ token_grids=token_grids,
286
+ scale_factor=r,
287
+ device=x_.device,
288
+ ) # (new_seq, r²)
289
+
290
+ # Gather → (new_seq, r², embed_dim)
291
+ gathered = x_[gather_idx] # fancy indexing keeps gradient
292
+
293
+ # Merge the r² group dimension into channels to finish the shuffle
294
+ out = gathered.reshape(gathered.size(0), embed_dim * r * r)
295
+
296
+ # Restore batch dimension if needed
297
+ if keep_batch_dim:
298
+ out = out.unsqueeze(0)
299
+ return out
300
+
301
+
302
+# ============================================================================
303
+# Configuration
304
+# ============================================================================
305
+
306
+MAX_PIXELS = 60_000_000 # 60-megapixel ceiling ≈ 8200 × 7300 px
307
+
308
+# Vision preprocessing constants
309
+VISION_MEAN = (0.5, 0.5, 0.5)
310
+VISION_STD = (0.5, 0.5, 0.5)
311
+VISION_SCALE = 1 / 255
312
+
313
+
314
+def _make_writeable(arr: np.ndarray) -> np.ndarray:
315
+ """Return *arr* itself if it is already writeable, otherwise try to flip the
316
+ write flag in-place and finally fall back to `arr.copy()`.
317
+ This guarantees the buffer handed to `torch.from_numpy()` is always
318
+ writeable, silencing the PyTorch warning about undefined behaviour.
319
+ """
320
+ if arr.flags.writeable:
321
+ return arr
322
+
323
+ # First, try the cheap path — in-place flag toggle (works for mmap'd arrays
324
+ # and some shared memory buffers):
325
+ try:
326
+ arr.setflags(write=True)
327
+ return arr # success: no data copy
328
+ except ValueError:
329
+ # Buffer is inherently read-only (e.g. backed by PyAV / PIL): make copy
330
+ return arr.copy()
331
+
332
+
333
+def extract_image_pil(image: PIL.Image.Image) -> torch.Tensor | None:
334
+ if image.width * image.height > MAX_PIXELS:
335
+ raise ValueError(
336
+ f"Image (w={image.width}, h={image.height}) > MAX=`{MAX_PIXELS}`"
337
+ )
338
+ img = image if image.mode == "RGB" else image.convert("RGB")
339
+ arr = np.asarray(img)
340
+ arr = _make_writeable(arr)
341
+ return torch.from_numpy(arr)
342
+
343
+
344
+def get_image_size_for_max_num_patches(
345
+ image_height: int,
346
+ image_width: int,
347
+ patch_size: int,
348
+ max_num_patches: int,
349
+ min_num_patches: int | None = None,
350
+ eps: float = 1e-5,
351
+ pixel_shuffle_scale: int = 1,
352
+) -> tuple[int, int]:
353
+ r"""Compute a target resolution whose patch grid satisfies patching parametrization.
354
+
355
+ Args:
356
+ image_height (`int`):
357
+ Height in pixels of the source image prior to any resizing.
358
+ image_width (`int`):
359
+ Width in pixels of the source image prior to any resizing.
360
+ patch_size (`int`):
361
+ Size of the square patch used by the vision encoder.
362
+ max_num_patches (`int`):
363
+ Upper bound on `(height / patch_size) * (width / patch_size)` after
364
+ resizing.
365
+ min_num_patches (`int`, *optional*):
366
+ Lower bound on the number of patches. When provided the image will
367
+ be scaled up if necessary.
368
+ eps (`float`, *optional*, defaults to 1e-5):
369
+ Convergence tolerance for the internal binary search to determine
370
+ the target dimensions.
371
+ pixel_shuffle_scale (`int`, *optional*, defaults to 1):
372
+ Additional stride multiplier applied when pixel shuffle later
373
+ reduces spatial resolution.
374
+
375
+ Returns:
376
+ `tuple[int, int]`: Height and width (in pixels) that are multiples of
377
+ `patch_size * pixel_shuffle_scale` and respect both the maximum and
378
+ optional minimum patch-count constraints.
379
+ """
380
+
381
+ def get_scaled_image_size(scale, original_size, patch_size, pixel_shuffle_scale):
382
+ scaled_size = scale * original_size
383
+ divisor = patch_size * pixel_shuffle_scale
384
+ scaled_size = math.ceil(scaled_size / divisor) * divisor
385
+ scaled_size = max(divisor, scaled_size)
386
+ return int(scaled_size)
387
+
388
+ # Ensure divisibility
389
+ divisor = patch_size * pixel_shuffle_scale
390
+ adjusted_height = math.ceil(image_height / divisor) * divisor
391
+ adjusted_height = max(divisor, adjusted_height)
392
+ adjusted_width = math.ceil(image_width / divisor) * divisor
393
+ adjusted_width = max(divisor, adjusted_width)
394
+
395
+ num_patches = (adjusted_height / patch_size) * (adjusted_width / patch_size)
396
+
397
+ if min_num_patches is not None and num_patches < min_num_patches:
398
+ # Scale up
399
+ scale_min, scale_max = 1.0, 100.0
400
+ while (scale_max - scale_min) >= eps:
401
+ scale = (scale_min + scale_max) / 2
402
+ target_height = get_scaled_image_size(
403
+ scale, image_height, patch_size, pixel_shuffle_scale
404
+ )
405
+ target_width = get_scaled_image_size(
406
+ scale, image_width, patch_size, pixel_shuffle_scale
407
+ )
408
+ num_patches = (target_height / patch_size) * (target_width / patch_size)
409
+ if num_patches >= min_num_patches:
410
+ scale_max = scale
411
+ else:
412
+ scale_min = scale
413
+ scale = scale_max
414
+ target_height = get_scaled_image_size(
415
+ scale, image_height, patch_size, pixel_shuffle_scale
416
+ )
417
+ target_width = get_scaled_image_size(
418
+ scale, image_width, patch_size, pixel_shuffle_scale
419
+ )
420
+ return target_height, target_width
421
+ elif num_patches <= max_num_patches:
422
+ return adjusted_height, adjusted_width
423
+ else:
424
+ # Scale down
425
+ scale_min, scale_max = eps / 10, 1.0
426
+ while (scale_max - scale_min) >= eps:
427
+ scale = (scale_min + scale_max) / 2
428
+ target_height = get_scaled_image_size(
429
+ scale, image_height, patch_size, pixel_shuffle_scale
430
+ )
431
+ target_width = get_scaled_image_size(
432
+ scale, image_width, patch_size, pixel_shuffle_scale
433
+ )
434
+ num_patches = (target_height / patch_size) * (target_width / patch_size)
435
+ if num_patches <= max_num_patches:
436
+ scale_min = scale
437
+ else:
438
+ scale_max = scale
439
+ scale = scale_min
440
+ target_height = get_scaled_image_size(
441
+ scale, image_height, patch_size, pixel_shuffle_scale
442
+ )
443
+ target_width = get_scaled_image_size(
444
+ scale, image_width, patch_size, pixel_shuffle_scale
445
+ )
446
+ return target_height, target_width
447
+
448
+
449
+_MEAN_TENSOR = torch.tensor(VISION_MEAN, dtype=torch.float32).view(1, 1, 1, -1)
450
+_STD_TENSOR = torch.tensor(VISION_STD, dtype=torch.float32).view(1, 1, 1, -1)
451
+
452
+
453
+def _resolve_vision_token_id(model_config: ModelConfig, vision_token: str) -> int:
454
+ tokenizer_name = model_config.tokenizer or model_config.model
455
+ tokenizer = get_cached_tokenizer(
456
+ get_tokenizer(
457
+ tokenizer_name,
458
+ tokenizer_mode=model_config.tokenizer_mode,
459
+ trust_remote_code=model_config.trust_remote_code,
460
+ revision=model_config.tokenizer_revision or model_config.revision,
461
+ )
462
+ )
463
+ return tokenizer.encode(vision_token, add_special_tokens=False)[0]
464
+
465
+
466
+def prepare_image_tensor(
467
+ image: torch.Tensor,
468
+ scale: float = VISION_SCALE,
469
+) -> torch.Tensor:
470
+ r"""Standardize RGB images prior to patch extraction via rescaling and whitening.
471
+
472
+ Args:
473
+ image (`torch.Tensor`):
474
+ Tensor with shape `(..., height, width, 3)` containing RGB values.
475
+ The tensor is converted to floating point if needed.
476
+ scale (`float`, *optional*, defaults to `VISION_SCALE`):
477
+ Scalar multiplier applied before normalization.
478
+ Returns:
479
+ `torch.Tensor`: Normalized tensor with the same shape as the input and
480
+ dtype `torch.float32`.
481
+ """
482
+ if not torch.is_floating_point(image):
483
+ image = image.float()
484
+ rescaled = image * scale
485
+
486
+ # Use precomputed tensors and move to the correct device if needed
487
+ mean_tensor = _MEAN_TENSOR.to(image.device)
488
+ std_tensor = _STD_TENSOR.to(image.device)
489
+
490
+ normalized = (rescaled - mean_tensor) / std_tensor
491
+ return normalized
492
+
493
+
494
+def patchify_vision(image: torch.Tensor, patch_size: int) -> torch.Tensor:
495
+ r"""Convert normalized images into flattened ViT-style patches.
496
+
497
+ Args:
498
+ image (`torch.Tensor`):
499
+ Tensor of shape `(num_images, height, width, channels)`.
500
+ patch_size (`int`):
501
+ Edge length of the square patches
502
+
503
+ Returns:
504
+ `torch.Tensor`:
505
+ Patch tensor where each position stores the flattened pixels
506
+ belonging to that patch.
507
+
508
+ Raises:
509
+ ValueError: If `height` or `width` is not divisible by `patch_size`.
510
+ """
511
+ num_images, height, width, channels = image.shape
512
+ if height % patch_size or width % patch_size:
513
+ raise ValueError(
514
+ "Dimensions of images "
515
+ f"{image.shape} are not divisible by patch_size={patch_size}."
516
+ )
517
+ patches = image.reshape(
518
+ num_images,
519
+ height // patch_size,
520
+ patch_size,
521
+ width // patch_size,
522
+ patch_size,
523
+ channels,
524
+ )
525
+ patches = patches.permute(0, 1, 3, 2, 4, 5)
526
+ patches = patches.reshape(
527
+ num_images,
528
+ height // patch_size,
529
+ width // patch_size,
530
+ channels * patch_size * patch_size,
531
+ )
532
+ return patches
533
+
534
+
535
+def process_vision_for_patches(
536
+ images: torch.Tensor,
537
+ patch_size: int,
538
+ max_num_patches: int,
539
+ min_num_patches: int | None = None,
540
+ pixel_shuffle_scale: int = 1,
541
+) -> tuple[torch.Tensor, list[int]]:
542
+ r"""Resize, normalize, and patchify RGB images for the vision encoder.
543
+
544
+ Args:
545
+ images (`torch.Tensor`):
546
+ Either `(height, width, channels)` for a single image or
547
+ `(num_images, height, width, channels)` for a batch. Channels are
548
+ expected to be RGB.
549
+ patch_size (`int`):
550
+ Edge length of square patches; implictly controls resize grid granularity.
551
+ max_num_patches (`int`):
552
+ Maximum number of patches allowed after resizing.
553
+ min_num_patches (`int`, *optional*):
554
+ Minimum number of patches. If provided, the routine upsamples images
555
+ as needed to satisfy the lower bound.
556
+ pixel_shuffle_scale (`int`, *optional*, defaults to 1):
557
+ Pixel shuffle scale factor; influences the target grid that the
558
+ function produces.
559
+
560
+ Returns:
561
+ `tuple[torch.Tensor, list[int]]`: A pair `(patches, dims_virtual)`
562
+ where `patches` has shape `(num_images, target_h / patch_size, target_w
563
+ / patch_size, channels * patch_size**2)` and `dims_virtual` encodes
564
+ effective `(images, height, width)` dimensions after optional pixel
565
+ shuffling.
566
+ """
567
+ # Add batch dim if single image
568
+ if images.dim() == 3:
569
+ images = images.unsqueeze(0)
570
+
571
+ # Permute to channel first for resize
572
+ images = images.permute(0, 3, 1, 2)
573
+
574
+ # Get target dimensions
575
+ _, _, orig_height, orig_width = images.shape
576
+ target_height, target_width = get_image_size_for_max_num_patches(
577
+ orig_height,
578
+ orig_width,
579
+ patch_size,
580
+ max_num_patches,
581
+ min_num_patches=min_num_patches,
582
+ pixel_shuffle_scale=pixel_shuffle_scale,
583
+ )
584
+
585
+ # Resize
586
+ images = F.interpolate(
587
+ images,
588
+ size=(target_height, target_width),
589
+ mode="bilinear",
590
+ align_corners=False,
591
+ )
592
+
593
+ # Back to channel last
594
+ images = images.permute(0, 2, 3, 1)
595
+
596
+ # Normalize
597
+ images = prepare_image_tensor(images)
598
+
599
+ # Patchify
600
+ patches = patchify_vision(images, patch_size=patch_size)
601
+
602
+ # Calculate dimensions for the patches
603
+ n_images, h_patches, w_patches, _ = patches.shape
604
+ dims_virtual = (
605
+ [1, h_patches, w_patches]
606
+ if pixel_shuffle_scale == 1
607
+ else [1, h_patches // pixel_shuffle_scale, w_patches // pixel_shuffle_scale]
608
+ )
609
+
610
+ return patches, dims_virtual
611
+
612
+
613
+class IsaacImageProcessorKwargs(TypedDict, total=False):
614
+ patch_size: int
615
+ max_num_patches: int
616
+ min_num_patches: int
617
+ pixel_shuffle_scale: int
618
+
619
+
620
+class IsaacImageProcessor:
621
+ patch_size = 16
622
+ max_num_patches = 6144
623
+ min_num_patches = 256
624
+ pixel_shuffle_scale = 2
625
+
626
+ valid_kwargs = IsaacImageProcessorKwargs
627
+ model_input_names = ["pixel_values", "image_grid_thw"]
628
+
629
+ def __init__(self, kwargs):
630
+ self.patch_size = kwargs.pop("patch_size", self.patch_size)
631
+ self.vision_max_num_patches = kwargs.pop(
632
+ "vision_max_num_patches", self.max_num_patches
633
+ )
634
+ self.vision_min_num_patches = kwargs.pop(
635
+ "vision_min_num_patches", self.min_num_patches
636
+ )
637
+ self.pixel_shuffle_scale = kwargs.pop("pixel_shuffle_scale", 2)
638
+
639
+ def preprocess(
640
+ self,
641
+ images: list[torch.Tensor],
642
+ return_tensors: str | TensorType | None,
643
+ **kwargs: Unpack[IsaacImageProcessorKwargs],
644
+ ) -> BatchFeature:
645
+ """Preprocess images into format compatibile with vLLM input processing."""
646
+
647
+ all_pixel_values: list[torch.Tensor] = []
648
+ all_image_grids: list[torch.Tensor] = []
649
+
650
+ for image in images:
651
+ image_tensor = extract_image_pil(image)
652
+
653
+ patches, dims_virtual = process_vision_for_patches(
654
+ image_tensor,
655
+ patch_size=self.patch_size,
656
+ max_num_patches=self.vision_max_num_patches,
657
+ min_num_patches=self.vision_min_num_patches,
658
+ pixel_shuffle_scale=self.pixel_shuffle_scale,
659
+ )
660
+
661
+ # Isaac packs a dummy temporal dim for images
662
+ patches = patches.unsqueeze(1) # [N, T=1, Hp, Wp, D]
663
+
664
+ hp, wp, dim = patches.shape[-3], patches.shape[-2], patches.shape[-1]
665
+ current_num_patches = hp * wp
666
+ pixel_values = patches.reshape(current_num_patches, dim) # [N_tokens, D]
667
+
668
+ # Use real patch dimensions for image_grid_thw, not virtual dimensions
669
+ # This ensures the vision model receives correct grid info for pixel shuffle
670
+ dims_real = [1, hp, wp] # Real patch dimensions
671
+ image_grid_thw = torch.tensor(dims_real).unsqueeze(0)
672
+
673
+ all_pixel_values.append(pixel_values)
674
+ all_image_grids.append(image_grid_thw)
675
+
676
+ if all_pixel_values:
677
+ final_pixel_values = torch.cat(all_pixel_values, dim=0)
678
+ final_image_grids = torch.cat(all_image_grids, dim=0)
679
+ else:
680
+ final_pixel_values = torch.empty(0, 0)
681
+ final_image_grids = torch.empty(0, 3)
682
+
683
+ return BatchFeature(
684
+ data={
685
+ "pixel_values": final_pixel_values,
686
+ "image_grid_thw": final_image_grids,
687
+ },
688
+ tensor_type=return_tensors,
689
+ )
690
+
691
+
692
+class IsaacProcessor:
693
+ """Processor wrapper (tokenizer + IsaacImageProcessor)."""
694
+
695
+ def __init__(self, image_processor=None, tokenizer=None, **kwargs):
696
+ self.image_token = kwargs.pop("image_token", "<image>")
697
+ self.image_processor = image_processor or IsaacImageProcessor(kwargs)
698
+ self.tokenizer = tokenizer
699
+
700
+ def __call__(self, text=None, images=None, **kwargs) -> BatchFeature:
701
+ result = {}
702
+
703
+ if images is not None:
704
+ image_inputs = self.image_processor.preprocess(images, **kwargs)
705
+ image_grid_thw = image_inputs["image_grid_thw"]
706
+ result.update(image_inputs)
707
+
708
+ if text is not None:
709
+ if not isinstance(text, list):
710
+ text = [text]
711
+
712
+ text = text.copy() # below lines change text in-place
713
+ merge_length = self.image_processor.pixel_shuffle_scale**2
714
+ index = 0
715
+ for i in range(len(text)):
716
+ while self.image_token in text[i]:
717
+ num_image_tokens = image_grid_thw[index].prod() // merge_length
718
+ text[i] = text[i].replace(
719
+ self.image_token, "<|placeholder|>" * num_image_tokens, 1
720
+ )
721
+ index += 1
722
+ text[i] = text[i].replace("<|placeholder|>", "<|image_pad|>")
723
+
724
+ if text is not None:
725
+ result.update(self.tokenizer(text, **kwargs))
726
+
727
+ return BatchFeature(result)
728
+
729
+ def apply_chat_template(
730
+ self,
731
+ messages: list[dict[str, Any]],
732
+ tokenize: bool = False,
733
+ add_generation_prompt: bool = False,
734
+ **kwargs,
735
+ ) -> Any:
736
+ # Convert mixed content messages to simple text format
737
+ processed_messages = []
738
+
739
+ for message in messages:
740
+ if "content" in message and isinstance(message["content"], list):
741
+ # Handle mixed content (text + image)
742
+ text_parts = []
743
+ for content_item in message["content"]:
744
+ if content_item.get("type") == "text":
745
+ text_parts.append(content_item.get("text", ""))
746
+ elif content_item.get("type") == "image":
747
+ # Replace image with vision token
748
+ text_parts.append(self.image_token)
749
+
750
+ processed_message = {
751
+ "role": message.get("role", "user"),
752
+ "content": "".join(text_parts),
753
+ }
754
+ processed_messages.append(processed_message)
755
+ else:
756
+ # Regular text message
757
+ processed_messages.append(message)
758
+
759
+ return self.tokenizer.apply_chat_template(
760
+ processed_messages,
761
+ tokenize=tokenize,
762
+ add_generation_prompt=add_generation_prompt,
763
+ **kwargs,
764
+ )
765
+
766
+
767
+class IsaacProcessingInfo(BaseProcessingInfo):
768
+ def get_hf_config(self) -> IsaacConfig:
769
+ if hasattr(self.ctx, "get_hf_config"):
770
+ original_config = self.ctx.get_hf_config()
771
+ # Map HF config parameters to our vLLM config parameters
772
+ return IsaacConfig(
773
+ # Vision parameters - map from HF names
774
+ vision_config=getattr(original_config, "vision_config", None),
775
+ vision_patch_size=getattr(original_config, "video_patch_size", 16),
776
+ vision_max_num_patches=getattr(
777
+ original_config, "vision_max_num_patches", 256
778
+ ),
779
+ vision_min_num_patches=getattr(
780
+ original_config, "vision_min_num_patches", None
781
+ ),
782
+ pixel_shuffle_scale=getattr(original_config, "pixel_shuffle_scale", 1),
783
+ max_sequence_length=getattr(
784
+ original_config, "max_sequence_length", 16384
785
+ ),
786
+ vision_token=getattr(original_config, "vision_token", "<image>"),
787
+ vision_attn_implementation=getattr(
788
+ original_config, "vision_attn_implementation", None
789
+ ),
790
+ )
791
+ return IsaacConfig()
792
+
793
+ def get_hf_processor(self, **kwargs) -> IsaacProcessor:
794
+ hf_config = self.get_hf_config()
795
+ processor_kwargs = {
796
+ "image_token": hf_config.vision_token,
797
+ }
798
+ processor_kwargs.update(kwargs)
799
+ return self.ctx.get_hf_processor(IsaacProcessor, **processor_kwargs)
800
+
801
+ def get_tokenizer(self):
802
+ return self.ctx.tokenizer
803
+
804
+ def get_image_size_with_most_features(self) -> ImageSize:
805
+ hf_config = self.get_hf_config()
806
+ # Get target dimensions
807
+ target_height, target_width = get_image_size_for_max_num_patches(
808
+ 9999999,
809
+ 9999999,
810
+ hf_config.video_patch_size,
811
+ hf_config.vision_max_num_patches,
812
+ min_num_patches=hf_config.vision_min_num_patches,
813
+ pixel_shuffle_scale=hf_config.pixel_shuffle_scale,
814
+ )
815
+ return ImageSize(width=target_width, height=target_height)
816
+
817
+ def get_image_processor(self, **kwargs) -> IsaacImageProcessor:
818
+ return self.get_hf_processor(**kwargs).image_processor
819
+
820
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
821
+ return {"image": None}
822
+
823
+ def get_mm_max_tokens_per_item(
824
+ self,
825
+ seq_len: int,
826
+ mm_counts: Mapping[str, int],
827
+ ) -> Mapping[str, int]:
828
+ hf_config = self.get_hf_config()
829
+ num_vision_tokens = hf_config.vision_max_num_patches // (
830
+ hf_config.pixel_shuffle_scale**2
831
+ )
832
+ return {"image": num_vision_tokens}
833
+
834
+
835
+class IsaacDummyInputsBuilder(BaseDummyInputsBuilder[IsaacProcessingInfo]):
836
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
837
+ num_images = mm_counts.get("image", 0)
838
+
839
+ hf_processor = self.info.get_hf_processor()
840
+ image_token: str = hf_processor.image_token
841
+
842
+ return image_token * num_images
843
+
844
+ def get_dummy_mm_data(
845
+ self,
846
+ seq_len: int,
847
+ mm_counts: Mapping[str, int],
848
+ mm_options: Mapping[str] | None = None,
849
+ ) -> MultiModalDataDict:
850
+ num_images = mm_counts.get("image", 0)
851
+
852
+ target_width, target_height = self.info.get_image_size_with_most_features()
853
+ image_overrides = mm_options.get("image") if mm_options else None
854
+
855
+ return {
856
+ "image": self._get_dummy_images(
857
+ width=target_width,
858
+ height=target_height,
859
+ num_images=num_images,
860
+ overrides=image_overrides,
861
+ ),
862
+ }
863
+
864
+
865
+class IsaacImagePixelInputs(TensorSchema):
866
+ """
867
+ Schema for validating Isaac image inputs.
868
+
869
+ Dimensions:
870
+ - np: Number of patches
871
+ - d: Patch dimension
872
+ - ni: Number of images
873
+
874
+ The schema enforces:
875
+ - pixel_values must be 2D: (num_patches, patch_dim)
876
+ - image_grid_thw must be 2D: (num_images, 3)
877
+ where 3 represents [T, H, W]
878
+ """
879
+
880
+ pixel_values: Annotated[
881
+ torch.Tensor,
882
+ TensorShape("np", "d"),
883
+ ]
884
+
885
+ image_grid_thw: Annotated[
886
+ torch.Tensor,
887
+ TensorShape("ni", 3),
888
+ ]
889
+
890
+
891
+class IsaacMultiModalProcessor(BaseMultiModalProcessor):
892
+ def _get_mm_fields_config(
893
+ self,
894
+ hf_inputs: BatchFeature,
895
+ hf_processor_mm_kwargs: Mapping[str, object],
896
+ ) -> Mapping[str, MultiModalFieldConfig]:
897
+ # Configure multimodal fields for Isaac model
898
+ image_grid_thw = hf_inputs.get("image_grid_thw", torch.empty((0, 3)))
899
+ image_grid_sizes = image_grid_thw.prod(-1)
900
+
901
+ return {
902
+ "pixel_values": MultiModalFieldConfig.flat_from_sizes(
903
+ "image", image_grid_sizes
904
+ ),
905
+ "image_grid_thw": MultiModalFieldConfig.batched("image"),
906
+ }
907
+
908
+ def _get_prompt_updates(
909
+ self,
910
+ mm_items: MultiModalDataItems,
911
+ hf_processor_mm_kwargs: Mapping[str, Any],
912
+ out_mm_kwargs: MultiModalKwargsItems,
913
+ ) -> Sequence[PromptUpdate]:
914
+ image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
915
+
916
+ pixel_shuffle_scale = getattr(image_processor, "pixel_shuffle_scale", 2)
917
+ merge_length = pixel_shuffle_scale**2
918
+
919
+ def get_replacement_isaac(item_idx: int):
920
+ out_item = out_mm_kwargs["image"][item_idx]
921
+ grid_thw = out_item["image_grid_thw"].data
922
+ assert isinstance(grid_thw, torch.Tensor)
923
+
924
+ feature_size = int(grid_thw.prod()) // merge_length
925
+ repl_full = "<|image_pad|>" * feature_size
926
+ return PromptUpdateDetails.select_text(repl_full, "<|image_pad|>")
927
+
928
+ return [
929
+ PromptReplacement(
930
+ modality="image",
931
+ target="<image>",
932
+ replacement=get_replacement_isaac,
933
+ )
934
+ ]
935
+
936
+
937
+class Siglip2VisionAttention(nn.Module):
938
+ def __init__(
939
+ self,
940
+ config: PixelShuffleSiglip2VisionConfig,
941
+ quant_config: QuantizationConfig | None = None,
942
+ *,
943
+ prefix: str = "",
944
+ multimodal_config: MultiModalConfig | None = None,
945
+ ) -> None:
946
+ super().__init__()
947
+
948
+ use_data_parallel = (
949
+ multimodal_config.mm_encoder_tp_mode == "data"
950
+ if multimodal_config
951
+ else False
952
+ )
953
+ self.tp_size = (
954
+ 1
955
+ if use_data_parallel
956
+ else parallel_state.get_tensor_model_parallel_world_size()
957
+ )
958
+ self.tp_rank = parallel_state.get_tensor_model_parallel_rank()
959
+ self.hidden_size_per_attention_head = dist_utils.divide(
960
+ config.hidden_size, config.num_attention_heads
961
+ )
962
+ self.num_attention_heads_per_partition = dist_utils.divide(
963
+ config.num_attention_heads, self.tp_size
964
+ )
965
+
966
+ self.qkv_proj = QKVParallelLinear(
967
+ hidden_size=config.hidden_size,
968
+ head_size=self.hidden_size_per_attention_head,
969
+ total_num_heads=config.num_attention_heads,
970
+ total_num_kv_heads=config.num_attention_heads,
971
+ bias=True,
972
+ quant_config=quant_config,
973
+ prefix=f"{prefix}.qkv_proj",
974
+ disable_tp=use_data_parallel,
975
+ )
976
+ self.out_proj = RowParallelLinear(
977
+ input_size=config.hidden_size,
978
+ output_size=config.hidden_size,
979
+ quant_config=quant_config,
980
+ prefix=f"{prefix}.out_proj",
981
+ disable_tp=use_data_parallel,
982
+ )
983
+
984
+ self.attn = MMEncoderAttention(
985
+ num_heads=self.num_attention_heads_per_partition,
986
+ head_size=self.hidden_size_per_attention_head,
987
+ prefix=f"{prefix}.attn",
988
+ multimodal_config=multimodal_config,
989
+ )
990
+
991
+ def split_qkv(self, qkv: torch.Tensor) -> tuple[torch.Tensor, ...]:
992
+ seq_len, bs, _ = qkv.shape
993
+ q, k, v = qkv.chunk(3, dim=2)
994
+ new_shape = (
995
+ seq_len,
996
+ bs,
997
+ self.num_attention_heads_per_partition,
998
+ self.hidden_size_per_attention_head,
999
+ )
1000
+ q, k, v = (x.view(*new_shape) for x in (q, k, v))
1001
+ return q, k, v
1002
+
1003
+ def forward(
1004
+ self,
1005
+ hidden_states: torch.Tensor,
1006
+ *,
1007
+ cu_seqlens: torch.Tensor,
1008
+ max_seqlen: torch.Tensor | None,
1009
+ ) -> torch.Tensor:
1010
+ batch_size, _, _ = hidden_states.shape
1011
+ if batch_size != 1:
1012
+ raise ValueError("packed variable-length attention expects batch_size=1")
1013
+ x = rearrange(hidden_states, "b s d -> s b d")
1014
+ x, _ = self.qkv_proj(x)
1015
+ q, k, v = self.split_qkv(x)
1016
+ q, k, v = (rearrange(t, "s b h d -> b s h d") for t in (q, k, v))
1017
+
1018
+ context_layer = self.attn(
1019
+ query=q,
1020
+ key=k,
1021
+ value=v,
1022
+ cu_seqlens=cu_seqlens,
1023
+ max_seqlen=max_seqlen,
1024
+ )
1025
+ context_layer = rearrange(context_layer, "b s h d -> s b (h d)").contiguous()
1026
+
1027
+ output, _ = self.out_proj(context_layer)
1028
+ output = rearrange(output, "s b d -> b s d")
1029
+ return output
1030
+
1031
+
1032
+class Siglip2EncoderLayer(nn.Module):
1033
+ def __init__(
1034
+ self,
1035
+ config: PixelShuffleSiglip2VisionConfig,
1036
+ quant_config: QuantizationConfig | None = None,
1037
+ *,
1038
+ prefix: str = "",
1039
+ multimodal_config: MultiModalConfig | None = None,
1040
+ ) -> None:
1041
+ super().__init__()
1042
+ self.embed_dim = config.hidden_size
1043
+ self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
1044
+ self.self_attn = Siglip2VisionAttention(
1045
+ config,
1046
+ quant_config=quant_config,
1047
+ prefix=f"{prefix}.self_attn",
1048
+ multimodal_config=multimodal_config,
1049
+ )
1050
+ self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
1051
+ self.mlp = SiglipMLP(
1052
+ config,
1053
+ quant_config=quant_config,
1054
+ prefix=f"{prefix}.mlp",
1055
+ )
1056
+
1057
+ def forward(
1058
+ self,
1059
+ hidden_states: torch.Tensor,
1060
+ *,
1061
+ cu_seqlens: torch.Tensor,
1062
+ max_seqlen: torch.Tensor | None,
1063
+ ) -> torch.Tensor:
1064
+ residual = hidden_states
1065
+
1066
+ hidden_states = self.layer_norm1(hidden_states)
1067
+ hidden_states = self.self_attn(
1068
+ hidden_states=hidden_states,
1069
+ cu_seqlens=cu_seqlens,
1070
+ max_seqlen=max_seqlen,
1071
+ )
1072
+ hidden_states = residual + hidden_states
1073
+
1074
+ residual = hidden_states
1075
+ hidden_states = self.layer_norm2(hidden_states)
1076
+ hidden_states = self.mlp(hidden_states)
1077
+ hidden_states = residual + hidden_states
1078
+
1079
+ return hidden_states
1080
+
1081
+
1082
+class Siglip2Encoder(nn.Module):
1083
+ def __init__(
1084
+ self,
1085
+ config: PixelShuffleSiglip2VisionConfig,
1086
+ quant_config: QuantizationConfig | None = None,
1087
+ *,
1088
+ prefix: str = "",
1089
+ multimodal_config: MultiModalConfig | None = None,
1090
+ ) -> None:
1091
+ super().__init__()
1092
+ self.config = config
1093
+ self.layers = nn.ModuleList(
1094
+ [
1095
+ Siglip2EncoderLayer(
1096
+ config,
1097
+ quant_config=quant_config,
1098
+ prefix=f"{prefix}.layers.{layer_idx}",
1099
+ multimodal_config=multimodal_config,
1100
+ )
1101
+ for layer_idx in range(config.num_hidden_layers)
1102
+ ]
1103
+ )
1104
+
1105
+ def forward(
1106
+ self,
1107
+ inputs_embeds: torch.Tensor,
1108
+ *,
1109
+ cu_seqlens: torch.Tensor | None = None,
1110
+ max_seqlen: torch.Tensor | None = None,
1111
+ ) -> torch.Tensor:
1112
+ hidden_states = inputs_embeds
1113
+ for encoder_layer in self.layers:
1114
+ hidden_states = encoder_layer(
1115
+ hidden_states,
1116
+ cu_seqlens=cu_seqlens,
1117
+ max_seqlen=max_seqlen,
1118
+ )
1119
+ return hidden_states
1120
+
1121
+
1122
+class Siglip2VisionTransformer(nn.Module):
1123
+ def __init__(
1124
+ self,
1125
+ config: PixelShuffleSiglip2VisionConfig,
1126
+ quant_config: QuantizationConfig | None = None,
1127
+ prefix: str = "",
1128
+ multimodal_config: MultiModalConfig | None = None,
1129
+ ):
1130
+ super().__init__()
1131
+ self.config = config
1132
+ self.quant_config = quant_config
1133
+ embed_dim = config.hidden_size
1134
+
1135
+ self.embeddings = Siglip2VariableSequenceEmbeddings(config)
1136
+ self.pixel_shuffle_scale_factor = config.pixel_shuffle_scale_factor
1137
+ self.encoder = Siglip2Encoder(
1138
+ config,
1139
+ quant_config=quant_config,
1140
+ prefix=f"{prefix}.encoder",
1141
+ multimodal_config=multimodal_config,
1142
+ )
1143
+ self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
1144
+
1145
+ def forward(
1146
+ self,
1147
+ packed_seq_patches: tuple[torch.Tensor, torch.Tensor],
1148
+ ) -> torch.Tensor:
1149
+ r"""
1150
+ spatial_shapes (`torch.LongTensor` of shape `(batch_size, 2)`):
1151
+ Tensor containing the spatial dimensions (height, width)
1152
+ of the input images.
1153
+ """
1154
+
1155
+ seq_patches, token_grids = packed_seq_patches
1156
+ seq_sizes = torch.prod(token_grids, dim=-1)
1157
+
1158
+ # Get embeddings from packed sequence
1159
+ hidden_states = self.embeddings((seq_patches, seq_sizes, token_grids))
1160
+
1161
+ # Add a pseudo batch dimension for the encoder
1162
+ hidden_states = hidden_states.unsqueeze(0)
1163
+
1164
+ cu_seqlens, max_seqlen = create_cumulative_seq_lengths(
1165
+ seq_sizes, hidden_states.device
1166
+ )
1167
+
1168
+ hidden_states = self.encoder(
1169
+ inputs_embeds=hidden_states,
1170
+ cu_seqlens=cu_seqlens,
1171
+ max_seqlen=max_seqlen,
1172
+ )
1173
+ hidden_states = self.post_layernorm(hidden_states)
1174
+
1175
+ if self.pixel_shuffle_scale_factor > 1:
1176
+ hidden_states = pixel_shuffle_varlen(
1177
+ x=hidden_states,
1178
+ token_grids=token_grids,
1179
+ scale_factor=self.pixel_shuffle_scale_factor,
1180
+ )
1181
+ # Remove the pseudo batch dimension we added earlier
1182
+ hidden_states = hidden_states.squeeze(0)
1183
+
1184
+ # return last_hidden_state
1185
+ return hidden_states
1186
+
1187
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1188
+ stacked_params_mapping = [
1189
+ # (param_name, shard_name, shard_id)
1190
+ ("qkv_proj", "q_proj", "q"),
1191
+ ("qkv_proj", "k_proj", "k"),
1192
+ ("qkv_proj", "v_proj", "v"),
1193
+ ]
1194
+ params_dict = dict(self.named_parameters())
1195
+ loaded_params: set[str] = set()
1196
+
1197
+ for name, loaded_weight in weights:
1198
+ for param_name, weight_name, shard_id in stacked_params_mapping:
1199
+ if weight_name not in name:
1200
+ continue
1201
+ name = name.replace(weight_name, param_name)
1202
+
1203
+ param = params_dict[name]
1204
+ weight_loader = param.weight_loader
1205
+ weight_loader(param, loaded_weight, shard_id)
1206
+ break
1207
+ else:
1208
+ param = params_dict[name]
1209
+ weight_loader = getattr(param, "weight_loader", default_weight_loader)
1210
+ weight_loader(param, loaded_weight)
1211
+ loaded_params.add(name)
1212
+ return loaded_params
1213
+
1214
+
1215
+class IsaacVisionEmbedding(nn.Module):
1216
+ def __init__(
1217
+ self,
1218
+ vision_cfg: PixelShuffleSiglip2VisionConfig,
1219
+ hidden_dim: int,
1220
+ output_dim: int,
1221
+ quant_config: QuantizationConfig | None = None,
1222
+ multimodal_config: MultiModalConfig | None = None,
1223
+ prefix: str = "",
1224
+ ):
1225
+ super().__init__()
1226
+ self.transformer = Siglip2VisionTransformer(
1227
+ vision_cfg,
1228
+ quant_config=quant_config,
1229
+ prefix=maybe_prefix(prefix, "0"),
1230
+ multimodal_config=multimodal_config,
1231
+ )
1232
+ self.linear_fc1 = ColumnParallelLinear(
1233
+ hidden_dim,
1234
+ 4 * hidden_dim,
1235
+ bias=False,
1236
+ quant_config=quant_config,
1237
+ prefix=maybe_prefix(prefix, "1"),
1238
+ return_bias=False,
1239
+ )
1240
+ self.act = nn.SiLU()
1241
+ self.linear_fc2 = RowParallelLinear(
1242
+ 4 * hidden_dim,
1243
+ output_dim,
1244
+ bias=False,
1245
+ quant_config=quant_config,
1246
+ prefix=maybe_prefix(prefix, "3"),
1247
+ return_bias=False,
1248
+ )
1249
+
1250
+ def forward(
1251
+ self, packed_seq_patches: tuple[torch.Tensor, torch.Tensor]
1252
+ ) -> torch.Tensor:
1253
+ hidden_states = self.transformer(packed_seq_patches)
1254
+ hidden_states = self.linear_fc1(hidden_states)
1255
+ hidden_states = self.act(hidden_states)
1256
+ hidden_states = self.linear_fc2(hidden_states)
1257
+ return hidden_states
1258
+
1259
+
1260
+@MULTIMODAL_REGISTRY.register_processor(
1261
+ IsaacMultiModalProcessor,
1262
+ info=IsaacProcessingInfo,
1263
+ dummy_inputs=IsaacDummyInputsBuilder,
1264
+)
1265
+class IsaacForConditionalGeneration(
1266
+ nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP, SupportsMRoPE
1267
+):
1268
+ packed_modules_mapping = {
1269
+ "qkv_proj": [
1270
+ "q_proj",
1271
+ "k_proj",
1272
+ "v_proj",
1273
+ ],
1274
+ "gate_up_proj": [
1275
+ "gate_proj",
1276
+ "up_proj",
1277
+ ],
1278
+ }
1279
+
1280
+ supports_encoder_tp_data = True
1281
+
1282
+ # To ensure correct weight loading and mapping.
1283
+ hf_to_vllm_mapper = WeightsMapper(
1284
+ orig_to_new_prefix={
1285
+ "lm_head.": "language_model.lm_head.",
1286
+ "model.vision_embedding.0": "vision_embedding.transformer",
1287
+ "model.vision_embedding.1": "vision_embedding.linear_fc1",
1288
+ "model.vision_embedding.2": "vision_embedding.act",
1289
+ "model.vision_embedding.3": "vision_embedding.linear_fc2",
1290
+ "model.vision_embedding.": "vision_embedding.",
1291
+ "model.": "language_model.model.",
1292
+ }
1293
+ )
1294
+
1295
+ @classmethod
1296
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1297
+ if modality.startswith("image"):
1298
+ return "<image>"
1299
+
1300
+ raise ValueError("Only image modality is supported")
1301
+
1302
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
1303
+ super().__init__()
1304
+ config: IsaacConfig = vllm_config.model_config.hf_config
1305
+ quant_config = vllm_config.quant_config
1306
+ self.config = config
1307
+ self.multimodal_config = vllm_config.model_config.multimodal_config
1308
+
1309
+ head_dim = config.head_dim
1310
+ calculated_mrope_section = [
1311
+ head_dim // 4, # 2x more for temporal dim
1312
+ head_dim // 8,
1313
+ head_dim // 8,
1314
+ ]
1315
+
1316
+ self.vision_token_id = _resolve_vision_token_id(
1317
+ vllm_config.model_config, config.vision_token
1318
+ )
1319
+ config.image_token_id = self.vision_token_id
1320
+
1321
+ config.rope_scaling["mrope_section"] = calculated_mrope_section
1322
+ self.language_model = init_vllm_registered_model(
1323
+ vllm_config=vllm_config,
1324
+ architectures=["Qwen3ForCausalLM"],
1325
+ prefix=maybe_prefix(prefix, "language_model"),
1326
+ )
1327
+ self.make_empty_intermediate_tensors = (
1328
+ self.language_model.make_empty_intermediate_tensors
1329
+ )
1330
+
1331
+ vision_cfg = config.vision_config
1332
+ if vision_cfg is None:
1333
+ raise ValueError("IsaacConfig should always have vision_config")
1334
+ attn_impl = (
1335
+ config.vision_attn_implementation
1336
+ if config.vision_attn_implementation is not None
1337
+ else getattr(config, "_attn_implementation", None)
1338
+ )
1339
+ if attn_impl is not None:
1340
+ vision_cfg._attn_implementation = attn_impl
1341
+
1342
+ hidden_dim = vision_cfg.hidden_size * (vision_cfg.pixel_shuffle_scale_factor**2)
1343
+ self.vision_embedding = IsaacVisionEmbedding(
1344
+ vision_cfg=vision_cfg,
1345
+ hidden_dim=hidden_dim,
1346
+ output_dim=config.hidden_size,
1347
+ quant_config=quant_config,
1348
+ multimodal_config=self.multimodal_config,
1349
+ prefix=maybe_prefix(prefix, "vision_embedding"),
1350
+ )
1351
+
1352
+ def iter_mm_grid_hw(
1353
+ self, input_tokens: list[int], mm_features: list[MultiModalFeatureSpec]
1354
+ ) -> Iterator[tuple[int, int, int]]:
1355
+ spatial_merge_size = self.config.vision_config.pixel_shuffle_scale_factor
1356
+ for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
1357
+ offset = mm_feature.mm_position.offset
1358
+ if mm_feature.modality == "image":
1359
+ t, h, w = mm_feature.data["image_grid_thw"].data.tolist()
1360
+ assert t == 1, f"Image must have 1 frame, got {t}"
1361
+ yield offset, h // spatial_merge_size, w // spatial_merge_size
1362
+ else:
1363
+ raise ValueError(f"Unsupported modality: {mm_feature.modality}")
1364
+
1365
+ def get_mrope_input_positions(
1366
+ self,
1367
+ input_tokens: list[int],
1368
+ mm_features: list[MultiModalFeatureSpec],
1369
+ ) -> tuple[torch.Tensor, int]:
1370
+ llm_pos_ids_list = []
1371
+ st = 0
1372
+ for offset, llm_grid_h, llm_grid_w in self.iter_mm_grid_hw(
1373
+ input_tokens, mm_features
1374
+ ):
1375
+ text_len = offset - st
1376
+ st_idx = llm_pos_ids_list[-1].max() + 1 if len(llm_pos_ids_list) > 0 else 0
1377
+ llm_pos_ids_list.append(
1378
+ np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
1379
+ )
1380
+
1381
+ grid_indices = np.indices((1, llm_grid_h, llm_grid_w)).reshape(3, -1)
1382
+ grid_indices[0, :] = grid_indices[0, :] + text_len + st_idx
1383
+ llm_pos_ids_list.append(grid_indices)
1384
+ st = offset + llm_grid_h * llm_grid_w
1385
+
1386
+ if st < len(input_tokens):
1387
+ st_idx = llm_pos_ids_list[-1][0, -1] + 1 if len(llm_pos_ids_list) > 0 else 0
1388
+ text_len = len(input_tokens) - st
1389
+ llm_pos_ids_list.append(
1390
+ np.broadcast_to(np.arange(text_len), (3, text_len)) + st_idx
1391
+ )
1392
+
1393
+ llm_positions = np.concatenate(llm_pos_ids_list, axis=1).reshape(3, -1)
1394
+ mrope_position_delta = (llm_positions.max() + 1 - len(input_tokens)).item()
1395
+
1396
+ return torch.from_numpy(llm_positions), mrope_position_delta
1397
+
1398
+ def _parse_and_validate_image_input(
1399
+ self, **kwargs: object
1400
+ ) -> IsaacImagePixelInputs | None:
1401
+ pixel_values = kwargs.get("pixel_values")
1402
+ image_grid_thw = kwargs.get("image_grid_thw")
1403
+ if pixel_values is None or image_grid_thw is None:
1404
+ return None
1405
+
1406
+ # TensorSchema will automatically validate shapes on initialization
1407
+ return IsaacImagePixelInputs(
1408
+ pixel_values=pixel_values,
1409
+ image_grid_thw=image_grid_thw,
1410
+ )
1411
+
1412
+ def _process_image_input(
1413
+ self,
1414
+ image_input: IsaacImagePixelInputs,
1415
+ ) -> tuple[torch.Tensor, ...]:
1416
+ pixel_values = image_input["pixel_values"]
1417
+ image_grid_thw = image_input["image_grid_thw"]
1418
+ if pixel_values.numel() == 0:
1419
+ return ()
1420
+
1421
+ device = next(self.language_model.parameters()).device
1422
+ dtype = self.vision_embedding.linear_fc1.weight.dtype
1423
+ pixel_values = pixel_values.to(device=device, dtype=dtype)
1424
+ spatial_grids = image_grid_thw[:, 1:3].to(device, dtype=torch.int32)
1425
+
1426
+ vision_embeddings = self.vision_embedding((pixel_values, spatial_grids))
1427
+ merge_size = self.config.vision_config.pixel_shuffle_scale_factor
1428
+ sizes = spatial_grids.prod(-1) // (merge_size * merge_size)
1429
+ return tuple(vision_embeddings.split(sizes.tolist()))
1430
+
1431
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
1432
+ image_input = self._parse_and_validate_image_input(**kwargs)
1433
+ if image_input is None:
1434
+ return ()
1435
+ return self._process_image_input(image_input)
1436
+
1437
+ def get_multimodal_embeddings(
1438
+ self, **kwargs: object
1439
+ ) -> MultiModalEmbeddings | None:
1440
+ # Backward compatibility for older runners.
1441
+ embeddings = self.embed_multimodal(**kwargs)
1442
+ if not embeddings:
1443
+ return []
1444
+ return embeddings
1445
+
1446
+ def get_language_model(self) -> torch.nn.Module:
1447
+ return self.language_model
1448
+
1449
+ def forward(
1450
+ self,
1451
+ input_ids: torch.Tensor,
1452
+ positions: torch.Tensor,
1453
+ intermediate_tensors: IntermediateTensors | None = None,
1454
+ inputs_embeds: torch.Tensor | None = None,
1455
+ **kwargs: object,
1456
+ ) -> torch.Tensor | IntermediateTensors:
1457
+ return self.language_model(
1458
+ input_ids=input_ids,
1459
+ positions=positions,
1460
+ intermediate_tensors=intermediate_tensors,
1461
+ inputs_embeds=inputs_embeds,
1462
+ **kwargs,
1463
+ )
1464
+
1465
+ def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
1466
+ return self.language_model.compute_logits(hidden_states)
1467
+
1468
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1469
+ loader = AutoWeightsLoader(self)
1470
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1471
+
1472
+ def get_mm_mapping(self) -> MultiModelKeys:
1473
+ """
1474
+ Get the module prefix in multimodal models
1475
+ """
1476
+ return MultiModelKeys.from_string_field(
1477
+ language_model="language_model",
1478
+ connector="vision_embedding.linear_fc2", # The final linear layer
1479
+ tower_model="vision_embedding",
1480
+ )
vllm/model_executor/models/registry.py
+1
@@ -333,6 +333,7 @@ _MULTIMODAL_MODELS = {
333
"idefics3",
334
"Idefics3ForConditionalGeneration",
335
),
336
+ "IsaacForConditionalGeneration": ("isaac", "IsaacForConditionalGeneration"),
337
"SmolVLMForConditionalGeneration": ("smolvlm", "SmolVLMForConditionalGeneration"), # noqa: E501
338
"KeyeForConditionalGeneration": ("keye", "KeyeForConditionalGeneration"),
339
"KeyeVL1_5ForConditionalGeneration": (
vllm/transformers_utils/config.py
+1
@@ -81,6 +81,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
81
deepseek_v32="DeepseekV3Config",
82
flex_olmo="FlexOlmoConfig",
83
hunyuan_vl="HunYuanVLConfig",
84
+ isaac="IsaacConfig",
85
kimi_linear="KimiLinearConfig",
86
kimi_vl="KimiVLConfig",
87
RefinedWeb="RWConfig", # For tiiuae/falcon-40b(-instruct)
vllm/transformers_utils/configs/__init__.py
+4
@@ -25,6 +25,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
25
"HunYuanVLConfig": "vllm.transformers_utils.configs.hunyuan_vl",
26
"HunYuanVLTextConfig": "vllm.transformers_utils.configs.hunyuan_vl",
27
"HunYuanVLVisionConfig": "vllm.transformers_utils.configs.hunyuan_vl",
28
+ "IsaacConfig": "vllm.transformers_utils.configs.isaac",
29
# RWConfig is for the original tiiuae/falcon-40b(-instruct) and
30
# tiiuae/falcon-7b(-instruct) models. Newer Falcon models will use the
31
# `FalconConfig` class from the official HuggingFace transformers library.
@@ -41,6 +42,7 @@ _CLASS_TO_MODULE: dict[str, str] = {
42
"NemotronHConfig": "vllm.transformers_utils.configs.nemotron_h",
43
"Olmo3Config": "vllm.transformers_utils.configs.olmo3",
44
"OvisConfig": "vllm.transformers_utils.configs.ovis",
45
+ "PixelShuffleSiglip2VisionConfig": "vllm.transformers_utils.configs.isaac",
46
"RadioConfig": "vllm.transformers_utils.configs.radio",
47
"SpeculatorsConfig": "vllm.transformers_utils.configs.speculators.base",
48
"UltravoxConfig": "vllm.transformers_utils.configs.ultravox",
@@ -65,6 +67,7 @@ __all__ = [
67
"HunYuanVLConfig",
68
"HunYuanVLTextConfig",
69
"HunYuanVLVisionConfig",
70
+ "IsaacConfig",
71
"RWConfig",
72
"JAISConfig",
73
"Lfm2MoeConfig",
@@ -78,6 +81,7 @@ __all__ = [
81
"NemotronHConfig",
82
"Olmo3Config",
83
"OvisConfig",
84
+ "PixelShuffleSiglip2VisionConfig",
85
"RadioConfig",
86
"SpeculatorsConfig",
87
"UltravoxConfig",
vllm/transformers_utils/configs/isaac.py
new
+86
@@ -0,0 +1,86 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from __future__ import annotations
4
+
5
+from transformers import Qwen3Config
6
+from transformers.models.siglip2.configuration_siglip2 import Siglip2VisionConfig
7
+
8
+
9
+class PixelShuffleSiglip2VisionConfig(Siglip2VisionConfig):
10
+ """Vision configuration for Isaac with Pixel Shuffle support.
11
+
12
+ Extends Siglip2VisionConfig with additional fields for pixel shuffle.
13
+ """
14
+
15
+ model_type = "pixel_shuffle_siglip2"
16
+ base_config_key = "vision_config"
17
+
18
+ def __init__(
19
+ self,
20
+ pixel_shuffle_scale_factor: int = 1,
21
+ num_patches: int = 256,
22
+ **kwargs,
23
+ ):
24
+ super().__init__(**kwargs)
25
+
26
+ # Add our custom fields
27
+ self.pixel_shuffle_scale_factor = pixel_shuffle_scale_factor
28
+ self.num_patches = num_patches
29
+
30
+
31
+class IsaacConfig(Qwen3Config):
32
+ """Configuration class for Isaac multimodal model."""
33
+
34
+ model_type = "isaac"
35
+ sub_configs = {"vision_config": PixelShuffleSiglip2VisionConfig}
36
+
37
+ def __init__(
38
+ self,
39
+ vision_config=None,
40
+ vision_patch_size: int = 16,
41
+ vision_max_num_patches: int = 256,
42
+ vision_min_num_patches: int | None = None,
43
+ pixel_shuffle_scale: int = 1,
44
+ max_sequence_length: int = 16384,
45
+ vision_token: str = "<image>",
46
+ vision_attn_implementation: str | None = None,
47
+ **kwargs,
48
+ ):
49
+ super().__init__(**kwargs)
50
+
51
+ # EventStreamProcessor parameters (for backward compatibility)
52
+ self.video_patch_size = vision_patch_size
53
+ self.vision_max_num_patches = vision_max_num_patches
54
+ self.vision_min_num_patches = vision_min_num_patches
55
+ self.pixel_shuffle_scale = pixel_shuffle_scale
56
+
57
+ # Processing parameters
58
+ self.max_sequence_length = max_sequence_length
59
+ self.vision_token = vision_token
60
+
61
+ # Handle vision config - PixelShuffleSiglip2VisionConfig instance
62
+ if isinstance(vision_config, dict):
63
+ self.vision_config = PixelShuffleSiglip2VisionConfig(**vision_config)
64
+ elif vision_config is None:
65
+ self.vision_config = PixelShuffleSiglip2VisionConfig()
66
+ else:
67
+ self.vision_config = vision_config
68
+
69
+ # Ensure compatibility with pretrained checkpoints
70
+ self.vision_config.pixel_shuffle_scale_factor = getattr(
71
+ self.vision_config,
72
+ "pixel_shuffle_scale_factor",
73
+ pixel_shuffle_scale,
74
+ )
75
+ self.vision_config.num_patches = getattr(
76
+ self.vision_config,
77
+ "num_patches",
78
+ vision_max_num_patches,
79
+ )
80
+ self.vision_attn_implementation = vision_attn_implementation
81
+
82
+
83
+__all__ = [
84
+ "IsaacConfig",
85
+ "PixelShuffleSiglip2VisionConfig",
86
+]