Coverage for transformer_lens/model_bridge/bridge_core.py: 85%

925 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-10-23 20:45 +0000

1"""Framework-agnostic bridge surface shared by TransformerBridge and RemoteBridge.""" 

2from __future__ import annotations 

3 

4import inspect 

5import re 

6import warnings 

7from contextlib import contextmanager 

8from functools import lru_cache, partial 

9from typing import ( 

10 Any, 

11 Callable, 

12 Dict, 

13 FrozenSet, 

14 Iterable, 

15 Iterator, 

16 List, 

17 Literal, 

18 Mapping, 

19 Optional, 

20 Tuple, 

21 Union, 

22 cast, 

23 overload, 

24) 

25 

26import torch 

27from torch.nn import functional as F 

28 

29from transformer_lens.ActivationCache import ActivationCache 

30from transformer_lens.hook_points import HookPoint, LensHandle 

31from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

32from transformer_lens.model_bridge.driver_protocol import to_torch 

33from transformer_lens.model_bridge.exceptions import StopAtLayerException 

34from transformer_lens.model_bridge.generalized_components.base import alias_generation 

35from transformer_lens.utilities.aliases import resolve_alias 

36from transformer_lens.utilities.lm_utils import lm_cross_entropy_loss 

37from transformer_lens.utilities.slice import Slice, SliceInput 

38 

39# Aliased: the caching methods take a ``remove_batch_dim`` flag that would shadow it. 

40from transformer_lens.utilities.tensors import remove_batch_dim as drop_batch_dim 

41 

42_BLOCK_PATTERN = re.compile("blocks\\.(\\d+)") 

43 

44# Block-list container attributes a bridge may expose. 

45_BLOCK_LIST_ATTRS = ("blocks", "encoder_blocks", "decoder_blocks", "L_blocks", "H_blocks") 

46# Encoder blocks name self-attention ``attn``; decoder blocks name it ``self_attn`` 

47# (``cross_attn`` is a separate submodule, deliberately excluded from stacking). 

48_SELF_ATTENTION_NAMES = {"attn": ("attn", "self_attn")} 

49 

50 

51def _input_batch_size(*inputs: Any) -> Optional[int]: 

52 """The batch size of the first input that states it (a string, list of strings or tensor). 

53 

54 A 1-D tensor is one sequence. Returns None when no input says, so the caller can fall 

55 back to inferring it from what was cached. 

56 """ 

57 for value in inputs: 

58 if isinstance(value, str): 

59 return 1 

60 if isinstance(value, list) and value and all(isinstance(v, str) for v in value): 

61 return len(value) 

62 if isinstance(value, torch.Tensor) and value.ndim > 0: 

63 return 1 if value.ndim == 1 else int(value.shape[0]) 

64 return None 

65 

66 

67def build_alias_to_canonical_map(hook_dict: Any, prefix: str = "") -> dict: 

68 """Map alias hook names to their canonical names (where ``.name`` differs from the key).""" 

69 aliases: dict = {} 

70 for key, value in hook_dict.items(): 

71 full_key = f"{prefix}.{key}" if prefix else key 

72 if isinstance(value, dict): 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true

73 aliases.update(build_alias_to_canonical_map(value, full_key)) 

74 elif hasattr(value, "name"): 74 ↛ 70line 74 didn't jump to line 70 because the condition on line 74 was always true

75 if key != value.name: 

76 aliases[full_key] = value.name 

77 return aliases 

78 

79 

80class BridgeCore: 

81 """Framework-agnostic bridge surface: hooks, cache, run_with_*, driver wiring. 

82 

83 Holds state shared by every bridge (adapter, cfg, tokenizer, driver, hook 

84 registries). Subclasses add framework-specific state — ``TransformerBridge`` 

85 walks the wrapped ``nn.Module``; ``RemoteBridge`` builds components from 

86 adapter metadata. 

87 """ 

88 

89 hook_aliases: Dict[str, Union[str, List[str]]] = { 

90 # Prefer embed_ln.hook_out for post-LN models (Bloom, BERT) 

91 "hook_embed": ["embed_ln.hook_out", "embed.hook_out"], 

92 "hook_pos_embed": ["pos_embed.hook_out", "rotary_emb.hook_out"], 

93 "hook_unembed": "unembed.hook_out", 

94 } 

95 

96 def __init__( 

97 self, 

98 adapter: ArchitectureAdapter, 

99 tokenizer: Any, 

100 driver: Any, 

101 ) -> None: 

102 """Subclasses call this AFTER ``nn.Module.__init__`` (if applicable), 

103 then do framework-specific setup.""" 

104 self.adapter = adapter 

105 self.cfg = adapter.cfg 

106 self._tokenizer = None 

107 if tokenizer is not None: 

108 self.tokenizer = tokenizer # Use the property setter 

109 if self.cfg.d_vocab_out == -1: 

110 self.cfg.d_vocab_out = self.cfg.d_vocab 

111 self.compatibility_mode = False 

112 self._weights_processed = False 

113 self._hook_cache = None 

114 self._hook_registry: Dict[str, HookPoint] = {} 

115 self._hook_registry_initialized = False 

116 self._hook_alias_registry: Dict[str, Union[str, List[str]]] = {} 

117 self._block_alias_cache: Optional[Tuple[Tuple[int, int], Dict[str, str]]] = None 

118 self._property_alias_registry: Dict[str, str] = {} 

119 self.context_level = 0 

120 self._driver = driver 

121 if not hasattr(adapter, "component_mapping") or adapter.component_mapping is None: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true

122 raise ValueError("Adapter must have a component_mapping attribute") 

123 

124 # ---- tokenizer property ---- 

125 

126 @property 

127 def tokenizer(self) -> Any: 

128 """The tokenizer used for encoding/decoding text.""" 

129 return self._tokenizer 

130 

131 @tokenizer.setter 

132 def tokenizer(self, value: Any) -> None: 

133 """Set tokenizer and re-run wiring (d_vocab, BOS/EOS detection, padding). 

134 

135 On initial assignment (during __init__), the boot path has already called 

136 configure_tokenizer, so we skip calling it again. However, we still infer 

137 d_vocab if it wasn't set from the model config (d_vocab == -1). 

138 

139 On reassignment, we re-run configure_tokenizer and update d_vocab to keep 

140 cfg in sync with the new tokenizer. 

141 """ 

142 is_reassignment = getattr(self, "_tokenizer", None) is not None 

143 cfg = getattr(self, "cfg", None) 

144 if value is not None and cfg is not None: 

145 if is_reassignment: 

146 from transformer_lens.model_bridge.sources._bridge_builder import ( 

147 configure_tokenizer, 

148 ) 

149 

150 value = configure_tokenizer(value, cfg) 

151 

152 # Infer d_vocab: on initial assignment only if not set (-1), 

153 # on reassignment always update to match new tokenizer. 

154 # Use getattr for cfg attributes since tests may use SimpleNamespace. 

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

156 if d_vocab == -1 or is_reassignment: 

157 if hasattr(value, "get_vocab"): 157 ↛ 160line 157 didn't jump to line 160 because the condition on line 157 was always true

158 vocab = value.get_vocab() 

159 cfg.d_vocab = max(vocab.values()) + 1 

160 elif hasattr(value, "vocab"): 

161 cfg.d_vocab = max(value.vocab.values()) + 1 

162 else: 

163 cfg.d_vocab = getattr(value, "vocab_size", 50257) 

164 d_vocab_out = getattr(cfg, "d_vocab_out", None) 

165 if d_vocab_out == -1 or is_reassignment: 

166 cfg.d_vocab_out = getattr(cfg, "d_vocab", d_vocab_out) 

167 self._tokenizer = value 

168 

169 # ---- hook registry ---- 

170 

171 def _initialize_hook_registry(self) -> None: 

172 """Initialize the hook registry by scanning existing components.""" 

173 if self._hook_registry_initialized: 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true

174 return 

175 self._scan_existing_hooks(self, "") 

176 self._hook_registry_initialized = True 

177 

178 def _scan_existing_hooks(self, module: Any, prefix: str = "") -> None: 

179 """Walk components for HookPoint instances. Framework-specific.""" 

180 raise NotImplementedError( 

181 f"{type(self).__name__} must implement _scan_existing_hooks " 

182 "(walks the component tree to find HookPoint instances)." 

183 ) 

184 

185 def clear_hook_registry(self) -> None: 

186 """Clear the hook registry and force re-initialization.""" 

187 self._hook_registry.clear() 

188 self._hook_registry_initialized = False 

189 

190 @property 

191 def hook_dict(self) -> dict[str, HookPoint]: 

192 """All HookPoint objects, including aliases — TransformerLens-compatible.""" 

193 hooks = self._hook_registry.copy() 

194 self._add_aliases_to_hooks(hooks) 

195 return hooks 

196 

197 @property 

198 def mod_dict(self) -> Dict[str, Any]: 

199 """Module/hook name -> object, HookedRootModule-compatible. 

200 

201 Union of the named-module tree and the aliased hook view, so both 

202 canonical (``blocks.0.mlp.hook_out``) and HT-style 

203 (``blocks.0.hook_mlp_out``) names resolve to the same HookPoint. 

204 """ 

205 # BridgeCore is a mixin; concrete bridges (TransformerBridge/RemoteBridge) 

206 # are nn.Modules, so named_modules() is always present at runtime. 

207 mods: Dict[str, Any] = { 

208 name: module 

209 for name, module in cast(torch.nn.Module, self).named_modules() 

210 if name != "" 

211 } 

212 mods.update(self.hook_dict) 

213 return mods 

214 

215 # ---- alias registry ---- 

216 

217 def _register_aliases(self) -> None: 

218 """Register bridge-level aliases (hook_embed, hook_pos_embed, etc.) by 

219 resolving each alias target path and installing the target HookPoint 

220 as a direct attribute under the alias name.""" 

221 if self.hook_aliases: 221 ↛ exitline 221 didn't return from function '_register_aliases' because the condition on line 221 was always true

222 self._hook_alias_registry.update(self.hook_aliases) 

223 for alias_name, target_path in self.hook_aliases.items(): 

224 try: 

225 if isinstance(target_path, list): 

226 for single_target in target_path: 

227 try: 

228 target_obj = self 

229 for part in single_target.split("."): 

230 target_obj = getattr(target_obj, part) 

231 object.__setattr__(self, alias_name, target_obj) 

232 break 

233 except AttributeError: 

234 continue 

235 else: 

236 target_obj = self 

237 for part in target_path.split("."): 

238 target_obj = getattr(target_obj, part) 

239 object.__setattr__(self, alias_name, target_obj) 

240 except AttributeError: 

241 pass 

242 

243 def _collect_component_aliases( 

244 self, component_mapping: Any, prefix: str = "", _ancestors: FrozenSet[int] = frozenset() 

245 ) -> dict: 

246 """Recursively collect aliases from the architecture's component templates. 

247 

248 ``_ancestors`` is path-scoped, not globally-visited: a cycle is cut 

249 (else RecursionError at boot) while a diamond-shared component still 

250 contributes aliases under both names. 

251 """ 

252 aliases: dict = {} 

253 if id(component_mapping) in _ancestors: 

254 return aliases 

255 _ancestors = _ancestors | {id(component_mapping)} 

256 if isinstance(component_mapping, dict): 

257 for name, component in component_mapping.items(): 

258 sub_prefix = f"{prefix}.{name}" if prefix else name 

259 aliases.update(self._collect_component_aliases(component, sub_prefix, _ancestors)) 

260 else: 

261 if hasattr(component_mapping, "hook_aliases") and component_mapping.hook_aliases: 

262 for alias_name, target in component_mapping.hook_aliases.items(): 

263 full_alias = f"{prefix}.{alias_name}" if prefix else alias_name 

264 if isinstance(target, list): 

265 # Fallback chain: candidates in declared priority order. 

266 full_target: Any = tuple(f"{prefix}.{t}" if prefix else t for t in target) 

267 else: 

268 full_target = f"{prefix}.{target}" if prefix else target 

269 aliases[full_alias] = full_target 

270 if hasattr(component_mapping, "submodules") and component_mapping.submodules: 

271 for sub_name, sub_component in component_mapping.submodules.items(): 

272 sub_prefix = f"{prefix}.{sub_name}" if prefix else sub_name 

273 aliases.update( 

274 self._collect_component_aliases(sub_component, sub_prefix, _ancestors) 

275 ) 

276 return aliases 

277 

278 @staticmethod 

279 @lru_cache(maxsize=128) 

280 def _compute_hook_aliases_cached( 

281 hook_names_tuple: Tuple[str, ...], 

282 component_aliases_tuple: Tuple[Tuple[str, Union[str, Tuple[str, ...]]], ...], 

283 ) -> Tuple[Tuple[str, str], ...]: 

284 """Cached computation of hook aliases.""" 

285 aliases: dict = {} 

286 # For list-valued (fallback-chain) targets, remember which candidate 

287 # resolved each alias so an earlier (higher-priority) candidate wins. 

288 alias_priority: dict = {} 

289 component_aliases = dict(component_aliases_tuple) 

290 for hook_name in hook_names_tuple: 

291 for alias_pattern, target_patterns in component_aliases.items(): 

292 candidates = ( 

293 target_patterns if isinstance(target_patterns, tuple) else (target_patterns,) 

294 ) 

295 for priority, target_pattern in enumerate(candidates): 

296 if "blocks." in target_pattern and "blocks." in hook_name: 

297 block_match = _BLOCK_PATTERN.search(hook_name) 

298 if not block_match: 298 ↛ 299line 298 didn't jump to line 299 because the condition on line 298 was never true

299 continue 

300 block_num = block_match.group(1) 

301 dynamic_alias_pattern = alias_pattern.replace( 

302 "blocks.", f"blocks.{block_num}." 

303 ) 

304 dynamic_target_pattern = target_pattern.replace( 

305 "blocks.", f"blocks.{block_num}." 

306 ) 

307 if hook_name.endswith(dynamic_target_pattern): 

308 target_len = len(dynamic_target_pattern) 

309 alias_name = hook_name[:-target_len] + dynamic_alias_pattern 

310 if alias_priority.get(alias_name, len(candidates)) > priority: 

311 aliases[alias_name] = hook_name 

312 alias_priority[alias_name] = priority 

313 elif hook_name.endswith(target_pattern): 

314 target_len = len(target_pattern) 

315 alias_name = hook_name[:-target_len] + alias_pattern 

316 if alias_priority.get(alias_name, len(candidates)) > priority: 316 ↛ 295line 316 didn't jump to line 295 because the condition on line 316 was always true

317 aliases[alias_name] = hook_name 

318 alias_priority[alias_name] = priority 

319 return tuple(aliases.items()) 

320 

321 def _collect_hook_aliases_from_registry(self) -> dict: 

322 """Collect aliases based on existing hooks in the registry.""" 

323 if hasattr(self.adapter, "component_mapping"): 

324 component_aliases = self._collect_component_aliases(self.adapter.component_mapping) 

325 hook_names_tuple = tuple(sorted(self._hook_registry.keys())) 

326 component_aliases_tuple = tuple(sorted(component_aliases.items())) 

327 aliases_tuple = self._compute_hook_aliases_cached( 

328 hook_names_tuple, component_aliases_tuple 

329 ) 

330 aliases = dict(aliases_tuple) 

331 aliases.update(self._collect_block_instance_aliases()) 

332 return aliases 

333 return {} 

334 

335 def _collect_block_instance_aliases(self) -> Dict[str, str]: 

336 """Collect per-block-instance aliases, overriding template-derived ones. 

337 

338 Templates cannot see per-layer rebinds (OlmoHybrid, MoE dense/sparse). 

339 Memoized on (registry size, alias generation): size alone misses a 

340 dense<->sparse rebind, which changes targets without changing size. 

341 """ 

342 cache_key = (len(self._hook_registry), alias_generation()) 

343 cached = self._block_alias_cache 

344 if cached is not None and cached[0] == cache_key: 

345 return cached[1] 

346 aliases: Dict[str, str] = {} 

347 unresolved: List[str] = [] 

348 for bl_name in _BLOCK_LIST_ATTRS: 

349 block_list = getattr(self, bl_name, None) 

350 if block_list is None: 

351 continue 

352 for i, block in enumerate(block_list): 

353 # A block with no registered hooks means the registry hasn't 

354 # scanned it yet — unresolved aliases there are timing, not drops. 

355 block_prefix = f"{bl_name}.{i}." 

356 if f"{block_prefix}hook_in" not in self._hook_registry: 

357 continue 

358 # Walk the block and its submodule tree: components rebind 

359 # aliases per layer at bind time either at block level 

360 # (OlmoHybrid) or one level down (MoEBridge's dense/sparse 

361 # dispatch). id()-seen guards against shared/cyclic submodule 

362 # references, which would otherwise hang boot. 

363 # (prefix, component, ids-on-this-path): path-scoped rather than 

364 # globally-visited so a cycle is cut while a component shared 

365 # under two names still contributes aliases at both. 

366 stack: List[Tuple[str, Any, FrozenSet[int]]] = [("", block, frozenset())] 

367 while stack: 

368 sub_prefix, component, ancestors = stack.pop() 

369 if id(component) in ancestors: 

370 continue 

371 ancestors = ancestors | {id(component)} 

372 component_aliases = getattr(component, "hook_aliases", None) 

373 if component_aliases: 

374 for alias_name, target in component_aliases.items(): 

375 targets = target if isinstance(target, list) else [target] 

376 for single_target in targets: 

377 full_target = f"{block_prefix}{sub_prefix}{single_target}" 

378 if full_target in self._hook_registry: 

379 aliases[f"{block_prefix}{sub_prefix}{alias_name}"] = full_target 

380 break 

381 else: 

382 unresolved.append(f"{block_prefix}{sub_prefix}{alias_name}") 

383 for nested_name, nested in ( 

384 getattr(component, "submodules", None) or {} 

385 ).items(): 

386 stack.append((f"{sub_prefix}{nested_name}.", nested, ancestors)) 

387 if unresolved: 

388 # Surface drops instead of silently swallowing, mirroring 

389 # GeneralizedComponent._register_aliases. 

390 warnings.warn( 

391 f"{len(unresolved)} block hook alias(es) did not resolve to a " 

392 f"registered hook (e.g. '{unresolved[0]}'). Any such alias falls " 

393 "back to the architecture template's mapping, which for a " 

394 "per-layer rebind is the wrong tensor for this layer.", 

395 stacklevel=2, 

396 ) 

397 self._block_alias_cache = (cache_key, aliases) 

398 return aliases 

399 

400 def _add_aliases_to_hooks(self, hooks: Dict[str, HookPoint]) -> None: 

401 """Add aliases to hooks in place. Registry-first so RemoteBridge works.""" 

402 component_aliases = self._collect_hook_aliases_from_registry() 

403 all_aliases = {**self.hook_aliases, **component_aliases} 

404 if not all_aliases: 404 ↛ 405line 404 didn't jump to line 405 because the condition on line 404 was never true

405 return 

406 for alias_name, target in all_aliases.items(): 

407 targets = target if isinstance(target, list) else [target] 

408 for t in targets: 

409 hp = self._hook_registry.get(t) 

410 if hp is not None: 

411 hooks[alias_name] = hp 

412 break 

413 # Fall back to attribute walk for TransformerBridge nested paths 

414 # not directly keyed in the registry. 

415 try: 

416 target_hook = resolve_alias(self, alias_name, {alias_name: t}) 

417 if target_hook is not None: 417 ↛ 408line 417 didn't jump to line 408 because the condition on line 417 was always true

418 hooks[alias_name] = target_hook 

419 break 

420 except AttributeError: 

421 continue 

422 

423 # ---- captures from driver ---- 

424 

425 def _replay_captures(self, captured: Mapping[str, Any]) -> None: 

426 """Fire driver-delivered captures through the registry. Unknown names dropped silently.""" 

427 for hook_name, activation in captured.items(): 

428 hp = self._hook_registry.get(hook_name) 

429 if hp is None: 

430 continue 

431 hp(to_torch(activation)) 

432 

433 # ---- driver dispatch (subclasses concrete-override) ---- 

434 

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

436 """Subclasses implement how the driver gets called.""" 

437 raise NotImplementedError(f"{type(self).__name__} must implement forward()") 

438 

439 def to_tokens(self, *args: Any, **kwargs: Any) -> Any: 

440 """Subclasses implement against their tokenizer surface.""" 

441 raise NotImplementedError(f"{type(self).__name__} must implement to_tokens()") 

442 

443 def close(self) -> None: 

444 """Release driver-managed resources. Idempotent — safe to call multiple times.""" 

445 self._driver.close() 

446 

447 def _input_device(self) -> Any: 

448 """Driver's expected device for inputs; None for remote / meta / dispatched drivers.""" 

449 if not self._driver.supports("parameters"): 

450 return None 

451 params = getattr(self._driver, "parameters", None) 

452 if not callable(params): 452 ↛ 453line 452 didn't jump to line 453 because the condition on line 452 was never true

453 return None 

454 try: 

455 device = next(params()).device 

456 except (StopIteration, NotImplementedError, RuntimeError): 

457 return None 

458 # Meta device → inputs would silently become meta tensors with no data. 

459 if device.type == "meta": 

460 return None 

461 return device 

462 

463 def loss_fn( 

464 self, 

465 logits: torch.Tensor, 

466 tokens: torch.Tensor, 

467 attention_mask: Optional[torch.Tensor] = None, 

468 per_token: bool = False, 

469 ) -> torch.Tensor: 

470 """Cross-entropy loss matching HookedTransformer's formula (log_softmax + gather).""" 

471 if tokens.device != logits.device: 471 ↛ 472line 471 didn't jump to line 472 because the condition on line 471 was never true

472 tokens = tokens.to(logits.device) 

473 if attention_mask is not None: 

474 if attention_mask.device != logits.device: 474 ↛ 475line 474 didn't jump to line 475 because the condition on line 474 was never true

475 attention_mask = attention_mask.to(logits.device) 

476 attention_mask = self._prepare_loss_attention_mask(attention_mask, tokens) 

477 return lm_cross_entropy_loss(logits, tokens, attention_mask, per_token) 

478 

479 def _causal_labels_loss( 

480 self, 

481 logits: torch.Tensor, 

482 labels: torch.Tensor, 

483 attention_mask: Optional[torch.Tensor] = None, 

484 per_token: bool = False, 

485 ) -> torch.Tensor: 

486 """Compute shifted causal loss against explicit labels, ignoring ``-100``.""" 

487 if labels.device != logits.device: 487 ↛ 488line 487 didn't jump to line 488 because the condition on line 487 was never true

488 labels = labels.to(logits.device) 

489 if labels.shape != logits.shape[:-1]: 489 ↛ 490line 489 didn't jump to line 490 because the condition on line 489 was never true

490 raise ValueError( 

491 "causal labels must match the logits batch and position dimensions, " 

492 f"got labels {tuple(labels.shape)} and logits {tuple(logits.shape)}" 

493 ) 

494 

495 losses = F.cross_entropy( 

496 logits[:, :-1].flatten(0, 1), 

497 labels[:, 1:].flatten(), 

498 reduction="none", 

499 ignore_index=-100, 

500 ).view_as(labels[:, 1:]) 

501 valid_targets = labels[:, 1:] != -100 

502 if attention_mask is not None: 

503 if attention_mask.device != logits.device: 503 ↛ 504line 503 didn't jump to line 504 because the condition on line 503 was never true

504 attention_mask = attention_mask.to(logits.device) 

505 token_mask = self._prepare_loss_attention_mask(attention_mask, labels) 

506 valid_targets &= token_mask[:, :-1] & token_mask[:, 1:] 

507 losses = losses.masked_fill(~valid_targets, 0.0) 

508 return losses if per_token else losses.sum() / valid_targets.sum() 

509 

510 def _prepare_loss_attention_mask( 

511 self, attention_mask: torch.Tensor, tokens: torch.Tensor 

512 ) -> torch.Tensor: 

513 """Reduce a forward mask using the adapter's 4D boolean polarity.""" 

514 batch, pos = tokens.shape 

515 if attention_mask.ndim not in (2, 4): 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true

516 raise ValueError( 

517 "attention_mask must be 2D [batch, key_pos] or 4D " 

518 f"[batch, *, query_pos, key_pos], got shape {tuple(attention_mask.shape)}" 

519 ) 

520 if attention_mask.shape[0] != batch: 520 ↛ 521line 520 didn't jump to line 521 because the condition on line 520 was never true

521 raise ValueError( 

522 "attention_mask batch dimension must match tokens, " 

523 f"got {attention_mask.shape[0]} and {batch}" 

524 ) 

525 

526 if attention_mask.ndim == 2: 

527 if attention_mask.shape[1] < pos: 527 ↛ 528line 527 didn't jump to line 528 because the condition on line 527 was never true

528 raise ValueError( 

529 "attention_mask must cover every scored token, " 

530 f"got length {attention_mask.shape[1]} for {pos} tokens" 

531 ) 

532 return attention_mask[:, -pos:].bool() 

533 

534 query_pos, key_pos = attention_mask.shape[-2:] 

535 if key_pos < pos: 535 ↛ 536line 535 didn't jump to line 536 because the condition on line 535 was never true

536 raise ValueError( 

537 "attention_mask must cover every scored token, " 

538 f"got key length {key_pos} for {pos} tokens" 

539 ) 

540 

541 if attention_mask.dtype is torch.bool: 

542 blocked = ~attention_mask if self.adapter.bool_4d_mask_is_keep else attention_mask 

543 else: 

544 blocked = attention_mask < -1.0 

545 if query_pos == 1: 

546 # Broadcast key-only masks use one query row for the full sequence. 

547 keep = ~blocked[..., 0, -pos:] 

548 else: 

549 if query_pos < pos: 549 ↛ 550line 549 didn't jump to line 550 because the condition on line 549 was never true

550 raise ValueError( 

551 "attention_mask must contain a query row for every scored token, " 

552 f"got {query_pos} rows for {pos} tokens" 

553 ) 

554 # The aligned diagonal excludes causal masking while retaining padding. 

555 diagonal = torch.diagonal( 

556 blocked, 

557 offset=key_pos - query_pos, 

558 dim1=-2, 

559 dim2=-1, 

560 ) 

561 if diagonal.shape[-1] < pos: 561 ↛ 562line 561 didn't jump to line 562 because the condition on line 561 was never true

562 raise ValueError( 

563 "attention_mask diagonal must cover every scored token, " 

564 f"got length {diagonal.shape[-1]} for {pos} tokens" 

565 ) 

566 keep = ~diagonal[..., -pos:] 

567 

568 # A token is padding only when every broadcast/head mask blocks its key. 

569 return keep.reshape(batch, -1, pos).any(dim=1) 

570 

571 def _finalize_return( 

572 self, 

573 return_type: Optional[str], 

574 logits: Optional[torch.Tensor], 

575 input_ids: Optional[torch.Tensor], 

576 *, 

577 attention_mask: Optional[torch.Tensor] = None, 

578 labels: Optional[torch.Tensor] = None, 

579 is_audio_model: bool = False, 

580 is_visual_model: bool = False, 

581 inputs_embeds_was_used: bool = False, 

582 loss_per_token: bool = False, 

583 ) -> Any: 

584 """Post-process driver output into the user's requested return_type.""" 

585 if return_type == "logits": 

586 return logits 

587 if return_type is None: 

588 return None 

589 self._check_loss_supported(return_type) 

590 if return_type == "loss": 

591 if is_audio_model: 591 ↛ 592line 591 didn't jump to line 592 because the condition on line 591 was never true

592 raise ValueError( 

593 "Audio models do not support return_type='loss'. " 

594 "CTC loss requires aligned frame-level labels." 

595 ) 

596 if is_visual_model: 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

597 raise ValueError( 

598 "Vision classification models do not support return_type='loss' " 

599 "via this path (no next-token LM target exists for image " 

600 "classification). Compute cross-entropy against `labels` " 

601 "yourself from the returned logits, or use hf_generate()-style " 

602 "direct access to self.original_model for HF's own loss." 

603 ) 

604 if inputs_embeds_was_used and labels is None: 604 ↛ 605line 604 didn't jump to line 605 because the condition on line 604 was never true

605 raise ValueError( 

606 "Cannot compute loss with inputs_embeds — token IDs required for labels." 

607 ) 

608 assert isinstance(logits, torch.Tensor), f"Expected logits tensor, got {type(logits)}" 

609 if labels is not None: 

610 return self._causal_labels_loss( 

611 logits, 

612 labels, 

613 attention_mask=attention_mask, 

614 per_token=loss_per_token, 

615 ) 

616 assert input_ids is not None, "input_ids required for return_type='loss'" 

617 return self.loss_fn( 

618 logits, 

619 input_ids, 

620 attention_mask=attention_mask, 

621 per_token=loss_per_token, 

622 ) 

623 if return_type == "both": 

624 if is_audio_model: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true

625 raise ValueError( 

626 "Audio models do not support return_type='both'. " 

627 "CTC loss requires aligned frame-level labels." 

628 ) 

629 if is_visual_model: 629 ↛ 630line 629 didn't jump to line 630 because the condition on line 629 was never true

630 raise ValueError( 

631 "Vision classification models do not support return_type='both' " 

632 "via this path (no next-token LM target exists for image " 

633 "classification). Compute cross-entropy against `labels` " 

634 "yourself from the returned logits." 

635 ) 

636 if inputs_embeds_was_used and labels is None: 636 ↛ 637line 636 didn't jump to line 637 because the condition on line 636 was never true

637 raise ValueError( 

638 "Cannot compute loss with inputs_embeds — token IDs required for labels." 

639 ) 

640 assert isinstance(logits, torch.Tensor), f"Expected logits tensor, got {type(logits)}" 

641 if labels is not None: 

642 loss = self._causal_labels_loss( 

643 logits, 

644 labels, 

645 attention_mask=attention_mask, 

646 per_token=loss_per_token, 

647 ) 

648 else: 

649 assert input_ids is not None, "input_ids required for return_type='both'" 

650 loss = self.loss_fn( 

651 logits, 

652 input_ids, 

653 attention_mask=attention_mask, 

654 per_token=loss_per_token, 

655 ) 

656 return (logits, loss) 

657 if return_type == "predictions": 

658 assert self.tokenizer is not None, "Tokenizer required for return_type='predictions'" 

659 assert isinstance(logits, torch.Tensor), f"Expected logits tensor, got {type(logits)}" 

660 if logits.shape[-1] == 2: 660 ↛ 670line 660 didn't jump to line 670 because the condition on line 660 was always true

661 # Next Sentence Prediction — 2-class output 

662 logprobs = logits.log_softmax(dim=-1) 

663 predictions = [ 

664 "The sentences are sequential", 

665 "The sentences are NOT sequential", 

666 ] 

667 return predictions[int(logprobs.argmax(dim=-1).item())] 

668 else: 

669 # Masked Language Modeling — decode [MASK] tokens 

670 assert input_ids is not None, "input_ids required for MLM predictions" 

671 logprobs = logits[input_ids == self.tokenizer.mask_token_id].log_softmax(dim=-1) 

672 preds = self.tokenizer.decode(logprobs.argmax(dim=-1)) 

673 if " " in preds: 

674 parts = preds.split(" ") 

675 return [f"Prediction {i}: {p}" for i, p in enumerate(parts)] 

676 return preds 

677 raise ValueError(f"Invalid return_type: {return_type}") 

678 

679 def _check_loss_supported(self, return_type: Optional[str]) -> None: 

680 """Loss needs full-sequence logits; final-position-only drivers would NaN.""" 

681 if return_type in ("loss", "both") and not getattr( 

682 self._driver, "provides_sequence_logits", True 

683 ): 

684 raise NotImplementedError( 

685 f"return_type={return_type!r} is unsupported on this driver: it " 

686 "provides next-token logits for the final position only, so loss " 

687 "over earlier positions is undefined. Use return_type='logits' " 

688 "and read logits[..., -1, :]." 

689 ) 

690 

691 # ---- hook lookup / mutation ---- 

692 

693 @staticmethod 

694 def _is_embedding_stage_hook(name: str) -> bool: 

695 """Hooks belonging to the pre-block token/positional embedding stage. 

696 

697 Excluded from ``start_at_layer`` output: the caller's residual already 

698 carries the embedding, so the embedding stage is logically skipped even 

699 though HF still computes (and discards) it. ``unembed``/``hook_unembed`` 

700 are the output stage and deliberately not matched. 

701 """ 

702 return name in ("hook_embed", "hook_pos_embed", "hook_tokens") or name.startswith( 

703 ("embed.", "pos_embed.") 

704 ) 

705 

706 def _check_hook_fireable(self, *names: str) -> None: 

707 """Fail loud when the driver declares it can't fire a requested hook — 

708 attaching anyway would yield a silently-unhooked forward / empty cache.""" 

709 non_fireable: frozenset = getattr(self._driver, "non_fireable_hook_points", frozenset()) 

710 for name in names: 

711 if name in non_fireable: 

712 raise NotImplementedError( 

713 f"this backend cannot fire {name!r}; use boot_transformers() " 

714 "for full hook coverage." 

715 ) 

716 

717 def _resolve_hook_point( 

718 self, name: str, aliases: Dict[str, str], hook_dict: Dict[str, HookPoint] 

719 ) -> Tuple[str, HookPoint]: 

720 """Resolve an (aliased) string hook name to its HookPoint, enforcing 

721 fireability. A name that resolves to nothing raises — a typo'd name must 

722 not run unhooked.""" 

723 canonical = aliases.get(name, name) 

724 self._check_hook_fireable(name, canonical) 

725 hook_point = hook_dict.get(canonical) 

726 if hook_point is None: 

727 raise KeyError(f"Hook name {name!r} does not exist on this model.") 

728 return canonical, hook_point 

729 

730 def get_hook_point(self, hook_name: str) -> Optional[HookPoint]: 

731 """Get a hook point by name from the bridge's hook system.""" 

732 if hook_name in self._hook_registry: 732 ↛ 734line 732 didn't jump to line 734 because the condition on line 732 was always true

733 return self._hook_registry[hook_name] 

734 try: 

735 parts = hook_name.split(".") 

736 current: Any = self 

737 for part in parts: 

738 current = getattr(current, part) 

739 if isinstance(current, HookPoint): 

740 return current 

741 except AttributeError: 

742 pass 

743 return None 

744 

745 def check_hooks_to_add( 

746 self, 

747 hook_point: HookPoint, 

748 hook_point_name: str, 

749 hook: Callable, 

750 dir: Literal["fwd", "bwd"] = "fwd", 

751 is_permanent: bool = False, 

752 prepend: bool = False, 

753 ) -> None: 

754 """Validate a hook before it is added; override to add checks. 

755 

756 Raises for a gated-off hook point — a targeted attach there would 

757 silently never fire. Every explicit-name attach path routes through 

758 here; filter sweeps pre-skip gated matches (with a warning) before 

759 reaching it, since a filter was not necessarily targeting them. 

760 

761 Gating keys on the POINT's own canonical name, not the requested 

762 spelling: on adapters with ``hook_alias_overrides`` a gated HT name 

763 (e.g. ``blocks.0.hook_mlp_in`` on BERT) resolves to an always-firing 

764 point, and refusing it would reject a hook that works. 

765 Driver-fireability is enforced separately in ``_check_hook_fireable``. 

766 """ 

767 reason = self._gated_hook_reason(hook_point.name or hook_point_name) 

768 if reason is not None: 

769 raise ValueError( 

770 f"Cannot add hook {hook_point_name} because {reason} is False. " 

771 f"Call set_{reason}(True) first." 

772 ) 

773 

774 def _add_fn_to_hook_point( 

775 self, 

776 hook_point: HookPoint, 

777 name: str, 

778 hook_fn: Callable, 

779 dir: Literal["fwd", "bwd"], 

780 is_permanent: bool, 

781 ) -> None: 

782 """Run the extension-point check, then attach the hook function.""" 

783 self.check_hooks_to_add(hook_point, name, hook_fn, dir=dir, is_permanent=is_permanent) 

784 hook_point.add_hook(hook_fn, dir=dir, is_permanent=is_permanent) 

785 

786 def _gated_hook_reason(self, hook_point_name: str) -> Optional[str]: 

787 """Return the disabled setter name if hook_point_name is gated off, else None.""" 

788 if hook_point_name.endswith("attn.hook_result") and not self.cfg.use_attn_result: 

789 return "use_attn_result" 

790 if ( 

791 hook_point_name.endswith(("hook_q_input", "hook_k_input", "hook_v_input")) 

792 and not self.cfg.use_split_qkv_input 

793 ): 

794 return "use_split_qkv_input" 

795 if hook_point_name.endswith("mlp_in") and not self.cfg.use_hook_mlp_in: 

796 return "use_hook_mlp_in" 

797 if hook_point_name.endswith("attn_in") and not self.cfg.use_attn_in: 

798 return "use_attn_in" 

799 return None 

800 

801 def _warn_gated_skipped(self, api: str, names: List[str], stacklevel: int = 3) -> None: 

802 """One warning per sweep for filter-matched gated-off names.""" 

803 if names: 

804 warnings.warn( 

805 f"{api}: skipped {len(names)} gated-off hook name(s) " 

806 f"that would never fire: {names}. Call the relevant " 

807 "set_use_*(True) setter first to enable them.", 

808 stacklevel=stacklevel, 

809 ) 

810 

811 def add_hook( 

812 self, 

813 name: Union[str, Callable[[str], bool]], 

814 hook_fn: Any, 

815 dir: Literal["fwd", "bwd"] = "fwd", 

816 is_permanent: bool = False, 

817 ) -> None: 

818 """Add a hook to a specific component or to all components matching a filter. 

819 

820 Args: 

821 name: Either a string hook point name (e.g. "blocks.0.attn.hook_q") 

822 or a callable filter ``(str) -> bool`` that is applied to every 

823 hook point name; the hook is added to each point where the filter 

824 returns True. 

825 hook_fn: The hook function ``(activation, hook) -> activation | None``. 

826 dir: Hook direction, ``"fwd"`` or ``"bwd"``. 

827 is_permanent: If True the hook survives ``reset_hooks()`` calls. 

828 """ 

829 if callable(name) and not isinstance(name, str): 

830 hook_dict = self.hook_dict 

831 seen_hooks: set = set() 

832 gated_names_skipped: List[str] = [] 

833 for hook_name, hook_point in hook_dict.items(): 

834 if name(hook_name): 

835 hook_id = id(hook_point) 

836 if hook_id in seen_hooks: 836 ↛ 837line 836 didn't jump to line 837 because the condition on line 836 was never true

837 continue 

838 seen_hooks.add(hook_id) 

839 # A filter is a sweep, not a targeted request, so a gated-off 

840 # match is skipped rather than raised on — but silently 

841 # attaching here would leave a dead hook that never fires, 

842 # which is the failure this warns about. 

843 if self._gated_hook_reason(hook_point.name or hook_name) is not None: 

844 gated_names_skipped.append(hook_name) 

845 continue 

846 self._add_fn_to_hook_point(hook_point, hook_name, hook_fn, dir, is_permanent) 

847 self._warn_gated_skipped("add_hook", gated_names_skipped) 

848 return 

849 

850 # Explicit-name attaches raise on gated-off points inside 

851 # check_hooks_to_add (via _add_fn_to_hook_point) — every path below 

852 # funnels through it. 

853 # Fast path: canonical registry names skip the alias-map build (hook_dict + 

854 # map construction cost ~ms on large models; add_hook is often called per layer). 

855 registry_hp = self._hook_registry.get(name) 

856 if registry_hp is not None: 

857 self._check_hook_fireable(name) 

858 self._add_fn_to_hook_point(registry_hp, name, hook_fn, dir, is_permanent) 

859 return 

860 # Same alias resolution run_with_hooks uses, so HT-style names work here too. 

861 canonical = build_alias_to_canonical_map(self.hook_dict).get(name, name) 

862 if canonical != name: 

863 self._check_hook_fireable(name, canonical) 

864 else: 

865 self._check_hook_fireable(name) 

866 registry_hp = self._hook_registry.get(canonical) 

867 if registry_hp is not None: 867 ↛ 871line 867 didn't jump to line 871 because the condition on line 867 was always true

868 self._add_fn_to_hook_point(registry_hp, canonical, hook_fn, dir, is_permanent) 

869 return 

870 

871 component: Any = self 

872 parts = name.split(".") 

873 for part in parts[:-1]: 

874 if hasattr(component, part): 

875 component = getattr(component, part) 

876 else: 

877 raise AttributeError(f"Component path '{'.'.join(parts[:-1])}' not found") 

878 hook_name = parts[-1] 

879 if hasattr(component, hook_name): 

880 hook_point = getattr(component, hook_name) 

881 if isinstance(hook_point, HookPoint): 

882 self._add_fn_to_hook_point(hook_point, name, hook_fn, dir, is_permanent) 

883 else: 

884 raise AttributeError( 

885 f"'{hook_name}' is not a hook point. Found object of type: {type(hook_point)} with value: {hook_point}" 

886 ) 

887 else: 

888 raise AttributeError(f"Hook point '{hook_name}' not found on component") 

889 

890 def add_perma_hook( 

891 self, 

892 name: Union[str, Callable[[str], bool]], 

893 hook_fn: Callable, 

894 dir: Literal["fwd", "bwd"] = "fwd", 

895 ) -> None: 

896 """Add a permanent hook that survives ``reset_hooks()`` calls. 

897 

898 Convenience wrapper for ``add_hook(..., is_permanent=True)``. To remove, 

899 call ``reset_hooks(including_permanent=True)`` or remove from the 

900 underlying ``HookPoint`` directly. 

901 """ 

902 self.add_hook(name, hook_fn, dir=dir, is_permanent=True) 

903 

904 def hook_points(self) -> Iterable[HookPoint]: 

905 """All :class:`HookPoint` instances (registry is canonical and complete).""" 

906 return self._hook_registry.values() 

907 

908 def clear_contexts(self) -> None: 

909 """Clear the stored ``ctx`` on every hook point.""" 

910 for hp in self._hook_registry.values(): 

911 hp.clear_context() 

912 

913 def remove_all_hook_fns( 

914 self, 

915 direction: Literal["fwd", "bwd", "both"] = "both", 

916 including_permanent: bool = False, 

917 level: Optional[int] = None, 

918 ) -> None: 

919 """Remove hook functions from every hook point.""" 

920 for hp in self._hook_registry.values(): 

921 hp.remove_hooks(direction, including_permanent=including_permanent, level=level) 

922 

923 def reset_hooks( 

924 self, 

925 clear_contexts: bool = True, 

926 direction: Literal["fwd", "bwd", "both"] = "both", 

927 including_permanent: bool = False, 

928 level: Optional[int] = None, 

929 ) -> None: 

930 """Remove hooks from every hook point; mirrors ``HookedRootModule.reset_hooks``. 

931 

932 The hook registry is canonical and complete (every component's HookPoint 

933 is registered), so a single pass covers the whole model. 

934 

935 Args: 

936 clear_contexts: Also clear each hook point's stored ``ctx``. 

937 direction: Which direction(s) to remove — ``"fwd"``, ``"bwd"``, or ``"both"``. 

938 including_permanent: If True, also remove hooks added via ``add_perma_hook``. 

939 level: If set, only remove hooks registered at this context level. 

940 """ 

941 if clear_contexts: 

942 self.clear_contexts() 

943 self.remove_all_hook_fns(direction, including_permanent=including_permanent, level=level) 

944 

945 @staticmethod 

946 def _normalize_names_filter( 

947 names_filter: Optional[Union[str, List[str], Callable[[str], bool]]], 

948 ) -> Callable[[str], bool]: 

949 """Turn None / str / list / callable into a name predicate.""" 

950 if names_filter is None: 

951 return lambda name: True 

952 if isinstance(names_filter, str): 

953 return lambda name: name == names_filter 

954 if isinstance(names_filter, list): 

955 return lambda name: name in names_filter 

956 if callable(names_filter): 956 ↛ 958line 956 didn't jump to line 958 because the condition on line 956 was always true

957 return names_filter 

958 raise ValueError("names_filter must be None, a string, a list of strings, or a callable") 

959 

960 @staticmethod 

961 def _pos_slice_dim(name: str, hook_point: Any = None) -> int: 

962 """Position axis of a hook's activation in its batched layout. 

963 

964 Position is dim 1 for most bridge activations (resid ``[b, p, d]``, per-head 

965 ``[b, p, h, d]``, token ids ``[b, p]``) and -2 for attention patterns/scores 

966 (``[b, h, q_pos, k_pos]``). Hooks fired on another layout, such as a 

967 post-reshape qk-norm (``[b, h, p, d]``) or a Mamba conv input 

968 (``[b, channels, p]``), declare their axis via ``HookPoint.pos_dim``. 

969 """ 

970 pos_dim = getattr(hook_point, "pos_dim", None) 

971 if pos_dim is not None: 

972 return pos_dim 

973 return -2 if name.endswith(("hook_pattern", "hook_attn_scores")) else 1 

974 

975 def get_caching_hooks( 

976 self, 

977 names_filter: Optional[Union[str, List[str], Callable[[str], bool]]] = None, 

978 incl_bwd: bool = False, 

979 device: Any = None, 

980 remove_batch_dim: bool = False, 

981 cache: Optional[dict] = None, 

982 pos_slice: Optional[Union[Slice, SliceInput]] = None, 

983 ) -> Tuple[dict, list, list]: 

984 """Build caching hooks without adding them. Mirrors ``HookedRootModule.get_caching_hooks``. 

985 

986 ``remove_batch_dim`` drops a leading dimension of size 1 and leaves any other tensor as 

987 it is, so a batch larger than 1 keeps its batch dimension instead of losing examples. 

988 The check is per tensor, so a broadcast activation with a size-1 leading dimension 

989 (e.g. rotary ``hook_cos``/``hook_sin``) is squeezed even when the batch is larger. 

990 Unlike ``run_with_cache``, which raises for a batch larger than 1, a hook sees one tensor 

991 at a time and cannot tell a batch dimension from a flattened ``[batch * pos, ...]`` one. 

992 Hooks that run under ``run_with_hooks(remove_batch_dim=True)`` already receive tensors 

993 without a batch dimension, so set the flag on only one of the two. 

994 

995 Returns ``(cache, fwd_hooks, bwd_hooks)`` where each hook is a 

996 ``(name, hook_fn)`` pair suitable for ``hooks()`` / ``run_with_hooks``. 

997 Activations are keyed by the HookPoint's canonical name, and also by any 

998 alias spelling that ``names_filter`` matched. This is broader than 

999 ``run_with_cache`` for callable filters: one that matches only an alias 

1000 yields both the canonical and alias keys here, while ``run_with_cache`` 

1001 stores only the alias. Backward hooks append ``"_grad"``. ``bwd_hooks`` 

1002 is empty unless ``incl_bwd``. 

1003 """ 

1004 if cache is None: 

1005 cache = {} 

1006 pos_slice_obj = Slice.unwrap(pos_slice) 

1007 filter_fn = self._normalize_names_filter(names_filter) 

1008 

1009 def save_hook( 

1010 tensor: Any, hook: Any, is_backward: bool = False, alias_names: Tuple[str, ...] = () 

1011 ) -> None: 

1012 assert hook.name is not None 

1013 suffix = "_grad" if is_backward else "" 

1014 stored = tensor.detach().to(device) 

1015 # Slice before dropping the batch dim: _pos_slice_dim indexes the 

1016 # batched layout, so the order must match run_with_cache. 

1017 if pos_slice_obj is not None and stored.dim() >= 2: 1017 ↛ 1019line 1017 didn't jump to line 1019 because the condition on line 1017 was always true

1018 stored = pos_slice_obj.apply(stored, dim=self._pos_slice_dim(hook.name, hook)) 

1019 if remove_batch_dim: 

1020 stored = drop_batch_dim(stored) 

1021 for key in (hook.name, *alias_names): 

1022 cache[key + suffix] = stored 

1023 

1024 # One hook point can be reachable under several names (canonical + aliases). 

1025 # Gather every name the filter matched so the cache has an entry under the 

1026 # spelling the caller asked for, not just the canonical one. 

1027 matched: Dict[int, Tuple[str, HookPoint, List[str]]] = {} 

1028 for name, hook_point in self.hook_dict.items(): 

1029 if not filter_fn(name): 

1030 continue 

1031 entry = matched.setdefault(id(hook_point), (name, hook_point, [])) 

1032 if name != hook_point.name: 

1033 entry[2].append(name) 

1034 

1035 fwd_hooks: list = [] 

1036 bwd_hooks: list = [] 

1037 for name, hook_point, alias_names in matched.values(): 

1038 # The default sweep must not emit gated-off points: they never 

1039 # fire (empty cache entries at best), and feeding them to 

1040 # hooks()/run_with_hooks — the advertised composition — would 

1041 # raise. An explicit filter keeps them so downstream can warn. 

1042 if names_filter is None and self._gated_hook_reason(hook_point.name or name): 

1043 continue 

1044 # The unfiltered sweep matches every alias too; keep it canonical-only. 

1045 extra_names = () if names_filter is None else tuple(alias_names) 

1046 fwd_hooks.append((name, partial(save_hook, is_backward=False, alias_names=extra_names))) 

1047 if incl_bwd: 

1048 bwd_hooks.append( 

1049 (name, partial(save_hook, is_backward=True, alias_names=extra_names)) 

1050 ) 

1051 return cache, fwd_hooks, bwd_hooks 

1052 

1053 def add_caching_hooks( 

1054 self, 

1055 names_filter: Optional[Union[str, List[str], Callable[[str], bool]]] = None, 

1056 incl_bwd: bool = False, 

1057 device: Any = None, 

1058 remove_batch_dim: bool = False, 

1059 cache: Optional[dict] = None, 

1060 ) -> dict: 

1061 """Attach caching hooks to the model (does not run it). Returns the cache dict. 

1062 

1063 Mirrors ``HookedRootModule.add_caching_hooks``. The hooks persist until 

1064 ``reset_hooks()``. ``remove_batch_dim`` behaves as in :meth:`get_caching_hooks`: a leading 

1065 dimension of size 1 is dropped and a larger batch keeps its batch dimension. 

1066 """ 

1067 cache, fwd_hooks, bwd_hooks = self.get_caching_hooks( 

1068 names_filter, 

1069 incl_bwd=incl_bwd, 

1070 device=device, 

1071 remove_batch_dim=remove_batch_dim, 

1072 cache=cache, 

1073 ) 

1074 

1075 # Gated-off hook points never fire, so caching them is a no-op; skip 

1076 # them rather than trip add_hook's explicit-name guard. Warn only when 

1077 # the caller EXPLICITLY asked for them (run_with_cache's rule: the 

1078 # default filter matches everything and must not read as a request). 

1079 def _point_reason(name: str): 

1080 hp = self.hook_dict.get(name) 

1081 return self._gated_hook_reason(hp.name or name if hp is not None else name) 

1082 

1083 gated_skipped = [] 

1084 for name, hook_fn in fwd_hooks: 

1085 if _point_reason(name) is None: 

1086 self.add_hook(name, hook_fn, dir="fwd") 

1087 else: 

1088 gated_skipped.append(name) 

1089 for name, hook_fn in bwd_hooks: 1089 ↛ 1090line 1089 didn't jump to line 1090 because the loop on line 1089 never started

1090 if _point_reason(name) is None: 

1091 self.add_hook(name, hook_fn, dir="bwd") 

1092 elif name not in gated_skipped: 

1093 gated_skipped.append(name) 

1094 if names_filter is not None: 

1095 self._warn_gated_skipped("add_caching_hooks", gated_skipped) 

1096 return cache 

1097 

1098 def cache_all( 

1099 self, 

1100 cache: Optional[dict], 

1101 incl_bwd: bool = False, 

1102 device: Any = None, 

1103 remove_batch_dim: bool = False, 

1104 ) -> None: 

1105 """Deprecated: cache every activation. Use ``run_with_cache`` / ``add_caching_hooks``.""" 

1106 warnings.warn( 

1107 "cache_all is deprecated; use run_with_cache or add_caching_hooks.", 

1108 DeprecationWarning, 

1109 stacklevel=2, 

1110 ) 

1111 self.add_caching_hooks( 

1112 names_filter=None, 

1113 cache=cache, 

1114 incl_bwd=incl_bwd, 

1115 device=device, 

1116 remove_batch_dim=remove_batch_dim, 

1117 ) 

1118 

1119 def cache_some( 

1120 self, 

1121 cache: Optional[dict], 

1122 names: Callable[[str], bool], 

1123 incl_bwd: bool = False, 

1124 device: Any = None, 

1125 remove_batch_dim: bool = False, 

1126 ) -> None: 

1127 """Deprecated: cache activations matching ``names``. Use ``run_with_cache``.""" 

1128 warnings.warn( 

1129 "cache_some is deprecated; use run_with_cache or add_caching_hooks.", 

1130 DeprecationWarning, 

1131 stacklevel=2, 

1132 ) 

1133 self.add_caching_hooks( 

1134 names_filter=names, 

1135 cache=cache, 

1136 incl_bwd=incl_bwd, 

1137 device=device, 

1138 remove_batch_dim=remove_batch_dim, 

1139 ) 

1140 

1141 @staticmethod 

1142 def _remove_temporary_hook( 

1143 hook_point: HookPoint, direction: Literal["fwd", "bwd"], handle: LensHandle 

1144 ) -> None: 

1145 """Remove an invocation's handle, not retained hooks at a reused lexical level.""" 

1146 if handle.is_permanent: 1146 ↛ 1147line 1146 didn't jump to line 1147 because the condition on line 1146 was never true

1147 return 

1148 handle.hook.remove() 

1149 handles = hook_point.fwd_hooks if direction == "fwd" else hook_point.bwd_hooks 

1150 handles[:] = [existing for existing in handles if existing is not handle] 

1151 

1152 def hooks( 

1153 self, 

1154 fwd_hooks: List = [], 

1155 bwd_hooks: List = [], 

1156 reset_hooks_end: bool = True, 

1157 clear_contexts: bool = False, 

1158 ) -> Any: 

1159 """Context manager for temporarily adding hooks. 

1160 

1161 ``reset_hooks_end`` removes the hooks this context added when it exits — 

1162 hooks the caller attached beforehand are left alone either way; 

1163 ``clear_contexts`` also wipes the touched hook points' ``ctx``. 

1164 

1165 Example: 

1166 with model.hooks(fwd_hooks=[("hook_embed", my_hook)]): 

1167 output = model("Hello world") 

1168 """ 

1169 

1170 @contextmanager 

1171 def _hooks_context() -> Iterator["BridgeCore"]: 

1172 added_hooks: List[Tuple[HookPoint, Literal["fwd", "bwd"], LensHandle]] = [] 

1173 context_level = getattr(self, "context_level", 0) + 1 

1174 self.context_level = context_level 

1175 

1176 def add_hook_to_point( 

1177 hook_point: HookPoint, 

1178 hook_fn: Callable, 

1179 name: str, 

1180 dir: Literal["fwd", "bwd"] = "fwd", 

1181 ) -> None: 

1182 self.check_hooks_to_add(hook_point, name, hook_fn, dir=dir) 

1183 if self.compatibility_mode and name != hook_point.name: 1183 ↛ 1184line 1183 didn't jump to line 1184 because the condition on line 1183 was never true

1184 alias_names_list: list = [] 

1185 if hook_point.name is not None: 

1186 alias_names_list.append(hook_point.name) 

1187 alias_names_list.append(name) 

1188 handle = hook_point.add_hook( 

1189 hook_fn, 

1190 dir=dir, 

1191 level=context_level, 

1192 alias_names=alias_names_list, 

1193 ) 

1194 else: 

1195 handle = hook_point.add_hook(hook_fn, dir=dir, level=context_level) 

1196 added_hooks.append((hook_point, dir, handle)) 

1197 

1198 def apply_hooks(hook_list: List[Tuple[Any, Callable]], is_fwd: bool) -> None: 

1199 direction: Literal["fwd", "bwd"] = "fwd" if is_fwd else "bwd" 

1200 hook_dict = self.hook_dict 

1201 aliases = build_alias_to_canonical_map(hook_dict) 

1202 for hook_name_or_filter, hook_fn in hook_list: 

1203 if isinstance(hook_name_or_filter, str): 

1204 actual_hook_name, hook_point = self._resolve_hook_point( 

1205 hook_name_or_filter, aliases, hook_dict 

1206 ) 

1207 add_hook_to_point(hook_point, hook_fn, actual_hook_name, direction) 

1208 else: 

1209 seen_hooks = set() 

1210 gated_skipped: List[str] = [] 

1211 for n, hook_point in hook_dict.items(): 

1212 if hook_name_or_filter(n): 

1213 hook_id = id(hook_point) 

1214 if hook_id in seen_hooks: 

1215 continue 

1216 seen_hooks.add(hook_id) 

1217 if self._gated_hook_reason(hook_point.name or n) is not None: 

1218 gated_skipped.append(n) 

1219 continue 

1220 hook_name_to_use = hook_point.name if hook_point.name else n 

1221 add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction) 

1222 self._warn_gated_skipped("hooks", gated_skipped, stacklevel=4) 

1223 

1224 try: 

1225 apply_hooks(fwd_hooks, True) 

1226 apply_hooks(bwd_hooks, False) 

1227 yield self 

1228 finally: 

1229 try: 

1230 for hook_point, direction, handle in added_hooks: 

1231 if reset_hooks_end: 

1232 self._remove_temporary_hook(hook_point, direction, handle) 

1233 if clear_contexts: 1233 ↛ 1234line 1233 didn't jump to line 1234 because the condition on line 1233 was never true

1234 hook_point.clear_context() 

1235 finally: 

1236 self.context_level -= 1 

1237 

1238 return _hooks_context() 

1239 

1240 # ---- high-level execution: run_with_hooks ---- 

1241 

1242 def run_with_hooks( 

1243 self, 

1244 input: Any, 

1245 fwd_hooks: List[Tuple[Union[str, Callable], Callable]] = [], 

1246 bwd_hooks: List[Tuple[Union[str, Callable], Callable]] = [], 

1247 reset_hooks_end: bool = True, 

1248 clear_contexts: bool = False, 

1249 return_type: Optional[str] = "logits", 

1250 stop_at_layer: Optional[int] = None, 

1251 start_at_layer: Optional[int] = None, 

1252 remove_batch_dim: bool = False, 

1253 **kwargs: Any, 

1254 ) -> Any: 

1255 """Run the model with specified forward and backward hooks. 

1256 

1257 ``stop_at_layer`` raises :class:`StopAtLayerException` to stop early 

1258 (KV cache cleaned up on stop). ``start_at_layer`` treats ``input`` as the 

1259 residual entering block ``k`` (see :meth:`forward`); hooks on blocks below 

1260 ``k`` are skipped to match HookedTransformer. ``remove_batch_dim`` 

1261 squeezes/unsqueezes the leading dim around each hook callback whenever 

1262 that tensor's leading dim is 1 — a per-tensor rule, so broadcast 

1263 activations such as rotary ``hook_cos``/``hook_sin`` lose their size-1 

1264 leading dim even at batch > 1. 

1265 ``reset_hooks_end`` removes the hooks this call added when it finishes — 

1266 hooks the caller attached beforehand are left alone either way; 

1267 ``clear_contexts`` also wipes the touched hook points' ``ctx``. 

1268 """ 

1269 if "names_filter" in kwargs: 

1270 # **kwargs would silently absorb it; fail loud. 

1271 raise TypeError( 

1272 "run_with_hooks() got an unexpected keyword argument 'names_filter'; " 

1273 "use run_with_cache(names_filter=...) to scope caching." 

1274 ) 

1275 added_hooks: List[Tuple[HookPoint, Literal["fwd", "bwd"], LensHandle]] = [] 

1276 effective_stop_layer = None 

1277 if stop_at_layer is not None and hasattr(self, "blocks"): 

1278 if stop_at_layer < 0: 1278 ↛ 1279line 1278 didn't jump to line 1279 because the condition on line 1278 was never true

1279 effective_stop_layer = len(self.blocks) + stop_at_layer 

1280 else: 

1281 effective_stop_layer = stop_at_layer 

1282 effective_start_layer = None 

1283 if start_at_layer is not None and hasattr(self, "blocks"): 1283 ↛ 1288line 1283 didn't jump to line 1288 because the condition on line 1283 was always true

1284 effective_start_layer = ( 

1285 len(self.blocks) + start_at_layer if start_at_layer < 0 else start_at_layer 

1286 ) 

1287 

1288 def add_hook_to_point( 

1289 hook_point: HookPoint, 

1290 hook_fn: Callable, 

1291 name: str, 

1292 dir: Literal["fwd", "bwd"] = "fwd", 

1293 ) -> None: 

1294 if effective_start_layer is not None and self._is_embedding_stage_hook(name): 1294 ↛ 1295line 1294 didn't jump to line 1295 because the condition on line 1294 was never true

1295 return 

1296 if name.startswith("blocks."): 

1297 try: 

1298 layer_num: Optional[int] = int(name.split(".")[1]) 

1299 except (IndexError, ValueError): 

1300 layer_num = None 

1301 if layer_num is not None: 1301 ↛ 1306line 1301 didn't jump to line 1306 because the condition on line 1301 was always true

1302 if effective_stop_layer is not None and layer_num >= effective_stop_layer: 

1303 return 

1304 if effective_start_layer is not None and layer_num < effective_start_layer: 

1305 return 

1306 self.check_hooks_to_add(hook_point, name, hook_fn, dir=dir) 

1307 if self.compatibility_mode and name != hook_point.name: 1307 ↛ 1308line 1307 didn't jump to line 1308 because the condition on line 1307 was never true

1308 alias_names_list: list = [] 

1309 if hook_point.name is not None: 

1310 alias_names_list.append(hook_point.name) 

1311 alias_names_list.append(name) 

1312 handle = hook_point.add_hook( 

1313 hook_fn, 

1314 dir=dir, 

1315 level=context_level, 

1316 alias_names=alias_names_list, 

1317 ) 

1318 else: 

1319 handle = hook_point.add_hook(hook_fn, dir=dir, level=context_level) 

1320 added_hooks.append((hook_point, dir, handle)) 

1321 

1322 def apply_hooks( 

1323 hook_list: List[Tuple[Union[str, Callable], Callable]], is_fwd: bool 

1324 ) -> None: 

1325 direction: Literal["fwd", "bwd"] = "fwd" if is_fwd else "bwd" 

1326 hook_dict = self.hook_dict 

1327 aliases = build_alias_to_canonical_map(hook_dict) 

1328 for hook_name_or_filter, hook_fn in hook_list: 

1329 if remove_batch_dim: 

1330 original_hook_fn = hook_fn 

1331 

1332 # Default arg captures hook_fn by value (avoids closure issue) 

1333 def wrapped_hook_fn(tensor, hook, _orig_fn=original_hook_fn): 

1334 if tensor.shape[0] == 1: 

1335 tensor_no_batch = tensor.squeeze(0) 

1336 result = _orig_fn(tensor_no_batch, hook) 

1337 if result is not None and result.dim() == tensor_no_batch.dim(): 

1338 result = result.unsqueeze(0) 

1339 return result 

1340 else: 

1341 return _orig_fn(tensor, hook) 

1342 

1343 hook_fn = wrapped_hook_fn 

1344 if isinstance(hook_name_or_filter, str): 

1345 # Resolve BEFORE gating: an aliased HT name may point at an 

1346 # always-firing override, which must not be refused. The 

1347 # message still names the caller's spelling. 

1348 actual_hook_name, hook_point = self._resolve_hook_point( 

1349 hook_name_or_filter, aliases, hook_dict 

1350 ) 

1351 reason = self._gated_hook_reason(hook_point.name or actual_hook_name) 

1352 if reason is not None: 

1353 raise ValueError( 

1354 f"Cannot add hook {hook_name_or_filter} because {reason} is False. " 

1355 f"Call set_{reason}(True) first." 

1356 ) 

1357 add_hook_to_point(hook_point, hook_fn, actual_hook_name, direction) 

1358 else: 

1359 seen_hooks: set = set() 

1360 gated_skipped: List[str] = [] 

1361 for n, hook_point in hook_dict.items(): 

1362 if hook_name_or_filter(n): 

1363 hook_id = id(hook_point) 

1364 if hook_id in seen_hooks: 1364 ↛ 1365line 1364 didn't jump to line 1365 because the condition on line 1364 was never true

1365 continue 

1366 seen_hooks.add(hook_id) 

1367 if self._gated_hook_reason(hook_point.name or n) is not None: 

1368 gated_skipped.append(n) 

1369 continue 

1370 hook_name_to_use = hook_point.name if hook_point.name else n 

1371 add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction) 

1372 self._warn_gated_skipped("run_with_hooks", gated_skipped, stacklevel=4) 

1373 

1374 context_level = getattr(self, "context_level", 0) + 1 

1375 self.context_level = context_level 

1376 try: 

1377 if effective_stop_layer is not None: 

1378 stop_at_layer = effective_stop_layer 

1379 apply_hooks(fwd_hooks, True) 

1380 apply_hooks(bwd_hooks, False) 

1381 if start_at_layer is not None: 

1382 kwargs["start_at_layer"] = start_at_layer 

1383 try: 

1384 output = self.forward( 

1385 input, return_type=return_type, stop_at_layer=stop_at_layer, **kwargs 

1386 ) 

1387 except StopAtLayerException as e: 

1388 output = e.layer_output 

1389 return output 

1390 finally: 

1391 try: 

1392 for hook_point, direction, handle in added_hooks: 

1393 if reset_hooks_end: 

1394 self._remove_temporary_hook(hook_point, direction, handle) 

1395 if clear_contexts: 

1396 hook_point.clear_context() 

1397 finally: 

1398 self.context_level -= 1 

1399 

1400 # ---- high-level execution: run_with_cache ---- 

1401 

1402 @overload 

1403 def run_with_cache( 

1404 self, 

1405 input: Union[str, List[str], torch.Tensor], 

1406 return_cache_object: Literal[True] = True, 

1407 remove_batch_dim: bool = False, 

1408 **kwargs, 

1409 ) -> Tuple[Any, ActivationCache]: 

1410 """Run with cache - placeholder implementation.""" 

1411 pass 

1412 

1413 @overload 

1414 def run_with_cache( 

1415 self, 

1416 input: Union[str, List[str], torch.Tensor], 

1417 return_cache_object: Literal[False], 

1418 remove_batch_dim: bool = False, 

1419 **kwargs, 

1420 ) -> Tuple[Any, Dict[str, torch.Tensor]]: 

1421 """Run with cache - placeholder implementation.""" 

1422 pass 

1423 

1424 def run_with_cache( 

1425 self, 

1426 input: Union[str, List[str], torch.Tensor], 

1427 return_cache_object: bool = True, 

1428 remove_batch_dim: bool = False, 

1429 names_filter: Optional[Union[str, List[str], Callable[[str], bool]]] = None, 

1430 stop_at_layer: Optional[int] = None, 

1431 start_at_layer: Optional[int] = None, 

1432 pos_slice: Optional[Union[Slice, SliceInput]] = None, 

1433 incl_bwd: bool = False, 

1434 reset_hooks_end: bool = True, 

1435 clear_contexts: bool = False, 

1436 **kwargs, 

1437 ) -> Tuple[Any, Union[ActivationCache, Dict[str, torch.Tensor]]]: 

1438 """Run the model and cache activations. Returns ``(output, cache)``. 

1439 

1440 ``stop_at_layer`` raises :class:`StopAtLayerException` to stop early. 

1441 ``start_at_layer`` treats ``input`` as the residual entering block ``k`` 

1442 (see :meth:`forward`); blocks below ``k`` are excluded from the cache to 

1443 match HookedTransformer. ``pos_slice`` slices each cached activation along 

1444 its position dimension (dim 1 for resid/per-head/token-id activations; the 

1445 query position ``-2`` for attention patterns/scores; ``HookPoint.pos_dim`` 

1446 for hooks fired on another layout). ``incl_bwd`` also caches gradients 

1447 under ``"<name>_grad"`` by running ``output.backward()``; 

1448 the caller must request a scalar output (``return_type="loss"``) and the 

1449 model must be on the gradients-capable transformers driver. 

1450 ``reset_hooks_end`` removes the hooks this call added when it finishes — 

1451 hooks the caller attached beforehand are left alone either way; 

1452 ``clear_contexts`` also wipes the touched hook points' ``ctx``. ``device`` 

1453 offloads cached activations (matches ``ActivationCache.to``); the model and 

1454 inputs stay where the caller put them. 

1455 """ 

1456 if incl_bwd and stop_at_layer is not None: 

1457 raise ValueError( 

1458 "incl_bwd=True cannot be combined with stop_at_layer: the run returns the " 

1459 "intermediate activation at that layer, not a scalar to call backward() on." 

1460 ) 

1461 if incl_bwd and not torch.is_grad_enabled(): 

1462 raise ValueError( 

1463 "incl_bwd=True needs autograd, but gradient tracking is off " 

1464 "(torch.no_grad(), set_grad_enabled(False) or inference mode). " 

1465 "Run the call with gradients enabled." 

1466 ) 

1467 pos_slice_obj = Slice.unwrap(pos_slice) 

1468 aliases = build_alias_to_canonical_map(self.hook_dict) 

1469 

1470 def create_names_filter_fn(filter_input): 

1471 if filter_input is None: 

1472 return lambda name: True 

1473 elif isinstance(filter_input, str): 

1474 mapped_name = aliases.get(filter_input, None) 

1475 if mapped_name: 

1476 return lambda name: name == mapped_name or name == filter_input 

1477 else: 

1478 return lambda name: name == filter_input 

1479 elif isinstance(filter_input, list): 

1480 mapped_list = [] 

1481 for item in filter_input: 

1482 mapped_list.append(item) 

1483 mapped_name = aliases.get(item, None) 

1484 if mapped_name: 

1485 mapped_list.append(mapped_name) 

1486 return lambda name: name in mapped_list 

1487 elif callable(filter_input): 1487 ↛ 1490line 1487 didn't jump to line 1490 because the condition on line 1487 was always true

1488 return filter_input 

1489 else: 

1490 raise ValueError("names_filter must be a string, list of strings, or callable") 

1491 

1492 names_filter_fn = create_names_filter_fn(names_filter) 

1493 if isinstance(names_filter, (str, list)): 

1494 requested = [names_filter] if isinstance(names_filter, str) else names_filter 

1495 for name in requested: 

1496 self._check_hook_fireable(name, aliases.get(name, name)) 

1497 cache: Dict[str, torch.Tensor] = {} 

1498 hooks: List[Tuple[HookPoint, str]] = [] 

1499 visited: set[int] = set() 

1500 stop_hook_point: Optional[HookPoint] = None 

1501 stop_hook_fn: Optional[Callable] = None 

1502 

1503 # None → no-op .to(None), tensors stay on their current device. 

1504 cache_device = kwargs.pop("device", None) 

1505 

1506 def _store(name: str, value: torch.Tensor, hook: Any, suffix: str = "") -> None: 

1507 stored = value.detach().to(cache_device) 

1508 if pos_slice_obj is not None and stored.dim() >= 2: 1508 ↛ 1512line 1508 didn't jump to line 1512 because the condition on line 1508 was always true

1509 # A gradient has its activation's layout, so the axis comes from 

1510 # the unsuffixed name — see _pos_slice_dim. 

1511 stored = pos_slice_obj.apply(stored, dim=self._pos_slice_dim(name, hook)) 

1512 cache[name + suffix] = stored 

1513 

1514 def make_cache_hook(name: str): 

1515 def cache_hook(tensor: torch.Tensor, *, hook: Any) -> torch.Tensor: 

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

1517 cache[name] = None 

1518 elif isinstance(tensor, torch.Tensor): 1518 ↛ 1520line 1518 didn't jump to line 1520 because the condition on line 1518 was always true

1519 _store(name, tensor, hook) 

1520 elif isinstance(tensor, tuple): 

1521 if len(tensor) > 0 and isinstance(tensor[0], torch.Tensor): 

1522 _store(name, tensor[0], hook) 

1523 else: 

1524 pass 

1525 else: 

1526 try: 

1527 if hasattr(tensor, "detach"): 

1528 _store(name, tensor, hook) 

1529 except: 

1530 pass 

1531 return tensor 

1532 

1533 return cache_hook 

1534 

1535 hook_dict = self.hook_dict 

1536 effective_stop_layer = None 

1537 if stop_at_layer is not None and hasattr(self, "blocks"): 

1538 if stop_at_layer < 0: 

1539 effective_stop_layer = len(self.blocks) + stop_at_layer 

1540 else: 

1541 effective_stop_layer = stop_at_layer 

1542 effective_start_layer = None 

1543 if start_at_layer is not None and hasattr(self, "blocks"): 

1544 effective_start_layer = ( 

1545 len(self.blocks) + start_at_layer if start_at_layer < 0 else start_at_layer 

1546 ) 

1547 matched_any = False 

1548 for hook_name, hook in hook_dict.items(): 

1549 if names_filter_fn(hook_name): 

1550 matched_any = True 

1551 if effective_start_layer is not None and self._is_embedding_stage_hook(hook_name): 

1552 continue 

1553 if hook_name.startswith("blocks."): 

1554 try: 

1555 layer_num = int(hook_name.split(".")[1]) 

1556 except (IndexError, ValueError): 

1557 layer_num = None 

1558 if layer_num is not None: 1558 ↛ 1566line 1558 didn't jump to line 1566 because the condition on line 1558 was always true

1559 # stop/start bound the executed range; blocks outside it 

1560 # either don't run (stop) or run on discarded input (start), 

1561 # so their activations must not enter the cache. 

1562 if effective_stop_layer is not None and layer_num >= effective_stop_layer: 

1563 continue 

1564 if effective_start_layer is not None and layer_num < effective_start_layer: 

1565 continue 

1566 hooks.append((hook, hook_name)) 

1567 # Explicit string/list filters matching nothing must not return (logits, {}) silently. 

1568 if not matched_any and names_filter and isinstance(names_filter, (str, list)): 1568 ↛ 1574line 1568 didn't jump to line 1574 because the condition on line 1568 was always true

1569 raise KeyError( 

1570 f"names_filter {names_filter!r} matched no hook points on this model; " 

1571 "check the name against model.hook_dict (this backend may not serve it)." 

1572 ) 

1573 

1574 def make_grad_cache_hook(name: str): 

1575 def grad_hook(tensor: torch.Tensor, *, hook: Any) -> None: 

1576 if isinstance(tensor, torch.Tensor): 1576 ↛ 1579line 1576 didn't jump to line 1579 because the condition on line 1576 was always true

1577 _store(name, tensor, hook, suffix="_grad") 

1578 # A non-None return from a backward hook replaces grad_input; stay read-only. 

1579 return None 

1580 

1581 return grad_hook 

1582 

1583 processed_args = [input] 

1584 # Driver-aware input placement: torch drivers move input_ids to the model's 

1585 # device; remote drivers (no local parameters) leave them as-is. 

1586 target_device = self._input_device() 

1587 if processed_args and isinstance(processed_args[0], str): 

1588 assert self.tokenizer is not None, "Tokenizer must be set to pass string input." 

1589 prepend_bos = kwargs.pop("prepend_bos", None) 

1590 input_ids = self.to_tokens(processed_args[0], prepend_bos=prepend_bos) 

1591 if target_device is not None: 1591 ↛ 1593line 1591 didn't jump to line 1593 because the condition on line 1591 was always true

1592 input_ids = input_ids.to(target_device) 

1593 kwargs["input_ids"] = input_ids 

1594 processed_args = processed_args[1:] 

1595 elif "input" in kwargs and isinstance(kwargs["input"], str): 1595 ↛ 1596line 1595 didn't jump to line 1596 because the condition on line 1595 was never true

1596 assert self.tokenizer is not None, "Tokenizer must be set to pass string input." 

1597 prepend_bos = kwargs.pop("prepend_bos", None) 

1598 input_ids = self.to_tokens(kwargs["input"], prepend_bos=prepend_bos) 

1599 if target_device is not None: 

1600 input_ids = input_ids.to(target_device) 

1601 kwargs["input_ids"] = input_ids 

1602 del kwargs["input"] 

1603 if stop_at_layer is not None and hasattr(self, "blocks"): 

1604 if stop_at_layer < 0: 

1605 stop_at_layer = len(self.blocks) + stop_at_layer 

1606 last_layer_to_process = stop_at_layer - 1 

1607 

1608 def stop_hook(tensor: torch.Tensor, *, hook: Any) -> torch.Tensor: 

1609 raise StopAtLayerException(tensor) 

1610 

1611 if stop_at_layer >= 0 and stop_at_layer < len(self.blocks): 1611 ↛ 1618line 1611 didn't jump to line 1618 because the condition on line 1611 was always true

1612 # Stop at the beginning of the specified block, not at the end of the previous block 

1613 block_hook_name = f"blocks.{stop_at_layer}.hook_in" 

1614 hook_dict = self.hook_dict 

1615 if block_hook_name in hook_dict: 1615 ↛ 1618line 1615 didn't jump to line 1618 because the condition on line 1615 was always true

1616 stop_hook_point = hook_dict[block_hook_name] 

1617 stop_hook_fn = stop_hook 

1618 filtered_kwargs = kwargs.copy() 

1619 # ``cache_device`` is honored by ``make_cache_hook`` above (``tensor.detach().to(cache_device)``); 

1620 # the model and inputs stay where the caller put them, matching ``ActivationCache.to``. 

1621 if cache_device is not None and getattr(self.cfg, "n_devices", 1) > 1: 

1622 # Moving a dispatched model to a single device collapses accelerate's 

1623 # split and breaks its routing hooks. The cache will stay spread across 

1624 # the per-layer devices; callers can .to(cache_device) on cache entries 

1625 # after the fact if they need a single-device cache. 

1626 warnings.warn( 

1627 f"run_with_cache(device={cache_device!r}) ignored: model is dispatched " 

1628 f"across {self.cfg.n_devices} devices via device_map. Cached activations " 

1629 "will remain on their per-layer devices.", 

1630 stacklevel=2, 

1631 ) 

1632 if start_at_layer is not None: 

1633 filtered_kwargs["start_at_layer"] = start_at_layer 

1634 # Only validate gated hooks when the caller explicitly supplied a 

1635 # names_filter. The default filter matches every hook and must not 

1636 # cause gated hooks to be treated as explicitly requested. 

1637 gated_names_skipped: List[str] = [] 

1638 if names_filter is not None: 

1639 kept = [] 

1640 for hp, name in hooks: 

1641 if self._gated_hook_reason(hp.name or name) is not None: 

1642 gated_names_skipped.append(name) 

1643 else: 

1644 kept.append((hp, name)) 

1645 hooks = kept 

1646 if gated_names_skipped: 

1647 warnings.warn( 

1648 f"run_with_cache: skipped {len(gated_names_skipped)} gated-off hook name(s) " 

1649 f"that will never be cached: {gated_names_skipped}. Call the relevant " 

1650 "set_use_*(True) setter first to enable them.", 

1651 stacklevel=2, 

1652 ) 

1653 context_level = getattr(self, "context_level", 0) + 1 

1654 self.context_level = context_level 

1655 added_hooks: List[Tuple[HookPoint, Literal["fwd", "bwd"], LensHandle]] = [] 

1656 stop_handle: Optional[LensHandle] = None 

1657 try: 

1658 for hp, name in hooks: 

1659 fwd_handle = hp.add_hook(make_cache_hook(name), level=context_level) 

1660 added_hooks.append((hp, "fwd", fwd_handle)) 

1661 if incl_bwd: 

1662 bwd_handle = hp.add_hook( 

1663 make_grad_cache_hook(name), dir="bwd", level=context_level 

1664 ) 

1665 added_hooks.append((hp, "bwd", bwd_handle)) 

1666 if stop_hook_point is not None and stop_hook_fn is not None: 

1667 stop_handle = stop_hook_point.add_hook(stop_hook_fn, level=context_level) 

1668 added_hooks.append((stop_hook_point, "fwd", stop_handle)) 

1669 except Exception: 

1670 try: 

1671 for hp, direction, handle in added_hooks: 

1672 self._remove_temporary_hook(hp, direction, handle) 

1673 finally: 

1674 self.context_level -= 1 

1675 raise 

1676 try: 

1677 if ( 

1678 "output_attentions" not in filtered_kwargs 

1679 and self.adapter.supports_hf_output_attentions 

1680 ): 

1681 # Attention-free remote-code models (e.g. HyenaDNA) reject the 

1682 # kwarg outright; only pass it when the forward actually accepts 

1683 # it. Non-torch drivers expose no local module to introspect — 

1684 # keep the kwarg for them, as before. 

1685 underlying = getattr(self._driver, "underlying_model", None) 

1686 if underlying is None: 

1687 filtered_kwargs["output_attentions"] = True 

1688 else: 

1689 fwd_params = inspect.signature(underlying.forward).parameters 

1690 if "output_attentions" in fwd_params or any( 

1691 p.kind is inspect.Parameter.VAR_KEYWORD for p in fwd_params.values() 

1692 ): 

1693 filtered_kwargs["output_attentions"] = True 

1694 if processed_args: 

1695 output = self.forward(processed_args[0], **filtered_kwargs) 

1696 elif "input_ids" in filtered_kwargs: 1696 ↛ 1702line 1696 didn't jump to line 1702 because the condition on line 1696 was always true

1697 output = self.forward( 

1698 filtered_kwargs["input_ids"], 

1699 **{k: v for k, v in filtered_kwargs.items() if k != "input_ids"}, 

1700 ) 

1701 else: 

1702 output = self.forward(**filtered_kwargs) 

1703 if hasattr(output, "logits"): 1703 ↛ 1704line 1703 didn't jump to line 1704 because the condition on line 1703 was never true

1704 output = output.logits 

1705 if incl_bwd: 

1706 # Gradients land in the cache via the bwd hooks, which the finally below 

1707 # removes, so the backward pass has to happen inside this try. 

1708 if not isinstance(output, torch.Tensor) or output.numel() != 1: 

1709 shape = tuple(output.shape) if isinstance(output, torch.Tensor) else None 

1710 raise ValueError( 

1711 "incl_bwd=True needs a scalar output to call backward() on, got " 

1712 f"{type(output).__name__}{f' of shape {shape}' if shape else ''}. " 

1713 'Pass return_type="loss".' 

1714 ) 

1715 if not output.requires_grad: 

1716 raise ValueError( 

1717 "incl_bwd=True got an output with no grad_fn — the model's parameters " 

1718 "have requires_grad=False, so there is nothing to differentiate." 

1719 ) 

1720 output.backward() 

1721 except StopAtLayerException as e: 

1722 output = e.layer_output 

1723 except Exception as e: 

1724 raise e 

1725 finally: 

1726 try: 

1727 for hp, direction, handle in added_hooks: 

1728 # Retain requested cache hooks, but never internal early-stop hooks. 

1729 if reset_hooks_end or handle is stop_handle: 

1730 self._remove_temporary_hook(hp, direction, handle) 

1731 if clear_contexts: 

1732 for hp, _ in hooks: 

1733 hp.clear_context() 

1734 if stop_hook_point is not None: 1734 ↛ 1735line 1734 didn't jump to line 1735 because the condition on line 1734 was never true

1735 stop_hook_point.clear_context() 

1736 finally: 

1737 self.context_level -= 1 

1738 if self.compatibility_mode == True: 

1739 reverse_aliases = {} 

1740 for old_name, new_name in aliases.items(): 

1741 if isinstance(new_name, list): 1741 ↛ 1742line 1741 didn't jump to line 1742 because the condition on line 1741 was never true

1742 for single_new_name in new_name: 

1743 reverse_aliases[single_new_name] = old_name 

1744 else: 

1745 reverse_aliases[new_name] = old_name 

1746 # Gradient entries are keyed "<hook_name>_grad", so alias lookups run on the 

1747 # base name and the suffix is re-attached to the aliased key. 

1748 suffixes = ("", "_grad") if incl_bwd else ("",) 

1749 cache_items_to_add = {} 

1750 for cache_name, cached_value in cache.items(): 

1751 base_name, suffix = ( 

1752 (cache_name[: -len("_grad")], "_grad") 

1753 if cache_name.endswith("_grad") 

1754 else (cache_name, "") 

1755 ) 

1756 old_name = reverse_aliases.get(base_name) 

1757 if old_name is not None: 

1758 cache_items_to_add[old_name + suffix] = cached_value 

1759 cache.update(cache_items_to_add) 

1760 for alias_name, target_name in aliases.items(): 

1761 targets = target_name if isinstance(target_name, list) else [target_name] 

1762 for suffix in suffixes: 

1763 if alias_name + suffix in cache: 

1764 continue 

1765 for single_target in targets: 

1766 if single_target + suffix in cache: 1766 ↛ 1767line 1766 didn't jump to line 1767 because the condition on line 1766 was never true

1767 cache[alias_name + suffix] = cache[single_target + suffix] 

1768 break 

1769 # The input states the batch size; inferring it from the cached shapes goes wrong 

1770 # when the filter selects only flattened or position-indexed hooks. 

1771 batch_size = _input_batch_size( 

1772 processed_args[0] if processed_args else None, 

1773 filtered_kwargs.get("input_ids"), 

1774 filtered_kwargs.get("inputs_embeds"), 

1775 ) 

1776 if return_cache_object: 

1777 activation_cache = ActivationCache(cache, self, has_batch_dim=True) 

1778 if remove_batch_dim: 

1779 activation_cache.remove_batch_dim(batch_size=batch_size) 

1780 return (output, activation_cache) 

1781 else: 

1782 if remove_batch_dim: 

1783 # Same batch-size check and squeeze rule as the ActivationCache path. 

1784 tensors = {k: v for k, v in cache.items() if isinstance(v, torch.Tensor)} 

1785 cache.update( 

1786 ActivationCache(tensors, self) 

1787 .remove_batch_dim(batch_size=batch_size) 

1788 .cache_dict 

1789 ) 

1790 return (output, cache)