How a vLLM model file is written (llama.py)
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
A vLLM model file builds the network from tensor-parallel layers, names every layer with a prefix, and leaves attention, compilation and weight loading to vLLM.
The idea#
A decoder model is a stack of layers: an embedding, N decoder layers and an LM head. Each decoder layer has a norm, attention, a norm and an MLP (Inside a transformer). vLLM does not run the Transformers class of the checkpoint. It runs its own class for the architecture name, for example LlamaForCausalLM in vllm/model_executor/models/llama.py (Out-of-tree models and the model registry).
The vLLM class has the same math and the same parameter tree as the Transformers class. It differs in four ways:
- Constructor contract. Every class takes
vllm_config(the whole VllmConfig) and a prefix (of a layer), the dotted name of the layer in the tree. - Tensor-parallel layers. Linear layers and the embedding are vLLM classes that hold only the slice of this tensor parallelism rank, and do the communication themselves (The tensor-parallel layers (linear.py and vocab_parallel_embedding.py)).
- An attention layer object. The model gives Q, K and V to an
Attentionlayer. That layer finds its KV cache and the metadata of the step itself, through the forward context (The forward context: how layers find per-step data). - Pipeline stages. Layers that another pipeline parallelism stage owns are placeholders, and a stage that is not the last returns IntermediateTensors instead of hidden states.
Other work is not in the model file. A decorator adds torch.compile; the compile backend and the model runner add CUDA graphs. A loader streams the weights in, and each parameter cuts its own slice ("Weight loading: from checkpoint files to GPU parameters"). Mixin classes tell the registry what the model supports (Model capability protocols (SupportsPP, SupportsLoRA, ...)).
An everyday comparison. Think of a new office building. The builders put a room number on every door, such as "3.12.B" (floor, corridor, room). Later, the furniture company, the phone list and the cleaning plan all use these numbers. A door with a wrong number gets the wrong desk or no phone. The comparison stops here. In vLLM, nobody checks the number on the door. Two systems read the number, and two systems walk the real module tree. When the number and the path are different, the systems disagree.
The picture#
Read the tree from the top. Each parent passes a longer prefix to its children, so the prefix of a row is the dotted path to it. Blue and violet rows are the tensor-parallel linear layers. The amber row is the attention layer that registers itself under its prefix.
In the code#
Step 1: the constructor contract#
The worker builds the model in initialize_model(). It checks by name that the constructor has the parameters vllm_config and prefix. Then it calls the class with both, inside set_current_vllm_config (vllm/model_executor/model_loader/utils.py:96-103). A class without them gets a DeprecationWarning and an argument guess; Out-of-tree models and the model registry explains that path. The root gets prefix="": Model Runner V2 calls load_model() without a prefix, so the default "" goes to create_model() (vllm/v1/worker/gpu/model_runner.py:403-406, vllm/model_executor/model_loader/base_loader.py:63-80).
def __init__(
self,
*,
vllm_config: VllmConfig,
prefix: str = "",
layer_type: type[nn.Module] = LlamaDecoderLayer,
):
super().__init__()
config = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
self.config = config
self.model = self._init_model(
vllm_config=vllm_config,
prefix=maybe_prefix(prefix, "model"),
layer_type=layer_type,
)
if get_pp_group().is_last_rank:
self.lm_head = ParallelLMHead(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "lm_head"),
)
if config.tie_word_embeddings:
self.lm_head = self.lm_head.tie_weights(self.model.embed_tokens)
logit_scale = getattr(config, "logit_scale", 1.0)
self.logits_processor = LogitsProcessor(
config.vocab_size, scale=logit_scale
)
else:
self.lm_head = PPMissingLayer()
self.make_empty_intermediate_tensors = (
self.model.make_empty_intermediate_tensors
)- Lines 487-488. Keyword-only
vllm_configandprefix. The registry also checks the keywordvllm_configwhen it inspects the class (Model capability protocols (SupportsPP, SupportsLoRA, ...)). - Lines 492-493. The model reads what it needs from
vllm_config: the Hugging Face config and the quantization config. Inner classes do the same. - Lines 496-500. The inner model gets the prefix
maybe_prefix(prefix, "model"), which is"model"for the root (vllm/model_executor/models/utils.py:971-982). - Lines 502-508. Only the last pipeline stage builds the LM head.
- Lines 519-521. The model exposes a factory for empty intermediate tensors; pipeline parallelism needs it.
Step 2: how each layer gets its prefix, and why it matters#
Each constructor adds the attribute name to the prefix it got. The decoder layer gives f"{prefix}.self_attn" and f"{prefix}.mlp" (vllm/model_executor/models/llama.py:285-307). The attention class gives .qkv_proj, .o_proj and .attn (vllm/model_executor/models/llama.py:165-181, vllm/model_executor/models/llama.py:212-222). make_layers() builds the list of decoder layers with prefix=f"{prefix}.{idx}" (vllm/model_executor/models/utils.py:911-917). So the QKV layer of layer 0 gets the prefix model.layers.0.self_attn.qkv_proj.
Four parts of vLLM use the name of a layer:
| Who | Reads | What it does with it |
|---|---|---|
| Quantization config | the prefix argument | get_quant_method(layer, prefix), where the prefix is "the full name of the layer in the state dict" (vllm/model_executor/layers/quantization/base_config.py:203-217). Each linear layer calls it in its constructor (vllm/model_executor/layers/linear.py:290-296). FP8, for example, leaves a layer unquantized when the prefix is in ignored_layers (vllm/model_executor/layers/quantization/fp8.py:177-187). The default match is an exact string compare (vllm/model_executor/layers/quantization/utils/quant_utils.py:624-625). |
| Attention layer | the prefix argument | Registers itself as static_forward_context[prefix]; a second layer with the same prefix raises ValueError (vllm/model_executor/layers/attention/attention.py:443-445). The model runner binds each KV cache tensor by this name (vllm/v1/worker/utils.py:879-880). The layer index for sliding windows comes from the prefix too (vllm/model_executor/models/llama.py:141). |
| Weight loading | the module tree | AutoWeightsLoader matches checkpoint names to named_children() and named_parameters(), not to the prefix (vllm/model_executor/models/utils.py:402-424). |
| LoRA | the module tree | The LoRA manager walks model.named_modules() to find the layers to wrap (vllm/lora/model_manager.py:486). |
No code checks that the prefix equals the attribute path of the layer. But the quantization config compares the prefix with checkpoint names, and the weight loader and LoRA use the module tree. So use the attribute path as the prefix; then all four parts use the same name (derived from the four rows above). The section What can go wrong gives the failures.
Step 3: tensor-parallel layers#
LlamaAttention divides the heads by the TP size and builds a QKVParallelLinear and a RowParallelLinear:
tp_size = get_tensor_model_parallel_world_size()
self.total_num_heads = num_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= tp_size:
# Number of KV heads is greater than TP size, so we partition
# the KV heads across multiple tensor parallel GPUs.
assert self.total_num_kv_heads % tp_size == 0
else:
# Number of KV heads is less than TP size, so we replicate
# the KV heads across multiple tensor parallel GPUs.
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
head_dim = getattr(config, "head_dim", None)
self.head_dim = head_dim or self.hidden_size // self.total_num_heads
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.max_position_embeddings = max_position_embeddings
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=self.total_num_heads,
total_num_kv_heads=self.total_num_kv_heads,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = RowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=hidden_size,
bias=bias_o_proj,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)- Lines 145-146. The query heads must divide by the TP size. Start-up checks this earlier, with a clear error (
vllm/config/model.py:1557-1564). - Lines 148-156. KV heads are split when there are enough of them, and replicated when TP is larger (Tensor and pipeline parallelism in practice has the table).
- Lines 160-161.
q_sizeandkv_sizeare per rank. The forward pass splits the QKV output with them. - Lines 165-181. The model gives the total head counts to
QKVParallelLinearand the full input size too_proj. The layers divide by the TP size themselves.
LlamaMLP uses a MergedColumnParallelLinear for gate and up, and a RowParallelLinear for down (vllm/model_executor/models/llama.py:95-111); Tensor and pipeline parallelism in practice shows the shapes. The embedding is a VocabParallelEmbedding and the head is a ParallelLMHead (vllm/model_executor/models/llama.py:382-386, vllm/model_executor/models/llama.py:503-508). The model code calls no collective itself. The page The tensor-parallel layers (linear.py and vocab_parallel_embedding.py) explains each layer class.
Step 4: the attention layer and the CustomOps#
attn_cls = (
EncoderOnlyAttention
if attn_type == AttentionType.ENCODER_ONLY
else Attention
)
self.attn = attn_cls(
self.num_heads,
self.head_dim,
self.scaling,
num_kv_heads=self.num_kv_heads,
cache_config=cache_config,
quant_config=quant_config,
per_layer_sliding_window=sliding_window,
attn_type=attn_type,
prefix=f"{prefix}.attn",
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = self.rotary_emb(positions, q, k)
attn_output = self.attn(q, k, v)
output, _ = self.o_proj(attn_output)
return output- Lines 212-222.
Attentiongets the per-rank head counts, the scale, the cache and quantization configs, an optional sliding window, and its prefix. When it is built, it selects an attention backend, unless the caller gives one (vllm/model_executor/layers/attention/attention.py:349-350). It also registers itself in the forward context. - Line 229. One GEMM computes Q, K and V for the heads of this rank:
[T, q_size + 2 × kv_size]. - Lines 230-231. The split, then RoPE on Q and K.
- Line 232.
self.attn(q, k, v)passes no KV cache and no metadata. The attention op uses the layer name to find the metadata and the layer object (with its KV cache) in the forward context (vllm/model_executor/layers/attention/attention.py:705-717). The page The forward context: how layers find per-step data explains how. - Line 233. The row-parallel
o_projadds the partial sums of the ranks.
Three small layers are CustomOps: RMSNorm (vllm/model_executor/layers/layernorm.py:38-39), the rotary embedding from get_rope() (vllm/model_executor/layers/rotary_embedding/base.py:14-15) and SiluAndMul (vllm/model_executor/layers/activation.py:115-116). Each one picks its forward method when it is built (CustomOp, out-of-tree ops and IR op providers). get_rope() keeps a cache keyed by its arguments, so all 32 layers share one rotary object (vllm/model_executor/layers/rotary_embedding/__init__.py:84-85, vllm/model_executor/layers/rotary_embedding/__init__.py:351).
Step 5: LlamaModel, compilation and pipeline stages#
@support_torch_compile(
# TODO[#32068]: Investigate recompilation
# mark_unbacked_dims={"input_ids": 0},
dynamic_arg_dims={
"input_ids": {0: "b"},
"positions": {0: "b"},
"intermediate_tensors": {0: "b"},
"inputs_embeds": {0: "b"},
},
)@support_torch_compile on the inner model. Dimension 0 of each tensor argument (the token count) is dynamic.The decorator wraps LlamaModel.forward for torch.compile and CUDA graphs (torch.compile and CUDA graphs in vLLM). It sits on LlamaModel, not on LlamaForCausalLM, so compute_logits stays outside the compiled graph.
The constructor builds the parts of this pipeline stage only:
if (
get_pp_group().is_first_rank
or (config.tie_word_embeddings and get_pp_group().is_last_rank)
or spec_decode_needs_target_embed(vllm_config)
):
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
config.hidden_size,
quant_config=quant_config,
)
else:
self.embed_tokens = PPMissingLayer()
self.start_layer, self.end_layer, self.layers = make_layers(
config.num_hidden_layers,
lambda prefix: layer_type(vllm_config=vllm_config, prefix=prefix),
prefix=f"{prefix}.layers",
)
if get_pp_group().is_last_rank:
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
else:
self.norm = PPMissingLayer()
self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
["hidden_states", "residual"], config.hidden_size
)- Lines 377-388. The embedding is real on the first stage. It is also real on the last stage when the head shares its weight, or when a speculative drafter needs it. Elsewhere it is a PPMissingLayer.
- Lines 389-393.
make_layers()builds real layers only for indicesstart_layertoend_layer - 1. All other slots arePPMissingLayer, so the indices and names stay the same on every stage (vllm/model_executor/models/utils.py:886-919). - Lines 394-397. The final norm exists on the last stage only.
- Lines 399-401. Empty intermediate tensors have the keys
hidden_statesandresidual, each[batch_size, hidden_size]of zeros (vllm/model_executor/models/utils.py:955-968).
def forward(
self,
input_ids: torch.Tensor | None,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None,
inputs_embeds: torch.Tensor | None = None,
**extra_layer_kwargs,
) -> torch.Tensor | IntermediateTensors | tuple[torch.Tensor, list[torch.Tensor]]:
if get_pp_group().is_first_rank:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_input_ids(input_ids)
residual = None
else:
assert intermediate_tensors is not None
hidden_states = intermediate_tensors["hidden_states"]
residual = intermediate_tensors["residual"]
remote_aux = self.collect_remote_aux_hidden_states(intermediate_tensors)
aux_hidden_states: list[torch.Tensor] = []
if get_pp_group().is_first_rank:
self._maybe_add_hidden_state(
aux_hidden_states, self.start_layer, hidden_states, residual
)
for idx, layer in enumerate(
islice(self.layers, self.start_layer, self.end_layer),
start=self.start_layer,
):
hidden_states, residual = layer(
positions, hidden_states, residual, **extra_layer_kwargs
)
self._maybe_add_hidden_state(
aux_hidden_states, idx + 1, hidden_states, residual
)
if not get_pp_group().is_last_rank:
return IntermediateTensors(
{
"hidden_states": hidden_states,
"residual": residual,
**self.pack_local_aux_hidden_states(aux_hidden_states),
}
)
hidden_states, _ = self.norm(hidden_states, residual)
aux_hidden_states = remote_aux + aux_hidden_states
if len(aux_hidden_states) > 0:
return hidden_states, aux_hidden_states
return hidden_states- Lines 414-423. The first stage starts from the token embeddings, or from
inputs_embedswhen the runner gives them. Other stages readhidden_statesandresidualfrom the stage before. - Lines 432-438. The loop runs only the layers of this stage. Each layer returns the new hidden states and the residual.
- Lines 443-450. A stage that is not the last returns
IntermediateTensors. - Line 452. The last stage applies the final norm. It returns hidden states of shape
[T, hidden_size], not logits.
The decoder layer keeps the residual outside the norm. input_layernorm(hidden_states, residual) adds the residual and normalizes in one call, and returns both tensors (vllm/model_executor/models/llama.py:320-328, vllm/model_executor/layers/layernorm.py:85-99).
Step 6: LlamaForCausalLM: forward, logits and mixins#
def forward(
self,
input_ids: torch.Tensor | None,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor | IntermediateTensors:
model_output = self.model(
input_ids, positions, intermediate_tensors, inputs_embeds
)
return model_output
def compute_logits(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor | None:
logits = self.logits_processor(self.lm_head, hidden_states)
return logits
def compute_logits_local(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor:
return self.logits_processor(self.lm_head, hidden_states, skip_gather=True)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
return loader.load_weights(weights)- Lines 541-543.
forward(input_ids, positions, intermediate_tensors, inputs_embeds)only calls the inner model. The model runner builds these four arguments (vllm/v1/worker/gpu/model_runner.py:1984-2009). - Line 550.
compute_logitsapplies the head throughLogitsProcessor. To sample, the runner calls it only on the rows that sample a token,hidden_states[logits_indices](vllm/v1/worker/gpu/model_runner.py:1637-1638). Prompt logprobs call it on prompt rows too (vllm/v1/worker/gpu/model_runner.py:2177-2178). - Line 557.
compute_logits_localskips the gather. The runner uses it for batch-sharded sampling. - Lines 560-561.
load_weightshands the checkpoint stream toAutoWeightsLoader("Weight loading: from checkpoint files to GPU parameters").
The class header lists the mixins (vllm/model_executor/models/llama.py:464-472):
| Mixin | What it adds |
|---|---|
SupportsLoRA | LoRA support; with packed_modules_mapping and embedding_modules (vllm/model_executor/models/llama.py:474-482) |
SupportsPP | pipeline parallelism; the factory of lines 519-521 is its required member |
SupportsEagle, SupportsEagle3 | the model can be the target of EAGLE drafts; EAGLE-3 reads auxiliary hidden states (vllm/model_executor/models/interfaces.py:1684-1740) |
SupportsQuant | its __new__ stores the quant config on the model and gives the config the rename rules of hf_to_vllm_mapper and the packed_modules_mapping (vllm/model_executor/models/interfaces.py:1260-1299). For Llama the rename part is empty, because get_rename_mapper() drops the stacked rules (vllm/model_executor/models/utils.py:180-199). |
LocalArgmaxMixin | get_top_tokens for a speculative-decoding draft: each rank takes the argmax of its vocabulary slice, and the ranks all-gather only (value, index) pairs, not the full logits (vllm/model_executor/models/interfaces.py:1570-1605, vllm/model_executor/layers/logits_processor.py:231-250) |
LlamaModel also inherits EagleModelMixin, which collects the hidden states of chosen layers for EAGLE-3 (vllm/model_executor/models/interfaces.py:1608-1681). The page Model capability protocols (SupportsPP, SupportsLoRA, ...) explains how the registry detects each protocol.
Step 7: how other files reuse llama.py#
Nine registry names point to llama.py. Seven of them use LlamaForCausalLM directly, for example InternLM3ForCausalLM and TeleChat3ForCausalLM (vllm/model_executor/models/registry.py:140-155, vllm/model_executor/models/registry.py:212). The embedding table also copies every name that maps to LlamaForCausalLM (vllm/model_executor/models/registry.py:231-238). (Counts from an evaluation of the tables at this commit.)
Other model files subclass the Llama classes. MistralForCausalLM subclasses LlamaForCausalLM (vllm/model_executor/models/mistral.py:236), and so does Phi3ForCausalLM (vllm/model_executor/models/phi3.py:10). TeleChat2ForCausalLM adds its own name mapper and builds TeleChat2Model, a subclass of LlamaModel (vllm/model_executor/models/telechat2.py:107-127, vllm/model_executor/models/telechat2.py:35). 21 files in vllm/ import from llama.py (git grep at this commit). The file has two hooks for this reuse:
- A
layer_typeargument, so a subclass can give its own decoder layer class (vllm/model_executor/models/llama.py:360-366,vllm/model_executor/models/llama.py:523-529). LlamaDecoderLayer.get_quant_config(), which the docstring says to override in subclasses (vllm/model_executor/models/llama.py:332-334).
A second model tree: vllm/models/#
Some new model families do not live in vllm/model_executor/models/. They have one package per family in vllm/models/, with sub-packages per hardware vendor. The package __init__.py imports the implementation for current_platform, for example the NVIDIA, AMD, XPU or CPU variant of DeepSeek V4 (vllm/models/deepseek_v4/__init__.py:1-48). For the registry they are normal lazy entries (Out-of-tree models and the model registry). This site does not explain these packages further; llama.py is the one model example.
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: shapes through one step, Llama-3-8B with TP = 2
The numbers come from the Llama-3-8B config.json on the Hugging Face Hub, not from the vLLM repository. Hidden size 4,096, intermediate size 14,336, 32 layers. 32 query heads, 8 KV heads, head size 128, vocabulary 128,256. Take a step with T = 2,048 tokens, of which R = 16 sample a token. All shapes are per rank.
| Step | Code | Shape per rank |
|---|---|---|
| input ids, positions | forward(input_ids, positions, ...) | [2048] each (one flat list of tokens) |
| embedding (after its all-reduce) | llama.py:418 | [2048, 4096] |
qkv_proj output | llama.py:229 | [2048, 2048 + 512 + 512] = [2048, 3072] |
| split | llama.py:230 | q [2048, 2048], k [2048, 512], v [2048, 512] |
| attention output | attention.py:529 | [2048, 16 × 128] = [2048, 2048] |
o_proj (after all-reduce) | llama.py:233 | [2048, 4096] |
gate_up_proj | llama.py:119 | [2048, 2 × 7168] |
SiluAndMul | llama.py:120 | [2048, 7168] |
down_proj (after all-reduce) | llama.py:121 | [2048, 4096] |
| final norm | llama.py:452 | [2048, 4096] |
| rows that sample | model_runner.py:1637 | [16, 4096] |
| logits on this rank | logits_processor.py:188 | [16, 64128] |
| logits after the gather | logits_processor.py:193-198 | [16, 128256] |
Each decoder layer does two all-reduces of [2048, 4096] BF16 values: one in o_proj and one in down_proj. The embedding adds one more all-reduce per step (vllm/model_executor/layers/vocab_parallel_embedding.py:558-563). So a step has 2 × 32 + 1 = 65 all-reduces of 16 MiB each (derived). The logits add one all-gather of [16, 64128] per rank. Batch-sharded sampling, which is off by default, changes this last step (vllm/config/parallel.py:192-198).
Deep diveAll prefixes of one decoder layer
The root prefix is "". These are the prefixes that the constructors build for layer 0 (derived by following the f-strings of llama.py):
| Module | Prefix | Built at |
|---|---|---|
LlamaModel | model | vllm/model_executor/models/llama.py:498 |
LlamaDecoderLayer | model.layers.0 | vllm/model_executor/models/llama.py:389-393, vllm/model_executor/models/utils.py:914 |
LlamaAttention | model.layers.0.self_attn | vllm/model_executor/models/llama.py:297 |
QKVParallelLinear | model.layers.0.self_attn.qkv_proj | vllm/model_executor/models/llama.py:172 |
RowParallelLinear (o) | model.layers.0.self_attn.o_proj | vllm/model_executor/models/llama.py:180 |
Attention | model.layers.0.self_attn.attn | vllm/model_executor/models/llama.py:221 |
LlamaMLP | model.layers.0.mlp | vllm/model_executor/models/llama.py:306 |
MergedColumnParallelLinear | model.layers.0.mlp.gate_up_proj | vllm/model_executor/models/llama.py:101 |
RowParallelLinear (down) | model.layers.0.mlp.down_proj | vllm/model_executor/models/llama.py:110 |
ParallelLMHead | lm_head | vllm/model_executor/models/llama.py:507 |
VocabParallelEmbedding | none passed (default "") | vllm/model_executor/models/llama.py:382-386, vllm/model_executor/layers/vocab_parallel_embedding.py:260 |
The Llama embedding gets the prefix "". So a name in a quantization config, such as model.embed_tokens, cannot match it: an exact or suffix compare with "" fails (vllm/model_executor/layers/quantization/utils/quant_utils.py:624-637). compressed-tensors can still match it by its class name, for example with the target Embedding (vllm/model_executor/layers/quantization/compressed_tensors/utils.py:105-109, vllm/model_executor/layers/quantization/utils/config_utils.py:444-445). (Derived.)
extract_layer_index() finds the one integer in a prefix: model.layers.0.self_attn gives 0. A prefix with two integers fails its assertion, unless the caller says that a layer has several attention modules (vllm/model_executor/models/utils.py:1010-1042).
Deep divePipeline parallelism in the model file, step by step
Take 32 layers and PP = 2. get_pp_indices() gives stage 0 the layers 0-15 and stage 1 the layers 16-31 (Tensor and pipeline parallelism in practice explains the split rule).
- Build, stage 0.
embed_tokensis real.layers[0:16]are real;layers[16:32]arePPMissingLayer.normandlm_headarePPMissingLayer, and there is nologits_processor(vllm/model_executor/models/llama.py:502-517). - Build, stage 1.
embed_tokensisPPMissingLayerunless the head shares its weight or a speculative drafter needs it.layers[0:16]are placeholders;norm,lm_headandlogits_processorare real. - Forward, stage 0. Embed, run layers 0-15, return
IntermediateTensors({"hidden_states", "residual"}), each[T, 4096]. - Forward, stage 1. The runner fills its own buffers from the received tensors and passes them as
intermediate_tensors(vllm/v1/worker/gpu/model_runner.py:1993-2009). The model reads them (llama.py lines 421-423), runs layers 16-31, then the final norm. - Weights.
AutoWeightsLoaderskips everyPPMissingLayer, so the checkpoint tensors of other stages are read but not stored (vllm/model_executor/models/utils.py:382-383). Hand-written loaders callis_pp_missing_parameter()instead (vllm/model_executor/models/utils.py:944-952).
PPMissingLayer is a torch.nn.Identity whose forward returns its first argument (vllm/model_executor/models/utils.py:852-860). IntermediateTensors is a thin wrapper around a dict of tensors, with indexing by key or by slice (vllm/sequence.py:12-34). With EAGLE-3 and PP, the dict also carries keys aux_hidden_states_<i> (vllm/model_executor/models/interfaces.py:1652-1661).
Deep diveA checklist for your own model file (derived from llama.py)
- Constructors take
vllm_configandprefix; children getf"{prefix}.{attr}"ormaybe_prefix(prefix, attr). - Linear layers are
QKVParallelLinear,MergedColumnParallelLinear,RowParallelLinearorReplicatedLinear, each withquant_configandprefix. - Attention is an
Attentionlayer with the per-rank head counts and a unique prefix. - The inner model has
embed_input_idsandforward(input_ids, positions, intermediate_tensors, inputs_embeds); the outer class hascompute_logits. The registry checks these names (Model capability protocols (SupportsPP, SupportsLoRA, ...)). - For PP: build layers with
make_layers, usePPMissingLayer, returnIntermediateTensors, setmake_empty_intermediate_tensors, and inheritSupportsPP. - For loading: a
load_weightswithAutoWeightsLoaderand aWeightsMapperfor stacked parameters ("Weight loading: from checkpoint files to GPU parameters"). - For LoRA:
packed_modules_mappingthat lists the parts of each stacked layer.
What can go wrong#
- Two attention layers with the same prefix. The second one raises
ValueError: Duplicate layer namewhile the model is built (vllm/model_executor/layers/attention/attention.py:443-444). A model that builds its layers in a loop must put the index in the prefix. - A prefix that is not the real path. No error occurs at once. The quantization config compares the wrong name with its lists, so a layer can get the wrong method. For example, it can get FP8 although the checkpoint lists it in
ignored_layers(vllm/model_executor/layers/quantization/fp8.py:180-190). An attention prefix without exactly one layer number fails the assertion ofextract_layer_index()(vllm/model_executor/models/llama.py:141,vllm/model_executor/models/utils.py:1027-1030). - Heads that do not divide. 32 query heads with TP = 3 stop start-up with "must be divisible by tensor parallel size" (
vllm/config/model.py:1559-1564). - Another activation.
LlamaMLPaccepts onlysiluand raisesValueErrorfor other values ofhidden_act(vllm/model_executor/models/llama.py:112-115). - PP on a model without the mixin. Start-up raises
NotImplementedErrorwhen the class does not implementSupportsPP(vllm/config/model.py:1569-1576).
Why it matters. A new architecture, a fork of a model, or an out-of-tree model plugin starts with a file like llama.py. Learn the contract, the prefixes and the TP layer classes. Then you can read any model file in vLLM. You can also write a new one that works with quantization, LoRA, PP and torch.compile.
Check yourself#
- 1Which two constructor parameters must every vLLM model class have, and who checks them?easy
Show answer
vllm_configandprefix.initialize_model()checks the constructor signature by name; without both, it warns and guesses the arguments (vllm/model_executor/model_loader/utils.py:96-112). - 2What prefix does the QKV layer of decoder layer 5 get, and which component would break if the prefix weremedium
"qkv"only?Show answer
model.layers.5.self_attn.qkv_proj(llama.py lines 172, 297, 498 andmake_layers). With"qkv", the quantization config would look up the wrong name, for example in FP8'signored_layerscheck (vllm/model_executor/layers/quantization/fp8.py:177-187). Weight loading would still work, becauseAutoWeightsLoaderuses the module tree. - 3Why doesmedium
LlamaAttention.forwardnot pass the KV cache or the attention metadata toself.attn?Show answer
The
Attentionlayer registered itself under its prefix instatic_forward_context(vllm/model_executor/layers/attention/attention.py:443-445). The runner binds the KV cache by that name. It also sets the metadata in the forward context before each step, so the attention op finds both by the layer name (vllm/model_executor/layers/attention/attention.py:705-717). - 4A Llama with 32 layers runs with PP = 2 and TP = 2. How many all-reduces does the first stage do per step, and what does it return?hard
Show answer
Stage 0 runs layers 0-15, each with two all-reduces (
o_proj,down_proj), plus one in the embedding: 33 (derived fromvllm/model_executor/layers/linear.py:1803-1806andvllm/model_executor/layers/vocab_parallel_embedding.py:558-563). It returnsIntermediateTensorswithhidden_statesandresidual, each[T, 4096](vllm/model_executor/models/llama.py:443-450). - 5Why ishard
@support_torch_compileonLlamaModeland not onLlamaForCausalLM? Name one consequence.Show answer
The decorator wraps the
forwardof the class it decorates (vllm/model_executor/models/llama.py:337-347).compute_logitsis a method ofLlamaForCausalLM, so the runner calls it outside the compiled graph ofLlamaModel. To sample, it passes only the rows inlogits_indices(vllm/v1/worker/gpu/model_runner.py:1637-1638).