main
py 414 lines 14.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Adapted from NVIDIA Eagle2.5-VL model
4 # https://huggingface.co/nvidia/Eagle2.5-8B
5
6 from collections.abc import Iterable
7 from typing import Annotated, Literal, TypeAlias
8
9 import torch
10 import torch.nn as nn
11 from transformers import PreTrainedConfig
12
13 from vllm.config import VllmConfig
14 from vllm.model_executor.layers.quantization import QuantizationConfig
15 from vllm.model_executor.models.module_mapping import MultiModelKeys
16 from vllm.model_executor.models.siglip import SiglipVisionModel
17 from vllm.multimodal import MULTIMODAL_REGISTRY
18 from vllm.sequence import IntermediateTensors
19 from vllm.transformers_utils.processors.internvl import (
20 InternVLImageProcessor,
21 InternVLProcessor,
22 )
23 from vllm.utils.tensor_schema import TensorSchema, TensorShape
24
25 from .interfaces import (
26 MultiModalEmbeddings,
27 SupportsLoRA,
28 SupportsMultiModal,
29 SupportsPP,
30 )
31 from .internvl import (
32 BaseInternVLDummyInputsBuilder,
33 BaseInternVLMultiModalProcessor,
34 BaseInternVLProcessingInfo,
35 )
36 from .utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
37
38
39 class Eagle2_5_VLImagePixelInputs(TensorSchema):
40 """Dimensions:
41 - bn: Batch size * number of images
42 - bnp: Batch size * number of images * (1 + num_patches)
43 - c: Number of channels (3)
44 - h: Height of each image patch
45 - w: Width of each image patch
46 """
47
48 type: Literal["pixel_values"]
49 pixel_values_flat: Annotated[torch.Tensor, TensorShape("bnp", 3, "h", "w")]
50 num_patches: Annotated[torch.Tensor, TensorShape("bn")]
51
52
53 class Eagle2_5_VLImageEmbeddingInputs(TensorSchema):
54 """Dimensions:
55 - n: Number of images
56 - f: Total image feature size
57 - h: Hidden size (must match the hidden size of language model backbone)
58 """
59
60 type: Literal["image_embeds"]
61 data: Annotated[torch.Tensor | list[torch.Tensor], TensorShape("n", "f", "h")]
62
63
64 Eagle2_5_VLImageInputs: TypeAlias = (
65 Eagle2_5_VLImagePixelInputs | Eagle2_5_VLImageEmbeddingInputs
66 )
67
68
69 class Eagle2_5_VLProcessingInfo(BaseInternVLProcessingInfo):
70 """Processing info for Eagle2.5-VL model."""
71
72 def get_image_processor(self, **kwargs):
73 config = self.get_hf_config()
74 vision_config = config.vision_config
75
76 kwargs = self.ctx.get_merged_mm_kwargs(kwargs)
77 kwargs.setdefault(
78 "image_size", config.force_image_size or vision_config.image_size
79 )
80 kwargs.setdefault("min_dynamic_patch", config.min_dynamic_patch)
81 kwargs.setdefault("max_dynamic_patch", config.max_dynamic_patch)
82 kwargs.setdefault("dynamic_image_size", config.dynamic_image_size)
83 kwargs.setdefault("use_thumbnail", config.use_thumbnail)
84
85 return InternVLImageProcessor(**kwargs)
86
87 def get_hf_processor(self, **kwargs) -> InternVLProcessor:
88 config = self.get_hf_config()
89 vision_config = config.vision_config
90
91 image_processor = self.get_image_processor(**kwargs)
92 image_size = image_processor.image_size
93 patch_size = vision_config.patch_size
94 downsample_ratio = config.downsample_ratio
95 image_seq_length = int((image_size // patch_size) ** 2 * (downsample_ratio**2))
96
97 return InternVLProcessor(
98 tokenizer=self.get_tokenizer(),
99 image_processor=image_processor,
100 image_seq_length=image_seq_length,
101 )
102
103
104 class Eagle2_5_VLDummyInputsBuilder(
105 BaseInternVLDummyInputsBuilder[Eagle2_5_VLProcessingInfo]
106 ):
107 """Dummy inputs builder for Eagle2.5-VL model."""
108
109 pass
110
111
112 class Eagle2_5_VLMultiModalProcessor(
113 BaseInternVLMultiModalProcessor[Eagle2_5_VLProcessingInfo]
114 ):
115 """Multi-modal processor for Eagle2.5-VL model."""
116
117 pass
118
119
120 @MULTIMODAL_REGISTRY.register_processor(
121 Eagle2_5_VLMultiModalProcessor,
122 info=Eagle2_5_VLProcessingInfo,
123 dummy_inputs=Eagle2_5_VLDummyInputsBuilder,
124 )
125 class Eagle2_5_VLForConditionalGeneration(
126 nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA
127 ):
128 """Eagle2.5-VL model for conditional generation.
129
130 Architecture:
131 - Vision Encoder: SigLIP
132 - Language Model: Qwen2
133 - Projection: MLP with pixel shuffle downsampling
134 """
135
136 supports_encoder_tp_data = True
137
138 @classmethod
139 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
140 if modality.startswith("image"):
141 return "<image>"
142 raise ValueError("Only image modality is supported")
143
144 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
145 super().__init__()
146
147 config = vllm_config.model_config.hf_config
148 quant_config = vllm_config.quant_config
149 multimodal_config = vllm_config.model_config.multimodal_config
150 assert multimodal_config is not None
151
152 self.config = config
153 self.multimodal_config = multimodal_config
154 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
155
156 # Image configuration
157 image_size = (
158 getattr(config, "force_image_size", None) or config.vision_config.image_size
159 )
160 patch_size = config.vision_config.patch_size
161 self.patch_size = patch_size
162 self.downsample_ratio = getattr(config, "downsample_ratio", 0.5)
163 self.num_image_token = int(
164 (image_size // patch_size) ** 2 * (self.downsample_ratio**2)
165 )
166
167 self.select_layer = getattr(config, "select_layer", -1)
168
169 with self._mark_tower_model(vllm_config, "image"):
170 # Vision encoder (SigLIP)
171 self.vision_model = self._init_vision_model(
172 config,
173 quant_config=quant_config,
174 prefix=maybe_prefix(prefix, "vision_model"),
175 )
176
177 # MLP projection
178 self.mlp1 = self._init_mlp1(config)
179
180 with self._mark_language_model(vllm_config):
181 # Language model (Qwen2)
182 self.language_model = init_vllm_registered_model(
183 vllm_config=vllm_config,
184 hf_config=config.text_config,
185 prefix=maybe_prefix(prefix, "language_model"),
186 )
187
188 self.img_context_token_id: int | None = None
189
190 self.make_empty_intermediate_tensors = (
191 self.language_model.make_empty_intermediate_tensors
192 )
193
194 def _init_vision_model(
195 self,
196 config: PreTrainedConfig,
197 quant_config: QuantizationConfig | None,
198 prefix: str,
199 ):
200 """Initialize SigLIP vision model."""
201 vision_config = config.vision_config
202
203 # Determine number of hidden layers based on select_layer
204 vision_feature_layer = self.select_layer
205 if vision_feature_layer < 0:
206 num_hidden_layers = (
207 vision_config.num_hidden_layers + vision_feature_layer + 1
208 )
209 else:
210 num_hidden_layers = vision_feature_layer + 1
211
212 # Disable the pooling head - Eagle2.5 needs all patch tokens,
213 # not a single pooled output
214 vision_config.vision_use_head = False
215
216 return SiglipVisionModel(
217 vision_config,
218 quant_config=quant_config,
219 num_hidden_layers_override=num_hidden_layers,
220 prefix=prefix,
221 )
222
223 def _init_mlp1(self, config: PreTrainedConfig) -> nn.Module:
224 """Initialize MLP projection layer."""
225 vit_hidden_size = config.vision_config.hidden_size
226 llm_hidden_size = config.text_config.hidden_size
227
228 return nn.Sequential(
229 nn.LayerNorm(vit_hidden_size * int(1 / self.downsample_ratio) ** 2),
230 nn.Linear(
231 vit_hidden_size * int(1 / self.downsample_ratio) ** 2, llm_hidden_size
232 ),
233 nn.GELU(),
234 nn.Linear(llm_hidden_size, llm_hidden_size),
235 )
236
237 def pixel_shuffle(self, x: torch.Tensor, scale_factor: float = 0.5) -> torch.Tensor:
238 """Pixel shuffle operation for downsampling vision features.
239
240 Args:
241 x: Input tensor of shape (n, w, h, c)
242 scale_factor: Downsampling factor
243
244 Returns:
245 Downsampled tensor
246
247 """
248 n, w, h, c = x.size()
249 # N, W, H, C --> N, W, H * scale, C // scale
250 x = x.view(n, w, int(h * scale_factor), int(c / scale_factor))
251 # N, W, H * scale, C // scale --> N, H * scale, W, C // scale
252 x = x.permute(0, 2, 1, 3).contiguous()
253 # N, H * scale, W, C // scale --> N, H * scale, W * scale, C // (scale ** 2)
254 x = x.view(
255 n,
256 int(h * scale_factor),
257 int(w * scale_factor),
258 int(c / (scale_factor * scale_factor)),
259 )
260 x = x.permute(0, 2, 1, 3).contiguous()
261 return x
262
263 def extract_feature(self, pixel_values: torch.Tensor) -> torch.Tensor:
264 """Extract visual features from pixel values.
265
266 Args:
267 pixel_values: Input pixel values of shape (batch, channels, height, width)
268
269 Returns:
270 Visual embeddings
271
272 """
273 vit_embeds = self.vision_model(pixel_values=pixel_values)
274
275 h = w = int(vit_embeds.shape[1] ** 0.5)
276 vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], h, w, -1)
277 vit_embeds = self.pixel_shuffle(vit_embeds, scale_factor=self.downsample_ratio)
278 vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], -1, vit_embeds.shape[-1])
279 vit_embeds = self.mlp1(vit_embeds)
280 return vit_embeds
281
282 def _parse_and_validate_image_input(
283 self, **kwargs: object
284 ) -> Eagle2_5_VLImageInputs | None:
285 """Parse and validate image inputs."""
286 pixel_values_flat = kwargs.pop("pixel_values_flat", None)
287 image_num_patches = kwargs.pop("image_num_patches", None)
288 image_embeds = kwargs.pop("image_embeds", None)
289
290 if pixel_values_flat is None and image_embeds is None:
291 return None
292
293 if image_embeds is not None:
294 return Eagle2_5_VLImageEmbeddingInputs(
295 type="image_embeds",
296 data=image_embeds,
297 )
298
299 image_token_id = kwargs.get("image_token_id")
300 if image_token_id is not None:
301 if isinstance(image_token_id, torch.Tensor):
302 image_token_id = image_token_id.flatten().unique().item()
303 assert isinstance(image_token_id, int)
304 self.img_context_token_id = image_token_id
305
306 if pixel_values_flat is not None:
307 image_size = getattr(self.config, "force_image_size", None)
308 if image_size is None:
309 image_size = self.config.vision_config.image_size
310 expected_h = expected_w = image_size
311 resolve_bindings = {"h": expected_h, "w": expected_w}
312
313 return Eagle2_5_VLImagePixelInputs(
314 type="pixel_values",
315 pixel_values_flat=pixel_values_flat,
316 num_patches=image_num_patches,
317 resolve_bindings=resolve_bindings,
318 )
319
320 raise AssertionError("This line should be unreachable.")
321
322 def _process_image_input(
323 self,
324 image_input: Eagle2_5_VLImageInputs,
325 ) -> tuple[torch.Tensor, ...]:
326 """Process image input to get embeddings."""
327 if image_input["type"] == "image_embeds":
328 return image_input["data"]
329
330 assert self.vision_model is not None
331
332 image_embeds = self.extract_feature(image_input["pixel_values_flat"])
333
334 num_patches = image_input["num_patches"]
335
336 # Only one image in the current batch
337 if len(num_patches) == 1:
338 return (image_embeds.view(-1, self.config.text_config.hidden_size),)
339
340 # Split embeddings by image
341 feature_size = image_embeds.shape[1]
342 image_embeds = image_embeds.view(-1, self.config.text_config.hidden_size)
343 image_feature_sizes = [
344 num_patches * feature_size for num_patches in num_patches
345 ]
346 return image_embeds.split(image_feature_sizes)
347
348 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
349 """Embed multimodal inputs."""
350 image_input = self._parse_and_validate_image_input(**kwargs)
351 if image_input is None:
352 return []
353
354 image_embeddings = self._process_image_input(image_input)
355 return tuple(image_embeddings)
356
357 def embed_input_ids(
358 self,
359 input_ids: torch.Tensor,
360 multimodal_embeddings: MultiModalEmbeddings | None = None,
361 *,
362 is_multimodal: torch.Tensor | None = None,
363 ) -> torch.Tensor:
364 """Embed input IDs with optional multimodal embeddings."""
365 if multimodal_embeddings is None or is_multimodal is None:
366 return super().embed_input_ids(input_ids)
367
368 return super().embed_input_ids(
369 input_ids,
370 multimodal_embeddings=multimodal_embeddings,
371 is_multimodal=is_multimodal,
372 )
373
374 def forward(
375 self,
376 input_ids: torch.Tensor | None,
377 positions: torch.Tensor,
378 intermediate_tensors: IntermediateTensors | None = None,
379 inputs_embeds: torch.Tensor | None = None,
380 **kwargs: object,
381 ) -> IntermediateTensors:
382 """Forward pass through the model."""
383 if intermediate_tensors is not None:
384 inputs_embeds = None
385
386 forward_kwargs = {
387 "input_ids": input_ids,
388 "positions": positions,
389 "intermediate_tensors": intermediate_tensors,
390 "inputs_embeds": inputs_embeds,
391 }
392
393 hidden_states = self.language_model.model(**forward_kwargs)
394 return hidden_states
395
396 def compute_logits(
397 self,
398 hidden_states: torch.Tensor,
399 ) -> torch.Tensor | None:
400 """Compute logits from hidden states."""
401 return self.language_model.compute_logits(hidden_states)
402
403 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
404 """Load model weights."""
405 loader = AutoWeightsLoader(self)
406 return loader.load_weights(weights)
407
408 def get_mm_mapping(self) -> MultiModelKeys:
409 """Get the module prefix mapping for multimodal models."""
410 return MultiModelKeys.from_string_field(
411 language_model="language_model",
412 connector="mlp1",
413 tower_model="vision_model",
414 )