Coverage for transformer_lens/benchmarks/weight_processing.py: 5%

339 statements  

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

1"""Weight processing benchmarks for TransformerBridge.""" 

2 

3from typing import Optional, cast 

4 

5import torch 

6 

7from transformer_lens import HookedTransformer 

8from transformer_lens.benchmarks.utils import ( 

9 BenchmarkResult, 

10 BenchmarkSeverity, 

11 bridge_self_target_loss, 

12 is_tiny_test_model, 

13 safe_allclose, 

14) 

15from transformer_lens.model_bridge import TransformerBridge 

16from transformer_lens.model_bridge.generalized_components.attention import ( 

17 PerLayerGeometryError, 

18) 

19 

20 

21def benchmark_weight_processing( 

22 bridge: TransformerBridge, 

23 test_text: str, 

24 reference_model: Optional[HookedTransformer] = None, 

25) -> BenchmarkResult: 

26 """Benchmark weight processing (folding, centering) application. 

27 

28 Args: 

29 bridge: TransformerBridge model to test 

30 test_text: Input text for testing 

31 reference_model: Optional HookedTransformer reference model 

32 

33 Returns: 

34 BenchmarkResult with weight processing verification details 

35 """ 

36 try: 

37 from transformer_lens.components.layer_norm_pre import LayerNormPre 

38 from transformer_lens.model_bridge.generalized_components.normalization import ( 

39 NormalizationBridge, 

40 ) 

41 

42 # Check layer norm folding 

43 if not isinstance(bridge.ln_final, NormalizationBridge): 

44 return BenchmarkResult( 

45 name="weight_processing", 

46 severity=BenchmarkSeverity.WARNING, 

47 message=f"Bridge ln_final is {type(bridge.ln_final).__name__}, expected NormalizationBridge", 

48 ) 

49 

50 # Verify NormalizationBridge has LayerNormPre functionality 

51 if not hasattr(bridge.ln_final, "_layernorm_pre_forward"): 

52 return BenchmarkResult( 

53 name="weight_processing", 

54 severity=BenchmarkSeverity.WARNING, 

55 message="Bridge ln_final missing LayerNormPre functionality", 

56 ) 

57 

58 if not hasattr(bridge.ln_final.config, "layer_norm_folding"): 

59 return BenchmarkResult( 

60 name="weight_processing", 

61 severity=BenchmarkSeverity.WARNING, 

62 message="Bridge ln_final missing layer_norm_folding config", 

63 ) 

64 

65 if reference_model is not None: 

66 # Check that reference model has LayerNormPre 

67 if not isinstance(reference_model.ln_final, LayerNormPre): 

68 return BenchmarkResult( 

69 name="weight_processing", 

70 severity=BenchmarkSeverity.WARNING, 

71 message=f"Reference ln_final is {type(reference_model.ln_final).__name__}, expected LayerNormPre", 

72 ) 

73 

74 # Check weight centering - writing weights should be approximately centered 

75 mlp_blocks = bridge.blocks_with("mlp") 

76 if not mlp_blocks: 

77 return BenchmarkResult( 

78 name="weight_processing", 

79 severity=BenchmarkSeverity.WARNING, 

80 message="No blocks have MLP submodule — cannot check centering", 

81 ) 

82 _mlp_idx, mlp_block = mlp_blocks[0] 

83 bridge_w_out = mlp_block.mlp.W_out 

84 reference_w_out = reference_model.blocks[_mlp_idx].mlp.W_out 

85 

86 bridge_mean = torch.mean(torch.abs(torch.mean(bridge_w_out, dim=-1, keepdim=True))) 

87 reference_mean = torch.mean( 

88 torch.abs(torch.mean(reference_w_out, dim=-1, keepdim=True)) 

89 ) 

90 

91 if bridge_mean.item() > 1e-3: 

92 return BenchmarkResult( 

93 name="weight_processing", 

94 severity=BenchmarkSeverity.WARNING, 

95 message=f"Bridge weights not well-centered: {bridge_mean.item():.6f}", 

96 details={"bridge_mean": bridge_mean.item()}, 

97 ) 

98 

99 if reference_mean.item() > 1e-3: 

100 return BenchmarkResult( 

101 name="weight_processing", 

102 severity=BenchmarkSeverity.WARNING, 

103 message=f"Reference weights not well-centered: {reference_mean.item():.6f}", 

104 details={"reference_mean": reference_mean.item()}, 

105 ) 

106 

107 return BenchmarkResult( 

108 name="weight_processing", 

109 severity=BenchmarkSeverity.INFO, 

110 message="Weight processing verified (folding and centering applied)", 

111 details={ 

112 "bridge_mean": bridge_mean.item(), 

113 "reference_mean": reference_mean.item(), 

114 }, 

115 ) 

116 

117 return BenchmarkResult( 

118 name="weight_processing", 

119 severity=BenchmarkSeverity.INFO, 

120 message="Weight processing structure verified", 

121 ) 

122 

123 except Exception as e: 

124 return BenchmarkResult( 

125 name="weight_processing", 

126 severity=BenchmarkSeverity.ERROR, 

127 message=f"Weight processing check failed: {str(e)}", 

128 passed=False, 

129 ) 

130 

131 

132def benchmark_weight_sharing( 

133 bridge: TransformerBridge, 

134 test_text: str, 

135 reference_model: Optional[HookedTransformer] = None, 

136 atol: float = 1e-3, 

137) -> BenchmarkResult: 

138 """Benchmark weight sharing and modification effects. 

139 

140 Args: 

141 bridge: TransformerBridge model to test 

142 test_text: Input text for testing 

143 reference_model: Optional HookedTransformer reference model 

144 atol: Absolute tolerance for effect comparison 

145 

146 Returns: 

147 BenchmarkResult with weight sharing verification details 

148 """ 

149 try: 

150 # Get baseline loss 

151 bridge_original = bridge_self_target_loss(bridge, test_text) 

152 

153 if reference_model is not None: 

154 reference_original = reference_model(test_text, return_type="loss") 

155 

156 bridge_attn_blocks = bridge.blocks_with("attn") 

157 if not bridge_attn_blocks: 

158 return BenchmarkResult( 

159 name="weight_sharing", 

160 severity=BenchmarkSeverity.INFO, 

161 message="No blocks have attention submodule — skipping weight sharing check", 

162 ) 

163 bridge_attn_idx, bridge_attn_block = bridge_attn_blocks[0] 

164 

165 # Verify weights are identical before modification 

166 bridge_W_V = torch.clone(cast(torch.Tensor, bridge_attn_block.attn.W_V)) 

167 reference_W_V = torch.clone( 

168 cast(torch.Tensor, reference_model.blocks[bridge_attn_idx].attn.W_V) 

169 ) 

170 

171 # Check if models have GQA (different head counts for K/V vs Q) 

172 has_gqa = ( 

173 hasattr(bridge.cfg, "n_key_value_heads") 

174 and bridge.cfg.n_key_value_heads != bridge.cfg.n_heads 

175 ) 

176 

177 # For GQA models, HookedTransformer may not support GQA correctly yet 

178 # Skip the weight comparison if shapes don't match 

179 if bridge_W_V.shape != reference_W_V.shape: # type: ignore[union-attr] 

180 if has_gqa: 

181 # This is expected - HookedTransformer doesn't support GQA yet 

182 # Skip this benchmark for GQA models 

183 return BenchmarkResult( 

184 name="weight_sharing", 

185 severity=BenchmarkSeverity.INFO, 

186 message=f"GQA model detected - skipping HT comparison (Bridge W_V: {bridge_W_V.shape}, HT W_V: {reference_W_V.shape})", # type: ignore[union-attr] 

187 details={ 

188 "bridge_shape": str(bridge_W_V.shape), # type: ignore[union-attr] 

189 "reference_shape": str(reference_W_V.shape), # type: ignore[union-attr] 

190 }, 

191 ) 

192 else: 

193 return BenchmarkResult( 

194 name="weight_sharing", 

195 severity=BenchmarkSeverity.WARNING, 

196 message=f"Weight shapes differ: Bridge {bridge_W_V.shape} vs Reference {reference_W_V.shape}", # type: ignore[union-attr] 

197 details={ 

198 "bridge_shape": str(bridge_W_V.shape), # type: ignore[union-attr] 

199 "reference_shape": str(reference_W_V.shape), # type: ignore[union-attr] 

200 }, 

201 ) 

202 

203 if not safe_allclose(bridge_W_V, reference_W_V): # type: ignore[arg-type] 

204 return BenchmarkResult( 

205 name="weight_sharing", 

206 severity=BenchmarkSeverity.WARNING, 

207 message="Weights differ before modification", 

208 ) 

209 

210 # Modify weights in both models 

211 with torch.no_grad(): 

212 bridge_attn_block.attn.W_V[0, :, :] = 0 # type: ignore[union-attr,operator] 

213 reference_model.blocks[bridge_attn_idx].attn.W_V[0, :, :] = 0 

214 

215 # Test modified losses 

216 bridge_modified = bridge_self_target_loss(bridge, test_text) 

217 reference_modified = reference_model(test_text, return_type="loss") 

218 

219 bridge_change = bridge_modified - bridge_original 

220 reference_change = reference_modified - reference_original 

221 

222 # Restore weights 

223 with torch.no_grad(): 

224 bridge_attn_block.attn.W_V.copy_(bridge_W_V) # type: ignore[union-attr,operator,arg-type] 

225 reference_model.blocks[bridge_attn_idx].attn.W_V.copy_(reference_W_V) 

226 

227 diff = abs(bridge_change - reference_change) 

228 if diff < atol: 

229 return BenchmarkResult( 

230 name="weight_sharing", 

231 severity=BenchmarkSeverity.INFO, 

232 message=f"Weight modifications have similar effects: {bridge_change:.6f}{reference_change:.6f}", 

233 details={"diff": diff.item(), "atol": atol}, 

234 ) 

235 else: 

236 return BenchmarkResult( 

237 name="weight_sharing", 

238 severity=BenchmarkSeverity.WARNING, 

239 message=f"Weight modification effects differ: {bridge_change:.6f} vs {reference_change:.6f}", 

240 details={"diff": diff.item(), "atol": atol}, 

241 ) 

242 

243 # No reference model - just verify modification has an effect 

244 # Find first block with attention (hybrid models may not have attn on block 0) 

245 bridge_attn_blocks = bridge.blocks_with("attn") 

246 if not bridge_attn_blocks: 

247 return BenchmarkResult( 

248 name="weight_sharing", 

249 severity=BenchmarkSeverity.INFO, 

250 message="No blocks have attention submodule — skipping weight sharing check", 

251 ) 

252 _ws_idx, ws_attn_block = bridge_attn_blocks[0] 

253 

254 original_W_V = ws_attn_block.attn.W_V.clone() 

255 with torch.no_grad(): 

256 ws_attn_block.attn.W_V[0, :, :] = 0 

257 

258 bridge_modified = bridge_self_target_loss(bridge, test_text) 

259 change = abs(bridge_modified - bridge_original) 

260 

261 # Restore weights 

262 with torch.no_grad(): 

263 ws_attn_block.attn.W_V.copy_(original_W_V) 

264 

265 if change < 1e-6: 

266 return BenchmarkResult( 

267 name="weight_sharing", 

268 severity=BenchmarkSeverity.WARNING, 

269 message=f"Weight modification had minimal effect: {change:.6f}", 

270 details={"change": change.item()}, 

271 ) 

272 

273 return BenchmarkResult( 

274 name="weight_sharing", 

275 severity=BenchmarkSeverity.INFO, 

276 message=f"Weight modification affects forward pass: change={change:.6f}", 

277 details={"change": change.item()}, 

278 ) 

279 

280 except Exception as e: 

281 return BenchmarkResult( 

282 name="weight_sharing", 

283 severity=BenchmarkSeverity.ERROR, 

284 message=f"Weight sharing check failed: {str(e)}", 

285 passed=False, 

286 ) 

287 

288 

289def benchmark_weight_modification( 

290 bridge: TransformerBridge, 

291 test_text: str, 

292 reference_model: Optional[HookedTransformer] = None, 

293) -> BenchmarkResult: 

294 """Benchmark that weight modifications propagate correctly. 

295 

296 Args: 

297 bridge: TransformerBridge model to test 

298 test_text: Input text for testing 

299 reference_model: Optional HookedTransformer reference model (not used) 

300 

301 Returns: 

302 BenchmarkResult with weight modification verification details 

303 """ 

304 try: 

305 # Get original loss 

306 original_loss = bridge_self_target_loss(bridge, test_text) 

307 

308 # Find first block with attention (hybrid models may not have attn on block 0) 

309 wm_attn_blocks = bridge.blocks_with("attn") 

310 if not wm_attn_blocks: 

311 return BenchmarkResult( 

312 name="weight_modification", 

313 severity=BenchmarkSeverity.INFO, 

314 message="No blocks have attention submodule — skipping weight modification check", 

315 ) 

316 _wm_idx, wm_attn_block = wm_attn_blocks[0] 

317 

318 # Modify W_V weights 

319 with torch.no_grad(): 

320 original_w_v = wm_attn_block.attn.W_V.clone() 

321 # Check dimensionality - GQA models may have 2D tensors instead of 3D 

322 if original_w_v.ndim == 3: 

323 # Standard 3D tensor: [n_heads, d_model, d_head] 

324 wm_attn_block.attn.W_V[0, :, :] = 0 

325 elif original_w_v.ndim == 2: 

326 # 2D tensor (e.g., GQA models): [n_heads * d_head, d_model] or similar 

327 wm_attn_block.attn.W_V[0, :] = 0 

328 else: 

329 return BenchmarkResult( 

330 name="weight_modification", 

331 severity=BenchmarkSeverity.WARNING, 

332 message=f"Unexpected W_V shape: {original_w_v.shape} (ndim={original_w_v.ndim})", 

333 passed=False, 

334 ) 

335 

336 # Get modified loss (with error handling to restore weights) 

337 try: 

338 modified_loss = bridge_self_target_loss(bridge, test_text) 

339 except Exception as forward_error: 

340 # Restore weights before reporting error 

341 with torch.no_grad(): 

342 wm_attn_block.attn.W_V.copy_(original_w_v) 

343 

344 # Some models (e.g., models with complex attention mechanisms) may have 

345 # forward pass issues after weight modification. Report as skipped. 

346 return BenchmarkResult( 

347 name="weight_modification", 

348 severity=BenchmarkSeverity.SKIPPED, 

349 message=f"Weight modification not testable for this architecture: {str(forward_error)}", 

350 details={"error": str(forward_error), "architecture_limitation": True}, 

351 ) 

352 

353 # Restore weights 

354 with torch.no_grad(): 

355 wm_attn_block.attn.W_V.copy_(original_w_v) 

356 

357 # Loss should change 

358 change = abs(modified_loss - original_loss) 

359 if change < 1e-6: 

360 # W_V modification didn't propagate. This can happen in models with 

361 # combined QKV projections (e.g., Bloom) where the split V weight 

362 # is separate from the combined QKV weight used in forward. 

363 # Try MLP weight modification as fallback. 

364 mlp_fallback_error = None 

365 mlp_blocks = bridge.blocks_with("mlp") 

366 mlp_block = mlp_blocks[0][1] if mlp_blocks else None 

367 try: 

368 if mlp_block is None: 

369 raise AttributeError("No blocks have mlp submodule") 

370 with torch.no_grad(): 

371 original_mlp_w = mlp_block.mlp.out.weight.clone() 

372 mlp_block.mlp.out.weight[0, :] = 0 

373 mlp_modified_loss = bridge_self_target_loss(bridge, test_text) 

374 with torch.no_grad(): 

375 mlp_block.mlp.out.weight.copy_(original_mlp_w) 

376 mlp_change = abs(mlp_modified_loss - original_loss) 

377 if mlp_change > 1e-6: 

378 return BenchmarkResult( 

379 name="weight_modification", 

380 severity=BenchmarkSeverity.INFO, 

381 message=f"Weight modification propagates via MLP (change: {mlp_change:.6f}). " 

382 f"W_V not propagated (combined QKV architecture).", 

383 details={"change": mlp_change.item(), "fallback": "mlp"}, 

384 ) 

385 except Exception as mlp_err: 

386 mlp_fallback_error = str(mlp_err) 

387 

388 details = {"change": change.item()} 

389 if mlp_fallback_error is not None: 

390 details["mlp_fallback_error"] = mlp_fallback_error 

391 return BenchmarkResult( 

392 name="weight_modification", 

393 severity=BenchmarkSeverity.DANGER, 

394 message=f"Weight modification did not affect loss (change: {change:.6f})", 

395 details=details, 

396 passed=False, 

397 ) 

398 

399 return BenchmarkResult( 

400 name="weight_modification", 

401 severity=BenchmarkSeverity.INFO, 

402 message=f"Weight modification propagates correctly (change: {change:.6f})", 

403 details={"change": change.item()}, 

404 ) 

405 

406 except Exception as e: 

407 # Some architectures (e.g., Gemma 3 with complex attention, OpenELM with 

408 # combined QKV) don't expose W_V. Report as skipped, not passed. 

409 if ( 

410 "cannot be multiplied" in str(e) 

411 or "shape" in str(e).lower() 

412 or "has no attribute" in str(e) 

413 ): 

414 return BenchmarkResult( 

415 name="weight_modification", 

416 severity=BenchmarkSeverity.SKIPPED, 

417 message=f"Weight modification not testable for this architecture: {str(e)}", 

418 details={"error": str(e), "architecture_limitation": True}, 

419 ) 

420 return BenchmarkResult( 

421 name="weight_modification", 

422 severity=BenchmarkSeverity.ERROR, 

423 message=f"Weight modification check failed: {str(e)}", 

424 passed=False, 

425 ) 

426 

427 

428def benchmark_layer_norm_folding( 

429 bridge: TransformerBridge, 

430 test_text: str, 

431 reference_model: Optional[HookedTransformer] = None, 

432) -> BenchmarkResult: 

433 """Benchmark layer norm folding - norm weights should be identity after folding. 

434 

435 Args: 

436 bridge: TransformerBridge model to test 

437 test_text: Input text for testing 

438 reference_model: Optional HookedTransformer reference model (not used) 

439 

440 Returns: 

441 BenchmarkResult with layer norm folding verification details 

442 """ 

443 try: 

444 # Skip for architectures that don't support fold_ln (e.g., post-LN like BERT) 

445 adapter = getattr(bridge, "adapter", None) 

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

447 return BenchmarkResult( 

448 name="layer_norm_folding", 

449 severity=BenchmarkSeverity.SKIPPED, 

450 message="Skipped (post-LN architecture does not support fold_ln)", 

451 passed=True, 

452 ) 

453 

454 # Get state dict from bridge (should return TransformerLens format keys) 

455 state_dict = bridge.state_dict() 

456 

457 # Check both ln1 (attention LN) and ln2 (MLP LN) in TransformerLens format. 

458 # Models with combined QKV projections (e.g., OpenELM's qkv_proj) cannot 

459 # fold ln1 into attention weights, but ln2 should always be foldable. 

460 tolerance = 0.01 

461 # For rmsnorm_uses_offset models (Gemma/Gemma2), HF computes x*(1+weight), 

462 # so the identity weight after folding is 0.0 (gives 1+0=1). For standard 

463 # models, identity is 1.0. 

464 cfg = getattr(getattr(bridge, "adapter", None), "cfg", None) 

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

466 expected_val = 0.0 if rmsnorm_uses_offset else 1.0 

467 folded = [] 

468 not_folded = [] 

469 

470 for ln_name in ["ln1", "ln2"]: 

471 ln_key = f"blocks.0.{ln_name}.weight" 

472 if ln_key not in state_dict: 

473 continue 

474 ln_weight = state_dict[ln_key] 

475 mean_val = torch.mean(ln_weight).item() 

476 if abs(mean_val - expected_val) < tolerance: 

477 folded.append((ln_name, ln_key, mean_val)) 

478 else: 

479 not_folded.append((ln_name, ln_key, mean_val)) 

480 

481 if not folded and not not_folded: 

482 # No LN weights found — model uses non-parametric LayerNorm 

483 # (e.g., OLMo v1 has fixed weight=1, bias=0 with no learnable params). 

484 # Nothing to fold, so this is a pass. 

485 return BenchmarkResult( 

486 name="layer_norm_folding", 

487 severity=BenchmarkSeverity.INFO, 

488 message="No learnable layer norm weights (non-parametric LayerNorm)", 

489 passed=True, 

490 ) 

491 

492 if folded and not not_folded: 

493 # All LN weights are folded 

494 names = ", ".join(f"{n} (mean={m:.6f})" for n, _, m in folded) 

495 return BenchmarkResult( 

496 name="layer_norm_folding", 

497 severity=BenchmarkSeverity.INFO, 

498 message=f"Layer norm folding verified: {names}", 

499 details={"folded": [n for n, _, _ in folded]}, 

500 ) 

501 elif folded and not_folded: 

502 # Partial folding — some LN weights folded, some not. 

503 # This is expected for models with combined QKV (ln1 can't fold). 

504 folded_names = ", ".join(f"{n} (mean={m:.6f})" for n, _, m in folded) 

505 unfolded_names = ", ".join(f"{n} (mean={m:.6f})" for n, _, m in not_folded) 

506 return BenchmarkResult( 

507 name="layer_norm_folding", 

508 severity=BenchmarkSeverity.WARNING, 

509 message=( 

510 f"Partial LN folding: {folded_names} folded; " 

511 f"{unfolded_names} preserved (expected for combined QKV models)" 

512 ), 

513 details={ 

514 "folded": [n for n, _, _ in folded], 

515 "not_folded": [n for n, _, _ in not_folded], 

516 }, 

517 passed=True, 

518 ) 

519 else: 

520 # No LN weights folded 

521 names = ", ".join(f"{n} (mean={m:.6f})" for n, _, m in not_folded) 

522 return BenchmarkResult( 

523 name="layer_norm_folding", 

524 severity=BenchmarkSeverity.WARNING, 

525 message=f"Layer norm weights not identity after folding: {names}", 

526 details={"not_folded": [n for n, _, _ in not_folded]}, 

527 passed=False, 

528 ) 

529 

530 except Exception as e: 

531 return BenchmarkResult( 

532 name="layer_norm_folding", 

533 severity=BenchmarkSeverity.ERROR, 

534 message=f"Layer norm folding check failed: {str(e)}", 

535 passed=False, 

536 ) 

537 

538 

539def benchmark_attention_output_centering( 

540 bridge: TransformerBridge, 

541 test_text: str, 

542 reference_model: Optional[HookedTransformer] = None, 

543) -> BenchmarkResult: 

544 """Benchmark attention output centering - W_O should have mean ≈ 0. 

545 

546 Args: 

547 bridge: TransformerBridge model to test 

548 test_text: Input text for testing 

549 reference_model: Optional HookedTransformer reference model (not used) 

550 

551 Returns: 

552 BenchmarkResult with attention output centering verification details 

553 """ 

554 try: 

555 # Skip centering check for tiny/test models — random weights don't 

556 # center meaningfully and produce false failures. 

557 if is_tiny_test_model(getattr(bridge.cfg, "model_name", "") or ""): 

558 return BenchmarkResult( 

559 name="attention_output_centering", 

560 severity=BenchmarkSeverity.INFO, 

561 message="Skipped for tiny/test model (random weights don't center meaningfully)", 

562 ) 

563 

564 attn_blocks = bridge.blocks_with("attn") 

565 if not attn_blocks: 

566 # Attention-less (pure SSM) or hybrids whose attention lives inside a 

567 # passthrough mixer (NemotronH): nothing to center — skip, don't fail. 

568 return BenchmarkResult( 

569 name="attention_output_centering", 

570 severity=BenchmarkSeverity.SKIPPED, 

571 message="No blocks expose an attention submodule (SSM / passthrough-mixer hybrid)", 

572 ) 

573 

574 # Check W_O accessibility on first attention block. Explicit probe, 

575 # not hasattr: hasattr invokes the property and only swallows 

576 # AttributeError, so the per-layer-geometry ValueError would escape to 

577 # the generic handler before the loop's raw-weight fallback runs. 

578 first_idx, first_attn_block = attn_blocks[0] 

579 w_o_missing = False 

580 try: 

581 _ = first_attn_block.attn.W_O 

582 except AttributeError: 

583 w_o_missing = True 

584 except PerLayerGeometryError: 

585 pass # per-layer geometry; the loop below reads the raw projection 

586 if w_o_missing: 

587 # No mapped output projection (JetMoe's MoA keeps per-expert W_O 

588 # inside the delegated module): structurally nothing to center — 

589 # skip like the SSM case above rather than fail. 

590 if getattr(first_attn_block.attn, "o", None) is None: 

591 return BenchmarkResult( 

592 name="attention_output_centering", 

593 severity=BenchmarkSeverity.SKIPPED, 

594 message="No mapped output projection (delegated per-expert W_O)", 

595 ) 

596 return BenchmarkResult( 

597 name="attention_output_centering", 

598 severity=BenchmarkSeverity.WARNING, 

599 message="W_O not accessible on bridge model", 

600 passed=False, 

601 ) 

602 

603 # Compute mean across all attention blocks 

604 tolerance = 0.01 # 1% tolerance 

605 worst_mean = 0.0 

606 for idx, block in attn_blocks: 

607 try: 

608 column_means = torch.mean(block.attn.W_O, dim=-1) 

609 except PerLayerGeometryError: 

610 # Per-layer attention geometry (OpenELM varies head counts per 

611 # layer): the factorized accessor refuses, but centering is 

612 # head-agnostic — the d_model mean reads straight off the 2D 

613 # projection. 

614 raw = block.attn.o.weight 

615 in_out = block.attn._weight_layout_in_out(block.attn.o) 

616 column_means = raw.mean(dim=-1) if in_out else raw.mean(dim=0) 

617 mean_abs = torch.mean(torch.abs(column_means)).item() 

618 worst_mean = max(worst_mean, mean_abs) 

619 

620 n_attn = len(attn_blocks) 

621 n_total = len(bridge.blocks) 

622 block_info = f" ({n_attn}/{n_total} blocks have attention)" if n_attn < n_total else "" 

623 

624 if worst_mean < tolerance: 

625 return BenchmarkResult( 

626 name="attention_output_centering", 

627 severity=BenchmarkSeverity.INFO, 

628 message=f"Attention output centering verified (worst_mean={worst_mean:.6f}){block_info}", 

629 details={"mean": worst_mean, "tolerance": tolerance, "n_attn_blocks": n_attn}, 

630 ) 

631 else: 

632 return BenchmarkResult( 

633 name="attention_output_centering", 

634 severity=BenchmarkSeverity.WARNING, 

635 message=f"Attention output weights not well-centered (worst_mean={worst_mean:.6f}){block_info}", 

636 details={"mean": worst_mean, "tolerance": tolerance, "n_attn_blocks": n_attn}, 

637 passed=False, 

638 ) 

639 

640 except Exception as e: 

641 return BenchmarkResult( 

642 name="attention_output_centering", 

643 severity=BenchmarkSeverity.ERROR, 

644 message=f"Attention output centering check failed: {str(e)}", 

645 passed=False, 

646 ) 

647 

648 

649def benchmark_mlp_output_centering( 

650 bridge: TransformerBridge, 

651 test_text: str, 

652 reference_model: Optional[HookedTransformer] = None, 

653) -> BenchmarkResult: 

654 """Benchmark MLP output centering - MLP output weights should have mean ≈ 0. 

655 

656 Args: 

657 bridge: TransformerBridge model to test 

658 test_text: Input text for testing 

659 reference_model: Optional HookedTransformer reference model (not used) 

660 

661 Returns: 

662 BenchmarkResult with MLP output centering verification details 

663 """ 

664 try: 

665 # Skip centering check for tiny/test models — random weights don't 

666 # center meaningfully and produce false failures. 

667 if is_tiny_test_model(getattr(bridge.cfg, "model_name", "") or ""): 

668 return BenchmarkResult( 

669 name="mlp_output_centering", 

670 severity=BenchmarkSeverity.INFO, 

671 message="Skipped for tiny/test model (random weights don't center meaningfully)", 

672 ) 

673 

674 # Find an MLP-like submodule (may be "mlp", "shared_mlp", etc.) 

675 from transformer_lens.model_bridge.generalized_components.moe import MoEBridge 

676 

677 mlp_module = None 

678 for block in bridge.blocks: 

679 for name in ("mlp", "shared_mlp"): 

680 if name in block._modules: 

681 mlp_module = block._modules[name] 

682 break 

683 if mlp_module is not None: 

684 break 

685 if mlp_module is None: 

686 # Pure SSM, or a hybrid whose MLP lives inside a passthrough mixer: 

687 # no standalone MLP to center — skip, don't fail. 

688 return BenchmarkResult( 

689 name="mlp_output_centering", 

690 severity=BenchmarkSeverity.SKIPPED, 

691 message="No block exposes an MLP submodule (SSM / passthrough-mixer hybrid)", 

692 ) 

693 

694 if isinstance(mlp_module, MoEBridge): 

695 return BenchmarkResult( 

696 name="mlp_output_centering", 

697 severity=BenchmarkSeverity.INFO, 

698 message="Skipped for MoE models (no single W_out weight)", 

699 details={"is_moe": True}, 

700 ) 

701 

702 # Check if W_out exists and is accessible (HT format or bridge format) 

703 w_out = None 

704 if hasattr(mlp_module, "W_out"): 

705 w_out = mlp_module.W_out 

706 elif hasattr(mlp_module, "out"): 

707 out_module = mlp_module.out 

708 if hasattr(out_module, "original_component") and hasattr( 

709 out_module.original_component, "weight" 

710 ): 

711 w_out = out_module.original_component.weight 

712 elif hasattr(out_module, "weight"): 

713 w_out = out_module.weight 

714 if w_out is None: 

715 return BenchmarkResult( 

716 name="mlp_output_centering", 

717 severity=BenchmarkSeverity.WARNING, 

718 message="W_out not accessible on bridge model", 

719 passed=False, 

720 ) 

721 

722 # Compute mean along output dimension 

723 mean_abs = torch.mean(torch.abs(torch.mean(w_out, dim=-1))).item() 

724 

725 tolerance = 0.01 # 1% tolerance 

726 

727 if mean_abs < tolerance: 

728 return BenchmarkResult( 

729 name="mlp_output_centering", 

730 severity=BenchmarkSeverity.INFO, 

731 message=f"MLP output centering verified (mean={mean_abs:.6f})", 

732 details={"mean": mean_abs, "tolerance": tolerance}, 

733 ) 

734 else: 

735 return BenchmarkResult( 

736 name="mlp_output_centering", 

737 severity=BenchmarkSeverity.WARNING, 

738 message=f"MLP output weights not well-centered (mean={mean_abs:.6f})", 

739 details={"mean": mean_abs, "tolerance": tolerance}, 

740 passed=False, 

741 ) 

742 

743 except Exception as e: 

744 return BenchmarkResult( 

745 name="mlp_output_centering", 

746 severity=BenchmarkSeverity.ERROR, 

747 message=f"MLP output centering check failed: {str(e)}", 

748 passed=False, 

749 ) 

750 

751 

752def benchmark_unembed_centering( 

753 bridge: TransformerBridge, 

754 test_text: str, 

755 reference_model: Optional[HookedTransformer] = None, 

756) -> BenchmarkResult: 

757 """Benchmark unembed centering - unembed matrix should have mean ≈ 0. 

758 

759 Args: 

760 bridge: TransformerBridge model to test 

761 test_text: Input text for testing 

762 reference_model: Optional HookedTransformer reference model (not used) 

763 

764 Returns: 

765 BenchmarkResult with unembed centering verification details 

766 """ 

767 try: 

768 # Get state dict from bridge (should return TransformerLens format keys) 

769 state_dict = bridge.state_dict() 

770 

771 # Check for unembed weight in TransformerLens format 

772 unembed_key = "unembed.weight" 

773 

774 # Fallback: if TL format key doesn't exist, try common HF format patterns 

775 if unembed_key not in state_dict: 

776 # Try standard HF format 

777 if "lm_head.weight" in state_dict: 

778 unembed_key = "lm_head.weight" 

779 else: 

780 return BenchmarkResult( 

781 name="unembed_centering", 

782 severity=BenchmarkSeverity.WARNING, 

783 message="Could not find unembed weights in state dict", 

784 passed=False, 

785 ) 

786 

787 # Get the unembed weight tensor 

788 w_u = state_dict[unembed_key] 

789 

790 # Compute mean along vocabulary dimension (dim 0) 

791 mean_abs = torch.mean(torch.abs(torch.mean(w_u, dim=0))).item() 

792 

793 tolerance = 0.01 # 1% tolerance (consistent with attn/mlp centering) 

794 

795 if mean_abs < tolerance: 

796 return BenchmarkResult( 

797 name="unembed_centering", 

798 severity=BenchmarkSeverity.INFO, 

799 message=f"Unembed centering verified (mean={mean_abs:.6f})", 

800 details={"mean": mean_abs, "tolerance": tolerance, "key": unembed_key}, 

801 ) 

802 else: 

803 return BenchmarkResult( 

804 name="unembed_centering", 

805 severity=BenchmarkSeverity.WARNING, 

806 message=f"Unembed matrix not well-centered (mean={mean_abs:.6f})", 

807 details={"mean": mean_abs, "tolerance": tolerance, "key": unembed_key}, 

808 passed=False, 

809 ) 

810 

811 except Exception as e: 

812 return BenchmarkResult( 

813 name="unembed_centering", 

814 severity=BenchmarkSeverity.ERROR, 

815 message=f"Unembed centering check failed: {str(e)}", 

816 passed=False, 

817 ) 

818 

819 

820def benchmark_value_bias_folding( 

821 bridge: TransformerBridge, 

822 test_text: str, 

823 reference_model: Optional[HookedTransformer] = None, 

824) -> BenchmarkResult: 

825 """Benchmark value bias folding - b_V should be zero after folding. 

826 

827 Args: 

828 bridge: TransformerBridge model to test 

829 test_text: Input text for testing 

830 reference_model: Optional HookedTransformer reference model (not used) 

831 

832 Returns: 

833 BenchmarkResult with value bias folding verification details 

834 """ 

835 try: 

836 # Skip for GQA models (where n_key_value_heads != n_heads) 

837 # Value bias folding doesn't work the same way because V outputs are repeated 

838 if hasattr(bridge.cfg, "n_key_value_heads") and bridge.cfg.n_key_value_heads is not None: 

839 if bridge.cfg.n_key_value_heads != bridge.cfg.n_heads: 

840 return BenchmarkResult( 

841 name="value_bias_folding", 

842 severity=BenchmarkSeverity.INFO, 

843 message="Skipped for GQA models (n_key_value_heads != n_heads)", 

844 details={ 

845 "is_gqa": True, 

846 "n_heads": bridge.cfg.n_heads, 

847 "n_kv_heads": bridge.cfg.n_key_value_heads, 

848 }, 

849 ) 

850 

851 attn_blocks = bridge.blocks_with("attn") 

852 if not attn_blocks: 

853 return BenchmarkResult( 

854 name="value_bias_folding", 

855 severity=BenchmarkSeverity.INFO, 

856 message="No blocks have attention submodule (expected for hybrid models without mapped attn)", 

857 details={"has_bias": False}, 

858 ) 

859 

860 first_idx, first_attn_block = attn_blocks[0] 

861 

862 # Check if b_V exists 

863 if not hasattr(first_attn_block.attn, "b_V"): 

864 return BenchmarkResult( 

865 name="value_bias_folding", 

866 severity=BenchmarkSeverity.INFO, 

867 message="No value bias found (expected for models without biases)", 

868 details={"has_bias": False}, 

869 ) 

870 

871 b_v = first_attn_block.attn.b_V 

872 

873 if b_v is None: 

874 return BenchmarkResult( 

875 name="value_bias_folding", 

876 severity=BenchmarkSeverity.INFO, 

877 message="Value bias is None (expected for models without biases)", 

878 details={"has_bias": False}, 

879 ) 

880 

881 # Check if b_V is approximately zero 

882 max_abs = torch.max(torch.abs(b_v)).item() 

883 tolerance = 1e-6 

884 

885 if max_abs < tolerance: 

886 return BenchmarkResult( 

887 name="value_bias_folding", 

888 severity=BenchmarkSeverity.INFO, 

889 message=f"Value bias folding verified (max_abs={max_abs:.6e})", 

890 details={"max_abs": max_abs, "tolerance": tolerance}, 

891 ) 

892 else: 

893 return BenchmarkResult( 

894 name="value_bias_folding", 

895 severity=BenchmarkSeverity.WARNING, 

896 message=f"Value bias not zero after folding (max_abs={max_abs:.6e})", 

897 details={"max_abs": max_abs, "tolerance": tolerance}, 

898 passed=False, 

899 ) 

900 

901 except Exception as e: 

902 return BenchmarkResult( 

903 name="value_bias_folding", 

904 severity=BenchmarkSeverity.ERROR, 

905 message=f"Value bias folding check failed: {str(e)}", 

906 passed=False, 

907 ) 

908 

909 

910def benchmark_no_nan_inf( 

911 bridge: TransformerBridge, 

912 test_text: str, 

913 reference_model: Optional[HookedTransformer] = None, 

914) -> BenchmarkResult: 

915 """Benchmark that weights contain no NaN or Inf values. 

916 

917 Args: 

918 bridge: TransformerBridge model to test 

919 test_text: Input text for testing 

920 reference_model: Optional HookedTransformer reference model (not used) 

921 

922 Returns: 

923 BenchmarkResult with NaN/Inf verification details 

924 """ 

925 try: 

926 # Get state dict from original model 

927 state_dict = bridge.state_dict() 

928 

929 # Check for NaN/Inf in all tensors 

930 nan_keys = [] 

931 inf_keys = [] 

932 

933 for key, value in state_dict.items(): 

934 if torch.isnan(value).any(): 

935 nan_keys.append(key) 

936 if torch.isinf(value).any(): 

937 inf_keys.append(key) 

938 

939 if nan_keys or inf_keys: 

940 message_parts = [] 

941 if nan_keys: 

942 message_parts.append(f"NaN in {len(nan_keys)} tensors") 

943 if inf_keys: 

944 message_parts.append(f"Inf in {len(inf_keys)} tensors") 

945 

946 return BenchmarkResult( 

947 name="no_nan_inf", 

948 severity=BenchmarkSeverity.DANGER, 

949 message=f"Invalid values found: {', '.join(message_parts)}", 

950 details={"nan_keys": nan_keys, "inf_keys": inf_keys}, 

951 passed=False, 

952 ) 

953 

954 return BenchmarkResult( 

955 name="no_nan_inf", 

956 severity=BenchmarkSeverity.INFO, 

957 message="No NaN or Inf values found in weights", 

958 details={"num_tensors_checked": len(state_dict)}, 

959 ) 

960 

961 except Exception as e: 

962 return BenchmarkResult( 

963 name="no_nan_inf", 

964 severity=BenchmarkSeverity.ERROR, 

965 message=f"NaN/Inf check failed: {str(e)}", 

966 passed=False, 

967 ) 

968 

969 

970def benchmark_weight_magnitudes( 

971 bridge: TransformerBridge, 

972 test_text: str, 

973 reference_model: Optional[HookedTransformer] = None, 

974) -> BenchmarkResult: 

975 """Benchmark that weight magnitudes are in reasonable ranges. 

976 

977 Args: 

978 bridge: TransformerBridge model to test 

979 test_text: Input text for testing 

980 reference_model: Optional HookedTransformer reference model (not used) 

981 

982 Returns: 

983 BenchmarkResult with weight magnitude verification details 

984 """ 

985 try: 

986 # Get state dict from original model 

987 state_dict = bridge.state_dict() 

988 

989 # Check magnitude ranges 

990 too_small_keys = [] 

991 too_large_keys = [] 

992 

993 min_threshold = 1e-6 

994 max_threshold = 1000.0 

995 

996 # For rmsnorm_uses_offset models (Gemma/Gemma2), fold_ln sets LN weights 

997 # to 0.0 (identity for (1+w) normalization). Skip LN weights for these models. 

998 cfg = getattr(getattr(bridge, "adapter", None), "cfg", None) 

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

1000 

1001 for key, value in state_dict.items(): 

1002 # Skip non-weight tensors (buffers, etc.) 

1003 if "weight" not in key and "bias" not in key: 

1004 continue 

1005 

1006 # Skip internal _original_component keys - these are implementation details 

1007 if "_original_component" in key: 

1008 continue 

1009 

1010 # Skip value biases - they are expected to be zero after folding 

1011 if ".v.bias" in key: 

1012 continue 

1013 

1014 # Skip attention projection biases - they can be zero in some models 

1015 if ( 

1016 ".k_proj.bias" in key 

1017 or ".q_proj.bias" in key 

1018 or ".v_proj.bias" in key 

1019 or ".o_proj.bias" in key 

1020 or ".k.bias" in key 

1021 or ".q.bias" in key 

1022 or ".v.bias" in key 

1023 or ".o.bias" in key 

1024 ): 

1025 continue 

1026 

1027 # Skip layer norm biases - they are expected to be zero after folding 

1028 if ( 

1029 "ln1.bias" in key 

1030 or "ln2.bias" in key 

1031 or "ln_1.bias" in key 

1032 or "ln_2.bias" in key 

1033 or "ln_final.bias" in key 

1034 or "input_layernorm.bias" in key 

1035 or "post_attention_layernorm.bias" in key 

1036 ): 

1037 continue 

1038 

1039 # For rmsnorm_uses_offset models, fold_ln sets LN weights to 0.0 

1040 # (identity for (1+w) normalization). Skip all LN weight keys — 

1041 # including post-norms (ln1_post, ln2_post) which aren't folded but 

1042 # use the same (1+w) convention — to avoid false magnitude warnings. 

1043 if rmsnorm_uses_offset and ( 

1044 "ln1.weight" in key 

1045 or "ln2.weight" in key 

1046 or "ln1_post.weight" in key 

1047 or "ln2_post.weight" in key 

1048 or "ln_1.weight" in key 

1049 or "ln_2.weight" in key 

1050 or "ln_final.weight" in key 

1051 or "input_layernorm.weight" in key 

1052 or "post_attention_layernorm.weight" in key 

1053 ): 

1054 continue 

1055 

1056 # Skip unembed bias - it may be zero after processing 

1057 if "unembed.bias" in key: 

1058 continue 

1059 

1060 # Skip zero biases - many models initialize biases to zero which is 

1061 # mathematically equivalent to having no bias. This is a valid state. 

1062 if "bias" in key and torch.all(value == 0).item(): 

1063 continue 

1064 

1065 mean_abs = torch.mean(torch.abs(value)).item() 

1066 max_abs = torch.max(torch.abs(value)).item() 

1067 

1068 if mean_abs > 0.0 and mean_abs < min_threshold: 

1069 # For non-zero weights, check if they're suspiciously small 

1070 too_small_keys.append((key, mean_abs)) 

1071 

1072 if max_abs > max_threshold: 

1073 too_large_keys.append((key, max_abs)) 

1074 

1075 if too_small_keys or too_large_keys: 

1076 message_parts = [] 

1077 if too_small_keys: 

1078 message_parts.append(f"{len(too_small_keys)} too small") 

1079 if too_large_keys: 

1080 message_parts.append(f"{len(too_large_keys)} too large") 

1081 

1082 return BenchmarkResult( 

1083 name="weight_magnitudes", 

1084 severity=BenchmarkSeverity.WARNING, 

1085 message=f"Weight magnitude issues: {', '.join(message_parts)}", 

1086 details={ 

1087 "too_small": too_small_keys[:5], # Limit to first 5 

1088 "too_large": too_large_keys[:5], # Limit to first 5 

1089 }, 

1090 passed=False, 

1091 ) 

1092 

1093 return BenchmarkResult( 

1094 name="weight_magnitudes", 

1095 severity=BenchmarkSeverity.INFO, 

1096 message="All weight magnitudes in reasonable ranges", 

1097 details={"min_threshold": min_threshold, "max_threshold": max_threshold}, 

1098 ) 

1099 

1100 except Exception as e: 

1101 return BenchmarkResult( 

1102 name="weight_magnitudes", 

1103 severity=BenchmarkSeverity.ERROR, 

1104 message=f"Weight magnitude check failed: {str(e)}", 

1105 passed=False, 

1106 )