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 15, 07:15 UTC
[Model] enable data parallel for Llama4 vision encoder (#18368)
Signed-off-by: yzhen <yzhen@devgpu093.cco2.facebook.com> Co-authored-by: yZhen <yZhen@fb.com> Co-authored-by: yzhen <yzhen@devgpu093.cco2.facebook.com>
jennyyyyzhen committed
Jun 2, 2025 at 04:22 UTC
ebb1ec931871ee1baca23673049865856c44ce4e
4 files changed
+214
-68
vllm/config.py
+4
@@ -1790,6 +1790,10 @@ class ParallelConfig:
1790
rank: int = 0
1791
"""Global rank in distributed setup."""
1792
1793
+ enable_multimodal_encoder_data_parallel: bool = False
1794
+ """ Use data parallelism instead of tensor parallelism for vision encoder.
1795
+ Only support LLama4 for now"""
1796
+
1797
@property
1798
def world_size_across_dp(self) -> int:
1799
"""world_size_across_dp is TPxPPxDP, it is the size of the world
vllm/engine/arg_utils.py
+8
@@ -423,6 +423,9 @@ class EngineArgs:
423
use_tqdm_on_load: bool = LoadConfig.use_tqdm_on_load
424
pt_load_map_location: str = LoadConfig.pt_load_map_location
425
426
+ enable_multimodal_encoder_data_parallel: bool = \
427
+ ParallelConfig.enable_multimodal_encoder_data_parallel
428
+
429
def __post_init__(self):
430
# support `EngineArgs(compilation_config={...})`
431
# without having to manually construct a
@@ -637,6 +640,9 @@ class EngineArgs:
640
**parallel_kwargs["worker_cls"])
641
parallel_group.add_argument("--worker-extension-cls",
642
**parallel_kwargs["worker_extension_cls"])
643
+ parallel_group.add_argument(
644
+ "--enable-multimodal-encoder-data-parallel",
645
+ **parallel_kwargs["enable_multimodal_encoder_data_parallel"])
646
647
# KV cache arguments
648
cache_kwargs = get_kwargs(CacheConfig)
@@ -1078,6 +1084,8 @@ class EngineArgs:
1084
distributed_executor_backend=self.distributed_executor_backend,
1085
worker_cls=self.worker_cls,
1086
worker_extension_cls=self.worker_extension_cls,
1087
+ enable_multimodal_encoder_data_parallel=self.
1088
+ enable_multimodal_encoder_data_parallel,
1089
)
1090
1091
speculative_config = self.create_speculative_config(
vllm/model_executor/models/mllama4.py
+167
-68
@@ -34,6 +34,7 @@ from vllm.distributed import get_tensor_model_parallel_world_size
34
from vllm.inputs import InputProcessingContext
35
from vllm.model_executor.layers.linear import (ColumnParallelLinear,
36
QKVParallelLinear,
37
+ ReplicatedLinear,
38
RowParallelLinear)
39
from vllm.model_executor.layers.quantization import QuantizationConfig
40
from vllm.model_executor.layers.rotary_embedding import get_rope
@@ -49,6 +50,7 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
50
BaseProcessingInfo, PromptReplacement,
51
PromptUpdate, PromptUpdateDetails)
52
from vllm.multimodal.profiling import BaseDummyInputsBuilder
53
+from vllm.multimodal.utils import run_dp_sharded_vision_model
54
from vllm.sequence import IntermediateTensors
55
56
from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
@@ -84,23 +86,29 @@ class Llama4ImagePatchInputs(TypedDict):
86
87
class Llama4VisionMLP(nn.Module):
88
87
- def __init__(self,
88
- input_size: int,
89
- intermediate_size: int,
90
- output_size: int,
91
- bias: bool,
92
- output_activation: bool,
93
- quant_config: Optional[QuantizationConfig] = None,
94
- prefix: str = ""):
89
+ def __init__(
90
+ self,
91
+ input_size: int,
92
+ intermediate_size: int,
93
+ output_size: int,
94
+ bias: bool,
95
+ output_activation: bool,
96
+ quant_config: Optional[QuantizationConfig] = None,
97
+ prefix: str = "",
98
+ use_data_parallel: bool = False,
99
+ ):
100
super().__init__()
96
- self.fc1 = ColumnParallelLinear(
101
+ cls_fc1 = (ReplicatedLinear
102
+ if use_data_parallel else ColumnParallelLinear)
103
+ self.fc1 = cls_fc1(
104
input_size=input_size,
105
output_size=intermediate_size,
106
bias=bias,
107
quant_config=quant_config,
108
prefix=f"{prefix}.fc1",
109
)
103
- self.fc2 = RowParallelLinear(
110
+ cls_fc2 = ReplicatedLinear if use_data_parallel else RowParallelLinear
111
+ self.fc2 = cls_fc2(
112
input_size=intermediate_size,
113
output_size=output_size,
114
bias=bias,
@@ -155,10 +163,12 @@ def pixel_shuffle(input_tensor, shuffle_ratio):
163
int(channels / shuffle_ratio))
164
reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
165
158
- reshaped_tensor = reshaped_tensor.view(batch_size,
159
- int(height * shuffle_ratio),
160
- int(width * shuffle_ratio),
161
- int(channels / (shuffle_ratio**2)))
166
+ reshaped_tensor = reshaped_tensor.view(
167
+ batch_size,
168
+ int(height * shuffle_ratio),
169
+ int(width * shuffle_ratio),
170
+ int(channels / (shuffle_ratio**2)),
171
+ )
172
reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
173
174
output_tensor = reshaped_tensor.view(batch_size, -1,
@@ -173,6 +183,7 @@ class Llama4VisionPixelShuffleMLP(nn.Module):
183
config,
184
quant_config: Optional[QuantizationConfig] = None,
185
prefix: str = "",
186
+ use_data_parallel: bool = False,
187
):
188
super().__init__()
189
self.pixel_shuffle_ratio = config.pixel_shuffle_ratio
@@ -186,7 +197,9 @@ class Llama4VisionPixelShuffleMLP(nn.Module):
197
bias=config.multi_modal_projector_bias,
198
output_activation=True,
199
quant_config=quant_config,
189
- prefix=f"{prefix}.mlp")
200
+ prefix=f"{prefix}.mlp",
201
+ use_data_parallel=use_data_parallel,
202
+ )
203
204
def forward(self, encoded_patches: torch.Tensor) -> torch.Tensor:
205
encoded_patches = pixel_shuffle(encoded_patches,
@@ -201,10 +214,12 @@ class Llama4VisionAttention(nn.Module):
214
config: Llama4VisionConfig,
215
quant_config: Optional[QuantizationConfig],
216
prefix: str = "",
217
+ use_data_parallel: bool = False,
218
):
219
super().__init__()
220
self.config = config
207
- self.tp_size = get_tensor_model_parallel_world_size()
221
+ self.tp_size = (1 if use_data_parallel else
222
+ get_tensor_model_parallel_world_size())
223
self.embed_dim = config.hidden_size
224
self.num_heads = config.num_attention_heads
225
self.head_dim = config.hidden_size // self.num_heads
@@ -217,22 +232,39 @@ class Llama4VisionAttention(nn.Module):
232
233
self.attn = MultiHeadAttention(self.num_local_heads, self.head_dim,
234
self.scaling)
220
- self.qkv_proj = QKVParallelLinear(
221
- self.embed_dim,
222
- self.head_dim,
223
- self.num_heads,
224
- bias=True,
225
- quant_config=quant_config,
226
- prefix=f"{prefix}.qkv_proj",
227
- )
228
- self.o_proj = RowParallelLinear(
229
- self.num_heads * self.head_dim,
230
- self.embed_dim,
231
- bias=True,
232
- input_is_parallel=True,
233
- quant_config=quant_config,
234
- prefix=f"{prefix}.o_proj",
235
- )
235
+
236
+ if use_data_parallel:
237
+ self.qkv_proj = ReplicatedLinear(
238
+ self.embed_dim,
239
+ self.q_size + 2 * self.kv_size,
240
+ bias=True,
241
+ quant_config=quant_config,
242
+ prefix=f"{prefix}.qkv_proj",
243
+ )
244
+ self.o_proj = ReplicatedLinear(
245
+ self.num_heads * self.head_dim,
246
+ self.embed_dim,
247
+ bias=True,
248
+ quant_config=quant_config,
249
+ prefix=f"{prefix}.o_proj",
250
+ )
251
+ else:
252
+ self.qkv_proj = QKVParallelLinear(
253
+ self.embed_dim,
254
+ self.head_dim,
255
+ self.num_heads,
256
+ bias=True,
257
+ quant_config=quant_config,
258
+ prefix=f"{prefix}.qkv_proj",
259
+ )
260
+ self.o_proj = RowParallelLinear(
261
+ self.num_heads * self.head_dim,
262
+ self.embed_dim,
263
+ bias=True,
264
+ input_is_parallel=True,
265
+ quant_config=quant_config,
266
+ prefix=f"{prefix}.o_proj",
267
+ )
268
269
self.rotary_emb = get_rope(
270
head_size=self.head_dim,
@@ -275,22 +307,29 @@ class Llama4VisionEncoderLayer(nn.Module):
307
config: Llama4VisionConfig,
308
quant_config: Optional[QuantizationConfig],
309
prefix: str = "",
310
+ use_data_parallel: bool = False,
311
):
312
super().__init__()
313
self.hidden_size = config.hidden_size
314
self.num_attention_heads = config.num_attention_heads
315
self.intermediate_size = config.intermediate_size
316
284
- self.self_attn = Llama4VisionAttention(config,
285
- quant_config=quant_config,
286
- prefix=f"{prefix}.self_attn")
287
- self.mlp = Llama4VisionMLP(input_size=config.hidden_size,
288
- intermediate_size=config.intermediate_size,
289
- output_size=config.hidden_size,
290
- bias=True,
291
- output_activation=False,
292
- quant_config=quant_config,
293
- prefix=f"{prefix}.mlp")
317
+ self.self_attn = Llama4VisionAttention(
318
+ config,
319
+ quant_config=quant_config,
320
+ prefix=f"{prefix}.self_attn",
321
+ use_data_parallel=use_data_parallel,
322
+ )
323
+ self.mlp = Llama4VisionMLP(
324
+ input_size=config.hidden_size,
325
+ intermediate_size=config.intermediate_size,
326
+ output_size=config.hidden_size,
327
+ bias=True,
328
+ output_activation=False,
329
+ quant_config=quant_config,
330
+ prefix=f"{prefix}.mlp",
331
+ use_data_parallel=use_data_parallel,
332
+ )
333
334
self.input_layernorm = nn.LayerNorm(config.hidden_size)
335
self.post_attention_layernorm = nn.LayerNorm(config.hidden_size)
@@ -322,6 +361,7 @@ class Llama4VisionEncoder(nn.Module):
361
config: Llama4VisionConfig,
362
quant_config: Optional[QuantizationConfig],
363
prefix: str = "",
364
+ use_data_parallel: bool = False,
365
):
366
super().__init__()
367
self.config = config
@@ -330,6 +370,7 @@ class Llama4VisionEncoder(nn.Module):
370
config,
371
quant_config=quant_config,
372
prefix=f"{prefix}.layers.{layer_idx}",
373
+ use_data_parallel=use_data_parallel,
374
) for layer_idx in range(config.num_hidden_layers)
375
])
376
@@ -357,23 +398,33 @@ class Llama4VisionEncoder(nn.Module):
398
399
class Llama4UnfoldConvolution(nn.Module):
400
360
- def __init__(self,
361
- config: Llama4VisionConfig,
362
- quant_config: Optional[QuantizationConfig] = None,
363
- prefix: str = ""):
401
+ def __init__(
402
+ self,
403
+ config: Llama4VisionConfig,
404
+ quant_config: Optional[QuantizationConfig] = None,
405
+ prefix: str = "",
406
+ use_data_parallel: bool = False,
407
+ ):
408
super().__init__()
409
kernel_size = config.patch_size
410
if isinstance(kernel_size, int):
411
kernel_size = (kernel_size, kernel_size)
412
self.unfold = torch.nn.Unfold(kernel_size=kernel_size,
413
stride=config.patch_size)
370
- self.linear = ColumnParallelLinear(config.num_channels *
371
- kernel_size[0] * kernel_size[1],
372
- config.hidden_size,
373
- bias=False,
374
- quant_config=quant_config,
375
- gather_output=True,
376
- prefix=f"{prefix}.linear")
414
+ params = {
415
+ "input_size":
416
+ config.num_channels * kernel_size[0] * kernel_size[1],
417
+ "output_size": config.hidden_size,
418
+ "bias": False,
419
+ "quant_config": quant_config,
420
+ "prefix": f"{prefix}.linear",
421
+ }
422
+ if use_data_parallel:
423
+ cls = ReplicatedLinear
424
+ else:
425
+ cls = ColumnParallelLinear
426
+ params["gather_output"] = True
427
+ self.linear = cls(**params)
428
429
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
430
hidden_states = self.unfold(hidden_states)
@@ -389,6 +440,7 @@ class Llama4VisionModel(nn.Module):
440
config: Llama4VisionConfig,
441
quant_config: Optional[QuantizationConfig] = None,
442
prefix: str = "",
443
+ use_data_parallel: bool = False,
444
):
445
super().__init__()
446
self.config = config
@@ -403,7 +455,9 @@ class Llama4VisionModel(nn.Module):
455
self.patch_embedding = Llama4UnfoldConvolution(
456
config,
457
quant_config=quant_config,
406
- prefix=f"{prefix}.patch_embedding")
458
+ prefix=f"{prefix}.patch_embedding",
459
+ use_data_parallel=use_data_parallel,
460
+ )
461
462
self.class_embedding = nn.Parameter(self.scale *
463
torch.randn(self.hidden_size))
@@ -415,11 +469,18 @@ class Llama4VisionModel(nn.Module):
469
self.layernorm_post = nn.LayerNorm(self.hidden_size, eps=1e-5)
470
471
# encoders
418
- self.model = Llama4VisionEncoder(config,
419
- quant_config=quant_config,
420
- prefix=f"{prefix}.model")
472
+ self.model = Llama4VisionEncoder(
473
+ config,
474
+ quant_config=quant_config,
475
+ prefix=f"{prefix}.model",
476
+ use_data_parallel=use_data_parallel,
477
+ )
478
self.vision_adapter = Llama4VisionPixelShuffleMLP(
422
- config, quant_config, prefix=f"{prefix}.vision_adapter")
479
+ config,
480
+ quant_config,
481
+ prefix=f"{prefix}.vision_adapter",
482
+ use_data_parallel=use_data_parallel,
483
+ )
484
485
def forward(
486
self,
@@ -528,8 +589,9 @@ class Mllama4MultiModalProcessor(BaseMultiModalProcessor[Mllama4ProcessingInfo]
589
vision_config = self.info.get_hf_config().vision_config
590
591
if processed_outputs.get("pixel_values") is not None:
531
- assert "images" in mm_data, \
532
- "images expected to be in mm_data when pixel_values is present"
592
+ assert (
593
+ "images" in mm_data
594
+ ), "images expected to be in mm_data when pixel_values is present"
595
596
images = mm_data["images"]
597
parsed_images = (self._get_data_parser().parse_mm_data({
@@ -546,8 +608,8 @@ class Mllama4MultiModalProcessor(BaseMultiModalProcessor[Mllama4ProcessingInfo]
608
get_best_fit(
609
(image.size[1], image.size[0]),
610
torch.tensor(possible_resolutions),
549
- resize_to_max_canvas=image_processor.resize_to_max_canvas)
550
- for image in parsed_images
611
+ resize_to_max_canvas=image_processor.resize_to_max_canvas,
612
+ ) for image in parsed_images
613
]
614
# TODO tile height/width do not necessarily need to match
615
aspect_ratios = [(image_size[0] // tile_size,
@@ -659,13 +721,17 @@ class Llama4ForConditionalGeneration(nn.Module, SupportsMultiModal,
721
config = vllm_config.model_config.hf_config
722
quant_config = vllm_config.quant_config
723
multimodal_config = vllm_config.model_config.multimodal_config
724
+ self.use_data_parallel = (vllm_config.parallel_config.
725
+ enable_multimodal_encoder_data_parallel)
726
self.config = config
727
self.quant_config = quant_config
728
self.multimodal_config = multimodal_config
665
- self.vision_model = Llama4VisionModel(config.vision_config,
666
- None,
667
- prefix=maybe_prefix(
668
- prefix, "vision_model"))
729
+ self.vision_model = Llama4VisionModel(
730
+ config.vision_config,
731
+ None,
732
+ prefix=maybe_prefix(prefix, "vision_model"),
733
+ use_data_parallel=self.use_data_parallel,
734
+ )
735
self.multi_modal_projector = Llama4MultiModalProjector(
736
self.config,
737
None,
@@ -709,7 +775,13 @@ class Llama4ForConditionalGeneration(nn.Module, SupportsMultiModal,
775
flat_data = image_input["flat_data"]
776
patches_per_image = image_input["patches_per_image"].tolist()
777
712
- vision_embeddings_flat = self.vision_model(flat_data)
778
+ # shard image input
779
+ if self.use_data_parallel:
780
+ vision_embeddings_flat = run_dp_sharded_vision_model(
781
+ flat_data, self.vision_model)
782
+ else:
783
+ vision_embeddings_flat = self.vision_model(flat_data)
784
+
785
vision_embeddings_flat = self.multi_modal_projector(
786
vision_embeddings_flat)
787
@@ -796,6 +868,30 @@ class Llama4ForConditionalGeneration(nn.Module, SupportsMultiModal,
868
869
return get_prefix_weights(), get_other_weights()
870
871
+ def _consolidate_qkv_weights(
872
+ self, weights: Iterable[tuple[str, torch.Tensor]]
873
+ ) -> Iterable[tuple[str, torch.Tensor]]:
874
+ qkv_idx_mappings = {
875
+ ".self_attn.q_proj": 0,
876
+ ".self_attn.k_proj": 1,
877
+ ".self_attn.v_proj": 2,
878
+ }
879
+ qkv_weights = {}
880
+ for name, loaded_weight in weights:
881
+ for weight_name, idx in qkv_idx_mappings.items():
882
+ if weight_name not in name:
883
+ continue
884
+ new_name = name.replace(weight_name, ".self_attn.qkv_proj")
885
+ if new_name not in qkv_weights:
886
+ qkv_weights[new_name] = [None] * 3
887
+ qkv_weights[new_name][idx] = loaded_weight
888
+ break
889
+ else:
890
+ yield name, loaded_weight
891
+ for key, weight in qkv_weights.items():
892
+ qkv_weight = torch.cat(weight, dim=0)
893
+ yield key, qkv_weight
894
+
895
def load_weights(self, weights: Iterable[tuple[str,
896
torch.Tensor]]) -> set[str]:
897
@@ -818,9 +914,12 @@ class Llama4ForConditionalGeneration(nn.Module, SupportsMultiModal,
914
assert loaded_language_model_params is not None
915
updated_params.update(loaded_language_model_params)
916
917
+ if self.use_data_parallel:
918
+ other_weights = self._consolidate_qkv_weights(other_weights)
919
+
920
for name, loaded_weight in other_weights:
921
for param_name, weight_name, shard_id in stacked_params_mapping:
823
- if weight_name not in name:
922
+ if weight_name not in name or self.use_data_parallel:
923
continue
924
name = name.replace(weight_name, param_name)
925
param = params_dict[name]
vllm/multimodal/utils.py
+35
@@ -12,6 +12,9 @@ from PIL import Image
12
13
import vllm.envs as envs
14
from vllm.connections import HTTPConnection, global_http_connection
15
+from vllm.distributed import (get_tensor_model_parallel_rank,
16
+ get_tensor_model_parallel_world_size,
17
+ tensor_model_parallel_all_gather)
18
19
from .audio import AudioMediaIO
20
from .base import MediaIO
@@ -390,3 +393,35 @@ def group_mm_inputs_by_modality(
393
return [
394
list(group) for _, group in groupby(mm_inputs, key=modality_group_func)
395
]
396
+
397
+
398
+def run_dp_sharded_vision_model(image_input: torch.Tensor,
399
+ vision_model: torch.nn.Module) -> torch.Tensor:
400
+ """Run a vision model with data parallelism (DP) sharding. The function
401
+ will shard the input image tensor on the first dimension and run the vision
402
+ model
403
+
404
+ Args:
405
+ image_input (torch.Tensor): Image input tensor.
406
+ vision_model (torch.nn.Module): Vision model.
407
+
408
+ Returns:
409
+ torch.Tensor: Output image embeddings
410
+ """
411
+
412
+ num_chunks = image_input.shape[0]
413
+ mp_world_size = get_tensor_model_parallel_world_size()
414
+ num_chunks_per_rank = (num_chunks + mp_world_size - 1) // mp_world_size
415
+ num_padded_chunks = num_chunks_per_rank * mp_world_size - num_chunks
416
+ pad = (0, ) * (2 * (image_input.dim() - 1)) + (0, num_padded_chunks)
417
+ image_input_padded = torch.nn.functional.pad(image_input, pad)
418
+ rank = get_tensor_model_parallel_rank()
419
+ image_input_per_rank = image_input_padded[rank *
420
+ num_chunks_per_rank:(rank + 1) *
421
+ num_chunks_per_rank, ...]
422
+
423
+ vision_embeddings = vision_model(image_input_per_rank)
424
+ vision_embeddings = tensor_model_parallel_all_gather(vision_embeddings,
425
+ dim=0)
426
+ vision_embeddings = vision_embeddings[:num_chunks, ...]
427
+ return vision_embeddings