main
py 581 lines 20.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 import math
6 from abc import ABC, abstractmethod
7 from collections.abc import Callable
8 from typing import Generic, Literal, Protocol, TypeAlias, TypeVar
9
10 import torch
11 from transformers import PreTrainedConfig
12
13 from vllm.config import MultiModalConfig, get_current_vllm_config_or_none
14 from vllm.distributed import (
15 get_tensor_model_parallel_rank,
16 get_tensor_model_parallel_world_size,
17 tensor_model_parallel_all_gather,
18 )
19 from vllm.logger import init_logger
20 from vllm.platforms import current_platform
21 from vllm.utils.math_utils import round_up
22 from vllm.v1.attention.backends.registry import AttentionBackendEnum
23
24 logger = init_logger(__name__)
25
26 _C = TypeVar("_C", bound=PreTrainedConfig)
27 _C_co = TypeVar("_C_co", bound=PreTrainedConfig, covariant=True)
28
29
30 class _RootConfig(Protocol[_C_co]):
31 @property
32 def vision_config(self) -> _C_co: ...
33
34
35 class VisionEncoderInfo(ABC, Generic[_C]):
36 def __init__(self, hf_config: _RootConfig[_C]) -> None:
37 super().__init__()
38
39 self.hf_config = hf_config
40 self.vision_config = hf_config.vision_config
41
42 @abstractmethod
43 def get_num_image_tokens(
44 self,
45 *,
46 image_width: int,
47 image_height: int,
48 ) -> int:
49 raise NotImplementedError
50
51 @abstractmethod
52 def get_image_size(self) -> int:
53 raise NotImplementedError
54
55 @abstractmethod
56 def get_patch_size(self) -> int:
57 raise NotImplementedError
58
59 @abstractmethod
60 def get_patch_grid_length(self) -> int:
61 raise NotImplementedError
62
63
64 class VisionLanguageConfig(Protocol):
65 @property
66 def vision_config(self) -> PreTrainedConfig: ...
67
68
69 def get_vision_encoder_info(hf_config: VisionLanguageConfig) -> VisionEncoderInfo:
70 # Avoid circular imports
71 from .clip import CLIPEncoderInfo, CLIPVisionConfig
72 from .pixtral import PixtralHFEncoderInfo, PixtralVisionConfig
73 from .siglip import SiglipEncoderInfo, SiglipVisionConfig
74
75 if isinstance(hf_config.vision_config, CLIPVisionConfig):
76 return CLIPEncoderInfo(hf_config)
77 if isinstance(hf_config.vision_config, PixtralVisionConfig):
78 return PixtralHFEncoderInfo(hf_config)
79 if isinstance(hf_config.vision_config, SiglipVisionConfig):
80 return SiglipEncoderInfo(hf_config)
81
82 msg = f"Unsupported vision config: {type(hf_config.vision_config)}"
83 raise NotImplementedError(msg)
84
85
86 def _get_vit_attn_backend(
87 head_size: int,
88 dtype: torch.dtype,
89 *,
90 attn_backend_override: AttentionBackendEnum | None = None,
91 ) -> AttentionBackendEnum:
92 """Get the available attention backend for Vision Transformer."""
93 return current_platform.get_vit_attn_backend(
94 head_size,
95 dtype,
96 backend=attn_backend_override,
97 )
98
99
100 def get_vit_attn_backend(
101 head_size: int,
102 dtype: torch.dtype,
103 ) -> AttentionBackendEnum:
104 """Get the attention backend for Vision Transformer."""
105 mm_cfg = get_multimodal_config()
106 attn_backend_override = (
107 mm_cfg.mm_encoder_attn_backend if mm_cfg is not None else None
108 )
109 return _get_vit_attn_backend(
110 head_size,
111 dtype,
112 attn_backend_override=attn_backend_override,
113 )
114
115
116 def get_multimodal_config() -> MultiModalConfig | None:
117 """Return the current ``MultiModalConfig``, or ``None`` when no engine
118 config context is active (e.g., during unit tests) or when the current
119 ``model_config`` does not carry a ``multimodal_config`` (e.g., minimal
120 stubs used in tests)."""
121 vllm_config = get_current_vllm_config_or_none()
122 if vllm_config is None or vllm_config.model_config is None:
123 return None
124 return getattr(vllm_config.model_config, "multimodal_config", None)
125
126
127 def get_fp8_padded_hidden_size(num_heads: int, head_dim: int) -> int | None:
128 """Return the padded hidden size for FP8 ViT encoder attention, or
129 ``None`` when FP8 is not enabled.
130
131 cuDNN FP8 prefill attention requires ``head_dim`` to be a multiple of
132 16. For non-aligned ``head_dim`` (e.g. 72), Q/K/V are padded to the
133 nearest multiple of 16.
134 """
135 mm_cfg = get_multimodal_config()
136 if mm_cfg is None or mm_cfg.mm_encoder_attn_dtype != "fp8":
137 return None
138 return num_heads * round_up(head_dim, 16)
139
140
141 def is_vit_use_data_parallel(num_heads: int | None = None) -> bool:
142 """Get the tensor parallel type for Vision Transformer."""
143 mm_cfg = get_multimodal_config()
144 can_split = (
145 num_heads % get_tensor_model_parallel_world_size() == 0
146 if num_heads is not None
147 else None
148 )
149 if num_heads is not None and not can_split:
150 logger.warning_once(
151 "The number of vision attention heads is not divisible by "
152 "the tensor parallel size. Falling back to data parallelism "
153 "for the vision encoder."
154 )
155 return True
156 return mm_cfg is not None and mm_cfg.mm_encoder_tp_mode == "data"
157
158
159 VisionFeatureSelectStrategyStr = Literal["class", "default", "full"]
160
161 VisionFeatureSelectStrategy: TypeAlias = (
162 VisionFeatureSelectStrategyStr | Callable[[torch.Tensor], torch.Tensor]
163 )
164
165
166 def _get_vision_feature_selector(
167 strategy: VisionFeatureSelectStrategy | str,
168 ) -> Callable[[torch.Tensor], torch.Tensor]:
169 if callable(strategy):
170 return strategy
171
172 # https://github.com/huggingface/transformers/blob/cd74917ffc3e8f84e4a886052c5ab32b7ac623cc/src/transformers/models/clip/modeling_clip.py#L762
173 if strategy == "class":
174 return lambda feats: feats[:, :1, :]
175
176 # https://github.com/huggingface/transformers/blob/4a02bc7004285bdb12cc033e87ad2578ce2fa900/src/transformers/models/llava/modeling_llava.py#L196
177 if strategy == "default":
178 return lambda feats: feats[:, 1:, :]
179
180 if strategy == "full":
181 return lambda feats: feats
182
183 raise ValueError(f"Unexpected feature select strategy: {strategy!r}")
184
185
186 def get_num_selected_vision_tokens(
187 num_vision_tokens: int,
188 strategy: VisionFeatureSelectStrategy | str,
189 ) -> int:
190 if callable(strategy):
191 dummy_features = torch.empty(1, num_vision_tokens, 64) # [B, L, D]
192 dummy_selected_features = strategy(dummy_features)
193 return dummy_selected_features.shape[1]
194
195 if strategy == "class":
196 return 1
197
198 if strategy == "default":
199 return num_vision_tokens - 1
200
201 if strategy == "full":
202 return num_vision_tokens
203
204 raise ValueError(f"Unexpected feature select strategy: {strategy!r}")
205
206
207 def resolve_visual_encoder_outputs(
208 encoder_outputs: torch.Tensor | list[torch.Tensor],
209 post_layer_norm: torch.nn.LayerNorm | None,
210 *,
211 select_layers: list[int] | None = None,
212 max_possible_layers: int | None = None,
213 last_hs_proc: Callable[[torch.Tensor], torch.Tensor] | None = None,
214 feature_select_strategy: VisionFeatureSelectStrategy | None = None,
215 ) -> torch.Tensor:
216 """Given the outputs a visual encoder module that may correspond to the
217 output of the last layer, or a list of hidden states to be stacked,
218 handle post normalization and resolve it into a single output tensor.
219
220 Args:
221 encoder_outputs: Output of encoder's last layer or all hidden states.
222 post_layer_norm: Post norm to apply to the output of the encoder.
223 select_layers: Optional layer indices to grab from the encoder
224 outputs; if provided, encoder outputs must be a list.
225 max_possible_layers: Total layers in the fully loaded visual encoder.
226 last_hs_proc: Optional callable to be applied to the last layer if it
227 is used, e.g., pooling head for Siglip. This is done prior to
228 feature selection and layer normalization. If select_layers are
229 provided, the output of last_hs_proc must be able to be
230 concatenated with the other select_layers along the last dimension.
231 feature_select_strategy: Defines how to select the hidden states
232 from each layer.
233
234 """
235 if select_layers is None:
236 if not isinstance(encoder_outputs, torch.Tensor):
237 raise ValueError(
238 "Expected only a single encoder output when "
239 "`select_layers` is not provided"
240 )
241
242 # Preprocess the encoder outputs as needed, e.g., map head
243 # and layer norm for siglip, which runs before feature selection
244 if last_hs_proc is not None:
245 encoder_outputs = last_hs_proc(encoder_outputs)
246
247 if feature_select_strategy is not None:
248 select_features = _get_vision_feature_selector(feature_select_strategy)
249 encoder_outputs = select_features(encoder_outputs)
250
251 if post_layer_norm is not None:
252 return post_layer_norm(encoder_outputs)
253
254 return encoder_outputs
255
256 if max_possible_layers is None:
257 raise ValueError(
258 "`max_possible_layers` must be provided alongside `select_layers`"
259 )
260
261 # Get the hidden states corresponding to the layer indices.
262 # Negative values are relative to the full visual encoder,
263 # so offset them depending on how many layers were loaded.
264 # NOTE: this assumes that encoder_outputs is a list containing
265 # the inputs to the visual encoder, followed by the hidden states
266 # of each layer.
267 num_loaded_layers = len(encoder_outputs) - 1
268 offset = max_possible_layers - num_loaded_layers
269 hs_pool = [
270 encoder_outputs[layer_idx]
271 if layer_idx >= 0
272 else encoder_outputs[layer_idx + offset]
273 for layer_idx in select_layers
274 ]
275
276 uses_last_layer = select_layers[-1] in (max_possible_layers - 1, -1)
277 if last_hs_proc is not None and uses_last_layer:
278 hs_pool[-1] = last_hs_proc(hs_pool[-1])
279
280 if feature_select_strategy is not None:
281 select_features = _get_vision_feature_selector(feature_select_strategy)
282 hs_pool = [select_features(hs) for hs in hs_pool]
283
284 # Apply post-norm on the final hidden state if we are using it
285 if post_layer_norm is not None and uses_last_layer:
286 hs_pool[-1] = post_layer_norm(hs_pool[-1])
287
288 return torch.cat(hs_pool, dim=-1)
289
290
291 def run_dp_sharded_vision_model(
292 image_input: torch.Tensor, vision_model: torch.nn.Module
293 ) -> torch.Tensor:
294 """Run a vision model with data parallelism (DP) sharding. The function
295 will shard the input image tensor on the first dimension and run the vision
296 model
297
298 Args:
299 image_input (torch.Tensor): Image input tensor.
300 vision_model (torch.nn.Module): Vision model.
301
302 Returns:
303 torch.Tensor: Output image embeddings
304
305 """
306 num_chunks = image_input.shape[0]
307 mp_world_size = get_tensor_model_parallel_world_size()
308 num_chunks_per_rank = (num_chunks + mp_world_size - 1) // mp_world_size
309 num_padded_chunks = num_chunks_per_rank * mp_world_size - num_chunks
310 pad = (0,) * (2 * (image_input.dim() - 1)) + (0, num_padded_chunks)
311 image_input_padded = torch.nn.functional.pad(image_input, pad)
312 rank = get_tensor_model_parallel_rank()
313 image_input_per_rank = image_input_padded[
314 rank * num_chunks_per_rank : (rank + 1) * num_chunks_per_rank, ...
315 ]
316
317 vision_embeddings = vision_model(image_input_per_rank)
318 # Ensure tensor is contiguous before all_gather
319 vision_embeddings = vision_embeddings.contiguous()
320 vision_embeddings = tensor_model_parallel_all_gather(vision_embeddings, dim=0)
321 vision_embeddings = vision_embeddings[:num_chunks, ...]
322 return vision_embeddings
323
324
325 def get_load_balance_assignment(
326 sizes: list[int],
327 num_gpus: int = 2,
328 ) -> tuple[list[int], list[int], list[int]]:
329 """Generate load balancing assignment and metadata
330 for distributing data across GPUs.
331 The load is determined by the total image sizes,
332 not the number of images.
333
334 Args:
335 sizes: The size of each image
336 num_gpus: Number of GPUs to balance across
337
338 Returns:
339 shuffle_indices:
340 Indices to reorder data for balanced loading
341 gpu_sample_counts:
342 Number of samples assigned to each GPU
343 grouped_sizes_per_gpu:
344 Total size assigned to each GPU
345
346 Example:
347 ```
348 sizes = [1000, 100, 200, 50]
349 num_gpus = 2
350 ```
351
352 """
353 n_samples = len(sizes)
354
355 # Handle edge cases
356 if n_samples == 0:
357 return [], [0] * num_gpus, [0] * num_gpus
358
359 # Use greedy algorithm - balance by total size, not sample count
360 gpu_assignments = [list[int]() for _ in range(num_gpus)]
361 gpu_loads = [0] * num_gpus # This tracks total SIZE, not sample count
362
363 # Sort indices by size (largest first for better load balancing)
364 # sizes = [1000, 100, 200, 50]
365 # large_to_small_indices = [0, 2, 1, 3]
366 large_to_small_indices = sorted(
367 range(n_samples), key=lambda i: sizes[i], reverse=True
368 )
369
370 for idx in large_to_small_indices:
371 # Find GPU with minimum current load (by total size)
372 min_gpu = min(range(num_gpus), key=lambda i: gpu_loads[i])
373 gpu_assignments[min_gpu].append(idx)
374 gpu_loads[min_gpu] += sizes[idx]
375
376 # Create shuffle indices and counts
377 shuffle_indices = list[int]()
378 gpu_sample_counts = list[int]()
379 for gpu_id in range(num_gpus):
380 # GPU_0 = [1000] = [0]
381 # GPU_1 = [200, 100, 50] = [2, 1, 3]
382 # shuffle_indices = [0, 2, 1, 3]
383 shuffle_indices.extend(gpu_assignments[gpu_id])
384 # GPU_0 = [1]
385 # GPU_1 = [3]
386 # gpu_sample_counts = [1, 3]
387 gpu_sample_counts.append(len(gpu_assignments[gpu_id]))
388
389 return (shuffle_indices, gpu_sample_counts, gpu_loads)
390
391
392 def run_dp_sharded_mrope_vision_model(
393 vision_model: torch.nn.Module,
394 pixel_values: torch.Tensor,
395 grid_thw_list: list[list[int]],
396 *,
397 rope_type: Literal["rope_3d", "rope_2d"],
398 ) -> tuple[torch.Tensor, ...]:
399 """Run a vision model with data parallelism (DP) sharding.
400 The function will shard the input image tensor on the
401 first dimension and run the vision model.
402 This function is used to run the vision model with mrope.
403
404 Args:
405 vision_model (torch.nn.Module): Vision model.
406 pixel_values (torch.Tensor): Image/Video input tensor.
407 grid_thw_list: List of grid dimensions for each image
408 rope_type: Type of rope used in the vision model.
409 Different rope types have different dimension to do ViT.
410 "rope_3d" for 3D rope (e.g., Qwen2.5-VL)
411 "rope_2d" for 2D rope (e.g., Kimi-VL)
412
413 Returns:
414 torch.Tensor: Output image embeddings
415
416 Example:
417 ```
418 vision_model.out_hidden_size = 64
419 vision_model.spatial_merge_size = 2
420 pixel_values.shape = (1350, channel)
421 grid_thw_list = [[1, 10, 100], [1, 10, 10], [1, 10, 20], [1, 50]]
422 tp_size = 2
423 ```
424
425 """
426 tp_size = get_tensor_model_parallel_world_size()
427
428 # GPU_0 tp_rank_local = 0
429 # GPU_1 tp_rank_local = 1
430 tp_rank_local = get_tensor_model_parallel_rank()
431
432 # patches_per_image = [1000, 100, 200, 50]
433 patches_per_image = [math.prod(grid_thw) for grid_thw in grid_thw_list]
434 # patches_per_image = [0, 1000, 1100, 1300, 1350]
435 cum_patches_per_image = [0, *itertools.accumulate(patches_per_image)]
436
437 # Get load balancing assignment with all metadata
438 # image_to_tp_rank = [0, 2, 1, 3]
439 # gpu_sample_counts = [1, 3]
440 # grouped_pixel_values_len = [1000, 350]
441 (image_to_tp_rank, gpu_sample_counts, grouped_pixel_values_len) = (
442 get_load_balance_assignment(patches_per_image, tp_size)
443 )
444
445 # cu_gpu_sample_counts = [0, 1, 4]
446 cum_gpu_sample_counts = [0, *itertools.accumulate(gpu_sample_counts)]
447
448 # GPU_0 image_idxs_local = [0]
449 # GPU_1 image_idxs_local = [2, 1, 3]
450 image_idxs_local = image_to_tp_rank[
451 cum_gpu_sample_counts[tp_rank_local] : cum_gpu_sample_counts[tp_rank_local + 1]
452 ]
453
454 # Get the pixel values for the local images based on the image_idxs_local
455 if len(image_idxs_local) > 0:
456 pixel_values_local = torch.cat(
457 [
458 pixel_values[cum_patches_per_image[i] : cum_patches_per_image[i + 1]]
459 for i in image_idxs_local
460 ]
461 )
462 else:
463 # Handle case where this rank has no images
464 pixel_values_local = torch.empty(
465 (0, pixel_values.shape[1]),
466 device=pixel_values.device,
467 dtype=pixel_values.dtype,
468 )
469 # embed_dim_reduction_factor = 2 * 2
470 if rope_type == "rope_2d":
471 embed_dim_reduction_factor = (
472 vision_model.merge_kernel_size[0] * vision_model.merge_kernel_size[1]
473 )
474 else:
475 embed_dim_reduction_factor = (
476 vision_model.spatial_merge_size * vision_model.spatial_merge_size
477 )
478
479 # Find the max length across all ranks
480 # The output embedding of every DP rank has to be
481 # padded to this length for tensor_model_parallel_all_gather
482 # to work
483 max_len_per_rank = max(grouped_pixel_values_len) // embed_dim_reduction_factor
484 local_grid_thw_list = [grid_thw_list[i] for i in image_idxs_local]
485 embed_dtype = next(vision_model.parameters()).dtype
486
487 # Run the vision model on the local pixel_values_local
488 if rope_type == "rope_2d":
489 if pixel_values_local.shape[0] > 0:
490 image_embeds_local = vision_model(
491 pixel_values_local, torch.tensor(local_grid_thw_list)
492 )
493 if isinstance(image_embeds_local, list):
494 image_embeds_local = torch.cat(image_embeds_local, dim=0)
495 else:
496 out_dim = getattr(vision_model.config, "hidden_size", None)
497 image_embeds_local = torch.empty(
498 (0, embed_dim_reduction_factor, out_dim),
499 device=pixel_values.device,
500 dtype=embed_dtype,
501 )
502 else:
503 if pixel_values_local.shape[0] > 0:
504 image_embeds_local = vision_model(pixel_values_local, local_grid_thw_list)
505 else:
506 # Handle empty case
507 image_embeds_local = torch.empty(
508 (0, vision_model.out_hidden_size),
509 device=pixel_values.device,
510 dtype=embed_dtype,
511 )
512
513 # Pad the output based on max_len_per_rank
514 # for tensor_model_parallel_all_gather to work
515 current_len = image_embeds_local.shape[0]
516 if current_len < max_len_per_rank:
517 padding_size = max_len_per_rank - current_len
518 if rope_type == "rope_2d":
519 padding = torch.empty(
520 (
521 padding_size,
522 image_embeds_local.shape[1],
523 image_embeds_local.shape[2],
524 ),
525 dtype=image_embeds_local.dtype,
526 device=image_embeds_local.device,
527 )
528 else:
529 padding = torch.empty(
530 (padding_size, image_embeds_local.shape[1]),
531 dtype=image_embeds_local.dtype,
532 device=image_embeds_local.device,
533 )
534 image_embeds_local_padded = torch.cat([image_embeds_local, padding], dim=0)
535 else:
536 image_embeds_local_padded = image_embeds_local
537
538 # Do all_gather to collect embeddings from all ranks
539 gathered_embeds = tensor_model_parallel_all_gather(image_embeds_local_padded, dim=0)
540
541 # Remove padding and reconstruct per-rank embeddings
542 rank_embeddings = list[torch.Tensor]()
543 for rank in range(tp_size):
544 start_idx = rank * max_len_per_rank
545 end_idx = start_idx + (
546 grouped_pixel_values_len[rank] // embed_dim_reduction_factor
547 )
548 rank_embeddings.append(gathered_embeds[start_idx:end_idx])
549
550 patches_per_output_image = [
551 (patch_size // embed_dim_reduction_factor) for patch_size in patches_per_image
552 ]
553
554 # Reconstruct embeddings in the original order
555 original_order_embeddings = [None] * len(grid_thw_list)
556 current_idx = 0
557 for rank in range(tp_size):
558 count = gpu_sample_counts[rank]
559 if count > 0:
560 # Get images assigned to this rank in shuffled order
561 # GPU_0 = image_idxs_local [0]
562 # GPU_1 = image_idxs_local [2, 1, 3]
563 rank_images = image_to_tp_rank[current_idx : current_idx + count]
564
565 rank_embed = rank_embeddings[rank]
566 # Split rank embeddings back to individual images
567 embed_start = 0
568 for img_idx in rank_images:
569 img_patches = patches_per_output_image[img_idx]
570 original_order_embeddings[img_idx] = rank_embed[
571 embed_start : embed_start + img_patches
572 ]
573 embed_start += img_patches
574 current_idx += count
575 out_embeddings = tuple(
576 embed for embed in original_order_embeddings if embed is not None
577 )
578 assert len(out_embeddings) == len(original_order_embeddings), (
579 "Found unassigned embeddings"
580 )
581 return out_embeddings