Coverage for transformer_lens/model_bridge/generalized_components/block.py: 90%

250 statements  

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

1"""Block bridge component. 

2 

3This module contains the bridge component for transformer blocks. 

4""" 

5from __future__ import annotations 

6 

7import inspect 

8import re 

9import weakref 

10from typing import Any, Callable, Dict, Optional, cast 

11 

12import torch 

13 

14from transformer_lens.hook_points import HookPoint 

15from transformer_lens.model_bridge.exceptions import StopAtLayerException 

16from transformer_lens.model_bridge.generalized_components.base import ( 

17 GeneralizedComponent, 

18) 

19 

20# Layer-type variant submodule names. Tuple for deterministic iteration order. 

21# Extend here when adding new hybrid variant types. 

22VARIANT_SUBMODULE_NAMES: tuple[str, ...] = ("attn", "linear_attn", "mamba", "mixer", "ssm") 

23_VARIANT_SUBMODULE_SET: frozenset[str] = frozenset(VARIANT_SUBMODULE_NAMES) 

24 

25# Infrastructure modules excluded from submodule introspection. 

26_BLOCK_INTERNAL_MODULES: frozenset[str] = frozenset({"hook_in", "hook_out", "_original_component"}) 

27 

28# Norm-module prefixes excluded from layer_types() labels. 

29_NORM_PREFIXES: tuple[str, ...] = ("ln", "layer_norm", "norm", "rms") 

30 

31 

32class BlockBridge(GeneralizedComponent): 

33 """Bridge component for transformer blocks. 

34 

35 This component provides standardized input/output hooks and monkey-patches 

36 HuggingFace blocks to insert hooks at positions matching HookedTransformer. 

37 """ 

38 

39 is_list_item: bool = True 

40 hook_out_is_single_residual_stream: bool = True 

41 # hook_mlp_in is a direct HookPoint on this class (not aliased) so it can 

42 # fire on the MLP-branch entry (pre-ln2, or the MLP input on post-norm 

43 # blocks); see __init__. The normalized mlp input stays at block.mlp.hook_in. 

44 hook_aliases = { 

45 "hook_resid_pre": "hook_in", 

46 "hook_resid_mid": "ln2.hook_in", 

47 "hook_resid_post": "hook_out", 

48 "hook_attn_in": "attn.hook_attn_in", 

49 "hook_attn_out": "attn.hook_out", 

50 "hook_q_input": "attn.hook_q_input", 

51 "hook_k_input": "attn.hook_k_input", 

52 "hook_v_input": "attn.hook_v_input", 

53 "hook_mlp_out": "mlp.hook_out", 

54 } 

55 

56 def __init__( 

57 self, 

58 name: str, 

59 config: Optional[Any] = None, 

60 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

61 hook_alias_overrides: Optional[Dict[str, str]] = None, 

62 mlp_reads_resid_directly: bool = False, 

63 ): 

64 """Initialize the block bridge. 

65 

66 Args: 

67 name: The name of the component in the model 

68 config: Optional configuration (unused for BlockBridge) 

69 submodules: Dictionary of submodules to register 

70 hook_alias_overrides: Optional dictionary to override default hook aliases. 

71 For example, {"hook_attn_out": "ln1_post.hook_out"} will make hook_attn_out 

72 point to ln1_post.hook_out instead of the default attn.hook_out. 

73 mlp_reads_resid_directly: True for post-norm blocks where the MLP consumes 

74 the mid-residual with no pre-MLP norm (OLMo 2 layout). Moves the 

75 hook_mlp_in capture from ln2 (whose input there is the raw MLP output) 

76 to the MLP itself. 

77 """ 

78 # ln1_post/ln2_post redirect attn_out/mlp_out to match HookedTransformer's 

79 # placement (hook fires after the post-norm, not before). 

80 auto_overrides = {} 

81 if submodules is not None: 

82 if "ln1_post" in submodules: 

83 auto_overrides["hook_attn_out"] = "ln1_post.hook_out" 

84 if "ln2_post" in submodules: 

85 auto_overrides["hook_mlp_out"] = "ln2_post.hook_out" 

86 merged_overrides = {**auto_overrides, **(hook_alias_overrides or {})} 

87 

88 # Guard the sequential-block case: attn + mlp with no ln2 would silently 

89 # point hook_resid_mid at the wrong tensor. Use ParallelBlockBridge for 

90 # parallel-residual architectures. 

91 # Skip the check on generic-container / attn-only uses (no mlp). 

92 has_attn_like = submodules is not None and any( 

93 k in submodules for k in _VARIANT_SUBMODULE_SET 

94 ) 

95 has_mlp = submodules is not None and "mlp" in submodules 

96 has_ln2 = submodules is not None and "ln2" in submodules 

97 if has_attn_like and has_mlp and not has_ln2 and type(self) is BlockBridge: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true

98 raise ValueError( 

99 f"BlockBridge at '{name}': 'ln2' submodule not declared. " 

100 f"Either declare ln2, or use ParallelBlockBridge for a " 

101 f"parallel-residual architecture." 

102 ) 

103 

104 # Call parent with merged overrides 

105 super().__init__( 

106 name, 

107 config, 

108 submodules=submodules if submodules is not None else {}, 

109 hook_alias_overrides=merged_overrides if merged_overrides else None, 

110 ) 

111 

112 self._original_block_forward: Optional[Callable[..., Any]] = None 

113 self._capture_hooks_wired: bool = False 

114 self._capture_hook_handles: list[torch.utils.hooks.RemovableHandle] = [] 

115 # Fallback for _read_use_hook_mlp_in when block.config is None. 

116 self._use_hook_mlp_in: bool = False 

117 self.mlp_reads_resid_directly = mlp_reads_resid_directly 

118 # Fires on the MLP-branch entry (pre-ln2, or the MLP input on post-norm 

119 # blocks) when use_hook_mlp_in is set. See #1317. 

120 self.hook_mlp_in = HookPoint() 

121 

122 def _wire_ln1_module(self) -> None: 

123 """Keep the raw ln1 execution reference outside the ownership tree.""" 

124 from transformer_lens.model_bridge.generalized_components.attention import ( 

125 AttentionBridge, 

126 ) 

127 

128 ln1 = self.submodules.get("ln1") if self.submodules else None 

129 attn = self.submodules.get("attn") if self.submodules else None 

130 if not isinstance(attn, AttentionBridge): 

131 return 

132 

133 ln1_module = None 

134 if ( 

135 ln1 is not None 

136 and getattr(attn, "supports_split_qkv_fork", False) 

137 and getattr(ln1, "original_component", None) is not None 

138 ): 

139 ln1_module = ln1.original_component 

140 

141 attn._modules.pop("_ln1_module", None) 

142 object.__setattr__(attn, "_ln1_module", ln1_module) 

143 

144 def _maybe_wire_capture_hooks(self) -> None: 

145 """Install the block's capture hooks (split-qkv fork, hook_mlp_in). 

146 

147 Registered on the bridge submodule, not ``original_component`` — the 

148 manual bridge forward never calls the raw module. Idempotent. 

149 """ 

150 self._wire_ln1_module() 

151 if self._capture_hooks_wired: 

152 return 

153 from transformer_lens.model_bridge.generalized_components.attention import ( 

154 AttentionBridge, 

155 ) 

156 

157 ln1 = self.submodules.get("ln1") if self.submodules else None 

158 attn = self.submodules.get("attn") if self.submodules else None 

159 if ( 

160 ln1 is not None 

161 and isinstance(attn, AttentionBridge) 

162 and getattr(attn, "supports_split_qkv_fork", False) 

163 and getattr(ln1, "original_component", None) is not None 

164 ): 

165 attn_ref = cast(AttentionBridge, weakref.proxy(attn)) 

166 

167 def _capture_pre_ln1(_module: torch.nn.Module, args: tuple) -> None: 

168 if args and isinstance(args[0], torch.Tensor): 168 ↛ exitline 168 didn't return from function '_capture_pre_ln1' because the condition on line 168 was always true

169 attn_ref._captured_pre_ln_residual = args[0] 

170 

171 handle = ln1.register_forward_pre_hook(_capture_pre_ln1) 

172 self._capture_hook_handles.append(handle) 

173 

174 # hook_mlp_in must capture the MLP-branch entry point: ln2's input on 

175 # pre-norm blocks, the MLP's own input on post-norm blocks (where ln2 

176 # follows the MLP and its input is the raw MLP output). 

177 if self.mlp_reads_resid_directly: 

178 capture_target = self.submodules.get("mlp") if self.submodules else None 

179 else: 

180 capture_target = self.submodules.get("ln2") if self.submodules else None 

181 if ( 

182 capture_target is not None 

183 and getattr(capture_target, "original_component", None) is not None 

184 ): 

185 hook_mlp_in = self.hook_mlp_in 

186 block_ref = weakref.proxy(self) 

187 

188 def _capture_mlp_in(_module: torch.nn.Module, args: tuple) -> Any: 

189 if not block_ref._read_use_hook_mlp_in(): 

190 return None 

191 if args and isinstance(args[0], torch.Tensor): 191 ↛ 194line 191 didn't jump to line 194 because the condition on line 191 was always true

192 hooked = hook_mlp_in(args[0]) 

193 return (hooked,) + args[1:] 

194 return None 

195 

196 handle = capture_target.register_forward_pre_hook(_capture_mlp_in) 

197 self._capture_hook_handles.append(handle) 

198 

199 self._capture_hooks_wired = True 

200 

201 def _teardown_capture_hooks(self) -> None: 

202 """Remove the capture hooks installed by _maybe_wire_capture_hooks (and subclass extensions).""" 

203 for handle in self._capture_hook_handles: 

204 handle.remove() 

205 self._capture_hook_handles.clear() 

206 self._capture_hooks_wired = False 

207 

208 def _read_use_hook_mlp_in(self) -> bool: 

209 """Prefer ``block.config.use_hook_mlp_in``; fall back to the block-local flag.""" 

210 cfg = self.config 

211 if cfg is not None and hasattr(cfg, "use_hook_mlp_in"): 

212 return bool(cfg.use_hook_mlp_in) 

213 return self._use_hook_mlp_in 

214 

215 def _clear_attention_capture(self) -> None: 

216 """Release the transient residual captured for attention input forks.""" 

217 from transformer_lens.model_bridge.generalized_components.attention import ( 

218 AttentionBridge, 

219 ) 

220 

221 attn = self.submodules.get("attn") if self.submodules else None 

222 if isinstance(attn, AttentionBridge): 

223 attn._captured_pre_ln_residual = None 

224 

225 def forward(self, *args: Any, **kwargs: Any) -> Any: 

226 """Forward pass through the block bridge. 

227 

228 Args: 

229 *args: Input arguments 

230 **kwargs: Input keyword arguments 

231 

232 Returns: 

233 The output from the original component 

234 

235 Raises: 

236 StopAtLayerException: If stop_at_layer is set and this block should stop execution 

237 """ 

238 if self.original_component is None: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true

239 raise RuntimeError( 

240 f"Original component not set for {self.name}. Call set_original_component() first." 

241 ) 

242 

243 self._maybe_wire_capture_hooks() 

244 self._clear_attention_capture() 

245 self._check_stop_at_layer(*args, **kwargs) 

246 args, kwargs = self._hook_input_hidden_states(args, kwargs) 

247 

248 # Filter kwargs to only include parameters accepted by the original component 

249 # This prevents errors when passing encoder-specific params to decoder-only models 

250 filtered_kwargs = self._filter_kwargs_for_forward(kwargs, len(args)) 

251 

252 try: 

253 output = self.original_component(*args, **filtered_kwargs) 

254 finally: 

255 self._clear_attention_capture() 

256 force_tuple_for_bare_tensor = self._is_standalone_hidden_state_call(args, filtered_kwargs) 

257 return self._apply_output_hook( 

258 output, force_tuple_for_bare_tensor=force_tuple_for_bare_tensor 

259 ) 

260 

261 def _apply_output_hook( 

262 self, 

263 output: Any, 

264 wrap_single_element: bool = True, 

265 force_tuple_for_bare_tensor: bool = False, 

266 ) -> Any: 

267 """Hook the primary tensor in the output and return the result. 

268 

269 Args: 

270 output: Raw output from the original component (tensor or tuple). 

271 wrap_single_element: If True, single-element tuples stay as tuples after 

272 hooking (default, required by most HF models). If False, single-element 

273 tuples are unwrapped to a bare tensor (Bloom convention). 

274 force_tuple_for_bare_tensor: If True, bare tensor outputs are wrapped into 

275 a one-element tuple after hooking. This keeps standalone BlockBridge 

276 calls compatible with HF block APIs that expose tuple-like block outputs, 

277 while preserving tensor outputs during newer HF parent-model execution. 

278 """ 

279 if isinstance(output, tuple) and len(output) > 0: 

280 first = output[0] 

281 if isinstance(first, torch.Tensor): 281 ↛ 286line 281 didn't jump to line 286 because the condition on line 281 was always true

282 first = self.hook_out(first) 

283 if len(output) == 1: 

284 return (first,) if wrap_single_element else first 

285 output = (first,) + output[1:] 

286 return output 

287 if isinstance(output, torch.Tensor): 

288 output = self.hook_out(output) 

289 if force_tuple_for_bare_tensor and wrap_single_element: 

290 return (output,) 

291 return output 

292 return output 

293 

294 @staticmethod 

295 def _is_standalone_hidden_state_call(args: tuple, kwargs: dict) -> bool: 

296 """Return True for direct block(hidden_states) style calls. 

297 

298 Transformers versions differ on whether parent model loops expect block 

299 outputs as tuples or tensors. We preserve the original tensor return during 

300 full-model execution, but expose tuple-like output for standalone component 

301 calls so `output[0]` does not accidentally drop the batch dimension. 

302 """ 

303 if len(args) == 1 and isinstance(args[0], torch.Tensor) and not kwargs: 

304 return True 

305 return ( 

306 len(args) == 0 

307 and set(kwargs.keys()) == {"hidden_states"} 

308 and isinstance(kwargs["hidden_states"], torch.Tensor) 

309 ) 

310 

311 def _extract_layer_idx(self) -> Optional[int]: 

312 """Parse this block's layer index from its name (TL/GPT-2/LLaMA patterns).""" 

313 if self.name is None: 

314 return None 

315 match = re.search(r"(?:^|\.)(?:blocks|h|layers)\.(\d+)", self.name) 

316 return int(match.group(1)) if match else None 

317 

318 def _check_stop_at_layer(self, *args: Any, **kwargs: Any) -> None: 

319 """Check if execution should stop before this block. Raises StopAtLayerException. 

320 

321 The _stop_at_layer_idx attribute is set by the bridge's forward method. 

322 Supports TL/GPT-2/LLaMA naming patterns for layer index extraction. 

323 """ 

324 if not (hasattr(self, "_stop_at_layer_idx") and self._stop_at_layer_idx is not None): 

325 return 

326 if self.name is not None: 326 ↛ 331line 326 didn't jump to line 331 because the condition on line 326 was always true

327 # Anchored alternation: native models name blocks top-level 

328 # ("layers.0"), which the old leading-dot pattern missed entirely. 

329 match = re.search(r"(?:^|\.)(?:blocks|h|layers)\.(\d+)", self.name) 

330 else: 

331 match = None 

332 if match: 332 ↛ exitline 332 didn't return from function '_check_stop_at_layer' because the condition on line 332 was always true

333 layer_idx = int(match.group(1)) 

334 if layer_idx == self._stop_at_layer_idx: 

335 if len(args) > 0 and isinstance(args[0], torch.Tensor): 335 ↛ 337line 335 didn't jump to line 337 because the condition on line 335 was always true

336 input_tensor = args[0] 

337 elif "hidden_states" in kwargs and isinstance( 

338 kwargs["hidden_states"], torch.Tensor 

339 ): 

340 input_tensor = kwargs["hidden_states"] 

341 else: 

342 raise ValueError(f"Cannot find input tensor to stop at layer {layer_idx}") 

343 input_tensor = self.hook_in(input_tensor) 

344 raise StopAtLayerException(input_tensor) 

345 

346 def _hook_input_hidden_states(self, args: tuple, kwargs: dict) -> tuple[tuple, dict]: 

347 """Apply hook_in to the hidden_states input, whether in args or kwargs.""" 

348 if len(args) > 0 and isinstance(args[0], torch.Tensor): 348 ↛ 351line 348 didn't jump to line 351 because the condition on line 348 was always true

349 hooked_input = self.hook_in(args[0]) 

350 args = (hooked_input,) + args[1:] 

351 elif "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], torch.Tensor): 

352 kwargs["hidden_states"] = self.hook_in(kwargs["hidden_states"]) 

353 return args, kwargs 

354 

355 def _filter_kwargs_for_forward( 

356 self, kwargs: Dict[str, Any], num_positional_args: int = 0 

357 ) -> Dict[str, Any]: 

358 """Filter kwargs to only include parameters accepted by original_component.forward(). 

359 

360 This prevents TypeErrors when the bridge passes parameters (like encoder_attention_mask) 

361 that aren't accepted by decoder-only models. It also removes any kwargs that would 

362 conflict with positional arguments already being passed. 

363 

364 Args: 

365 kwargs: The full set of keyword arguments 

366 num_positional_args: Number of positional arguments being passed (to avoid conflicts) 

367 

368 Returns: 

369 Filtered kwargs containing only accepted parameters 

370 """ 

371 if self.original_component is None: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true

372 return kwargs 

373 

374 try: 

375 # Get the signature of the original component's forward method 

376 sig = inspect.signature(self.original_component.forward) 

377 param_list = list(sig.parameters.keys()) 

378 valid_params = set(param_list) 

379 

380 # Check if the signature accepts **kwargs (VAR_KEYWORD) 

381 accepts_var_keyword = any( 

382 p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() 

383 ) 

384 

385 # If it accepts **kwargs, pass everything through 

386 if accepts_var_keyword: 

387 return kwargs 

388 

389 # Skip params already provided positionally 

390 positional_param_names = set(param_list[:num_positional_args]) 

391 

392 # Filter kwargs: include only if in signature AND not already provided positionally 

393 filtered = { 

394 k: v 

395 for k, v in kwargs.items() 

396 if k in valid_params and k not in positional_param_names 

397 } 

398 return filtered 

399 

400 except (ValueError, TypeError): 

401 # If we can't inspect the signature, pass through all kwargs 

402 # (better to potentially fail than to silently drop important params) 

403 return kwargs 

404 

405 

406class MLABlockBridge(BlockBridge): 

407 """Block wrapping Multi-Head Latent Attention (DeepSeek V2/V3/R1). 

408 

409 MLA has no standalone q/k/v projections — Q flows through compressed 

410 q_a_proj→q_a_layernorm→q_b_proj, and K/V share a joint kv_a_proj_with_mqa 

411 entry point. There is no single HookPoint that represents "input that 

412 becomes Q/K/V", so the block-level ``hook_q_input``/``hook_k_input``/ 

413 ``hook_v_input``/``hook_attn_in`` aliases do not apply. Type-level 

414 distinction means a reader of the adapter sees ``MLABlockBridge`` and 

415 knows those hooks are absent. 

416 """ 

417 

418 def __init__( 

419 self, 

420 name: str, 

421 config: Optional[Any] = None, 

422 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

423 hook_alias_overrides: Optional[Dict[str, str]] = None, 

424 ): 

425 super().__init__( 

426 name, 

427 config=config, 

428 submodules=submodules, 

429 hook_alias_overrides=hook_alias_overrides, 

430 ) 

431 if self.hook_aliases is BlockBridge.hook_aliases: 431 ↛ 433line 431 didn't jump to line 433 because the condition on line 431 was always true

432 self.hook_aliases = dict(self.hook_aliases) 

433 for alias in ("hook_q_input", "hook_k_input", "hook_v_input", "hook_attn_in"): 

434 self.hook_aliases.pop(alias, None) 

435 

436 

437class ParallelBlockBridge(BlockBridge): 

438 """Block where attn and MLP both read the pre-attention residual. 

439 

440 For GPT-J, NeoX, Pythia, Phi, Cohere, CodeGen, and some Falcon variants, 

441 output = resid_pre + attn_out + mlp_out — no distinct post-attention 

442 residual exists. Matches legacy HookedTransformer which omits hook_resid_mid 

443 when ``cfg.parallel_attn_mlp=True``. Type-level distinction means a reader 

444 of the adapter sees ``ParallelBlockBridge`` and knows the hook is absent. 

445 """ 

446 

447 def __init__( 

448 self, 

449 name: str, 

450 config: Optional[Any] = None, 

451 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

452 hook_alias_overrides: Optional[Dict[str, str]] = None, 

453 ): 

454 super().__init__( 

455 name, 

456 config=config, 

457 submodules=submodules, 

458 hook_alias_overrides=hook_alias_overrides, 

459 ) 

460 # Ensure instance-level copy before mutating; base may have left the 

461 # class-level dict shared when no overrides were passed. 

462 if self.hook_aliases is BlockBridge.hook_aliases: 462 ↛ 464line 462 didn't jump to line 464 because the condition on line 462 was always true

463 self.hook_aliases = dict(self.hook_aliases) 

464 self.hook_aliases.pop("hook_resid_mid", None) 

465 

466 

467class ScaledResidualBlockBridge(BlockBridge): 

468 """Block whose sublayer outputs are scaled before the residual add. 

469 

470 Granite-family HF blocks compute ``residual + sublayer_out * residual_multiplier`` 

471 inline, so no submodule output equals the tensor added to the residual stream 

472 and the legacy aliases cannot be fixed by re-pointing. ``hook_attn_out`` / 

473 ``hook_mlp_out`` become real HookPoints on the block firing on the scaled 

474 contribution: 

475 

476 - no hooks attached: the forward is untouched (bit-exact); 

477 - read-only hooks: they observe ``module_out * scale``, forward stays bit-exact; 

478 - a hook that changes the tensor (returned new or mutated in place): the module 

479 output is rewritten to ``hooked / scale`` so HF's multiply reconstructs the 

480 written value as the contribution (~1-ulp rounding; exact for zero-ablation); 

481 - any backward hooks: the rewrite always happens so the autograd graph routes 

482 through the HookPoint. 

483 """ 

484 

485 def __init__( 

486 self, 

487 name: str, 

488 config: Optional[Any] = None, 

489 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

490 hook_alias_overrides: Optional[Dict[str, str]] = None, 

491 mlp_reads_resid_directly: bool = False, 

492 residual_contribution_scale: float = 1.0, 

493 scaled_attn_submodule: str = "attn", 

494 scaled_mlp_submodule: Optional[str] = "mlp", 

495 ): 

496 """scaled_mlp_submodule=None when no single submodule feeds the MLP-side 

497 add (GraniteMoeHybrid sums moe + shared_mlp inline) — hook_mlp_out then 

498 stays absent rather than firing with a partial tensor.""" 

499 super().__init__( 

500 name, 

501 config=config, 

502 submodules=submodules, 

503 hook_alias_overrides=hook_alias_overrides, 

504 mlp_reads_resid_directly=mlp_reads_resid_directly, 

505 ) 

506 scale = float(residual_contribution_scale) 

507 if scale == 0.0: 

508 raise ValueError( 

509 f"ScaledResidualBlockBridge at '{name}': residual_contribution_scale " 

510 f"must be nonzero (the write path divides by it)." 

511 ) 

512 self.residual_contribution_scale = scale 

513 self.scaled_attn_submodule = scaled_attn_submodule 

514 self.scaled_mlp_submodule = scaled_mlp_submodule 

515 if self.hook_aliases is BlockBridge.hook_aliases: 515 ↛ 517line 515 didn't jump to line 517 because the condition on line 515 was always true

516 self.hook_aliases = dict(self.hook_aliases) 

517 for alias in ("hook_attn_out", "hook_mlp_out"): 

518 self.hook_aliases.pop(alias, None) 

519 self.hook_attn_out = HookPoint() 

520 if scaled_mlp_submodule is not None: 

521 self.hook_mlp_out = HookPoint() 

522 

523 def set_original_component(self, original_component: torch.nn.Module) -> None: 

524 """Prune contribution HookPoints the bound layer cannot fire. 

525 

526 Heterogeneous blocks (GraniteMoeHybrid mamba layers) lack the attn 

527 submodule; a HookPoint that exists but never fires is a silent-no-op 

528 intervention trap, so the name must be absent instead. 

529 """ 

530 super().set_original_component(original_component) 

531 targets = [(self.scaled_attn_submodule, "hook_attn_out")] 

532 if self.scaled_mlp_submodule is not None: 

533 targets.append((self.scaled_mlp_submodule, "hook_mlp_out")) 

534 for sub_name, hook_name in targets: 

535 sub = self.submodules.get(sub_name) if self.submodules else None 

536 remote = getattr(sub, "name", None) 

537 first = remote.split(".", 1)[0] if isinstance(remote, str) else None 

538 missing = sub is None or ( 

539 first is not None and getattr(original_component, first, None) is None 

540 ) 

541 if missing: 

542 if hasattr(self, hook_name): 542 ↛ 546line 542 didn't jump to line 546 because the condition on line 542 was always true

543 delattr(self, hook_name) 

544 # __setattr__ auto-registered the HookPoint; get_hooks() serves 

545 # from this registry, so the module deletion alone is not enough. 

546 self._hook_registry.pop(hook_name, None) 

547 

548 def _maybe_wire_capture_hooks(self) -> None: 

549 """Extend the base wiring with the scaled-contribution forward hooks. 

550 

551 Shares the base flag and handle list so _teardown_capture_hooks also 

552 removes these hooks and re-wiring stays idempotent. 

553 """ 

554 if self._capture_hooks_wired: 

555 return 

556 super()._maybe_wire_capture_hooks() 

557 targets = [(self.scaled_attn_submodule, "hook_attn_out")] 

558 if self.scaled_mlp_submodule is not None: 

559 targets.append((self.scaled_mlp_submodule, "hook_mlp_out")) 

560 for sub_name, hook_name in targets: 

561 hook_point = getattr(self, hook_name, None) 

562 sub = self.submodules.get(sub_name) if self.submodules else None 

563 if ( 

564 hook_point is None 

565 or sub is None 

566 or getattr(sub, "original_component", None) is None 

567 ): 

568 continue 

569 handle = sub.register_forward_hook(self._make_scaled_contribution_hook(hook_point)) 

570 self._capture_hook_handles.append(handle) 

571 

572 def _make_scaled_contribution_hook( 

573 self, hook_point: HookPoint 

574 ) -> Callable[[torch.nn.Module, tuple, Any], Any]: 

575 """Build a forward hook exposing ``output * scale`` through hook_point.""" 

576 scale = self.residual_contribution_scale 

577 

578 def _hook(_module: torch.nn.Module, _args: tuple, output: Any) -> Any: 

579 if not hook_point.has_hooks(dir="both"): 

580 return None 

581 is_tuple = isinstance(output, tuple) 

582 out = output[0] if is_tuple else output 

583 if not isinstance(out, torch.Tensor): 583 ↛ 584line 583 didn't jump to line 584 because the condition on line 583 was never true

584 return None 

585 scaled = out * scale 

586 hooked = hook_point(scaled) 

587 # Compare against a freshly computed reference: an in-place mutation 

588 # alters `scaled` itself, so an identity check would miss it. 

589 if not hook_point.has_hooks(dir="bwd") and torch.equal(hooked, out * scale): 

590 return None 

591 new = hooked / scale 

592 return ((new,) + output[1:]) if is_tuple else new 

593 

594 return _hook 

595 

596 

597class DelegatedAttentionBlockBridge(BlockBridge): 

598 """Block whose attention is delegated wholesale to HF (no split-qkv fork). 

599 

600 For architectures with heterogeneous per-layer attention structure — e.g. 

601 Gemma 4, where KV-shared layers have no ``k_proj``/``v_proj`` at all and 

602 K==V layers have no ``v_proj`` — there is no uniform HookPoint that 

603 represents "input that becomes Q/K/V", so the block-level ``hook_q_input``/ 

604 ``hook_k_input``/``hook_v_input``/``hook_attn_in`` aliases do not apply. 

605 Type-level distinction means a reader of the adapter sees 

606 ``DelegatedAttentionBlockBridge`` and knows those hooks are absent. 

607 """ 

608 

609 # Tell the component benchmark this block's attention is delegated wholesale 

610 # to HF and cannot be tested in isolation (requires model-specific kwargs). 

611 maintain_native_attention: bool = True 

612 

613 def __init__( 

614 self, 

615 name: str, 

616 config: Optional[Any] = None, 

617 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

618 hook_alias_overrides: Optional[Dict[str, str]] = None, 

619 ): 

620 super().__init__( 

621 name, 

622 config=config, 

623 submodules=submodules, 

624 hook_alias_overrides=hook_alias_overrides, 

625 ) 

626 if self.hook_aliases is BlockBridge.hook_aliases: 

627 self.hook_aliases = dict(self.hook_aliases) 

628 for alias in ("hook_q_input", "hook_k_input", "hook_v_input", "hook_attn_in"): 

629 self.hook_aliases.pop(alias, None)