Coverage for transformer_lens/model_bridge/sources/vllm/overlays/decoder_only.py: 100%
15 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""Generic overlay for any decoder-only model vLLM supports.
3vLLM's decoder-only models all share the same internal structure:
4``model.embed_tokens`` / ``model.layers.{i}`` (each with ``self_attn`` and
5``mlp`` submodules) / ``model.norm`` / ``lm_head``. This overlay hooks that
6shared abstraction, so one file works for Llama, Qwen, Mistral, Gemma, Phi3,
7Qwen3, Kimi, GLM, and every other model that inherits the standard shape.
9Non-decoder-only architectures (Mamba SSM, T5 encoder-decoder, BERT, MoE
10per-expert) break the convention and would need their own overlays.
12Two hooks capture different points than HF/HookedTransformer:
14- ``blocks.{i}.hook_out``: vLLM's layer returns ``(mlp_delta, residual)``
15 separately (fused-residual). The plugin's hook materializes the sum so the
16 captured value matches HF's "post-MLP residual stream".
17- ``ln_final.hook_normalized``: vLLM exposes ``x * rsqrt(var+eps) * weight``;
18 HF/HT exposes the pre-weight value. The driver un-folds the user-facing
19 capture (÷ weight, or ÷ (1 + weight) for Gemma) so the cache matches
20 ``boot_transformers``; logit reconstruction consumes the raw post-weight
21 value internally. If the norm weight is unreachable the driver warns and
22 serves the raw post-weight value.
23"""
24from __future__ import annotations
26from typing import Any, Dict, List, Tuple
28from .base import AdapterOverlay
31class DecoderOnlyOverlay(AdapterOverlay):
32 """Default overlay for vLLM decoder-only models."""
34 def capture_specs(self, hf_config: Any) -> Dict[str, Tuple[str, int]]:
35 d_model = hf_config.hidden_size
36 n_layers = hf_config.num_hidden_layers
37 # unembed.hook_out is intentionally NOT captured here — vLLM's sampler
38 # computes logits via a direct matmul on the final hidden state and
39 # never invokes lm_head.__call__, so register_forward_hook on lm_head
40 # would install but never fire. See nonfiring_hooks() below; the
41 # driver synthesizes the next-token logits from vLLM's sampler output.
42 specs: Dict[str, Tuple[str, int]] = {
43 "embed.hook_out": ("model.embed_tokens", d_model),
44 "ln_final.hook_normalized": ("model.norm", d_model),
45 }
46 for i in range(n_layers):
47 specs[f"blocks.{i}.hook_out"] = (f"model.layers.{i}", d_model)
48 specs[f"blocks.{i}.attn.hook_out"] = (f"model.layers.{i}.self_attn", d_model)
49 specs[f"blocks.{i}.mlp.hook_out"] = (f"model.layers.{i}.mlp", d_model)
50 return specs
52 def nonfiring_hooks(self) -> List[str]:
53 # vLLM's universal fused-kernel limitations — PagedAttention fuses the
54 # QK^T → softmax → attn-weight path; QKVParallelLinear fuses RoPE inside
55 # the projection. Same restriction on every decoder-only model.
56 return [
57 "blocks.{i}.attn.hook_pattern",
58 "blocks.{i}.attn.hook_attn_scores",
59 "blocks.{i}.attn.hook_rot_q",
60 "blocks.{i}.attn.hook_rot_k",
61 # Head-split projections live inside the fused QKVParallelLinear /
62 # RowParallelLinear kernels — unobservable per-head.
63 "blocks.{i}.attn.hook_q",
64 "blocks.{i}.attn.hook_k",
65 "blocks.{i}.attn.hook_v",
66 "blocks.{i}.attn.hook_z",
67 # Block/MLP inputs: vLLM passes (hidden, residual) fused between layers,
68 # so the HF-convention pre-block/pre-MLP stream is never materialized.
69 "blocks.{i}.hook_in",
70 "blocks.{i}.mlp.hook_in",
71 "blocks.{i}.attn.hook_in",
72 # vLLM's sampler bypasses lm_head.__call__ — capture-via-forward-hook
73 # never fires; driver synthesizes argmax-matching logits from the
74 # sampler's returned token.
75 "unembed.hook_out",
76 ]