Coverage for transformer_lens/utilities/tl_checkpoint_conversion.py: 100%
49 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"""One-time converter for legacy TL-property-format checkpoints (#1588).
3Rotary-model checkpoints are unsupported: their ``rotary_sin``/``rotary_cos``
4buffer keys fail loudly as unrecognized keys (drop them first if you need to
5convert one — the bridge recomputes rotary embeddings from the config).
7Historical training runs (OthelloGPT, grokking demos, ARENA content) were
8saved via ``HookedTransformer.state_dict()`` before ``TransformerBridge``
9existed, using property-style keys ("blocks.0.attn.W_Q", "embed.W_E", ...)
10and per-head tensor shapes. ``convert_tl_checkpoint`` maps those onto the
11key/tensor format ``TransformerBridge.boot_native(cfg).load_state_dict``
12accepts natively, so these checkpoints can be converted once and re-saved in
13bridge format. This is deliberately a standalone converter rather than a
14second key convention taught to ``load_state_dict`` itself: convert once,
15``bridge.load_state_dict(converted)``, then re-save with ``bridge.state_dict()``.
16"""
18from __future__ import annotations
20from typing import Callable, Optional
22import einops
23import torch
25from transformer_lens.config.transformer_bridge_config import TransformerBridgeConfig
27# Buffers that live on HookedTransformer's attention blocks but have no
28# Parameter counterpart on the bridge side (causal mask, IGNORE sentinel).
29_DROPPED_BUFFER_SUFFIXES = (".mask", ".IGNORE")
31TensorConvert = Callable[[torch.Tensor, TransformerBridgeConfig, str], torch.Tensor]
34def _validate_shape(tensor: torch.Tensor, expected: tuple[int, ...], key: str) -> None:
35 # Merging/splitting per-head dims (unlike a plain transpose) produces a
36 # validly-shaped result for *any* head count, since d_model == n_heads *
37 # d_head for any factoring of it — a wrong cfg silently mis-groups heads
38 # without ever tripping a downstream shape-mismatch error. Check the
39 # untouched per-head shape explicitly before reshaping.
40 if tuple(tensor.shape) != expected:
41 raise ValueError(
42 f"convert_tl_checkpoint: {key!r} has shape {tuple(tensor.shape)}, "
43 f"expected {expected} for the given cfg. The checkpoint may not "
44 "match this cfg (n_heads/n_key_value_heads/d_head/d_model)."
45 )
48def _kv_heads(cfg: TransformerBridgeConfig) -> int:
49 return cfg.n_key_value_heads or cfg.n_heads
52def _convert_w_q(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
53 _validate_shape(t, (cfg.n_heads, cfg.d_model, cfg.d_head), key)
54 return einops.rearrange(t, "n_heads d_model d_head -> (n_heads d_head) d_model")
57def _convert_w_kv(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
58 _validate_shape(t, (_kv_heads(cfg), cfg.d_model, cfg.d_head), key)
59 return einops.rearrange(t, "n_heads d_model d_head -> (n_heads d_head) d_model")
62def _convert_w_o(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
63 _validate_shape(t, (cfg.n_heads, cfg.d_head, cfg.d_model), key)
64 return einops.rearrange(t, "n_heads d_head d_model -> d_model (n_heads d_head)")
67def _convert_b_q(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
68 _validate_shape(t, (cfg.n_heads, cfg.d_head), key)
69 return einops.rearrange(t, "n_heads d_head -> (n_heads d_head)")
72def _convert_b_kv(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
73 _validate_shape(t, (_kv_heads(cfg), cfg.d_head), key)
74 return einops.rearrange(t, "n_heads d_head -> (n_heads d_head)")
77def _identity(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
78 return t
81def _transpose(t: torch.Tensor, cfg: TransformerBridgeConfig, key: str) -> torch.Tensor:
82 return t.T.contiguous()
85# Old TL-property suffix -> (new bridge-key suffix, tensor conversion).
86# Checked in order, longest/most-specific first, so e.g. ".b_Q" is matched
87# before the generic ".b" LayerNorm-bias fallback.
88_SUFFIX_CONVERSIONS: list[tuple[str, str, TensorConvert]] = [
89 (".W_Q", ".q.weight", _convert_w_q),
90 # GQA stores K/V under a leading-underscore name (the raw Parameter);
91 # plain ".W_K"/".W_V" become expanding (non-Parameter) properties instead.
92 ("._W_K", ".k.weight", _convert_w_kv),
93 ("._W_V", ".v.weight", _convert_w_kv),
94 (".W_K", ".k.weight", _convert_w_kv),
95 (".W_V", ".v.weight", _convert_w_kv),
96 (".W_O", ".o.weight", _convert_w_o),
97 (".b_Q", ".q.bias", _convert_b_q),
98 ("._b_K", ".k.bias", _convert_b_kv),
99 ("._b_V", ".v.bias", _convert_b_kv),
100 (".b_K", ".k.bias", _convert_b_kv),
101 (".b_V", ".v.bias", _convert_b_kv),
102 (".b_O", ".o.bias", _identity),
103 (".W_in", ".in.weight", _transpose),
104 (".b_in", ".in.bias", _identity),
105 (".W_out", ".out.weight", _transpose),
106 (".b_out", ".out.bias", _identity),
107 (".W_gate", ".gate.weight", _transpose),
108 (".b_gate", ".gate.bias", _identity),
109 (".W_U", ".weight", _transpose),
110 (".b_U", ".bias", _identity),
111 (".W_E", ".weight", _identity),
112 (".W_pos", ".weight", _identity),
113 (".w", ".weight", _identity),
114 (".b", ".bias", _identity),
115]
118def _convert_key_and_tensor(
119 key: str, tensor: torch.Tensor, cfg: TransformerBridgeConfig
120) -> Optional[tuple[str, torch.Tensor]]:
121 for old_suffix, new_suffix, convert in _SUFFIX_CONVERSIONS:
122 if key.endswith(old_suffix):
123 new_key = key[: -len(old_suffix)] + new_suffix
124 return new_key, convert(tensor, cfg, key)
125 return None
128def convert_tl_checkpoint(
129 state_dict: dict[str, torch.Tensor],
130 cfg: TransformerBridgeConfig,
131) -> dict[str, torch.Tensor]:
132 """Convert a legacy TL-property-format state dict to the key/tensor
133 format ``TransformerBridge.boot_native(cfg).load_state_dict`` accepts.
135 Args:
136 state_dict: A state dict in the old ``HookedTransformer`` convention
137 (e.g. from ``HookedTransformer.state_dict()``), with keys like
138 ``"blocks.0.attn.W_Q"`` and per-head tensor shapes.
139 cfg: The config the checkpoint was trained/saved under. Used both to
140 reshape per-head attention weights and to validate that the
141 checkpoint's per-head shapes actually match this cfg — a
142 mismatched cfg would otherwise silently mis-group heads without
143 ever tripping a shape error, since d_model == n_heads * d_head
144 holds for any wrong factoring too.
146 Returns:
147 A state dict with modern bridge keys (e.g. ``"blocks.0.attn.q.weight"``)
148 and flat ``nn.Linear``-oriented tensor shapes, ready for
149 ``bridge.load_state_dict(converted, strict=True)``.
150 """
151 converted: dict[str, torch.Tensor] = {}
152 for key, tensor in state_dict.items():
153 if key.endswith(_DROPPED_BUFFER_SUFFIXES):
154 continue
155 result = _convert_key_and_tensor(key, tensor, cfg)
156 if result is None:
157 raise ValueError(
158 f"convert_tl_checkpoint: don't know how to convert key {key!r} "
159 "(not a recognized TL-property parameter or buffer suffix)."
160 )
161 new_key, new_tensor = result
162 converted[new_key] = new_tensor
163 return converted