@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 4ebc0b9640
Fork 0

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

[Bugfix] Proper input validation for multi-modal encoder-decoder models (#16156)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>

Cyrus Leung committed Apr 9, 2025 at 00:45 UTC 4ebc0b96401ab908e72f894138de154efbfdffd6
10 files changed +113 -62
examples/offline_inference/encoder_decoder_multimodal.py
+1 -1
@@ -56,7 +56,7 @@ def run_florence2():
56 def run_mllama():
57 engine_args = EngineArgs(
58 model="meta-llama/Llama-3.2-11B-Vision-Instruct",
59 - max_model_len=4096,
59 + max_model_len=8192,
60 max_num_seqs=2,
61 limit_mm_per_prompt={"image": 1},
62 dtype="half",
examples/offline_inference/vision_language.py
+1 -1
@@ -556,7 +556,7 @@ def run_mllama(questions: list[str], modality: str) -> ModelRequestData:
556 # The configuration below has been confirmed to launch on a single L40 GPU.
557 engine_args = EngineArgs(
558 model=model_name,
559 - max_model_len=4096,
559 + max_model_len=8192,
560 max_num_seqs=2,
561 disable_mm_preprocessor_cache=args.disable_mm_preprocessor_cache,
562 )
examples/offline_inference/vision_language_multi_image.py
+2 -2
@@ -318,8 +318,8 @@ def load_mllama(question: str, image_urls: list[str]) -> ModelRequestData:
318 # The configuration below has been confirmed to launch on a single L40 GPU.
319 engine_args = EngineArgs(
320 model=model_name,
321 - max_model_len=4096,
322 - max_num_seqs=16,
321 + max_model_len=8192,
322 + max_num_seqs=2,
323 limit_mm_per_prompt={"image": len(image_urls)},
324 )
325
tests/engine/test_short_mm_context.py
+2 -1
@@ -18,7 +18,8 @@ models = ["llava-hf/llava-1.5-7b-hf"]
18 def test_context_length_too_short(vllm_runner, image_assets, model):
19 images = [asset.pil_image for asset in image_assets]
20
21 - with pytest.raises(ValueError, match="too long to fit into the model"):
21 + with pytest.raises(ValueError,
22 + match="longer than the maximum model length"):
23 vllm_model = vllm_runner(
24 model,
25 max_model_len=128, # LLaVA has a feature size of 576
tests/entrypoints/llm/test_prompt_validation.py
+1 -1
@@ -15,7 +15,7 @@ def v1(run_with_both_engines):
15
16 def test_empty_prompt():
17 llm = LLM(model="openai-community/gpt2", enforce_eager=True)
18 - with pytest.raises(ValueError, match='Prompt cannot be empty'):
18 + with pytest.raises(ValueError, match='decoder prompt cannot be empty'):
19 llm.generate([""])
20
21
tests/entrypoints/openai/test_prompt_validation.py
+1 -1
@@ -17,7 +17,7 @@ async def test_empty_prompt():
17 client = remote_server.get_async_client()
18
19 with pytest.raises(openai.BadRequestError,
20 - match=re.compile('.+Prompt cannot be empty.+')):
20 + match="decoder prompt cannot be empty"):
21 await client.completions.create(model=model_name,
22 prompt="",
23 max_tokens=5,
tests/models/encoder_decoder/vision_language/test_mllama.py
+4 -4
@@ -211,7 +211,7 @@ def _run_test(
211 # max_model_len should be greater than image_feature_size
212 with vllm_runner(model,
213 dtype=dtype,
214 - max_model_len=4096,
214 + max_model_len=8192,
215 max_num_seqs=3,
216 tensor_parallel_size=tensor_parallel_size,
217 distributed_executor_backend=distributed_executor_backend,
@@ -422,7 +422,7 @@ def test_bnb_regression(
422 llm = LLM(
423 model=model,
424 dtype=dtype,
425 - max_model_len=4096,
425 + max_model_len=8192,
426 max_num_seqs=2,
427 quantization="bitsandbytes",
428 )
@@ -475,7 +475,7 @@ def test_explicit_implicit_prompt(
475 llm = LLM(
476 model=model,
477 dtype=dtype,
478 - max_model_len=4096,
478 + max_model_len=8192,
479 max_num_seqs=2,
480 tensor_parallel_size=1,
481 )
@@ -506,7 +506,7 @@ def test_regression(vllm_runner, image_assets, model, dtype, max_tokens,
506 with global_force_attn_backend_context_manager(attn_backend), vllm_runner(
507 model,
508 dtype=dtype,
509 - max_model_len=4096,
509 + max_model_len=8192,
510 max_num_seqs=2,
511 tensor_parallel_size=1,
512 limit_mm_per_prompt={"image":
vllm/engine/llm_engine.py
+46 -17
@@ -8,7 +8,7 @@ from contextlib import contextmanager
8 from dataclasses import dataclass
9 from functools import partial
10 from typing import (TYPE_CHECKING, Any, Callable, ClassVar, Deque, Dict,
11 - Iterable, List, Mapping, NamedTuple, Optional)
11 + Iterable, List, Literal, Mapping, NamedTuple, Optional)
12 from typing import Sequence as GenericSequence
13 from typing import Set, Type, Union, cast, overload
14
@@ -30,7 +30,7 @@ from vllm.entrypoints.openai.logits_processors import (
30 get_logits_processors as get_openai_logits_processors)
31 from vllm.executor.executor_base import ExecutorBase
32 from vllm.inputs import (INPUT_REGISTRY, InputRegistry, ProcessorInputs,
33 - PromptType)
33 + PromptType, SingletonInputs)
34 from vllm.inputs.parse import is_token_prompt, split_enc_dec_inputs
35 from vllm.inputs.preprocess import InputPreprocessor
36 from vllm.logger import init_logger
@@ -40,6 +40,7 @@ from vllm.model_executor.guided_decoding import (
40 get_local_guided_decoding_logits_processor)
41 from vllm.model_executor.layers.sampler import SamplerOutput
42 from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
43 +from vllm.multimodal.processing import EncDecMultiModalProcessor
44 from vllm.outputs import (PoolingRequestOutput, RequestOutput,
45 RequestOutputFactory)
46 from vllm.pooling_params import PoolingParams
@@ -2029,29 +2030,57 @@ class LLMEngine:
2030 lora_request: Optional[LoRARequest]):
2031 encoder_inputs, decoder_inputs = split_enc_dec_inputs(inputs)
2032
2032 - # For encoder-decoder multimodal models, the max_prompt_len
2033 - # restricts the decoder prompt length
2034 - if self.model_config.is_multimodal_model:
2035 - prompt_inputs = decoder_inputs
2036 - else:
2037 - prompt_inputs = encoder_inputs or decoder_inputs
2033 + if encoder_inputs is not None:
2034 + self._validate_model_input(encoder_inputs,
2035 + lora_request,
2036 + prompt_type="encoder")
2037
2039 - prompt_ids = prompt_inputs["prompt_token_ids"]
2038 + self._validate_model_input(decoder_inputs,
2039 + lora_request,
2040 + prompt_type="decoder")
2041
2041 - if prompt_ids is None or len(prompt_ids) == 0:
2042 - raise ValueError("Prompt cannot be empty")
2042 + def _validate_model_input(
2043 + self,
2044 + prompt_inputs: SingletonInputs,
2045 + lora_request: Optional[LoRARequest],
2046 + *,
2047 + prompt_type: Literal["encoder", "decoder"],
2048 + ):
2049 + if prompt_type == "encoder" and self.tokenizer is not None:
2050 + tokenizer = self.tokenizer.get_lora_tokenizer(lora_request)
2051 + model_config = self.model_config
2052
2044 - if self.model_config.is_multimodal_model:
2045 - max_prompt_len = self.model_config.max_model_len
2053 + if model_config.is_multimodal_model:
2054 + mm_registry = self.input_preprocessor.mm_registry
2055 + mm_processor = mm_registry.create_processor(
2056 + model_config, tokenizer=tokenizer)
2057 + assert isinstance(mm_processor, EncDecMultiModalProcessor)
2058
2047 - if len(prompt_ids) > max_prompt_len:
2048 - raise ValueError(
2049 - f"The prompt (total length {len(prompt_ids)}) is too long "
2050 - f"to fit into the model (context length {max_prompt_len}). "
2059 + if mm_processor.pad_dummy_encoder_prompt:
2060 + return # Skip encoder length check for Whisper
2061 +
2062 + prompt_ids = prompt_inputs["prompt_token_ids"]
2063 +
2064 + if not prompt_ids:
2065 + raise ValueError(f"The {prompt_type} prompt cannot be empty")
2066 +
2067 + max_prompt_len = self.model_config.max_model_len
2068 + if len(prompt_ids) >= max_prompt_len:
2069 + if self.model_config.is_multimodal_model:
2070 + suggestion = (
2071 "Make sure that `max_model_len` is no smaller than the "
2072 "number of text tokens plus multimodal tokens. For image "
2073 "inputs, the number of image tokens depends on the number "
2074 "of images, and possibly their aspect ratios as well.")
2075 + else:
2076 + suggestion = (
2077 + "Make sure that `max_model_len` is no smaller than the "
2078 + "number of text tokens.")
2079 +
2080 + raise ValueError(
2081 + f"The {prompt_type} prompt (length {len(prompt_ids)}) is "
2082 + f"longer than the maximum model length of {max_prompt_len}. "
2083 + f"{suggestion}")
2084
2085 # TODO: Find out how many placeholder tokens are there so we can
2086 # check that chunked prefill does not truncate them
vllm/multimodal/profiling.py
+6 -7
@@ -213,8 +213,12 @@ class MultiModalProfiler(Generic[_I]):
213
214 total_len = len(encoder_prompt_token_ids)
215
216 - # Encoder-decoder multimodal models only support v0
217 - if total_len > seq_len:
216 + processor = cast(EncDecMultiModalProcessor, self.processor)
217 + if processor.pad_dummy_encoder_prompt:
218 + num_tokens_to_pad = max(total_len, seq_len) - total_len
219 + encoder_prompt_token_ids.extend([0] * num_tokens_to_pad)
220 + # NOTE: Whisper allows total_len > seq_len.
221 + elif total_len > seq_len and not envs.VLLM_USE_V1:
222 # `max_num_batched_tokens` is defined by `SchedulerConfig`
223 logger.warning_once(
224 "The encoder sequence length used for profiling ("
@@ -229,11 +233,6 @@ class MultiModalProfiler(Generic[_I]):
233 "increase `max_model_len`, reduce `max_num_seqs`, "
234 "and/or reduce `mm_counts`.")
235
232 - processor = cast(EncDecMultiModalProcessor, self.processor)
233 - if processor.pad_dummy_encoder_prompt:
234 - num_tokens_to_pad = max(total_len, seq_len) - total_len
235 - encoder_prompt_token_ids.extend([0] * num_tokens_to_pad)
236 -
236 return DummyEncoderData(encoder_prompt_token_ids)
237
238 def get_decoder_dummy_data(
vllm/v1/engine/processor.py
+49 -27
@@ -2,16 +2,17 @@
2
3 import time
4 from collections.abc import Mapping
5 -from typing import Optional, Union
5 +from typing import Literal, Optional, Union
6
7 from vllm.config import VllmConfig
8 -from vllm.inputs import ProcessorInputs, PromptType
8 +from vllm.inputs import ProcessorInputs, PromptType, SingletonInputs
9 from vllm.inputs.parse import split_enc_dec_inputs
10 from vllm.inputs.preprocess import InputPreprocessor
11 from vllm.lora.request import LoRARequest
12 from vllm.multimodal import (MULTIMODAL_REGISTRY, MultiModalKwargs,
13 MultiModalRegistry)
14 from vllm.multimodal.inputs import PlaceholderRange
15 +from vllm.multimodal.processing import EncDecMultiModalProcessor
16 from vllm.multimodal.utils import merge_and_sort_multimodal_metadata
17 from vllm.pooling_params import PoolingParams
18 from vllm.prompt_adapter.request import PromptAdapterRequest
@@ -287,41 +288,62 @@ class Processor:
288 lora_request: Optional[LoRARequest] = None):
289 encoder_inputs, decoder_inputs = split_enc_dec_inputs(inputs)
290
290 - # For encoder-decoder multimodal models, the max_prompt_len
291 - # restricts the decoder prompt length
292 - if self.model_config.is_multimodal_model:
293 - prompt_inputs = decoder_inputs
294 - else:
295 - prompt_inputs = encoder_inputs or decoder_inputs
291 + if encoder_inputs is not None:
292 + self._validate_model_input(encoder_inputs,
293 + lora_request,
294 + prompt_type="encoder")
295
297 - prompt_ids = prompt_inputs["prompt_token_ids"]
296 + self._validate_model_input(decoder_inputs,
297 + lora_request,
298 + prompt_type="decoder")
299
299 - if prompt_ids is None or len(prompt_ids) == 0:
300 - raise ValueError("Prompt cannot be empty")
300 + def _validate_model_input(
301 + self,
302 + prompt_inputs: SingletonInputs,
303 + lora_request: Optional[LoRARequest],
304 + *,
305 + prompt_type: Literal["encoder", "decoder"],
306 + ):
307 + tokenizer = self.tokenizer.get_lora_tokenizer(lora_request)
308
302 - max_input_id = max(prompt_ids)
303 - max_allowed = self.tokenizer.get_lora_tokenizer(
304 - lora_request).max_token_id
305 - if max_input_id > max_allowed:
306 - raise ValueError(
307 - "Token id {} is out of vocabulary".format(max_input_id))
309 + if prompt_type == "encoder":
310 + model_config = self.model_config
311
309 - if len(prompt_ids) >= self.model_config.max_model_len:
310 - raise ValueError(
311 - f"Prompt length of {len(prompt_ids)} is longer than the "
312 - f"maximum model length of {self.model_config.max_model_len}.")
312 + if model_config.is_multimodal_model:
313 + mm_registry = self.input_preprocessor.mm_registry
314 + mm_processor = mm_registry.create_processor(
315 + model_config, tokenizer=tokenizer)
316 + assert isinstance(mm_processor, EncDecMultiModalProcessor)
317
314 - if self.model_config.is_multimodal_model:
315 - max_prompt_len = self.model_config.max_model_len
318 + if mm_processor.pad_dummy_encoder_prompt:
319 + return # Skip encoder length check for Whisper
320
317 - if len(prompt_ids) > max_prompt_len:
318 - raise ValueError(
319 - f"The prompt (total length {len(prompt_ids)}) is too long "
320 - f"to fit into the model (context length {max_prompt_len}). "
321 + prompt_ids = prompt_inputs["prompt_token_ids"]
322 +
323 + if not prompt_ids:
324 + raise ValueError(f"The {prompt_type} prompt cannot be empty")
325 +
326 + max_input_id = max(prompt_ids)
327 + if max_input_id > tokenizer.max_token_id:
328 + raise ValueError(f"Token id {max_input_id} is out of vocabulary")
329 +
330 + max_prompt_len = self.model_config.max_model_len
331 + if len(prompt_ids) >= max_prompt_len:
332 + if self.model_config.is_multimodal_model:
333 + suggestion = (
334 "Make sure that `max_model_len` is no smaller than the "
335 "number of text tokens plus multimodal tokens. For image "
336 "inputs, the number of image tokens depends on the number "
337 "of images, and possibly their aspect ratios as well.")
338 + else:
339 + suggestion = (
340 + "Make sure that `max_model_len` is no smaller than the "
341 + "number of text tokens.")
342 +
343 + raise ValueError(
344 + f"The {prompt_type} prompt (length {len(prompt_ids)}) is "
345 + f"longer than the maximum model length of {max_prompt_len}. "
346 + f"{suggestion}")
347
348 # TODO: Find out how many placeholder tokens are there so we can
349 # check that chunked prefill does not truncate them