Coverage for transformer_lens/model_bridge/sources/tl_legacy.py: 53%

106 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""Loader for legacy TransformerLens-format HF repos. 

2 

3NeelNanda/*, ArthurConmy/*, and Baidicoot/* repos predate the HF model format: 

4no ``model_type`` in config.json (AutoConfig refuses them), weights stored as 

5``*.pth`` state dicts in HookedTransformer property format (or older layouts 

6converted below), and training checkpoints as ``checkpoints/*_<label>.pth`` 

7files rather than revisions. This module derives a TransformerBridgeConfig from 

8the repo's TL-style config.json, fetches and normalizes the state dict, and 

9loads it into a ``boot_native`` bridge via ``convert_tl_checkpoint``. 

10 

11The two legacy layout converters are ports of the HookedTransformer loaders 

12(pretrained/weight_conversions/{neel_solu_old,mingpt}.py), rehomed here so this 

13path survives the 4.0 deletion of the legacy loading stack. 

14""" 

15 

16from __future__ import annotations 

17 

18import json 

19from typing import Any, Optional, Union 

20 

21import einops 

22import torch 

23 

24from transformer_lens.config import TransformerBridgeConfig 

25from transformer_lens.model_bridge.bridge import TransformerBridge 

26from transformer_lens.utilities.tl_checkpoint_conversion import convert_tl_checkpoint 

27 

28TL_LEGACY_PREFIXES = ("NeelNanda/", "ArthurConmy/", "Baidicoot/") 

29 

30 

31def _fetch_json(repo_id: str, filename: str) -> dict: 

32 from huggingface_hub import hf_hub_download 

33 

34 with open(hf_hub_download(repo_id, filename)) as f: 

35 return json.load(f) 

36 

37 

38def derive_tl_legacy_config(repo_id: str) -> TransformerBridgeConfig: 

39 """TransformerBridgeConfig from a legacy TL repo's config.json.""" 

40 cfg_json = _fetch_json(repo_id, "config.json") 

41 architecture = cfg_json.get( 

42 "architecture", "neel" if "_old" not in repo_id else "neel-solu-old" 

43 ) 

44 normalization = cfg_json.get("normalization", cfg_json.get("normalization_type")) 

45 if cfg_json.get("shortformer_pos", False): 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 raise NotImplementedError( 

47 f"{repo_id} uses shortformer positional embeddings, which the native " 

48 "bridge does not implement." 

49 ) 

50 cfg = TransformerBridgeConfig( 

51 d_model=cfg_json["d_model"], 

52 n_layers=cfg_json["n_layers"], 

53 d_mlp=cfg_json["d_mlp"], 

54 d_head=cfg_json["d_head"], 

55 n_heads=cfg_json["n_heads"], 

56 n_ctx=cfg_json["n_ctx"], 

57 d_vocab=cfg_json["d_vocab"], 

58 act_fn=cfg_json["act_fn"], 

59 attn_only=cfg_json["attn_only"], 

60 final_rms=cfg_json.get("final_rms", False), 

61 normalization_type=normalization, 

62 positional_embedding_type="standard", 

63 tokenizer_name=cfg_json.get("tokenizer_name"), 

64 architecture="TransformerLensNative", 

65 ) 

66 cfg.original_architecture = architecture # type: ignore[attr-defined] 

67 return cfg 

68 

69 

70def _convert_neel_solu_old_weights(state_dict: dict, cfg: TransformerBridgeConfig) -> dict: 

71 """Old-layout SoLU repos ('*_old'): left-facing weights below 8L, and 8L's 

72 W_pos alone left-facing. Port of the HookedTransformer converter.""" 

73 reverse_pos = cfg.n_layers <= 8 

74 reverse_weights = cfg.n_layers <= 6 

75 new_state_dict = {} 

76 for k, v in state_dict.items(): 

77 k = k.replace("norm", "ln") 

78 if k.startswith("ln."): 

79 k = k.replace("ln.", "ln_final.") 

80 new_state_dict[k] = v 

81 if reverse_pos: 

82 new_state_dict["pos_embed.W_pos"] = new_state_dict["pos_embed.W_pos"].T 

83 if reverse_weights: 

84 for k, v in new_state_dict.items(): 

85 if "W_" in k and "W_pos" not in k: 

86 new_state_dict[k] = v.transpose(-2, -1) 

87 return new_state_dict 

88 

89 

90def _convert_mingpt_weights(old_state_dict: dict, cfg: TransformerBridgeConfig) -> dict: 

91 """minGPT layout (Baidicoot/Othello-GPT): unconcatenated QKV. Port of the 

92 HookedTransformer converter.""" 

93 state_dict = { 

94 "embed.W_E": old_state_dict["tok_emb.weight"], 

95 "pos_embed.W_pos": old_state_dict["pos_emb"].squeeze(), 

96 } 

97 for l in range(cfg.n_layers): 

98 state_dict[f"blocks.{l}.ln1.w"] = old_state_dict[f"blocks.{l}.ln1.weight"] 

99 state_dict[f"blocks.{l}.ln1.b"] = old_state_dict[f"blocks.{l}.ln1.bias"] 

100 for name, hf in (("Q", "query"), ("K", "key"), ("V", "value")): 

101 w = einops.rearrange( 

102 old_state_dict[f"blocks.{l}.attn.{hf}.weight"], "(i h) m->i m h", i=cfg.n_heads 

103 ) 

104 b = einops.rearrange( 

105 old_state_dict[f"blocks.{l}.attn.{hf}.bias"], "(i h)->i h", i=cfg.n_heads 

106 ) 

107 state_dict[f"blocks.{l}.attn.W_{name}"] = w 

108 state_dict[f"blocks.{l}.attn.b_{name}"] = b 

109 state_dict[f"blocks.{l}.attn.W_O"] = einops.rearrange( 

110 old_state_dict[f"blocks.{l}.attn.proj.weight"], "m (i h)->i h m", i=cfg.n_heads 

111 ) 

112 state_dict[f"blocks.{l}.attn.b_O"] = old_state_dict[f"blocks.{l}.attn.proj.bias"] 

113 state_dict[f"blocks.{l}.ln2.w"] = old_state_dict[f"blocks.{l}.ln2.weight"] 

114 state_dict[f"blocks.{l}.ln2.b"] = old_state_dict[f"blocks.{l}.ln2.bias"] 

115 state_dict[f"blocks.{l}.mlp.W_in"] = old_state_dict[f"blocks.{l}.mlp.0.weight"].T 

116 state_dict[f"blocks.{l}.mlp.b_in"] = old_state_dict[f"blocks.{l}.mlp.0.bias"] 

117 state_dict[f"blocks.{l}.mlp.W_out"] = old_state_dict[f"blocks.{l}.mlp.2.weight"].T 

118 state_dict[f"blocks.{l}.mlp.b_out"] = old_state_dict[f"blocks.{l}.mlp.2.bias"] 

119 state_dict["ln_final.w"] = old_state_dict["ln_f.weight"] 

120 state_dict["ln_final.b"] = old_state_dict["ln_f.bias"] 

121 state_dict["unembed.W_U"] = old_state_dict["head.weight"].T 

122 return state_dict 

123 

124 

125def fetch_tl_legacy_state_dict( 

126 repo_id: str, 

127 cfg: TransformerBridgeConfig, 

128 checkpoint_value: Optional[int] = None, 

129 dtype: torch.dtype = torch.float32, 

130) -> dict: 

131 """Download and normalize a legacy repo's state dict to TL property format.""" 

132 from huggingface_hub import HfApi, hf_hub_download 

133 

134 repo_files = HfApi().list_repo_files(repo_id) 

135 suffix = f"{checkpoint_value}.pth" if checkpoint_value is not None else "final.pth" 

136 matches = [f for f in repo_files if f.endswith(suffix)] 

137 if not matches: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 raise FileNotFoundError(f"No '*{suffix}' file in {repo_id}; files: {repo_files[:8]}...") 

139 state_dict = torch.load( 

140 hf_hub_download(repo_id, matches[0]), map_location="cpu", weights_only=True 

141 ) 

142 state_dict = {k: v.to(dtype) for k, v in state_dict.items()} 

143 

144 original_architecture = getattr(cfg, "original_architecture", None) 

145 if original_architecture == "neel-solu-old": 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true

146 state_dict = _convert_neel_solu_old_weights(state_dict, cfg) 

147 elif original_architecture == "mingpt": 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true

148 state_dict = _convert_mingpt_weights(state_dict, cfg) 

149 return state_dict 

150 

151 

152def boot( 

153 model_name: str, 

154 checkpoint_index: Optional[int] = None, 

155 checkpoint_value: Optional[int] = None, 

156 device: Optional[Union[str, torch.device]] = None, 

157 dtype: torch.dtype = torch.float32, 

158 tokenizer: Optional[Any] = None, 

159) -> TransformerBridge: 

160 """Build a bridge for a legacy TransformerLens-format HF repo. 

161 

162 ``checkpoint_index`` / ``checkpoint_value`` select a training checkpoint 

163 (``checkpoints/*_<label>.pth``); by default the final weights load. The 

164 resolved values are stamped on ``cfg.checkpoint_index`` / 

165 ``cfg.checkpoint_value``, mirroring the legacy loader. 

166 """ 

167 if not model_name.startswith(TL_LEGACY_PREFIXES): 

168 raise ValueError( 

169 f"{model_name!r} is not a known legacy TransformerLens repo family " 

170 f"{TL_LEGACY_PREFIXES}. Use TransformerBridge.boot_transformers for " 

171 "HuggingFace-format models." 

172 ) 

173 cfg = derive_tl_legacy_config(model_name) 

174 

175 if checkpoint_index is not None or checkpoint_value is not None: 

176 from transformer_lens.tools.model_registry.checkpoints import ( 

177 get_checkpoint_labels, 

178 ) 

179 

180 labels, _ = get_checkpoint_labels(model_name) 

181 if checkpoint_value is None: 181 ↛ 191line 181 didn't jump to line 191 because the condition on line 181 was always true

182 assert checkpoint_index is not None 

183 # Negative indices count from the end (checkpoint_index=-1 is the 

184 # final checkpoint), matching the legacy loader's list indexing. 

185 if not -len(labels) <= checkpoint_index < len(labels): 

186 raise ValueError( 

187 f"checkpoint_index={checkpoint_index} out of range " 

188 f"[-{len(labels)}, {len(labels)}) for {model_name!r}." 

189 ) 

190 checkpoint_value = labels[checkpoint_index] 

191 elif checkpoint_value not in labels: 

192 raise ValueError( 

193 f"checkpoint_value={checkpoint_value} not in available checkpoints for " 

194 f"{model_name!r} ({len(labels)} labels, {labels[0]}..{labels[-1]})." 

195 ) 

196 cfg.checkpoint_index = labels.index(checkpoint_value) # type: ignore[attr-defined] 

197 cfg.checkpoint_value = checkpoint_value # type: ignore[attr-defined] 

198 else: 

199 cfg.checkpoint_index = None # type: ignore[attr-defined] 

200 cfg.checkpoint_value = None # type: ignore[attr-defined] 

201 

202 if tokenizer is None and cfg.tokenizer_name is not None: 202 ↛ 207line 202 didn't jump to line 207 because the condition on line 202 was always true

203 from transformers import AutoTokenizer 

204 

205 tokenizer = AutoTokenizer.from_pretrained(cfg.tokenizer_name) 

206 

207 from transformer_lens.model_bridge.sources.native import boot as _boot_native 

208 

209 bridge = _boot_native( 

210 cfg, tokenizer=tokenizer, device=device, dtype=dtype, model_name=model_name 

211 ) 

212 legacy_sd = fetch_tl_legacy_state_dict( 

213 model_name, cfg, checkpoint_value=checkpoint_value, dtype=dtype 

214 ) 

215 converted = convert_tl_checkpoint(legacy_sd, cfg) 

216 # Several legacy repos ship no unembed bias; the legacy loader zero-filled 

217 # missing params, so mirror that for exactly this key rather than failing 

218 # a strict load or silently accepting arbitrary gaps. 

219 if "unembed.bias" not in converted: 219 ↛ 221line 219 didn't jump to line 221 because the condition on line 219 was always true

220 converted["unembed.bias"] = torch.zeros(cfg.d_vocab, dtype=dtype) 

221 result = bridge.load_state_dict(converted, strict=True) 

222 assert not result.missing_keys and not result.unexpected_keys 

223 return bridge 

224 

225 

226setattr(TransformerBridge, "boot_tl_legacy", staticmethod(boot))