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
EVS Support (Video tokens pruning) (#22980)
Signed-off-by: Eugene Khvedchenia <ekhvedchenia@nvidia.com> Signed-off-by: Eugene Khvedchenya <ekhvedchenya@gmail.com> Co-authored-by: Roger Wang <hey@rogerw.io>
Eugene Khvedchenya committed
Sep 26, 2025 at 06:54 UTC
392edee34a008af2453d936cb3cdbd97842984a7
8 files changed
+783
-39
tests/models/multimodal/generation/test_qwen2_5_vl.py
new
+132
@@ -0,0 +1,132 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+
6
+from vllm.multimodal.video import sample_frames_from_video
7
+
8
+from ....conftest import VIDEO_ASSETS
9
+
10
+models = ["Qwen/Qwen2.5-VL-3B-Instruct"]
11
+target_dtype = "bfloat16"
12
+
13
+VIDEO_PLACEHOLDER = "<|vision_start|><|video_pad|><|vision_end|>"
14
+
15
+
16
+def qwen2_5_vl_chat_template(*query):
17
+ return f"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{''.join(query)}<|im_end|><|im_start|>assistant\n" # noqa: E501
18
+
19
+
20
+VIDEO_PROMPTS = VIDEO_ASSETS.prompts({
21
+ "baby_reading":
22
+ qwen2_5_vl_chat_template(
23
+ VIDEO_PLACEHOLDER,
24
+ "Describe this video with a short sentence ",
25
+ "(no more than 20 words)",
26
+ ),
27
+})
28
+
29
+
30
+@pytest.mark.core_model
31
+@pytest.mark.parametrize("model", models)
32
+@pytest.mark.parametrize("video_pruning_rate", [0.0, 0.75])
33
+@pytest.mark.parametrize("num_frames", [16])
34
+@pytest.mark.parametrize("dtype", [target_dtype])
35
+@pytest.mark.parametrize("max_tokens", [128])
36
+def test_qwen2_5_vl_evs_functionality(vllm_runner, video_assets, model,
37
+ video_pruning_rate: float,
38
+ num_frames: int, dtype: str,
39
+ max_tokens: int) -> None:
40
+ """Test EVS (Efficient Video Sampling) functionality with different
41
+ pruning rates.
42
+ """
43
+
44
+ # Sample frames from video assets
45
+ sampled_vids = [
46
+ sample_frames_from_video(asset.np_ndarrays, num_frames)
47
+ for asset in video_assets
48
+ ]
49
+
50
+ prompts = [VIDEO_PROMPTS[0]]
51
+ videos = [sampled_vids[0]]
52
+
53
+ # Initialize model with EVS configuration
54
+ with vllm_runner(model,
55
+ runner="generate",
56
+ max_model_len=4000,
57
+ max_num_seqs=1,
58
+ dtype=dtype,
59
+ limit_mm_per_prompt={"video": 1},
60
+ tensor_parallel_size=1,
61
+ video_pruning_rate=video_pruning_rate) as vllm_model:
62
+
63
+ # Generate output - this should not crash
64
+ outputs = vllm_model.generate_greedy(prompts,
65
+ max_tokens,
66
+ videos=videos)
67
+
68
+ # Basic validation that we got a response
69
+ assert len(outputs) == 1
70
+ output_ids, output_text = outputs[0]
71
+
72
+ # Ensure we got some output
73
+ assert len(output_ids) > 0
74
+ assert len(output_text) > 0
75
+
76
+ # Ensure the output is a string
77
+ assert isinstance(output_text, str)
78
+
79
+
80
+@pytest.mark.core_model
81
+@pytest.mark.parametrize("model", models)
82
+@pytest.mark.parametrize("video_pruning_rate", [0.0, 0.75])
83
+@pytest.mark.parametrize("num_frames", [16])
84
+@pytest.mark.parametrize("dtype", [target_dtype])
85
+@pytest.mark.parametrize("max_tokens", [128])
86
+def test_qwen2_5_vl_evs_batched_videos(vllm_runner, video_assets, model,
87
+ video_pruning_rate: float,
88
+ num_frames: int, dtype: str,
89
+ max_tokens: int) -> None:
90
+ """Test EVS functionality with batched videos.
91
+
92
+ This test validates that:
93
+ 1. The model handles batched video inputs correctly with EVS
94
+ 2. Both pruning configurations work with multiple videos
95
+ 3. The model doesn't crash when processing multiple videos simultaneously
96
+ """
97
+ # Sample frames from video assets
98
+ sampled_vids = [
99
+ sample_frames_from_video(asset.np_ndarrays, num_frames)
100
+ for asset in video_assets
101
+ ]
102
+
103
+ # Test batched videos
104
+ prompts = [VIDEO_PROMPTS[0], VIDEO_PROMPTS[0]]
105
+ videos = [sampled_vids[0],
106
+ sampled_vids[0]] # Use same video twice for testing
107
+
108
+ # Initialize model with EVS configuration
109
+ with vllm_runner(model,
110
+ runner="generate",
111
+ max_model_len=4000,
112
+ max_num_seqs=2,
113
+ dtype=dtype,
114
+ limit_mm_per_prompt={"video": 2},
115
+ tensor_parallel_size=1,
116
+ video_pruning_rate=video_pruning_rate) as vllm_model:
117
+
118
+ # Generate output - this should not crash
119
+ outputs = vllm_model.generate_greedy(prompts,
120
+ max_tokens,
121
+ videos=videos)
122
+
123
+ # Basic validation that we got responses for both videos
124
+ assert len(outputs) == 2
125
+
126
+ for output_ids, output_text in outputs:
127
+ # Ensure we got some output for each video
128
+ assert len(output_ids) > 0
129
+ assert len(output_text) > 0
130
+
131
+ # Ensure the output is a string
132
+ assert isinstance(output_text, str)
vllm/config/model.py
+16
-11
@@ -283,6 +283,7 @@ class ModelConfig:
283
mm_encoder_tp_mode: InitVar[Optional[MMEncoderTPMode]] = None
284
interleave_mm_strings: InitVar[Optional[bool]] = None
285
skip_mm_profiling: InitVar[Optional[bool]] = None
286
+ video_pruning_rate: InitVar[Optional[float]] = None
287
288
def compute_hash(self) -> str:
289
"""
@@ -311,6 +312,7 @@ class ModelConfig:
312
factors.append(self.override_generation_config)
313
factors.append(self.rope_scaling)
314
factors.append(self.rope_theta)
315
+ factors.append(self.video_pruning_rate)
316
317
# hf_config can control how the model looks!
318
try:
@@ -338,17 +340,19 @@ class ModelConfig:
340
return hashlib.sha256(str(factors).encode()).hexdigest()
341
342
def __post_init__(
341
- self,
342
- # Multimodal config init vars
343
- limit_mm_per_prompt: Optional[dict[str, int]],
344
- media_io_kwargs: Optional[dict[str, dict[str, Any]]],
345
- mm_processor_kwargs: Optional[dict[str, Any]],
346
- mm_processor_cache_gb: Optional[float],
347
- mm_processor_cache_type: Optional[MMCacheType],
348
- mm_shm_cache_max_object_size_mb: Optional[int],
349
- mm_encoder_tp_mode: Optional[MMEncoderTPMode],
350
- interleave_mm_strings: Optional[bool],
351
- skip_mm_profiling: Optional[bool]) -> None:
343
+ self,
344
+ # Multimodal config init vars
345
+ limit_mm_per_prompt: Optional[dict[str, int]],
346
+ media_io_kwargs: Optional[dict[str, dict[str, Any]]],
347
+ mm_processor_kwargs: Optional[dict[str, Any]],
348
+ mm_processor_cache_gb: Optional[float],
349
+ mm_processor_cache_type: Optional[MMCacheType],
350
+ mm_shm_cache_max_object_size_mb: Optional[int],
351
+ mm_encoder_tp_mode: Optional[MMEncoderTPMode],
352
+ interleave_mm_strings: Optional[bool],
353
+ skip_mm_profiling: Optional[bool],
354
+ video_pruning_rate: Optional[float],
355
+ ) -> None:
356
# Set the default seed to 0 in V1.
357
# NOTE(woosuk): In V0, we set the default seed to None because the
358
# driver worker shares the same process as the user process, and thus
@@ -612,6 +616,7 @@ class ModelConfig:
616
mm_encoder_tp_mode=mm_encoder_tp_mode,
617
interleave_mm_strings=interleave_mm_strings,
618
skip_mm_profiling=skip_mm_profiling,
619
+ video_pruning_rate=video_pruning_rate,
620
)
621
622
mm_config_kwargs = {
vllm/config/multimodal.py
+9
@@ -78,6 +78,11 @@ class MultiModalConfig:
78
This reduces engine startup time but shifts the responsibility to users for
79
estimating the peak memory usage of the activation of multimodal encoder and
80
embedding cache."""
81
+ video_pruning_rate: Optional[float] = None
82
+ """Sets pruning rate for video pruning via Efficient Video Sampling.
83
+ Value sits in range [0;1) and determines fraction of media tokens
84
+ from each video to be pruned.
85
+ """
86
87
def compute_hash(self) -> str:
88
"""
@@ -118,3 +123,7 @@ class MultiModalConfig:
123
"""
124
kwargs = self.mm_processor_kwargs or {}
125
return kwargs | dict(inference_kwargs)
126
+
127
+ def is_multimodal_pruning_enabled(self):
128
+ return (self.video_pruning_rate is not None
129
+ and self.video_pruning_rate > 0)
vllm/engine/arg_utils.py
+5
@@ -391,6 +391,7 @@ class EngineArgs:
391
mm_encoder_tp_mode: MMEncoderTPMode = MultiModalConfig.mm_encoder_tp_mode
392
io_processor_plugin: Optional[str] = None
393
skip_mm_profiling: bool = MultiModalConfig.skip_mm_profiling
394
+ video_pruning_rate: float = MultiModalConfig.video_pruning_rate
395
# LoRA fields
396
enable_lora: bool = False
397
enable_lora_bias: bool = LoRAConfig.bias_enabled
@@ -813,6 +814,9 @@ class EngineArgs:
814
multimodal_group.add_argument("--skip-mm-profiling",
815
**multimodal_kwargs["skip_mm_profiling"])
816
817
+ multimodal_group.add_argument(
818
+ "--video-pruning-rate", **multimodal_kwargs["video_pruning_rate"])
819
+
820
# LoRA related configs
821
lora_kwargs = get_kwargs(LoRAConfig)
822
lora_group = parser.add_argument_group(
@@ -1032,6 +1036,7 @@ class EngineArgs:
1036
model_impl=self.model_impl,
1037
override_attention_dtype=self.override_attention_dtype,
1038
logits_processors=self.logits_processors,
1039
+ video_pruning_rate=self.video_pruning_rate,
1040
io_processor_plugin=self.io_processor_plugin,
1041
)
1042
vllm/model_executor/models/interfaces.py
+55
@@ -115,6 +115,42 @@ class SupportsMultiModal(Protocol):
115
...
116
117
118
+@runtime_checkable
119
+class SupportsMultiModalPruning(Protocol):
120
+ """The interface required for models that support returning both input
121
+ embeddings and positions. Model may require custom positions for dynamic
122
+ pruning of multimodal embeddings.
123
+ """
124
+ supports_multimodal_pruning: ClassVar[Literal[True]] = True
125
+
126
+ def recompute_mrope_positions(
127
+ self, input_ids: list[int],
128
+ multimodal_embeddings: MultiModalEmbeddings,
129
+ mrope_positions: torch.LongTensor, num_computed_tokens: int
130
+ ) -> tuple[MultiModalEmbeddings, Tensor, int]:
131
+ """
132
+ Update part of input mrope positions (starting with
133
+ num_computed_tokens index). Original mrope_positions are computed
134
+ for unpruned sequence and becomes incorrect once pruning occurs,
135
+ so once we prune media tokens we should reflect this in the
136
+ mrope_positions before we feed it to LLM.
137
+
138
+ Args:
139
+ input_ids: (N,) All input tokens of the prompt containing
140
+ entire sequence.
141
+ multimodal_embeddings: Tuple of multimodal embeddings that
142
+ fits into the prefill chunk that is being processed.
143
+ mrope_positions: Existing mrope positions (3, N) for entire
144
+ sequence
145
+ num_computed_tokens: A number of computed tokens so far.
146
+
147
+ Returns:
148
+ Tuple of (multimodal_embeddings, mrope_positions,
149
+ mrope_position_delta).
150
+ """
151
+ ...
152
+
153
+
154
@overload
155
def supports_multimodal(
156
model: type[object]) -> TypeIs[type[SupportsMultiModal]]:
@@ -142,6 +178,25 @@ def supports_multimodal_encoder_tp_data(
178
return getattr(model, "supports_encoder_tp_data", False)
179
180
181
+@overload
182
+def supports_multimodal_pruning(
183
+ model: type[object]) -> TypeIs[type[SupportsMultiModalPruning]]:
184
+ ...
185
+
186
+
187
+@overload
188
+def supports_multimodal_pruning(
189
+ model: object) -> TypeIs[SupportsMultiModalPruning]:
190
+ ...
191
+
192
+
193
+def supports_multimodal_pruning(
194
+ model: Union[type[object], object],
195
+) -> Union[TypeIs[type[SupportsMultiModalPruning]],
196
+ TypeIs[SupportsMultiModalPruning]]:
197
+ return getattr(model, "supports_multimodal_pruning", False)
198
+
199
+
200
@runtime_checkable
201
class SupportsScoreTemplate(Protocol):
202
"""The interface required for all models that support score template."""
vllm/model_executor/models/qwen2_5_vl.py
+226
-12
@@ -25,9 +25,9 @@
25
# See the License for the specific language governing permissions and
26
# limitations under the License.
27
"""Inference-only Qwen2.5-VL model compatible with HuggingFace weights."""
28
-from collections.abc import Iterable, Mapping
28
+from collections.abc import Iterable, Mapping, Sequence
29
from functools import lru_cache, partial
30
-from typing import Annotated, Callable, Literal, Optional, Union
30
+from typing import Annotated, Any, Callable, Literal, Optional, Union
31
32
import torch
33
import torch.nn as nn
@@ -58,7 +58,13 @@ from vllm.model_executor.layers.quantization.gptq_marlin import (
58
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
59
from vllm.model_executor.models.module_mapping import MultiModelKeys
60
from vllm.multimodal import MULTIMODAL_REGISTRY
61
-from vllm.multimodal.inputs import MultiModalFieldConfig
61
+from vllm.multimodal.evs import (compute_mrope_for_media,
62
+ compute_retained_tokens_count,
63
+ compute_retention_mask,
64
+ recompute_mrope_positions)
65
+from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargs
66
+from vllm.multimodal.parse import MultiModalDataItems
67
+from vllm.multimodal.processing import PromptReplacement, PromptUpdate
68
from vllm.platforms import _Backend
69
from vllm.sequence import IntermediateTensors
70
from vllm.transformers_utils.config import uses_mrope
@@ -66,7 +72,8 @@ from vllm.utils import is_pin_memory_available
72
from vllm.utils.tensor_schema import TensorSchema, TensorShape
73
74
from .interfaces import (MultiModalEmbeddings, SupportsLoRA,
69
- SupportsMultiModal, SupportsPP, SupportsQuant)
75
+ SupportsMultiModal, SupportsMultiModalPruning,
76
+ SupportsPP, SupportsQuant)
77
from .qwen2_vl import Qwen2VLDummyInputsBuilder as Qwen2_5_VLDummyInputsBuilder
78
from .qwen2_vl import (Qwen2VLMultiModalProcessor, Qwen2VLProcessingInfo,
79
apply_rotary_pos_emb_vision)
@@ -86,9 +93,9 @@ class Qwen2_5_VLImagePixelInputs(TensorSchema):
93
- np: Number of patches
94
- ni: Number of images
95
- cps: Number of channels * patch_size * patch_size
89
-
96
+
97
Historical context:
91
- - pixel_values shape: (num_patches, num_channels * patch_size *
98
+ - pixel_values shape: (num_patches, num_channels * patch_size *
99
patch_size)
100
- image_grid_thw shape: (num_images, 3) in (grid_t, grid_h, grid_w)
101
formatnum_channels * patch_size * patch_size
@@ -112,7 +119,7 @@ class Qwen2_5_VLImageEmbeddingInputs(TensorSchema):
119
- nf: Number of image features
120
- hs: Hidden size
121
- ni: Number of images
115
-
122
+
123
Historical context:
124
- image_embeds shape: (num_image_features, hidden_size)
125
- num_image_features varies based on the number and resolution of the
@@ -143,11 +150,11 @@ class Qwen2_5_VLVideoPixelInputs(TensorSchema):
150
Dimensions:
151
- np: Number of patches
152
- nv: Number of videos
146
- - ctps: Number of channels * temporal_patch_size * patch_size *
153
+ - ctps: Number of channels * temporal_patch_size * patch_size *
154
patch_size
148
-
155
+
156
Historical context:
150
- - pixel_values_videos shape: (num_patches, num_channels *
157
+ - pixel_values_videos shape: (num_patches, num_channels *
158
temporal_patch_size * patch_size * patch_size)
159
- video_grid_thw shape: (num_videos, 3) in (grid_t, grid_h, grid_w)
160
format
@@ -179,7 +186,7 @@ class Qwen2_5_VLVideoEmbeddingInputs(TensorSchema):
186
- nf: Number of video features
187
- hs: Hidden size
188
- nv: Number of videos
182
-
189
+
190
Historical context:
191
- video_embeds shape: (num_video_features, hidden_size)
192
- num_video_features varies based on the number and resolution of the
@@ -905,6 +912,55 @@ class Qwen2_5_VLMultiModalProcessor(Qwen2VLMultiModalProcessor):
912
second_per_grid_ts=MultiModalFieldConfig.batched("video"),
913
)
914
915
+ def _get_prompt_updates(
916
+ self,
917
+ mm_items: MultiModalDataItems,
918
+ hf_processor_mm_kwargs: Mapping[str, Any],
919
+ out_mm_kwargs: MultiModalKwargs,
920
+ ) -> Sequence[PromptUpdate]:
921
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
922
+ image_processor = self.info.get_image_processor(
923
+ **hf_processor_mm_kwargs)
924
+ tokenizer = self.info.get_tokenizer()
925
+ vocab = tokenizer.get_vocab()
926
+
927
+ placeholder = {
928
+ "image": vocab[hf_processor.image_token],
929
+ "video": vocab[hf_processor.video_token],
930
+ }
931
+
932
+ merge_length = image_processor.merge_size**2
933
+
934
+ def get_replacement_qwen2vl(item_idx: int, modality: str):
935
+ out_item = out_mm_kwargs[modality][item_idx]
936
+ grid_thw = out_item[f"{modality}_grid_thw"].data
937
+ assert isinstance(grid_thw, torch.Tensor)
938
+
939
+ num_tokens = int(grid_thw.prod()) // merge_length
940
+
941
+ # EVS-specific code
942
+ video_pruning_rate = self.info.ctx.get_mm_config(
943
+ ).video_pruning_rate
944
+ if (modality == "video" and video_pruning_rate is not None
945
+ and video_pruning_rate > 0.0):
946
+ num_tokens = compute_retained_tokens_count(
947
+ grid_thw,
948
+ image_processor.merge_size,
949
+ video_pruning_rate,
950
+ )
951
+ # End of EVS-specific code
952
+
953
+ return [placeholder[modality]] * num_tokens
954
+
955
+ return [
956
+ PromptReplacement(
957
+ modality=modality,
958
+ target=[placeholder[modality]],
959
+ replacement=partial(get_replacement_qwen2vl,
960
+ modality=modality),
961
+ ) for modality in ("image", "video")
962
+ ]
963
+
964
965
@MULTIMODAL_REGISTRY.register_processor(
966
Qwen2_5_VLMultiModalProcessor,
@@ -912,7 +968,8 @@ class Qwen2_5_VLMultiModalProcessor(Qwen2VLMultiModalProcessor):
968
dummy_inputs=Qwen2_5_VLDummyInputsBuilder)
969
class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
970
SupportsLoRA, SupportsPP,
915
- SupportsQuant):
971
+ SupportsQuant,
972
+ SupportsMultiModalPruning):
973
974
packed_modules_mapping = {
975
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
@@ -949,6 +1006,9 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1006
self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
1007
self.config = config
1008
self.multimodal_config = multimodal_config
1009
+ self.video_pruning_rate = multimodal_config.video_pruning_rate
1010
+ self.is_multimodal_pruning_enabled = (
1011
+ multimodal_config.is_multimodal_pruning_enabled())
1012
1013
if multimodal_config.get_limit_per_prompt("image") or \
1014
multimodal_config.get_limit_per_prompt("video"):
@@ -1090,6 +1150,36 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1150
1151
return image_embeds.split(sizes)
1152
1153
+ def _postprocess_image_embeds_evs(
1154
+ self, image_embeds_split: tuple[torch.Tensor, ...],
1155
+ image_input: Qwen2_5_VLImageInputs) -> tuple[torch.Tensor, ...]:
1156
+ """
1157
+ Append mrope positions for each for images.
1158
+ This is necessary to recover correct mrope
1159
+ positions after video pruning
1160
+
1161
+ Args:
1162
+ image_embeds_split: Tuple of image embeddings for
1163
+ each image item.
1164
+ image_input: Image input data.
1165
+
1166
+ Returns:
1167
+ Tuple of image embeddings for each image item.
1168
+ Resulting embeddings will have extra 4 channels for
1169
+ computed mrope positions.
1170
+ """
1171
+ merge_size = self.visual.spatial_merge_size
1172
+ grid_thw = image_input["image_grid_thw"]
1173
+ grid_thw_list = grid_thw.tolist()
1174
+ image_embeds_out = []
1175
+ for emb, size in zip(image_embeds_split, grid_thw_list):
1176
+ positions = compute_mrope_for_media(size,
1177
+ merge_size).to(emb.device)
1178
+ emb = torch.cat([emb, positions], dim=1)
1179
+ image_embeds_out.append(emb)
1180
+ image_embeds_split = image_embeds_out
1181
+ return tuple(image_embeds_split)
1182
+
1183
def _process_video_input(
1184
self,
1185
video_input: Qwen2_5_VLVideoInputs) -> tuple[torch.Tensor, ...]:
@@ -1119,6 +1209,114 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1209
1210
return video_embeds.split(sizes)
1211
1212
+ def _postprocess_video_embeds_evs(
1213
+ self, video_embeds_split: tuple[torch.Tensor, ...],
1214
+ video_input: Qwen2_5_VLVideoInputs) -> tuple[torch.Tensor, ...]:
1215
+ """
1216
+ Prunes video embeddings via Efficient Video Sampling (EVS)
1217
+ and then appends mrope positions for each retained embeddings
1218
+
1219
+ Args:
1220
+ video_embeds_split: Tuple of video embeddings for each video item.
1221
+ video_input: Video input data.
1222
+
1223
+ Returns:
1224
+ Tuple of video embeddings for each video item.
1225
+ Resulting embeddings will have extra 4 channels for
1226
+ computed mrope positions.
1227
+ """
1228
+ grid_thw = video_input["video_grid_thw"]
1229
+ assert grid_thw.ndim == 2
1230
+ grid_thw_list = grid_thw.tolist()
1231
+ merge_size = self.visual.spatial_merge_size
1232
+
1233
+ # Cast to long to match the original code
1234
+ # https://github.com/huggingface/transformers/blob/41980ce93e775f6c88500c51c8db7946fc6a2add/src/transformers/models/qwen2_5_vl/modular_qwen2_5_vl.py#L491 # noqa
1235
+ second_per_grid_ts = video_input["second_per_grid_ts"].long()
1236
+ tokens_per_second = self.config.vision_config.tokens_per_second
1237
+
1238
+ video_embeds_out = []
1239
+ for emb, size, video_second_per_grid_t in zip(video_embeds_split,
1240
+ grid_thw_list,
1241
+ second_per_grid_ts):
1242
+ # For each video, we compute retention mask using EVS
1243
+ retention_mask = compute_retention_mask(
1244
+ emb,
1245
+ size,
1246
+ spatial_merge_size=self.visual.spatial_merge_size,
1247
+ q=self.video_pruning_rate,
1248
+ )
1249
+ positions = compute_mrope_for_media(
1250
+ size,
1251
+ merge_size,
1252
+ tokens_per_second=tokens_per_second,
1253
+ video_second_per_grid=video_second_per_grid_t.item(),
1254
+ ).to(emb.device)
1255
+
1256
+ emb = emb[retention_mask]
1257
+ positions = positions[retention_mask]
1258
+ emb = torch.cat([emb, positions], dim=1)
1259
+ video_embeds_out.append(emb)
1260
+ return tuple(video_embeds_out)
1261
+
1262
+ def recompute_mrope_positions(
1263
+ self,
1264
+ input_ids: list[int],
1265
+ multimodal_embeddings: tuple[torch.Tensor, ...],
1266
+ mrope_positions: torch.LongTensor,
1267
+ num_computed_tokens: int,
1268
+ ) -> tuple[tuple[torch.Tensor, ...], torch.Tensor, int]:
1269
+ """
1270
+ Update part of input mrope positions (starting with
1271
+ num_computed_tokens index). Original mrope_positions are computed
1272
+ for unpruned sequence and becomes incorrect once pruning occurs,
1273
+ so once we prune media tokens we should reflect this in the
1274
+ mrope_positions before we feed it to LLM.
1275
+
1276
+ Args:
1277
+ input_ids: (N,) All input tokens of the prompt (Containing
1278
+ entire sequence).
1279
+ multimodal_embeddings: Tuple of multimodal embeddings.
1280
+ mrope_positions: Existing mrope positions (3, N) for entire
1281
+ sequence
1282
+ num_computed_tokens: A number of computed tokens so far.
1283
+
1284
+ Returns:
1285
+ Tuple of (multimodal_embeddings, mrope_positions,
1286
+ mrope_position_delta).
1287
+ """
1288
+ image_token_id = self.config.image_token_id
1289
+ video_token_id = self.config.video_token_id
1290
+ vision_start_token_id = self.config.vision_start_token_id
1291
+
1292
+ # Device
1293
+ device = (multimodal_embeddings[0].device
1294
+ if len(multimodal_embeddings) else mrope_positions.device)
1295
+
1296
+ # Tensors
1297
+ input_ids_t = torch.as_tensor(input_ids,
1298
+ device=device,
1299
+ dtype=torch.long)
1300
+
1301
+ # fmt: off
1302
+ mm_embeddings_out = [mm[:, :-4] for mm in
1303
+ multimodal_embeddings]
1304
+ mm_embeddings_pos = [mm[:, -4:].permute(1, 0).long() for mm in
1305
+ multimodal_embeddings]
1306
+ # fmt: in
1307
+
1308
+ positions, mrope_positions_delta = recompute_mrope_positions(
1309
+ input_ids_t,
1310
+ mm_embeddings_pos,
1311
+ mrope_positions,
1312
+ num_computed_tokens,
1313
+ vision_start_token_id,
1314
+ image_token_id,
1315
+ video_token_id,
1316
+ )
1317
+
1318
+ return tuple(mm_embeddings_out), positions, mrope_positions_delta
1319
+
1320
def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
1321
mm_input_by_modality = {}
1322
@@ -1156,9 +1354,17 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1354
multimodal_input = mm_input_by_modality[modality]
1355
if modality == "image":
1356
vision_embeddings = self._process_image_input(multimodal_input)
1357
+ if self.is_multimodal_pruning_enabled:
1358
+ vision_embeddings = self._postprocess_image_embeds_evs(
1359
+ vision_embeddings, multimodal_input
1360
+ )
1361
multimodal_embeddings += vision_embeddings
1362
if modality == "video":
1363
video_embeddings = self._process_video_input(multimodal_input)
1364
+ if self.is_multimodal_pruning_enabled:
1365
+ video_embeddings = self._postprocess_video_embeds_evs(
1366
+ video_embeddings, multimodal_input
1367
+ )
1368
multimodal_embeddings += video_embeddings
1369
return multimodal_embeddings
1370
@@ -1184,6 +1390,10 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1390
inputs_embeds = self.get_input_embeddings(input_ids)
1391
if image_input is not None:
1392
image_embeds = self._process_image_input(image_input)
1393
+ if self.is_multimodal_pruning_enabled:
1394
+ image_embeds = self._postprocess_image_embeds_evs(
1395
+ image_embeds, image_input
1396
+ )
1397
inputs_embeds = merge_multimodal_embeddings(
1398
input_ids,
1399
inputs_embeds,
@@ -1193,6 +1403,10 @@ class Qwen2_5_VLForConditionalGeneration(nn.Module, SupportsMultiModal,
1403
1404
if video_input is not None:
1405
video_embeds = self._process_video_input(video_input)
1406
+ if self.is_multimodal_pruning_enabled:
1407
+ video_embeds = self._postprocess_video_embeds_evs(
1408
+ video_embeds, video_input
1409
+ )
1410
inputs_embeds = merge_multimodal_embeddings(
1411
input_ids,
1412
inputs_embeds,
vllm/multimodal/evs.py
new
+273
@@ -0,0 +1,273 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4
+#
5
+# NVIDIA CORPORATION and its licensors retain all intellectual property
6
+# and proprietary rights in and to this software, related documentation
7
+# and any modifications thereto. Any use, reproduction, disclosure or
8
+# distribution of this software and related documentation without an express
9
+# license agreement from NVIDIA CORPORATION is strictly prohibited.
10
+
11
+import typing
12
+
13
+import torch
14
+
15
+
16
+def compute_retained_tokens_count(video_size_thw: torch.LongTensor,
17
+ spatial_merge_size: int, q: float) -> int:
18
+ """
19
+ Compute the number of retained tokens for a given video.
20
+ Method ensures that we retain all the tokens from the first frame
21
+ regardless of the pruning rate.
22
+
23
+ Args:
24
+ video_size_thw: The size of the video in the format of (T, H, W).
25
+ spatial_merge_size: The size of the spatial merge.
26
+ q: The pruning rate.
27
+
28
+ Returns:
29
+ The number of retained tokens.
30
+ """
31
+ T, H, W = map(int, video_size_thw)
32
+ min_num_tokens = (H // spatial_merge_size) * (W // spatial_merge_size)
33
+ evs_num_tokens = int(T * min_num_tokens * (1 - q))
34
+ return max(min_num_tokens, evs_num_tokens)
35
+
36
+
37
+def compute_retention_mask(
38
+ video_embeds: torch.Tensor,
39
+ video_size_thw: torch.LongTensor,
40
+ spatial_merge_size: int,
41
+ q: float,
42
+) -> torch.Tensor:
43
+ """
44
+ Computes the retention mask for input video embeddings.
45
+
46
+ Args:
47
+ video_embeds (`torch.Tensor`): The input video embeddings
48
+ of shape `(T * H * W // spatial_merge_size ^ 2, hidden_size)`
49
+ video_size_thw (`torch.LongTensor` of shape `(3)`):
50
+ The temporal, height and width of video.
51
+ spatial_merge_size: Size reduction for rows & cols dimensions.
52
+ q: (`float`): Pruning rate factor [0,1)
53
+
54
+ Returns:
55
+ `torch.Tensor`: The retention mask for the video embeddings of
56
+ `(T * H * W // spatial_merge_size ^ 2)` shape.
57
+ """
58
+ T, H, W = video_size_thw
59
+
60
+ # Use reshape instead of einops to avoid graph breaks
61
+ video_embeds = video_embeds.reshape(
62
+ T,
63
+ H // spatial_merge_size,
64
+ W // spatial_merge_size,
65
+ video_embeds.size(-1),
66
+ )
67
+
68
+ # Core EVS
69
+ similarity = torch.nn.functional.cosine_similarity(video_embeds[1:, ...],
70
+ video_embeds[:-1, ...],
71
+ dim=-1)
72
+ dissimilarity = 1 - similarity
73
+
74
+ # Always ensure we include all tokens from the first frame
75
+ dissimilarity = torch.cat(
76
+ [255 * torch.ones_like(video_embeds[:1, :, :, 0]), dissimilarity],
77
+ dim=0)
78
+
79
+ dissimilarity_flat = dissimilarity.view(-1)
80
+ order = torch.argsort(dissimilarity_flat,
81
+ dim=-1,
82
+ descending=True,
83
+ stable=True)
84
+ retain_num_tokens = compute_retained_tokens_count(video_size_thw,
85
+ spatial_merge_size, q)
86
+ topk_indices = order[:retain_num_tokens]
87
+
88
+ retention_mask = torch.zeros_like(dissimilarity_flat, dtype=torch.bool)
89
+ retention_mask[topk_indices] = True
90
+ retention_mask = retention_mask.reshape(dissimilarity.size())
91
+
92
+ mask = retention_mask.view(-1) # "T H W -> (T H W)"
93
+ return mask
94
+
95
+
96
+def compute_mrope_for_media(
97
+ video_size_thw: torch.LongTensor,
98
+ spatial_merge_size: int,
99
+ tokens_per_second: float = 1.0,
100
+ video_second_per_grid: float = 1.0,
101
+) -> torch.Tensor:
102
+ """
103
+ Computes the mrope for video embeddings based on the grid dimensions.
104
+ Computed mrope positions match original qwen 2.5 implementation,
105
+ but positions are built for media being the first element in sequence.
106
+
107
+ Args:
108
+ video_size_thw: Media size (num frames, rows, cols)
109
+ spatial_merge_size: Size reduction for rows & cols dimensions.
110
+ tokens_per_second: Number of tokens per second.
111
+ video_second_per_grid: Number of seconds per video.
112
+
113
+ Returns:
114
+ Tensor of shape `(T * H * W, 4)` where last dimension
115
+ represents mrope positions [0:3), while the last channel
116
+ contains value of llm_grid_w repeated for all positions.
117
+ """
118
+ llm_grid_t = video_size_thw[0]
119
+ llm_grid_h = video_size_thw[1] // spatial_merge_size
120
+ llm_grid_w = video_size_thw[2] // spatial_merge_size
121
+
122
+ t_index = ((torch.arange(llm_grid_t).view(-1, 1).expand(
123
+ -1, llm_grid_h * llm_grid_w).mul(
124
+ tokens_per_second * video_second_per_grid)).long().flatten())
125
+ h_index = (torch.arange(llm_grid_h).view(1, -1,
126
+ 1).expand(llm_grid_t, -1,
127
+ llm_grid_w).flatten())
128
+ w_index = (torch.arange(llm_grid_w).view(1, 1, -1).expand(
129
+ llm_grid_t, llm_grid_h, -1).flatten())
130
+ llm_grid_w = (torch.tensor([llm_grid_w
131
+ ]).view(1, 1,
132
+ 1).expand(llm_grid_t, llm_grid_h,
133
+ llm_grid_w).flatten())
134
+
135
+ positions = torch.stack([t_index, h_index, w_index, llm_grid_w], dim=1)
136
+ return positions
137
+
138
+
139
+def recompute_mrope_positions(
140
+ input_ids: torch.LongTensor,
141
+ multimodal_positions: list[torch.Tensor],
142
+ mrope_positions: torch.LongTensor,
143
+ num_computed_tokens: int,
144
+ vision_start_token_id: int,
145
+ image_token_id: int,
146
+ video_token_id: int,
147
+) -> tuple[torch.LongTensor, int]:
148
+ """
149
+ Update part of input mrope positions.
150
+ Original mrope_positions are computed incorrectly, so once we prune media
151
+ tokens we should reflect this in the mrope positions for the LLM.
152
+
153
+ This method supports chunked prefill approach where
154
+ multimodal_embeddings are passed to LLM in chunks, so input
155
+ multimodal_embeddings may contain zero, some or even some part of all
156
+ multimodal_embeddings for a given prompt.
157
+
158
+ Each multimodal_positions has 4 extra channels
159
+ (First 3 channels corresponds to original 3 mrope positions, last channel
160
+ is the maximum width of the media repeated). Provided multimodal_positions
161
+ do not reflect location of media position in sequence - they are computed
162
+ like the media is in the 0-th position in the sequence.
163
+
164
+ Method works as follows: it recomputes mrope_positions starting from the
165
+ `num_computed_tokens` for `total_len_of_multimodal_embeddings` and then
166
+ shifts all text tokens that goes after total_len_of_multimodal_embeddings.
167
+
168
+ It also handles case when multimodal_embeddings is partial
169
+ (e.g. one media is split into two prefill stages)
170
+
171
+ Args:
172
+ input_ids: (N,) All input tokens of the prompt (entire sequence).
173
+ multimodal_positions: List of mrope positsions for each media.
174
+ mrope_positions: Existing mrope positions (4, N) for entire sequence.
175
+ num_computed_tokens: A number of computed tokens so far.
176
+ vision_start_token_id: Token indicating start of vision media.
177
+ image_token_id: Image token id
178
+ video_token_id: Video token id
179
+
180
+ Returns:
181
+ Tuple of (mrope_positions, mrope_position_delta).
182
+ """
183
+
184
+ # Tensors
185
+ positions: torch.LongTensor = typing.cast(
186
+ torch.LongTensor, mrope_positions.clone()) # (3, N)
187
+ N = input_ids.numel()
188
+
189
+ image_mask = input_ids.eq(image_token_id)
190
+ video_mask = input_ids.eq(video_token_id)
191
+ media_mask = image_mask | video_mask
192
+ text_mask = ~media_mask
193
+
194
+ # Early exit: no media in this chunk
195
+ if len(multimodal_positions) == 0:
196
+ delta = (int((positions.max().item() + 1) -
197
+ N) if positions.numel() else -N)
198
+ return positions, delta
199
+
200
+ total_mm_tokens = torch.count_nonzero(media_mask)
201
+ seen_mm_tokens = torch.count_nonzero(media_mask[:num_computed_tokens])
202
+
203
+ # Early exit: we've updated positions for all media tokens
204
+ # (and consequently - for all remaining text tokens)
205
+ if seen_mm_tokens == total_mm_tokens:
206
+ delta = (int((positions.max().item() + 1) -
207
+ N) if positions.numel() else -N)
208
+ return positions, delta
209
+
210
+ vision_start_indices = (input_ids == vision_start_token_id).nonzero(
211
+ as_tuple=True)[0]
212
+
213
+ for mm_pos in multimodal_positions:
214
+ # Each mm_pos can be a complete embedding for single media
215
+ # or it can be a part of a single media (due to chunked prefill)
216
+
217
+ # Cases to cover
218
+ # - Current prefill chunk has no vision start indexes at all
219
+ # - Vision start token appeared in previous prefill round
220
+ # - Regular case
221
+ seen_vision_start_indices = vision_start_indices[vision_start_indices <
222
+ num_computed_tokens]
223
+
224
+ if len(seen_vision_start_indices):
225
+ # If we have encountered some vision start indexes,
226
+ # then we should check the condition:
227
+ # | --- prefill 1 ------| ---- prefill 2 ----- |
228
+ # | TTTTTTTTTSVVVVVVVVVV|VVVVVVTTTTTTTTTTTTTTTT|
229
+ last_vision_start_token = seen_vision_start_indices[-1]
230
+ seem_mm_tokens_before_last_vision_start = torch.count_nonzero(
231
+ media_mask[:last_vision_start_token])
232
+ in_the_middle_of_media = (
233
+ seen_mm_tokens > seem_mm_tokens_before_last_vision_start)
234
+
235
+ if in_the_middle_of_media:
236
+ mm_embeddings_seen = (seen_mm_tokens -
237
+ seem_mm_tokens_before_last_vision_start)
238
+ global_mm_start = last_vision_start_token
239
+ else:
240
+ # We have completed previous mm_embedding part and
241
+ # ready to start a new one
242
+ next_vision_start_token = vision_start_indices[
243
+ vision_start_indices >= num_computed_tokens][0]
244
+ mm_embeddings_seen = 0
245
+ global_mm_start = next_vision_start_token
246
+
247
+ else:
248
+ # If there were no vision start indexes so far,
249
+ # let's find first vision start index
250
+ next_vision_start_token = vision_start_indices[
251
+ vision_start_indices >= num_computed_tokens][0]
252
+
253
+ mm_embeddings_seen = 0
254
+ global_mm_start = next_vision_start_token
255
+
256
+ # Offset right after vision_start_token
257
+ base = positions[-1, global_mm_start] + 1
258
+ local_start = global_mm_start + 1 + mm_embeddings_seen
259
+ local_end = local_start + mm_pos.shape[1]
260
+ positions[:, local_start:local_end] = mm_pos[0:3] + base
261
+
262
+ # mm_pos[3, 0] is the max width of the media
263
+ offset = mm_pos[3, 0] + base
264
+
265
+ text_pos_sum = torch.cumsum(text_mask[local_end:].long(), dim=0)
266
+
267
+ positions[:, local_end:N] = text_pos_sum + offset - 1
268
+
269
+ # Include distance to the next vision start token
270
+ num_computed_tokens += mm_pos.shape[1]
271
+
272
+ mrope_positions_delta = (positions.max() + 1 - N).item()
273
+ return positions, mrope_positions_delta
vllm/v1/worker/gpu_model_runner.py
+67
-16
@@ -40,11 +40,15 @@ from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
40
from vllm.model_executor.layers.mamba.abstract import MambaBase
41
from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
42
from vllm.model_executor.model_loader import TensorizerLoader, get_model_loader
43
+# yapf conflicts with isort for this block
44
+# yapf: disable
45
from vllm.model_executor.models.interfaces import (SupportsMultiModal,
46
is_mixture_of_experts,
47
supports_eagle3,
48
supports_mrope,
49
+ supports_multimodal_pruning,
50
supports_transcription)
51
+# yapf: enable
52
from vllm.model_executor.models.interfaces_base import (
53
VllmModelForPooling, is_pooling_model, is_text_generation_model)
54
from vllm.multimodal import MULTIMODAL_REGISTRY
@@ -206,7 +210,8 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
210
self.enable_prompt_embeds = model_config.enable_prompt_embeds
211
self.is_multimodal_raw_input_only_model = (
212
model_config.is_multimodal_raw_input_only_model)
209
-
213
+ # This will be overridden in load_model()
214
+ self.is_multimodal_pruning_enabled = False
215
self.max_model_len = model_config.max_model_len
216
self.dcp_world_size = self.parallel_config.decode_context_parallel_size
217
self.max_num_tokens = scheduler_config.max_num_batched_tokens
@@ -1530,29 +1535,47 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1535
# encoder outputs.
1536
model = cast(SupportsMultiModal, self.model)
1537
encoder_outputs = []
1533
- for _, num_items, mm_kwargs_group in group_mm_kwargs_by_modality(
1538
+ for modality, num_items, mm_kwargs_group in group_mm_kwargs_by_modality(
1539
mm_kwargs,
1540
device=self.device,
1541
pin_memory=self.pin_memory,
1542
merge_by_field_config=model.merge_by_field_config,
1543
):
1539
- # Run the encoder.
1540
- # `curr_group_outputs` is either of the following:
1541
- # 1. A tensor of shape (num_items, feature_size, hidden_size)
1542
- # in case feature_size is fixed across all multimodal items.
1543
- # 2. A list or tuple (length: num_items) of tensors, each of shape
1544
- # (feature_size, hidden_size) in case the feature size is dynamic
1545
- # depending on the input multimodal items.
1546
- curr_group_outputs = model.get_multimodal_embeddings(
1547
- **mm_kwargs_group)
1544
+ # (ekhvedchenia): Temporary hack to limit peak memory usage when
1545
+ # processing multimodal data.This solves the issue with scheduler
1546
+ # putting too many video samples into a single batch. Scheduler
1547
+ # uses pruned vision tokens count to compare it versus compute
1548
+ # budget which is incorrect (Either input media size or non-pruned
1549
+ # output vision tokens count should be considered)
1550
+ curr_group_outputs = []
1551
+
1552
+ if self.is_multimodal_pruning_enabled and modality == "video":
1553
+ micro_batch_size = 1
1554
+ for i in range(0, num_items, micro_batch_size):
1555
+ micro_batch_mm_inputs = dict(
1556
+ (k, v[i:i + micro_batch_size])
1557
+ for k, v in mm_kwargs_group.items())
1558
+
1559
+ micro_batch_outputs = model.get_multimodal_embeddings(
1560
+ **micro_batch_mm_inputs)
1561
+
1562
+ curr_group_outputs.extend(micro_batch_outputs)
1563
+ else:
1564
+ # Run the encoder.
1565
+ # `curr_group_outputs` is either of the following:
1566
+ # 1. A tensor of shape (num_items, feature_size, hidden_size)
1567
+ # in case feature_size is fixed across all multimodal items.
1568
+ # 2. A list or tuple (length: num_items) of tensors,
1569
+ # each of shape (feature_size, hidden_size) in case the feature
1570
+ # size is dynamic depending on the input multimodal items.
1571
+ curr_group_outputs = model.get_multimodal_embeddings(
1572
+ **mm_kwargs_group)
1573
1574
sanity_check_mm_encoder_outputs(
1575
curr_group_outputs,
1576
expected_num_items=num_items,
1577
)
1553
-
1554
- for output in curr_group_outputs:
1555
- encoder_outputs.append(output)
1578
+ encoder_outputs.extend(curr_group_outputs)
1579
1580
# Cache the encoder outputs by mm_hash
1581
for (mm_hash, pos_info), output in zip(mm_hashes_pos, encoder_outputs):
@@ -1566,8 +1589,11 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1589
scheduler_output: "SchedulerOutput",
1590
shift_computed_tokens: int = 0,
1591
) -> list[torch.Tensor]:
1592
+ should_sync_mrope_positions = False
1593
mm_embeds: list[torch.Tensor] = []
1594
for req_id in self.input_batch.req_ids:
1595
+ mm_embeds_req: list[torch.Tensor] = []
1596
+
1597
num_scheduled_tokens = scheduler_output.num_scheduled_tokens[
1598
req_id]
1599
req_state = self.requests[req_id]
@@ -1609,7 +1635,28 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1635
encoder_output[start_idx:end_idx],
1636
is_embed=is_embed,
1637
)
1612
- mm_embeds.append(mm_embeds_item)
1638
+ mm_embeds_req.append(mm_embeds_item)
1639
+
1640
+ if self.is_multimodal_pruning_enabled and self.uses_mrope:
1641
+ should_sync_mrope_positions = True
1642
+ mm_embeds_req, new_mrope_positions, new_delta = (
1643
+ self.model.recompute_mrope_positions(
1644
+ input_ids=req_state.prompt_token_ids,
1645
+ multimodal_embeddings=mm_embeds_req,
1646
+ mrope_positions=req_state.mrope_positions,
1647
+ num_computed_tokens=req_state.num_computed_tokens,
1648
+ ))
1649
+ assert req_state.mrope_positions is not None
1650
+ req_state.mrope_positions.copy_(new_mrope_positions)
1651
+ req_state.mrope_position_delta = new_delta
1652
+
1653
+ mm_embeds.extend(mm_embeds_req)
1654
+
1655
+ if should_sync_mrope_positions:
1656
+ self._calc_mrope_positions(scheduler_output)
1657
+ self.mrope_positions.copy_to_gpu(
1658
+ scheduler_output.total_num_scheduled_tokens)
1659
+
1660
return mm_embeds
1661
1662
def _extract_encoder_inputs(
@@ -2589,6 +2636,10 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2636
time_after_load - time_before_load)
2637
prepare_communication_buffer_for_model(self.model)
2638
2639
+ self.is_multimodal_pruning_enabled = (supports_multimodal_pruning(
2640
+ self.model) and self.model_config.multimodal_config.
2641
+ is_multimodal_pruning_enabled())
2642
+
2643
if is_mixture_of_experts(
2644
self.model) and self.parallel_config.enable_eplb:
2645
logger.info("EPLB is enabled for model %s.",
@@ -2843,7 +2894,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2894
Args:
2895
num_tokens: Number of tokens to run the dummy forward pass.
2896
cudagraph_runtime_mode: used to control the behavior.
2846
- - if not set will determine the cudagraph mode based on using
2897
+ - if not set will determine the cudagraph mode based on using
2898
the self.cudagraph_dispatcher.
2899
- CUDAGraphMode.NONE: No cudagraph, for warm up and profile run
2900
- CUDAGraphMode.PIECEWISE: Piecewise cudagraph.