Coverage for transformer_lens/weight_processing.py: 77%

844 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +0000

1""" 

2Weight Processing Functions for Transformer Models. 

3 

4This module contains all the weight processing functions extracted from HookedTransformer, 

5organized into a single ProcessWeights class with static methods. These functions are used 

6to modify transformer model weights for better interpretability and analysis. 

7""" 

8import re 

9from typing import Any, Dict, Optional, Union, overload 

10 

11import einops 

12import torch 

13 

14import transformer_lens.utilities as utils 

15from transformer_lens.config.transformer_lens_config import TransformerLensConfig 

16from transformer_lens.FactoredMatrix import FactoredMatrix 

17from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

18from transformer_lens.utilities import filter_dict_by_prefix 

19from transformer_lens.utilities.architectures import POST_NORM_ARCHITECTURES 

20 

21 

22class ProcessWeights: 

23 """ 

24 A collection of static methods for processing transformer model weights. 

25 

26 These methods are extracted from HookedTransformer and provide various weight 

27 transformations for improved model interpretability: 

28 - LayerNorm folding: Merges LayerNorm parameters into subsequent linear layers 

29 - Weight centering: Centers weights that write to the residual stream 

30 - Unembed centering: Centers unembedding weights (translation invariant) 

31 - Value bias folding: Consolidates value biases into output biases 

32 - Attention matrix refactoring: Experimental QK/OV matrix factorization 

33 

34 When an architecture adapter is provided, the methods will translate TransformerLens 

35 parameter names to the target format (e.g., HuggingFace) for processing. 

36 """ 

37 

38 @staticmethod 

39 def _get_param_key(tl_key: str, adapter=None) -> str: 

40 """Convert legacy TL key format (W_Q, b_Q) to component-based format (q.weight, q.bias). 

41 

42 Args: 

43 tl_key: TransformerLens format parameter key (e.g., "blocks.0.attn.W_Q") 

44 adapter: Architecture adapter for translating paths 

45 

46 Returns: 

47 The component-based key (e.g., "blocks.0.attn.q.weight") 

48 """ 

49 if adapter is None: 

50 return tl_key 

51 

52 return ProcessWeights._prepare_component_path(tl_key) 

53 

54 @staticmethod 

55 def _prepare_component_path(tl_key: str) -> str: 

56 """Map a TransformerLens key to bridge-style component path. 

57 

58 Converts TransformerLens weight names (like "W_Q", "b_in") to bridge-style 

59 paths (like "q.weight", "in.bias"). The full path is assembled before being 

60 passed to the architecture adapter for translation. 

61 

62 Args: 

63 tl_key: TransformerLens key like "blocks.0.attn.W_Q" 

64 

65 Returns: 

66 Full path like "blocks.0.attn.q.weight" 

67 """ 

68 suffix_map: Dict[str, str] = { 

69 "W_Q": "q.weight", 

70 "_W_Q": "q.weight", 

71 "b_Q": "q.bias", 

72 "_b_Q": "q.bias", 

73 "W_K": "k.weight", 

74 "_W_K": "k.weight", 

75 "b_K": "k.bias", 

76 "_b_K": "k.bias", 

77 "W_V": "v.weight", 

78 "_W_V": "v.weight", 

79 "b_V": "v.bias", 

80 "_b_V": "v.bias", 

81 "W_O": "o.weight", 

82 "b_O": "o.bias", 

83 "W_in": "in.weight", 

84 "b_in": "in.bias", 

85 "W_gate": "gate.weight", 

86 "b_gate": "gate.bias", 

87 "W_out": "out.weight", 

88 "b_out": "out.bias", 

89 "W_E": "weight", 

90 "b_E": "bias", 

91 "W_pos": "weight", 

92 "b_pos": "bias", 

93 "W_U": "weight", 

94 "b_U": "bias", 

95 "w": "weight", 

96 "b": "bias", 

97 "weight": "weight", 

98 "bias": "bias", 

99 } 

100 if "." not in tl_key: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 return tl_key 

102 base_path, suffix = tl_key.rsplit(".", 1) 

103 if suffix in suffix_map: 103 ↛ 106line 103 didn't jump to line 106 because the condition on line 103 was always true

104 replacement = suffix_map[suffix] 

105 return f"{base_path}.{replacement}" 

106 return tl_key 

107 

108 @staticmethod 

109 def _resolve_state_dict_key( 

110 state_dict: Dict[str, torch.Tensor], 

111 key: str, 

112 layer: Optional[int] = None, 

113 ) -> str: 

114 """Resolve a bridge-style key to the actual key in the state_dict. 

115 

116 Some architectures (e.g., BD3LM's symbolic attention) store parameters 

117 with HF-style prefixes instead of bridge-style prefixes. This method 

118 handles the key resolution by falling back to a suffix search. 

119 

120 Args: 

121 state_dict: Model state dictionary 

122 key: The expected key (e.g., "blocks.0.mlp.in.weight") 

123 layer: Optional layer index for layer-specific searches 

124 

125 Returns: 

126 The actual key found in state_dict, or the original key if no match 

127 """ 

128 if key in state_dict: 

129 return key 

130 

131 # Extract the component path after "blocks.{i}." 

132 import re 

133 

134 match = re.match(r"blocks\.(\d+)\.(.*)", key) 

135 if match: 135 ↛ 151line 135 didn't jump to line 151 because the condition on line 135 was always true

136 layer_idx = match.group(1) 

137 component_suffix = match.group(2) 

138 # Search for keys ending with the component suffix that include the layer index 

139 for sd_key in state_dict: 

140 if sd_key.endswith(f".{component_suffix}") and f".{layer_idx}." in sd_key: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true

141 return sd_key 

142 

143 # Mixed MoE/dense adapters (Llama4, Laguna, LLaDA2-MoE) put a non-MoE 

144 # layer's gated MLP in the MoE slot under dense_gate/dense_in/dense_out 

145 # so it coexists with the router/experts. When the standard block-level 

146 # mlp.{gate,in,out} key is absent, fall back to that dense_ variant. 

147 # Fails closed: the standard resolution above already ran (so a real 

148 # mlp.gate router is never shadowed), the match is anchored to a block's 

149 # top-level MLP (not any nested `.mlp.`), and the dense key is returned 

150 # only if it exists — models without the convention are untouched. 

151 proj_match = re.match(r"(blocks\.\d+\.mlp\.)(in|gate|out)(\..+)$", key) 

152 if proj_match: 

153 dense_key = f"{proj_match.group(1)}dense_{proj_match.group(2)}{proj_match.group(3)}" 

154 if dense_key in state_dict: 

155 return dense_key 

156 dm = re.match(r"blocks\.(\d+)\.(.*)", dense_key) 

157 if dm: 157 ↛ 162line 157 didn't jump to line 162 because the condition on line 157 was always true

158 for sd_key in state_dict: 

159 if sd_key.endswith(f".{dm.group(2)}") and f".{dm.group(1)}." in sd_key: 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

160 return sd_key 

161 

162 return key 

163 

164 @staticmethod 

165 def _safe_get_tensor( 

166 state_dict: Dict[str, torch.Tensor], 

167 tl_key: str, 

168 adapter=None, 

169 default: Optional[torch.Tensor] = None, 

170 ) -> Optional[torch.Tensor]: 

171 """Safely get a tensor from state_dict, handling optional parameters. 

172 

173 This is the recommended way to access parameters that may not exist in all architectures 

174 (e.g., biases in Qwen2/LLaMA/Gemma). Returns None if the parameter doesn't exist, 

175 rather than raising a KeyError. 

176 

177 Args: 

178 state_dict: Model state dictionary 

179 tl_key: TransformerLens format parameter key (e.g., "blocks.0.attn.b_Q") 

180 adapter: Optional architecture adapter for key translation 

181 default: Optional default value to return if key not found (defaults to None) 

182 

183 Returns: 

184 The tensor if found, otherwise the default value (None if not specified) 

185 

186 Examples: 

187 # Get optional bias (may be None for Qwen2/LLaMA) 

188 b_Q = ProcessWeights._safe_get_tensor(state_dict, "blocks.0.attn.b_Q", adapter) 

189 

190 # Get required weight (will be None if missing, can check explicitly) 

191 W_Q = ProcessWeights._safe_get_tensor(state_dict, "blocks.0.attn.W_Q", adapter) 

192 if W_Q is None: 

193 raise ValueError("Required weight W_Q not found") 

194 """ 

195 actual_key = ProcessWeights._get_param_key(tl_key, adapter) 

196 return state_dict.get(actual_key, default) 

197 

198 @staticmethod 

199 def fold_layer_norm_bias_single( 

200 w_tensor: torch.Tensor, b_tensor: torch.Tensor, ln_bias: torch.Tensor 

201 ) -> torch.Tensor: 

202 """Fold LayerNorm bias into a single attention bias. 

203 

204 Args: 

205 w_tensor: Weight tensor [n_heads, d_model, d_head] 

206 b_tensor: Bias tensor [n_heads, d_head] 

207 ln_bias: LayerNorm bias [d_model] 

208 

209 Returns: 

210 New bias tensor with folded LayerNorm bias 

211 """ 

212 return b_tensor + (w_tensor * ln_bias[None, :, None]).sum(-2) 

213 

214 @staticmethod 

215 def fold_layer_norm_weight_single( 

216 w_tensor: torch.Tensor, ln_weight: torch.Tensor 

217 ) -> torch.Tensor: 

218 """Fold LayerNorm weight into a single attention weight. 

219 

220 Args: 

221 w_tensor: Weight tensor [n_heads, d_model, d_head] 

222 ln_weight: LayerNorm weight [d_model] 

223 

224 Returns: 

225 New weight tensor with folded LayerNorm weight 

226 """ 

227 return w_tensor * ln_weight[None, :, None] 

228 

229 @staticmethod 

230 def center_weight_single(w_tensor: torch.Tensor) -> torch.Tensor: 

231 """Center a single attention weight by subtracting the mean. 

232 

233 Args: 

234 w_tensor: Weight tensor [n_heads, d_model, d_head] 

235 

236 Returns: 

237 Centered weight tensor 

238 """ 

239 return w_tensor - einops.reduce( 

240 w_tensor, "head_index d_model d_head -> head_index 1 d_head", "mean" 

241 ) 

242 

243 @staticmethod 

244 def fold_layer_norm_biases( 

245 wq_tensor: torch.Tensor, 

246 wk_tensor: torch.Tensor, 

247 wv_tensor: torch.Tensor, 

248 bq_tensor: Optional[torch.Tensor], 

249 bk_tensor: Optional[torch.Tensor], 

250 bv_tensor: Optional[torch.Tensor], 

251 ln_bias: torch.Tensor, 

252 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: 

253 """Fold LayerNorm bias into attention biases. 

254 

255 When QKV biases don't exist (e.g., GPT-Neo), creates zero-initialized biases 

256 to absorb the LN bias contribution, similar to how MLP folding handles missing biases. 

257 

258 Args: 

259 wq_tensor, wk_tensor, wv_tensor: Weight tensors [n_heads, d_model, d_head] 

260 bq_tensor, bk_tensor, bv_tensor: Bias tensors [n_heads, d_head] or None if no bias 

261 ln_bias: LayerNorm bias [d_model] 

262 

263 Returns: 

264 Tuple of (new_bq, new_bk, new_bv) with folded biases (always non-None) 

265 """ 

266 

267 def _zero_bias(w: torch.Tensor) -> torch.Tensor: 

268 return torch.zeros(w.shape[0], w.shape[2], dtype=w.dtype, device=w.device) 

269 

270 new_bq = ProcessWeights.fold_layer_norm_bias_single( 

271 wq_tensor, bq_tensor if bq_tensor is not None else _zero_bias(wq_tensor), ln_bias 

272 ) 

273 new_bk = ProcessWeights.fold_layer_norm_bias_single( 

274 wk_tensor, bk_tensor if bk_tensor is not None else _zero_bias(wk_tensor), ln_bias 

275 ) 

276 new_bv = ProcessWeights.fold_layer_norm_bias_single( 

277 wv_tensor, bv_tensor if bv_tensor is not None else _zero_bias(wv_tensor), ln_bias 

278 ) 

279 return (new_bq, new_bk, new_bv) 

280 

281 @staticmethod 

282 def fold_layer_norm_weights( 

283 wq_tensor: torch.Tensor, 

284 wk_tensor: torch.Tensor, 

285 wv_tensor: torch.Tensor, 

286 ln_weight: torch.Tensor, 

287 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: 

288 """Fold LayerNorm weight into attention weights. 

289 

290 Args: 

291 wq_tensor, wk_tensor, wv_tensor: Weight tensors [n_heads, d_model, d_head] 

292 ln_weight: LayerNorm weight [d_model] 

293 

294 Returns: 

295 Tuple of (new_wq, new_wk, new_wv) with folded weights 

296 """ 

297 new_wq = ProcessWeights.fold_layer_norm_weight_single(wq_tensor, ln_weight) 

298 new_wk = ProcessWeights.fold_layer_norm_weight_single(wk_tensor, ln_weight) 

299 new_wv = ProcessWeights.fold_layer_norm_weight_single(wv_tensor, ln_weight) 

300 return (new_wq, new_wk, new_wv) 

301 

302 @staticmethod 

303 def center_attention_weights( 

304 wq_tensor: torch.Tensor, wk_tensor: torch.Tensor, wv_tensor: torch.Tensor 

305 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: 

306 """Center attention weights by subtracting the mean. 

307 

308 Args: 

309 wq_tensor, wk_tensor, wv_tensor: Weight tensors [n_heads, d_model, d_head] 

310 

311 Returns: 

312 Tuple of (centered_wq, centered_wk, centered_wv) 

313 """ 

314 centered_wq = ProcessWeights.center_weight_single(wq_tensor) 

315 centered_wk = ProcessWeights.center_weight_single(wk_tensor) 

316 centered_wv = ProcessWeights.center_weight_single(wv_tensor) 

317 return (centered_wq, centered_wk, centered_wv) 

318 

319 @staticmethod 

320 def extract_attention_tensors_for_folding( 

321 state_dict: Dict[str, torch.Tensor], cfg, layer: int, adapter 

322 ) -> Dict[str, Union[torch.Tensor, None, Dict[str, str]]]: 

323 """Extract attention tensors in TransformerLens format for layer norm folding. 

324 

325 Args: 

326 state_dict: The state dictionary containing tensors 

327 cfg: Model configuration object 

328 layer: Layer index 

329 adapter: Optional architecture adapter for parameter key translation 

330 

331 Returns: 

332 Dictionary with keys: 'wq', 'wk', 'wv', 'bq', 'bk', 'bv', 'ln1_b', 'ln1_w' 

333 All tensors are in TransformerLens format for consistent processing 

334 """ 

335 b_Q_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_Q", adapter) 

336 W_Q_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.W_Q", adapter) 

337 b_K_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_K", adapter) 

338 W_K_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.W_K", adapter) 

339 b_V_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_V", adapter) 

340 W_V_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.W_V", adapter) 

341 ln1_b_key = ProcessWeights._get_param_key(f"blocks.{layer}.ln1.b", adapter) 

342 ln1_w_key = ProcessWeights._get_param_key(f"blocks.{layer}.ln1.w", adapter) 

343 

344 # For GQA models, Q, K and V weights may use underscore prefix (_W_Q, _W_K, _W_V) 

345 # Check if standard keys exist, otherwise update to use underscore-prefixed versions 

346 if W_Q_key not in state_dict: 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true

347 W_Q_key = W_Q_key.replace(".W_Q", "._W_Q") 

348 if W_K_key not in state_dict: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true

349 W_K_key = W_K_key.replace(".W_K", "._W_K") 

350 if W_V_key not in state_dict: 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true

351 W_V_key = W_V_key.replace(".W_V", "._W_V") 

352 if b_Q_key not in state_dict: 

353 b_Q_key = b_Q_key.replace(".b_Q", "._b_Q") 

354 if b_K_key not in state_dict: 

355 b_K_key = b_K_key.replace(".b_K", "._b_K") 

356 if b_V_key not in state_dict: 

357 b_V_key = b_V_key.replace(".b_V", "._b_V") 

358 

359 wq_tensor: Optional[torch.Tensor] = state_dict.get(W_Q_key) 

360 wk_tensor: Optional[torch.Tensor] = state_dict.get(W_K_key) 

361 wv_tensor: Optional[torch.Tensor] = state_dict.get(W_V_key) 

362 bq_tensor: Optional[torch.Tensor] = state_dict.get(b_Q_key) 

363 bk_tensor: Optional[torch.Tensor] = state_dict.get(b_K_key) 

364 bv_tensor: Optional[torch.Tensor] = state_dict.get(b_V_key) 

365 ln1_b = state_dict.get(ln1_b_key, None) 

366 ln1_w = state_dict.get(ln1_w_key, None) 

367 if adapter: 

368 wq_tensor = ProcessWeights.convert_tensor_to_tl_format( 

369 W_Q_key, state_dict, wq_tensor, cfg, adapter, layer 

370 ) 

371 wk_tensor = ProcessWeights.convert_tensor_to_tl_format( 

372 W_K_key, state_dict, wk_tensor, cfg, adapter, layer 

373 ) 

374 wv_tensor = ProcessWeights.convert_tensor_to_tl_format( 

375 W_V_key, state_dict, wv_tensor, cfg, adapter, layer 

376 ) 

377 bq_tensor = ProcessWeights.convert_tensor_to_tl_format( 

378 b_Q_key, state_dict, bq_tensor, cfg, adapter, layer 

379 ) 

380 bk_tensor = ProcessWeights.convert_tensor_to_tl_format( 

381 b_K_key, state_dict, bk_tensor, cfg, adapter, layer 

382 ) 

383 bv_tensor = ProcessWeights.convert_tensor_to_tl_format( 

384 b_V_key, state_dict, bv_tensor, cfg, adapter, layer 

385 ) 

386 

387 # Auto-reshape 1D biases for 3D weights (e.g., OPT) 

388 def _reshape_bias_if_needed(bias, weight): 

389 if bias is not None and weight is not None: 

390 if len(weight.shape) == 3 and len(bias.shape) == 1: 

391 n_heads = weight.shape[0] 

392 d_head = weight.shape[2] 

393 if bias.shape[0] == n_heads * d_head: 393 ↛ 395line 393 didn't jump to line 395 because the condition on line 393 was always true

394 return bias.reshape(n_heads, d_head) 

395 return bias 

396 

397 bq_tensor = _reshape_bias_if_needed(bq_tensor, wq_tensor) 

398 bk_tensor = _reshape_bias_if_needed(bk_tensor, wk_tensor) 

399 bv_tensor = _reshape_bias_if_needed(bv_tensor, wv_tensor) 

400 

401 return { 

402 "wq": wq_tensor, 

403 "wk": wk_tensor, 

404 "wv": wv_tensor, 

405 "bq": bq_tensor, 

406 "bk": bk_tensor, 

407 "bv": bv_tensor, 

408 "ln1_b": ln1_b, 

409 "ln1_w": ln1_w, 

410 "keys": { 

411 "W_Q": W_Q_key, 

412 "W_K": W_K_key, 

413 "W_V": W_V_key, 

414 "b_Q": b_Q_key, 

415 "b_K": b_K_key, 

416 "b_V": b_V_key, 

417 "ln1_b": ln1_b_key, 

418 "ln1_w": ln1_w_key, 

419 }, 

420 } 

421 

422 @staticmethod 

423 def _fold_layer( 

424 state_dict: Dict[str, torch.Tensor], 

425 cfg, 

426 layer_idx: int, 

427 fold_biases: bool, 

428 center_weights: bool, 

429 adapter, 

430 gqa: str, 

431 ) -> Dict[str, torch.Tensor]: 

432 """Fold LayerNorm for a single layer. 

433 

434 Args: 

435 state_dict: The state dictionary to process (modified in place) 

436 cfg: Model configuration object 

437 layer_idx: The layer index to process 

438 fold_biases: Whether to fold LayerNorm biases 

439 center_weights: Whether to center weights after folding 

440 adapter: Optional architecture adapter for parameter key translation 

441 gqa: GQA prefix string (empty or "_") 

442 """ 

443 layer = layer_idx 

444 tensors = ProcessWeights.extract_attention_tensors_for_folding( 

445 state_dict, cfg, layer, adapter 

446 ) 

447 wq_tensor = tensors["wq"] 

448 wk_tensor = tensors["wk"] 

449 wv_tensor = tensors["wv"] 

450 bq_tensor = tensors["bq"] 

451 bk_tensor = tensors["bk"] 

452 bv_tensor = tensors["bv"] 

453 ln1_b = tensors["ln1_b"] 

454 ln1_w = tensors["ln1_w"] 

455 keys = tensors["keys"] 

456 

457 # Fold LN into QKV (skip if combined QKV, e.g., OpenELM) 

458 if wq_tensor is not None: 458 ↛ 533line 458 didn't jump to line 533 because the condition on line 458 was always true

459 assert isinstance(wq_tensor, torch.Tensor) 

460 assert isinstance(keys, dict) 

461 if wk_tensor is not None: 461 ↛ 463line 461 didn't jump to line 463 because the condition on line 461 was always true

462 assert isinstance(wk_tensor, torch.Tensor) 

463 if wv_tensor is not None: 463 ↛ 465line 463 didn't jump to line 465 because the condition on line 463 was always true

464 assert isinstance(wv_tensor, torch.Tensor) 

465 if bq_tensor is not None: 

466 assert isinstance(bq_tensor, torch.Tensor) 

467 if bk_tensor is not None: 

468 assert isinstance(bk_tensor, torch.Tensor) 

469 if bv_tensor is not None: 

470 assert isinstance(bv_tensor, torch.Tensor) 

471 # RMS norm (Gemma): ln1_b may be None, only ln1_w required 

472 if ln1_w is not None: 

473 assert isinstance(ln1_w, torch.Tensor) 

474 # Fold biases if present (RMS norm has none; missing QKV biases get zeros) 

475 if fold_biases and ln1_b is not None: 

476 assert isinstance(ln1_b, torch.Tensor) 

477 assert wq_tensor is not None 

478 assert wk_tensor is not None 

479 assert wv_tensor is not None 

480 bq_tensor, bk_tensor, bv_tensor = ProcessWeights.fold_layer_norm_biases( 

481 wq_tensor, wk_tensor, wv_tensor, bq_tensor, bk_tensor, bv_tensor, ln1_b 

482 ) 

483 if keys["ln1_b"] in state_dict: 483 ↛ 485line 483 didn't jump to line 485 because the condition on line 483 was always true

484 state_dict[keys["ln1_b"]] = torch.zeros_like(ln1_b) 

485 alternate_b_key = ( 

486 keys["ln1_b"].replace("ln_1", "ln1") 

487 if "ln_1" in keys["ln1_b"] 

488 else keys["ln1_b"].replace("ln1", "ln_1") 

489 ) 

490 if alternate_b_key != keys["ln1_b"] and alternate_b_key in state_dict: 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 state_dict[alternate_b_key] = torch.zeros_like(ln1_b) 

492 # Fold ln1_w; use (1+w) for rmsnorm_uses_offset (Gemma), then set to identity 

493 rmsnorm_uses_offset = getattr(cfg, "rmsnorm_uses_offset", False) 

494 effective_ln1_w = (1.0 + ln1_w) if rmsnorm_uses_offset else ln1_w 

495 if wk_tensor is not None and wv_tensor is not None: 495 ↛ 500line 495 didn't jump to line 500 because the condition on line 495 was always true

496 wq_tensor, wk_tensor, wv_tensor = ProcessWeights.fold_layer_norm_weights( 

497 wq_tensor, wk_tensor, wv_tensor, effective_ln1_w 

498 ) 

499 # Set ln1.w to identity: ones (standard) or zeros (rmsnorm_uses_offset) 

500 identity_val = ( 

501 torch.zeros_like(ln1_w) if rmsnorm_uses_offset else torch.ones_like(ln1_w) 

502 ) 

503 if keys["ln1_w"] in state_dict: 503 ↛ 505line 503 didn't jump to line 505 because the condition on line 503 was always true

504 state_dict[keys["ln1_w"]] = identity_val 

505 alternate_w_key = ( 

506 keys["ln1_w"].replace("ln_1", "ln1") 

507 if "ln_1" in keys["ln1_w"] 

508 else keys["ln1_w"].replace("ln1", "ln_1") 

509 ) 

510 if alternate_w_key != keys["ln1_w"] and alternate_w_key in state_dict: 510 ↛ 511line 510 didn't jump to line 511 because the condition on line 510 was never true

511 state_dict[alternate_w_key] = identity_val 

512 if center_weights and wk_tensor is not None and (wv_tensor is not None): 

513 wq_tensor, wk_tensor, wv_tensor = ProcessWeights.center_attention_weights( 

514 wq_tensor, wk_tensor, wv_tensor 

515 ) 

516 state_dict = ProcessWeights._store_processed_attention_tensors( 

517 state_dict, 

518 keys, 

519 wq_tensor, 

520 wk_tensor, 

521 wv_tensor, 

522 bq_tensor, 

523 bk_tensor, 

524 bv_tensor, 

525 adapter, 

526 cfg, 

527 layer, 

528 ) 

529 

530 # ln1_post.w (Gemma 2/3): keep original; independent post-attention normalization 

531 

532 # Fold MLP LN: shared ln1 (Phi-2, GPT-J) or separate ln2 (Pythia) 

533 if getattr(cfg, "parallel_attn_mlp", False) and ln1_w is not None: 

534 # Check if a separate ln2 exists for this layer 

535 ln2_check_key = ProcessWeights._resolve_state_dict_key( 

536 state_dict, 

537 ProcessWeights._get_param_key(f"blocks.{layer_idx}.ln2.w", adapter), 

538 layer_idx, 

539 ) 

540 if ln2_check_key in state_dict: 540 ↛ 547line 540 didn't jump to line 547 because the condition on line 540 was always true

541 # Separate ln2 (e.g., GPT-NeoX/Pythia) — fold ln2 → MLP normally 

542 state_dict = ProcessWeights._fold_mlp_layer_norm( 

543 state_dict, cfg, layer, fold_biases, center_weights, adapter 

544 ) 

545 else: 

546 # Shared ln1 (e.g., Phi-2, GPT-J) — fold ln1 → MLP via override 

547 assert isinstance(ln1_w, torch.Tensor) 

548 assert ln1_b is None or isinstance(ln1_b, torch.Tensor) 

549 state_dict = ProcessWeights._fold_mlp_layer_norm( 

550 state_dict, 

551 cfg, 

552 layer, 

553 fold_biases, 

554 center_weights, 

555 adapter, 

556 override_ln_w=ln1_w, 

557 override_ln_b=ln1_b, 

558 ) 

559 else: 

560 state_dict = ProcessWeights._fold_mlp_layer_norm( 

561 state_dict, cfg, layer, fold_biases, center_weights, adapter 

562 ) 

563 

564 return state_dict 

565 

566 @staticmethod 

567 def _fold_mlp_layer_norm( 

568 state_dict: Dict[str, torch.Tensor], 

569 cfg, 

570 layer: int, 

571 fold_biases: bool, 

572 center_weights: bool, 

573 adapter, 

574 override_ln_w: Optional[torch.Tensor] = None, 

575 override_ln_b: Optional[torch.Tensor] = None, 

576 ) -> Dict[str, torch.Tensor]: 

577 """Fold LayerNorm into MLP layer. 

578 

579 Args: 

580 state_dict: The state dictionary to process (modified in place) 

581 cfg: Model configuration object 

582 layer: The layer index to process 

583 fold_biases: Whether to fold LayerNorm biases 

584 center_weights: Whether to center weights after folding 

585 adapter: Optional architecture adapter for parameter key translation 

586 override_ln_w: Override LN weight tensor. Used for parallel architectures 

587 where MLP reads from ln1 (same as attention) instead of a separate ln2. 

588 override_ln_b: Override LN bias tensor. Used with override_ln_w. 

589 """ 

590 if getattr(cfg, "attn_only", False): 

591 return state_dict 

592 

593 mlp_b_in_key = ProcessWeights._resolve_state_dict_key( 

594 state_dict, ProcessWeights._get_param_key(f"blocks.{layer}.mlp.b_in", adapter), layer 

595 ) 

596 mlp_W_in_key = ProcessWeights._resolve_state_dict_key( 

597 state_dict, ProcessWeights._get_param_key(f"blocks.{layer}.mlp.W_in", adapter), layer 

598 ) 

599 mlp_W_gate_key = ( 

600 ProcessWeights._resolve_state_dict_key( 

601 state_dict, 

602 ProcessWeights._get_param_key(f"blocks.{layer}.mlp.W_gate", adapter), 

603 layer, 

604 ) 

605 if getattr(cfg, "gated_mlp", False) 

606 else None 

607 ) 

608 mlp_b_gate_key = ( 

609 ProcessWeights._resolve_state_dict_key( 

610 state_dict, 

611 ProcessWeights._get_param_key(f"blocks.{layer}.mlp.b_gate", adapter), 

612 layer, 

613 ) 

614 if getattr(cfg, "gated_mlp", False) 

615 else None 

616 ) 

617 

618 # For parallel architectures, ln1 values are passed via override params. 

619 # Otherwise, look up ln2 from the state dict. 

620 ln2_w: Optional[torch.Tensor] 

621 ln2_b: Optional[torch.Tensor] 

622 if override_ln_w is not None: 622 ↛ 623line 622 didn't jump to line 623 because the condition on line 622 was never true

623 ln2_w = override_ln_w 

624 ln2_b = override_ln_b 

625 ln2_w_key = None # No state dict key to zero out (already done by attention folding) 

626 ln2_b_key = None 

627 has_ln = True 

628 else: 

629 ln2_b_key = ProcessWeights._resolve_state_dict_key( 

630 state_dict, ProcessWeights._get_param_key(f"blocks.{layer}.ln2.b", adapter), layer 

631 ) 

632 ln2_w_key = ProcessWeights._resolve_state_dict_key( 

633 state_dict, ProcessWeights._get_param_key(f"blocks.{layer}.ln2.w", adapter), layer 

634 ) 

635 has_ln = ln2_w_key in state_dict 

636 ln2_w = state_dict.get(ln2_w_key) if has_ln else None 

637 ln2_b = state_dict.get(ln2_b_key) if has_ln else None 

638 

639 # CRITICAL FIX: For RMS norm (Gemma), ln2_b doesn't exist. Only require ln2_w! 

640 if has_ln and ln2_w is not None: 

641 # MoE layers: fold ln2 into router gate and each expert's W_in/W_gate 

642 if getattr(cfg, "num_experts", None) is not None and cfg.num_experts > 0: 

643 # MoE: fold into router + experts; skip identity if wrapped 

644 expert_fold_count = 0 

645 expected_expert_folds = cfg.num_experts * 2 # W_in + W_gate per expert 

646 

647 # Fold into router gate 

648 router_key = ProcessWeights._resolve_state_dict_key( 

649 state_dict, f"blocks.{layer}.mlp.W_gate.weight", layer 

650 ) 

651 if router_key in state_dict: 

652 state_dict[router_key] = state_dict[router_key] * ln2_w[None, :] 

653 # Fold into each expert's W_in and W_gate (SwiGLU gate) 

654 for e in range(cfg.num_experts): 

655 for suffix in ("W_in.weight", "W_gate.weight"): 

656 key = ProcessWeights._resolve_state_dict_key( 

657 state_dict, 

658 f"blocks.{layer}.mlp.experts.{e}.{suffix}", 

659 layer, 

660 ) 

661 if key in state_dict: 

662 state_dict[key] = state_dict[key] * ln2_w[None, :] 

663 expert_fold_count += 1 

664 

665 # Only set ln2 to identity if we actually folded into expert weights. 

666 if expert_fold_count > 0: 

667 if ln2_w_key is not None: 667 ↛ 680line 667 didn't jump to line 680 because the condition on line 667 was always true

668 state_dict[ln2_w_key] = torch.ones_like(ln2_w) 

669 alternate_ln2_w_key = ( 

670 ln2_w_key.replace("ln_2", "ln2") 

671 if "ln_2" in ln2_w_key 

672 else ln2_w_key.replace("ln2", "ln_2") 

673 ) 

674 if alternate_ln2_w_key != ln2_w_key and alternate_ln2_w_key in state_dict: 674 ↛ 675line 674 didn't jump to line 675 because the condition on line 674 was never true

675 state_dict[alternate_ln2_w_key] = torch.ones_like(ln2_w) 

676 else: 

677 # No expert weights found — undo router gate fold for consistency. 

678 if router_key in state_dict: 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true

679 state_dict[router_key] = state_dict[router_key] / ln2_w[None, :] 

680 return state_dict 

681 

682 mlp_W_in = ProcessWeights.convert_tensor_to_tl_format( 

683 mlp_W_in_key, state_dict, state_dict.get(mlp_W_in_key), cfg, adapter, layer 

684 ) 

685 assert mlp_W_in is not None, f"MLP W_in not found at key {mlp_W_in_key}" 

686 # rmsnorm_uses_offset: effective scale is (1+w), identity is 0.0 

687 rmsnorm_uses_offset = getattr(cfg, "rmsnorm_uses_offset", False) 

688 effective_ln2_w = (1.0 + ln2_w) if rmsnorm_uses_offset else ln2_w 

689 if mlp_W_in.shape[1] == effective_ln2_w.shape[0]: 

690 ln2_w_broadcast = effective_ln2_w[None, :] 

691 sum_dim = -1 

692 if ln2_b is not None: 

693 ln2_b_broadcast = ln2_b[None, :] 

694 elif mlp_W_in.shape[0] == effective_ln2_w.shape[0]: 694 ↛ 700line 694 didn't jump to line 700 because the condition on line 694 was always true

695 ln2_w_broadcast = effective_ln2_w[:, None] 

696 sum_dim = -2 

697 if ln2_b is not None: 

698 ln2_b_broadcast = ln2_b[:, None] 

699 else: 

700 raise ValueError( 

701 f"Cannot broadcast MLP weight {mlp_W_in.shape} with layer norm weight {effective_ln2_w.shape}" 

702 ) 

703 # Only fold biases if they exist (LayerNorm). RMS norm has no biases. 

704 if fold_biases and ln2_b is not None: 

705 mlp_b_in = ProcessWeights.convert_tensor_to_tl_format( 

706 mlp_b_in_key, state_dict, state_dict.get(mlp_b_in_key), cfg, adapter, layer 

707 ) 

708 ln2_b_folded = (mlp_W_in * ln2_b_broadcast).sum(sum_dim) 

709 if mlp_b_in is not None: 709 ↛ 713line 709 didn't jump to line 713 because the condition on line 709 was always true

710 new_mlp_b_in = mlp_b_in + ln2_b_folded 

711 else: 

712 # MLP has no bias — create one from the folded LN bias 

713 new_mlp_b_in = ln2_b_folded 

714 state_dict[mlp_b_in_key] = ProcessWeights.convert_tensor_to_hf_format( 

715 mlp_b_in_key, new_mlp_b_in, cfg, adapter, layer 

716 ) 

717 # Set ln2.b to zero (skip for parallel override — ln1 already zeroed) 

718 if ln2_b_key is not None: 718 ↛ 727line 718 didn't jump to line 727 because the condition on line 718 was always true

719 state_dict[ln2_b_key] = torch.zeros_like(ln2_b) 

720 alternate_ln2_b_key = ( 

721 ln2_b_key.replace("ln_2", "ln2") 

722 if "ln_2" in ln2_b_key 

723 else ln2_b_key.replace("ln2", "ln_2") 

724 ) 

725 if alternate_ln2_b_key != ln2_b_key and alternate_ln2_b_key in state_dict: 725 ↛ 726line 725 didn't jump to line 726 because the condition on line 725 was never true

726 state_dict[alternate_ln2_b_key] = torch.zeros_like(ln2_b) 

727 new_mlp_W_in = mlp_W_in * ln2_w_broadcast 

728 state_dict[mlp_W_in_key] = ProcessWeights.convert_tensor_to_hf_format( 

729 mlp_W_in_key, new_mlp_W_in, cfg, adapter, layer 

730 ) 

731 if getattr(cfg, "gated_mlp", False) and mlp_W_gate_key is not None: 

732 mlp_W_gate = ProcessWeights.convert_tensor_to_tl_format( 

733 mlp_W_gate_key, state_dict, state_dict.get(mlp_W_gate_key), cfg, adapter, layer 

734 ) 

735 # Combined gate+up (OpenELM): no separate gate, already folded above 

736 if mlp_W_gate is not None: 736 ↛ 761line 736 didn't jump to line 761 because the condition on line 736 was always true

737 new_mlp_W_gate = mlp_W_gate * ln2_w_broadcast 

738 state_dict[mlp_W_gate_key] = ProcessWeights.convert_tensor_to_hf_format( 

739 mlp_W_gate_key, new_mlp_W_gate, cfg, adapter, layer 

740 ) 

741 # Also fold ln2 bias into gate bias (mirrors the in-proj bias folding above) 

742 if fold_biases and ln2_b is not None and mlp_b_gate_key is not None: 

743 mlp_b_gate = ProcessWeights.convert_tensor_to_tl_format( 

744 mlp_b_gate_key, 

745 state_dict, 

746 state_dict.get(mlp_b_gate_key), 

747 cfg, 

748 adapter, 

749 layer, 

750 ) 

751 ln2_b_gate_folded = (mlp_W_gate * ln2_b_broadcast).sum(sum_dim) 

752 if mlp_b_gate is not None: 752 ↛ 753line 752 didn't jump to line 753 because the condition on line 752 was never true

753 new_mlp_b_gate = mlp_b_gate + ln2_b_gate_folded 

754 else: 

755 new_mlp_b_gate = ln2_b_gate_folded 

756 state_dict[mlp_b_gate_key] = ProcessWeights.convert_tensor_to_hf_format( 

757 mlp_b_gate_key, new_mlp_b_gate, cfg, adapter, layer 

758 ) 

759 # After folding, set ln2.w to identity (skip for parallel override — 

760 # ln1 was already set to identity by the attention folding code). 

761 if ln2_w_key is not None: 761 ↛ 773line 761 didn't jump to line 773 because the condition on line 761 was always true

762 identity_ln2 = ( 

763 torch.zeros_like(ln2_w) if rmsnorm_uses_offset else torch.ones_like(ln2_w) 

764 ) 

765 state_dict[ln2_w_key] = identity_ln2 

766 alternate_ln2_w_key = ( 

767 ln2_w_key.replace("ln_2", "ln2") 

768 if "ln_2" in ln2_w_key 

769 else ln2_w_key.replace("ln2", "ln_2") 

770 ) 

771 if alternate_ln2_w_key != ln2_w_key and alternate_ln2_w_key in state_dict: 771 ↛ 772line 771 didn't jump to line 772 because the condition on line 771 was never true

772 state_dict[alternate_ln2_w_key] = identity_ln2 

773 if center_weights and mlp_W_in_key in state_dict: 

774 mlp_W_in_centered = ProcessWeights.convert_tensor_to_tl_format( 

775 mlp_W_in_key, state_dict, state_dict.get(mlp_W_in_key), cfg, adapter, layer 

776 ) 

777 assert mlp_W_in_centered is not None, f"MLP W_in not found at key {mlp_W_in_key}" 

778 # Center along d_model: TL [d_model, d_mlp] or HF [d_mlp, d_model] 

779 d_model = cfg.d_model if cfg is not None else None 

780 if ( 

781 d_model is not None 

782 and mlp_W_in_centered.shape[0] == d_model 

783 and mlp_W_in_centered.shape[-1] != d_model 

784 ): 

785 # TL format [d_model, d_mlp] 

786 mlp_W_in_centered = mlp_W_in_centered - mlp_W_in_centered.mean(0, keepdim=True) 

787 elif ( 787 ↛ 796line 787 didn't jump to line 796 because the condition on line 787 was always true

788 d_model is not None 

789 and mlp_W_in_centered.shape[-1] == d_model 

790 and mlp_W_in_centered.shape[0] != d_model 

791 ): 

792 # HF format [d_mlp, d_model] 

793 mlp_W_in_centered = mlp_W_in_centered - mlp_W_in_centered.mean(-1, keepdim=True) 

794 else: 

795 # Fallback: assume TL format 

796 mlp_W_in_centered = mlp_W_in_centered - mlp_W_in_centered.mean(0, keepdim=True) 

797 state_dict[mlp_W_in_key] = ProcessWeights.convert_tensor_to_hf_format( 

798 mlp_W_in_key, mlp_W_in_centered, cfg, adapter, layer 

799 ) 

800 if getattr(cfg, "act_fn", None) is not None and cfg.act_fn.startswith("solu"): 

801 mlp_b_out_key = ProcessWeights._get_param_key(f"blocks.{layer}.mlp.b_out", adapter) 

802 mlp_W_out_key = ProcessWeights._get_param_key(f"blocks.{layer}.mlp.W_out", adapter) 

803 mlp_ln_b_key = ProcessWeights._get_param_key(f"blocks.{layer}.mlp.ln.b", adapter) 

804 mlp_ln_w_key = ProcessWeights._get_param_key(f"blocks.{layer}.mlp.ln.w", adapter) 

805 

806 mlp_b_out = ProcessWeights.convert_tensor_to_tl_format( 

807 mlp_b_out_key, state_dict, state_dict.get(mlp_b_out_key), cfg, adapter, layer 

808 ) 

809 mlp_W_out = ProcessWeights.convert_tensor_to_tl_format( 

810 mlp_W_out_key, state_dict, state_dict.get(mlp_W_out_key), cfg, adapter, layer 

811 ) 

812 mlp_ln_b = state_dict.get(mlp_ln_b_key) 

813 mlp_ln_w = state_dict.get(mlp_ln_w_key) 

814 assert mlp_b_out is not None, f"MLP b_out not found at key {mlp_b_out_key}" 

815 assert mlp_W_out is not None, f"MLP W_out not found at key {mlp_W_out_key}" 

816 assert mlp_ln_b is not None, f"MLP ln.b not found at key {mlp_ln_b_key}" 

817 assert mlp_ln_w is not None, f"MLP ln.w not found at key {mlp_ln_w_key}" 

818 

819 if fold_biases: 819 ↛ 827line 819 didn't jump to line 827 because the condition on line 819 was always true

820 new_mlp_b_out = mlp_b_out + (mlp_W_out * mlp_ln_b[:, None]).sum(-2) 

821 state_dict[mlp_b_out_key] = ProcessWeights.convert_tensor_to_hf_format( 

822 mlp_b_out_key, new_mlp_b_out, cfg, adapter, layer 

823 ) 

824 if mlp_ln_b_key in state_dict: 824 ↛ 827line 824 didn't jump to line 827 because the condition on line 824 was always true

825 state_dict[mlp_ln_b_key] = torch.zeros_like(mlp_ln_b) 

826 

827 new_mlp_W_out = mlp_W_out * mlp_ln_w[:, None] 

828 

829 if center_weights: 829 ↛ 849line 829 didn't jump to line 849 because the condition on line 829 was always true

830 # Center along d_mlp dimension. Detect format: 

831 # TL format [d_mlp, d_model] -> center along dim=0 

832 # HF format [d_model, d_mlp] -> center along dim=-1 

833 d_model_val = cfg.d_model if cfg is not None else None 

834 if ( 834 ↛ 840line 834 didn't jump to line 840 because the condition on line 834 was always true

835 d_model_val is not None 

836 and new_mlp_W_out.shape[-1] == d_model_val 

837 and new_mlp_W_out.shape[0] != d_model_val 

838 ): 

839 new_mlp_W_out = new_mlp_W_out - new_mlp_W_out.mean(0, keepdim=True) 

840 elif ( 

841 d_model_val is not None 

842 and new_mlp_W_out.shape[0] == d_model_val 

843 and new_mlp_W_out.shape[-1] != d_model_val 

844 ): 

845 new_mlp_W_out = new_mlp_W_out - new_mlp_W_out.mean(-1, keepdim=True) 

846 else: 

847 new_mlp_W_out = new_mlp_W_out - new_mlp_W_out.mean(0, keepdim=True) 

848 

849 state_dict[mlp_W_out_key] = ProcessWeights.convert_tensor_to_hf_format( 

850 mlp_W_out_key, new_mlp_W_out, cfg, adapter, layer 

851 ) 

852 

853 if mlp_ln_w_key in state_dict: 853 ↛ 858line 853 didn't jump to line 858 because the condition on line 853 was always true

854 state_dict[mlp_ln_w_key] = torch.ones_like(mlp_ln_w) 

855 

856 # ln2_post.w (Gemma 2/3): keep original; independent post-MLP normalization 

857 

858 return state_dict 

859 

860 @staticmethod 

861 def _store_processed_attention_tensors( 

862 state_dict: Dict[str, torch.Tensor], 

863 keys: Dict[str, str], 

864 wq_tensor: Optional[torch.Tensor], 

865 wk_tensor: Optional[torch.Tensor], 

866 wv_tensor: Optional[torch.Tensor], 

867 bq_tensor: Optional[torch.Tensor], 

868 bk_tensor: Optional[torch.Tensor], 

869 bv_tensor: Optional[torch.Tensor], 

870 adapter, 

871 cfg, 

872 layer: int, 

873 ) -> Dict[str, torch.Tensor]: 

874 """Store processed attention tensors back to state dict in appropriate format. 

875 

876 Args: 

877 state_dict: The state dictionary to update (modified in place) 

878 keys: Dictionary mapping tensor names to state dict keys 

879 wq_tensor, wk_tensor, wv_tensor: Processed attention weight tensors 

880 bq_tensor, bk_tensor, bv_tensor: Processed attention bias tensors 

881 adapter: Optional architecture adapter for parameter key translation 

882 cfg: Model configuration object 

883 layer: The layer index 

884 """ 

885 if wq_tensor is None: 885 ↛ 886line 885 didn't jump to line 886 because the condition on line 885 was never true

886 return state_dict 

887 wq_key = keys["W_Q"] 

888 wk_key = keys["W_K"] 

889 wv_key = keys["W_V"] 

890 bq_key = keys["b_Q"] 

891 bk_key = keys["b_K"] 

892 bv_key = keys["b_V"] 

893 

894 # Store processed tensors directly in 3D format (set_processed_weights will flatten to 2D) 

895 if wq_tensor is None or wk_tensor is None or wv_tensor is None: 895 ↛ 896line 895 didn't jump to line 896 because the condition on line 895 was never true

896 raise ValueError(f"Required attention weights missing for layer {layer}") 

897 state_dict[wq_key] = ProcessWeights.convert_tensor_to_hf_format( 

898 wq_key, wq_tensor, cfg, adapter, layer_idx=layer 

899 ) 

900 state_dict[wk_key] = ProcessWeights.convert_tensor_to_hf_format( 

901 wk_key, wk_tensor, cfg, adapter, layer_idx=layer 

902 ) 

903 state_dict[wv_key] = ProcessWeights.convert_tensor_to_hf_format( 

904 wv_key, wv_tensor, cfg, adapter, layer_idx=layer 

905 ) 

906 if bq_tensor is not None: 

907 state_dict[bq_key] = ProcessWeights.convert_tensor_to_hf_format( 

908 bq_key, bq_tensor, cfg, adapter, layer_idx=layer 

909 ) 

910 if bk_tensor is not None: 

911 state_dict[bk_key] = ProcessWeights.convert_tensor_to_hf_format( 

912 bk_key, bk_tensor, cfg, adapter, layer_idx=layer 

913 ) 

914 if bv_tensor is not None: 

915 state_dict[bv_key] = ProcessWeights.convert_tensor_to_hf_format( 

916 bv_key, bv_tensor, cfg, adapter, layer_idx=layer 

917 ) 

918 

919 return state_dict 

920 

921 @staticmethod 

922 def _fold_unembed_layer_norm( 

923 state_dict: Dict[str, torch.Tensor], cfg, fold_biases: bool, center_weights: bool, adapter 

924 ) -> Dict[str, torch.Tensor]: 

925 """Fold LayerNorm into unembedding layer. 

926 

927 Args: 

928 state_dict: The state dictionary to process (modified in place) 

929 cfg: Model configuration object 

930 fold_biases: Whether to fold LayerNorm biases 

931 center_weights: Whether to center weights after folding 

932 adapter: Optional architecture adapter for parameter key translation 

933 """ 

934 unembed_b_U_key = ProcessWeights._get_param_key("unembed.b_U", adapter) 

935 unembed_W_U_key = ProcessWeights._get_param_key("unembed.W_U", adapter) 

936 ln_final_b_key = ProcessWeights._get_param_key("ln_final.b", adapter) 

937 ln_final_w_key = ProcessWeights._get_param_key("ln_final.w", adapter) 

938 

939 # Skip layer norm folding if ln_final doesn't exist 

940 # (e.g., encoder-decoder models like T5 have encoder_ln_final/decoder_ln_final instead) 

941 if ln_final_w_key not in state_dict: 941 ↛ 942line 941 didn't jump to line 942 because the condition on line 941 was never true

942 return state_dict 

943 

944 has_unembed_bias = unembed_b_U_key in state_dict 

945 unembed_weight = ProcessWeights.convert_tensor_to_tl_format( 

946 unembed_W_U_key, state_dict, state_dict.get(unembed_W_U_key), cfg, adapter, None 

947 ) 

948 ln_weight = state_dict[ln_final_w_key] 

949 assert unembed_weight is not None, f"Unembed weight not found at key {unembed_W_U_key}" 

950 # rmsnorm_uses_offset: effective scale is (1+w), identity is 0.0 

951 rmsnorm_uses_offset = getattr(cfg, "rmsnorm_uses_offset", False) 

952 effective_ln_weight = (1.0 + ln_weight) if rmsnorm_uses_offset else ln_weight 

953 if len(unembed_weight.shape) == 2 and len(ln_weight.shape) == 1: 953 ↛ 963line 953 didn't jump to line 963 because the condition on line 953 was always true

954 if unembed_weight.shape[1] == ln_weight.shape[0]: 

955 new_unembed_weight = unembed_weight * effective_ln_weight[None, :] 

956 elif unembed_weight.shape[0] == ln_weight.shape[0]: 956 ↛ 959line 956 didn't jump to line 959 because the condition on line 956 was always true

957 new_unembed_weight = unembed_weight * effective_ln_weight[:, None] 

958 else: 

959 raise ValueError( 

960 f"Cannot broadcast unembedding weight {unembed_weight.shape} with layer norm weight {ln_weight.shape}" 

961 ) 

962 else: 

963 raise ValueError( 

964 f"Unexpected tensor shapes: unembedding {unembed_weight.shape}, layer norm {ln_weight.shape}" 

965 ) 

966 state_dict[unembed_W_U_key] = ProcessWeights.convert_tensor_to_hf_format( 

967 unembed_W_U_key, new_unembed_weight, cfg, adapter, None 

968 ) 

969 # Set ln_final.w to identity: zeros (rmsnorm_uses_offset) or ones (standard) 

970 identity_val = ( 

971 torch.zeros_like(ln_weight) if rmsnorm_uses_offset else torch.ones_like(ln_weight) 

972 ) 

973 if ln_final_w_key in state_dict: 973 ↛ 975line 973 didn't jump to line 975 because the condition on line 973 was always true

974 state_dict[ln_final_w_key] = identity_val 

975 alternate_final_w_key = ( 

976 ln_final_w_key.replace("ln_f", "ln_final") 

977 if "ln_f" in ln_final_w_key 

978 else ln_final_w_key.replace("ln_final", "ln_f") 

979 ) 

980 if alternate_final_w_key != ln_final_w_key and alternate_final_w_key in state_dict: 980 ↛ 981line 980 didn't jump to line 981 because the condition on line 980 was never true

981 state_dict[alternate_final_w_key] = identity_val 

982 if center_weights: 

983 unembed_weight_centered = ProcessWeights.convert_tensor_to_tl_format( 

984 unembed_W_U_key, state_dict, state_dict.get(unembed_W_U_key), cfg, adapter, None 

985 ) 

986 assert ( 

987 unembed_weight_centered is not None 

988 ), f"Unembed weight not found at key {unembed_W_U_key}" 

989 if len(unembed_weight_centered.shape) == 2: 989 ↛ 1010line 989 didn't jump to line 1010 because the condition on line 989 was always true

990 # Center along d_model: detect TL vs HF format 

991 d_vocab = getattr(cfg, "d_vocab", None) if cfg is not None else None 

992 if ( 

993 d_vocab is not None 

994 and unembed_weight_centered.shape[0] == d_vocab 

995 and unembed_weight_centered.shape[-1] != d_vocab 

996 ): 

997 # HF format [d_vocab, d_model] — center along dim=-1 

998 unembed_weight_centered = ( 

999 unembed_weight_centered - unembed_weight_centered.mean(-1, keepdim=True) 

1000 ) 

1001 else: 

1002 # TL format [d_model, d_vocab] — center along dim=0 

1003 unembed_weight_centered = ( 

1004 unembed_weight_centered - unembed_weight_centered.mean(0, keepdim=True) 

1005 ) 

1006 state_dict[unembed_W_U_key] = ProcessWeights.convert_tensor_to_hf_format( 

1007 unembed_W_U_key, unembed_weight_centered, cfg, adapter, None 

1008 ) 

1009 else: 

1010 raise ValueError( 

1011 f"Unexpected unembedding weight shape: {unembed_weight_centered.shape}" 

1012 ) 

1013 

1014 return state_dict 

1015 

1016 @staticmethod 

1017 def _fold_final_rms_bias( 

1018 state_dict: Dict[str, torch.Tensor], cfg, fold_biases: bool, adapter 

1019 ) -> Dict[str, torch.Tensor]: 

1020 """Fold final RMS bias into unembedding (separate from regular unembed folding). 

1021 

1022 Args: 

1023 state_dict: The state dictionary to process (modified in place) 

1024 cfg: Model configuration object 

1025 fold_biases: Whether to fold LayerNorm biases 

1026 adapter: Optional architecture adapter for parameter key translation 

1027 """ 

1028 unembed_b_U_key = ProcessWeights._get_param_key("unembed.b_U", adapter) 

1029 unembed_W_U_key = ProcessWeights._get_param_key("unembed.W_U", adapter) 

1030 ln_final_b_key = ProcessWeights._get_param_key("ln_final.b", adapter) 

1031 has_unembed_bias = unembed_b_U_key in state_dict 

1032 has_ln_final_bias = ln_final_b_key in state_dict 

1033 if ( 

1034 not getattr(cfg, "final_rms", False) 

1035 and fold_biases 

1036 and has_unembed_bias 

1037 and has_ln_final_bias 

1038 ): 

1039 unembed_weight = ProcessWeights.convert_tensor_to_tl_format( 

1040 unembed_W_U_key, state_dict, state_dict.get(unembed_W_U_key), cfg, adapter, None 

1041 ) 

1042 ln_bias = state_dict[ln_final_b_key] 

1043 assert unembed_weight is not None, f"Unembed weight not found at key {unembed_W_U_key}" 

1044 if len(unembed_weight.shape) == 2 and len(ln_bias.shape) == 1: 1044 ↛ 1054line 1044 didn't jump to line 1054 because the condition on line 1044 was always true

1045 if unembed_weight.shape[1] == ln_bias.shape[0]: 

1046 bias_contribution = (unembed_weight * ln_bias[None, :]).sum(dim=-1) 

1047 elif unembed_weight.shape[0] == ln_bias.shape[0]: 1047 ↛ 1050line 1047 didn't jump to line 1050 because the condition on line 1047 was always true

1048 bias_contribution = (unembed_weight * ln_bias[:, None]).sum(dim=-2) 

1049 else: 

1050 raise ValueError( 

1051 f"Cannot broadcast unembedding weight {unembed_weight.shape} with layer norm bias {ln_bias.shape}" 

1052 ) 

1053 else: 

1054 raise ValueError( 

1055 f"Unexpected tensor shapes: unembedding {unembed_weight.shape}, layer norm bias {ln_bias.shape}" 

1056 ) 

1057 unembed_b_U = ProcessWeights.convert_tensor_to_tl_format( 

1058 unembed_b_U_key, state_dict, state_dict.get(unembed_b_U_key), cfg, adapter, None 

1059 ) 

1060 assert unembed_b_U is not None, f"Unembed bias not found at key {unembed_b_U_key}" 

1061 new_unembed_b_U = unembed_b_U + bias_contribution 

1062 state_dict[unembed_b_U_key] = ProcessWeights.convert_tensor_to_hf_format( 

1063 unembed_b_U_key, new_unembed_b_U, cfg, adapter, None 

1064 ) 

1065 if ln_final_b_key in state_dict: 1065 ↛ 1067line 1065 didn't jump to line 1067 because the condition on line 1065 was always true

1066 state_dict[ln_final_b_key] = torch.zeros_like(ln_bias) 

1067 alternate_final_b_key = ( 

1068 ln_final_b_key.replace("ln_f", "ln_final") 

1069 if "ln_f" in ln_final_b_key 

1070 else ln_final_b_key.replace("ln_final", "ln_f") 

1071 ) 

1072 if alternate_final_b_key != ln_final_b_key and alternate_final_b_key in state_dict: 1072 ↛ 1073line 1072 didn't jump to line 1073 because the condition on line 1072 was never true

1073 state_dict[alternate_final_b_key] = torch.zeros_like(ln_bias) 

1074 

1075 return state_dict 

1076 

1077 @staticmethod 

1078 def fold_layer_norm( 

1079 state_dict: Dict[str, torch.Tensor], 

1080 cfg, 

1081 fold_biases: bool = True, 

1082 center_weights: bool = True, 

1083 adapter=None, 

1084 ) -> Dict[str, torch.Tensor]: 

1085 """Fold Layer Norm. Can also be used to fold RMS Norm, when fold_biases and center_weights are set to False. 

1086 

1087 Takes in a state dict from a pretrained model, formatted to be consistent with 

1088 HookedTransformer but with LayerNorm weights and biases. Folds these into the neighbouring 

1089 weights. See further_comments.md for more details. 

1090 

1091 Args: 

1092 state_dict (Dict[str, torch.Tensor]): State dict of pretrained model. 

1093 cfg: Model configuration object with n_layers, n_key_value_heads, etc. 

1094 fold_biases (bool): Enables folding of LN biases. Should be disabled when RMS Norm is used. 

1095 center_weights (bool): Enables the centering of weights after folding in LN. Should be disabled when RMS Norm is used. 

1096 adapter: Optional architecture adapter for parameter key translation. 

1097 

1098 Returns: 

1099 Dict[str, torch.Tensor]: Modified state dict with LayerNorm folded into linear layers. 

1100 """ 

1101 # Make a deep copy to avoid modifying the original 

1102 state_dict = { 

1103 k: v.clone() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() 

1104 } 

1105 gqa = "" if getattr(cfg, "n_key_value_heads", None) is None else "_" 

1106 for l in range(cfg.n_layers): 

1107 state_dict = ProcessWeights._fold_layer( 

1108 state_dict, cfg, l, fold_biases, center_weights, adapter, gqa 

1109 ) 

1110 state_dict = ProcessWeights._fold_final_rms_bias(state_dict, cfg, fold_biases, adapter) 

1111 state_dict = ProcessWeights._fold_unembed_layer_norm( 

1112 state_dict, cfg, fold_biases, center_weights, adapter 

1113 ) 

1114 return state_dict 

1115 

1116 @staticmethod 

1117 def center_writing_weights( 

1118 state_dict: Dict[str, torch.Tensor], cfg, adapter=None 

1119 ) -> Dict[str, torch.Tensor]: 

1120 """Center Writing Weights. 

1121 

1122 Centers the weights of the model that write to the residual stream - W_out, W_E, W_pos and 

1123 W_out. This is done by subtracting the mean of the weights from the weights themselves. This 

1124 is done in-place. See fold_layer_norm for more details. 

1125 

1126 Args: 

1127 state_dict (Dict[str, torch.Tensor]): State dict of the model. 

1128 cfg: Model configuration object. 

1129 adapter: Optional architecture adapter for parameter key translation. 

1130 

1131 Returns: 

1132 Dict[str, torch.Tensor]: Modified state dict with centered writing weights. 

1133 """ 

1134 # Post-norm models leave the first attention's input un-normed, so centering 

1135 # the embedding would shift a residual stream nothing re-normalizes. 

1136 architecture = getattr(cfg, "original_architecture", None) 

1137 if architecture in POST_NORM_ARCHITECTURES: 

1138 print(f"Not centering embedding weights for {architecture}") 

1139 else: 

1140 # Make a deep copy to avoid modifying the original 

1141 embed_W_E_key = ProcessWeights._get_param_key("embed.W_E", adapter) 

1142 try: 

1143 pos_embed_W_pos_key = ( 

1144 ProcessWeights._get_param_key("pos_embed.W_pos", adapter) 

1145 if getattr(cfg, "positional_embedding_type", "standard") 

1146 not in ("rotary", "alibi", "none") 

1147 else None 

1148 ) 

1149 except ValueError: 

1150 pos_embed_W_pos_key = None 

1151 if embed_W_E_key not in state_dict: 1151 ↛ 1152line 1151 didn't jump to line 1152 because the condition on line 1151 was never true

1152 raise KeyError( 

1153 f"Expected embedding key '{embed_W_E_key}' not found in state_dict. Available keys: {list(state_dict.keys())[:10]}..." 

1154 ) 

1155 embed_W_E = ProcessWeights.convert_tensor_to_tl_format( 

1156 embed_W_E_key, state_dict, state_dict.get(embed_W_E_key), cfg, adapter, None 

1157 ) 

1158 assert embed_W_E is not None, f"Embedding not found at key {embed_W_E_key}" 

1159 embed_W_E = embed_W_E - embed_W_E.mean(-1, keepdim=True) 

1160 state_dict[embed_W_E_key] = ProcessWeights.convert_tensor_to_hf_format( 

1161 embed_W_E_key, embed_W_E, cfg, adapter, None 

1162 ) 

1163 

1164 if ( 

1165 getattr(cfg, "positional_embedding_type", "standard") 

1166 not in ("rotary", "alibi", "none") 

1167 and pos_embed_W_pos_key is not None 

1168 ): 

1169 if pos_embed_W_pos_key not in state_dict: 1169 ↛ 1170line 1169 didn't jump to line 1170 because the condition on line 1169 was never true

1170 raise KeyError( 

1171 f"Expected positional embedding key '{pos_embed_W_pos_key}' not found in state_dict. Available keys: {list(state_dict.keys())[:10]}..." 

1172 ) 

1173 pos_embed_W_pos = ProcessWeights.convert_tensor_to_tl_format( 

1174 pos_embed_W_pos_key, 

1175 state_dict, 

1176 state_dict.get(pos_embed_W_pos_key), 

1177 cfg, 

1178 adapter, 

1179 None, 

1180 ) 

1181 assert ( 

1182 pos_embed_W_pos is not None 

1183 ), f"Positional embedding not found at key {pos_embed_W_pos_key}" 

1184 pos_embed_W_pos = pos_embed_W_pos - pos_embed_W_pos.mean(-1, keepdim=True) 

1185 state_dict[pos_embed_W_pos_key] = ProcessWeights.convert_tensor_to_hf_format( 

1186 pos_embed_W_pos_key, pos_embed_W_pos, cfg, adapter, None 

1187 ) 

1188 for l in range(cfg.n_layers): 

1189 attn_W_O_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.W_O", adapter) 

1190 attn_b_O_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.b_O", adapter) 

1191 try: 

1192 mlp_W_out_key = ProcessWeights._resolve_state_dict_key( 

1193 state_dict, ProcessWeights._get_param_key(f"blocks.{l}.mlp.W_out", adapter), l 

1194 ) 

1195 mlp_b_out_key = ProcessWeights._resolve_state_dict_key( 

1196 state_dict, ProcessWeights._get_param_key(f"blocks.{l}.mlp.b_out", adapter), l 

1197 ) 

1198 except ValueError: 

1199 mlp_W_out_key = None 

1200 mlp_b_out_key = None 

1201 if attn_W_O_key in state_dict: 1201 ↛ 1219line 1201 didn't jump to line 1219 because the condition on line 1201 was always true

1202 attn_W_O = ProcessWeights.convert_tensor_to_tl_format( 

1203 attn_W_O_key, state_dict, state_dict.get(attn_W_O_key), cfg, adapter, l 

1204 ) 

1205 assert attn_W_O is not None, f"Attention W_O not found at key {attn_W_O_key}" 

1206 attn_W_O = attn_W_O - attn_W_O.mean(-1, keepdim=True) 

1207 state_dict[attn_W_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1208 attn_W_O_key, attn_W_O, cfg, adapter, l 

1209 ) 

1210 if attn_b_O_key in state_dict: 1210 ↛ 1219line 1210 didn't jump to line 1219 because the condition on line 1210 was always true

1211 attn_b_O = ProcessWeights.convert_tensor_to_tl_format( 

1212 attn_b_O_key, state_dict, state_dict.get(attn_b_O_key), cfg, adapter, l 

1213 ) 

1214 assert attn_b_O is not None, f"Attention b_O not found at key {attn_b_O_key}" 

1215 attn_b_O = attn_b_O - attn_b_O.mean() 

1216 state_dict[attn_b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1217 attn_b_O_key, attn_b_O, cfg, adapter, l 

1218 ) 

1219 if not getattr(cfg, "attn_only", False): 

1220 is_moe = getattr(cfg, "num_experts", None) is not None and cfg.num_experts > 0 

1221 if is_moe: 

1222 num_experts = cfg.num_experts 

1223 for e in range(num_experts): 

1224 expert_W_out_key = None 

1225 expert_b_out_key = None 

1226 expert_W_out_patterns = [ 

1227 f"blocks.{l}.mlp.experts.{e}.W_out", 

1228 f"blocks.{l}.mlp.experts.{e}.W_out.weight", 

1229 ] 

1230 for pattern in expert_W_out_patterns: 

1231 if pattern in state_dict: 1231 ↛ 1232line 1231 didn't jump to line 1232 because the condition on line 1231 was never true

1232 expert_W_out_key = pattern 

1233 break 

1234 if expert_W_out_key is None and adapter: 1234 ↛ 1235line 1234 didn't jump to line 1235 because the condition on line 1234 was never true

1235 try: 

1236 candidate = ProcessWeights._get_param_key( 

1237 f"blocks.{l}.mlp.experts.{e}.W_out", adapter 

1238 ) 

1239 expert_W_out_key = ProcessWeights._resolve_state_dict_key( 

1240 state_dict, candidate, l 

1241 ) 

1242 except ValueError: 

1243 pass 

1244 if expert_W_out_key and expert_W_out_key in state_dict: 1244 ↛ 1245line 1244 didn't jump to line 1245 because the condition on line 1244 was never true

1245 expert_W_out = ProcessWeights.convert_tensor_to_tl_format( 

1246 expert_W_out_key, 

1247 state_dict, 

1248 state_dict.get(expert_W_out_key), 

1249 cfg, 

1250 adapter, 

1251 l, 

1252 ) 

1253 assert ( 

1254 expert_W_out is not None 

1255 ), f"Expert W_out not found at key {expert_W_out_key}" 

1256 expert_W_out = expert_W_out - expert_W_out.mean(-1, keepdim=True) 

1257 state_dict[ 

1258 expert_W_out_key 

1259 ] = ProcessWeights.convert_tensor_to_hf_format( 

1260 expert_W_out_key, expert_W_out, cfg, adapter, l 

1261 ) 

1262 expert_b_out_patterns = [ 

1263 f"blocks.{l}.mlp.experts.{e}.b_out", 

1264 f"blocks.{l}.mlp.experts.{e}.b_out.bias", 

1265 ] 

1266 for pattern in expert_b_out_patterns: 

1267 if pattern in state_dict: 1267 ↛ 1268line 1267 didn't jump to line 1268 because the condition on line 1267 was never true

1268 expert_b_out_key = pattern 

1269 break 

1270 if expert_b_out_key is None and adapter: 1270 ↛ 1271line 1270 didn't jump to line 1271 because the condition on line 1270 was never true

1271 try: 

1272 candidate = ProcessWeights._get_param_key( 

1273 f"blocks.{l}.mlp.experts.{e}.b_out", adapter 

1274 ) 

1275 expert_b_out_key = ProcessWeights._resolve_state_dict_key( 

1276 state_dict, candidate, l 

1277 ) 

1278 except ValueError: 

1279 pass 

1280 if expert_b_out_key and expert_b_out_key in state_dict: 1280 ↛ 1281line 1280 didn't jump to line 1281 because the condition on line 1280 was never true

1281 expert_b_out = ProcessWeights.convert_tensor_to_tl_format( 

1282 expert_b_out_key, 

1283 state_dict, 

1284 state_dict.get(expert_b_out_key), 

1285 cfg, 

1286 adapter, 

1287 l, 

1288 ) 

1289 assert ( 

1290 expert_b_out is not None 

1291 ), f"Expert b_out not found at key {expert_b_out_key}" 

1292 expert_b_out = expert_b_out - expert_b_out.mean() 

1293 state_dict[ 

1294 expert_b_out_key 

1295 ] = ProcessWeights.convert_tensor_to_hf_format( 

1296 expert_b_out_key, expert_b_out, cfg, adapter, l 

1297 ) 

1298 elif mlp_W_out_key is not None and mlp_W_out_key in state_dict: 1298 ↛ 1188line 1298 didn't jump to line 1188 because the condition on line 1298 was always true

1299 mlp_W_out = ProcessWeights.convert_tensor_to_tl_format( 

1300 mlp_W_out_key, state_dict, state_dict.get(mlp_W_out_key), cfg, adapter, l 

1301 ) 

1302 assert mlp_W_out is not None, f"MLP W_out not found at key {mlp_W_out_key}" 

1303 # Center along d_model dimension. In TL format W_out is [d_mlp, d_model] 

1304 # so d_model is dim=-1. But bridge adapters may keep HF format 

1305 # [d_model, d_mlp] where d_model is dim=0. Detect via cfg.d_model. 

1306 if mlp_W_out.shape[-1] == cfg.d_model: 

1307 mlp_W_out = mlp_W_out - mlp_W_out.mean(-1, keepdim=True) 

1308 elif mlp_W_out.shape[0] == cfg.d_model: 1308 ↛ 1311line 1308 didn't jump to line 1311 because the condition on line 1308 was always true

1309 mlp_W_out = mlp_W_out - mlp_W_out.mean(0, keepdim=True) 

1310 else: 

1311 mlp_W_out = mlp_W_out - mlp_W_out.mean(-1, keepdim=True) 

1312 state_dict[mlp_W_out_key] = ProcessWeights.convert_tensor_to_hf_format( 

1313 mlp_W_out_key, mlp_W_out, cfg, adapter, l 

1314 ) 

1315 if mlp_b_out_key is not None and mlp_b_out_key in state_dict: 1315 ↛ 1188line 1315 didn't jump to line 1188 because the condition on line 1315 was always true

1316 mlp_b_out = ProcessWeights.convert_tensor_to_tl_format( 

1317 mlp_b_out_key, 

1318 state_dict, 

1319 state_dict.get(mlp_b_out_key), 

1320 cfg, 

1321 adapter, 

1322 l, 

1323 ) 

1324 assert mlp_b_out is not None, f"MLP b_out not found at key {mlp_b_out_key}" 

1325 mlp_b_out = mlp_b_out - mlp_b_out.mean() 

1326 state_dict[mlp_b_out_key] = ProcessWeights.convert_tensor_to_hf_format( 

1327 mlp_b_out_key, mlp_b_out, cfg, adapter, l 

1328 ) 

1329 return state_dict 

1330 

1331 @staticmethod 

1332 def center_unembed( 

1333 state_dict: Dict[str, torch.Tensor], cfg=None, adapter=None 

1334 ) -> Dict[str, torch.Tensor]: 

1335 """Center the unembedding weights W_U. 

1336 

1337 This is done by subtracting the mean of the weights from the weights themselves. This is 

1338 done in-place. As softmax is translation invariant, this changes the logits but not the log 

1339 probs, and makes the model logits (slightly) more interpretable - when trying to understand 

1340 how components contribute to the logits, we'll be less misled by components that just add 

1341 something to every logit. 

1342 

1343 Args: 

1344 state_dict (Dict[str, torch.Tensor]): State dict of the model. 

1345 cfg: Model configuration (used to determine d_vocab for correct centering dimension). 

1346 adapter: Optional architecture adapter for parameter key translation. 

1347 

1348 Returns: 

1349 Dict[str, torch.Tensor]: Modified state dict with centered unembedding weights. 

1350 """ 

1351 # Make a deep copy to avoid modifying the original 

1352 state_dict = { 

1353 k: v.clone() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() 

1354 } 

1355 unembed_W_U_key = ProcessWeights._get_param_key("unembed.W_U", adapter) 

1356 unembed_b_U_key = ProcessWeights._get_param_key("unembed.b_U", adapter) 

1357 if unembed_W_U_key not in state_dict: 

1358 raise KeyError( 

1359 f"Expected unembedding weight key '{unembed_W_U_key}' not found in state_dict. Available keys: {list(state_dict.keys())[:10]}..." 

1360 ) 

1361 W_U = ProcessWeights.convert_tensor_to_tl_format( 

1362 unembed_W_U_key, state_dict, state_dict.get(unembed_W_U_key), None, adapter, None 

1363 ) 

1364 assert W_U is not None, f"Unembed weight not found at key {unembed_W_U_key}" 

1365 

1366 # Detect W_U format to center along correct dim (wrong dim corrupts output) 

1367 vocab_dim = -1 # Default: TL format [d_model, d_vocab] 

1368 if cfg is not None: 

1369 d_vocab = getattr(cfg, "d_vocab", None) 

1370 d_model = getattr(cfg, "d_model", None) 

1371 if d_vocab is not None and W_U.shape[0] == d_vocab and W_U.shape[-1] != d_vocab: 

1372 # HF format [d_vocab, d_model] — center along dim=0 

1373 vocab_dim = 0 

1374 elif d_model is not None and W_U.shape[-1] == d_model and W_U.shape[0] != d_model: 

1375 # Padded-vocab checkpoints (e.g. HyenaDNA pads 12→16 rows) defeat 

1376 # the d_vocab match; the d_model axis is unambiguous. 

1377 vocab_dim = 0 

1378 W_U = W_U - W_U.mean(vocab_dim, keepdim=True) 

1379 state_dict[unembed_W_U_key] = ProcessWeights.convert_tensor_to_hf_format( 

1380 unembed_W_U_key, W_U, None, adapter, None 

1381 ) 

1382 if unembed_b_U_key in state_dict: 

1383 unembed_b_U = ProcessWeights.convert_tensor_to_tl_format( 

1384 unembed_b_U_key, state_dict, state_dict.get(unembed_b_U_key), None, adapter, None 

1385 ) 

1386 assert unembed_b_U is not None, f"Unembed bias not found at key {unembed_b_U_key}" 

1387 unembed_b_U = unembed_b_U - unembed_b_U.mean() 

1388 state_dict[unembed_b_U_key] = ProcessWeights.convert_tensor_to_hf_format( 

1389 unembed_b_U_key, unembed_b_U, None, adapter, None 

1390 ) 

1391 return state_dict 

1392 

1393 @staticmethod 

1394 def fold_value_biases( 

1395 state_dict: Dict[str, torch.Tensor], cfg, adapter=None 

1396 ) -> Dict[str, torch.Tensor]: 

1397 """Fold the value biases into the output bias. 

1398 

1399 Because attention patterns add up to 1, the value biases always have a constant effect on a 

1400 head's output. Further, as the outputs of each head in a layer add together, each head's 

1401 value bias has a constant effect on the *layer's* output, which can make it harder to 

1402 interpret the effect of any given head, and it doesn't matter which head a bias is 

1403 associated with. We can factor this all into a single output bias to the layer, and make it 

1404 easier to interpret the head's output. Formally, we take b_O_new = b_O_original + 

1405 sum_head(b_V_head @ W_O_head). 

1406 

1407 Args: 

1408 state_dict (Dict[str, torch.Tensor]): State dict of the model. 

1409 cfg: Model configuration object. 

1410 adapter: Optional architecture adapter for parameter key translation. 

1411 

1412 Returns: 

1413 Dict[str, torch.Tensor]: Modified state dict with value biases folded into output bias. 

1414 """ 

1415 # Make a deep copy to avoid modifying the original 

1416 state_dict = { 

1417 k: v.clone() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() 

1418 } 

1419 layer = 0 

1420 for layer in range(cfg.n_layers): 

1421 split_v_bias_key = f"blocks.{layer}.attn.v.bias" 

1422 if split_v_bias_key in state_dict: 

1423 b_V_key = split_v_bias_key 

1424 W_O_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.W_O", adapter) 

1425 b_O_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_O", adapter) 

1426 else: 

1427 if getattr(cfg, "n_key_value_heads", None) is None: 

1428 b_V_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_V", adapter) 

1429 else: 

1430 b_V_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn._b_V", adapter) 

1431 W_O_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.W_O", adapter) 

1432 b_O_key = ProcessWeights._get_param_key(f"blocks.{layer}.attn.b_O", adapter) 

1433 if b_V_key in state_dict: 

1434 b_V = ProcessWeights.convert_tensor_to_tl_format( 

1435 b_V_key, state_dict, state_dict.get(b_V_key), cfg, adapter, layer 

1436 ) 

1437 assert b_V is not None, f"Value bias not found at key {b_V_key}" 

1438 if b_V.numel() == 0: 1438 ↛ 1439line 1438 didn't jump to line 1439 because the condition on line 1438 was never true

1439 continue 

1440 W_O = ProcessWeights.convert_tensor_to_tl_format( 

1441 W_O_key, state_dict, state_dict.get(W_O_key), cfg, adapter, layer 

1442 ) 

1443 assert W_O is not None, f"Attention W_O not found at key {W_O_key}" 

1444 if b_O_key not in state_dict: 

1445 # Create zero b_O to absorb the folded value bias 

1446 b_O_original = torch.zeros(cfg.d_model, dtype=b_V.dtype, device=b_V.device) 

1447 state_dict[b_O_key] = b_O_original 

1448 else: 

1449 b_O_original_maybe = ProcessWeights.convert_tensor_to_tl_format( 

1450 b_O_key, state_dict, state_dict.get(b_O_key), cfg, adapter, layer 

1451 ) 

1452 assert ( 

1453 b_O_original_maybe is not None 

1454 ), f"Attention b_O not found at key {b_O_key}" 

1455 b_O_original = b_O_original_maybe 

1456 # Align W_O / b_O to b_V's device. 

1457 if W_O.device != b_V.device: 1457 ↛ 1458line 1457 didn't jump to line 1458 because the condition on line 1457 was never true

1458 W_O = W_O.to(b_V.device) 

1459 if b_O_original.device != b_V.device: 1459 ↛ 1460line 1459 didn't jump to line 1460 because the condition on line 1459 was never true

1460 b_O_original = b_O_original.to(b_V.device) 

1461 is_split_format = ".attn.v.bias" in b_V_key or ".attn.k.bias" in b_V_key 

1462 if is_split_format and len(b_V.shape) == 1 and (len(W_O.shape) == 2): 1462 ↛ 1463line 1462 didn't jump to line 1463 because the condition on line 1462 was never true

1463 n_heads = cfg.n_heads 

1464 d_head = cfg.d_head 

1465 d_model = cfg.d_model 

1466 b_V_only = b_V 

1467 b_V_reshaped = b_V_only.reshape(n_heads, d_head) 

1468 W_O_reshaped = einops.rearrange(W_O, "(i h) m -> i h m", i=n_heads) 

1469 folded_b_O = b_O_original + (b_V_reshaped[:, :, None] * W_O_reshaped).sum( 

1470 [0, 1] 

1471 ) 

1472 state_dict[b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1473 b_O_key, folded_b_O, cfg, adapter, layer 

1474 ) 

1475 tl_b_O_key = f"blocks.{layer}.attn.b_O" 

1476 if tl_b_O_key in state_dict: 

1477 state_dict[tl_b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1478 tl_b_O_key, folded_b_O, cfg, adapter, layer 

1479 ) 

1480 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1481 b_V_key, torch.zeros_like(b_V), cfg, adapter, layer 

1482 ) 

1483 elif len(b_V.shape) == 1 and len(W_O.shape) == 2: 1483 ↛ 1484line 1483 didn't jump to line 1484 because the condition on line 1483 was never true

1484 n_heads = cfg.n_heads 

1485 d_head = cfg.d_head 

1486 d_model = cfg.d_model 

1487 v_bias_start = 2 * n_heads * d_head 

1488 v_bias_end = 3 * n_heads * d_head 

1489 b_V_only = b_V[v_bias_start:v_bias_end] 

1490 if b_V_only.numel() == 0: 

1491 continue 

1492 b_V_reshaped = b_V_only.reshape(n_heads, d_head) 

1493 W_O_reshaped = einops.rearrange(W_O, "(i h) m -> i h m", i=n_heads) 

1494 folded_b_O = b_O_original + (b_V_reshaped[:, :, None] * W_O_reshaped).sum( 

1495 [0, 1] 

1496 ) 

1497 new_b_V = b_V.clone() 

1498 new_b_V[v_bias_start:v_bias_end] = 0 

1499 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1500 b_V_key, new_b_V, cfg, adapter, layer 

1501 ) 

1502 elif is_split_format and len(b_V.shape) == 1 and len(W_O.shape) == 3: 1502 ↛ 1504line 1502 didn't jump to line 1504 because the condition on line 1502 was never true

1503 # Split bias [n_heads * d_head] with W_O already in TL format [n_heads, d_head, d_model] 

1504 n_heads = cfg.n_heads 

1505 d_head = cfg.d_head 

1506 b_V_reshaped = b_V.reshape(n_heads, d_head) 

1507 if getattr(cfg, "n_key_value_heads", None) is not None: 

1508 b_V_reshaped = torch.repeat_interleave( 

1509 b_V_reshaped, dim=0, repeats=cfg.n_heads // cfg.n_key_value_heads 

1510 ) 

1511 folded_b_O = b_O_original + (b_V_reshaped[:, :, None] * W_O).sum([0, 1]) 

1512 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1513 b_V_key, torch.zeros_like(b_V), cfg, adapter, layer 

1514 ) 

1515 elif len(b_V.shape) == 2 and len(W_O.shape) == 3: 1515 ↛ 1529line 1515 didn't jump to line 1529 because the condition on line 1515 was always true

1516 b_V_original_shape = b_V.shape 

1517 if getattr(cfg, "n_key_value_heads", None) is not None: 

1518 b_V = torch.repeat_interleave( 

1519 b_V, dim=0, repeats=cfg.n_heads // cfg.n_key_value_heads 

1520 ) 

1521 folded_b_O = b_O_original + (b_V[:, :, None] * W_O).sum([0, 1]) 

1522 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1523 b_V_key, 

1524 torch.zeros(b_V_original_shape, dtype=b_V.dtype, device=b_V.device), 

1525 cfg, 

1526 adapter, 

1527 layer, 

1528 ) 

1529 elif len(b_V.shape) == 2 and len(W_O.shape) == 2: 

1530 n_heads = cfg.n_heads 

1531 d_head = cfg.d_head 

1532 d_model = cfg.d_model 

1533 b_V_original_shape = b_V.shape 

1534 

1535 # Handle split QKV format where bias might be [1, d_model] or [n_heads, d_head] 

1536 is_split_format = ".attn.v.bias" in b_V_key or ".attn.k.bias" in b_V_key 

1537 if is_split_format and b_V.shape[0] == 1 and b_V.shape[1] == n_heads * d_head: 

1538 # Reshape [1, n_heads * d_head] to [n_heads, d_head] 

1539 b_V = b_V.reshape(n_heads, d_head) 

1540 elif b_V.shape != (n_heads, d_head): 

1541 # If not already [n_heads, d_head], try to reshape 

1542 if b_V.numel() == n_heads * d_head: 

1543 b_V = b_V.reshape(n_heads, d_head) 

1544 

1545 if getattr(cfg, "n_key_value_heads", None) is not None: 

1546 b_V = torch.repeat_interleave( 

1547 b_V, dim=0, repeats=cfg.n_heads // cfg.n_key_value_heads 

1548 ) 

1549 

1550 W_O_reshaped = einops.rearrange(W_O, "(i h) m -> i h m", i=n_heads) 

1551 folded_b_O = b_O_original + (b_V[:, :, None] * W_O_reshaped).sum([0, 1]) 

1552 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1553 b_V_key, 

1554 torch.zeros(b_V_original_shape, dtype=b_V.dtype, device=b_V.device), 

1555 cfg, 

1556 adapter, 

1557 layer, 

1558 ) 

1559 else: 

1560 raise ValueError(f"Unexpected tensor shapes: b_V {b_V.shape}, W_O {W_O.shape}") 

1561 state_dict[b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1562 b_O_key, folded_b_O, cfg, adapter, layer 

1563 ) 

1564 return state_dict 

1565 

1566 @staticmethod 

1567 def process_weights( 

1568 state_dict: Dict[str, torch.Tensor], 

1569 cfg, 

1570 fold_ln: bool = True, 

1571 center_writing_weights: bool = True, 

1572 center_unembed: bool = True, 

1573 fold_value_biases: bool = True, 

1574 refactor_factored_attn_matrices: bool = False, 

1575 adapter=None, 

1576 ) -> Dict[str, torch.Tensor]: 

1577 """Apply all weight processing transformations in the correct order. 

1578 

1579 This is a convenience function that applies all the weight processing steps 

1580 in the same order as HookedTransformer.load_and_process_state_dict(). 

1581 

1582 Args: 

1583 state_dict (Dict[str, torch.Tensor]): State dict of the model. 

1584 cfg: Model configuration object. 

1585 fold_ln (bool): Whether to fold LayerNorm weights into subsequent layers. 

1586 center_writing_weights (bool): Whether to center weights writing to residual stream. 

1587 center_unembed (bool): Whether to center unembedding weights. 

1588 fold_value_biases (bool): Whether to fold value biases into output bias. 

1589 refactor_factored_attn_matrices (bool): Whether to refactor attention matrices. 

1590 adapter: Optional architecture adapter for parameter key translation. 

1591 

1592 Returns: 

1593 Dict[str, torch.Tensor]: Fully processed state dict. 

1594 """ 

1595 # Upcast to float32 for weight processing to avoid precision loss in 

1596 # reduced-precision dtypes (bfloat16, float16). Operations like LayerNorm 

1597 # folding involve multiplications that accumulate rounding errors when 

1598 # performed in low precision. 

1599 original_dtypes: Dict[str, torch.dtype] = {} 

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

1601 if isinstance(v, torch.Tensor) and v.is_floating_point() and v.dtype != torch.float32: 1601 ↛ 1602line 1601 didn't jump to line 1602 because the condition on line 1601 was never true

1602 original_dtypes[k] = v.dtype 

1603 state_dict[k] = v.float() 

1604 

1605 # Skip fold_ln for adapters that don't support it (e.g., post-LN architectures 

1606 # like BERT where LN placement means folding goes into the wrong sublayer). 

1607 if fold_ln and adapter and not getattr(adapter, "supports_fold_ln", True): 

1608 import warnings 

1609 

1610 warnings.warn( 

1611 f"{type(adapter).__name__} does not support fold_ln; norm weights " 

1612 "stay unfolded and analyses assuming folded LN will be off.", 

1613 UserWarning, 

1614 ) 

1615 fold_ln = False 

1616 if fold_ln: 

1617 if getattr(cfg, "normalization_type", "LN") in ["LN", "LNPre"]: 

1618 state_dict = ProcessWeights.fold_layer_norm( 

1619 state_dict, cfg, fold_biases=True, center_weights=True, adapter=adapter 

1620 ) 

1621 elif getattr(cfg, "normalization_type", "LN") in ["RMS", "RMSPre"]: 1621 ↛ 1632line 1621 didn't jump to line 1632 because the condition on line 1621 was always true

1622 state_dict = ProcessWeights.fold_layer_norm( 

1623 state_dict, cfg, fold_biases=False, center_weights=False, adapter=adapter 

1624 ) 

1625 # Note: Each folding function (_fold_layer for attention, _fold_mlp_layer_norm 

1626 # for MLP) sets its own LN weights to 1.0 after successful folding. 

1627 # We must NOT unconditionally set all LN weights to 1.0 here, because 

1628 # models with combined QKV projections (e.g., OpenELM's qkv_proj) may 

1629 # not be able to fold attention LN — setting ln1.w=1.0 without folding 

1630 # destroys the RMS scaling. 

1631 # Some adapters (e.g., post-LN) don't support center_writing_weights. 

1632 if ( 1632 ↛ 1637line 1632 didn't jump to line 1637 because the condition on line 1632 was never true

1633 center_writing_weights 

1634 and adapter 

1635 and not getattr(adapter, "supports_center_writing_weights", True) 

1636 ): 

1637 import warnings 

1638 

1639 warnings.warn( 

1640 f"{type(adapter).__name__} does not support center_writing_weights; " 

1641 "writing weights stay uncentered.", 

1642 UserWarning, 

1643 ) 

1644 center_writing_weights = False 

1645 if center_writing_weights: 

1646 if getattr(cfg, "normalization_type", "LN") in ["LN", "LNPre"] and ( 

1647 not getattr(cfg, "final_rms", False) 

1648 ): 

1649 state_dict = ProcessWeights.center_writing_weights(state_dict, cfg, adapter=adapter) 

1650 if center_unembed: 

1651 state_dict = ProcessWeights.center_unembed(state_dict, cfg=cfg, adapter=adapter) 

1652 if fold_value_biases: 

1653 state_dict = ProcessWeights.fold_value_biases(state_dict, cfg, adapter=adapter) 

1654 if center_writing_weights and getattr(cfg, "normalization_type", "LN") in [ 

1655 "LN", 

1656 "LNPre", 

1657 ]: 

1658 for layer_idx in range(cfg.n_layers): 

1659 b_O_key = ProcessWeights._get_param_key(f"blocks.{layer_idx}.attn.b_O", adapter) 

1660 if b_O_key in state_dict: 1660 ↛ 1658line 1660 didn't jump to line 1658 because the condition on line 1660 was always true

1661 b_O = ProcessWeights.convert_tensor_to_tl_format( 

1662 b_O_key, state_dict, state_dict.get(b_O_key), cfg, adapter, layer_idx 

1663 ) 

1664 assert b_O is not None, f"Attention b_O not found at key {b_O_key}" 

1665 b_O = b_O - b_O.mean() 

1666 state_dict[b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1667 b_O_key, b_O, cfg, adapter, layer_idx 

1668 ) 

1669 if refactor_factored_attn_matrices: 

1670 state_dict = ProcessWeights.refactor_factored_attn_matrices( 

1671 state_dict, cfg, adapter=adapter 

1672 ) 

1673 

1674 # Downcast back to original dtypes 

1675 for k, orig_dtype in original_dtypes.items(): 1675 ↛ 1676line 1675 didn't jump to line 1676 because the loop on line 1675 never started

1676 if k in state_dict and isinstance(state_dict[k], torch.Tensor): 

1677 state_dict[k] = state_dict[k].to(orig_dtype) 

1678 

1679 return state_dict 

1680 

1681 @staticmethod 

1682 def refactor_factored_attn_matrices( 

1683 state_dict: Dict[str, torch.Tensor], cfg, adapter=None 

1684 ) -> Dict[str, torch.Tensor]: 

1685 """Experimental method for managing queries, keys and values. 

1686 

1687 As argued in [A Mathematical Framework for Transformer 

1688 Circuits](https://transformer-circuits.pub/2021/framework/index.html), queries, keys and 

1689 values are somewhat arbitrary intermediate terms when computing with the low rank factored 

1690 matrices W_QK = W_Q @ W_K.T and W_OV = W_V @ W_O, and these matrices are the only thing 

1691 determining head behaviour. But there are many ways to find a low rank factorization to a 

1692 given matrix, and hopefully some of these are more interpretable than others! This method is 

1693 one attempt, which makes all of the matrices have orthogonal rows or columns, W_O into a 

1694 rotation and W_Q and W_K having the nth column in each having the same norm. The formula is 

1695 $W_V = U @ S,W_O=Vh.T,W_Q=U@S.sqrt(),W_K=Vh@S.sqrt()$. 

1696 

1697 More details: 

1698 

1699 If W_OV = U @ S @ Vh.T in its singular value decomposition, (where S is in R^d_head not 

1700 R^d_model, as W_OV is low rank), W_OV = (U @ S) @ (Vh.T) is an equivalent low rank 

1701 factorisation, where rows/columns of each matrix are orthogonal! So setting $W_V=US$ and 

1702 $W_O=Vh.T$ works just as well. I *think* this is a more interpretable setup, because now 

1703 $W_O$ is just a rotation, and doesn't change the norm, so $z$ has the same norm as the 

1704 result of the head. 

1705 

1706 For $W_QK = W_Q @ W_K.T$ we use the refactor $W_Q = U @ S.sqrt()$ and $W_K = Vh @ S.sqrt()$, 

1707 which is also equivalent ($S==S.sqrt() @ S.sqrt()$ as $S$ is diagonal). Here we keep the 

1708 matrices as having the same norm, since there's not an obvious asymmetry between the keys 

1709 and queries. 

1710 

1711 Biases are more fiddly to deal with. For OV it's pretty easy - we just need (x @ W_V + b_V) 

1712 @ W_O + b_O to be preserved, so we can set b_V' = 0. and b_O' = b_V @ W_O + b_O (note that 

1713 b_V in R^{head_index x d_head} while b_O in R^{d_model}, so we need to sum b_V @ W_O along 

1714 the head_index dimension too). 

1715 

1716 For QK it's messy - we need to preserve the bilinear form of (x @ W_Q + b_Q) * (y @ W_K + 

1717 b_K), which is fairly messy. To deal with the biases, we concatenate them to W_Q and W_K to 

1718 simulate a d_model+1 dimensional input (whose final coordinate is always 1), do the SVD 

1719 factorization on this effective matrix, then separate out into final weights and biases. 

1720 

1721 Args: 

1722 state_dict (Dict[str, torch.Tensor]): State dict of the model. 

1723 cfg: Model configuration object. 

1724 adapter: Optional architecture adapter for parameter key translation. 

1725 

1726 Returns: 

1727 Dict[str, torch.Tensor]: Modified state dict with refactored attention matrices. 

1728 """ 

1729 # Make a deep copy to avoid modifying the original 

1730 state_dict = { 

1731 k: v.clone() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items() 

1732 } 

1733 assert ( 

1734 getattr(cfg, "positional_embedding_type", "standard") != "rotary" 

1735 ), "You can't refactor the QK circuit when using rotary embeddings (as the QK matrix depends on the position of the query and key)" 

1736 

1737 for l in range(cfg.n_layers): 

1738 W_Q_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.W_Q", adapter) 

1739 b_Q_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.b_Q", adapter) 

1740 W_K_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.W_K", adapter) 

1741 b_K_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.b_K", adapter) 

1742 W_V_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.W_V", adapter) 

1743 W_O_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.W_O", adapter) 

1744 b_V_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.b_V", adapter) 

1745 b_O_key = ProcessWeights._get_param_key(f"blocks.{l}.attn.b_O", adapter) 

1746 

1747 # Skip hybrid layers without attention (other loops already guard individually) 

1748 if W_Q_key not in state_dict: 

1749 continue 

1750 # If Q is present, K/V/O must be too 

1751 for _required_key in [W_K_key, W_V_key, W_O_key]: 

1752 if _required_key not in state_dict: 

1753 raise ValueError( 

1754 f"Inconsistent attention weights at layer {l}: " 

1755 f"'{W_Q_key}' found but '{_required_key}' missing. " 

1756 f"All of W_Q, W_K, W_V, W_O must be present together." 

1757 ) 

1758 

1759 # W_QK = W_Q @ W_K.T 

1760 # Concatenate biases to make a d_model+1 input dimension 

1761 W_Q = ProcessWeights.convert_tensor_to_tl_format( 

1762 W_Q_key, state_dict, state_dict.get(W_Q_key), cfg, adapter, l 

1763 ) 

1764 b_Q = ProcessWeights.convert_tensor_to_tl_format( 

1765 b_Q_key, state_dict, state_dict.get(b_Q_key), cfg, adapter, l 

1766 ) 

1767 W_K = ProcessWeights.convert_tensor_to_tl_format( 

1768 W_K_key, state_dict, state_dict.get(W_K_key), cfg, adapter, l 

1769 ) 

1770 b_K = ProcessWeights.convert_tensor_to_tl_format( 

1771 b_K_key, state_dict, state_dict.get(b_K_key), cfg, adapter, l 

1772 ) 

1773 assert W_Q is not None, f"W_Q not found at key {W_Q_key}" 

1774 assert b_Q is not None, f"b_Q not found at key {b_Q_key}" 

1775 assert W_K is not None, f"W_K not found at key {W_K_key}" 

1776 assert b_K is not None, f"b_K not found at key {b_K_key}" 

1777 

1778 W_Q_eff = torch.cat([W_Q, b_Q[:, None, :]], dim=1) 

1779 W_K_eff = torch.cat([W_K, b_K[:, None, :]], dim=1) 

1780 

1781 W_Q_eff_even, W_K_eff_even_T = ( 

1782 FactoredMatrix(W_Q_eff, W_K_eff.transpose(-1, -2)).make_even().pair 

1783 ) 

1784 W_K_eff_even = W_K_eff_even_T.transpose(-1, -2) 

1785 

1786 state_dict[W_Q_key] = ProcessWeights.convert_tensor_to_hf_format( 

1787 W_Q_key, W_Q_eff_even[:, :-1, :], cfg, adapter, l 

1788 ) 

1789 state_dict[b_Q_key] = ProcessWeights.convert_tensor_to_hf_format( 

1790 b_Q_key, W_Q_eff_even[:, -1, :], cfg, adapter, l 

1791 ) 

1792 state_dict[W_K_key] = ProcessWeights.convert_tensor_to_hf_format( 

1793 W_K_key, W_K_eff_even[:, :-1, :], cfg, adapter, l 

1794 ) 

1795 state_dict[b_K_key] = ProcessWeights.convert_tensor_to_hf_format( 

1796 b_K_key, W_K_eff_even[:, -1, :], cfg, adapter, l 

1797 ) 

1798 

1799 # W_OV = W_V @ W_O 

1800 W_V = ProcessWeights.convert_tensor_to_tl_format( 

1801 W_V_key, state_dict, state_dict.get(W_V_key), cfg, adapter, l 

1802 ) 

1803 W_O = ProcessWeights.convert_tensor_to_tl_format( 

1804 W_O_key, state_dict, state_dict.get(W_O_key), cfg, adapter, l 

1805 ) 

1806 

1807 # Factors the bias to be consistent. 

1808 b_V = ProcessWeights.convert_tensor_to_tl_format( 

1809 b_V_key, state_dict, state_dict.get(b_V_key), cfg, adapter, l 

1810 ) 

1811 b_O = ProcessWeights.convert_tensor_to_tl_format( 

1812 b_O_key, state_dict, state_dict.get(b_O_key), cfg, adapter, l 

1813 ) 

1814 assert W_V is not None, f"W_V not found at key {W_V_key}" 

1815 assert W_O is not None, f"W_O not found at key {W_O_key}" 

1816 assert b_V is not None, f"b_V not found at key {b_V_key}" 

1817 assert b_O is not None, f"b_O not found at key {b_O_key}" 

1818 

1819 # Add singleton dimension for broadcasting 

1820 b_V_expanded = einops.rearrange(b_V, "head_index d_head -> head_index d_head 1") 

1821 

1822 b_V_times_W_O = b_V_expanded * W_O 

1823 

1824 # Sum over d_head and head_index dimensions 

1825 b_V_contribution = b_V_times_W_O.sum(1).sum(0) 

1826 

1827 effective_bias = b_O + b_V_contribution 

1828 state_dict[b_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1829 b_V_key, torch.zeros_like(b_V), cfg, adapter, l 

1830 ) 

1831 state_dict[b_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1832 b_O_key, effective_bias, cfg, adapter, l 

1833 ) 

1834 

1835 # Helper class to efficiently deal with low rank factored matrices. 

1836 W_OV = FactoredMatrix(W_V, W_O) 

1837 U, S, Vh = W_OV.svd() 

1838 state_dict[W_V_key] = ProcessWeights.convert_tensor_to_hf_format( 

1839 W_V_key, U @ S.diag_embed(), cfg, adapter, l 

1840 ) 

1841 state_dict[W_O_key] = ProcessWeights.convert_tensor_to_hf_format( 

1842 W_O_key, utils.transpose(Vh), cfg, adapter, l 

1843 ) 

1844 

1845 return state_dict 

1846 

1847 @overload 

1848 @staticmethod 

1849 def convert_tensor_to_tl_format( 

1850 param_name: str, 

1851 model_state_dict: Dict[str, torch.Tensor], 

1852 tensor: torch.Tensor, 

1853 cfg: Optional["TransformerLensConfig"], 

1854 adapter: Optional["ArchitectureAdapter"] = None, 

1855 layer_idx: Optional[int] = None, 

1856 ) -> torch.Tensor: 

1857 ... 

1858 

1859 @overload 

1860 @staticmethod 

1861 def convert_tensor_to_tl_format( 

1862 param_name: str, 

1863 model_state_dict: Dict[str, torch.Tensor], 

1864 tensor: None, 

1865 cfg: Optional["TransformerLensConfig"], 

1866 adapter: Optional["ArchitectureAdapter"] = None, 

1867 layer_idx: Optional[int] = None, 

1868 ) -> None: 

1869 ... 

1870 

1871 @staticmethod 

1872 def convert_tensor_to_tl_format( 

1873 param_name: str, 

1874 model_state_dict: Dict[str, torch.Tensor], 

1875 tensor: Optional[torch.Tensor], 

1876 cfg: Optional["TransformerLensConfig"], 

1877 adapter: Optional["ArchitectureAdapter"] = None, 

1878 layer_idx: Optional[int] = None, 

1879 ) -> Optional[torch.Tensor]: 

1880 """Convert a tensor from its original format to TransformerLens format. 

1881 

1882 Args: 

1883 param_name: The parameter name in TransformerLens format (e.g., "blocks.0.attn.W_Q") 

1884 model_state_dict: The model's state dictionary containing the actual tensors 

1885 tensor: The tensor to convert, or None for optional parameters 

1886 cfg: Model configuration 

1887 adapter: Optional architecture adapter for component retrieval and key translation. 

1888 If None, the tensor is returned unchanged. 

1889 layer_idx: Layer index (required for layer-specific parameters) 

1890 

1891 Returns: 

1892 The tensor converted to TransformerLens format, or None if the parameter doesn't exist 

1893 (which is valid for optional parameters like biases in models that don't use them). 

1894 If adapter is None, returns the tensor unchanged. 

1895 """ 

1896 # If no adapter provided, return tensor unchanged (handle None gracefully) 

1897 if adapter is None: 

1898 return tensor 

1899 

1900 if ( 1900 ↛ 1981line 1900 didn't jump to line 1981 because the condition on line 1900 was always true

1901 hasattr(adapter, "weight_processing_conversions") 

1902 and adapter.weight_processing_conversions is not None 

1903 ): 

1904 # Create placeholder param name by replacing layer index with {i} 

1905 placeholder_param_name = param_name 

1906 if "blocks." in param_name: 

1907 placeholder_param_name = re.sub(r"blocks\.(\d+)\.", "blocks.{i}.", param_name) 

1908 

1909 # Check if we have a conversion for this parameter. 

1910 # Try exact match first, then strip .weight suffix for adapters 

1911 # that define conversions without the suffix (e.g. Pythia's "blocks.{i}.attn.q"). 

1912 # NOTE: Only strip .weight, NOT .bias — stripping .bias would incorrectly 

1913 # match bias keys against weight conversions (e.g. "blocks.{i}.attn.q.bias" 

1914 # would match the weight conversion for "blocks.{i}.attn.q"). 

1915 matched_key = None 

1916 if placeholder_param_name in adapter.weight_processing_conversions: 

1917 matched_key = placeholder_param_name 

1918 elif placeholder_param_name.endswith(".weight"): 

1919 stripped = placeholder_param_name[: -len(".weight")] 

1920 if stripped in adapter.weight_processing_conversions: 

1921 matched_key = stripped 

1922 

1923 if matched_key is not None: 

1924 param_conversion = adapter.weight_processing_conversions[matched_key] 

1925 

1926 # Handle both ParamProcessingConversion objects and legacy string mappings 

1927 if isinstance(param_conversion, str): 1927 ↛ 1930line 1927 didn't jump to line 1930 because the condition on line 1927 was never true

1928 # Legacy string mapping - just return the tensor as-is 

1929 # (string mappings are handled elsewhere in the architecture adapter) 

1930 return tensor 

1931 else: 

1932 # Skip conversion for optional parameters that don't exist (e.g. biases) 

1933 if tensor is None and param_name not in model_state_dict: 1933 ↛ 1934line 1933 didn't jump to line 1934 because the condition on line 1933 was never true

1934 return None 

1935 # Try ParamProcessingConversion.convert() first (uses source_key 

1936 # to fetch from state dict — needed for split conversions like 

1937 # GPT-2's QKV). If source_key resolves to a missing key and we 

1938 # already have the tensor, fall back to applying the tensor 

1939 # conversion directly (needed for adapters like GPT-Neo whose 

1940 # source_key references HF keys not in the bridge state dict). 

1941 if ( 

1942 hasattr(param_conversion, "source_key") 

1943 and param_conversion.source_key is not None 

1944 ): 

1945 resolved_key = param_conversion._resolve_key( 

1946 param_name, param_conversion.source_key 

1947 ) 

1948 if resolved_key not in model_state_dict and tensor is not None: 1948 ↛ 1975line 1948 didn't jump to line 1975 because the condition on line 1948 was always true

1949 # Source key not in state dict — the tensor is already in 

1950 # bridge format (e.g. already split from combined QKV). 

1951 # If the conversion is a ChainTensorConversion that includes 

1952 # a SplitTensorConversion, skip the split step since 

1953 # it was already applied during bridge construction. 

1954 from transformer_lens.conversion_utils.conversion_steps.chain_tensor_conversion import ( 

1955 ChainTensorConversion, 

1956 ) 

1957 from transformer_lens.conversion_utils.conversion_steps.split_tensor_conversion import ( 

1958 SplitTensorConversion, 

1959 ) 

1960 

1961 tc = param_conversion.tensor_conversion 

1962 if isinstance(tc, ChainTensorConversion): 1962 ↛ 1963line 1962 didn't jump to line 1963 because the condition on line 1962 was never true

1963 non_split = [ 

1964 c 

1965 for c in tc.conversions 

1966 if not isinstance(c, SplitTensorConversion) 

1967 ] 

1968 if len(non_split) < len(tc.conversions): 

1969 # Apply only the non-split conversions 

1970 result = tensor 

1971 for conv in non_split: 

1972 result = conv.handle_conversion(result, model_state_dict) 

1973 return result 

1974 return tc.convert(tensor, model_state_dict) 

1975 return param_conversion.convert(model_state_dict, param_name) 

1976 else: 

1977 # No conversion defined, return tensor as-is (may be None for optional params) 

1978 return tensor 

1979 else: 

1980 # No conversions defined, return tensor as-is (may be None for optional params) 

1981 return tensor 

1982 

1983 @overload 

1984 @staticmethod 

1985 def convert_tensor_to_hf_format( 

1986 param_name: str, 

1987 tensor: torch.Tensor, 

1988 cfg: Optional["TransformerLensConfig"], 

1989 adapter: Optional["ArchitectureAdapter"] = None, 

1990 layer_idx: Optional[int] = None, 

1991 ) -> torch.Tensor: 

1992 ... 

1993 

1994 @overload 

1995 @staticmethod 

1996 def convert_tensor_to_hf_format( 

1997 param_name: str, 

1998 tensor: None, 

1999 cfg: Optional["TransformerLensConfig"], 

2000 adapter: Optional["ArchitectureAdapter"] = None, 

2001 layer_idx: Optional[int] = None, 

2002 ) -> None: 

2003 ... 

2004 

2005 @staticmethod 

2006 def convert_tensor_to_hf_format( 

2007 param_name: str, 

2008 tensor: Optional[torch.Tensor], 

2009 cfg: Optional["TransformerLensConfig"], 

2010 adapter: Optional["ArchitectureAdapter"] = None, 

2011 layer_idx: Optional[int] = None, 

2012 ) -> Optional[torch.Tensor]: 

2013 """Convert a tensor from TransformerLens format back to its original format. 

2014 

2015 Args: 

2016 param_name: The parameter name in TransformerLens format (e.g., "blocks.0.attn.W_Q") 

2017 tensor: The tensor to convert (in TransformerLens format), or None if parameter is optional 

2018 cfg: Model configuration 

2019 adapter: Optional architecture adapter for component retrieval and key translation. 

2020 If None, the tensor is returned unchanged. 

2021 layer_idx: Layer index (required for layer-specific parameters) 

2022 

2023 Returns: 

2024 The tensor converted back to original format, or None if tensor was None. 

2025 If adapter is None, returns the tensor unchanged. 

2026 """ 

2027 # Handle None tensors (optional parameters) 

2028 if tensor is None: 2028 ↛ 2029line 2028 didn't jump to line 2029 because the condition on line 2028 was never true

2029 return None 

2030 

2031 # If no adapter provided, return tensor unchanged 

2032 if adapter is None: 

2033 return tensor 

2034 

2035 if ( 2035 ↛ 2087line 2035 didn't jump to line 2087 because the condition on line 2035 was always true

2036 hasattr(adapter, "weight_processing_conversions") 

2037 and adapter.weight_processing_conversions is not None 

2038 ): 

2039 # Create placeholder param name by replacing layer index with {i} 

2040 placeholder_param_name = param_name 

2041 if "blocks." in param_name: 

2042 placeholder_param_name = re.sub(r"blocks\.(\d+)\.", "blocks.{i}.", param_name) 

2043 

2044 # Check if we have a conversion for this parameter. 

2045 # Try exact match first, then strip .weight suffix (not .bias — see convert_tensor_to_tl_format). 

2046 matched_key = None 

2047 if placeholder_param_name in adapter.weight_processing_conversions: 

2048 matched_key = placeholder_param_name 

2049 elif placeholder_param_name.endswith(".weight"): 

2050 stripped = placeholder_param_name[: -len(".weight")] 

2051 if stripped in adapter.weight_processing_conversions: 

2052 matched_key = stripped 

2053 

2054 if matched_key is not None: 

2055 param_conversion = adapter.weight_processing_conversions[matched_key] 

2056 

2057 # Handle both ParamProcessingConversion objects and legacy string mappings 

2058 if isinstance(param_conversion, str): 2058 ↛ 2060line 2058 didn't jump to line 2060 because the condition on line 2058 was never true

2059 # Legacy string mapping - just return the tensor as-is 

2060 return tensor 

2061 else: 

2062 # Revert the conversion. For ChainTensorConversions that include 

2063 # SplitTensorConversion, skip the split revert step (which is a 

2064 # no-op anyway) to match the forward conversion path. 

2065 from transformer_lens.conversion_utils.conversion_steps.chain_tensor_conversion import ( 

2066 ChainTensorConversion, 

2067 ) 

2068 from transformer_lens.conversion_utils.conversion_steps.split_tensor_conversion import ( 

2069 SplitTensorConversion, 

2070 ) 

2071 

2072 tc = param_conversion.tensor_conversion 

2073 if isinstance(tc, ChainTensorConversion): 2073 ↛ 2074line 2073 didn't jump to line 2074 because the condition on line 2073 was never true

2074 non_split = [ 

2075 c for c in tc.conversions if not isinstance(c, SplitTensorConversion) 

2076 ] 

2077 if len(non_split) < len(tc.conversions): 

2078 # Revert only the non-split conversions in reverse order 

2079 result = tensor 

2080 for conv in reversed(non_split): 

2081 result = conv.revert(result) 

2082 return result 

2083 return param_conversion.revert(tensor) 

2084 else: 

2085 return tensor 

2086 else: 

2087 return tensor 

2088 

2089 @staticmethod 

2090 def distribute_weights_to_components( 

2091 state_dict: Dict[str, torch.Tensor], 

2092 component_mapping: Dict[str, Any], 

2093 verbose: bool = False, 

2094 ) -> None: 

2095 """Distribute processed weights from state_dict to generalized components. 

2096 

2097 This function loops through the component_mapping and extracts relevant weights 

2098 for each component using filter_dict_by_prefix, then calls set_processed_weights 

2099 on each component. For list components (like blocks), it determines the number 

2100 of items and distributes weights to each indexed component. 

2101 

2102 Args: 

2103 state_dict: Dictionary of processed weights in MODERN TransformerLens format 

2104 (e.g., blocks.0.attn.q.weight, not transformer.h.0.attn.q.weight) 

2105 component_mapping: Dictionary (real_components) mapping TL keys to tuples of 

2106 (remote_path, component_instance), where component_instance can be either 

2107 a single component or a list of components 

2108 verbose: If True, print detailed information about weight distribution 

2109 

2110 Example: 

2111 For a real_components mapping like: 

2112 { 

2113 "embed": ("transformer.wte", <EmbeddingBridge instance>), 

2114 "blocks": ("transformer.h", [<BlockBridge 0>, <BlockBridge 1>, ...]), 

2115 "unembed": ("lm_head", <UnembeddingBridge instance>) 

2116 } 

2117 

2118 With modern TL keys in state_dict like "embed.weight", "blocks.0.attn.q.weight": 

2119 1. Extract weights starting with "embed" and pass to embed component 

2120 2. For blocks, extract all "blocks.*" weights, determine the number of blocks, 

2121 then for each block index, extract weights for that specific block 

2122 3. Extract "unembed" weights and pass to unembed component 

2123 """ 

2124 if verbose: 2124 ↛ 2125line 2124 didn't jump to line 2125 because the condition on line 2124 was never true

2125 print(f"\n{'='*80}") 

2126 print(f"distribute_weights_to_components: Starting weight distribution") 

2127 print(f"State dict has {len(state_dict)} keys") 

2128 print(f"Component mapping has {len(component_mapping)} components") 

2129 print(f"{'='*80}\n") 

2130 

2131 for component_name, component_tuple in component_mapping.items(): 

2132 # component_mapping is real_components format: (remote_path, instance) 

2133 # instance can be either a single component or a list of components 

2134 if not isinstance(component_tuple, tuple): 2134 ↛ 2135line 2134 didn't jump to line 2135 because the condition on line 2134 was never true

2135 raise ValueError( 

2136 f"Expected tuple for component '{component_name}' in real_components, " 

2137 f"but got {type(component_tuple).__name__}: {component_tuple}" 

2138 ) 

2139 remote_key, component = component_tuple 

2140 is_list = isinstance(component, list) 

2141 

2142 # Use the component_name (TL format) as prefix instead of remote_key (HF format) 

2143 # since state_dict now has modern TL keys 

2144 tl_prefix = component_name 

2145 

2146 if verbose: 2146 ↛ 2147line 2146 didn't jump to line 2147 because the condition on line 2146 was never true

2147 print(f"\nProcessing component: {component_name}") 

2148 print(f" Remote key (HF): {remote_key}") 

2149 print(f" TL prefix: {tl_prefix}") 

2150 print(f" Is list: {is_list}") 

2151 

2152 if is_list: 

2153 # This is a list component like "blocks" 

2154 # Extract all weights that start with this prefix 

2155 all_list_weights = filter_dict_by_prefix(state_dict, tl_prefix) 

2156 

2157 if verbose: 2157 ↛ 2158line 2157 didn't jump to line 2158 because the condition on line 2157 was never true

2158 print(f" Found {len(all_list_weights)} weights for list component") 

2159 print(f" List has {len(component)} instances") 

2160 

2161 # Component is a list of actual instances 

2162 for i, instance in enumerate(component): 

2163 # Extract weights for this specific index 

2164 # This will get keys like "0.attn.q.weight" and strip the "0." to get "attn.q.weight" 

2165 indexed_weights = filter_dict_by_prefix(all_list_weights, str(i)) 

2166 

2167 if verbose: 2167 ↛ 2168line 2167 didn't jump to line 2168 because the condition on line 2167 was never true

2168 print(f" Instance {i}: Found {len(indexed_weights)} weights") 

2169 for key in indexed_weights.keys(): 

2170 print(f" - {key}") 

2171 

2172 # Skip if no weights found for this component (e.g., Q/K/V Linear sub-components 

2173 # that get their weights from parent JointQKVAttentionBridge) 

2174 if len(indexed_weights) == 0: 2174 ↛ 2175line 2174 didn't jump to line 2175 because the condition on line 2174 was never true

2175 if verbose: 

2176 print(f" Skipping instance {i} - no weights found") 

2177 continue 

2178 

2179 instance.set_processed_weights(indexed_weights, verbose=verbose) 

2180 else: 

2181 # This is a single component (not a list) 

2182 component_weights = filter_dict_by_prefix(state_dict, tl_prefix) 

2183 

2184 if verbose: 2184 ↛ 2185line 2184 didn't jump to line 2185 because the condition on line 2184 was never true

2185 print(f" Found {len(component_weights)} weights for single component") 

2186 for key in component_weights.keys(): 

2187 print(f" - {key}") 

2188 

2189 # Skip if no weights found for this component 

2190 if len(component_weights) == 0: 

2191 if verbose: 2191 ↛ 2192line 2191 didn't jump to line 2192 because the condition on line 2191 was never true

2192 print(f" Skipping component - no weights found") 

2193 continue 

2194 

2195 component.set_processed_weights(component_weights, verbose=verbose)