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 14, 05:30 UTC
[Model] Support DP for ViT on Kimi-VL-A3B-Thinking-2506 (#23817)
Signed-off-by: Junhong <liujunhong11@huawei.com> Signed-off-by: LJH-LBJ <98734602+LJH-LBJ@users.noreply.github.com> Co-authored-by: Junhong <liujunhong11@huawei.com> Co-authored-by: LJH-LBJ <98734602+LJH-LBJ@users.noreply.github.com> Co-authored-by: Isotr0py <2037008807@qq.com>
WeiQing Chen committed
Sep 2, 2025 at 00:56 UTC
a0e0efd6bdcfd071e5f4123319628887dfd4973d
6 files changed
+157
-62
docs/configuration/optimization.md
+1
@@ -174,6 +174,7 @@ Regardless, you need to set `mm_encoder_tp_mode="data"` in engine arguments to u
174
175
Known supported models:
176
177
+- Kimi-VL (<gh-pr:23817>)
178
- Llama4 (<gh-pr:18368>)
179
- MiniCPM-V-2.5 or above (<gh-pr:23327>, <gh-pr:23948>)
180
- Qwen2.5-VL (<gh-pr:22742>)
tests/multimodal/test_utils.py
+12
-6
@@ -636,8 +636,10 @@ def run_dp_sharded_mrope_vision_model_vs_direct(local_rank: int,
636
637
# Run the model through the sharded function
638
with torch.inference_mode():
639
- sharded_output = run_dp_sharded_mrope_vision_model(
640
- vision_model, pixel_values, grid_thw_list)
639
+ sharded_output = run_dp_sharded_mrope_vision_model(vision_model,
640
+ pixel_values,
641
+ grid_thw_list,
642
+ rope_type="rope_3d")
643
sharded_output = torch.cat(sharded_output, dim=0)
644
645
# Check that the world size is setup correctly
@@ -691,8 +693,10 @@ def run_dp_sharded_mrope_vision_model_empty_input_worker(
693
694
# Should handle empty input gracefully
695
with torch.inference_mode():
694
- output = run_dp_sharded_mrope_vision_model(vision_model, pixel_values,
695
- grid_thw_list)
696
+ output = run_dp_sharded_mrope_vision_model(vision_model,
697
+ pixel_values,
698
+ grid_thw_list,
699
+ rope_type="rope_3d")
700
701
assert len(output) == 0
702
@@ -745,8 +749,10 @@ def run_dp_sharded_mrope_vision_model_uneven_load_worker(
749
750
# Should handle uneven distribution without errors
751
with torch.inference_mode():
748
- output_tuple = run_dp_sharded_mrope_vision_model(
749
- vision_model, pixel_values, grid_thw_list)
752
+ output_tuple = run_dp_sharded_mrope_vision_model(vision_model,
753
+ pixel_values,
754
+ grid_thw_list,
755
+ rope_type="rope_3d")
756
757
# Verify output shape is reasonable
758
merge_factor = vision_model.spatial_merge_size**2
vllm/model_executor/models/kimi_vl.py
+39
-15
@@ -56,6 +56,7 @@ from transformers.activations import GELUActivation
56
from vllm.config import VllmConfig
57
from vllm.distributed import get_pp_group
58
from vllm.model_executor.layers.fused_moe import FusedMoE
59
+from vllm.model_executor.layers.linear import ReplicatedLinear
60
from vllm.model_executor.layers.logits_processor import LogitsProcessor
61
from vllm.model_executor.layers.vocab_parallel_embedding import (
62
DEFAULT_VOCAB_PADDING_SIZE, ParallelLMHead)
@@ -76,6 +77,7 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
77
BaseProcessingInfo, PromptReplacement,
78
PromptUpdate)
79
from vllm.multimodal.profiling import BaseDummyInputsBuilder
80
+from vllm.multimodal.utils import run_dp_sharded_mrope_vision_model
81
from vllm.sequence import IntermediateTensors
82
from vllm.transformers_utils.configs import KimiVLConfig, MoonViTConfig
83
from vllm.transformers_utils.configs.deepseek_vl2 import DeepseekV2Config
@@ -93,8 +95,10 @@ class MaxImageTokenMeta:
95
96
class KimiVLMultiModalProjector(nn.Module):
97
96
- def __init__(self, config: KimiVLConfig):
98
+ def __init__(self, config: KimiVLConfig, \
99
+ use_data_parallel: bool = False, prefix: str = ""):
100
super().__init__()
101
+ self.use_data_parallel = use_data_parallel
102
103
self.hidden_size = (config.vision_config.hidden_size *
104
config.vision_config.merge_kernel_size[0] *
@@ -102,20 +106,24 @@ class KimiVLMultiModalProjector(nn.Module):
106
107
self.pre_norm = torch.nn.LayerNorm(config.vision_config.hidden_size,
108
eps=1e-5)
105
- self.linear_1 = nn.Linear(self.hidden_size,
106
- self.hidden_size,
107
- bias=True)
109
+ self.linear_1 = ReplicatedLinear(self.hidden_size,
110
+ self.hidden_size,
111
+ bias=True,
112
+ prefix=maybe_prefix(
113
+ prefix, "linear_1"))
114
+ self.linear_2 = ReplicatedLinear(self.hidden_size,
115
+ config.text_config.hidden_size,
116
+ bias=True,
117
+ prefix=maybe_prefix(
118
+ prefix, "linear_2"))
119
self.act = GELUActivation()
109
- self.linear_2 = nn.Linear(self.hidden_size,
110
- config.text_config.hidden_size,
111
- bias=True)
120
121
def forward(self, image_features: torch.Tensor) -> torch.Tensor:
122
hidden_states = self.pre_norm(image_features).view(
123
-1, self.hidden_size)
116
- hidden_states = self.linear_1(hidden_states)
124
+ hidden_states, _ = self.linear_1(hidden_states)
125
hidden_states = self.act(hidden_states)
118
- hidden_states = self.linear_2(hidden_states)
126
+ hidden_states, _ = self.linear_2(hidden_states)
127
return hidden_states
128
129
@@ -273,6 +281,8 @@ class KimiVLMultiModalProcessor(BaseMultiModalProcessor[KimiVLProcessingInfo]):
281
class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal,
282
SupportsPP):
283
284
+ supports_encoder_tp_data = True
285
+
286
@classmethod
287
def get_placeholder_str(cls, modality: str, i: int) -> Optional[str]:
288
if modality.startswith("image"):
@@ -292,10 +302,17 @@ class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal,
302
quant_config = vllm_config.quant_config
303
304
assert isinstance(config.vision_config, MoonViTConfig)
295
-
296
- self.vision_tower = MoonVitPretrainedModel(config.vision_config)
297
-
298
- self.multi_modal_projector = KimiVLMultiModalProjector(config=config)
305
+ self.use_data_parallel = model_config.multimodal_config.mm_encoder_tp_mode == "data"
306
+ self.hidden_size = config.text_config.hidden_size
307
+ self.vision_tower = MoonVitPretrainedModel(config.vision_config,
308
+ self.use_data_parallel,
309
+ prefix=maybe_prefix(
310
+ prefix, "vision_tower"))
311
+
312
+ self.multi_modal_projector = KimiVLMultiModalProjector(
313
+ config=config,
314
+ use_data_parallel=self.use_data_parallel,
315
+ prefix=maybe_prefix(prefix, "multi_modal_projector"))
316
317
self.quant_config = quant_config
318
sub_vllm_config = copy.deepcopy(vllm_config)
@@ -376,13 +393,19 @@ class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal,
393
394
pixel_values = inputs["pixel_values"]
395
image_grid_hws = inputs["image_grid_hws"]
379
- return self.vision_tower(pixel_values, image_grid_hws)
396
+ if self.use_data_parallel:
397
+ return run_dp_sharded_mrope_vision_model(self.vision_tower,
398
+ pixel_values,
399
+ image_grid_hws.tolist(),
400
+ rope_type="rope_2d")
401
+ else:
402
+ return self.vision_tower(pixel_values, image_grid_hws)
403
404
def _process_image_input(self,
405
image_input: KimiVLImageInputs) -> torch.Tensor:
406
assert image_input["type"] == "pixel_values"
407
image_features = self._process_image_pixels(image_input)
385
- assert isinstance(image_features, list)
408
+ assert isinstance(image_features, (list, tuple))
409
lengths = [x.shape[0] for x in image_features]
410
return self.multi_modal_projector(
411
torch.cat(image_features)).split(lengths)
@@ -496,6 +519,7 @@ class KimiVLForConditionalGeneration(nn.Module, SupportsMultiModal,
519
expert_params_mapping = []
520
521
params_dict = dict(self.named_parameters())
522
+
523
for args in weights:
524
name, loaded_weight = args[:2]
525
kwargs = args[2] if len(args) > 2 else {}
vllm/model_executor/models/moonvit.py
+55
-22
@@ -42,7 +42,6 @@
42
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44
# SOFTWARE.
45
-import math
45
from collections.abc import Sequence
46
from copy import deepcopy
47
from functools import cached_property
@@ -55,6 +54,8 @@ from transformers.activations import ACT2FN, PytorchGELUTanh
54
from transformers.modeling_utils import PreTrainedModel
55
from transformers.utils import is_flash_attn_2_available
56
57
+from vllm.model_executor.layers.linear import ReplicatedLinear
58
+from vllm.model_executor.models.utils import maybe_prefix
59
from vllm.transformers_utils.configs.moonvit import MoonViTConfig
60
61
if is_flash_attn_2_available():
@@ -383,21 +384,30 @@ class MLP2(nn.Module):
384
bias: whether to use bias in linear layer.
385
"""
386
386
- def __init__(self, dims: list[int], activation, bias=True):
387
+ def __init__(self,
388
+ dims: list[int],
389
+ activation,
390
+ bias=True,
391
+ prefix: str = "",
392
+ use_data_parallel: bool = False):
393
super().__init__()
394
assert len(dims) == 3
389
- self.fc0 = nn.Linear(dims[0], dims[1], bias=bias)
390
- self.fc1 = nn.Linear(dims[1], dims[2], bias=bias)
395
+ self.use_data_parallel = use_data_parallel
396
+ self.fc0 = ReplicatedLinear(dims[0],
397
+ dims[1],
398
+ bias=bias,
399
+ prefix=maybe_prefix(prefix, "fc0"))
400
+ self.fc1 = ReplicatedLinear(dims[1],
401
+ dims[2],
402
+ bias=bias,
403
+ prefix=maybe_prefix(prefix, "fc1"))
404
self.activation = activation
392
- for m in [self.fc0, self.fc1]:
393
- nn.init.trunc_normal_(m.weight, std=math.sqrt(2 / m.in_features))
394
- if m.bias is not None:
395
- nn.init.zeros_(m.bias)
405
406
def forward(self, x: torch.Tensor) -> torch.Tensor:
398
- x = self.fc0(x)
407
+ x, _ = self.fc0(x)
408
x = self.activation(x)
400
- return self.fc1(x)
409
+ x, _ = self.fc1(x)
410
+ return x
411
412
413
class MoonVitEncoderLayer(nn.Module):
@@ -407,6 +417,8 @@ class MoonVitEncoderLayer(nn.Module):
417
num_heads: int,
418
hidden_dim: int,
419
mlp_dim: int,
420
+ prefix: str = "",
421
+ use_data_parallel: bool = False,
422
*,
423
attn_implementation: str = "sdpa",
424
activation=F.gelu,
@@ -423,9 +435,19 @@ class MoonVitEncoderLayer(nn.Module):
435
436
self.norm0 = nn.LayerNorm(hidden_dim)
437
self.norm1 = nn.LayerNorm(hidden_dim)
426
- self.mlp = MLP2([hidden_dim, mlp_dim, hidden_dim], activation)
427
- self.wqkv = nn.Linear(hidden_dim, hidden_dim * 3, bias=attn_bias)
428
- self.wo = nn.Linear(hidden_dim, hidden_dim, bias=attn_bias)
438
+ self.use_data_parallel = use_data_parallel
439
+ self.mlp = MLP2([hidden_dim, mlp_dim, hidden_dim],
440
+ activation,
441
+ prefix=f"{prefix}.mlp",
442
+ use_data_parallel=use_data_parallel)
443
+ self.wqkv = ReplicatedLinear(hidden_dim,
444
+ hidden_dim * 3,
445
+ bias=attn_bias,
446
+ prefix=f"{prefix}.wqkv")
447
+ self.wo = ReplicatedLinear(hidden_dim,
448
+ hidden_dim,
449
+ bias=attn_bias,
450
+ prefix=f"{prefix}.wo")
451
452
def attention_qkvpacked(
453
self,
@@ -438,7 +460,7 @@ class MoonVitEncoderLayer(nn.Module):
460
x (torch.Tensor): (batch_size, seqlen, hidden_dim)
461
cu_seqlens (torch.Tensor):
462
"""
441
- xqkv = self.wqkv(x)
463
+ xqkv, _ = self.wqkv(x)
464
465
qkv_shape = xqkv.size()[:-1] + (
466
3,
@@ -457,8 +479,7 @@ class MoonVitEncoderLayer(nn.Module):
479
xv,
480
q_cu_seqlens=cu_seqlens,
481
k_cu_seqlens=cu_seqlens)
460
-
461
- attn_out = self.wo(attn_out)
482
+ attn_out, _ = self.wo(attn_out)
483
return attn_out
484
485
def forward(
@@ -494,13 +515,17 @@ class MoonVitEncoder(nn.Module):
515
hidden_dim: int,
516
num_layers: int,
517
block_cfg: dict,
518
+ prefix: str = "",
519
+ use_data_parallel: bool = False,
520
) -> None:
521
super().__init__()
522
523
self.rope_2d = Rope2DPosEmb(
524
block_cfg["hidden_dim"] // block_cfg["num_heads"], 512, 512)
525
self.blocks = nn.ModuleList(
503
- [MoonVitEncoderLayer(**block_cfg) for _ in range(num_layers)])
526
+ [MoonVitEncoderLayer(use_data_parallel=use_data_parallel, \
527
+ prefix=f"{prefix}.blocks.{layer_idx}", \
528
+ **block_cfg) for layer_idx in range(num_layers)])
529
self.final_layernorm = nn.LayerNorm(hidden_dim)
530
531
def forward(self, hidden_states: torch.Tensor,
@@ -508,10 +533,9 @@ class MoonVitEncoder(nn.Module):
533
rope_freqs_cis = self.rope_2d.get_freqs_cis_by_seqlens(
534
grid_hws=grid_hw)
535
511
- lengths = torch.cat((
512
- torch.zeros(1, device=hidden_states.device, dtype=grid_hw.dtype),
513
- grid_hw[:, 0] * grid_hw[:, 1],
514
- ))
536
+ lengths = torch.cat(
537
+ (torch.zeros(1, device=hidden_states.device, dtype=grid_hw.dtype),
538
+ (grid_hw[:, 0] * grid_hw[:, 1]).to(hidden_states.device)))
539
cu_seqlens = lengths.cumsum(dim=0, dtype=torch.int32)
540
541
for _, block in enumerate(self.blocks):
@@ -587,11 +611,19 @@ class MoonVitPretrainedModel(PreTrainedModel):
611
_supports_flash_attn_2 = True
612
_supports_sdpa = True
613
590
- def __init__(self, config: MoonViTConfig, *inputs, **kwargs):
614
+ def __init__(self,
615
+ config: MoonViTConfig,
616
+ use_data_parallel: bool = False,
617
+ prefix: str = "",
618
+ *inputs,
619
+ **kwargs):
620
super().__init__(config, *inputs, **kwargs)
621
config = deepcopy(config)
622
+ self.use_data_parallel = use_data_parallel
623
self.merge_kernel_size = config.merge_kernel_size
624
+ self.hidden_size = config.hidden_size
625
self.patch_size = config.patch_size
626
+ self.vit_processing_type = "rope_2d"
627
self.patch_embed = MoonVisionPatchEmbed(
628
out_dim=config.hidden_size,
629
patch_size=config.patch_size,
@@ -610,6 +642,7 @@ class MoonVitPretrainedModel(PreTrainedModel):
642
"attn_bias": True,
643
"attn_implementation": config._attn_implementation,
644
},
645
+ prefix=f"{prefix}.encoder",
646
)
647
648
def forward(self, pixel_values: torch.Tensor,
vllm/model_executor/models/qwen2_5_vl.py
+8
-4
@@ -1021,8 +1021,10 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1021
pixel_values = image_input["pixel_values"]
1022
1023
if self.use_data_parallel:
1024
- return run_dp_sharded_mrope_vision_model(
1025
- self.visual, pixel_values, grid_thw_list)
1024
+ return run_dp_sharded_mrope_vision_model(self.visual,
1025
+ pixel_values,
1026
+ grid_thw_list,
1027
+ rope_type="rope_3d")
1028
else:
1029
image_embeds = self.visual(pixel_values,
1030
grid_thw=grid_thw_list)
@@ -1048,8 +1050,10 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1050
else:
1051
pixel_values_videos = video_input["pixel_values_videos"]
1052
if self.use_data_parallel:
1051
- return run_dp_sharded_mrope_vision_model(
1052
- self.visual, pixel_values_videos, grid_thw_list)
1053
+ return run_dp_sharded_mrope_vision_model(self.visual,
1054
+ pixel_values_videos,
1055
+ grid_thw_list,
1056
+ rope_type="rope_3d")
1057
else:
1058
video_embeds = self.visual(pixel_values_videos,
1059
grid_thw=grid_thw_list)
vllm/multimodal/utils.py
+42
-15
@@ -9,7 +9,7 @@ from collections.abc import Iterable
9
from concurrent.futures import ThreadPoolExecutor
10
from itertools import groupby
11
from pathlib import Path
12
-from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
12
+from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
13
from urllib.parse import ParseResult, urlparse
14
from urllib.request import url2pathname
15
@@ -444,7 +444,6 @@ def run_dp_sharded_vision_model(image_input: torch.Tensor,
444
Args:
445
image_input (torch.Tensor): Image input tensor.
446
vision_model (torch.nn.Module): Vision model.
447
-
447
Returns:
448
torch.Tensor: Output image embeddings
449
"""
@@ -542,6 +541,8 @@ def run_dp_sharded_mrope_vision_model(
541
vision_model: torch.nn.Module,
542
pixel_values: torch.Tensor,
543
grid_thw_list: list[list[int]],
544
+ *,
545
+ rope_type: Literal["rope_3d", "rope_2d"],
546
) -> tuple[torch.Tensor, ...]:
547
"""Run a vision model with data parallelism (DP) sharding.
548
The function will shard the input image tensor on the
@@ -552,6 +553,10 @@ def run_dp_sharded_mrope_vision_model(
553
vision_model (torch.nn.Module): Vision model.
554
pixel_values (torch.Tensor): Image/Video input tensor.
555
grid_thw_list: List of grid dimensions for each image
556
+ rope_type: Type of rope used in the vision model.
557
+ Different rope types have different dimension to do ViT.
558
+ "rope_3d" for 3D rope (e.g., Qwen2.5-VL)
559
+ "rope_2d" for 2D rope (e.g., Kimi-VL)
560
Returns:
561
torch.Tensor: Output image embeddings
562
@@ -605,8 +610,12 @@ def run_dp_sharded_mrope_vision_model(
610
device=pixel_values.device,
611
dtype=pixel_values.dtype)
612
# embed_dim_reduction_factor = 2 * 2
608
- embed_dim_reduction_factor = (vision_model.spatial_merge_size *
609
- vision_model.spatial_merge_size)
613
+ if rope_type == "rope_2d":
614
+ embed_dim_reduction_factor = (vision_model.merge_kernel_size[0] *
615
+ vision_model.merge_kernel_size[1])
616
+ else:
617
+ embed_dim_reduction_factor = (vision_model.spatial_merge_size *
618
+ vision_model.spatial_merge_size)
619
620
# Find the max length across all ranks
621
# The output embedding of every DP rank has to be
@@ -617,23 +626,42 @@ def run_dp_sharded_mrope_vision_model(
626
local_grid_thw_list = [grid_thw_list[i] for i in image_idxs_local]
627
628
# Run the vision model on the local pixel_values_local
620
- if pixel_values_local.shape[0] > 0:
621
- image_embeds_local = vision_model(pixel_values_local,
622
- local_grid_thw_list)
629
+ if rope_type == "rope_2d":
630
+ if pixel_values_local.shape[0] > 0:
631
+ image_embeds_local = vision_model(
632
+ pixel_values_local, torch.tensor(local_grid_thw_list))
633
+ if isinstance(image_embeds_local, list):
634
+ image_embeds_local = torch.cat(image_embeds_local, dim=0)
635
+ else:
636
+ out_dim = getattr(vision_model.config, "hidden_size", None)
637
+ image_embeds_local = torch.empty(
638
+ (0, embed_dim_reduction_factor, out_dim),
639
+ device=pixel_values.device,
640
+ dtype=pixel_values.dtype)
641
else:
624
- # Handle empty case
625
- image_embeds_local = torch.empty((0, vision_model.out_hidden_size),
626
- device=pixel_values.device,
627
- dtype=pixel_values.dtype)
642
+ if pixel_values_local.shape[0] > 0:
643
+ image_embeds_local = vision_model(pixel_values_local,
644
+ local_grid_thw_list)
645
+ else:
646
+ # Handle empty case
647
+ image_embeds_local = torch.empty((0, vision_model.out_hidden_size),
648
+ device=pixel_values.device,
649
+ dtype=pixel_values.dtype)
650
651
# Pad the output based on max_len_per_rank
652
# for tensor_model_parallel_all_gather to work
653
current_len = image_embeds_local.shape[0]
654
if current_len < max_len_per_rank:
655
padding_size = max_len_per_rank - current_len
634
- padding = torch.empty((padding_size, image_embeds_local.shape[1]),
635
- dtype=image_embeds_local.dtype,
636
- device=image_embeds_local.device)
656
+ if rope_type == "rope_2d":
657
+ padding = torch.empty((padding_size, image_embeds_local.shape[1],
658
+ image_embeds_local.shape[2]),
659
+ dtype=image_embeds_local.dtype,
660
+ device=image_embeds_local.device)
661
+ else:
662
+ padding = torch.empty((padding_size, image_embeds_local.shape[1]),
663
+ dtype=image_embeds_local.dtype,
664
+ device=image_embeds_local.device)
665
image_embeds_local_padded = torch.cat([image_embeds_local, padding],
666
dim=0)
667
else:
@@ -674,7 +702,6 @@ def run_dp_sharded_mrope_vision_model(
702
embed_start:embed_start + img_patches]
703
embed_start += img_patches
704
current_idx += count
677
-
705
out_embeddings = tuple(embed for embed in original_order_embeddings
706
if embed is not None)
707
assert len(out_embeddings) == len(