vLLM Explained

"Weight loading: from checkpoint files to GPU parameters"

AdvancedvLLM conceptsabout 14 min read + 4 min deep dives

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:

  1. Pick a loader. The load format (--load-format, default auto) selects a model loader class. For auto it is DefaultModelLoader.
  2. 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.
  3. 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.
  4. Map the names. The model's load_weights() uses an AutoWeightsLoader. A WeightsMapper renames checkpoint names to vLLM names, for example q_proj to qkv_proj with the shard id "q".
  5. 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.
  6. 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#

Figure: the loader streams (name, tensor) pairs (panel 1); a mapper and the module tree route each tensor to a parameter (panel 2); the parameter of each rank copies only its own rows (panel 3). Shapes: Llama-3-8B with TP = 2.
Open full size ↗

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()
  1. Lines 73-74. The default dtype is the model dtype, and the default device is the GPU of this worker. So every torch.empty in a layer constructor makes a GPU tensor of the right dtype.
  2. Lines 76-80. create_model() calls initialize_model(), which builds the class with vllm_config and prefix (How a vLLM model file is written (llama.py)). The log line "Initializing model took ... seconds" measures this part.
  3. Line 88. self.load_weights() is the loader's method; for the default loader it streams the files (Step 3).
  4. 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
The default (lazy) strategy: the file is opened with safetensors, and each tensor is read when the loop reaches its name.

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
In _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_data
apply() 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
                )
  1. Line 409. _groupby_prefix() splits each name at the first dot: layers.0.self_attn.qkv_proj.weight becomes layers and 0.self_attn.qkv_proj.weight.
  2. Lines 412-415. A child module: recurse. For QKVParallelLinear the recursion ends early, because that layer has its own load_weights (line 388).
  3. Lines 416-424. A parameter: _load_param() calls param.weight_loader, or default_weight_loader when 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)
Column-parallel: cut along 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)
Row-parallel: cut along 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)
  1. Line 191. For q, the slice index is the TP rank. For k and v it is tp_rank // num_kv_head_replicas; num_heads here carries the replica count (vllm/model_executor/layers/linear.py:1203-1209).
  2. 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).
  3. Lines 193-195. The part of the checkpoint tensor. Then an assertion and one copy.

Other parameters:

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
  1. Lines 489-496. The list has the same content as Llama's mapper, as tuples (param_name, shard_name, shard_id).
  2. 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 namesClassWhat it does
auto, hf, safetensors, pt, npcache, mistral, fastsafetensors, instanttensorDefaultModelLoaderThe path of this page; the name selects the file patterns and the iterator (vllm/model_executor/model_loader/default_loader.py:184-203).
dummyDummyModelLoaderRandom values, no files; for profiling (vllm/model_executor/model_loader/dummy_loader.py:21-48).
sharded_state, runai_streamer_shardedShardedStateLoaderEach 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).
tensorizerTensorizerLoaderCoreWeave tensorizer files (vllm/config/load.py:48-50).
runai_streamerRunaiModelStreamerLoaderSafetensors through the Run:ai Model Streamer (vllm/config/load.py:51-52).
modelexpressModelExpressModelLoaderWeights through ModelExpress (vllm/config/load.py:59).
ipc_cacheIpcModelLoaderMaps 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 dive

Worked 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 nameShapeAfter the mapperShard 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:

ShardParameter rows (both ranks)Checkpoint rows, rank 0Checkpoint rows, rank 1
q0-2047 (offset 0, size 2048)0-20472048-4095
k2048-2559 (offset 16×128, size 512)0-511512-1023
v2560-3071 (offset 20×128, size 512)0-511512-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 dive

AutoWeightsLoader.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
  1. Line 458. Unexpected .bias tensors are ignored; the comment says GPTQ checkpoints often have them.
  2. 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.
  3. Line 471. Old rotary tables such as rotary_emb.inv_freq are dropped (vllm/model_executor/models/utils.py:233-240).
  4. Lines 473-474. Both steps are generators: no tensor is read before the tree walk asks for it.
  5. Line 476. The walk returns the names of all loaded parameters. The default loader compares them with model.named_parameters() (next deep dive).
  6. Line 477. With tied embeddings (lm_head.weight is embed_tokens.weight), the loader loads the first name only. If that name is missing in the checkpoint, it raises ValueError (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 dive

Memory, time and the completeness check

Deep dive

Flags and their defaults

FlagDefaultDefined atEffect
--load-formatautovllm/config/load.py:30-61Loader class and file patterns.
--download-dirHugging Face cachevllm/config/load.py:62-64Where to download and read the files.
--safetensors-load-strategyNonevllm/config/load.py:65-86lazy, eager, prefetch, torchao; None = lazy with auto-prefetch on NFS/Lustre.
--safetensors-prefetch-num-threads8vllm/config/load.py:12, vllm/config/load.py:87-91Prefetch threads.
--safetensors-prefetch-block-size16 MiBvllm/config/load.py:13, vllm/config/load.py:92-95Read size per prefetch call.
--model-loader-extra-config{}vllm/config/load.py:96-98For 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-104Files not to download, so the extra Llama files in original/ are skipped.
--pt-load-map-locationcpuvllm/config/load.py:108-117map_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#

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#

  1. 1
    Which loader class serves the default --load-format auto, and which iterator does it use for a safetensors checkpoint?
    easy
    Show answer

    DefaultModelLoader (vllm/model_executor/model_loader/__init__.py:51). Without the multi-thread option it uses safetensors_weights_iterator (vllm/model_executor/model_loader/default_loader.py:337-349).

  2. 2
    How does QKVParallelLinear know that a tensor is the K part?
    medium
    Show answer

    Llama's mapper renames .k_proj to .qkv_proj with the shard id "k" (vllm/model_executor/models/llama.py:351-353). WeightsMapper.apply() stores it as tensor.shard_id (vllm/model_executor/models/utils.py:158-164), and QKVParallelLinear.load_weights reads it back (vllm/model_executor/layers/linear.py:1342-1354).

  3. 3
    For a BF16 model with TP = 4, which dimension of o_proj.weight does each rank cut, and at which offset?
    medium
    Show answer

    o_proj is row-parallel, so the loader cuts input_dim = 1 at tp_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.

  4. 4
    A model has 8 KV heads and runs with TP = 16. Which rows of k_proj.weight does rank 5 load? Why does rank 4 load the same rows?
    hard
    Show answer

    num_kv_heads is 1 and num_kv_head_replicas is 2 (vllm/model_executor/layers/linear.py:1066-1068). The slice index for k is tp_rank // num_heads, where num_heads carries 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.

  5. 5
    A 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_tensor for each name (vllm/model_executor/model_loader/weight_utils.py:1243-1249). The tree walk reaches PPMissingLayer objects for those layers and returns without a copy (vllm/model_executor/models/utils.py:382-383). Hand-written loaders use is_pp_missing_parameter() for the same skip (vllm/model_executor/models/minicpm.py:524-525).