main
py 1,749 lines 64.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Utilities for downloading and initializing model weights."""
4
5 import asyncio
6 import concurrent.futures
7 import fnmatch
8 import glob
9 import hashlib
10 import json
11 import os
12 import tempfile
13 import threading
14 import time
15 from collections.abc import Callable, Generator, Iterable
16 from contextlib import contextmanager, suppress
17 from pathlib import Path
18 from typing import IO, TYPE_CHECKING, Any
19
20 import filelock
21 import huggingface_hub.constants
22 import numpy as np
23 import regex as re
24 import torch
25 from safetensors.torch import load, load_file, safe_open
26 from tqdm.auto import tqdm
27 from transformers.utils import SAFE_WEIGHTS_INDEX_NAME
28
29 from vllm import envs
30 from vllm.config import ModelConfig
31 from vllm.config.load import (
32 DEFAULT_SAFETENSORS_PREFETCH_BLOCK_SIZE,
33 DEFAULT_SAFETENSORS_PREFETCH_NUM_THREADS,
34 LoadConfig,
35 )
36 from vllm.distributed import get_tensor_model_parallel_rank, get_world_group
37 from vllm.logger import init_logger
38 from vllm.model_executor.layers.quantization import (
39 QuantizationConfig,
40 get_quantization_config,
41 )
42 from vllm.model_executor.model_loader.ep_weight_filter import (
43 should_skip_weight,
44 )
45 from vllm.platforms import current_platform
46 from vllm.tracing import instrument
47 from vllm.transformers_utils.repo_utils import hf_api, hf_fs
48 from vllm.utils.import_utils import PlaceholderModule
49
50 try:
51 from runai_model_streamer import SafetensorsStreamer
52 except ImportError:
53 runai_model_streamer = PlaceholderModule("runai_model_streamer") # type: ignore[assignment]
54 SafetensorsStreamer = runai_model_streamer.placeholder_attr("SafetensorsStreamer")
55
56 try:
57 from fastsafetensors import SingleGroup
58 except ImportError:
59 fastsafetensors = PlaceholderModule("fastsafetensors")
60 SingleGroup = fastsafetensors.placeholder_attr("SingleGroup")
61
62 from vllm.model_executor.layers.quantization.torchao import torchao_version_at_least
63
64 if TYPE_CHECKING:
65 from vllm.model_executor.models.utils import WeightsMapper
66
67 logger = init_logger(__name__)
68
69 # use system-level temp directory for file locks, so that multiple users
70 # can share the same lock without error.
71 # lock files in the temp directory will be automatically deleted when the
72 # system reboots, so users will not complain about annoying lock files
73 temp_dir = tempfile.gettempdir()
74
75
76 def enable_xet_high_performance():
77 """Automatically activates xet high performance mode"""
78 if "HF_XET_HIGH_PERFORMANCE" not in os.environ:
79 huggingface_hub.constants.HF_XET_HIGH_PERFORMANCE = True
80
81
82 enable_xet_high_performance()
83
84
85 class DisabledTqdm(tqdm):
86 def __init__(self, *args, **kwargs):
87 kwargs["disable"] = True
88 super().__init__(*args, **kwargs)
89
90
91 def get_lock(model_name_or_path: str | Path, cache_dir: str | None = None):
92 lock_dir = cache_dir or temp_dir
93 model_name_or_path = str(model_name_or_path)
94 os.makedirs(os.path.dirname(lock_dir), exist_ok=True)
95 model_name = model_name_or_path.replace("/", "-")
96 hash_name = hashlib.sha256(model_name.encode()).hexdigest()
97 # add hash to avoid conflict with old users' lock files
98 lock_file_name = hash_name + model_name + ".lock"
99 # mode 0o666 is required for the filelock to be shared across users
100 lock = filelock.FileLock(os.path.join(lock_dir, lock_file_name), mode=0o666)
101 return lock
102
103
104 @contextmanager
105 def atomic_writer(
106 filepath: str | Path, mode: str = "w", encoding: str | None = None
107 ) -> Generator[IO]:
108 """Context manager that provides an atomic file writing routine.
109
110 The context manager writes to a temporary file and, if successful,
111 atomically replaces the original file.
112
113 Args:
114 filepath (str or Path): The path to the file to write.
115 mode (str): The file mode for the temporary file (e.g., 'w', 'wb').
116 encoding (str): The encoding for text mode.
117
118 Yields:
119 file object: A handle to the temporary file.
120
121 """
122 # Create a temporary file in the same directory as the target file
123 # to ensure it's on the same filesystem for an atomic replace.
124 temp_dir = os.path.dirname(filepath)
125 temp_fd, temp_path = tempfile.mkstemp(dir=temp_dir)
126
127 try:
128 # Open the temporary file for writing
129 with os.fdopen(temp_fd, mode=mode, encoding=encoding) as temp_file:
130 yield temp_file
131
132 # If the 'with' block completes successfully,
133 # perform the atomic replace.
134 os.replace(temp_path, filepath)
135
136 except Exception:
137 logger.exception(
138 "Error during atomic write. Original file '%s' not modified", filepath
139 )
140 raise
141 finally:
142 # Clean up the temporary file if it still exists.
143 if os.path.exists(temp_path):
144 os.remove(temp_path)
145
146
147 def _natural_sort_key(filepath: str) -> list:
148 """Natural sort key for filenames with numeric components, such as
149 model-00001-of-00005.safetensors -> ['model-', 1, '-of-', 5, '.safetensors']"""
150 return [
151 int(s) if s.isdigit() else s
152 for s in re.split(r"(\d+)", os.path.basename(filepath))
153 ]
154
155
156 def maybe_download_from_modelscope(
157 model: str,
158 revision: str | None = None,
159 download_dir: str | None = None,
160 ignore_patterns: str | list[str] | None = None,
161 allow_patterns: list[str] | str | None = None,
162 ) -> str | None:
163 """Download model from ModelScope hub if VLLM_USE_MODELSCOPE is True.
164
165 Returns the path to the downloaded model, or None if the model is not
166 downloaded from ModelScope."""
167 if envs.VLLM_USE_MODELSCOPE:
168 # download model from ModelScope hub,
169 # lazy import so that modelscope is not required for normal use.
170 # pylint: disable=C.
171 from modelscope.hub.snapshot_download import snapshot_download
172
173 # Use file lock to prevent multiple processes from
174 # downloading the same model weights at the same time.
175 with get_lock(model, download_dir):
176 if not os.path.exists(model):
177 model_path = snapshot_download(
178 model_id=model,
179 cache_dir=download_dir,
180 local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
181 revision=revision,
182 ignore_file_pattern=ignore_patterns,
183 allow_patterns=allow_patterns,
184 )
185 else:
186 model_path = model
187 return model_path
188 return None
189
190
191 # TODO(woosuk): Move this to other place.
192 def get_quant_config(
193 model_config: ModelConfig, load_config: LoadConfig
194 ) -> QuantizationConfig:
195 if model_config.quantization is None:
196 raise ValueError("Model quantization method is not specified in the config.")
197 quant_cls = get_quantization_config(model_config.quantization)
198 from vllm.config.quantization import (
199 _ONLINE_SHORTHANDS,
200 QuantizationConfigArgs,
201 resolve_quantization_config,
202 )
203 from vllm.model_executor.layers.quantization.online.base import (
204 OnlineQuantizationConfig,
205 )
206
207 # resolve_quantization_config does not resolve `quantization_config` from
208 # ambiguous shorthands as `"mxfp4"`, `"mxfp8"`.
209 online_args = model_config.quantization_config
210
211 def maybe_compose_online_quantization(
212 checkpoint_config: QuantizationConfig,
213 ) -> QuantizationConfig:
214 if online_args is None:
215 return checkpoint_config
216 assert isinstance(online_args, QuantizationConfigArgs)
217
218 checkpoint_config.online_quantization_config = OnlineQuantizationConfig(
219 online_args
220 )
221 return checkpoint_config
222
223 # Read the quantization config from the HF model config, if available.
224 hf_quant_config = getattr(model_config.hf_config, "quantization_config", None)
225 # some vision model may keep quantization_config in their text_config
226 hf_text_config = getattr(model_config.hf_config, "text_config", None)
227 if hf_quant_config is None and hf_text_config is not None:
228 hf_quant_config = getattr(hf_text_config, "quantization_config", None)
229 if hf_quant_config is None:
230 # compressed-tensors uses a compressions_config
231 hf_quant_config = getattr(model_config.hf_config, "compression_config", None)
232
233 # Pipe information about heads to enable TP-aware loading of attn_head scales
234 if (
235 hf_quant_config is not None
236 and hf_quant_config.get("quant_method") == "compressed-tensors"
237 and "config_groups" in hf_quant_config
238 ):
239 n_heads = model_config.model_arch_config.total_num_attention_heads
240 n_kv_heads = model_config.model_arch_config.total_num_kv_heads
241
242 hf_quant_config["total_num_heads"] = n_heads
243 hf_quant_config["total_num_kv_heads"] = (
244 n_kv_heads if n_kv_heads is not None else n_heads
245 )
246
247 if hf_quant_config is not None:
248 # `model_config.quantization_config` may be set alongside a checkpoint
249 # quant config: the checkpoint determines `quant_cls`, and the user's
250 # QuantizationConfigArgs is consulted by individual quant methods
251 # (e.g. for activation overrides via the MXFP4 oracle).
252
253 # For modelopt_mixed, config.json's quantization_config may or may
254 # not contain the per-layer quantized_layers map. Newer checkpoints
255 # embed it directly; older ones keep it only in hf_quant_config.json.
256 # If it is missing, fall through to the file-based loading path.
257 if (
258 model_config.quantization == "modelopt_mixed"
259 and "quantized_layers" not in hf_quant_config
260 ):
261 pass # fall through to file-based loading below
262 else:
263 return maybe_compose_online_quantization(
264 quant_cls.from_config(hf_quant_config)
265 )
266
267 # if hf_quant_config is None, we will try to get config from
268 # hf_overrides
269 hf_overrides = model_config.hf_overrides
270 if callable(hf_overrides):
271 # A callable hf_overrides is a config-to-config transform (e.g. the
272 # one SpeculativeConfig installs on draft model configs); it cannot
273 # carry quantization config entries, so treat it as no overrides.
274 hf_overrides = {}
275 if not isinstance(hf_overrides, dict):
276 raise ValueError(
277 "hf_overrides must be a dict for get_quant_config "
278 "to get the quantization config from it."
279 )
280 quantization_config_file = hf_overrides.get("quantization_config_file")
281 if quantization_config_file is not None:
282 if hasattr(quant_cls, "from_config_file"):
283 return maybe_compose_online_quantization(
284 quant_cls.from_config_file(quantization_config_file)
285 )
286 else:
287 raise NotImplementedError(
288 "from_config_file is specified in hf_override config, "
289 "but quant_cls.from_config_file is not implemented in "
290 f"{quant_cls}"
291 )
292 quantization_config_json = hf_overrides.get("quantization_config_dict_json")
293 if quantization_config_json is not None:
294 if hasattr(quant_cls, "from_config_dict_json"):
295 return maybe_compose_online_quantization(
296 quant_cls.from_config_dict_json(quantization_config_json)
297 )
298 else:
299 raise NotImplementedError(
300 "from_config_dict_json is specified in hf_override config, "
301 "but quant_cls.from_config_dict_json is not implemented in "
302 f"{quant_cls}"
303 )
304
305 # Raw unambiguous online quantization doesn't read from checkpoint configs
306 # We must continue to load/read the config below in two cases:
307 # 1. composed online quantization, before `maybe_compose_online_quantization`,
308 # 2. Ambiguous online shorthands as `"mxpf4"`, `"mxfp8"`, for which `online_args`
309 # is not set yet.
310 if quant_cls is OnlineQuantizationConfig and online_args is not None:
311 assert isinstance(online_args, QuantizationConfigArgs)
312 return OnlineQuantizationConfig(args=online_args)
313
314 model_name_or_path = (
315 maybe_download_from_modelscope(
316 model_config.model,
317 revision=model_config.revision,
318 download_dir=load_config.download_dir,
319 allow_patterns=["*.json"],
320 )
321 or model_config.model
322 )
323 is_local = os.path.isdir(model_name_or_path)
324 if not is_local:
325 # Download the config files.
326 with get_lock(model_config.model, load_config.download_dir):
327 hf_folder = hf_api().snapshot_download(
328 model_config.model,
329 revision=model_config.revision,
330 allow_patterns="*.json",
331 cache_dir=load_config.download_dir,
332 local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
333 tqdm_class=DisabledTqdm,
334 )
335 else:
336 hf_folder = model_name_or_path
337
338 possible_config_filenames = quant_cls.get_config_filenames()
339
340 # If the quantization config is not found, use the default config.
341 if not possible_config_filenames:
342 if model_config.quantization == "fp8":
343 logger.warning_once(
344 "--quantization fp8 is deprecated for online quantization; "
345 "use --quantization fp8_per_tensor instead."
346 )
347 fp8_args = resolve_quantization_config("fp8_per_tensor", online_args)
348 assert fp8_args is not None
349 return OnlineQuantizationConfig(args=fp8_args)
350 if model_config.quantization in _ONLINE_SHORTHANDS:
351 args = online_args or _ONLINE_SHORTHANDS[model_config.quantization]
352 assert isinstance(args, QuantizationConfigArgs)
353 return OnlineQuantizationConfig(args=args)
354 return maybe_compose_online_quantization(quant_cls())
355
356 config_files = glob.glob(os.path.join(hf_folder, "*.json"))
357
358 quant_config_files = [
359 f for f in config_files if any(f.endswith(x) for x in possible_config_filenames)
360 ]
361 if len(quant_config_files) == 0:
362 if model_config.quantization in _ONLINE_SHORTHANDS:
363 args = online_args or _ONLINE_SHORTHANDS[model_config.quantization]
364 assert isinstance(args, QuantizationConfigArgs)
365 return OnlineQuantizationConfig(args=args)
366 raise ValueError(f"Cannot find the config file for {model_config.quantization}")
367 if len(quant_config_files) > 1:
368 raise ValueError(
369 f"Found multiple config files for {model_config.quantization}: "
370 f"{quant_config_files}"
371 )
372
373 quant_config_file = quant_config_files[0]
374 with open(quant_config_file) as f:
375 config = json.load(f)
376
377 if model_config.quantization in ("modelopt", "modelopt_mixed"):
378 if config.get("producer", {}).get("name") == "modelopt":
379 return maybe_compose_online_quantization(quant_cls.from_config(config))
380 else:
381 raise ValueError(
382 f"Unsupported quantization config"
383 f" found for {model_config.quantization} in {f}."
384 )
385
386 return maybe_compose_online_quantization(quant_cls.from_config(config))
387
388
389 def get_sparse_attention_config(
390 model_config: ModelConfig,
391 load_config: LoadConfig,
392 sparse_attention_config_filename: str = "sparse_attention_config.json",
393 ) -> dict[str, Any]:
394 model_name_or_path = model_config.model
395 is_local = os.path.isdir(model_name_or_path)
396 if not is_local:
397 # Download the config files.
398 with get_lock(model_name_or_path, load_config.download_dir):
399 hf_folder = hf_api().snapshot_download(
400 model_name_or_path,
401 revision=model_config.revision,
402 allow_patterns="*.json",
403 cache_dir=load_config.download_dir,
404 local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
405 tqdm_class=DisabledTqdm,
406 )
407 else:
408 hf_folder = model_name_or_path
409
410 config_file = os.path.join(hf_folder, sparse_attention_config_filename)
411 if not os.path.exists(config_file):
412 return {}
413
414 # Load the sparse attention config.
415 with open(config_file) as f:
416 config = json.load(f)
417 logger.info("Loaded sparse attention config from %s", config_file)
418
419 return config
420
421
422 @instrument(span_name="Download weights - HF")
423 def download_weights_from_hf(
424 model_name_or_path: str,
425 cache_dir: str | None,
426 allow_patterns: list[str],
427 revision: str | None = None,
428 subfolder: str | None = None,
429 ignore_patterns: str | list[str] | None = None,
430 ) -> str:
431 """Download model weights from Hugging Face Hub.
432
433 Args:
434 model_name_or_path (str): The model name or path.
435 cache_dir (Optional[str]): The cache directory to store the model
436 weights. If None, will use HF defaults.
437 allow_patterns (list[str]): The allowed patterns for the
438 weight files. Files matched by any of the patterns will be
439 downloaded.
440 revision (Optional[str]): The revision of the model.
441 subfolder (Optional[str]): The subfolder within the model repository
442 to download weights from.
443 ignore_patterns (Optional[Union[str, list[str]]]): The patterns to
444 filter out the weight files. Files matched by any of the patterns
445 will be ignored.
446
447 Returns:
448 str: The path to the downloaded model weights.
449
450 """
451 assert len(allow_patterns) > 0
452 local_only = huggingface_hub.constants.HF_HUB_OFFLINE
453 if not local_only:
454 # Attempt to reduce allow_patterns to a single pattern
455 # so we only have to call snapshot_download once.
456 try:
457 fs = hf_fs()
458 file_list = fs.ls(
459 os.path.join(model_name_or_path, subfolder or ""),
460 detail=False,
461 revision=revision,
462 )
463
464 # If downloading safetensors and an index file exists, use the
465 # specific file names from the index to avoid downloading
466 # unnecessary files (e.g., from subdirectories like "original/").
467 index_file = f"{model_name_or_path}/{SAFE_WEIGHTS_INDEX_NAME}"
468 if "*.safetensors" in allow_patterns and index_file in file_list:
469 index_path = hf_api().hf_hub_download(
470 repo_id=model_name_or_path,
471 filename=SAFE_WEIGHTS_INDEX_NAME,
472 cache_dir=cache_dir,
473 revision=revision,
474 subfolder=subfolder,
475 )
476 with open(index_path) as f:
477 weight_map = json.load(f)["weight_map"]
478 if weight_map:
479 # Extra [] so that weight_map files are treated as a
480 # single allow_pattern in the loop below
481 allow_patterns = [list(set(weight_map.values()))] # type: ignore[list-item]
482 else:
483 allow_patterns = ["*.safetensors"]
484 else:
485 # Use the first pattern found in the HF repo's files.
486 for pattern in allow_patterns:
487 if fnmatch.filter(file_list, pattern):
488 allow_patterns = [pattern]
489 break
490 except Exception as e:
491 logger.warning(
492 "Failed to get file list for '%s'. Trying each pattern in "
493 "allow_patterns individually until weights have been "
494 "downloaded. Error: %s",
495 model_name_or_path,
496 e,
497 )
498
499 logger.debug("Using model weights format %s", allow_patterns)
500 # Use file lock to prevent multiple processes from
501 # downloading the same model weights at the same time.
502 with get_lock(model_name_or_path, cache_dir):
503 start_time = time.perf_counter()
504 for allow_pattern in allow_patterns:
505 hf_folder = hf_api().snapshot_download(
506 model_name_or_path,
507 allow_patterns=allow_pattern,
508 ignore_patterns=ignore_patterns,
509 cache_dir=cache_dir,
510 tqdm_class=DisabledTqdm,
511 revision=revision,
512 local_files_only=local_only,
513 )
514 # If we have downloaded weights for this allow_pattern,
515 # we don't need to check the rest.
516 # allow_pattern can be a list (from weight_map) or str (glob)
517 if isinstance(allow_pattern, list):
518 break
519 if any(Path(hf_folder).glob(allow_pattern)):
520 break
521 time_taken = time.perf_counter() - start_time
522 if time_taken > 0.5:
523 logger.info(
524 "Time spent downloading weights for %s: %.6f seconds",
525 model_name_or_path,
526 time_taken,
527 )
528 return hf_folder
529
530
531 def download_safetensors_index_file_from_hf(
532 model_name_or_path: str,
533 index_file: str,
534 cache_dir: str | None,
535 subfolder: str | None = None,
536 revision: str | None = None,
537 ) -> None:
538 """Download hf safetensors index file from Hugging Face Hub.
539
540 Args:
541 model_name_or_path (str): The model name or path.
542 index_file (str): The safetensors index file name
543 cache_dir (Optional[str]): The cache directory to store the model
544 weights. If None, will use HF defaults.
545 subfolder (Optional[str]): The subfolder within the model repository
546 to download weights from.
547 revision (Optional[str]): The revision of the model.
548
549 """
550 # Use file lock to prevent multiple processes from
551 # downloading the same model weights at the same time.
552 with get_lock(model_name_or_path, cache_dir):
553 try:
554 # Download the safetensors index file.
555 hf_api().hf_hub_download(
556 repo_id=model_name_or_path,
557 filename=index_file,
558 cache_dir=cache_dir,
559 revision=revision,
560 subfolder=subfolder,
561 local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
562 )
563 # If file not found on remote or locally, we should not fail since
564 # only some models will have index_file.
565 except huggingface_hub.utils.LocalEntryNotFoundError:
566 logger.info("No %s found in local cache.", index_file)
567 except huggingface_hub.utils.EntryNotFoundError:
568 logger.info("No %s found in remote.", index_file)
569
570
571 # For models like Mistral-7B-v0.3, there are both sharded
572 # safetensors files and a consolidated safetensors file.
573 # Passing both of these to the weight loader functionality breaks.
574 # So, we use the index_file to
575 # look up which safetensors files should be used.
576 def filter_duplicate_safetensors_files(
577 hf_weights_files: list[str], hf_folder: str, index_file: str
578 ) -> list[str]:
579 # model.safetensors.index.json is a mapping from keys in the
580 # torch state_dict to safetensors file holding that weight.
581 index_file_name = os.path.join(hf_folder, index_file)
582 if not os.path.isfile(index_file_name):
583 return hf_weights_files
584
585 # Iterate through the weight_map (weight_name: safetensors files)
586 # to identify weights that we should use.
587 with open(index_file_name) as f:
588 weight_map = json.load(f)["weight_map"]
589 weight_files_in_index = set()
590 for weight_name in weight_map:
591 weight_files_in_index.add(os.path.join(hf_folder, weight_map[weight_name]))
592 # Check if files referenced in model.safetensors.index.json actually exist.
593 # Raise error if any file is missing.
594 hf_weights_files_set = set(hf_weights_files)
595 missing_files = weight_files_in_index - hf_weights_files_set
596 if missing_files:
597 raise FileNotFoundError(
598 f"Weight files referenced in index but missing: {missing_files}"
599 )
600 # Filter out any fields that are not found in the index file.
601 hf_weights_files = [f for f in hf_weights_files if f in weight_files_in_index]
602 return hf_weights_files
603
604
605 def filter_files_not_needed_for_inference(hf_weights_files: list[str]) -> list[str]:
606 """Exclude files that are not needed for inference.
607
608 See https://github.com/huggingface/transformers/blob/v4.34.0/src/transformers/trainer.py#L227-L233
609 """
610 blacklist = [
611 "training_args.bin",
612 "optimizer.bin",
613 "optimizer.pt",
614 "scheduler.pt",
615 "scaler.pt",
616 ]
617 hf_weights_files = [
618 f for f in hf_weights_files if not any(f.endswith(x) for x in blacklist)
619 ]
620 return hf_weights_files
621
622
623 def _normalize_module_prefixes(
624 language_model_names: Iterable[str] | None,
625 ) -> tuple[str, ...]:
626 names = tuple(language_model_names or ())
627 return tuple(n if n.endswith(".") else f"{n}." for n in names)
628
629
630 def _mapped_weight_name(weights_mapper: "WeightsMapper | None", key: str) -> str | None:
631 """Apply ``WeightsMapper.map_name`` when present; else identity."""
632 if weights_mapper is None:
633 return key
634 return weights_mapper.map_name(key)
635
636
637 def _hf_prefix_maps_to_lm(
638 new_name: str | None, lm_module_prefixes: tuple[str, ...]
639 ) -> bool:
640 if not new_name:
641 return False
642 new_p = new_name if new_name.endswith(".") else f"{new_name}."
643 return any(new_p.startswith(p) or p.startswith(new_p) for p in lm_module_prefixes)
644
645
646 def _shared_hf_root_module_prefixes(
647 lm_module_prefixes: tuple[str, ...],
648 weights_mapper: "WeightsMapper | None",
649 ) -> tuple[str, ...]:
650 """Module prefixes that are also an HF root for non-LM weights.
651
652 E.g. Molmo/Phi-4-MM/Muse mark LM as ``model`` while vision HF keys stay
653 under ``model.vision_*`` / ``model.embed_tokens_extend.*``. Detected via
654 ``WeightsMapper.orig_to_new_prefix`` (no hard-coded name list).
655 """
656 if weights_mapper is None:
657 return ()
658 mapping = weights_mapper.orig_to_new_prefix
659 if not mapping:
660 return ()
661
662 non_lm_origs: list[str] = []
663 for orig, new in mapping.items():
664 orig_p = orig if orig.endswith(".") else f"{orig}."
665 if not _hf_prefix_maps_to_lm(new, lm_module_prefixes):
666 non_lm_origs.append(orig_p)
667
668 if not non_lm_origs:
669 return ()
670
671 shared = [
672 p
673 for p in lm_module_prefixes
674 if any(orig.startswith(p) for orig in non_lm_origs)
675 ]
676 return tuple(shared)
677
678
679 def resolve_mm_encoder_only_lm_prefixes(
680 language_model_names: Iterable[str] | None,
681 *,
682 weights_mapper: "WeightsMapper | None" = None,
683 ) -> tuple[str, ...] | None:
684 """Resolve vLLM LM *module* prefixes for ``--mm-encoder-only`` shard skip.
685
686 Prefixes come from ``_language_model_names``. Classification of HF index
687 keys is done later via optional ``weights_mapper`` (see
688 ``filter_mm_encoder_only_safetensors_files``), so this helper does **not**
689 hard-code Qwen/HF nest lists.
690
691 Returns ``None`` (leave the safetensors file list unchanged) when
692 ``_language_model_names`` is empty/missing, or when a module prefix is a
693 shared HF checkpoint root for both LM and non-LM weights — fail-closed so
694 Molmo / Phi-4-MM / Muse cannot under-load the encoder.
695 """
696 prefixes = _normalize_module_prefixes(language_model_names)
697 if not prefixes:
698 return None
699 shared = _shared_hf_root_module_prefixes(prefixes, weights_mapper)
700 if shared:
701 logger.warning_once(
702 "mm-encoder-only whole-shard filter not applied: LM module "
703 "prefix(es) %s share an HF checkpoint root with non-LM weights "
704 "(via WeightsMapper). Encoder-only load keeps the full "
705 "safetensors file list; finer HF prefixes or key-level "
706 "filtering can enable skip later.",
707 shared,
708 )
709 return None
710 return prefixes
711
712
713 def filter_mm_encoder_only_safetensors_files(
714 hf_weights_files: list[str],
715 hf_folder: str,
716 index_file: str,
717 language_model_prefixes: Iterable[str],
718 *,
719 weights_mapper: "WeightsMapper | None" = None,
720 ) -> list[str]:
721 """Drop safetensors shards that only contain language-model weights.
722
723 Used with ``--mm-encoder-only`` so Encoder-only EPD instances avoid reading
724 pure LM shards from disk/DRAM.
725
726 Each HF index key is classified by mapping through ``weights_mapper`` (when
727 provided) and testing the *vLLM* name against ``language_model_prefixes``.
728 Without a mapper, HF names are compared directly (identity checkpoint
729 layout, e.g. Kimi ``language_model.*``).
730
731 A shard is kept if it contains any non-LM key. Without an index file,
732 returns ``hf_weights_files`` unchanged.
733 """
734 prefixes = tuple(language_model_prefixes)
735 if not prefixes:
736 return hf_weights_files
737
738 index_path = os.path.join(hf_folder, index_file)
739 if not os.path.isfile(index_path):
740 logger.warning_once(
741 "mm-encoder-only shard filter skipped: index file %s not found",
742 index_path,
743 )
744 return hf_weights_files
745
746 with open(index_path) as f:
747 weight_map: dict[str, str] = json.load(f)["weight_map"]
748
749 keys_by_file: dict[str, list[str]] = {}
750 for weight_name, weight_file in weight_map.items():
751 keys_by_file.setdefault(weight_file, []).append(weight_name)
752
753 def _is_lm_key(key: str) -> bool:
754 mapped = _mapped_weight_name(weights_mapper, key)
755 if mapped is None:
756 return False
757 return any(mapped.startswith(p) for p in prefixes)
758
759 def _is_lm_only(weight_file: str) -> bool:
760 keys = keys_by_file.get(weight_file)
761 if not keys:
762 # Not listed in the index (e.g. extra matched glob) — keep.
763 return False
764 return all(_is_lm_key(key) for key in keys)
765
766 kept: list[str] = []
767 skipped = 0
768 for path in hf_weights_files:
769 rel = os.path.relpath(path, hf_folder)
770 # Index weight_map values are usually basenames; tolerate either form.
771 basename = os.path.basename(path)
772 if _is_lm_only(rel) or _is_lm_only(basename):
773 skipped += 1
774 continue
775 kept.append(path)
776
777 if skipped:
778 logger.info_once(
779 "mm-encoder-only: skipped %d/%d safetensors shard(s) that only "
780 "contain language-model weights (prefixes=%s)",
781 skipped,
782 skipped + len(kept),
783 prefixes,
784 )
785 return kept
786
787
788 # explicitly use pure text format, with a newline at the end
789 # this makes it impossible to see the animation in the progress bar
790 # but will avoid messing up with ray or multiprocessing, which wraps
791 # each line of output with some prefix.
792 _BAR_FORMAT = "{desc}: {percentage:3.0f}% Completed | {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]\n" # noqa: E501
793
794
795 def enable_tqdm(use_tqdm_on_load: bool):
796 return use_tqdm_on_load and (
797 not torch.distributed.is_initialized() or torch.distributed.get_rank() == 0
798 )
799
800
801 def np_cache_weights_iterator(
802 model_name_or_path: str,
803 cache_dir: str | None,
804 hf_folder: str,
805 hf_weights_files: list[str],
806 use_tqdm_on_load: bool,
807 ) -> Generator[tuple[str, torch.Tensor], None, None]:
808 """Iterate over the weights in the model np files.
809
810 Will dump the model weights to numpy files if they are not already dumped.
811 """
812 # Convert the model weights from torch tensors to numpy arrays for
813 # faster loading.
814 np_folder = os.path.join(hf_folder, "np")
815 os.makedirs(np_folder, exist_ok=True)
816 weight_names_file = os.path.join(np_folder, "weight_names.json")
817 # Use file lock to prevent multiple processes from
818 # dumping the same model weights to numpy at the same time.
819 with get_lock(model_name_or_path, cache_dir):
820 if not os.path.exists(weight_names_file):
821 weight_names: list[str] = []
822 for bin_file in tqdm(
823 hf_weights_files,
824 desc="Loading np_cache checkpoint shards",
825 disable=not enable_tqdm(use_tqdm_on_load),
826 bar_format=_BAR_FORMAT,
827 ):
828 state = torch.load(bin_file, map_location="cpu", weights_only=True)
829 for name, param in state.items():
830 param_path = os.path.join(np_folder, name)
831 with open(param_path, "wb") as f:
832 np.save(f, param.cpu().detach().numpy())
833 weight_names.append(name)
834 with open(weight_names_file, "w") as f:
835 json.dump(weight_names, f)
836
837 with open(weight_names_file) as f:
838 weight_names = json.load(f)
839
840 for name in weight_names:
841 param_path = os.path.join(np_folder, name)
842 with open(param_path, "rb") as f:
843 param = np.load(f)
844 yield name, torch.from_numpy(param)
845
846
847 def drop_checkpoint_cache(
848 model_name_or_path: str,
849 revision: str | None = None,
850 cache_dir: str | None = None,
851 ) -> None:
852 checkpoint_dir: Path | None = None
853 if os.path.isdir(model_name_or_path):
854 checkpoint_dir = Path(model_name_or_path)
855 else:
856 with suppress(OSError, ValueError):
857 checkpoint_dir = Path(
858 hf_api().snapshot_download(
859 model_name_or_path,
860 revision=revision,
861 cache_dir=cache_dir,
862 local_files_only=True,
863 )
864 )
865 if checkpoint_dir is None:
866 logger.info_once(
867 "No local checkpoint found for %s; skipping page-cache eviction",
868 model_name_or_path,
869 )
870 return
871 for path in checkpoint_dir.glob("*.safetensors"):
872 try:
873 with path.open("rb") as file:
874 os.posix_fadvise(file.fileno(), 0, 0, os.POSIX_FADV_DONTNEED)
875 except OSError as exc:
876 logger.warning(
877 "Could not release checkpoint page cache for %s: %s", path, exc
878 )
879
880
881 def _get_checkpoints_size_bytes(files: list[str]) -> int:
882 """Return the total size of the checkpoint files in bytes."""
883 if not files:
884 return 0
885 return sum(os.path.getsize(f) for f in files)
886
887
888 def _get_available_ram_bytes() -> int:
889 """Return available RAM, honoring cgroup limits."""
890 import psutil
891
892 host_available = psutil.virtual_memory().available
893
894 from vllm.utils.cpu_resource_utils import (
895 get_cgroup_memory_limit,
896 get_cgroup_memory_usage,
897 )
898
899 cgroup_limit = get_cgroup_memory_limit()
900 if cgroup_limit is None:
901 return host_available
902 cgroup_usage = get_cgroup_memory_usage()
903 cgroup_available = (
904 cgroup_limit if cgroup_usage is None else max(0, cgroup_limit - cgroup_usage)
905 )
906 return min(host_available, cgroup_available)
907
908
909 def _get_fs_type(files: list[str]) -> str:
910 """Get the filesystem type of the first file in *files* (Linux only)."""
911 if not files:
912 return ""
913 try:
914 # Only the first file is checked — all checkpoint shards reside
915 # in the same directory and therefore on the same filesystem.
916 resolved = os.path.realpath(files[0])
917 best_mount = ""
918 best_fstype = ""
919 # /proc/mounts may contain nested mount points (e.g. "/" -> ext4,
920 # "/data" -> nfs4, "/data/local" -> ext4). We pick the entry with
921 # the longest matching mount_point — the same "longest prefix match"
922 # rule the kernel uses to decide which filesystem serves a path.
923 with open("/proc/mounts") as f:
924 for line in f:
925 parts = line.split()
926 if len(parts) < 3:
927 continue
928 mount_point, fstype = parts[1], parts[2]
929 if (
930 resolved == mount_point
931 or resolved.startswith(os.path.join(mount_point, ""))
932 ) and len(mount_point) > len(best_mount):
933 best_mount = mount_point
934 best_fstype = fstype
935 return best_fstype
936 except Exception:
937 # /proc/mounts is Linux-specific; on other OSes (or if the read
938 # fails for any reason) we fall back to an empty string.
939 return ""
940
941
942 def _prefetch_checkpoint(
943 file_path: str,
944 block_size: int = DEFAULT_SAFETENSORS_PREFETCH_BLOCK_SIZE,
945 ) -> None:
946 """Prefetch a checkpoint file into the OS page cache.
947
948 Reads the file in blocks so the kernel caches its pages before workers load
949 the same file.
950 """
951 if block_size < 1:
952 raise ValueError("safetensors prefetch block size must be >= 1")
953
954 with open(file_path, "rb") as f:
955 while f.read(block_size):
956 pass
957
958
959 def _prefetch_all_checkpoints(
960 sorted_files: list[str],
961 num_prefetch_threads: int = DEFAULT_SAFETENSORS_PREFETCH_NUM_THREADS,
962 block_size: int = DEFAULT_SAFETENSORS_PREFETCH_BLOCK_SIZE,
963 ) -> None:
964 """Start prefetching checkpoint files into page cache in a background thread."""
965 if num_prefetch_threads < 1:
966 raise ValueError("safetensors prefetch num threads must be >= 1")
967 if block_size < 1:
968 raise ValueError("safetensors prefetch block size must be >= 1")
969
970 if torch.distributed.is_initialized():
971 rank = torch.distributed.get_rank()
972 world_size = torch.distributed.get_world_size()
973 else:
974 rank = 0
975 world_size = 1
976 paths_to_prefetch = sorted_files[rank::world_size]
977 total_for_rank = len(paths_to_prefetch)
978
979 async def _prefetch_all() -> None:
980 loop = asyncio.get_running_loop()
981 completed = 0
982 next_log_pct = 10
983
984 async def prefetch_one(
985 path: str,
986 executor: concurrent.futures.ThreadPoolExecutor,
987 ) -> None:
988 nonlocal completed, next_log_pct
989 try:
990 await loop.run_in_executor(
991 executor, _prefetch_checkpoint, path, block_size
992 )
993 completed += 1
994 if total_for_rank > 0 and next_log_pct <= 100:
995 pct = 100 * completed / total_for_rank
996 if pct >= next_log_pct:
997 logger.info(
998 "Prefetching checkpoint files: %d%% (%d/%d)",
999 next_log_pct,
1000 completed,
1001 total_for_rank,
1002 )
1003 next_log_pct += 10
1004 except Exception:
1005 logger.warning(
1006 "Failed to prefetch checkpoint file %r.", path, exc_info=True
1007 )
1008
1009 with concurrent.futures.ThreadPoolExecutor(
1010 max_workers=num_prefetch_threads
1011 ) as executor:
1012 await asyncio.gather(
1013 *(prefetch_one(p, executor) for p in paths_to_prefetch)
1014 )
1015
1016 def _run_prefetch() -> None:
1017 start = time.perf_counter()
1018 asyncio.run(_prefetch_all())
1019 elapsed = time.perf_counter() - start
1020 logger.info(
1021 "Prefetching checkpoint files into page cache finished in %.2fs",
1022 elapsed,
1023 )
1024
1025 logger.info(
1026 "Prefetching checkpoint files into page cache started "
1027 "(in background, num_threads=%d, block_size=%d bytes)",
1028 num_prefetch_threads,
1029 block_size,
1030 )
1031 threading.Thread(target=_run_prefetch, daemon=True).start()
1032
1033
1034 def safetensors_weights_iterator(
1035 hf_weights_files: list[str],
1036 use_tqdm_on_load: bool,
1037 safetensors_load_strategy: str | None = None,
1038 local_expert_ids: set[int] | None = None,
1039 *,
1040 safetensors_prefetch_num_threads: int = DEFAULT_SAFETENSORS_PREFETCH_NUM_THREADS,
1041 safetensors_prefetch_block_size: int = DEFAULT_SAFETENSORS_PREFETCH_BLOCK_SIZE,
1042 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1043 """Iterate over the weights in the model safetensor files.
1044
1045 When *local_expert_ids* is provided, expert weights not belonging to
1046 this rank are skipped **before** reading from disk, which drastically
1047 reduces storage I/O for MoE models under EP.
1048 """
1049 loading_desc = "Loading safetensors checkpoint shards"
1050 if safetensors_load_strategy == "eager":
1051 loading_desc += " (eager)"
1052
1053 sorted_files = sorted(hf_weights_files, key=_natural_sort_key)
1054
1055 fs_type = _get_fs_type(sorted_files)
1056 is_net_fs = fs_type in ("nfs", "nfs4", "lustre")
1057 total_bytes = _get_checkpoints_size_bytes(sorted_files)
1058 avail_bytes = _get_available_ram_bytes()
1059 ram_threshold_pct = 90
1060 fits_in_ram = total_bytes <= (ram_threshold_pct / 100.0) * avail_bytes
1061 fs_name = fs_type.upper() if fs_type else "unknown"
1062
1063 logger.info_once(
1064 "Filesystem type for checkpoints: %s. Checkpoint size: %.2f GiB. "
1065 "Available RAM: %.2f GiB.",
1066 fs_name,
1067 total_bytes / 1024**3,
1068 avail_bytes / 1024**3,
1069 )
1070
1071 should_prefetch = safetensors_load_strategy == "prefetch"
1072 if safetensors_load_strategy is None:
1073 if is_net_fs and fits_in_ram:
1074 should_prefetch = True
1075 elif is_net_fs and not fits_in_ram:
1076 logger.warning_once(
1077 "Network filesystem (%s) detected but checkpoint total size "
1078 "(%.2f GiB) exceeds %d%% of available RAM (%.2f GiB). "
1079 "Skipping auto-prefetch.",
1080 fs_name,
1081 total_bytes / 1024**3,
1082 ram_threshold_pct,
1083 avail_bytes / 1024**3,
1084 )
1085 elif not is_net_fs and fits_in_ram:
1086 logger.info_once(
1087 "Auto-prefetch is disabled because the filesystem (%s) is not a "
1088 "recognized network FS (NFS/Lustre). If you want to force "
1089 "prefetching, start vLLM with --safetensors-load-strategy=prefetch.",
1090 fs_name,
1091 )
1092 elif not is_net_fs and not fits_in_ram:
1093 logger.info_once(
1094 "Auto-prefetch is disabled because the filesystem (%s) is not a "
1095 "recognized network FS (NFS/Lustre) and the checkpoint size "
1096 "(%.2f GiB) exceeds %d%% of available RAM (%.2f GiB).",
1097 fs_name,
1098 total_bytes / 1024**3,
1099 ram_threshold_pct,
1100 avail_bytes / 1024**3,
1101 )
1102 elif should_prefetch and not fits_in_ram:
1103 logger.warning_once(
1104 "safetensors_load_strategy='prefetch' was explicitly specified, but "
1105 "checkpoint total size (%.2f GiB) exceeds %d%% of available RAM "
1106 "(%.2f GiB). This may cause out-of-memory errors.",
1107 total_bytes / 1024**3,
1108 ram_threshold_pct,
1109 avail_bytes / 1024**3,
1110 )
1111
1112 if should_prefetch:
1113 _prefetch_all_checkpoints(
1114 sorted_files,
1115 num_prefetch_threads=safetensors_prefetch_num_threads,
1116 block_size=safetensors_prefetch_block_size,
1117 )
1118
1119 leftover_state_dict: dict[str, torch.Tensor] = {}
1120 for st_file in tqdm(
1121 sorted_files,
1122 desc=loading_desc,
1123 disable=not enable_tqdm(use_tqdm_on_load),
1124 bar_format=_BAR_FORMAT,
1125 ):
1126 if safetensors_load_strategy == "eager":
1127 with open(st_file, "rb") as f:
1128 state_dict = load(f.read())
1129 for name, param in state_dict.items():
1130 if not should_skip_weight(name, local_expert_ids):
1131 yield name, param
1132 elif safetensors_load_strategy == "torchao":
1133 # we can't load flattened torchao tensor subclasses directly into the model
1134 # instead we reconstruct the subclasses here before returning
1135 if not torchao_version_at_least("0.15.0"):
1136 raise ValueError(
1137 "Please use torchao version >= 0.15.0 "
1138 "to load torchao safetensors checkpoint"
1139 )
1140 from torchao.prototype.safetensors.safetensors_support import (
1141 unflatten_tensor_state_dict,
1142 )
1143
1144 with safe_open(st_file, framework="pt") as f:
1145 state_dict = {}
1146 for name in f.keys(): # noqa: SIM118
1147 if should_skip_weight(name, local_expert_ids):
1148 continue
1149 state_dict[name] = f.get_tensor(name)
1150
1151 # update with leftover tensor data from previous iteration, if any
1152 state_dict.update(leftover_state_dict)
1153 metadata = f.metadata()
1154 # due to sharded checkpoints, we are not guaranteed that we have all
1155 # tensor subclass data on one file
1156 # state_dict has the leftover data from this step and we wait for
1157 # missing information to be provided in a future iteration
1158 unflattened_state_dict, leftover_state_dict = (
1159 unflatten_tensor_state_dict(state_dict, metadata)
1160 )
1161 yield from unflattened_state_dict.items()
1162 else:
1163 with safe_open(st_file, framework="pt") as f:
1164 for name in f.keys(): # noqa: SIM118
1165 if should_skip_weight(name, local_expert_ids):
1166 continue
1167 param = f.get_tensor(name)
1168 yield name, param
1169
1170
1171 def multi_thread_safetensors_weights_iterator(
1172 hf_weights_files: list[str],
1173 use_tqdm_on_load: bool,
1174 max_workers: int = 4,
1175 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1176 """Multi-Thread iterate over the weights in the model safetensor files."""
1177
1178 def _load_file(st_file: str):
1179 result = load_file(st_file, device="cpu")
1180 return result
1181
1182 with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
1183 # Note to use generator here so we do not store all the loaded files in memory
1184 # at the same time, which can cause OOM for large models.
1185 futures = (executor.submit(_load_file, st_file) for st_file in hf_weights_files)
1186 futures_iter = tqdm(
1187 concurrent.futures.as_completed(futures),
1188 total=len(hf_weights_files),
1189 desc="Multi-thread loading shards",
1190 disable=not enable_tqdm(use_tqdm_on_load),
1191 bar_format=_BAR_FORMAT,
1192 )
1193
1194 for future in futures_iter:
1195 state_dict = future.result()
1196 del future
1197 for key in list(state_dict):
1198 yield key, state_dict.pop(key)
1199
1200
1201 def runai_safetensors_weights_iterator(
1202 hf_weights_files: list[str],
1203 use_tqdm_on_load: bool,
1204 is_distributed: bool = False,
1205 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1206 """Iterate over the weights in the model safetensor files."""
1207 with SafetensorsStreamer() as streamer:
1208 is_cuda_alike = current_platform.is_cuda_alike()
1209 device = (
1210 f"cuda:{current_platform.current_device()}"
1211 if is_distributed and is_cuda_alike
1212 else "cpu"
1213 )
1214
1215 streamer.stream_files(
1216 hf_weights_files,
1217 device=device,
1218 is_distributed=is_distributed,
1219 )
1220 total_tensors = sum(
1221 len(tensors_meta)
1222 for tensors_meta in streamer.files_to_tensors_metadata.values()
1223 )
1224
1225 tensor_iter = tqdm(
1226 streamer.get_tensors(),
1227 total=total_tensors,
1228 desc="Loading safetensors using Runai Model Streamer",
1229 bar_format=_BAR_FORMAT,
1230 disable=not enable_tqdm(use_tqdm_on_load),
1231 mininterval=2,
1232 )
1233
1234 for name, tensor in tensor_iter:
1235 yield name, tensor.clone()
1236
1237
1238 def fastsafetensors_weights_iterator(
1239 hf_weights_files: list[str],
1240 use_tqdm_on_load: bool,
1241 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1242 """Iterate over the weights in the model safetensor files
1243 using fastsafetensor library.
1244
1245 Uses ParallelLoader for pipelined loading: the producer thread
1246 prepares metadata for the next shard while the consumer yields
1247 tensors from the current shard.
1248 """
1249 from fastsafetensors.parallel_loader import ParallelLoader
1250
1251 if torch.distributed.is_initialized():
1252 pg = torch.distributed.group.WORLD
1253 else:
1254 pg = SingleGroup()
1255
1256 device = torch.device(f"cuda:{current_platform.current_device()}")
1257 hf_weights_files = sorted(hf_weights_files, key=_natural_sort_key)
1258
1259 # Use nogds=True for TP > 1 to avoid cuFileDriverOpen() which
1260 # initializes the GDS DMA subsystem for all visible GPUs, creating
1261 # unwanted CUDA contexts on every device.
1262 nogds = pg.size() > 1
1263
1264 queue_size = envs.VLLM_FASTSAFETENSORS_QUEUE_SIZE
1265 tqdm_enabled = enable_tqdm(use_tqdm_on_load)
1266
1267 def _make_loader(nogds: bool) -> "ParallelLoader":
1268 return ParallelLoader(
1269 pg=pg,
1270 hf_weights_files=hf_weights_files,
1271 queue_size=queue_size,
1272 use_tqdm_on_load=tqdm_enabled,
1273 device=str(device),
1274 nogds=nogds,
1275 )
1276
1277 # GDS can fail either at construction or lazily inside the producer
1278 # thread during iteration (e.g. cuFileHandleRegister returning
1279 # CU_FILE_HANDLE_NOT_REGISTERED on a filesystem without GDS support).
1280 # Catch both and fall back to nogds, but only before yielding any
1281 # tensor -- restarting mid-stream would reload earlier shards.
1282 pl = None
1283 yielded = False
1284 try:
1285 try:
1286 pl = _make_loader(nogds)
1287 for name, tensor in pl.iterate_weights():
1288 yielded = True
1289 yield name, tensor
1290 except RuntimeError as e:
1291 if nogds or yielded or "gds" not in str(e):
1292 raise
1293 logger.warning_once(
1294 "GDS not enabled, setting `nogds=True`.\n"
1295 "For more information, see: https://github.com/foundation-model-stack/"
1296 "fastsafetensors?tab=readme-ov-file#basic-api-usages"
1297 )
1298 if pl is not None:
1299 pl.close()
1300 pl = _make_loader(nogds=True)
1301 yield from pl.iterate_weights()
1302 finally:
1303 if pl is not None:
1304 pl.close()
1305
1306
1307 def instanttensor_weights_iterator(
1308 hf_weights_files: list[str],
1309 use_tqdm_on_load: bool,
1310 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1311 """Iterate over the weights in the model safetensor files
1312 using instanttensor library."""
1313 try:
1314 import instanttensor
1315 except ImportError as e:
1316 raise ImportError(
1317 "Please install instanttensor via `pip install vllm[instanttensor]`"
1318 ) from e
1319
1320 if not current_platform.is_cuda():
1321 raise ValueError("InstantTensor requires NVIDIA GPUs")
1322
1323 try:
1324 world_group = get_world_group()
1325 except AssertionError:
1326 # Entering here only in unit tests where the world group is not initialized.
1327 process_group = None
1328 else:
1329 process_group = world_group.device_group if world_group.world_size > 1 else None
1330
1331 device = current_platform.current_device()
1332
1333 # copy=True yields tensors that own their memory, staying valid after the
1334 # context exits or InstantTensor reuses its buffer.
1335 with instanttensor.safe_open(
1336 hf_weights_files,
1337 framework="pt",
1338 device=device,
1339 process_group=process_group,
1340 copy=True,
1341 ) as f:
1342 # Track bytes so the bar reports load throughput (GB/s).
1343 pbar = tqdm(
1344 total=f.total_tensor_size,
1345 desc="Loading safetensors using InstantTensor loader",
1346 disable=not enable_tqdm(use_tqdm_on_load),
1347 bar_format=_BAR_FORMAT,
1348 position=tqdm._get_free_pos(),
1349 unit="B",
1350 unit_scale=True,
1351 unit_divisor=1024,
1352 mininterval=1.0,
1353 )
1354 try:
1355 for name, tensor in f.tensors():
1356 pbar.update(tensor.numel() * tensor.element_size())
1357 yield name, tensor
1358 finally:
1359 pbar.close()
1360
1361
1362 def pt_weights_iterator(
1363 hf_weights_files: list[str],
1364 use_tqdm_on_load: bool,
1365 pt_load_map_location: str | dict[str, str] = "cpu",
1366 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1367 """Iterate over the weights in the model bin/pt files."""
1368 for bin_file in tqdm(
1369 hf_weights_files,
1370 desc="Loading pt checkpoint shards",
1371 disable=not enable_tqdm(use_tqdm_on_load),
1372 bar_format=_BAR_FORMAT,
1373 ):
1374 state = torch.load(
1375 bin_file, map_location=pt_load_map_location, weights_only=True
1376 )
1377 yield from state.items()
1378 del state
1379
1380
1381 def multi_thread_pt_weights_iterator(
1382 hf_weights_files: list[str],
1383 use_tqdm_on_load: bool,
1384 pt_load_map_location: str | dict[str, str] = "cpu",
1385 max_workers: int = 4,
1386 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1387 """Multi-Thread iterate over the weights in the model bin/pt files."""
1388
1389 def _load_file(bin_file: str):
1390 return torch.load(
1391 bin_file, map_location=pt_load_map_location, weights_only=True
1392 )
1393
1394 with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
1395 futures = [
1396 executor.submit(_load_file, bin_file) for bin_file in hf_weights_files
1397 ]
1398 futures_iter = tqdm(
1399 concurrent.futures.as_completed(futures),
1400 total=len(hf_weights_files),
1401 desc="Multi-thread loading pt checkpoint shards",
1402 disable=not enable_tqdm(use_tqdm_on_load),
1403 bar_format=_BAR_FORMAT,
1404 )
1405
1406 for future in futures_iter:
1407 state = future.result()
1408 yield from state.items()
1409 del state
1410
1411
1412 def default_weight_loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
1413 """Default weight loader."""
1414 try:
1415 if param.numel() == 1 and loaded_weight.numel() == 1:
1416 # Sometimes scalar values aren't considered tensors with shapes
1417 # so if both param and loaded_weight are a scalar,
1418 # reshape to match before copying
1419 param.data.copy_(loaded_weight.view(param.shape))
1420 else:
1421 assert param.size() == loaded_weight.size(), (
1422 f"Attempted to load weight ({loaded_weight.size()}) "
1423 f"into parameter ({param.size()})"
1424 )
1425
1426 param.data.copy_(loaded_weight)
1427 except Exception:
1428 # NOTE: This exception is added for the purpose of setting breakpoint to
1429 # debug weight loading issues.
1430 raise
1431
1432
1433 def row_parallel_weight_loader(
1434 param: torch.Tensor, loaded_weight: torch.Tensor
1435 ) -> None:
1436 """Load weights that are row-parallelized."""
1437 tp_rank = get_tensor_model_parallel_rank()
1438 shard_dim = 0 if param.dim() != 1 else None
1439
1440 if shard_dim is not None:
1441 shard_size = param.data.shape[shard_dim]
1442 start_idx = tp_rank * shard_size
1443 loaded_weight = loaded_weight.narrow(shard_dim, start_idx, shard_size)
1444
1445 return default_weight_loader(param, loaded_weight)
1446
1447
1448 LoaderFunction = Callable[[torch.Tensor, torch.Tensor], None]
1449
1450
1451 def sharded_weight_loader(shard_axis: int) -> LoaderFunction:
1452 """Create a weight loader that shards the weights along the given axis"""
1453
1454 def loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
1455 tp_rank = get_tensor_model_parallel_rank()
1456
1457 shard_size = param.data.shape[shard_axis]
1458 start_idx = tp_rank * shard_size
1459 loaded_weight = loaded_weight.narrow(shard_axis, start_idx, shard_size)
1460
1461 return default_weight_loader(param, loaded_weight)
1462
1463 return loader
1464
1465
1466 def composed_weight_loader(
1467 loader: LoaderFunction, fn: Callable[[torch.Tensor], torch.Tensor]
1468 ) -> LoaderFunction:
1469 """Create a weight loader that post-processes the weights after loading"""
1470
1471 def composed_loader(param: torch.Tensor, loaded_weight: torch.Tensor) -> None:
1472 loader(param, loaded_weight)
1473 param.data.copy_(fn(param))
1474 return
1475
1476 return composed_loader
1477
1478
1479 @torch.no_grad()
1480 def initialize_single_dummy_weight(
1481 param: torch.Tensor,
1482 low: float = -1e-3,
1483 high: float = 1e-3,
1484 seed: int = 1234,
1485 ) -> None:
1486 if param.device.type == "meta":
1487 return # deferred to finalize_layerwise_processing (e.g. online quant)
1488
1489 if (dummy_weight_value := getattr(param, "dummy_weight_value", None)) is not None:
1490 param.fill_(dummy_weight_value)
1491 return
1492
1493 if not torch.is_floating_point(param):
1494 if current_platform.is_rocm():
1495 # On ROCm, integer params (e.g. GPTQ qweight/qzeros) are left
1496 # as torch.empty() by default, giving non-deterministic values
1497 # across processes. Zero them for reproducibility.
1498 param.zero_()
1499 return
1500
1501 if current_platform.is_tpu():
1502 generator = torch.Generator(device="cpu")
1503 generator.manual_seed(seed)
1504 # Note: The param.uniform_ function cannot be used in this
1505 # context because it demands more TPU HBM than directly copying
1506 # from a CPU tensor.
1507 # Note: We avoid using torch.rank_like as it doesn't currently
1508 # support the generator argument.
1509 param.copy_(
1510 (high - low)
1511 * torch.rand(
1512 param.shape,
1513 generator=generator,
1514 dtype=param.dtype,
1515 layout=param.layout,
1516 requires_grad=param.requires_grad,
1517 device="cpu",
1518 )
1519 + low
1520 )
1521 torch._sync(param)
1522 return
1523
1524 generator = torch.Generator(device=param.data.device)
1525 generator.manual_seed(seed)
1526 if torch.finfo(param.data.dtype).bits < 16:
1527 # uniform_ doesn't support < 16-bit datatypes (FP8)
1528 tmp_param = torch.empty_like(param, dtype=torch.float16)
1529 tmp_param.uniform_(low, high, generator=generator)
1530 param.copy_(tmp_param)
1531 else:
1532 param.uniform_(low, high, generator=generator)
1533
1534
1535 def maybe_remap_kv_scale_name(name: str, params_dict: dict) -> str | None:
1536 """Remap the name of FP8 k/v_scale parameters.
1537
1538 This function handles the remapping of FP8 k/v_scale parameter names.
1539 It detects if the given name ends with a suffix and attempts to remap
1540 it to the expected name format in the model. If the remapped name is not
1541 found in the params_dict, a warning is printed and None is returned.
1542
1543 Args:
1544 name (str): The original loaded checkpoint parameter name.
1545 params_dict (dict): Dictionary containing the model's named parameters.
1546
1547 Returns:
1548 str: The remapped parameter name if successful, or the original name
1549 if no remapping is needed.
1550 None: If the remapped name is not found in params_dict.
1551
1552 """
1553 # Already in vLLM's expected form (e.g. weights pre-renamed by a
1554 # `WeightsMapper` from the quant config). Skip the regex remap, which
1555 # would otherwise double-apply the `.attn` prefix and drop the weight.
1556 if name in params_dict:
1557 return name
1558 if name.endswith(".kv_scale"):
1559 logger.warning_once(
1560 "DEPRECATED. Found kv_scale in the checkpoint. "
1561 "This format is deprecated in favor of separate k_scale and "
1562 "v_scale tensors and will be removed in a future release. "
1563 "Functionally, we will remap kv_scale to k_scale and duplicate "
1564 "k_scale to v_scale"
1565 )
1566 # NOTE: we remap the deprecated kv_scale to k_scale
1567 remapped_name = name.replace(".kv_scale", ".attn.k_scale")
1568 if remapped_name not in params_dict:
1569 logger.warning_once(
1570 "Found kv_scale in the checkpoint (e.g. %s), but not found the expected name in the model (e.g. %s). kv_scale is not loaded.", # noqa: E501
1571 name,
1572 remapped_name,
1573 )
1574 return None
1575 return remapped_name
1576
1577 if any("mla_attn" in key for key in params_dict):
1578 attn_str = "mla_attn.mla_attn"
1579 logger.debug_once(
1580 f"Found mla_attn with k_scale and v_scale in "
1581 f"the checkpoint, using {attn_str} as attn_str"
1582 )
1583 else:
1584 attn_str = "attn"
1585 # Define scale name mapping patterns in order of precedence
1586 scale_mapping_patterns = [
1587 # ModelOpt format: .self_attn.{k,v}_proj.{k,v}_scale ->
1588 # .self_attn.attn.{k,v}_scale
1589 (
1590 r"\.self_attn\.([kv])_proj\.([kv])_scale$",
1591 rf".self_attn.{attn_str}.\2_scale",
1592 ),
1593 # QKV proj format: .self_attn.qkv_proj.{k,v}_scale ->
1594 # .self_attn.attn.{k,v}_scale
1595 (r"\.self_attn\.qkv_proj\.([kv])_scale$", r".self_attn.attn.\1_scale"),
1596 # Qwen3 MoE format: .self_attn.qkqkv_proj.{k,v}_scale ->
1597 # .self_attn.attn.{k,v}_scale
1598 (r"\.self_attn\.qkqkv_proj\.([kv])_scale$", r".self_attn.attn.\1_scale"),
1599 # NemotronH format: .mixer.{k,v}_proj.{k,v}_scale ->
1600 # .mixer.attn.{k,v}_scale
1601 (r"\.mixer\.[kv]_proj\.([kv])_scale$", r".mixer.attn.\1_scale"),
1602 # HYV3 format: .self_attn.q.scale -> .self_attn.attn.q_scale
1603 (r"\.self_attn\.q\.scale$", r".self_attn.attn.q_scale"),
1604 # HYV3 format: .self_attn.{k,v}_cache.scale ->
1605 # .self_attn.attn.{k,v}_scale
1606 (r"\.self_attn\.([kv])_cache\.scale$", r".self_attn.attn.\1_scale"),
1607 # Default format: .{k,v}_scale -> .attn.{k,v}_scale
1608 (r"\.([qkv])_scale$", r".attn.\1_scale"),
1609 (r"\.([qkv])_zero_point$", r".attn.\1_zero_point"),
1610 ]
1611
1612 # Check if name ends with k_scale or v_scale
1613 if name.endswith(
1614 (
1615 ".k_scale",
1616 ".v_scale",
1617 ".q_scale",
1618 ".k_zero_point",
1619 ".v_zero_point",
1620 ".q_zero_point",
1621 ".q.scale",
1622 ".k_cache.scale",
1623 ".v_cache.scale",
1624 )
1625 ):
1626 import regex as re
1627
1628 for pattern, replacement in scale_mapping_patterns:
1629 if re.search(pattern, name):
1630 remapped_name = re.sub(pattern, replacement, name)
1631 if remapped_name not in params_dict:
1632 scale_type = name.split(".")[-1]
1633 logger.warning_once(
1634 "Found %s in the checkpoint (e.g. %s), but not found the expected name in the model (e.g. %s). %s is not loaded.", # noqa: E501
1635 scale_type,
1636 name,
1637 remapped_name,
1638 scale_type,
1639 )
1640 return None
1641 return remapped_name
1642
1643 # If there were no matches, return the untouched param name
1644 return name
1645
1646
1647 def maybe_remap_moe_expert_param_name(
1648 name: str,
1649 params_dict: dict[str, torch.nn.Parameter],
1650 ) -> str:
1651 """Remap MoE expert parameter names to account for routed_experts hierarchy.
1652
1653 This handles the transition from the old FusedMoE structure where weights
1654 were directly in the experts module, to the new MoERunner → RoutedExperts
1655 structure.
1656
1657 Checkpoint weights have names like:
1658 layers.0.mlp.experts.w13_weight
1659 layers.0.feed_forward.experts.w2_input_scale
1660 But actual parameters are now:
1661 layers.0.mlp.experts.routed_experts.w13_weight
1662 layers.0.feed_forward.experts.routed_experts.w2_input_scale
1663
1664 This function inserts 'routed_experts.' into the path when needed.
1665
1666 Args:
1667 name: Parameter name from checkpoint
1668 params_dict: Dictionary of model parameters (from named_parameters())
1669
1670 Returns:
1671 Remapped parameter name if routed_experts hierarchy exists,
1672 otherwise the original name
1673
1674 """
1675 # Only remap if this looks like an expert parameter
1676 if ".experts." not in name:
1677 return name
1678
1679 # Skip if already has routed_experts
1680 if ".experts.routed_experts." in name:
1681 return name
1682
1683 # Expert parameter patterns to check
1684 expert_param_suffixes = [
1685 "w13_weight",
1686 "w2_weight",
1687 "w13_weight_scale",
1688 "w2_weight_scale",
1689 "w13_input_scale",
1690 "w2_input_scale",
1691 "w13_bias",
1692 "w2_bias",
1693 "w13_scale",
1694 "w2_scale",
1695 "w13_qweight",
1696 "w2_qweight",
1697 "w13_qzeros",
1698 "w2_qzeros",
1699 "w13_weight_shape",
1700 "w2_weight_shape",
1701 ]
1702
1703 # Check if this is an expert weight parameter
1704 is_expert_param = any(
1705 f".{suffix}" in name or name.endswith(suffix)
1706 for suffix in expert_param_suffixes
1707 )
1708
1709 if not is_expert_param:
1710 return name
1711
1712 # Try inserting routed_experts after .experts.
1713 new_name = name.replace(".experts.", ".experts.routed_experts.", 1)
1714
1715 # Only use the new name if it exists in the model
1716 if new_name in params_dict:
1717 return new_name
1718
1719 # Otherwise return original name (old checkpoint format or different structure)
1720 return name
1721
1722
1723 def remap_moe_expert_weights(
1724 weights: Iterable[tuple[str, torch.Tensor]],
1725 params_dict: dict[str, torch.nn.Parameter],
1726 ) -> Generator[tuple[str, torch.Tensor], None, None]:
1727 """Remap MoE expert parameter names for backward compatibility.
1728
1729 This allows models with custom weight loading to automatically handle both old
1730 and new checkpoint formats without needing model-specific remapping code.
1731
1732 Usage:
1733 params_dict = dict(model.named_parameters())
1734 for name, weight in remap_moe_expert_weights(weights, params_dict):
1735 # name is automatically remapped if needed
1736 param = params_dict[name]
1737 ...
1738
1739 Args:
1740 weights: Iterator of (name, tensor) tuples from checkpoint
1741 params_dict: Dictionary of model parameters (from named_parameters())
1742
1743 Yields:
1744 (remapped_name, tensor) tuples
1745
1746 """
1747 for name, weight in weights:
1748 remapped_name = maybe_remap_moe_expert_param_name(name, params_dict)
1749 yield (remapped_name, weight)