"Weight loading: from checkpoint files to GPU parameters"
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
Each worker streams all checkpoint tensors; a mapper renames each one, and the target parameter's weight loader copies only this rank's slice.
The idea#
A checkpoint is a set of named tensors in safetensors files, and loading copies each tensor into the matching model parameter (Weight loading). The page Hugging Face checkpoints shows which files vLLM downloads and keeps. This page follows the tensors from the files into the parameters of each GPU.
vLLM loads the weights in six steps, in every worker:
- Pick a loader. The load format (
--load-format, defaultauto) selects a model loader class. Forautoit isDefaultModelLoader. - Build an empty model. The loader builds the model class on the GPU. Each tensor-parallel layer makes parameters of its per-rank shape, with no values yet.
- Stream the tensors. An iterator opens the files one by one and gives
(name, tensor)pairs. In the default path, the tensors stay on the CPU. - Map the names. The model's
load_weights()uses an AutoWeightsLoader. A WeightsMapper renames checkpoint names to vLLM names, for exampleq_projtoqkv_projwith the shard id"q". - Copy the slice. The target parameter has a weight loader. It cuts the part of the tensor that this rank owns and copies it into the right rows of the parameter.
- Post-process. Each layer's quantization method can repack or quantize its weights once, in
process_weights_after_loading.
An everyday comparison. Think of the branches of a library that all get the same delivery list. Each branch reads every line of the list. For each book, the list gives the shelf name in the publisher's words, and a branch translates it into its own shelf names. Some books go into a shared box with three marked parts, such as Q, K and V. Each branch keeps only the pages for its own readers. The comparison stops here. In vLLM, nobody sends a branch its part: every GPU walks the full list itself, and the parameter cuts its slice.
The picture#
Read panels 1 and 2 from left to right: they run the same way in every worker. Panel 3 shows the only step where the ranks differ. The rows of the stacked parameter are the same on both ranks; the rows taken from the checkpoint tensor depend on the rank.
In the code#
Step 1: the load format picks the loader#
_LOAD_FORMAT_TO_MODEL_LOADER: dict[str, type[BaseModelLoader]] = {
"auto": DefaultModelLoader,
"hf": DefaultModelLoader,
"dummy": DummyModelLoader,
"fastsafetensors": DefaultModelLoader,
"instanttensor": DefaultModelLoader,
"ipc_cache": IpcModelLoader,
"mistral": DefaultModelLoader,
"modelexpress": ModelExpressModelLoader,
"npcache": DefaultModelLoader,
"pt": DefaultModelLoader,
"runai_streamer": RunaiModelStreamerLoader,
"runai_streamer_sharded": ShardedStateLoader,
"safetensors": DefaultModelLoader,
"sharded_state": ShardedStateLoader,
"tensorizer": TensorizerLoader,
}get_model_loader() looks up load_config.load_format in this table and raises ValueError for an unknown name (vllm/model_executor/model_loader/__init__.py:123-128). The field default is "auto", and a validator lower-cases the value (vllm/config/load.py:30, vllm/config/load.py:136-138). EngineArgs copies this default without a change (vllm/engine/arg_utils.py:509). One later override exists: for a model in object storage (S3, GCS, Azure), auto becomes runai_streamer (vllm/config/vllm.py:2754-2762). A plugin can add a format with register_model_loader (Hooks in the engine core and the workers).
Model Runner V2 calls the loader in load_model() (vllm/v1/worker/gpu/model_runner.py:398-406). It runs in every worker: with TP = 2, two processes do all the steps below at the same time.
Step 2: build the model with empty parameters#
@instrument(span_name="Load model")
def load_model(
self, vllm_config: VllmConfig, model_config: ModelConfig, prefix: str = ""
) -> nn.Module:
"""Load a model with the given configurations."""
device_config = vllm_config.device_config
load_config = vllm_config.load_config
load_device = (
device_config.device if load_config.device is None else load_config.device
)
target_device = torch.device(load_device)
with set_default_torch_dtype(model_config.dtype):
with target_device:
time_before_load = time.perf_counter()
model = self.create_model(
vllm_config=vllm_config,
model_config=model_config,
prefix=prefix,
)
time_after_load = time.perf_counter()
logger.info_once(
"Initializing model took %.6f seconds",
time_after_load - time_before_load,
)
logger.debug("Loading weights on %s ...", load_device)
self.load_weights(model, model_config)
# Release transient tensors held by PyTorch's caching allocator
# before weight post-processing and raw CUDA driver allocations.
gc.collect()
torch.accelerator.empty_cache()
# Log peak GPU memory after loading weights. This is needed
# to have test coverage on peak memory for online quantization.
if current_platform.is_cuda_alike() or current_platform.is_xpu():
peak_memory = torch.accelerator.max_memory_allocated()
logger.debug_once(
"Peak GPU memory after loading weights: %s GiB",
format_gib(peak_memory),
)
# Process weights into kernel format. Note that when using online
# quantization, weights are (typically) quantized as they are loaded.
if _has_online_quant(model):
finalize_layerwise_processing(model, model_config)
process_weights_after_loading(model, model_config, target_device)
log_online_quantization_time(vllm_config)
gc.collect()
torch.accelerator.empty_cache()
return model.eval()- Lines 73-74. The default dtype is the model dtype, and the default device is the GPU of this worker. So every
torch.emptyin a layer constructor makes a GPU tensor of the right dtype. - Lines 76-80.
create_model()callsinitialize_model(), which builds the class withvllm_configandprefix(How a vLLM model file is written (llama.py)). The log line "Initializing model took ... seconds" measures this part. - Line 88.
self.load_weights()is the loader's method; for the default loader it streams the files (Step 3). - Lines 106-109. Online quantization finishes first; then
process_weights_after_loading()runs for every layer (Step 6).
The layer constructors do the per-rank sizing. UnquantizedLinearMethod.create_weights() makes torch.empty(sum(output_partition_sizes), input_size_per_partition) (vllm/model_executor/layers/linear.py:178-205). The page The tensor-parallel layers (linear.py and vocab_parallel_embedding.py) gives these sizes for each layer class.
Step 3: choose the files and stream the tensors#
DefaultModelLoader._prepare_weights() finds, downloads and filters the files (vllm/model_executor/model_loader/default_loader.py:147-263); Hugging Face checkpoints explains that part. Then _get_weights_iterator() picks an iterator by format (vllm/model_executor/model_loader/default_loader.py:301-365). The usual case is safetensors_weights_iterator():
else:
with safe_open(st_file, framework="pt") as f:
for name in f.keys(): # noqa: SIM118
if should_skip_weight(name, local_expert_ids):
continue
param = f.get_tensor(name)
yield name, param- Order. The files are sorted by a natural sort key, so
...-00002-of-...comes before...-00010-of-...(vllm/model_executor/model_loader/weight_utils.py:1134). - Strategies.
--safetensors-load-strategyisNoneby default.eagerreads the whole file into CPU memory first;torchaorebuilds torchao tensors (vllm/model_executor/model_loader/weight_utils.py:1207-1242). - Prefetch. With the default
None, vLLM prefetches the files into the page cache only on NFS or Lustre. They must also fit in 90% of the free RAM (vllm/model_executor/model_loader/weight_utils.py:1136-1155).prefetchforces it. Each process prefetches a different part of the files,sorted_files[rank::world_size]. Rank and world size are the global values oftorch.distributed, not the TP values (vllm/model_executor/model_loader/weight_utils.py:1051-1057). - PyTorch files. For
.binand.ptfiles,pt_weights_iterator()callstorch.load(..., weights_only=True)on each file and yields its items (vllm/model_executor/model_loader/weight_utils.py:1479-1495). - Many threads.
--model-loader-extra-config '{"enable_multithread_load": true}'selects the multi-thread iterators, with 8 threads by default (vllm/model_executor/model_loader/default_loader.py:58-59,vllm/model_executor/model_loader/default_loader.py:329-336).
The loader then calls model.load_weights() with the iterator, and logs "Loading weights took ... seconds" (vllm/model_executor/model_loader/default_loader.py:503-509).
Step 4: map the names (WeightsMapper and AutoWeightsLoader)#
Llama has two load_weights methods. The outer class makes an AutoWeightsLoader without a mapper (vllm/model_executor/models/llama.py:559-561). The loader groups the names by their first part and finds the child model. That child has its own load_weights, so the loader gives it the names without model. (vllm/model_executor/models/utils.py:387-400). LlamaModel.load_weights uses the mapper:
hf_to_vllm_mapper = WeightsMapper(
orig_to_new_stacked={
# weight_name: (param_name, shard_id)
".q_proj": (".qkv_proj", "q"),
".k_proj": (".qkv_proj", "k"),
".v_proj": (".qkv_proj", "v"),
".gate_proj": (".gate_up_proj", 0),
".up_proj": (".gate_up_proj", 1),
}
) shard_id: ShardIds | None = None
for substr, (new_key, new_shard_id) in self.orig_to_new_stacked.items():
if substr in key:
key = key.replace(substr, new_key, 1)
shard_id = new_shard_id_map_name_with_shard(): each stacked key that is in the name replaces its first occurrence, and the loop keeps the shard id. def apply(
self, weights: Iterable[tuple[str, torch.Tensor]]
) -> Iterable[tuple[str, torch.Tensor]]:
for name, data in weights:
result = self._map_name_with_shard(name)
if result is None:
continue
out_name, shard_id = result
if shard_id is None:
yield out_name, data
continue
shard_ids = shard_id if isinstance(shard_id, list) else [shard_id]
for i, one_shard_id in enumerate(shard_ids):
# Each shard carries its own id, so extra shards need their own
# tensor object; `detach` aliases the storage instead of copying.
shard_data = data if i == 0 else data.detach()
shard_data.shard_id = one_shard_id
yield out_name, shard_dataapply() puts the shard id on the tensor itself, as the attribute shard_id.The mapper has six kinds of rule: orig_to_new_renaming, _regex, _substr, _stacked, _prefix and _suffix, applied in this order. A regex, substring, prefix or suffix rule that maps to None drops the tensor (vllm/model_executor/models/utils.py:48-61, vllm/model_executor/models/utils.py:108-145). AutoWeightsLoader.load_weights() adds more mappers. With a quantization config, it adds the config's mappers for KV cache scale names and checkpoint names. It always adds a rule that drops old rotary tables (vllm/model_executor/models/utils.py:460-473).
Then the loader walks the module tree:
child_modules = dict(module.named_children())
child_params = dict(module.named_parameters(recurse=False))
# Add missing tensors the weight loader needs to be able to load
# that aren't registered as params, e.g., batchnorm statistics.
self._add_loadable_non_param_tensors(module, child_params)
for child_prefix, child_weights in self._groupby_prefix(weights):
prefix = self._get_qualname(base_prefix, child_prefix)
if child_prefix in child_modules:
yield from self._load_module(
prefix, child_modules[child_prefix], child_weights
)
elif child_prefix in child_params:
if self._can_skip(prefix):
logger.debug("Skipping param %s", prefix)
continue
yield from self._load_param(
prefix, child_params[child_prefix], child_weights
)- Line 409.
_groupby_prefix()splits each name at the first dot:layers.0.self_attn.qkv_proj.weightbecomeslayersand0.self_attn.qkv_proj.weight. - Lines 412-415. A child module: recurse. For
QKVParallelLinearthe recursion ends early, because that layer has its ownload_weights(line 388). - Lines 416-424. A parameter:
_load_param()callsparam.weight_loader, ordefault_weight_loaderwhen the parameter has none (vllm/model_executor/models/utils.py:340-341).
QKVParallelLinear.load_weights reads the shard id from the tensor and calls param.weight_loader(param, loaded_weight, shard_id) (vllm/model_executor/layers/linear.py:1339-1354). MergedColumnParallelLinear has the same method for the integer shard ids of gate_up_proj (vllm/model_executor/layers/linear.py:985-1000).
Step 5: the weight loader of each parameter#
A linear layer chooses one of two loader families when it is built. If the class name of its quantization method is in WEIGHT_LOADER_V2_SUPPORTED, it uses weight_loader_v2; else weight_loader (vllm/model_executor/layers/linear.py:530-534). UnquantizedLinearMethod is in the list, so a BF16 model uses v2 (vllm/model_executor/layers/linear.py:50-64). The v2 loaders give the work to the parameter class:
def load_column_parallel_weight(self, loaded_weight: torch.Tensor):
shard_size = self.data.shape[self.output_dim]
loaded_weight = loaded_weight.narrow(
self.output_dim, self.tp_rank * shard_size, shard_size
)
assert self.data.shape == loaded_weight.shape
self.data.copy_(loaded_weight)output_dim (dimension 0 of a [out, in] weight) at tp_rank × shard_size. def load_row_parallel_weight(self, loaded_weight: torch.Tensor):
shard_size = self.data.shape[self.input_dim]
loaded_weight = loaded_weight.narrow(
self.input_dim, self.tp_rank * shard_size, shard_size
)
if len(loaded_weight.shape) == 0:
loaded_weight = loaded_weight.reshape(1)
assert self.data.shape == loaded_weight.shape
self.data.copy_(loaded_weight)input_dim (dimension 1) at tp_rank × shard_size.The shard size is the size of the parameter on this rank, so the loader needs no TP arithmetic of its own. ModelWeightParameter inherits both methods, and the layer calls the one that fits (vllm/model_executor/parameter.py:229-234). The v1 loaders do the same cut with getattr(param, "output_dim") and "input_dim" (vllm/model_executor/layers/linear.py:589-606, vllm/model_executor/layers/linear.py:1758-1774).
For a stacked parameter, the loader first selects the part of the parameter, then the part of the tensor:
def load_qkv_weight(self, loaded_weight: torch.Tensor, **kwargs):
shard_offset: int = kwargs["shard_offset"]
shard_size: int = kwargs["shard_size"]
shard_id: str = kwargs["shard_id"]
num_heads: int = kwargs["num_heads"]
# TODO: move these to PackedColumnParameter and PackedvLLMParameter
if (
isinstance(self, (PackedColumnParameter, PackedvLLMParameter))
and self.output_dim == self.packed_dim
):
shard_size, shard_offset = self.adjust_shard_indexes_for_packing(
shard_offset=shard_offset, shard_size=shard_size
)
param_data = self.data
shard_id_int = self.tp_rank if shard_id == "q" else self.tp_rank // num_heads
param_data = param_data.narrow(self.output_dim, shard_offset, shard_size)
loaded_weight = loaded_weight.narrow(
self.output_dim, shard_id_int * shard_size, shard_size
)
assert param_data.shape == loaded_weight.shape
param_data.copy_(loaded_weight)- Line 191. For
q, the slice index is the TP rank. Forkandvit istp_rank // num_kv_head_replicas;num_headshere carries the replica count (vllm/model_executor/layers/linear.py:1203-1209). - Line 192. The part of the parameter: offset and size from the head counts of this rank (
vllm/model_executor/layers/linear.py:1101-1117). - Lines 193-195. The part of the checkpoint tensor. Then an assertion and one copy.
Other parameters:
- Embedding and LM head.
VocabParallelEmbedding.weight_loadercuts rowsorg_vocab_start_indextoorg_vocab_end_index, and fills the padding rows with zeros (vllm/model_executor/layers/vocab_parallel_embedding.py:497-520). - Norm weights.
RMSNorm.weightis a plainnn.Parameterwithout a loader (vllm/model_executor/layers/layernorm.py:65-67).default_weight_loaderchecks the shape and copies the full tensor on every rank (vllm/model_executor/model_loader/weight_utils.py:1529-1547).
Step 6: older model files: stacked_params_mapping#
Before WeightsMapper had stacked rules, each model wrote the loop itself. 38 files in vllm/model_executor/models/ still do. 98 other files use orig_to_new_stacked, and no file uses both (git grep at this commit). An example:
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
]
expert_params_mapping = [
# (param_name, weight_name, expert_id)
(
"ws" if weight_name in ["w1", "w3"] else "w2s",
f"experts.{expert_id}.{weight_name}.weight",
expert_id,
)
for expert_id in range(self.num_experts)
for weight_name in ["w1", "w2", "w3"]
]
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
# Models trained using ColossalAI may include these tensors in
# the checkpoint. Skip them.
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if is_pp_missing_parameter(name, self):
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break- Lines 489-496. The list has the same content as Llama's mapper, as tuples
(param_name, shard_name, shard_id). - Lines 517-528. For each name, find a matching tuple and rename. Skip parameters of other pipeline stages. Call the loader with the shard id as the third argument.
Step 7: quantized weights#
For a quantized checkpoint, the layer's quantization method makes the parameters in create_weights() (Quantization methods as plugins). FP8, for example, registers an FP8 weight and a weight_scale (or weight_scale_inv for block scales), each with the layer's weight loader (vllm/model_executor/layers/quantization/fp8.py:312-339). The checkpoint has tensors with the same names, so the mapping and slicing above work for them too. A per-tensor scale of a stacked layer has one slot per part (q, k, v). PerTensorScaleParameter copies each checkpoint scale into the slot of its shard id (vllm/model_executor/parameter.py:253-301); the v1 loaders use adjust_scalar_to_fused_array for the same job (vllm/model_executor/layers/linear.py:97-121).
After all tensors are copied, process_weights_after_loading() visits every module with a quantization method and calls its post-processing (vllm/model_executor/model_loader/utils.py:157-196). FP8 with per-tensor scales, for example, re-quantizes the parts of a stacked layer to one scale, the maximum of their scales, and transposes the weight. This happens only without block scales and without the Marlin kernel (vllm/model_executor/layers/quantization/fp8.py:372-409, vllm/model_executor/layers/quantization/utils/fp8_utils.py:1454-1479). The page Quantized checkpoints explains the formats.
Step 8: the other loaders#
The table maps 15 format names to 7 classes (vllm/model_executor/model_loader/__init__.py:50-66). There is no gguf and no bitsandbytes format at this commit.
| Format names | Class | What it does |
|---|---|---|
auto, hf, safetensors, pt, npcache, mistral, fastsafetensors, instanttensor | DefaultModelLoader | The path of this page; the name selects the file patterns and the iterator (vllm/model_executor/model_loader/default_loader.py:184-203). |
dummy | DummyModelLoader | Random values, no files; for profiling (vllm/model_executor/model_loader/dummy_loader.py:21-48). |
sharded_state, runai_streamer_sharded | ShardedStateLoader | Each rank reads its own pre-sharded files, for example model-rank-{rank}-part-{part}.safetensors (another pattern with PP), with no slicing (vllm/model_executor/model_loader/sharded_state_loader.py:30-39). |
tensorizer | TensorizerLoader | CoreWeave tensorizer files (vllm/config/load.py:48-50). |
runai_streamer | RunaiModelStreamerLoader | Safetensors through the Run:ai Model Streamer (vllm/config/load.py:51-52). |
modelexpress | ModelExpressModelLoader | Weights through ModelExpress (vllm/config/load.py:59). |
ipc_cache | IpcModelLoader | Maps already processed weights from a local cache daemon over CUDA IPC (vllm/config/load.py:41-43). |
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: q_proj, k_proj and v_proj of layer 0, TP = 2
These values are from the Llama-3-8B config.json on the Hugging Face Hub: hidden_size 4096, num_attention_heads 32, num_key_value_heads 8. So the head size is 4096 / 32 = 128 (llama.py line 159). The checkpoint stores PyTorch linear weights as [out, in].
| Checkpoint name | Shape | After the mapper | Shard id |
|---|---|---|---|
model.layers.0.self_attn.q_proj.weight | [4096, 4096] | layers.0.self_attn.qkv_proj.weight | "q" |
model.layers.0.self_attn.k_proj.weight | [1024, 4096] | layers.0.self_attn.qkv_proj.weight | "k" |
model.layers.0.self_attn.v_proj.weight | [1024, 4096] | layers.0.self_attn.qkv_proj.weight | "v" |
On each rank, QKVParallelLinear has num_heads = 16, num_kv_heads = 4 and num_kv_head_replicas = 1 (vllm/model_executor/layers/linear.py:1064-1071). The parameter is [16×128 + 4×128 + 4×128, 4096] = [3072, 4096], 24 MiB in BF16. The loads:
| Shard | Parameter rows (both ranks) | Checkpoint rows, rank 0 | Checkpoint rows, rank 1 |
|---|---|---|---|
q | 0-2047 (offset 0, size 2048) | 0-2047 | 2048-4095 |
k | 2048-2559 (offset 16×128, size 512) | 0-511 | 512-1023 |
v | 2560-3071 (offset 20×128, size 512) | 0-511 | 512-1023 |
Each rank reads 4096 × 4096 + 2 × 1024 × 4096 values of the checkpoint (48 MiB) and keeps half of them (24 MiB). For the whole model, each rank keeps about 4.0 of the 8.03 billion parameters: about 8.0 GB in BF16. (Derived from the shapes. The small norm weights are on both ranks.)
Deep diveAutoWeightsLoader.load_weights, line by line
@support_quantized_model_reload_from_hp_weights
def load_weights(
self,
weights: Iterable[tuple[str, torch.Tensor]],
*,
mapper: WeightsMapper | None = None,
) -> set[str]:
# Ignore unexpected biases (typically from GPTQ models)
self.ignore_unexpected_suffixes.append(".bias")
mapper = mapper or WeightsMapper()
# Many models store quant_config in the base model instead of the causal model.
# We look at the causal model's direct children for this reason.
modules = (self.module, *self.module.children())
iterator = (m.quant_config for m in modules if hasattr(m, "quant_config"))
if quant_config := next(iterator, None):
# Apply mappings for quantization-specific checkpoint tensors.
mapper |= quant_config.get_cache_scale_mapper()
mapper |= quant_config.get_checkpoint_weight_mapper()
ignore_unexpected_suffixes = quant_config._ignore_unexpected_suffixes
self.ignore_unexpected_suffixes.extend(ignore_unexpected_suffixes)
mapper |= self.REMOVE_UNUSED_ROTARY_EMBEDS_MAPPER
weights = mapper.apply(weights)
weights = self._filter_skipped(weights)
autoloaded_weights = set(self._load_module("", self.module, weights))
self._check_skipped_aliases(autoloaded_weights)
return autoloaded_weights- Line 458. Unexpected
.biastensors are ignored; the comment says GPTQ checkpoints often have them. - Lines 463-470. The quantization config can be on the model or on a direct child. If it exists, its mappers and its ignore list join in.
- Line 471. Old rotary tables such as
rotary_emb.inv_freqare dropped (vllm/model_executor/models/utils.py:233-240). - Lines 473-474. Both steps are generators: no tensor is read before the tree walk asks for it.
- Line 476. The walk returns the names of all loaded parameters. The default loader compares them with
model.named_parameters()(next deep dive). - Line 477. With tied embeddings (
lm_head.weightisembed_tokens.weight), the loader loads the first name only. If that name is missing in the checkpoint, it raisesValueError(vllm/model_executor/models/utils.py:491-502).
Inside the walk, a PPMissingLayer returns at once, so the tensors of other pipeline stages are skipped (vllm/model_executor/models/utils.py:382-383). A name that matches no child and no parameter raises ValueError with the list of available names (vllm/model_executor/models/utils.py:425-448).
Deep diveMemory, time and the completeness check
- Where the bytes go. The parameters are allocated on the GPU in Step 2, at their final per-rank size. The checkpoint tensors stay on the CPU; each loader copies a slice into the GPU parameter with
copy_. - Peak memory. After the load, the loader logs "Peak GPU memory after loading weights" at DEBUG level on CUDA, ROCm and XPU (
vllm/model_executor/model_loader/base_loader.py:95-102). - Three timers. "Initializing model took" (constructors,
vllm/model_executor/model_loader/base_loader.py:82-85), "Loading weights took" (the stream,vllm/model_executor/model_loader/default_loader.py:505-509), and "Model loading took ... GiB memory and ... seconds" (the wholeload_model,vllm/v1/worker/gpu/model_runner.py:428-435). - Completeness. For a model without quantization, the default loader checks that parameters got a value; else "Following weights were not initialized from checkpoint" (
vllm/model_executor/model_loader/default_loader.py:510-523,vllm/model_executor/model_loader/default_loader.py:567-572). In this case, it exempts all parameters of modules that have a quantization method (vllm/model_executor/model_loader/default_loader.py:533-566). Every quantize method hasprocess_weights_after_loading(vllm/model_executor/layers/quantization/base_config.py:80-85), so in a BF16 Llama the linear, embedding and head weights are exempt too. The check then covers only parameters such as the norm weights (derived).enable_weights_trackin--model-loader-extra-configturns the check on or off.
Deep diveFlags and their defaults
| Flag | Default | Defined at | Effect |
|---|---|---|---|
--load-format | auto | vllm/config/load.py:30-61 | Loader class and file patterns. |
--download-dir | Hugging Face cache | vllm/config/load.py:62-64 | Where to download and read the files. |
--safetensors-load-strategy | None | vllm/config/load.py:65-86 | lazy, eager, prefetch, torchao; None = lazy with auto-prefetch on NFS/Lustre. |
--safetensors-prefetch-num-threads | 8 | vllm/config/load.py:12, vllm/config/load.py:87-91 | Prefetch threads. |
--safetensors-prefetch-block-size | 16 MiB | vllm/config/load.py:13, vllm/config/load.py:92-95 | Read size per prefetch call. |
--model-loader-extra-config | {} | vllm/config/load.py:96-98 | For the default loader only enable_multithread_load, num_threads, enable_weights_track; other keys raise (vllm/model_executor/model_loader/default_loader.py:103-115). |
--ignore-patterns | ["original/**/*"] | vllm/config/load.py:102-104 | Files not to download, so the extra Llama files in original/ are skipped. |
--pt-load-map-location | cpu | vllm/config/load.py:108-117 | map_location of torch.load for .bin/.pt files. |
The flags come from the LoadConfig fields (vllm/engine/arg_utils.py:1069-1094).
What can go wrong#
- A shape mismatch. The weight loaders assert that the slice and the parameter have the same shape (
vllm/model_executor/parameter.py:197,vllm/model_executor/model_loader/weight_utils.py:1538-1541). A checkpoint for another config, or a wrong--hf-overrides, fails here. - Missing weights. An unquantized model raises "Following weights were not initialized from checkpoint" for an empty norm weight, or another parameter outside quantize-method modules (
vllm/model_executor/model_loader/default_loader.py:533-572). A missing linear weight gives no error here (derived). - A dtype mismatch. A tensor with another dtype is converted by
copy_, and the loader logs a warning (vllm/model_executor/models/utils.py:324-339). - An unknown format.
--load-format ggufraises "Load format `gguf` is not supported" (vllm/model_executor/model_loader/__init__.py:126-127). - Slow loads from network storage. On NFS, a large model that does not fit in 90% of the free RAM is not prefetched, and vLLM logs a warning (
vllm/model_executor/model_loader/weight_utils.py:1156-1165). Try--safetensors-load-strategy eagerorprefetch, or a pre-sharded checkpoint.
Why it matters. Weight loading is often the longest part of start-up, and its errors are the first errors of a new model or a new checkpoint. When you know the path from file to parameter, you can read the log timers, fix name and shape errors, and choose a faster format.
Check yourself#
- 1Which loader class serves the defaulteasy
--load-format auto, and which iterator does it use for a safetensors checkpoint?Show answer
DefaultModelLoader(vllm/model_executor/model_loader/__init__.py:51). Without the multi-thread option it usessafetensors_weights_iterator(vllm/model_executor/model_loader/default_loader.py:337-349). - 2How doesmedium
QKVParallelLinearknow that a tensor is the K part?Show answer
Llama's mapper renames
.k_projto.qkv_projwith the shard id"k"(vllm/model_executor/models/llama.py:351-353).WeightsMapper.apply()stores it astensor.shard_id(vllm/model_executor/models/utils.py:158-164), andQKVParallelLinear.load_weightsreads it back (vllm/model_executor/layers/linear.py:1342-1354). - 3For a BF16 model with TP = 4, which dimension ofmedium
o_proj.weightdoes each rank cut, and at which offset?Show answer
o_projis row-parallel, so the loader cutsinput_dim= 1 attp_rank × shard_size(vllm/model_executor/parameter.py:216-220). For Llama-3-8B, the parameter is[4096, 1024], so rank 2 takes columns 2048-3071. - 4A model has 8 KV heads and runs with TP = 16. Which rows ofhard
k_proj.weightdoes rank 5 load? Why does rank 4 load the same rows?Show answer
num_kv_headsis 1 andnum_kv_head_replicasis 2 (vllm/model_executor/layers/linear.py:1066-1068). The slice index forkistp_rank // num_heads, wherenum_headscarries the replica count: 5 // 2 = 2 (vllm/model_executor/parameter.py:191,vllm/model_executor/layers/linear.py:1203-1205). With a slice size of 128 rows, rank 5 loads rows 256-383 (KV head 2). Rank 4 gives 4 // 2 = 2 too, so both ranks hold a copy of KV head 2. - 5A model runs with PP = 2. What happens to the checkpoint tensors of layers 16-31 on stage 0?hard
Show answer
The iterator still reads every tensor of every file, because it calls
get_tensorfor each name (vllm/model_executor/model_loader/weight_utils.py:1243-1249). The tree walk reachesPPMissingLayerobjects for those layers and returns without a copy (vllm/model_executor/models/utils.py:382-383). Hand-written loaders useis_pp_missing_parameter()for the same skip (vllm/model_executor/models/minicpm.py:524-525).