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, 14:45 UTC
[V1] Enable multi-input by default (#15799)
Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
Cyrus Leung committed
Apr 12, 2025 at 16:52 UTC
d9fc8cd9da4a69cb4171efb7cb5a46308680c83c
21 files changed
+214
-105
docs/source/models/supported_models.md
+3
-1
@@ -759,7 +759,7 @@ On the other hand, modalities separated by `/` are mutually exclusive.
759
See [this page](#multimodal-inputs) on how to pass multi-modal inputs to the model.
760
761
:::{important}
762
-To enable multiple multi-modal items per text prompt, you have to set `limit_mm_per_prompt` (offline inference)
762
+**To enable multiple multi-modal items per text prompt in vLLM V0**, you have to set `limit_mm_per_prompt` (offline inference)
763
or `--limit-mm-per-prompt` (online serving). For example, to enable passing up to 4 images per text prompt:
764
765
Offline inference:
@@ -777,6 +777,8 @@ Online serving:
777
vllm serve Qwen/Qwen2-VL-7B-Instruct --limit-mm-per-prompt image=4
778
```
779
780
+**This is no longer required if you are using vLLM V1.**
781
+
782
:::
783
784
:::{note}
docs/source/serving/offline_inference.md
+24
@@ -110,6 +110,30 @@ If you run out of CPU RAM, try the following options:
110
- (Multi-modal models only) you can set the size of multi-modal input cache using `VLLM_MM_INPUT_CACHE_GIB` environment variable (default 4 GiB).
111
- (CPU backend only) you can set the size of KV cache using `VLLM_CPU_KVCACHE_SPACE` environment variable (default 4 GiB).
112
113
+#### Disable unused modalities
114
+
115
+You can disable unused modalities (except for text) by setting its limit to zero.
116
+
117
+For example, if your application only accepts image input, there is no need to allocate any memory for videos.
118
+
119
+```python
120
+from vllm import LLM
121
+
122
+# Accept images but not videos
123
+llm = LLM(model="Qwen/Qwen2.5-VL-3B-Instruct",
124
+ limit_mm_per_prompt={"video": 0})
125
+```
126
+
127
+You can even run a multi-modal model for text-only inference:
128
+
129
+```python
130
+from vllm import LLM
131
+
132
+# Don't accept images. Just text.
133
+llm = LLM(model="google/gemma-3-27b-it",
134
+ limit_mm_per_prompt={"image": 0})
135
+```
136
+
137
### Performance optimization and tuning
138
139
You can potentially improve the performance of vLLM by finetuning various options.
examples/offline_inference/audio_language.py
+5
@@ -196,6 +196,11 @@ def main(args):
196
req_data = model_example_map[model](question_per_audio_count[audio_count],
197
audio_count)
198
199
+ # Disable other modalities to save memory
200
+ default_limits = {"image": 0, "video": 0, "audio": 0}
201
+ req_data.engine_args.limit_mm_per_prompt = default_limits | dict(
202
+ req_data.engine_args.limit_mm_per_prompt or {})
203
+
204
engine_args = asdict(req_data.engine_args) | {"seed": args.seed}
205
llm = LLM(**engine_args)
206
examples/offline_inference/encoder_decoder_multimodal.py
+5
@@ -133,6 +133,11 @@ def main(args):
133
134
req_data = model_example_map[model]()
135
136
+ # Disable other modalities to save memory
137
+ default_limits = {"image": 0, "video": 0, "audio": 0}
138
+ req_data.engine_args.limit_mm_per_prompt = default_limits | dict(
139
+ req_data.engine_args.limit_mm_per_prompt or {})
140
+
141
engine_args = asdict(req_data.engine_args) | {"seed": args.seed}
142
llm = LLM(**engine_args)
143
examples/offline_inference/vision_language.py
+45
-34
@@ -45,7 +45,7 @@ def run_aria(questions: list[str], modality: str) -> ModelRequestData:
45
max_model_len=4096,
46
max_num_seqs=2,
47
dtype="bfloat16",
48
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
48
+ limit_mm_per_prompt={"image": 1},
49
)
50
51
prompts = [(f"<|im_start|>user\n<fim_prefix><|img|><fim_suffix>{question}"
@@ -71,7 +71,7 @@ def run_aya_vision(questions: list[str], modality: str) -> ModelRequestData:
71
max_model_len=2048,
72
max_num_seqs=2,
73
mm_processor_kwargs={"crop_to_patches": True},
74
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
74
+ limit_mm_per_prompt={"image": 1},
75
)
76
prompts = [
77
f"<|START_OF_TURN_TOKEN|><|USER_TOKEN|><image>{question}<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>"
@@ -92,7 +92,7 @@ def run_blip2(questions: list[str], modality: str) -> ModelRequestData:
92
prompts = [f"Question: {question} Answer:" for question in questions]
93
engine_args = EngineArgs(
94
model="Salesforce/blip2-opt-6.7b",
95
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
95
+ limit_mm_per_prompt={"image": 1},
96
)
97
98
return ModelRequestData(
@@ -110,7 +110,7 @@ def run_chameleon(questions: list[str], modality: str) -> ModelRequestData:
110
model="facebook/chameleon-7b",
111
max_model_len=4096,
112
max_num_seqs=2,
113
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
113
+ limit_mm_per_prompt={"image": 1},
114
)
115
116
return ModelRequestData(
@@ -129,8 +129,8 @@ def run_deepseek_vl2(questions: list[str], modality: str) -> ModelRequestData:
129
model=model_name,
130
max_model_len=4096,
131
max_num_seqs=2,
132
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
132
hf_overrides={"architectures": ["DeepseekVLV2ForCausalLM"]},
133
+ limit_mm_per_prompt={"image": 1},
134
)
135
136
prompts = [
@@ -155,7 +155,7 @@ def run_florence2(questions: list[str], modality: str) -> ModelRequestData:
155
max_num_seqs=2,
156
trust_remote_code=True,
157
dtype="bfloat16",
158
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
158
+ limit_mm_per_prompt={"image": 1},
159
)
160
161
prompts = ["<MORE_DETAILED_CAPTION>" for _ in questions]
@@ -175,7 +175,7 @@ def run_fuyu(questions: list[str], modality: str) -> ModelRequestData:
175
model="adept/fuyu-8b",
176
max_model_len=2048,
177
max_num_seqs=2,
178
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
178
+ limit_mm_per_prompt={"image": 1},
179
)
180
181
return ModelRequestData(
@@ -194,7 +194,7 @@ def run_gemma3(questions: list[str], modality: str) -> ModelRequestData:
194
max_model_len=2048,
195
max_num_seqs=2,
196
mm_processor_kwargs={"do_pan_and_scan": True},
197
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
197
+ limit_mm_per_prompt={"image": 1},
198
)
199
200
prompts = [("<bos><start_of_turn>user\n"
@@ -219,7 +219,7 @@ def run_glm4v(questions: list[str], modality: str) -> ModelRequestData:
219
trust_remote_code=True,
220
enforce_eager=True,
221
hf_overrides={"architectures": ["GLM4VForCausalLM"]},
222
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
222
+ limit_mm_per_prompt={"image": 1},
223
)
224
225
prompts = [
@@ -246,7 +246,7 @@ def run_h2ovl(questions: list[str], modality: str) -> ModelRequestData:
246
model=model_name,
247
trust_remote_code=True,
248
max_model_len=8192,
249
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
249
+ limit_mm_per_prompt={"image": 1},
250
)
251
252
tokenizer = AutoTokenizer.from_pretrained(model_name,
@@ -287,7 +287,7 @@ def run_idefics3(questions: list[str], modality: str) -> ModelRequestData:
287
"longest_edge": 3 * 364
288
},
289
},
290
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
290
+ limit_mm_per_prompt={"image": 1},
291
)
292
prompts = [(
293
f"<|begin_of_text|>User:<image>{question}<end_of_utterance>\nAssistant:"
@@ -314,7 +314,7 @@ def run_smolvlm(questions: list[str], modality: str) -> ModelRequestData:
314
"longest_edge": 384
315
},
316
},
317
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
317
+ limit_mm_per_prompt={"image": 1},
318
)
319
prompts = [
320
(f"<|im_start|>User:<image>{question}<end_of_utterance>\nAssistant:")
@@ -337,7 +337,7 @@ def run_internvl(questions: list[str], modality: str) -> ModelRequestData:
337
model=model_name,
338
trust_remote_code=True,
339
max_model_len=4096,
340
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
340
+ limit_mm_per_prompt={"image": 1},
341
)
342
343
tokenizer = AutoTokenizer.from_pretrained(model_name,
@@ -375,7 +375,7 @@ def run_llava(questions: list[str], modality: str) -> ModelRequestData:
375
engine_args = EngineArgs(
376
model="llava-hf/llava-1.5-7b-hf",
377
max_model_len=4096,
378
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
378
+ limit_mm_per_prompt={"image": 1},
379
)
380
381
return ModelRequestData(
@@ -392,7 +392,7 @@ def run_llava_next(questions: list[str], modality: str) -> ModelRequestData:
392
engine_args = EngineArgs(
393
model="llava-hf/llava-v1.6-mistral-7b-hf",
394
max_model_len=8192,
395
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
395
+ limit_mm_per_prompt={"image": 1},
396
)
397
398
return ModelRequestData(
@@ -414,7 +414,7 @@ def run_llava_next_video(questions: list[str],
414
model="llava-hf/LLaVA-NeXT-Video-7B-hf",
415
max_model_len=8192,
416
max_num_seqs=2,
417
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
417
+ limit_mm_per_prompt={"image": 1},
418
)
419
420
return ModelRequestData(
@@ -442,7 +442,7 @@ def run_llava_onevision(questions: list[str],
442
engine_args = EngineArgs(
443
model="llava-hf/llava-onevision-qwen2-7b-ov-hf",
444
max_model_len=16384,
445
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
445
+ limit_mm_per_prompt={"image": 1},
446
)
447
448
return ModelRequestData(
@@ -465,7 +465,7 @@ def run_mantis(questions: list[str], modality: str) -> ModelRequestData:
465
model="TIGER-Lab/Mantis-8B-siglip-llama3",
466
max_model_len=4096,
467
hf_overrides={"architectures": ["MantisForConditionalGeneration"]},
468
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
468
+ limit_mm_per_prompt={"image": 1},
469
)
470
stop_token_ids = [128009]
471
@@ -506,7 +506,7 @@ def run_minicpmv_base(questions: list[str], modality: str, model_name):
506
max_model_len=4096,
507
max_num_seqs=2,
508
trust_remote_code=True,
509
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
509
+ limit_mm_per_prompt={"image": 1},
510
)
511
# NOTE The stop_token_ids are different for various versions of MiniCPM-V
512
# 2.0
@@ -561,7 +561,7 @@ def run_mistral3(questions: list[str], modality: str) -> ModelRequestData:
561
max_model_len=8192,
562
max_num_seqs=2,
563
tensor_parallel_size=2,
564
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
564
+ limit_mm_per_prompt={"image": 1},
565
)
566
567
prompts = [f"<s>[INST]{question}\n[IMG][/INST]" for question in questions]
@@ -587,7 +587,7 @@ def run_mllama(questions: list[str], modality: str) -> ModelRequestData:
587
model=model_name,
588
max_model_len=8192,
589
max_num_seqs=2,
590
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
590
+ limit_mm_per_prompt={"image": 1},
591
)
592
593
tokenizer = AutoTokenizer.from_pretrained(model_name)
@@ -611,7 +611,7 @@ def run_mllama(questions: list[str], modality: str) -> ModelRequestData:
611
)
612
613
614
-def run_llama4(questions: list[str], modality: str):
614
+def run_llama4(questions: list[str], modality: str) -> ModelRequestData:
615
assert modality == "image"
616
617
model_name = "meta-llama/Llama-4-Scout-17B-16E-Instruct"
@@ -621,8 +621,8 @@ def run_llama4(questions: list[str], modality: str):
621
max_model_len=8192,
622
max_num_seqs=4,
623
tensor_parallel_size=8,
624
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
624
gpu_memory_utilization=0.4,
625
+ limit_mm_per_prompt={"image": 1},
626
)
627
628
tokenizer = AutoTokenizer.from_pretrained(model_name)
@@ -657,7 +657,7 @@ def run_molmo(questions: list[str], modality: str) -> ModelRequestData:
657
model=model_name,
658
trust_remote_code=True,
659
dtype="bfloat16",
660
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
660
+ limit_mm_per_prompt={"image": 1},
661
)
662
663
prompts = [
@@ -683,7 +683,7 @@ def run_nvlm_d(questions: list[str], modality: str) -> ModelRequestData:
683
trust_remote_code=True,
684
max_model_len=4096,
685
tensor_parallel_size=4,
686
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
686
+ limit_mm_per_prompt={"image": 1},
687
)
688
689
tokenizer = AutoTokenizer.from_pretrained(model_name,
@@ -710,7 +710,8 @@ def run_paligemma(questions: list[str], modality: str) -> ModelRequestData:
710
prompts = ["caption en" for _ in questions]
711
engine_args = EngineArgs(
712
model="google/paligemma-3b-mix-224",
713
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache)
713
+ limit_mm_per_prompt={"image": 1},
714
+ )
715
716
return ModelRequestData(
717
engine_args=engine_args,
@@ -726,7 +727,8 @@ def run_paligemma2(questions: list[str], modality: str) -> ModelRequestData:
727
prompts = ["caption en" for _ in questions]
728
engine_args = EngineArgs(
729
model="google/paligemma2-3b-ft-docci-448",
729
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache)
730
+ limit_mm_per_prompt={"image": 1},
731
+ )
732
733
return ModelRequestData(
734
engine_args=engine_args,
@@ -762,7 +764,7 @@ def run_phi3v(questions: list[str], modality: str) -> ModelRequestData:
764
max_num_seqs=2,
765
# Note - mm_processor_kwargs can also be passed to generate/chat calls
766
mm_processor_kwargs={"num_crops": 16},
765
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
767
+ limit_mm_per_prompt={"image": 1},
768
)
769
770
return ModelRequestData(
@@ -793,6 +795,7 @@ def run_phi4mm(questions: list[str], modality: str) -> ModelRequestData:
795
max_num_seqs=2,
796
enable_lora=True,
797
max_lora_rank=320,
798
+ limit_mm_per_prompt={"image": 1},
799
)
800
801
return ModelRequestData(
@@ -813,7 +816,7 @@ def run_pixtral_hf(questions: list[str], modality: str) -> ModelRequestData:
816
model=model_name,
817
max_model_len=6144,
818
max_num_seqs=2,
816
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
819
+ limit_mm_per_prompt={"image": 1},
820
)
821
822
prompts = [f"<s>[INST]{question}\n[IMG][/INST]" for question in questions]
@@ -834,7 +837,7 @@ def run_qwen_vl(questions: list[str], modality: str) -> ModelRequestData:
837
max_model_len=1024,
838
max_num_seqs=2,
839
hf_overrides={"architectures": ["QwenVLForConditionalGeneration"]},
837
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
840
+ limit_mm_per_prompt={"image": 1},
841
)
842
843
prompts = [f"{question}Picture 1: <img></img>\n" for question in questions]
@@ -859,7 +862,7 @@ def run_qwen2_vl(questions: list[str], modality: str) -> ModelRequestData:
862
"min_pixels": 28 * 28,
863
"max_pixels": 1280 * 28 * 28,
864
},
862
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
865
+ limit_mm_per_prompt={"image": 1},
866
)
867
868
if modality == "image":
@@ -894,7 +897,7 @@ def run_qwen2_5_vl(questions: list[str], modality: str) -> ModelRequestData:
897
"max_pixels": 1280 * 28 * 28,
898
"fps": 1,
899
},
897
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
900
+ limit_mm_per_prompt={"image": 1},
901
)
902
903
if modality == "image":
@@ -925,7 +928,7 @@ def run_skyworkr1v(questions: list[str], modality: str) -> ModelRequestData:
928
model=model_name,
929
trust_remote_code=True,
930
max_model_len=4096,
928
- disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
931
+ limit_mm_per_prompt={"image": 1},
932
)
933
934
tokenizer = AutoTokenizer.from_pretrained(model_name,
@@ -1082,7 +1085,15 @@ def main(args):
1085
1086
req_data = model_example_map[model](questions, modality)
1087
1085
- engine_args = asdict(req_data.engine_args) | {"seed": args.seed}
1088
+ # Disable other modalities to save memory
1089
+ default_limits = {"image": 0, "video": 0, "audio": 0}
1090
+ req_data.engine_args.limit_mm_per_prompt = default_limits | dict(
1091
+ req_data.engine_args.limit_mm_per_prompt or {})
1092
+
1093
+ engine_args = asdict(req_data.engine_args) | {
1094
+ "seed": args.seed,
1095
+ "disable_mm_preprocessor_cache": args.disable_mm_preprocessor_cache,
1096
+ }
1097
llm = LLM(**engine_args)
1098
1099
# To maintain code compatibility in this script, we add LoRA here.
examples/offline_inference/vision_language_embedding.py
+7
@@ -63,6 +63,7 @@ def run_e5_v(query: Query) -> ModelRequestData:
63
model="royokong/e5-v",
64
task="embed",
65
max_model_len=4096,
66
+ limit_mm_per_prompt={"image": 1},
67
)
68
69
return ModelRequestData(
@@ -93,6 +94,7 @@ def run_vlm2vec(query: Query) -> ModelRequestData:
94
task="embed",
95
trust_remote_code=True,
96
mm_processor_kwargs={"num_crops": 4},
97
+ limit_mm_per_prompt={"image": 1},
98
)
99
100
return ModelRequestData(
@@ -131,6 +133,11 @@ def run_encode(model: str, modality: QueryModality, seed: Optional[int]):
133
query = get_query(modality)
134
req_data = model_example_map[model](query)
135
136
+ # Disable other modalities to save memory
137
+ default_limits = {"image": 0, "video": 0, "audio": 0}
138
+ req_data.engine_args.limit_mm_per_prompt = default_limits | dict(
139
+ req_data.engine_args.limit_mm_per_prompt or {})
140
+
141
engine_args = asdict(req_data.engine_args) | {"seed": seed}
142
llm = LLM(**engine_args)
143
examples/offline_inference/vision_language_multi_image.py
+5
@@ -687,6 +687,11 @@ def run_chat(model: str, question: str, image_urls: list[str],
687
seed: Optional[int]):
688
req_data = model_example_map[model](question, image_urls)
689
690
+ # Disable other modalities to save memory
691
+ default_limits = {"image": 0, "video": 0, "audio": 0}
692
+ req_data.engine_args.limit_mm_per_prompt = default_limits | dict(
693
+ req_data.engine_args.limit_mm_per_prompt or {})
694
+
695
engine_args = asdict(req_data.engine_args) | {"seed": seed}
696
llm = LLM(**engine_args)
697
tests/entrypoints/openai/test_audio.py
+34
-27
@@ -12,7 +12,9 @@ from ...utils import RemoteOpenAIServer
12
MODEL_NAME = "fixie-ai/ultravox-v0_5-llama-3_2-1b"
13
TEST_AUDIO_URLS = [
14
AudioAsset("winning_call").url,
15
+ AudioAsset("mary_had_lamb").url,
16
]
17
+MAXIMUM_AUDIOS = 2
18
19
20
@pytest.fixture(scope="module")
@@ -24,6 +26,8 @@ def server():
26
"5",
27
"--enforce-eager",
28
"--trust-remote-code",
29
+ "--limit-mm-per-prompt",
30
+ f"audio={MAXIMUM_AUDIOS}",
31
]
32
33
with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
@@ -46,7 +50,7 @@ def base64_encoded_audio() -> dict[str, str]:
50
51
@pytest.mark.asyncio
52
@pytest.mark.parametrize("model_name", [MODEL_NAME])
49
-@pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
53
+@pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
54
async def test_single_chat_session_audio(client: openai.AsyncOpenAI,
55
model_name: str, audio_url: str):
56
messages = [{
@@ -100,7 +104,7 @@ async def test_single_chat_session_audio(client: openai.AsyncOpenAI,
104
105
@pytest.mark.asyncio
106
@pytest.mark.parametrize("model_name", [MODEL_NAME])
103
-@pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
107
+@pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
108
async def test_single_chat_session_audio_base64encoded(
109
client: openai.AsyncOpenAI, model_name: str, audio_url: str,
110
base64_encoded_audio: dict[str, str]):
@@ -158,7 +162,7 @@ async def test_single_chat_session_audio_base64encoded(
162
163
@pytest.mark.asyncio
164
@pytest.mark.parametrize("model_name", [MODEL_NAME])
161
-@pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
165
+@pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
166
async def test_single_chat_session_input_audio(
167
client: openai.AsyncOpenAI, model_name: str, audio_url: str,
168
base64_encoded_audio: dict[str, str]):
@@ -330,28 +334,21 @@ async def test_chat_streaming_input_audio(client: openai.AsyncOpenAI,
334
335
@pytest.mark.asyncio
336
@pytest.mark.parametrize("model_name", [MODEL_NAME])
333
-@pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
337
+@pytest.mark.parametrize(
338
+ "audio_urls", [TEST_AUDIO_URLS, TEST_AUDIO_URLS + [TEST_AUDIO_URLS[0]]])
339
async def test_multi_audio_input(client: openai.AsyncOpenAI, model_name: str,
335
- audio_url: str,
336
- base64_encoded_audio: dict[str, str]):
340
+ audio_urls: list[str]):
341
342
messages = [{
343
"role":
344
"user",
345
"content": [
342
- {
346
+ *({
347
"type": "audio_url",
348
"audio_url": {
349
"url": audio_url
350
}
347
- },
348
- {
349
- "type": "input_audio",
350
- "input_audio": {
351
- "data": base64_encoded_audio[audio_url],
352
- "format": "wav"
353
- }
354
- },
351
+ } for audio_url in audio_urls),
352
{
353
"type": "text",
354
"text": "What's happening in this audio?"
@@ -359,20 +356,30 @@ async def test_multi_audio_input(client: openai.AsyncOpenAI, model_name: str,
356
],
357
}]
358
362
- with pytest.raises(openai.BadRequestError): # test multi-audio input
363
- await client.chat.completions.create(
359
+ if len(audio_urls) > MAXIMUM_AUDIOS:
360
+ with pytest.raises(openai.BadRequestError): # test multi-audio input
361
+ await client.chat.completions.create(
362
+ model=model_name,
363
+ messages=messages,
364
+ max_completion_tokens=10,
365
+ temperature=0.0,
366
+ )
367
+
368
+ # the server should still work afterwards
369
+ completion = await client.completions.create(
370
+ model=model_name,
371
+ prompt=[0, 0, 0, 0, 0],
372
+ max_tokens=5,
373
+ temperature=0.0,
374
+ )
375
+ completion = completion.choices[0].text
376
+ assert completion is not None and len(completion) >= 0
377
+ else:
378
+ chat_completion = await client.chat.completions.create(
379
model=model_name,
380
messages=messages,
381
max_completion_tokens=10,
382
temperature=0.0,
383
)
369
-
370
- # the server should still work afterwards
371
- completion = await client.completions.create(
372
- model=model_name,
373
- prompt=[0, 0, 0, 0, 0],
374
- max_tokens=5,
375
- temperature=0.0,
376
- )
377
- completion = completion.choices[0].text
378
- assert completion is not None and len(completion) >= 0
384
+ message = chat_completion.choices[0].message
385
+ assert message.content is not None and len(message.content) >= 0
tests/models/decoder_only/vision_language/vlm_utils/core.py
+4
@@ -51,6 +51,10 @@ def run_test(
51
model_info.check_available_online(on_fail="skip")
52
model_info.check_transformers_version(on_fail="skip")
53
54
+ # Disable other modalities to save memory
55
+ default_limits = {"image": 0, "video": 0, "audio": 0}
56
+ limit_mm_per_prompt = default_limits | limit_mm_per_prompt
57
+
58
vllm_outputs_per_mm = []
59
hf_outputs_per_mm = []
60
tests/models/test_oot_registration.py
+1
@@ -90,6 +90,7 @@ def test_oot_registration_multimodal(
90
max_model_len=4096,
91
enforce_eager=True,
92
limit_mm_per_prompt={"image": 1})
93
+
94
first_token = llm.get_tokenizer().decode(0)
95
outputs = llm.generate(prompts, sampling_params)
96
tests/multimodal/test_processing.py
+5
-2
@@ -972,10 +972,13 @@ def test_limit_mm_per_prompt_dummy(model_id, limit, num_supported, is_valid):
972
if is_valid:
973
exc_ctx = nullcontext()
974
else:
975
- exc_ctx = pytest.raises(ValueError, match="this model only supports")
975
+ exc_ctx = pytest.raises(ValueError, match="The model only supports")
976
977
with exc_ctx:
978
- profiler.get_decoder_dummy_data(model_config.max_model_len)
978
+ profiler.get_decoder_dummy_data(
979
+ model_config.max_model_len,
980
+ mm_counts=limit_mm_per_prompt,
981
+ )
982
983
984
@pytest.mark.parametrize("model_id", ["llava-hf/llava-v1.6-mistral-7b-hf"])
vllm/config.py
+9
-3
@@ -2667,14 +2667,20 @@ class MultiModalConfig:
2667
usedforsecurity=False).hexdigest()
2668
return hash_str
2669
2670
+ def get_default_limit_per_prompt(self) -> int:
2671
+ """
2672
+ Return the default number of input items allowed per prompt
2673
+ for any modality if not specified by the user.
2674
+ """
2675
+ return 999 if envs.VLLM_USE_V1 else 1
2676
+
2677
def get_limit_per_prompt(self, modality: str) -> int:
2678
"""
2679
Get the maximum number of input items allowed per prompt
2680
for the given modality.
2674
-
2675
- If not set by the user, this defaults to `1`.
2681
"""
2677
- return self.limit_per_prompt.get(modality, 1)
2682
+ default = self.get_default_limit_per_prompt()
2683
+ return self.limit_per_prompt.get(modality, default)
2684
2685
# TODO: Add configs to init vision tower or not.
2686
vllm/engine/arg_utils.py
+3
-3
@@ -671,13 +671,13 @@ class EngineArgs:
671
type=nullable_kvs,
672
default=EngineArgs.limit_mm_per_prompt,
673
# The default value is given in
674
- # MultiModalConfig.get_limit_per_prompt
674
+ # MultiModalConfig.get_default_limit_per_prompt
675
help=('For each multimodal plugin, limit how many '
676
'input instances to allow for each prompt. '
677
'Expects a comma-separated list of items, '
678
'e.g.: `image=16,video=2` allows a maximum of 16 '
679
- 'images and 2 videos per prompt. Defaults to 1 for '
680
- 'each modality.'))
679
+ 'images and 2 videos per prompt. Defaults to '
680
+ '1 (V0) or 999 (V1) for each modality.'))
681
parser.add_argument(
682
'--mm-processor-kwargs',
683
default=None,
vllm/entrypoints/chat_utils.py
+24
-5
@@ -35,7 +35,7 @@ from typing_extensions import Required, TypeAlias, TypedDict
35
36
from vllm.config import ModelConfig
37
from vllm.logger import init_logger
38
-from vllm.multimodal import MultiModalDataDict
38
+from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalDataDict
39
from vllm.multimodal.utils import MediaConnector
40
from vllm.transformers_utils.processor import cached_get_processor
41
from vllm.transformers_utils.tokenizer import AnyTokenizer, MistralTokenizer
@@ -452,8 +452,6 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
452
453
self._model_config = model_config
454
self._tokenizer = tokenizer
455
- self._allowed_items = (model_config.multimodal_config.limit_per_prompt
456
- if model_config.multimodal_config else {})
455
456
self._items_by_modality = defaultdict[str, list[_T]](list)
457
@@ -465,6 +463,10 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
463
def allowed_local_media_path(self):
464
return self._model_config.allowed_local_media_path
465
466
+ @property
467
+ def mm_registry(self):
468
+ return MULTIMODAL_REGISTRY
469
+
470
@staticmethod
471
@cache
472
def _cached_token_str(tokenizer: AnyTokenizer, token_index: int) -> str:
@@ -540,12 +542,29 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
542
Add a multi-modal item to the current prompt and returns the
543
placeholder string to use, if any.
544
"""
543
- allowed_count = self._allowed_items.get(modality, 1)
545
+ mm_registry = self.mm_registry
546
+ model_config = self.model_config
547
+
548
+ input_modality = modality.replace("_embeds", "")
549
+
550
+ if mm_registry.has_processor(model_config):
551
+ mm_processor = mm_registry.create_processor(model_config)
552
+ allowed_counts = mm_processor.info.get_allowed_mm_limits()
553
+ allowed_count = allowed_counts.get(input_modality, 0)
554
+ else:
555
+ mm_config = model_config.multimodal_config
556
+ if mm_config is None:
557
+ msg = "This model does not support multi-modal inputs"
558
+ raise ValueError(msg)
559
+
560
+ allowed_count = mm_config.get_limit_per_prompt(input_modality)
561
+
562
current_count = len(self._items_by_modality[modality]) + 1
563
if current_count > allowed_count:
564
raise ValueError(
565
f"At most {allowed_count} {modality}(s) may be provided in "
548
- "one request.")
566
+ "one request. You can set `--limit-mm-per-prompt` to "
567
+ "increase this limit if the model supports it.")
568
569
self._items_by_modality[modality].append(item)
570
vllm/model_executor/models/minicpmo.py
+1
-1
@@ -126,7 +126,7 @@ class MiniCPMOMultiModalDataParser(MiniCPMVMultiModalDataParser):
126
def _parse_audio_data(
127
self,
128
data: Union[dict[str, torch.Tensor], ModalityData[AudioItem]],
129
- ) -> ModalityDataItems[Any, Any]:
129
+ ) -> Optional[ModalityDataItems[Any, Any]]:
130
if isinstance(data, dict):
131
return MiniCPMOAudioEmbeddingItems(
132
data,
vllm/model_executor/models/minicpmv.py
+2
-2
@@ -290,7 +290,7 @@ class MiniCPMVMultiModalDataParser(MultiModalDataParser):
290
def _parse_image_data(
291
self,
292
data: Union[dict[str, torch.Tensor], ModalityData[ImageItem]],
293
- ) -> ModalityDataItems[Any, Any]:
293
+ ) -> Optional[ModalityDataItems[Any, Any]]:
294
if isinstance(data, dict):
295
return MiniCPMVImageEmbeddingItems(
296
data,
@@ -302,7 +302,7 @@ class MiniCPMVMultiModalDataParser(MultiModalDataParser):
302
def _parse_video_data(
303
self,
304
data: Union[dict[str, torch.Tensor], ModalityData[VideoItem]],
305
- ) -> ModalityDataItems[Any, Any]:
305
+ ) -> Optional[ModalityDataItems[Any, Any]]:
306
if isinstance(data, dict):
307
return MiniCPMVVideoEmbeddingItems(
308
data,
vllm/model_executor/models/qwen2_vl.py
+2
-2
@@ -720,7 +720,7 @@ class Qwen2VLMultiModalDataParser(MultiModalDataParser):
720
def _parse_image_data(
721
self,
722
data: Union[dict[str, torch.Tensor], ModalityData[ImageItem]],
723
- ) -> ModalityDataItems[Any, Any]:
723
+ ) -> Optional[ModalityDataItems[Any, Any]]:
724
if isinstance(data, dict):
725
return DictEmbeddingItems(
726
data,
@@ -734,7 +734,7 @@ class Qwen2VLMultiModalDataParser(MultiModalDataParser):
734
def _parse_video_data(
735
self,
736
data: Union[dict[str, torch.Tensor], ModalityData[VideoItem]],
737
- ) -> ModalityDataItems[Any, Any]:
737
+ ) -> Optional[ModalityDataItems[Any, Any]]:
738
if isinstance(data, dict):
739
return DictEmbeddingItems(
740
data,
vllm/multimodal/processing.py
+29
-5
@@ -1034,6 +1034,20 @@ class BaseProcessingInfo:
1034
"""
1035
raise NotImplementedError
1036
1037
+ def get_allowed_mm_limits(self) -> Mapping[str, int]:
1038
+ """Return the maximum allowed number of items for each modality."""
1039
+ supported_mm_limits = self.get_supported_mm_limits()
1040
+ mm_config = self.ctx.get_mm_config()
1041
+
1042
+ allowed_limits = dict[str, int]()
1043
+ for modality, supported_limit in supported_mm_limits.items():
1044
+ user_limit = mm_config.get_limit_per_prompt(modality)
1045
+
1046
+ allowed_limits[modality] = (user_limit if supported_limit is None
1047
+ else min(user_limit, supported_limit))
1048
+
1049
+ return allowed_limits
1050
+
1051
1052
_I = TypeVar("_I", bound=BaseProcessingInfo)
1053
@@ -1087,14 +1101,24 @@ class BaseMultiModalProcessor(ABC, Generic[_I]):
1101
before passing them to :meth:`_get_hf_mm_data`.
1102
"""
1103
mm_items = self.data_parser.parse_mm_data(mm_data)
1090
- mm_config = self.info.ctx.get_mm_config()
1104
+ supported_mm_limits = self.info.get_supported_mm_limits()
1105
+ allowed_mm_limits = self.info.get_allowed_mm_limits()
1106
1107
for modality, items in mm_items.items():
1093
- limit = mm_config.get_limit_per_prompt(modality)
1094
- if len(items) > limit:
1108
+ supported_limit = supported_mm_limits.get(modality, 0)
1109
+ allowed_limit = allowed_mm_limits.get(modality, 0)
1110
+ num_items = len(items)
1111
+
1112
+ if supported_limit is not None and num_items > supported_limit:
1113
+ raise ValueError(
1114
+ f"The model only supports at most {supported_limit} "
1115
+ f"{modality} items, but you passed {num_items} "
1116
+ f"{modality} items in the same prompt.")
1117
+
1118
+ if num_items > allowed_limit:
1119
raise ValueError(
1096
- f"You set {modality}={limit} (or defaulted to 1) in "
1097
- f"`--limit-mm-per-prompt`, but passed {len(items)} "
1120
+ f"You set or defaulted to {modality}={allowed_limit} "
1121
+ f"in --limit-mm-per-prompt`, but passed {num_items} "
1122
f"{modality} items in the same prompt.")
1123
1124
return mm_items
vllm/multimodal/profiling.py
+1
-17
@@ -162,23 +162,7 @@ class MultiModalProfiler(Generic[_I]):
162
return self.processor.dummy_inputs
163
164
def get_mm_limits(self) -> Mapping[str, int]:
165
- mm_config = self.processing_info.ctx.get_mm_config()
166
- supported_mm_limits = self.processing_info.get_supported_mm_limits()
167
-
168
- mm_limits = {
169
- modality: mm_config.get_limit_per_prompt(modality)
170
- for modality in supported_mm_limits
171
- }
172
-
173
- for modality, supported_limit in supported_mm_limits.items():
174
- limit = mm_limits[modality]
175
- if supported_limit is not None and supported_limit < limit:
176
- raise ValueError(
177
- f"You set {modality}={limit} (or defaulted to 1) in "
178
- f"`--limit-mm-per-prompt`, but this model only supports "
179
- f"at most {supported_limit} {modality} items.")
180
-
181
- return mm_limits
165
+ return self.processing_info.get_allowed_mm_limits()
166
167
def _get_dummy_mm_inputs(
168
self,
vllm/multimodal/registry.py
+4
-2
@@ -265,8 +265,10 @@ class MultiModalRegistry:
265
266
return profiler.get_mm_max_tokens(
267
seq_len,
268
- {modality: 1
269
- for modality in mm_limits},
268
+ {
269
+ modality: 1
270
+ for modality, limit in mm_limits.items() if limit > 0
271
+ },
272
)
273
274
return {
vllm/multimodal/utils.py
+1
-1
@@ -264,7 +264,7 @@ fetch_video = global_media_connector.fetch_video
264
265
def encode_audio_base64(
266
audio: np.ndarray,
267
- sampling_rate: int,
267
+ sampling_rate: float,
268
) -> str:
269
"""Encode audio as base64."""
270
audio_io = AudioMediaIO()