main
py 655 lines 24.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """PyTorch Ovis model."""
4
5 from collections.abc import Iterable, Mapping
6 from functools import partial
7 from typing import Annotated, Literal, TypedDict
8
9 import torch
10 import torch.nn as nn
11 from transformers import BaseImageProcessor, BatchFeature, PreTrainedConfig
12
13 from vllm.config import VllmConfig
14 from vllm.config.multimodal import MultiModalDummyOptions
15 from vllm.inputs import MultiModalDataDict
16 from vllm.model_executor.layers.linear import ReplicatedLinear
17 from vllm.model_executor.layers.quantization import QuantizationConfig
18 from vllm.model_executor.models.ovis import VisualEmbedding
19 from vllm.model_executor.models.siglip2navit import Siglip2NavitModel
20 from vllm.model_executor.models.utils import (
21 AutoWeightsLoader,
22 flatten_bn,
23 init_vllm_registered_model,
24 maybe_prefix,
25 )
26 from vllm.multimodal import MULTIMODAL_REGISTRY
27 from vllm.multimodal.inputs import (
28 MultiModalFieldConfig,
29 MultiModalKwargsItems,
30 )
31 from vllm.multimodal.parse import ImageSize, MultiModalDataItems
32 from vllm.multimodal.processing import (
33 BaseDummyInputsBuilder,
34 BaseMultiModalProcessor,
35 BaseProcessingInfo,
36 PromptReplacement,
37 )
38 from vllm.renderers import TokenizeParams
39 from vllm.sequence import IntermediateTensors
40 from vllm.transformers_utils.processors.ovis2_5 import Ovis2_5Processor
41 from vllm.utils.tensor_schema import TensorSchema, TensorShape
42
43 from .interfaces import (
44 MultiModalEmbeddings,
45 SupportsMultiModal,
46 SupportsPP,
47 supports_pp,
48 )
49
50 IMAGE_TOKEN = "<image>"
51 VIDEO_TOKEN = "<video>"
52 INDICATOR_IDS = [151672, 151673, 151674, 151675]
53 IMAGE_PAD_TOKEN_ID = 151655
54
55
56 class Ovis2_5ImagePatchInputs(TensorSchema):
57 """Dimensions:
58 - bnp: Batch size * number of images * number of patches
59 - patch_size: patch_size_x * patch_size_y * num_channels
60 - patch_indicators: Batch size * (number of patches + 1)
61 - bn: Batch size * number of images
62 """
63
64 type: Literal["image_patches"]
65 flat_data: Annotated[torch.Tensor, TensorShape("bnp", "patch_size")]
66 indicator_tokens: Annotated[torch.Tensor, TensorShape("patch_indicators")]
67 patches_per_item: Annotated[list[int], TensorShape("bn")]
68 grids: Annotated[torch.Tensor, TensorShape("bn", 3)]
69 # This is used to restore the first two dimensions of `flat_data`.
70
71
72 class Ovis2_5VideoPatchInputs(TensorSchema):
73 """Dimensions:
74 - bnp: Batch size * number of videos * number of patches
75 - patch_size: patch_size_x * patch_size_y * num_channels
76 - patch_indicators: Batch size * (number of patches + 1)
77 - bn: Batch size * number of videos
78 """
79
80 type: Literal["video_patches"]
81 flat_data: Annotated[torch.Tensor, TensorShape("bnp", "patch_size")]
82 indicator_tokens: Annotated[torch.Tensor, TensorShape("patch_indicators")]
83 patches_per_item: Annotated[list[int], TensorShape("bn")]
84 grids: Annotated[torch.Tensor, TensorShape("bn", 3)]
85 # This is used to restore the first two dimensions of `flat_data`.
86
87
88 class Ovis2_5MultiModalInputs(TypedDict, total=False):
89 images: Ovis2_5ImagePatchInputs | None
90 videos: Ovis2_5VideoPatchInputs | None
91
92
93 class VisualTokenizer(torch.nn.Module):
94 """VIT."""
95
96 def __init__(
97 self,
98 config: PreTrainedConfig,
99 visual_vocab_size: int,
100 quant_config: QuantizationConfig | None = None,
101 prefix: str = "",
102 ):
103 super().__init__()
104 self.config = config
105 self.vit = self._init_backbone(
106 config=config,
107 quant_config=quant_config,
108 prefix=f"{prefix}.vit",
109 )
110 # reserved tokens for INDICATOR_IDS
111 head_dim = visual_vocab_size - len(INDICATOR_IDS)
112 self.head = torch.nn.Sequential(
113 ReplicatedLinear(
114 self.config.hidden_size * self.config.hidden_stride**2,
115 head_dim,
116 bias=False,
117 return_bias=False,
118 ),
119 torch.nn.LayerNorm(head_dim),
120 )
121
122 def _init_backbone(
123 self,
124 config: PreTrainedConfig,
125 quant_config: QuantizationConfig | None = None,
126 prefix: str = "",
127 ):
128 model_type = config.model_type
129 if model_type == "siglip2_navit":
130 return Siglip2NavitModel(
131 config=config,
132 quant_config=quant_config,
133 prefix=prefix,
134 )
135 raise ValueError(f"Unsupported visual tokenizer model_type: {model_type}")
136
137 @property
138 def dtype(self) -> torch.dtype:
139 return next(self.head.parameters()).dtype
140
141 @property
142 def device(self) -> torch.device:
143 return next(self.head.parameters()).device
144
145 def tokenize(self, logits: torch.Tensor) -> torch.Tensor:
146 tokens = torch.softmax(logits, dim=-1, dtype=torch.float32).to(logits.dtype)
147 return tokens
148
149 def encode(
150 self, pixel_values: torch.Tensor, grid_thws: torch.Tensor
151 ) -> torch.Tensor:
152 features = self.vit(pixel_values, grid_thws)
153 # refer to qwen2.5-vl patchmerger
154 seq_len, _ = features.shape
155 features = features.reshape(seq_len // (self.config.hidden_stride**2), -1)
156
157 return features
158
159 def forward(
160 self, pixel_values: torch.Tensor, grid_thws: torch.Tensor
161 ) -> torch.Tensor:
162 features = self.encode(pixel_values, grid_thws)
163 logits = self.head(features)
164 tokens = self.tokenize(logits)
165 # tokens' shape is [#Token, VocabSize-4],
166 # so padding with [#Token, 4], after which,
167 # tokens' shape should become [#Token, VocabSize];
168 tokens = torch.nn.functional.pad(
169 tokens,
170 (0, len(INDICATOR_IDS)),
171 mode="constant",
172 value=0,
173 )
174 return tokens
175
176
177 class Ovis2_5ProcessingInfo(BaseProcessingInfo):
178 def __init__(self, ctx) -> None:
179 super().__init__(ctx)
180 # extra_special_tokens adds tokens to the tokenizer lazily, on first
181 # access, but the renderer deep-copies this tokenizer into a thread
182 # pool right after this processor is constructed; the copies never
183 # see mutations made afterwards. Force it now, before that happens,
184 # on the tokenizer we actually use (get_hf_processor()'s cache can
185 # otherwise hand back one bound to a self-loaded tokenizer instead).
186 hf_processor = self.get_hf_processor()
187 hf_processor.tokenizer = self.get_tokenizer()
188 hf_processor.extra_special_tokens # noqa: B018
189
190 def get_hf_config(self):
191 return self.ctx.get_hf_config()
192
193 def get_hf_processor(self, **kwargs):
194 vit_config = self.get_hf_config().vit_config
195 return self.ctx.get_hf_processor(
196 Ovis2_5Processor,
197 patch_size=vit_config.patch_size,
198 hidden_stride=vit_config.hidden_stride,
199 temporal_patch_size=vit_config.temporal_patch_size,
200 )
201
202 def get_default_tok_params(self) -> TokenizeParams:
203 return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
204
205 def get_image_processor(self) -> BaseImageProcessor:
206 return self.get_hf_processor().image_processor # type: ignore
207
208 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
209 return {"image": None, "video": 1}
210
211 def get_image_size_with_most_features(self) -> ImageSize:
212 # NOTE(myselvess): max_pixels 1792 * 1792 hardcoded in original code
213 # TODO(myselvess): Be adjusted based on the max_pixels
214 return ImageSize(width=1792, height=1792)
215
216 def get_num_image_tokens(
217 self,
218 *,
219 image_width: int,
220 image_height: int,
221 num_frames: int = 1,
222 ) -> int:
223 hf_config = self.get_hf_config()
224 vit_config = hf_config.vit_config
225 patch_size = vit_config.patch_size
226 temporal_patch_size = vit_config.temporal_patch_size
227 # NOTE: Frames are padded to be divisible by `temporal_patch_size`
228 # https://github.com/huggingface/transformers/blob/v4.48.3/src/transformers/models/qwen2_vl/image_processing_qwen2_vl.py#L294
229 padded_num_frames = num_frames + (-num_frames % temporal_patch_size)
230 grid_t = max(padded_num_frames // temporal_patch_size, 1)
231 grid_h = image_height // patch_size
232 grid_w = image_width // patch_size
233 num_patches = grid_t * grid_h * grid_w
234 num_vision_tokens = num_patches
235 return num_vision_tokens
236
237 def get_max_image_tokens(self) -> int:
238 target_width, target_height = self.get_image_size_with_most_features()
239 return self.get_num_image_tokens(
240 image_width=target_width, image_height=target_height
241 )
242
243 def _get_max_video_frames(self, max_tokens: int) -> int:
244 target_width, target_height = self.get_image_size_with_most_features()
245 num_frames = 0
246 while True:
247 next_num_frames = num_frames + 1
248 next_max_tokens = self.get_num_video_tokens(
249 image_width=target_width,
250 image_height=target_height,
251 num_frames=next_num_frames,
252 )
253 if next_max_tokens > max_tokens:
254 break
255 num_frames = next_num_frames
256 return num_frames
257
258 def get_num_frames_with_most_features(
259 self,
260 seq_len: int,
261 mm_counts: Mapping[str, int],
262 ) -> int:
263 max_images = mm_counts.get("image", 0)
264 max_videos = mm_counts.get("video", 0)
265 max_image_tokens = self.get_max_image_tokens() * max_images
266 max_total_frames = self._get_max_video_frames(seq_len - max_image_tokens)
267 max_frames_per_video = max_total_frames // max(max_videos, 1)
268 return max(max_frames_per_video, 1)
269
270 def get_num_video_tokens(
271 self,
272 *,
273 image_width: int,
274 image_height: int,
275 num_frames: int,
276 ) -> int:
277 num_video_tokens = self.get_num_image_tokens(
278 image_width=image_width, image_height=image_height, num_frames=num_frames
279 )
280 return num_video_tokens
281
282 def get_max_video_tokens(
283 self,
284 seq_len: int,
285 mm_counts: Mapping[str, int],
286 ) -> int:
287 target_width, target_height = self.get_image_size_with_most_features()
288 return self.get_num_video_tokens(
289 image_width=target_width,
290 image_height=target_height,
291 num_frames=self.get_num_frames_with_most_features(seq_len, mm_counts),
292 )
293
294
295 class Ovis2_5DummyInputsBuilder(BaseDummyInputsBuilder[Ovis2_5ProcessingInfo]):
296 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
297 num_images = mm_counts.get("image", 0)
298 num_videos = mm_counts.get("video", 0)
299 return IMAGE_TOKEN * num_images + VIDEO_TOKEN * num_videos
300
301 def get_dummy_mm_data(
302 self,
303 seq_len: int,
304 mm_counts: Mapping[str, int],
305 mm_options: MultiModalDummyOptions,
306 ) -> MultiModalDataDict:
307 target_width, target_height = self.info.get_image_size_with_most_features()
308 target_num_frames = self.info.get_num_frames_with_most_features(
309 seq_len, mm_counts
310 )
311
312 mm_data = {
313 "image": self._get_dummy_images(
314 width=target_width,
315 height=target_height,
316 num_images=mm_counts.get("image", 0),
317 overrides=mm_options.get("image"),
318 ),
319 "video": self._get_dummy_videos(
320 width=target_width,
321 height=target_height,
322 num_frames=target_num_frames,
323 num_videos=mm_counts.get("video", 0),
324 overrides=mm_options.get("video"),
325 ),
326 }
327 return mm_data
328
329
330 class Ovis2_5MultiModalProcessor(BaseMultiModalProcessor[Ovis2_5ProcessingInfo]):
331 def visual_indicators_to_visual_tokens(
332 self,
333 visual_indicators: list[int],
334 ) -> list[int]:
335 """Filter image indicators placeholders and convert them to corresponding
336 tokens in visual tokenizer.
337 """
338 hf_config = self.info.get_hf_config()
339 vte_vocab_size = hf_config.visual_vocab_size
340 return [
341 vte_vocab_size - len(INDICATOR_IDS) + (x - INDICATOR_IDS[0])
342 for x in visual_indicators
343 if x >= INDICATOR_IDS[0]
344 ]
345
346 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
347 return self.dummy_inputs.get_dummy_text(mm_counts)
348
349 def _postprocess_hf_mm_data(
350 self,
351 mm_data: Mapping[str, object],
352 hf_processor_mm_kwargs: Mapping[str, object],
353 processed_data: BatchFeature,
354 ) -> BatchFeature:
355 if not mm_data:
356 return processed_data
357
358 hf_processor = self.info.get_hf_processor()
359
360 if "videos" in mm_data:
361 visual_indicators = [
362 hf_processor.construct_visual_indicators((1, 1, 1), True)
363 for grid in processed_data["video_grids"]
364 ]
365 indicator_tokens = [
366 self.visual_indicators_to_visual_tokens(indicator)
367 for indicator in visual_indicators
368 ]
369 processed_data["video_indicator_tokens"] = torch.tensor(indicator_tokens)
370 if "images" in mm_data:
371 visual_indicators = [
372 hf_processor.construct_visual_indicators((1, 1, 1), False)
373 for grid in processed_data["grids"]
374 ]
375 indicator_tokens = [
376 self.visual_indicators_to_visual_tokens(indicator)
377 for indicator in visual_indicators
378 ]
379
380 processed_data["indicator_tokens"] = torch.tensor(indicator_tokens)
381
382 return processed_data
383
384 def _get_mm_fields_config(
385 self,
386 hf_inputs: BatchFeature,
387 hf_processor_mm_kwargs: Mapping[str, object],
388 ) -> Mapping[str, MultiModalFieldConfig]:
389 return dict(
390 pixel_values=MultiModalFieldConfig.batched("image"),
391 grids=MultiModalFieldConfig.batched("image"),
392 indicator_tokens=MultiModalFieldConfig.batched("image"),
393 video_pixel_values=MultiModalFieldConfig.batched("video"),
394 video_indicator_tokens=MultiModalFieldConfig.batched("video"),
395 video_grids=MultiModalFieldConfig.batched("video"),
396 )
397
398 def _get_prompt_updates(
399 self,
400 mm_items: MultiModalDataItems,
401 hf_processor_mm_kwargs: Mapping[str, object],
402 out_mm_kwargs: MultiModalKwargsItems,
403 ) -> list[PromptReplacement]:
404 hf_processor = self.info.get_hf_processor()
405 placeholder = {
406 "image": hf_processor.get_token_value("image_token"),
407 "video": hf_processor.get_token_value("video_token"),
408 }
409
410 def get_replacement_ovis(item_idx, modality: str):
411 if modality == "image":
412 out_item = out_mm_kwargs["image"][item_idx]
413 grid = out_item["grids"].data
414 elif modality == "video":
415 out_item = out_mm_kwargs["video"][item_idx]
416 grid = out_item["video_grids"].data
417 hf_processor = self.info.get_hf_processor()
418 return hf_processor.construct_visual_placeholders(
419 grid[0],
420 )
421
422 return [
423 PromptReplacement(
424 modality=modality,
425 target=[placeholder[modality]],
426 replacement=partial(get_replacement_ovis, modality=modality),
427 )
428 for modality in ("image", "video")
429 ]
430
431
432 @MULTIMODAL_REGISTRY.register_processor(
433 Ovis2_5MultiModalProcessor,
434 info=Ovis2_5ProcessingInfo,
435 dummy_inputs=Ovis2_5DummyInputsBuilder,
436 )
437 class Ovis2_5(nn.Module, SupportsMultiModal, SupportsPP):
438 @classmethod
439 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
440 if modality.startswith("image"):
441 return IMAGE_TOKEN
442 if modality.startswith("video"):
443 return VIDEO_TOKEN
444
445 raise ValueError("Only image or video modality is supported")
446
447 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
448 super().__init__()
449 config = vllm_config.model_config.hf_config
450 quant_config = vllm_config.quant_config
451
452 self.config: PreTrainedConfig = config
453
454 with self._mark_language_model(vllm_config):
455 self.llm = init_vllm_registered_model(
456 vllm_config=vllm_config.with_hf_config(config.text_config),
457 prefix=maybe_prefix(prefix, "llm"),
458 )
459
460 with self._mark_tower_model(vllm_config, {"image", "video"}):
461 self.visual_tokenizer = VisualTokenizer(
462 config=config.vit_config,
463 visual_vocab_size=config.visual_vocab_size,
464 quant_config=quant_config,
465 prefix=maybe_prefix(prefix, "visual_tokenizer"),
466 )
467 self.vte = VisualEmbedding(config.visual_vocab_size, config.hidden_size)
468
469 self.image_pad_token_id: int = IMAGE_PAD_TOKEN_ID
470
471 language_model = self.get_language_model()
472 assert supports_pp(language_model)
473 self.make_empty_intermediate_tensors = (
474 language_model.make_empty_intermediate_tensors
475 )
476
477 def _parse_and_validate_image_input(
478 self, **kwargs: object
479 ) -> Ovis2_5ImagePatchInputs | None:
480 pixel_values = kwargs.pop("pixel_values", None)
481 indicator_tokens = kwargs.pop("indicator_tokens", None)
482 grids = kwargs.pop("grids", None)
483 if pixel_values is None and indicator_tokens is None:
484 return None
485
486 if pixel_values is not None and indicator_tokens is not None:
487 if not isinstance(pixel_values, (torch.Tensor, list)):
488 raise ValueError(
489 f"Incorrect type of pixel values. Got type: {type(pixel_values)}"
490 )
491
492 if not isinstance(indicator_tokens, (torch.Tensor, list)):
493 raise ValueError(
494 "Incorrect type of indicator_tokens. "
495 f"Got type: {type(indicator_tokens)}"
496 )
497
498 return Ovis2_5ImagePatchInputs(
499 type="image_patches",
500 flat_data=flatten_bn(pixel_values, concat=True),
501 patches_per_item=[
502 x.shape[0] // (self.config.vit_config.hidden_stride**2)
503 for x in pixel_values
504 ],
505 indicator_tokens=flatten_bn(indicator_tokens, concat=True),
506 grids=flatten_bn(grids, concat=True),
507 )
508
509 raise AssertionError("This line should be unreachable.")
510
511 def _parse_and_validate_video_input(
512 self, **kwargs: object
513 ) -> Ovis2_5VideoPatchInputs | None:
514 pixel_values = kwargs.pop("video_pixel_values", None)
515 indicator_tokens = kwargs.pop("video_indicator_tokens", None)
516 grids = kwargs.pop("video_grids", None)
517 if pixel_values is None and indicator_tokens is None:
518 return None
519
520 if pixel_values is not None and indicator_tokens is not None:
521 if not isinstance(pixel_values, (torch.Tensor, list)):
522 raise ValueError(
523 f"Incorrect type of pixel values. Got type: {type(pixel_values)}"
524 )
525
526 if not isinstance(indicator_tokens, (torch.Tensor, list)):
527 raise ValueError(
528 "Incorrect type of indicator_tokens. "
529 f"Got type: {type(indicator_tokens)}"
530 )
531
532 return Ovis2_5VideoPatchInputs(
533 type="video_patches",
534 flat_data=flatten_bn(pixel_values, concat=True),
535 patches_per_item=[
536 x.shape[0] // (self.config.vit_config.hidden_stride**2)
537 for x in pixel_values
538 ],
539 indicator_tokens=flatten_bn(indicator_tokens, concat=True),
540 grids=flatten_bn(grids, concat=True),
541 )
542
543 raise AssertionError("This line should be unreachable.")
544
545 def _process_visual_input(
546 self, visual_input: Ovis2_5ImagePatchInputs | Ovis2_5VideoPatchInputs
547 ) -> MultiModalEmbeddings:
548 image_patches_flat = visual_input["flat_data"]
549 patches_per_image = visual_input["patches_per_item"]
550 indicator_tokens = visual_input["indicator_tokens"]
551 grid_thws = visual_input["grids"]
552
553 indicator_per_image = list(
554 map(lambda x: 2 if x > 1 else x + 2, patches_per_image)
555 )
556
557 target_dtype = self.visual_tokenizer.dtype
558 visual_tokens = self.visual_tokenizer(
559 image_patches_flat.to(target_dtype), grid_thws
560 )
561
562 visual_embeds = self.vte(visual_tokens) # 1:1 numeric eq.
563 indicator_embeds = self.vte(indicator_tokens)
564
565 visual_embeds_per_image = visual_embeds.split(patches_per_image, dim=0)
566 indicator_embeds_per_image = indicator_embeds.split(indicator_per_image)
567
568 vision_embeddings = []
569 for indicator, visual in zip(
570 indicator_embeds_per_image, visual_embeds_per_image
571 ):
572 vision_embeddings_per_image = []
573 visual = visual.unsqueeze(0)
574 for i in range(visual.shape[0]):
575 vision_embeddings_per_image.append(
576 torch.cat([indicator[i : i + 1], visual[i]], dim=0)
577 )
578 vision_embeddings_per_image.append(indicator[i + 1 :])
579 vision_embeddings.append(torch.cat(vision_embeddings_per_image, dim=0))
580 return tuple(vision_embeddings)
581
582 def _parse_and_validate_multimodal_inputs(
583 self, **kwargs: object
584 ) -> Ovis2_5MultiModalInputs:
585 modalities: Ovis2_5MultiModalInputs = {}
586
587 # Preserve the order of modalities if there are multiple of them
588 # from the order of kwargs.
589 for input_key in kwargs:
590 if (
591 input_key in ("pixel_values", "indicator_tokens", "grids")
592 and "images" not in modalities
593 ):
594 modalities["images"] = self._parse_and_validate_image_input(**kwargs)
595 if (
596 input_key
597 in ("video_pixel_values", "video_indicator_tokens", "video_grids")
598 and "videos" not in modalities
599 ):
600 modalities["videos"] = self._parse_and_validate_video_input(**kwargs)
601
602 return modalities
603
604 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
605 modalities = self._parse_and_validate_multimodal_inputs(**kwargs)
606 if not modalities:
607 return []
608
609 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
610 # NOTE: It is important to iterate over the keys in this dictionary
611 # to preserve the order of the modalities.
612 for modality in modalities:
613 if modality == "images":
614 image_input = modalities["images"]
615 assert image_input is not None
616 image_embeddings = self._process_visual_input(image_input)
617 multimodal_embeddings += tuple(image_embeddings)
618 if modality == "videos":
619 video_input = modalities["videos"]
620 assert video_input is not None
621 video_embeddings = self._process_visual_input(video_input)
622 multimodal_embeddings += tuple(video_embeddings)
623
624 return multimodal_embeddings
625
626 def forward(
627 self,
628 input_ids: torch.Tensor | None,
629 positions: torch.Tensor,
630 intermediate_tensors: IntermediateTensors | None = None,
631 inputs_embeds: torch.Tensor | None = None,
632 **kwargs: object,
633 ) -> torch.Tensor | IntermediateTensors:
634 if intermediate_tensors is not None:
635 inputs_embeds = None
636
637 # up until here we have a inputs_embeds 100% numerical identity
638 # between the OG HF Transformers implementation and ours
639 hidden_states = self.llm(
640 input_ids=input_ids,
641 positions=positions,
642 intermediate_tensors=intermediate_tensors,
643 inputs_embeds=inputs_embeds,
644 )
645 return hidden_states
646
647 def compute_logits(
648 self,
649 hidden_states: torch.Tensor,
650 ) -> torch.Tensor | None:
651 return self.llm.compute_logits(hidden_states)
652
653 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
654 loader = AutoWeightsLoader(self)
655 return loader.load_weights(weights)