main
py 524 lines 20.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from __future__ import annotations
4
5 import dataclasses
6 import glob
7 import os
8 import time
9 from collections.abc import Generator, Iterable
10 from typing import TYPE_CHECKING, cast
11
12 import torch
13 from torch import nn
14 from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
15
16 from vllm.config import ModelConfig
17 from vllm.config.load import LoadConfig
18 from vllm.logger import init_logger
19 from vllm.model_executor.layers.quantization.torchao import torchao_version_at_least
20 from vllm.model_executor.model_loader.base_loader import BaseModelLoader
21 from vllm.model_executor.model_loader.ep_weight_filter import (
22 compute_local_expert_ids,
23 )
24 from vllm.model_executor.model_loader.weight_utils import (
25 download_safetensors_index_file_from_hf,
26 download_weights_from_hf,
27 fastsafetensors_weights_iterator,
28 filter_duplicate_safetensors_files,
29 filter_files_not_needed_for_inference,
30 filter_mm_encoder_only_safetensors_files,
31 get_quant_config,
32 instanttensor_weights_iterator,
33 maybe_download_from_modelscope,
34 multi_thread_pt_weights_iterator,
35 multi_thread_safetensors_weights_iterator,
36 np_cache_weights_iterator,
37 pt_weights_iterator,
38 resolve_mm_encoder_only_lm_prefixes,
39 safetensors_weights_iterator,
40 )
41 from vllm.tracing import instrument
42 from vllm.transformers_utils.repo_utils import list_filtered_repo_files
43
44 if TYPE_CHECKING:
45 from vllm.model_executor.models.utils import WeightsMapper
46
47 logger = init_logger(__name__)
48
49
50 class DefaultModelLoader(BaseModelLoader):
51 """Model loader that can load different file types from disk."""
52
53 # default number of thread when enable multithread weight loading
54 DEFAULT_NUM_THREADS = 8
55
56 @dataclasses.dataclass
57 class Source:
58 """A source for weights."""
59
60 model_or_path: str
61 """The model ID or path."""
62
63 revision: str | None
64 """The optional model revision."""
65
66 subfolder: str | None = None
67 """The subfolder inside the model repo."""
68
69 prefix: str = ""
70 """A prefix to prepend to all weights."""
71
72 fall_back_to_pt: bool = True
73 """Whether .pt weights can be used."""
74
75 allow_patterns_overrides: list[str] | None = None
76 """If defined, weights will load exclusively using these patterns."""
77
78 counter_before_loading_weights: float = 0.0
79 counter_after_loading_weights: float = 0.0
80
81 def __init__(self, load_config: LoadConfig):
82 super().__init__(load_config)
83 self.local_expert_ids: set[int] | None = None
84 # Set in load_weights when --mm-encoder-only; used to drop LM-only shards.
85 self._encoder_only_lm_prefixes: tuple[str, ...] | None = None
86 self._encoder_only_weights_mapper: WeightsMapper | None = None
87
88 extra_config = load_config.model_loader_extra_config
89 if not isinstance(extra_config, dict):
90 raise ValueError(
91 f"model_loader_extra_config must be a dict for load format "
92 f"{load_config.load_format}, got {type(extra_config).__name__}"
93 )
94 allowed_keys = {
95 "enable_multithread_load",
96 "num_threads",
97 "enable_weights_track",
98 }
99 unexpected_keys = set(extra_config.keys()) - allowed_keys
100
101 if unexpected_keys:
102 raise ValueError(
103 f"Unexpected extra config keys for load format "
104 f"{load_config.load_format}: "
105 f"{unexpected_keys}"
106 )
107
108 enable_multithread_load = extra_config.get("enable_multithread_load", False)
109 if not isinstance(enable_multithread_load, bool):
110 raise ValueError(
111 f"enable_multithread_load must be a bool, got "
112 f"{type(enable_multithread_load).__name__}"
113 )
114 num_threads = extra_config.get("num_threads")
115 if num_threads is not None and not (
116 isinstance(num_threads, int) and num_threads > 0
117 ):
118 raise ValueError(
119 f"num_threads must be a positive integer, got {num_threads!r}"
120 )
121
122 self.enable_weights_track: bool | None = extra_config.get(
123 "enable_weights_track", None
124 )
125
126 # The multi-thread loader ignores safetensors_load_strategy, so reject
127 # the combination instead of silently dropping the requested strategy.
128 if extra_config.get("enable_multithread_load") and (
129 load_config.safetensors_load_strategy not in (None, "lazy")
130 ):
131 raise ValueError(
132 "enable_multithread_load does not support "
133 "safetensors_load_strategy="
134 f"{load_config.safetensors_load_strategy!r}; the multi-thread "
135 "loader only implements the default lazy strategy."
136 )
137
138 def _prepare_weights(
139 self,
140 model_name_or_path: str,
141 subfolder: str | None,
142 revision: str | None,
143 fall_back_to_pt: bool,
144 allow_patterns_overrides: list[str] | None,
145 ) -> tuple[str, list[str], bool, str]:
146 """Prepare weights for the model.
147
148 If the model is not local, it will be downloaded."""
149 model_name_or_path = (
150 maybe_download_from_modelscope(model_name_or_path, revision)
151 or model_name_or_path
152 )
153
154 is_local = os.path.isdir(model_name_or_path)
155 load_format = self.load_config.load_format
156 use_safetensors = False
157 index_file = SAFE_WEIGHTS_INDEX_NAME
158
159 # First check for 'auto' format that mistral files format are present.
160 # This is to load mistral models with official format by default.
161 if load_format == "auto":
162 load_format = (
163 "mistral"
164 if len(
165 list_filtered_repo_files(
166 model_name_or_path=model_name_or_path,
167 allow_patterns=["consolidated*.safetensors"],
168 revision=revision,
169 )
170 )
171 > 0
172 else "hf"
173 )
174
175 # Some quantized models use .pt files for storing the weights.
176 if load_format == "hf":
177 allow_patterns = ["*.safetensors", "*.bin"]
178 elif (
179 load_format == "safetensors"
180 or load_format == "fastsafetensors"
181 or load_format == "instanttensor"
182 ):
183 use_safetensors = True
184 allow_patterns = ["*.safetensors"]
185 elif load_format == "mistral":
186 use_safetensors = True
187 allow_patterns = ["consolidated*.safetensors"]
188 index_file = "consolidated.safetensors.index.json"
189 elif load_format == "pt":
190 allow_patterns = ["*.pt"]
191 elif load_format == "npcache":
192 allow_patterns = ["*.bin"]
193 else:
194 raise ValueError(f"Unknown load_format: {load_format}")
195
196 # Don't fall back to .pt for explicit safetensors formats; otherwise a
197 # .pt file is matched and later opened as safetensors.
198 if fall_back_to_pt and not use_safetensors:
199 allow_patterns += ["*.pt"]
200
201 if allow_patterns_overrides is not None:
202 allow_patterns = allow_patterns_overrides
203
204 if not is_local:
205 hf_folder = download_weights_from_hf(
206 model_name_or_path,
207 self.load_config.download_dir,
208 allow_patterns,
209 revision,
210 subfolder=subfolder,
211 ignore_patterns=self.load_config.ignore_patterns,
212 )
213 else:
214 hf_folder = model_name_or_path
215
216 if subfolder is not None:
217 hf_folder = os.path.join(hf_folder, subfolder)
218
219 hf_weights_files: list[str] = []
220 for pattern in allow_patterns:
221 hf_weights_files += glob.glob(os.path.join(hf_folder, pattern))
222 if len(hf_weights_files) > 0:
223 if pattern.endswith(".safetensors"):
224 use_safetensors = True
225 break
226
227 if use_safetensors:
228 # For models like Mistral-7B-Instruct-v0.3
229 # there are both sharded safetensors files and a consolidated
230 # safetensors file. Using both breaks.
231 # Here, we download the `model.safetensors.index.json` and filter
232 # any files not found in the index.
233 if not is_local and len(hf_weights_files) > 1:
234 download_safetensors_index_file_from_hf(
235 model_name_or_path,
236 index_file,
237 cache_dir=self.load_config.download_dir,
238 subfolder=subfolder,
239 revision=revision,
240 )
241 hf_weights_files = filter_duplicate_safetensors_files(
242 hf_weights_files, hf_folder, index_file
243 )
244 else:
245 hf_weights_files = filter_files_not_needed_for_inference(hf_weights_files)
246
247 if len(hf_weights_files) == 0:
248 raise RuntimeError(
249 f"Cannot find any model weights with `{model_name_or_path}`"
250 )
251
252 return hf_folder, hf_weights_files, use_safetensors, index_file
253
254 def _get_weights_iterator(
255 self, source: Source
256 ) -> Generator[tuple[str, torch.Tensor], None, None]:
257 """Get an iterator for the model weights based on the load format."""
258 extra_config = self.load_config.model_loader_extra_config
259 hf_folder, hf_weights_files, use_safetensors, index_file = (
260 self._prepare_weights(
261 source.model_or_path,
262 source.subfolder,
263 source.revision,
264 source.fall_back_to_pt,
265 source.allow_patterns_overrides,
266 )
267 )
268 if (
269 self._encoder_only_lm_prefixes is not None
270 and use_safetensors
271 and hf_weights_files
272 ):
273 hf_weights_files = filter_mm_encoder_only_safetensors_files(
274 hf_weights_files,
275 hf_folder,
276 index_file,
277 self._encoder_only_lm_prefixes,
278 weights_mapper=self._encoder_only_weights_mapper,
279 )
280 if not hf_weights_files:
281 raise RuntimeError(
282 "mm-encoder-only shard filter removed all weight files for "
283 f"`{source.model_or_path}`; check language_model prefixes "
284 f"{self._encoder_only_lm_prefixes}"
285 )
286 if self.load_config.load_format == "npcache":
287 # Currently np_cache only support *.bin checkpoints
288 assert use_safetensors is False
289 weights_iterator = np_cache_weights_iterator(
290 source.model_or_path,
291 self.load_config.download_dir,
292 hf_folder,
293 hf_weights_files,
294 self.load_config.use_tqdm_on_load,
295 )
296 elif use_safetensors:
297 if self.load_config.load_format == "fastsafetensors":
298 weights_iterator = fastsafetensors_weights_iterator(
299 hf_weights_files,
300 self.load_config.use_tqdm_on_load,
301 )
302 elif self.load_config.load_format == "instanttensor":
303 weights_iterator = instanttensor_weights_iterator(
304 hf_weights_files,
305 self.load_config.use_tqdm_on_load,
306 )
307 else:
308 if extra_config.get("enable_multithread_load"):
309 weights_iterator = multi_thread_safetensors_weights_iterator(
310 hf_weights_files,
311 self.load_config.use_tqdm_on_load,
312 max_workers=extra_config.get(
313 "num_threads", self.DEFAULT_NUM_THREADS
314 ),
315 )
316 else:
317 weights_iterator = safetensors_weights_iterator(
318 hf_weights_files,
319 self.load_config.use_tqdm_on_load,
320 self.load_config.safetensors_load_strategy,
321 local_expert_ids=self.local_expert_ids,
322 safetensors_prefetch_num_threads=(
323 self.load_config.safetensors_prefetch_num_threads
324 ),
325 safetensors_prefetch_block_size=(
326 self.load_config.safetensors_prefetch_block_size
327 ),
328 )
329 else:
330 if extra_config.get("enable_multithread_load"):
331 weights_iterator = multi_thread_pt_weights_iterator(
332 hf_weights_files,
333 self.load_config.use_tqdm_on_load,
334 self.load_config.pt_load_map_location,
335 max_workers=extra_config.get(
336 "num_threads", self.DEFAULT_NUM_THREADS
337 ),
338 )
339 else:
340 weights_iterator = pt_weights_iterator(
341 hf_weights_files,
342 self.load_config.use_tqdm_on_load,
343 self.load_config.pt_load_map_location,
344 )
345
346 if self.counter_before_loading_weights == 0.0:
347 self.counter_before_loading_weights = time.perf_counter()
348 # Apply the prefix.
349 return ((source.prefix + name, tensor) for (name, tensor) in weights_iterator)
350
351 def get_all_weights(
352 self,
353 model_config: ModelConfig,
354 model: nn.Module,
355 ) -> Generator[tuple[str, torch.Tensor], None, None]:
356 primary_weights = DefaultModelLoader.Source(
357 model_config.model,
358 model_config.revision,
359 prefix="",
360 fall_back_to_pt=getattr(model, "fall_back_to_pt_during_load", True),
361 allow_patterns_overrides=getattr(model, "allow_patterns_overrides", None),
362 )
363 yield from self._get_weights_iterator(primary_weights)
364
365 secondary_weights = cast(
366 Iterable[DefaultModelLoader.Source],
367 getattr(model, "secondary_weights", ()),
368 )
369 for source in secondary_weights:
370 yield from self._get_weights_iterator(source)
371
372 def download_model(self, model_config: ModelConfig) -> None:
373 self._prepare_weights(
374 model_name_or_path=model_config.model,
375 subfolder=None,
376 revision=model_config.revision,
377 fall_back_to_pt=True,
378 allow_patterns_overrides=None,
379 )
380
381 def _init_mm_encoder_only_weight_filter(
382 self, model: nn.Module, model_config: ModelConfig
383 ) -> None:
384 """Skip pure language-model safetensors shards under --mm-encoder-only."""
385 mm_config = model_config.multimodal_config
386 if mm_config is None or not mm_config.mm_encoder_only:
387 self._encoder_only_lm_prefixes = None
388 self._encoder_only_weights_mapper = None
389 return
390
391 # Derive from _language_model_names; fail-closed on shared HF roots
392 # (Molmo/Phi-4-MM/Muse). Qwen nested/flat keys classified via mapper.
393 weights_mapper = cast(
394 "WeightsMapper | None", getattr(model, "hf_to_vllm_mapper", None)
395 )
396 self._encoder_only_lm_prefixes = resolve_mm_encoder_only_lm_prefixes(
397 getattr(model, "_language_model_names", None),
398 weights_mapper=weights_mapper,
399 )
400 self._encoder_only_weights_mapper = (
401 weights_mapper if self._encoder_only_lm_prefixes is not None else None
402 )
403
404 def _init_ep_weight_filter(self, model_config: ModelConfig) -> None:
405 """Compute local expert ids for EP weight filtering.
406
407 When expert parallelism is active, each rank only needs a subset of
408 expert weights. By computing the set upfront we can skip non-local
409 expert tensors *before* reading them from disk.
410 """
411 from vllm.config import get_current_vllm_config
412
413 vllm_config = get_current_vllm_config()
414 parallel_config = vllm_config.parallel_config
415
416 if not (
417 model_config.is_moe
418 and parallel_config.enable_expert_parallel
419 and parallel_config.enable_ep_weight_filter
420 ):
421 return
422
423 # When EPLB is enabled, redundant physical expert slots may map to
424 # logical experts that belong to other ranks in the default partition.
425 # The weight loader needs to see ALL logical expert weights so it can
426 # populate these redundant slots. Skip the filter entirely.
427 if parallel_config.enable_eplb:
428 return
429
430 num_experts = model_config.get_num_experts()
431 if num_experts <= 0:
432 return
433
434 # EP size/rank computation mirrors FusedMoEParallelConfig.make():
435 # ep_size = dp_size * pcp_size * tp_size (flattened)
436 # ep_rank = dp_rank * pcp_size * tp_size + pcp_rank * tp_size + tp_rank
437 from vllm.distributed import (
438 get_dp_group,
439 get_pcp_group,
440 get_tensor_model_parallel_rank,
441 )
442
443 dp_size = parallel_config.data_parallel_size
444 tp_size = parallel_config.tensor_parallel_size
445 pcp_size = parallel_config.prefill_context_parallel_size
446 dp_rank = get_dp_group().rank_in_group if dp_size > 1 else 0
447 tp_rank = get_tensor_model_parallel_rank() if tp_size > 1 else 0
448 pcp_rank = get_pcp_group().rank_in_group if pcp_size > 1 else 0
449 ep_size = dp_size * pcp_size * tp_size
450 ep_rank = dp_rank * pcp_size * tp_size + pcp_rank * tp_size + tp_rank
451
452 self.local_expert_ids = compute_local_expert_ids(
453 num_experts,
454 ep_size,
455 ep_rank,
456 placement=parallel_config.expert_placement_strategy,
457 )
458 if self.local_expert_ids is not None:
459 logger.info_once(
460 "EP weight filter: ep_size=%d, ep_rank=%d, loading %d/%d experts",
461 ep_size,
462 ep_rank,
463 len(self.local_expert_ids),
464 num_experts,
465 )
466
467 @instrument(span_name="Load weights")
468 def load_weights(self, model: nn.Module, model_config: ModelConfig) -> None:
469 if model_config.quantization == "torchao":
470 quant_config = get_quant_config(model_config, self.load_config)
471 if (
472 hasattr(quant_config, "is_checkpoint_torchao_serialized")
473 and quant_config.is_checkpoint_torchao_serialized
474 and torchao_version_at_least("0.15.0")
475 ):
476 self.load_config.safetensors_load_strategy = "torchao"
477
478 self._init_ep_weight_filter(model_config)
479 self._init_mm_encoder_only_weight_filter(model, model_config)
480
481 loaded_weights = model.load_weights(self.get_all_weights(model_config, model))
482
483 self.counter_after_loading_weights = time.perf_counter()
484 logger.info_once(
485 "Loading weights took %.2f seconds",
486 self.counter_after_loading_weights - self.counter_before_loading_weights,
487 )
488 # We only enable strict check for non-quantized models
489 # that have loaded weights tracking by default.
490 default_enable_weights_track = (
491 model_config.quantization is None and loaded_weights is not None
492 )
493 enable_weights_track = (
494 self.enable_weights_track
495 if self.enable_weights_track is not None
496 else default_enable_weights_track
497 )
498 if enable_weights_track:
499 self.track_weights_loading(model, loaded_weights)
500
501 def track_weights_loading(
502 self, model: nn.Module, loaded_weights: set[str] | None
503 ) -> None:
504 weights_to_load = {name for name, _ in model.named_parameters()}
505 if loaded_weights is not None:
506 # ignore online quantization scales
507 for name, module in model.named_modules():
508 quant_method = getattr(module, "quant_method", None)
509 has_online_quant = getattr(quant_method, "uses_meta_device", False)
510 has_postprocess_quant = getattr(
511 quant_method, "process_weights_after_loading", None
512 )
513 # ignore kv_cache scale and online quant scale,
514 # which can be missing in checkpoints
515 if has_online_quant or has_postprocess_quant:
516 for param_name, _ in module.named_parameters():
517 full_name = f"{name}.{param_name}" if name else param_name
518 loaded_weights.add(full_name)
519 weights_not_loaded = weights_to_load - loaded_weights
520 if weights_not_loaded:
521 raise ValueError(
522 "Following weights were not initialized from "
523 f"checkpoint: {weights_not_loaded}"
524 )