| 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 | ) |