main
py 546 lines 19.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import importlib
5 import inspect
6 from functools import lru_cache
7 from typing import TYPE_CHECKING, Any, cast, get_args, get_type_hints
8
9 from transformers import (
10 AutoFeatureExtractor,
11 AutoImageProcessor,
12 AutoProcessor,
13 AutoVideoProcessor,
14 processing_utils,
15 )
16 from transformers.feature_extraction_utils import FeatureExtractionMixin
17 from transformers.image_processing_utils import BaseImageProcessor
18 from transformers.models.auto.video_processing_auto import VIDEO_PROCESSOR_MAPPING_NAMES
19 from transformers.processing_utils import ProcessorMixin
20 from transformers.video_processing_utils import BaseVideoProcessor
21 from typing_extensions import TypeVar
22
23 from vllm.logger import init_logger
24 from vllm.transformers_utils import processors
25 from vllm.transformers_utils.repo_utils import get_hf_file_to_dict
26 from vllm.transformers_utils.utils import convert_model_repo_to_path
27 from vllm.utils.func_utils import get_allowed_kwarg_only_overrides
28
29 logger = init_logger(__name__)
30
31 if TYPE_CHECKING:
32 from vllm.config import ModelConfig
33
34
35 def _transformers_v4_compatibility_import():
36 """Some remote code processors still import `ChatTemplateLoadKwargs` which was a
37 subset of `ProcessorChatTemplateKwargs` as defined in Transformers v4.
38 In Transformers v5 these were merged into `ProcessorChatTemplateKwargs` and
39 `ChatTemplateLoadKwargs` was removed. For backward compatibility, we add an alias
40 for `ChatTemplateLoadKwargs` if it doesn't exist."""
41 old_import = getattr(processing_utils, "ChatTemplateLoadKwargs", None)
42 new_import = getattr(processing_utils, "ProcessorChatTemplateKwargs", None)
43 if old_import is None and new_import is not None:
44 processing_utils.ChatTemplateLoadKwargs = new_import
45
46
47 def _transformers_v4_compatibility_init() -> Any:
48 """Some remote code processors may define `optional_attributes` in their
49 `ProcessorMixin` subclass, and then pass these arbitrary attributes directly to
50 `ProcessorMixin.__init__`, which is no longer allowed in Transformers v5. For
51 backward compatibility, we intercept these optional attributes and set them on the
52 processor instance before calling the original `ProcessorMixin.__init__`.
53
54 This can be removed if `Molmo2ForConditionalGeneration` is upstreamed to
55 Transformers."""
56 if hasattr(ProcessorMixin.__init__, "_vllm_patched"):
57 return
58
59 original_init = ProcessorMixin.__init__
60
61 def __init__(self, *args, **kwargs):
62 for optional_attribute in getattr(self, "optional_attributes", []):
63 if optional_attribute in kwargs:
64 setattr(self, optional_attribute, kwargs.pop(optional_attribute))
65
66 original_init(self, *args, **kwargs)
67
68 # Only patch if ProcessorMixin is not mocked (for docs builds)
69 if not hasattr(ProcessorMixin, "_mock_name"):
70 __init__._vllm_patched = True # type: ignore[attr-defined]
71 ProcessorMixin.__init__ = __init__
72
73
74 _transformers_v4_compatibility_import()
75 _transformers_v4_compatibility_init()
76
77 _P = TypeVar("_P", bound=ProcessorMixin, default=ProcessorMixin)
78 _I = TypeVar("_I", bound=BaseImageProcessor, default=BaseImageProcessor)
79 _V = TypeVar("_V", bound=BaseVideoProcessor, default=BaseVideoProcessor)
80
81
82 class HashableDict(dict):
83 """A dictionary that can be hashed by lru_cache."""
84
85 # NOTE: pythonic dict is not hashable,
86 # we override on it directly for simplicity
87 def __hash__(self) -> int: # type: ignore[override]
88 return hash(frozenset(self.items()))
89
90
91 class HashableList(list):
92 """A list that can be hashed by lru_cache."""
93
94 def __hash__(self) -> int: # type: ignore[override]
95 return hash(tuple(self))
96
97
98 def _get_processor_factory_fn(processor_cls: type | tuple[type, ...]):
99 if isinstance(processor_cls, tuple) or processor_cls == ProcessorMixin:
100 return AutoProcessor.from_pretrained
101 if hasattr(processor_cls, "from_pretrained"):
102 return processor_cls.from_pretrained
103
104 return processor_cls
105
106
107 def _merge_mm_kwargs(
108 model_config: "ModelConfig",
109 processor_cls: type | tuple[type, ...],
110 /,
111 **kwargs,
112 ):
113 mm_config = model_config.get_multimodal_config()
114 merged_kwargs = mm_config.merge_mm_processor_kwargs(kwargs)
115
116 factory = _get_processor_factory_fn(processor_cls)
117 allowed_kwargs = get_allowed_kwarg_only_overrides(
118 factory,
119 merged_kwargs,
120 requires_kw_only=False,
121 allow_var_kwargs=True,
122 )
123 # NOTE: Pythonic dict is not hashable and will raise unhashable type
124 # error when calling `cached_get_processor`, therefore we need to
125 # wrap it to a hashable dict.
126 for key, value in allowed_kwargs.items():
127 if isinstance(value, dict):
128 allowed_kwargs[key] = HashableDict(value)
129 if isinstance(value, list):
130 allowed_kwargs[key] = HashableList(value)
131
132 return allowed_kwargs
133
134
135 def get_processor_config(
136 processor_name: str,
137 revision: str | None = "main",
138 ) -> dict:
139 config_file = [
140 "processor_config.json",
141 "preprocessor_config.json",
142 "tokenizer_config.json",
143 ]
144 for file in config_file:
145 config = get_hf_file_to_dict(file, processor_name, revision=revision)
146 if config and "processor_class" in config:
147 return config
148 return {}
149
150
151 def get_processor_cls_name_from_config(
152 processor_name: str,
153 revision: str | None = "main",
154 ) -> str | None:
155 config = get_processor_config(processor_name, revision=revision)
156
157 if not config:
158 return None
159
160 return config["processor_class"]
161
162
163 def get_video_processor_cls_name_from_config(
164 processor_name: str,
165 revision: str | None = "main",
166 ) -> str | None:
167 processor_name = convert_model_repo_to_path(processor_name)
168 config_file = [
169 "video_preprocessor_config.json",
170 "preprocessor_config.json",
171 ]
172 for file in config_file:
173 config = get_hf_file_to_dict(file, processor_name, revision=revision)
174 if config and "video_processor_type" in config:
175 return config["video_processor_type"]
176
177 # Some models ship no explicit ``video_processor_type`` in their
178 # preprocessor config. Fall back to transformers' ``model_type`` -> video
179 # processor mapping so these still resolve to their registered loader
180 # instead of the generic opencv fallback. The mapping is ``None`` for a
181 # given type when torchvision is unavailable; callers then use opencv.
182 model_config = get_hf_file_to_dict("config.json", processor_name, revision=revision)
183 if model_config and "model_type" in model_config:
184 return VIDEO_PROCESSOR_MAPPING_NAMES.get(model_config["model_type"])
185 return None
186
187
188 _cached_get_video_processor_cls_name = lru_cache(
189 get_video_processor_cls_name_from_config
190 )
191
192
193 def get_video_processor_cls_name(
194 model_config: "ModelConfig",
195 ) -> str | None:
196 model = model_config.model
197 revision = model_config.revision
198 return _cached_get_video_processor_cls_name(model, revision=revision)
199
200
201 def get_processor(
202 processor_name: str,
203 *args: Any,
204 revision: str | None = None,
205 trust_remote_code: bool = False,
206 processor_cls: type[_P] | tuple[type[_P], ...] = ProcessorMixin,
207 **kwargs: Any,
208 ) -> _P:
209 """Load a processor for the given model name via HuggingFace."""
210 if revision is None:
211 revision = "main"
212 try:
213 processor_name = convert_model_repo_to_path(processor_name)
214 registered_cls_name = get_processor_cls_name_from_config(
215 processor_name, revision=revision
216 )
217 registered_processor_cls = (
218 getattr(processors, registered_cls_name, None)
219 if registered_cls_name
220 else None
221 )
222 registered_processor_cls = cast(type[_P] | None, registered_processor_cls)
223 # Use registered processor class when it's available
224 # and explicit processor_cls is not set.
225 if isinstance(processor_cls, tuple) or processor_cls == ProcessorMixin:
226 _processor_cls = registered_processor_cls or AutoProcessor
227 processor = _processor_cls.from_pretrained(
228 processor_name,
229 *args,
230 revision=revision,
231 trust_remote_code=trust_remote_code,
232 **kwargs,
233 )
234 elif issubclass(processor_cls, ProcessorMixin):
235 processor = processor_cls.from_pretrained(
236 processor_name,
237 *args,
238 revision=revision,
239 trust_remote_code=trust_remote_code,
240 **kwargs,
241 )
242 else:
243 # Processors that are standalone classes unrelated to HF
244 processor = processor_cls(*args, **kwargs)
245 except ValueError as e:
246 # If the error pertains to the processor class not existing or not
247 # currently being imported, suggest using the --trust-remote-code flag.
248 # Unlike AutoTokenizer, AutoProcessor does not separate such errors
249 if not trust_remote_code:
250 err_msg = (
251 "Failed to load the processor. If the processor is "
252 "a custom processor not yet available in the HuggingFace "
253 "transformers library, consider setting "
254 "`trust_remote_code=True` in LLM or using the "
255 "`--trust-remote-code` flag in the CLI."
256 )
257 raise RuntimeError(err_msg) from e
258 else:
259 raise e
260
261 if not isinstance(processor, processor_cls):
262 raise TypeError(
263 "Invalid type of HuggingFace processor. "
264 f"Expected type: {processor_cls}, but "
265 f"found type: {type(processor)}"
266 )
267
268 return processor
269
270
271 cached_get_processor = lru_cache(get_processor)
272
273
274 @lru_cache
275 def get_processor_kwargs_type(
276 processor: ProcessorMixin,
277 ) -> type[processing_utils.ProcessingKwargs]:
278 try:
279 # get kwargs annotations in processor
280 call_params = inspect.signature(type(processor).__call__).parameters
281 call_kwargs = call_params.get("kwargs")
282 call_kwargs_annotations = call_kwargs.annotation if call_kwargs else None
283
284 # if the processor has explicit kwargs annotation, use it
285 if call_kwargs_annotations not in (None, inspect._empty): # noqa: SIM102
286 # get_type_hints will parse all type annotations at runtime,
287 # and if an annotation refers to a type or
288 # name that hasn’t been imported or defined, it will raise an error.
289 # So we use __annotations__ to get the raw annotations directly.
290 if anno_args := get_args(call_kwargs_annotations):
291 return anno_args[0]
292
293 # otherwise, try to get from ProcessorKwargs
294 module_name = type(processor).__module__
295 mod = importlib.import_module(module_name)
296 for name, obj in vars(mod).items():
297 if name.endswith("ProcessorKwargs"):
298 return obj
299
300 except Exception:
301 logger.exception("Failed to collect processor kwargs")
302
303 return processing_utils.ProcessingKwargs
304
305
306 @lru_cache
307 def get_processor_kwargs_keys(
308 kwargs_cls: type[processing_utils.ProcessingKwargs],
309 ) -> set[str]:
310 dynamic_kwargs: set[str] = set()
311 modality_kwargs = {
312 "text_kwargs",
313 "images_kwargs",
314 "videos_kwargs",
315 "audio_kwargs",
316 }
317
318 try:
319 # get kwargs annotations in processor
320 # merge text_kwargs / images_kwargs / videos_kwargs / audio_kwargs
321 kwargs_type_annotations = get_type_hints(kwargs_cls)
322 for kw_type in modality_kwargs:
323 if kw_type in kwargs_type_annotations:
324 # Use __annotations__ instead of get_type_hints() to avoid
325 # NameError from unresolved forward references (e.g.
326 # PILImageResampling). We only need key names, not types.
327 kw_cls = kwargs_type_annotations[kw_type]
328 kw_annotations: dict[str, Any] = {}
329 for base in reversed(kw_cls.__mro__):
330 kw_annotations.update(getattr(base, "__annotations__", {}))
331 for kw_name in kw_annotations:
332 dynamic_kwargs.add(kw_name)
333
334 except Exception:
335 logger.exception("Failed to collect processor kwargs")
336
337 return dynamic_kwargs | modality_kwargs
338
339
340 def cached_get_processor_without_dynamic_kwargs(
341 processor_name: str,
342 *args: Any,
343 revision: str | None = None,
344 trust_remote_code: bool = False,
345 processor_cls: type[_P] | tuple[type[_P], ...] = ProcessorMixin,
346 **kwargs: Any,
347 ) -> _P:
348 # Step 1: use default kwargs to get a temporary processor instance
349 processor = cached_get_processor(
350 processor_name,
351 revision=revision,
352 trust_remote_code=trust_remote_code,
353 processor_cls=processor_cls, # type: ignore[arg-type]
354 )
355
356 # Step 2: use temporary processor collect dynamic keys
357 dynamic_keys = get_processor_kwargs_keys(
358 get_processor_kwargs_type(processor) # type: ignore[arg-type]
359 )
360
361 # Step 3: use dynamic_keys filter kwargs
362 filtered_kwargs = {k: v for k, v in kwargs.items() if k not in dynamic_keys}
363
364 # Step 4: use filtered kwargs to get final processor instance
365 final_processor = cached_get_processor(
366 processor_name,
367 revision=revision,
368 trust_remote_code=trust_remote_code,
369 processor_cls=processor_cls, # type: ignore[arg-type]
370 **filtered_kwargs,
371 )
372
373 return final_processor
374
375
376 def cached_processor_from_config(
377 model_config: "ModelConfig",
378 processor_cls: type[_P] | tuple[type[_P], ...] = ProcessorMixin,
379 **kwargs: Any,
380 ) -> _P:
381 return cached_get_processor_without_dynamic_kwargs(
382 model_config.model,
383 revision=model_config.revision,
384 trust_remote_code=model_config.trust_remote_code,
385 processor_cls=processor_cls, # type: ignore[arg-type]
386 **_merge_mm_kwargs(model_config, processor_cls, **kwargs),
387 )
388
389
390 def get_feature_extractor(
391 processor_name: str,
392 *args: Any,
393 revision: str | None = None,
394 trust_remote_code: bool = False,
395 **kwargs: Any,
396 ):
397 """Load an audio feature extractor for the given model name
398 via HuggingFace."""
399 try:
400 processor_name = convert_model_repo_to_path(processor_name)
401 feature_extractor = AutoFeatureExtractor.from_pretrained(
402 processor_name,
403 *args,
404 revision=revision,
405 trust_remote_code=trust_remote_code,
406 **kwargs,
407 )
408 except ValueError as e:
409 # If the error pertains to the processor class not existing or not
410 # currently being imported, suggest using the --trust-remote-code flag.
411 # Unlike AutoTokenizer, AutoImageProcessor does not separate such errors
412 if not trust_remote_code:
413 err_msg = (
414 "Failed to load the feature extractor. If the feature "
415 "extractor is a custom extractor not yet available in the "
416 "HuggingFace transformers library, consider setting "
417 "`trust_remote_code=True` in LLM or using the "
418 "`--trust-remote-code` flag in the CLI."
419 )
420 raise RuntimeError(err_msg) from e
421 else:
422 raise e
423 return cast(FeatureExtractionMixin, feature_extractor)
424
425
426 cached_get_feature_extractor = lru_cache(get_feature_extractor)
427
428
429 def cached_feature_extractor_from_config(
430 model_config: "ModelConfig",
431 **kwargs: Any,
432 ):
433 return cached_get_feature_extractor(
434 model_config.model,
435 revision=model_config.revision,
436 trust_remote_code=model_config.trust_remote_code,
437 **_merge_mm_kwargs(model_config, AutoFeatureExtractor, **kwargs),
438 )
439
440
441 def get_image_processor(
442 processor_name: str,
443 *args: Any,
444 revision: str | None = None,
445 trust_remote_code: bool = False,
446 processor_cls_overrides: type[_I] | None = None,
447 **kwargs: Any,
448 ):
449 """Load an image processor for the given model name via HuggingFace."""
450 try:
451 processor_name = convert_model_repo_to_path(processor_name)
452 processor_cls = processor_cls_overrides or AutoImageProcessor
453 processor = processor_cls.from_pretrained(
454 processor_name,
455 *args,
456 revision=revision,
457 trust_remote_code=trust_remote_code,
458 **kwargs,
459 )
460 except ValueError as e:
461 # If the error pertains to the processor class not existing or not
462 # currently being imported, suggest using the --trust-remote-code flag.
463 # Unlike AutoTokenizer, AutoImageProcessor does not separate such errors
464 if not trust_remote_code:
465 err_msg = (
466 "Failed to load the image processor. If the image processor is "
467 "a custom processor not yet available in the HuggingFace "
468 "transformers library, consider setting "
469 "`trust_remote_code=True` in LLM or using the "
470 "`--trust-remote-code` flag in the CLI."
471 )
472 raise RuntimeError(err_msg) from e
473 else:
474 raise e
475
476 return cast(BaseImageProcessor, processor)
477
478
479 cached_get_image_processor = lru_cache(get_image_processor)
480
481
482 def cached_image_processor_from_config(
483 model_config: "ModelConfig",
484 **kwargs: Any,
485 ):
486 return cached_get_image_processor(
487 model_config.model,
488 revision=model_config.revision,
489 trust_remote_code=model_config.trust_remote_code,
490 **_merge_mm_kwargs(model_config, AutoImageProcessor, **kwargs),
491 )
492
493
494 def get_video_processor(
495 processor_name: str,
496 *args: Any,
497 revision: str | None = None,
498 trust_remote_code: bool = False,
499 processor_cls_overrides: type[_V] | None = None,
500 **kwargs: Any,
501 ):
502 """Load a video processor for the given model name via HuggingFace."""
503 try:
504 processor_name = convert_model_repo_to_path(processor_name)
505 processor_cls = processor_cls_overrides or AutoVideoProcessor
506 processor = processor_cls.from_pretrained(
507 processor_name,
508 *args,
509 revision=revision,
510 trust_remote_code=trust_remote_code,
511 **kwargs,
512 )
513 except ValueError as e:
514 # If the error pertains to the processor class not existing or not
515 # currently being imported, suggest using the --trust-remote-code flag.
516 # Unlike AutoTokenizer, AutoVideoProcessor does not separate such errors
517 if not trust_remote_code:
518 err_msg = (
519 "Failed to load the video processor. If the video processor is "
520 "a custom processor not yet available in the HuggingFace "
521 "transformers library, consider setting "
522 "`trust_remote_code=True` in LLM or using the "
523 "`--trust-remote-code` flag in the CLI."
524 )
525 raise RuntimeError(err_msg) from e
526 else:
527 raise e
528
529 return cast(BaseVideoProcessor, processor)
530
531
532 cached_get_video_processor = lru_cache(get_video_processor)
533
534
535 def cached_video_processor_from_config(
536 model_config: "ModelConfig",
537 processor_cls: type[_V] | None = None,
538 **kwargs: Any,
539 ):
540 return cached_get_video_processor(
541 model_config.model,
542 revision=model_config.revision,
543 trust_remote_code=model_config.trust_remote_code,
544 processor_cls_overrides=processor_cls, # type: ignore[arg-type]
545 **_merge_mm_kwargs(model_config, AutoVideoProcessor, **kwargs),
546 )