Signed-off-by: Daniel M. García-Ocaña Hernández <danielgarciaocana@gmail.com>
687 lines
24 KiB
Python
687 lines
24 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
import time
|
|
from collections.abc import Callable, Mapping
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass, field
|
|
from functools import cached_property
|
|
from typing import TYPE_CHECKING, Any, get_type_hints, overload
|
|
|
|
import torch
|
|
from typing_extensions import TypeVar
|
|
|
|
from vllm.exceptions import VLLMValidationError
|
|
from vllm.inputs import MultiModalDataDict
|
|
from vllm.logger import init_logger
|
|
from vllm.multimodal.parse import (
|
|
DictEmbeddingItems,
|
|
EmbeddingItems,
|
|
MultiModalDataItems,
|
|
MultiModalDataParser,
|
|
)
|
|
from vllm.tokenizers import TokenizerLike
|
|
from vllm.transformers_utils.processor import cached_processor_from_config
|
|
from vllm.utils.func_utils import get_allowed_kwarg_only_overrides
|
|
from vllm.utils.jsontree import JSONTree, json_map_leaves
|
|
from vllm.utils.mistral import is_mistral_tokenizer
|
|
|
|
if TYPE_CHECKING:
|
|
from transformers.configuration_utils import PreTrainedConfig
|
|
from transformers.feature_extraction_utils import BatchFeature
|
|
from transformers.processing_utils import ProcessorMixin
|
|
|
|
from vllm.config import ModelConfig
|
|
from vllm.renderers import TokenizeParams
|
|
else:
|
|
PreTrainedConfig = object
|
|
BatchFeature = object
|
|
ProcessorMixin = object
|
|
|
|
ModelConfig = object
|
|
TokenizeParams = object
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
# HuggingFace processors accept a shared flat namespace in addition to the
|
|
# nested processor kwarg scopes ``text_kwargs`` / ``images_kwargs`` /
|
|
# ``videos_kwargs`` / ``audio_kwargs``. vLLM recognizes these scopes when
|
|
# resolving ``mm_processor_kwargs``.
|
|
_HF_PROCESSOR_KWARG_SCOPES = (
|
|
"text_kwargs",
|
|
"images_kwargs",
|
|
"videos_kwargs",
|
|
"audio_kwargs",
|
|
)
|
|
|
|
|
|
def _merge_scoped_mm_processor_value(
|
|
flat_value: object,
|
|
scoped_value: object,
|
|
) -> object:
|
|
"""Merge a flat value with an existing, more specific scoped value.
|
|
|
|
Mappings are merged recursively; otherwise the scoped value replaces the
|
|
flat value.
|
|
"""
|
|
if isinstance(flat_value, Mapping) and isinstance(scoped_value, Mapping):
|
|
# Merge recursively so scoped leaves override matching flat leaves
|
|
# without dropping unrelated flat entries.
|
|
merged = dict(flat_value)
|
|
for key, value in scoped_value.items():
|
|
if key in merged:
|
|
merged[key] = _merge_scoped_mm_processor_value(merged[key], value)
|
|
else:
|
|
merged[key] = value
|
|
return merged
|
|
return scoped_value
|
|
|
|
|
|
def _resolve_mm_processor_kwargs(
|
|
kwargs: Mapping[str, object],
|
|
supported_mm_processor_kwargs: Mapping[str, set[str]] | None = None,
|
|
) -> dict[str, object]:
|
|
"""Resolve flat processor kwargs into HuggingFace processor kwarg scopes.
|
|
|
|
With ``supported_mm_processor_kwargs``, each scope lists the flat keys it
|
|
supports. Matching flat keys are routed to every scope that supports them,
|
|
existing scoped values take precedence, and keys not supported by any scope
|
|
remain flat.
|
|
|
|
Without ``supported_mm_processor_kwargs``, flat keys already represented in
|
|
at least one existing mapping-valued scope are removed from the shared flat
|
|
namespace; all other flat keys are left unchanged because there is no
|
|
information to determine which scopes should receive them.
|
|
"""
|
|
resolved = dict(kwargs)
|
|
|
|
# Without ``supported_mm_processor_kwargs``, remove flat entries already
|
|
# represented in at least one existing mapping-valued scope. There is no
|
|
# information to route the remaining flat entries.
|
|
if supported_mm_processor_kwargs is None:
|
|
# Collect all keys represented in existing mapping-valued scopes.
|
|
# Non-mapping scope values do not contain scoped kwargs and are left
|
|
# untouched.
|
|
represented_scoped_keys: set[str] = set()
|
|
for scoped_key in _HF_PROCESSOR_KWARG_SCOPES:
|
|
scoped_kwargs = resolved.get(scoped_key)
|
|
if isinstance(scoped_kwargs, Mapping):
|
|
represented_scoped_keys.update(scoped_kwargs)
|
|
|
|
# Remove flat entries for keys already represented in a scope.
|
|
for key in represented_scoped_keys:
|
|
if key not in _HF_PROCESSOR_KWARG_SCOPES:
|
|
resolved.pop(key, None)
|
|
return resolved
|
|
|
|
# With ``supported_mm_processor_kwargs``, collect the flat kwargs before
|
|
# routing each one to the processor kwarg scopes that support it.
|
|
flat_kwargs = {
|
|
key: value
|
|
for key, value in resolved.items()
|
|
if key not in _HF_PROCESSOR_KWARG_SCOPES
|
|
}
|
|
|
|
for key, flat_value in flat_kwargs.items():
|
|
# Find every processor kwarg scope that supports this flat key. If none
|
|
# supports it, keep the key in the shared flat namespace.
|
|
supported_scopes = [
|
|
scoped_key
|
|
for scoped_key in _HF_PROCESSOR_KWARG_SCOPES
|
|
if key in supported_mm_processor_kwargs.get(scoped_key, set())
|
|
]
|
|
if not supported_scopes:
|
|
continue
|
|
|
|
# Add the flat value to each supported destination scope.
|
|
for scoped_key in supported_scopes:
|
|
# Start from a copy of the existing scoped kwargs, or an empty scope
|
|
# if absent.
|
|
if scoped_key in resolved:
|
|
scoped_kwargs = resolved[scoped_key]
|
|
# A scope must be a mapping before keys can be added to it. Do
|
|
# not replace an explicit non-mapping value.
|
|
if not isinstance(scoped_kwargs, Mapping):
|
|
raise TypeError(f"`{scoped_key}` must be a mapping")
|
|
scoped_kwargs = dict(scoped_kwargs)
|
|
else:
|
|
scoped_kwargs = {}
|
|
|
|
# Add the flat value to this scope. Existing scoped values take
|
|
# precedence.
|
|
if key in scoped_kwargs:
|
|
scoped_kwargs[key] = _merge_scoped_mm_processor_value(
|
|
flat_value, scoped_kwargs[key]
|
|
)
|
|
else:
|
|
# Assign the flat value to this scope. For mappings, assign a
|
|
# copy so the same mapping object is not shared between the flat
|
|
# value and multiple scopes.
|
|
scoped_kwargs[key] = (
|
|
dict(flat_value) if isinstance(flat_value, Mapping) else flat_value
|
|
)
|
|
resolved[scoped_key] = scoped_kwargs
|
|
|
|
# After routing to every destination, remove the flat copy.
|
|
resolved.pop(key)
|
|
|
|
return resolved
|
|
|
|
|
|
@dataclass
|
|
class TimingContext:
|
|
"""Helper class to record execution times during multi-modal processing."""
|
|
|
|
enabled: bool = True
|
|
"""If disabled, `TimingContext.record` becomes a no-op."""
|
|
|
|
stage_secs: dict[str, float] = field(default_factory=dict)
|
|
"""The execution time (in seconds) for each processing stage."""
|
|
|
|
@property
|
|
def total_secs(self) -> float:
|
|
return sum(self.stage_secs.values())
|
|
|
|
@contextmanager
|
|
def record(self, stage: str):
|
|
"""Record the execution time for a processing stage."""
|
|
if not self.enabled:
|
|
yield
|
|
return
|
|
|
|
start_time = time.perf_counter()
|
|
try:
|
|
yield
|
|
finally:
|
|
elapsed = time.perf_counter() - start_time
|
|
self.stage_secs.setdefault(stage, 0.0)
|
|
self.stage_secs[stage] += elapsed
|
|
|
|
def get_stats_dict(self):
|
|
stats_dict = {
|
|
f"{stage}_secs": time_s for stage, time_s in self.stage_secs.items()
|
|
}
|
|
stats_dict["preprocessor_total_secs"] = self.total_secs
|
|
|
|
return stats_dict
|
|
|
|
|
|
_T = TypeVar("_T")
|
|
_C = TypeVar("_C", bound=PreTrainedConfig, default=PreTrainedConfig)
|
|
_P = TypeVar("_P", bound=ProcessorMixin, default=ProcessorMixin)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class InputProcessingContext:
|
|
"""Contains information about the model which may be used to
|
|
modify the inputs.
|
|
"""
|
|
|
|
model_config: ModelConfig
|
|
"""The configuration of the model."""
|
|
|
|
tokenizer: TokenizerLike | None
|
|
"""The tokenizer used to tokenize the inputs."""
|
|
|
|
def get_tokenizer(self) -> TokenizerLike:
|
|
if self.tokenizer is None:
|
|
raise ValueError(
|
|
"You cannot pass text prompts when `skip_tokenizer_init=True`"
|
|
)
|
|
|
|
return self.tokenizer
|
|
|
|
@overload
|
|
def get_hf_config(self, /) -> PreTrainedConfig: ...
|
|
|
|
@overload
|
|
def get_hf_config(
|
|
self,
|
|
typ: type[_C] | tuple[type[_C], ...],
|
|
/,
|
|
) -> _C: ...
|
|
|
|
def get_hf_config(
|
|
self,
|
|
typ: type[Any] | tuple[type[Any], ...] | None = None,
|
|
/,
|
|
) -> Any:
|
|
"""Get the HuggingFace configuration
|
|
(`transformers.PreTrainedConfig`) of the model,
|
|
additionally checking its type.
|
|
|
|
Raises:
|
|
TypeError: If the configuration is not of the specified type.
|
|
|
|
"""
|
|
if typ is None:
|
|
from transformers.configuration_utils import PreTrainedConfig
|
|
|
|
typ = PreTrainedConfig
|
|
|
|
hf_config = self.model_config.hf_config
|
|
if not isinstance(hf_config, typ):
|
|
raise TypeError(
|
|
"Invalid type of HuggingFace config. "
|
|
f"Expected type: {typ}, but "
|
|
f"found type: {type(hf_config)}"
|
|
)
|
|
|
|
return hf_config
|
|
|
|
def get_hf_image_processor_config(self) -> dict[str, Any]:
|
|
"""Get the HuggingFace image processor configuration of the model."""
|
|
return self.model_config.hf_image_processor_config
|
|
|
|
def get_mm_config(self):
|
|
"""Get the multimodal config of the model.
|
|
|
|
Raises:
|
|
RuntimeError: If the model is not a multimodal model.
|
|
|
|
"""
|
|
mm_config = self.model_config.multimodal_config
|
|
if mm_config is None:
|
|
raise RuntimeError("Not a multimodal model")
|
|
|
|
return mm_config
|
|
|
|
@overload
|
|
def get_hf_processor(self, /, **kwargs: object) -> ProcessorMixin: ...
|
|
|
|
@overload
|
|
def get_hf_processor(
|
|
self,
|
|
typ: type[_P] | tuple[type[_P], ...],
|
|
/,
|
|
**kwargs: object,
|
|
) -> _P: ...
|
|
|
|
def get_hf_processor(
|
|
self,
|
|
typ: type[Any] | tuple[type[Any], ...] | None = None,
|
|
/,
|
|
**kwargs: object,
|
|
) -> Any:
|
|
"""Get the HuggingFace processor
|
|
(`transformers.ProcessorMixin`) of the model,
|
|
additionally checking its type.
|
|
|
|
Raises:
|
|
TypeError: If the processor is not of the specified type.
|
|
|
|
"""
|
|
if typ is None:
|
|
from transformers.processing_utils import ProcessorMixin
|
|
|
|
typ = ProcessorMixin
|
|
|
|
tokenizer = self.tokenizer
|
|
if is_mistral_tokenizer(tokenizer):
|
|
tokenizer = tokenizer.transformers_tokenizer # type: ignore[union-attr]
|
|
|
|
merged_kwargs = self.get_merged_mm_kwargs(kwargs)
|
|
merged_kwargs.pop("tokenizer", None)
|
|
|
|
return cached_processor_from_config(
|
|
self.model_config,
|
|
processor_cls=typ,
|
|
tokenizer=tokenizer,
|
|
**merged_kwargs,
|
|
)
|
|
|
|
def init_processor(
|
|
self,
|
|
typ: type[_T],
|
|
/,
|
|
**kwargs: object,
|
|
) -> _T:
|
|
"""Initialize a HuggingFace-like processor class, merging the
|
|
keyword arguments with those in the model's configuration.
|
|
"""
|
|
merged_kwargs = self.get_merged_mm_kwargs(kwargs)
|
|
|
|
return typ(**merged_kwargs)
|
|
|
|
def _postprocess_output(
|
|
self,
|
|
output: JSONTree,
|
|
) -> JSONTree:
|
|
# "torch_shm" puts tensors on a torch.multiprocessing queue, which
|
|
# shares device tensors by CUDA IPC handle, so a device-side processor
|
|
# can hand `pixel_values` straight to the worker. Every other transport
|
|
# serializes host bytes, so the result has to be copied back first.
|
|
keep_on_device = (
|
|
self.model_config.get_multimodal_config().mm_tensor_ipc == "torch_shm"
|
|
)
|
|
|
|
def _postprocess_one(x: object):
|
|
if not isinstance(x, torch.Tensor):
|
|
return x
|
|
|
|
# Bind to a Tensor-typed local: reassigning the `object`-typed
|
|
# parameter would discard the isinstance narrowing.
|
|
tensor = x
|
|
|
|
# This mimics the behavior of transformers.BatchFeature
|
|
if tensor.is_floating_point():
|
|
tensor = tensor.to(dtype=self.model_config.dtype)
|
|
|
|
if not tensor.is_cpu and not keep_on_device:
|
|
tensor = tensor.cpu()
|
|
|
|
return tensor
|
|
|
|
return json_map_leaves(_postprocess_one, output)
|
|
|
|
def get_merged_mm_kwargs(
|
|
self,
|
|
kwargs: Mapping[str, object],
|
|
*,
|
|
supported_mm_processor_kwargs: Mapping[str, set[str]] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Merge configured and request ``mm_processor_kwargs``.
|
|
|
|
When ``supported_mm_processor_kwargs`` is provided, flat keys are matched
|
|
against the keys supported by each HuggingFace processor kwarg scope.
|
|
Matching flat keys are routed to every scope that supports them and
|
|
removed from the shared flat namespace; keys not supported by any scope
|
|
remain flat.
|
|
|
|
Without ``supported_mm_processor_kwargs``, flat keys already represented
|
|
in at least one existing mapping-valued scope are removed from the shared
|
|
flat namespace; all other flat keys are left unchanged because there is
|
|
no information to determine which scopes should receive them.
|
|
"""
|
|
mm_config = self.model_config.get_multimodal_config()
|
|
merged = mm_config.merge_mm_processor_kwargs(kwargs)
|
|
return _resolve_mm_processor_kwargs(
|
|
merged,
|
|
supported_mm_processor_kwargs=supported_mm_processor_kwargs,
|
|
)
|
|
|
|
def call_hf_processor(
|
|
self,
|
|
hf_processor: Callable[..., BatchFeature] | ProcessorMixin,
|
|
data: Mapping[str, object],
|
|
kwargs: Mapping[str, object] = {},
|
|
) -> BatchFeature:
|
|
"""Call `hf_processor` on the prompt `data`
|
|
(text, image, audio...) with configurable options `kwargs`.
|
|
"""
|
|
assert callable(hf_processor)
|
|
|
|
merged_kwargs = self.get_merged_mm_kwargs(kwargs)
|
|
|
|
allowed_kwargs = get_allowed_kwarg_only_overrides(
|
|
hf_processor,
|
|
merged_kwargs,
|
|
requires_kw_only=False,
|
|
allow_var_kwargs=True,
|
|
)
|
|
allowed_kwargs.setdefault("return_tensors", "pt")
|
|
|
|
try:
|
|
output = hf_processor(**data, **allowed_kwargs)
|
|
except Exception as exc:
|
|
msg = (
|
|
f"Failed to apply {type(hf_processor).__name__} "
|
|
f"on data={data} with kwargs={allowed_kwargs}"
|
|
)
|
|
|
|
raise ValueError(msg) from exc
|
|
|
|
# this emulates output.to(dtype=self.model_config.dtype)
|
|
from transformers.feature_extraction_utils import BatchFeature
|
|
|
|
if isinstance(output, BatchFeature):
|
|
output_ = self._postprocess_output(output.data)
|
|
return BatchFeature(output_) # type: ignore
|
|
|
|
logger.warning_once(
|
|
"%s did not return `BatchFeature`. "
|
|
"Make sure to match the behaviour of `ProcessorMixin` when "
|
|
"implementing custom processors.",
|
|
type(hf_processor).__name__,
|
|
)
|
|
|
|
return self._postprocess_output(output) # type: ignore
|
|
|
|
|
|
class BaseProcessingInfo:
|
|
"""Base class to provide the information necessary for data processing."""
|
|
|
|
def __init__(self, ctx: InputProcessingContext) -> None:
|
|
super().__init__()
|
|
|
|
self.ctx = ctx
|
|
|
|
@property
|
|
def model_id(self) -> str:
|
|
return self.ctx.model_config.model
|
|
|
|
def get_tokenizer(self) -> TokenizerLike:
|
|
return self.ctx.get_tokenizer()
|
|
|
|
def get_hf_config(self) -> PreTrainedConfig:
|
|
return self.ctx.get_hf_config()
|
|
|
|
def get_hf_processor(self, **kwargs: object) -> ProcessorMixin:
|
|
"""Subclasses can override this method to handle
|
|
specific kwargs from model config or user inputs.
|
|
"""
|
|
return self.ctx.get_hf_processor(**kwargs)
|
|
|
|
def get_supported_mm_processor_kwargs(self) -> dict[str, set[str]]:
|
|
"""Return supported kwarg names for each HF processor kwargs scope."""
|
|
processor = self.get_hf_processor()
|
|
processor_kwargs = get_type_hints(processor.valid_processor_kwargs)
|
|
|
|
supported = {
|
|
"text_kwargs": set(processor_kwargs["text_kwargs"].__annotations__),
|
|
}
|
|
if "image" in self.supported_mm_limits:
|
|
supported["images_kwargs"] = set(
|
|
processor.image_processor.valid_kwargs.__annotations__
|
|
)
|
|
if "video" in self.supported_mm_limits:
|
|
supported["videos_kwargs"] = set(
|
|
processor.video_processor.valid_kwargs.__annotations__
|
|
)
|
|
if "audio" in self.supported_mm_limits:
|
|
supported["audio_kwargs"] = set(
|
|
processor_kwargs["audio_kwargs"].__annotations__
|
|
)
|
|
|
|
return supported
|
|
|
|
@cached_property
|
|
def supported_mm_processor_kwargs(self) -> dict[str, set[str]]:
|
|
"""Supported kwarg names for each HF processor kwargs scope."""
|
|
return self.get_supported_mm_processor_kwargs()
|
|
|
|
def _merge_and_resolve_mm_processor_kwargs(
|
|
self,
|
|
mm_kwargs: Mapping[str, object],
|
|
) -> dict[str, Any]:
|
|
"""Merge configured and request ``mm_processor_kwargs``.
|
|
|
|
Flat kwargs are routed into the HuggingFace processor kwarg scopes that
|
|
support them after the configured/request merge. When a routed flat value
|
|
conflicts with an existing scoped value, the scoped value takes precedence.
|
|
"""
|
|
return self.ctx.get_merged_mm_kwargs(
|
|
mm_kwargs,
|
|
supported_mm_processor_kwargs=self.supported_mm_processor_kwargs,
|
|
)
|
|
|
|
def get_default_tok_params(self) -> TokenizeParams:
|
|
"""Construct the default parameters for tokenization."""
|
|
from vllm.renderers import TokenizeParams
|
|
|
|
model_config = self.ctx.model_config
|
|
encoder_config = model_config.encoder_config or {}
|
|
|
|
return TokenizeParams(
|
|
max_total_tokens=model_config.max_model_len,
|
|
do_lower_case=encoder_config.get("do_lower_case", False),
|
|
add_special_tokens=True,
|
|
)
|
|
|
|
@cached_property
|
|
def default_tok_params(self) -> TokenizeParams:
|
|
return self.get_default_tok_params()
|
|
|
|
def _get_expected_hidden_size(self) -> int | None:
|
|
"""Get expected hidden size for embedding validation if `mm_embeds` are enabled.
|
|
|
|
This validates hidden dimensions to prevent a vulnerability where embeddings
|
|
with correct `ndim` but wrong `shape` could cause crashes at inference time.
|
|
"""
|
|
model_config = self.ctx.model_config
|
|
mm_config = model_config.get_multimodal_config()
|
|
|
|
if mm_config.enable_mm_embeds:
|
|
return model_config.get_inputs_embeds_size()
|
|
|
|
return None
|
|
|
|
@property
|
|
def allow_missing_mm_embeddings(self) -> bool:
|
|
"""Whether pre-computed embedding tensors may be omitted."""
|
|
mm_config = self.ctx.model_config.multimodal_config
|
|
return mm_config is not None and mm_config.allow_missing_mm_embeddings
|
|
|
|
def get_data_parser(self) -> MultiModalDataParser:
|
|
"""Constructs a parser to preprocess multi-modal data items
|
|
before passing them to
|
|
[`_get_hf_mm_inputs`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs].
|
|
|
|
You can support additional modalities by creating a subclass
|
|
of [`MultiModalDataParser`][vllm.multimodal.parse.MultiModalDataParser]
|
|
that has additional subparsers.
|
|
"""
|
|
return MultiModalDataParser(
|
|
expected_hidden_size=self._get_expected_hidden_size(),
|
|
allow_missing_mm_embeddings=self.allow_missing_mm_embeddings,
|
|
)
|
|
|
|
@cached_property
|
|
def data_parser(self) -> MultiModalDataParser:
|
|
return self.get_data_parser()
|
|
|
|
@property
|
|
def skip_prompt_length_check(self) -> bool:
|
|
return False
|
|
|
|
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
|
|
"""Return the maximum supported number of items for each modality.
|
|
|
|
A value of `None` means unlimited number of items.
|
|
|
|
Omitting a modality from the returned dictionary means that
|
|
it is not supported at all.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@cached_property
|
|
def supported_mm_limits(self) -> Mapping[str, int | None]:
|
|
"""The maximum supported number of items for each modality."""
|
|
return self.get_supported_mm_limits()
|
|
|
|
@cached_property
|
|
def allowed_mm_limits(self) -> Mapping[str, int]:
|
|
"""The maximum allowed number of items for each modality."""
|
|
mm_config = self.ctx.get_mm_config()
|
|
|
|
allowed_limits = dict[str, int]()
|
|
for modality, supported_limit in self.supported_mm_limits.items():
|
|
user_limit = mm_config.get_limit_per_prompt(modality)
|
|
|
|
allowed_limits[modality] = (
|
|
user_limit
|
|
if supported_limit is None
|
|
else min(user_limit, supported_limit)
|
|
)
|
|
|
|
return allowed_limits
|
|
|
|
def validate_num_items(self, modality: str, num_items: int) -> None:
|
|
"""Raise `ValueError` if the number of input items for the given modality
|
|
is invalid.
|
|
"""
|
|
supported_limit = self.supported_mm_limits.get(modality, 0)
|
|
allowed_limit = self.allowed_mm_limits.get(modality, 0)
|
|
|
|
if supported_limit is None:
|
|
supported_limit = allowed_limit
|
|
|
|
limit = min(supported_limit, allowed_limit)
|
|
|
|
if num_items > limit:
|
|
msg = f"At most {limit} {modality}(s) may be provided in one prompt."
|
|
|
|
if num_items <= supported_limit:
|
|
msg += " Set `--limit-mm-per-prompt` to increase this limit."
|
|
|
|
raise VLLMValidationError(msg, parameter=modality)
|
|
|
|
def parse_mm_data(
|
|
self,
|
|
mm_data: MultiModalDataDict,
|
|
*,
|
|
validate: bool = True,
|
|
) -> MultiModalDataItems:
|
|
"""Normalize [`MultiModalDataDict`][vllm.inputs.MultiModalDataDict]
|
|
to [`MultiModalDataItems`][vllm.multimodal.parse.MultiModalDataItems]
|
|
before passing them to
|
|
[`_get_hf_mm_inputs`][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_mm_inputs].
|
|
"""
|
|
mm_items = self.data_parser.parse_mm_data(mm_data)
|
|
|
|
if validate:
|
|
mm_config = self.ctx.get_mm_config()
|
|
|
|
for modality, items in mm_items.items():
|
|
if isinstance(items, (EmbeddingItems, DictEmbeddingItems)):
|
|
if not mm_config.enable_mm_embeds:
|
|
raise ValueError(
|
|
f"You must set `--enable-mm-embeds` to input "
|
|
f"`{modality}_embeds`"
|
|
)
|
|
if mm_config.get_limit_per_prompt(modality) == 0:
|
|
logger.debug(
|
|
"Skipping count validation for modality "
|
|
"'%s' (embeddings with limit=0)",
|
|
modality,
|
|
)
|
|
continue
|
|
self.validate_num_items(modality, len(items))
|
|
|
|
return mm_items
|
|
|
|
def get_mm_max_tokens_per_item(
|
|
self,
|
|
seq_len: int,
|
|
mm_counts: Mapping[str, int],
|
|
) -> Mapping[str, int] | None:
|
|
"""Return the maximum number of tokens per item of for each modality.
|
|
|
|
When `None` (the default) is returned, vLLM will generate dummy inputs
|
|
(images/videos) at maximum possible sizes and process them to determine
|
|
the maximum token count per modality.
|
|
|
|
This approach works but can be very slow for certain models (e.g.,
|
|
Qwen2.5-VL), leading to very long startup time. For better performance,
|
|
each model can override this method to return pre-computed maximum token
|
|
counts, avoiding the need for dummy input generation and processing.
|
|
|
|
Note:
|
|
The maximum number of tokens per item of each modality returned
|
|
from this function should respect the model's maximum sequence
|
|
length and the maximum number of items of each modality allowed,
|
|
and agree with dummy inputs (images/videos) at maximum possible
|
|
sizes.
|
|
|
|
"""
|
|
return None
|