main
py 568 lines 19.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Copyright 2025 Bytedance Ltd. and/or its affiliates.
4 """Inference-only BAGEL model compatible with HuggingFace weights.
5
6 BAGEL is a unified multimodal model for image understanding and generation.
7 For vLLM, we focus on the image understanding (vision-to-text) capabilities.
8 """
9
10 from collections.abc import Iterable, Mapping, Sequence
11 from typing import Any, Literal, TypeAlias
12
13 import torch
14 import torch.nn as nn
15
16 from vllm.config import VllmConfig
17 from vllm.config.multimodal import MultiModalDummyOptions
18 from vllm.inputs import MultiModalDataDict
19 from vllm.logger import init_logger
20 from vllm.model_executor.layers.activation import get_act_fn
21 from vllm.model_executor.layers.linear import (
22 ColumnParallelLinear,
23 RowParallelLinear,
24 )
25 from vllm.model_executor.layers.quantization import QuantizationConfig
26 from vllm.multimodal import MULTIMODAL_REGISTRY
27 from vllm.multimodal.inputs import (
28 MultiModalFieldConfig,
29 MultiModalKwargsItems,
30 )
31 from vllm.multimodal.parse import MultiModalDataItems
32 from vllm.multimodal.processing import (
33 BaseDummyInputsBuilder,
34 BaseMultiModalProcessor,
35 BaseProcessingInfo,
36 PromptReplacement,
37 )
38 from vllm.sequence import IntermediateTensors
39 from vllm.transformers_utils.processors.bagel import BagelProcessor
40 from vllm.utils.tensor_schema import TensorSchema
41
42 from .interfaces import (
43 MultiModalEmbeddings,
44 SupportsLoRA,
45 SupportsMultiModal,
46 SupportsPP,
47 )
48 from .siglip import SiglipVisionModel
49 from .utils import (
50 AutoWeightsLoader,
51 StageMissingLayer,
52 WeightsMapper,
53 init_vllm_registered_model,
54 maybe_prefix,
55 )
56
57 logger = init_logger(__name__)
58
59
60 class BagelImagePixelInputs(TensorSchema):
61 """Dimensions:
62 - bn: Batch size * number of images
63 - c: Number of channels (3)
64 - h: Height of each image
65 - w: Width of each image
66 """
67
68 type: Literal["pixel_values"]
69 pixel_values: torch.Tensor # Shape: (bn, 3, h, w)
70
71
72 BagelImageInputs: TypeAlias = BagelImagePixelInputs
73
74
75 class BagelVisionMLP(nn.Module):
76 """MLP connector for vision features."""
77
78 def __init__(
79 self,
80 in_features: int,
81 hidden_features: int,
82 out_features: int,
83 act_layer: str = "gelu_pytorch_tanh",
84 quant_config: QuantizationConfig | None = None,
85 prefix: str = "",
86 ):
87 super().__init__()
88 self.fc1 = ColumnParallelLinear(
89 in_features,
90 hidden_features,
91 bias=True,
92 quant_config=quant_config,
93 prefix=f"{prefix}.fc1",
94 )
95 self.act = get_act_fn(act_layer)
96 self.fc2 = RowParallelLinear(
97 hidden_features,
98 out_features,
99 bias=True,
100 quant_config=quant_config,
101 prefix=f"{prefix}.fc2",
102 )
103
104 def forward(self, x: torch.Tensor) -> torch.Tensor:
105 x, _ = self.fc1(x)
106 x = self.act(x)
107 x, _ = self.fc2(x)
108 return x
109
110
111 class PositionEmbedding(nn.Module):
112 """2D position embedding for vision tokens using sin-cos embeddings."""
113
114 def __init__(self, max_num_patch_per_side: int, hidden_size: int):
115 super().__init__()
116 self.max_num_patch_per_side = max_num_patch_per_side
117 self.hidden_size = hidden_size
118
119 # Create learnable 2D position embeddings (frozen sin-cos)
120 pos_embed = self._get_2d_sincos_pos_embed(hidden_size, max_num_patch_per_side)
121 self.register_buffer(
122 "pos_embed",
123 torch.from_numpy(pos_embed).float(),
124 persistent=False,
125 )
126
127 @staticmethod
128 def _get_2d_sincos_pos_embed(embed_dim: int, grid_size: int):
129 """Generate 2D sin-cos position embeddings."""
130 import numpy as np
131
132 grid_h = np.arange(grid_size, dtype=np.float32)
133 grid_w = np.arange(grid_size, dtype=np.float32)
134 grid = np.meshgrid(grid_w, grid_h) # w goes first
135 grid = np.stack(grid, axis=0)
136 grid = grid.reshape([2, 1, grid_size, grid_size])
137 pos_embed = PositionEmbedding._get_2d_sincos_pos_embed_from_grid(
138 embed_dim, grid
139 )
140 return pos_embed
141
142 @staticmethod
143 def _get_2d_sincos_pos_embed_from_grid(embed_dim: int, grid):
144 """Generate 2D sin-cos position embeddings from grid."""
145 import numpy as np
146
147 assert embed_dim % 2 == 0
148 # use half of dimensions to encode grid_h
149 emb_h = PositionEmbedding._get_1d_sincos_pos_embed_from_grid(
150 embed_dim // 2, grid[0]
151 )
152 emb_w = PositionEmbedding._get_1d_sincos_pos_embed_from_grid(
153 embed_dim // 2, grid[1]
154 )
155 emb = np.concatenate([emb_h, emb_w], axis=1)
156 return emb
157
158 @staticmethod
159 def _get_1d_sincos_pos_embed_from_grid(embed_dim: int, pos):
160 """Generate 1D sin-cos position embeddings."""
161 import numpy as np
162
163 assert embed_dim % 2 == 0
164 omega = np.arange(embed_dim // 2, dtype=np.float64)
165 omega /= embed_dim / 2.0
166 omega = 1.0 / 10000**omega
167
168 pos = pos.reshape(-1)
169 out = np.einsum("m,d->md", pos, omega)
170
171 emb_sin = np.sin(out)
172 emb_cos = np.cos(out)
173 emb = np.concatenate([emb_sin, emb_cos], axis=1)
174 return emb
175
176 def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
177 """Args:
178 position_ids: Flattened position IDs, shape (N,) where each ID
179 corresponds to a position in the flattened grid
180 Returns:
181 Position embeddings of shape (N, hidden_size)
182
183 """
184 # Ensure position_ids are on the same device as pos_embed
185 position_ids = position_ids.to(self.pos_embed.device)
186 return self.pos_embed[position_ids]
187
188
189 class BagelProcessingInfo(BaseProcessingInfo):
190 """Processing information for BAGEL model."""
191
192 def get_hf_processor(self, **kwargs: object) -> BagelProcessor:
193 from vllm.transformers_utils.processor import cached_get_image_processor
194
195 image_processor = cached_get_image_processor(
196 self.ctx.model_config.model,
197 revision=self.ctx.model_config.revision,
198 trust_remote_code=self.ctx.model_config.trust_remote_code,
199 )
200
201 tokenizer = self.get_tokenizer()
202
203 return BagelProcessor(
204 image_processor=image_processor,
205 tokenizer=tokenizer,
206 **kwargs,
207 )
208
209 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
210 return {"image": None}
211
212 def get_mm_max_tokens_per_item(
213 self,
214 seq_len: int,
215 mm_counts: Mapping[str, int],
216 ) -> Mapping[str, int]:
217 hf_config = self.get_hf_config()
218 # Calculate max tokens per image
219 # For BAGEL: (vit_max_num_patch_per_side) ** 2
220 max_num_patches = hf_config.vit_max_num_patch_per_side**2
221 return {"image": max_num_patches}
222
223 def get_num_image_tokens(
224 self,
225 *,
226 image_width: int,
227 image_height: int,
228 ) -> int:
229 hf_config = self.get_hf_config()
230 vit_config = hf_config.vit_config
231 patch_size = vit_config.patch_size
232
233 # Calculate number of patches
234 num_patches_h = image_height // patch_size
235 num_patches_w = image_width // patch_size
236 return num_patches_h * num_patches_w
237
238
239 class BagelDummyInputsBuilder(BaseDummyInputsBuilder[BagelProcessingInfo]):
240 """Build dummy inputs for BAGEL model profiling."""
241
242 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
243 num_images = mm_counts.get("image", 0)
244 # Use a simple placeholder for each image
245 return "<|image_pad|>" * num_images
246
247 def get_dummy_mm_data(
248 self,
249 seq_len: int,
250 mm_counts: Mapping[str, int],
251 mm_options: MultiModalDummyOptions,
252 ) -> MultiModalDataDict:
253 hf_config = self.info.get_hf_config()
254 vit_config = hf_config.vit_config
255
256 # Use the configured image size
257 image_size = vit_config.image_size
258
259 return {
260 "image": self._get_dummy_images(
261 width=image_size,
262 height=image_size,
263 num_images=mm_counts.get("image", 0),
264 overrides=mm_options.get("image"),
265 ),
266 }
267
268
269 class BagelMultiModalProcessor(BaseMultiModalProcessor[BagelProcessingInfo]):
270 """Multimodal processor for BAGEL model."""
271
272 def _get_prompt_updates(
273 self,
274 mm_items: MultiModalDataItems,
275 hf_processor_mm_kwargs: Mapping[str, Any],
276 out_mm_kwargs: MultiModalKwargsItems,
277 ) -> Sequence[PromptReplacement]:
278 """Replace image placeholders with the correct number of tokens."""
279 hf_config = self.info.get_hf_config()
280
281 # Get the tokenizer to look up the image token ID
282 tokenizer = self.info.get_tokenizer()
283 image_token_id = tokenizer.get_vocab().get("<|image_pad|>")
284 if image_token_id is None:
285 raise ValueError(
286 "Image token '<|image_pad|>' not found in tokenizer vocabulary"
287 )
288
289 def get_replacement_bagel(item_idx: int):
290 # For BAGEL, calculate number of tokens based on max patch size
291 num_tokens = hf_config.vit_max_num_patch_per_side**2
292 # Use the image token ID from tokenizer
293 return [image_token_id] * num_tokens
294
295 return [
296 PromptReplacement(
297 modality="image",
298 target=[image_token_id],
299 replacement=get_replacement_bagel,
300 )
301 ]
302
303 def _get_mm_fields_config(
304 self,
305 hf_inputs: Any,
306 hf_processor_mm_kwargs: Mapping[str, object],
307 ) -> Mapping[str, MultiModalFieldConfig]:
308 return {
309 "pixel_values": MultiModalFieldConfig.batched("image"),
310 }
311
312
313 @MULTIMODAL_REGISTRY.register_processor(
314 BagelMultiModalProcessor,
315 info=BagelProcessingInfo,
316 dummy_inputs=BagelDummyInputsBuilder,
317 )
318 class BagelForConditionalGeneration(
319 nn.Module, SupportsMultiModal, SupportsLoRA, SupportsPP
320 ):
321 """BAGEL: A unified multimodal model for image understanding and generation.
322
323 For vLLM, we focus on the image understanding (vision-to-text) capabilities.
324 The image generation part is not supported in vLLM.
325 """
326
327 # pos_embed is handled by the PositionEmbedding module
328 hf_to_vllm_mapper = WeightsMapper(
329 orig_to_new_prefix={"vit_pos_embed.pos_embed": None}
330 )
331
332 @classmethod
333 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
334 if modality.startswith("image"):
335 return "<|image_pad|>"
336
337 raise ValueError("Only image modality is supported")
338
339 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
340 super().__init__()
341
342 config = vllm_config.model_config.hf_config
343 quant_config = vllm_config.quant_config
344 multimodal_config = vllm_config.model_config.multimodal_config
345
346 # Ensure we have a BagelConfig (check by name to handle trust_remote_code)
347 # When trust_remote_code=True, the config comes from transformers_modules
348 if type(config).__name__ != "BagelConfig":
349 raise ValueError(
350 f"Expected BagelConfig, got {type(config).__name__}. "
351 "Make sure the model config is properly loaded."
352 )
353
354 self.config = config
355 self.multimodal_config = multimodal_config
356
357 # Initialize language model (Qwen2)
358 # Pass the llm_config from BagelConfig to initialize Qwen2 properly
359 with self._mark_language_model(vllm_config):
360 self.language_model = init_vllm_registered_model(
361 vllm_config=vllm_config,
362 hf_config=config.llm_config,
363 prefix=maybe_prefix(prefix, "language_model"),
364 architectures=["Qwen2ForCausalLM"],
365 )
366
367 # Initialize vision model (SigLIP) if visual understanding is enabled
368 if config.visual_und:
369 # Fix vit_config: checkpoint has 26 layers (0-25) but config says 27
370 # Also disable head as it's not in checkpoint
371 vit_config = config.vit_config
372 if vit_config.num_hidden_layers == 27:
373 logger.warning(
374 "Overriding vit_config.num_hidden_layers from 27 to 26 "
375 "to match the Bagel model checkpoint."
376 )
377 vit_config.num_hidden_layers = 26
378 if not hasattr(vit_config, "vision_use_head"):
379 logger.warning(
380 "Setting vit_config.vision_use_head to False as it is not "
381 "present in the Bagel model checkpoint."
382 )
383 vit_config.vision_use_head = False
384
385 with self._mark_tower_model(vllm_config, "image"):
386 self.vit_model = SiglipVisionModel(
387 config=vit_config,
388 quant_config=quant_config,
389 prefix=maybe_prefix(prefix, "vit_model"),
390 )
391
392 # Initialize connector (MLP)
393 vit_hidden_size = config.vit_config.hidden_size
394 llm_hidden_size = config.llm_config.hidden_size
395
396 self.connector = BagelVisionMLP(
397 in_features=vit_hidden_size,
398 hidden_features=llm_hidden_size,
399 out_features=llm_hidden_size,
400 act_layer=config.connector_act,
401 quant_config=quant_config,
402 prefix=maybe_prefix(prefix, "connector"),
403 )
404
405 # Position embedding for vision tokens
406 self.vit_pos_embed = PositionEmbedding(
407 max_num_patch_per_side=config.vit_max_num_patch_per_side,
408 hidden_size=llm_hidden_size,
409 )
410 else:
411 self.vit_model = StageMissingLayer("image_tower")
412 self.connector = StageMissingLayer("image_tower")
413 self.vit_pos_embed = StageMissingLayer("image_tower")
414
415 self.make_empty_intermediate_tensors = (
416 self.language_model.make_empty_intermediate_tensors
417 )
418
419 def _parse_and_validate_image_input(
420 self, **kwargs: object
421 ) -> BagelImageInputs | None:
422 pixel_values = kwargs.pop("pixel_values", None)
423
424 if pixel_values is None:
425 return None
426
427 return BagelImagePixelInputs(
428 type="pixel_values",
429 pixel_values=pixel_values,
430 )
431
432 def _process_image_input(
433 self, image_input: BagelImageInputs
434 ) -> tuple[torch.Tensor, ...]:
435 """Process image inputs through vision encoder and connector."""
436 pixel_values = image_input["pixel_values"]
437
438 # Handle potential extra batch dimension
439 # Expected shape: (batch_size * num_images, 3, H, W)
440 # But might receive: (batch_size, num_images, 3, H, W)
441 if pixel_values.ndim == 5:
442 # Flatten batch and num_images dimensions
443 batch_size, num_images, channels, height, width = pixel_values.shape
444 pixel_values = pixel_values.reshape(
445 batch_size * num_images, channels, height, width
446 )
447
448 # Get vision features from SigLIP
449 # pixel_values shape: (batch_size * num_images, 3, H, W)
450 vision_features = self.vit_model(pixel_values)
451
452 # Pass through connector
453 vision_embeds = self.connector(vision_features)
454
455 # Add position embeddings
456 batch_size, num_patches, hidden_size = vision_embeds.shape
457 patch_size = self.config.vit_config.patch_size
458 image_size = self.config.vit_config.image_size
459
460 # Calculate grid dimensions
461 num_patches_per_side = image_size // patch_size
462
463 # Create flattened position IDs (0 to num_patches-1)
464 # For BAGEL, we use extrapolate mode by default
465 h_coords = torch.arange(num_patches_per_side, device=vision_embeds.device)
466 w_coords = torch.arange(num_patches_per_side, device=vision_embeds.device)
467 position_ids = (
468 h_coords[:, None] * self.config.vit_max_num_patch_per_side + w_coords
469 ).flatten()
470 position_ids = position_ids.unsqueeze(0).expand(batch_size, -1).flatten()
471
472 # Add position embeddings
473 pos_embeds = self.vit_pos_embed(position_ids)
474 pos_embeds = pos_embeds.reshape(batch_size, num_patches, hidden_size)
475 # Ensure pos_embeds are on the same device as vision_embeds
476 pos_embeds = pos_embeds.to(vision_embeds.device)
477 vision_embeds = vision_embeds + pos_embeds
478
479 # Split by image
480 return tuple(vision_embeds)
481
482 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
483 """Get multimodal embeddings from input."""
484 image_input = self._parse_and_validate_image_input(**kwargs)
485 if image_input is None:
486 return []
487
488 return self._process_image_input(image_input)
489
490 def forward(
491 self,
492 input_ids: torch.Tensor | None,
493 positions: torch.Tensor,
494 intermediate_tensors: IntermediateTensors | None = None,
495 inputs_embeds: torch.Tensor | None = None,
496 **kwargs: object,
497 ) -> torch.Tensor | IntermediateTensors:
498 """Run forward pass for BAGEL.
499
500 Args:
501 input_ids: Flattened (concatenated) input_ids corresponding to a batch.
502 positions: Flattened (concatenated) position ids corresponding to a batch.
503 intermediate_tensors: Intermediate tensors from prior forward pass.
504 inputs_embeds: Optional tensor of input embeddings.
505 **kwargs: Multimodal inputs for this batch, forwarded to the
506 multimodal embedding path.
507
508 """
509 if intermediate_tensors is not None:
510 inputs_embeds = None
511
512 hidden_states = self.language_model.model(
513 input_ids=input_ids,
514 positions=positions,
515 intermediate_tensors=intermediate_tensors,
516 inputs_embeds=inputs_embeds,
517 )
518 return hidden_states
519
520 def compute_logits(
521 self,
522 hidden_states: torch.Tensor,
523 ) -> torch.Tensor | None:
524 return self.language_model.compute_logits(hidden_states)
525
526 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
527 """Load weights from checkpoint."""
528 # Skip generation-related weights since we only support text2text and image2text
529 # Filter out all image generation components:
530 # - 'moe_gen': MoE generation weights
531 # - 'latent_pos_embed': Latent position embeddings for VAE
532 # - 'llm2vae', 'vae2llm': LLM-VAE projections
533 # - 'time_embedder': Timestep embeddings for diffusion
534 # - VAE encoder/decoder: Use specific prefixes to avoid matching vision encoder
535 generation_keywords = [
536 "moe_gen",
537 "latent_pos_embed",
538 "llm2vae",
539 "vae2llm",
540 "time_embedder",
541 ]
542 vae_prefixes = [
543 "decoder.",
544 "encoder.",
545 ] # VAE encoder/decoder, not vision encoder
546 filtered_weights = []
547 for name, tensor in weights:
548 # Skip generation-related keywords
549 if any(skip in name for skip in generation_keywords):
550 continue
551 if any(name.startswith(prefix) for prefix in vae_prefixes):
552 continue
553
554 if "patch_embedding.weight" in name and tensor.ndim == 2:
555 out_channels = tensor.shape[0]
556 in_features = tensor.shape[1]
557 patch_size = self.config.vit_config.patch_size
558 in_channels = self.config.vit_config.num_channels
559 if in_features == in_channels * patch_size * patch_size:
560 tensor = tensor.reshape(
561 out_channels, patch_size, patch_size, in_channels
562 )
563 tensor = tensor.permute(0, 3, 1, 2).contiguous()
564
565 filtered_weights.append((name, tensor))
566
567 loader = AutoWeightsLoader(self)
568 return loader.load_weights(filtered_weights, mapper=self.hf_to_vllm_mapper)