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

919 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-10-10 05:10 +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 

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 def hooks( 

1142 self, 

1143 fwd_hooks: List = [], 

1144 bwd_hooks: List = [], 

1145 reset_hooks_end: bool = True, 

1146 clear_contexts: bool = False, 

1147 ) -> Any: 

1148 """Context manager for temporarily adding hooks. 

1149 

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

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

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

1153 

1154 Example: 

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

1156 output = model("Hello world") 

1157 """ 

1158 

1159 @contextmanager 

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

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

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

1163 self.context_level = context_level 

1164 

1165 def add_hook_to_point( 

1166 hook_point: HookPoint, 

1167 hook_fn: Callable, 

1168 name: str, 

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

1170 ) -> None: 

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

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

1173 alias_names_list: list = [] 

1174 if hook_point.name is not None: 

1175 alias_names_list.append(hook_point.name) 

1176 alias_names_list.append(name) 

1177 hook_point.add_hook( 

1178 hook_fn, 

1179 dir=dir, 

1180 level=context_level, 

1181 alias_names=alias_names_list, 

1182 ) 

1183 else: 

1184 hook_point.add_hook(hook_fn, dir=dir, level=context_level) 

1185 added_hooks.append((hook_point, dir)) 

1186 

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

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

1189 hook_dict = self.hook_dict 

1190 aliases = build_alias_to_canonical_map(hook_dict) 

1191 for hook_name_or_filter, hook_fn in hook_list: 

1192 if isinstance(hook_name_or_filter, str): 

1193 actual_hook_name, hook_point = self._resolve_hook_point( 

1194 hook_name_or_filter, aliases, hook_dict 

1195 ) 

1196 add_hook_to_point(hook_point, hook_fn, actual_hook_name, direction) 

1197 else: 

1198 seen_hooks = set() 

1199 gated_skipped: List[str] = [] 

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

1201 if hook_name_or_filter(n): 

1202 hook_id = id(hook_point) 

1203 if hook_id in seen_hooks: 

1204 continue 

1205 seen_hooks.add(hook_id) 

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

1207 gated_skipped.append(n) 

1208 continue 

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

1210 add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction) 

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

1212 

1213 try: 

1214 apply_hooks(fwd_hooks, True) 

1215 apply_hooks(bwd_hooks, False) 

1216 yield self 

1217 finally: 

1218 try: 

1219 for hook_point, direction in added_hooks: 

1220 if reset_hooks_end: 

1221 # `level` keeps this to the hooks added above — hooks the 

1222 # caller attached before the context survive. 

1223 hook_point.remove_hooks(dir=direction, level=context_level) 

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

1225 hook_point.clear_context() 

1226 finally: 

1227 self.context_level -= 1 

1228 

1229 return _hooks_context() 

1230 

1231 # ---- high-level execution: run_with_hooks ---- 

1232 

1233 def run_with_hooks( 

1234 self, 

1235 input: Any, 

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

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

1238 reset_hooks_end: bool = True, 

1239 clear_contexts: bool = False, 

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

1241 stop_at_layer: Optional[int] = None, 

1242 start_at_layer: Optional[int] = None, 

1243 remove_batch_dim: bool = False, 

1244 **kwargs: Any, 

1245 ) -> Any: 

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

1247 

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

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

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

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

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

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

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

1255 leading dim even at batch > 1. 

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

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

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

1259 """ 

1260 if "names_filter" in kwargs: 

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

1262 raise TypeError( 

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

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

1265 ) 

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

1267 effective_stop_layer = None 

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

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

1270 effective_stop_layer = len(self.blocks) + stop_at_layer 

1271 else: 

1272 effective_stop_layer = stop_at_layer 

1273 effective_start_layer = None 

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

1275 effective_start_layer = ( 

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

1277 ) 

1278 

1279 def add_hook_to_point( 

1280 hook_point: HookPoint, 

1281 hook_fn: Callable, 

1282 name: str, 

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

1284 ) -> None: 

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

1286 return 

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

1288 try: 

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

1290 except (IndexError, ValueError): 

1291 layer_num = None 

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

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

1294 return 

1295 if effective_start_layer is not None and layer_num < effective_start_layer: 

1296 return 

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

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

1299 alias_names_list: list = [] 

1300 if hook_point.name is not None: 

1301 alias_names_list.append(hook_point.name) 

1302 alias_names_list.append(name) 

1303 hook_point.add_hook( 

1304 hook_fn, 

1305 dir=dir, 

1306 level=context_level, 

1307 alias_names=alias_names_list, 

1308 ) 

1309 else: 

1310 hook_point.add_hook(hook_fn, dir=dir, level=context_level) 

1311 added_hooks.append((hook_point, dir)) 

1312 

1313 def apply_hooks( 

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

1315 ) -> None: 

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

1317 hook_dict = self.hook_dict 

1318 aliases = build_alias_to_canonical_map(hook_dict) 

1319 for hook_name_or_filter, hook_fn in hook_list: 

1320 if remove_batch_dim: 

1321 original_hook_fn = hook_fn 

1322 

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

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

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

1326 tensor_no_batch = tensor.squeeze(0) 

1327 result = _orig_fn(tensor_no_batch, hook) 

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

1329 result = result.unsqueeze(0) 

1330 return result 

1331 else: 

1332 return _orig_fn(tensor, hook) 

1333 

1334 hook_fn = wrapped_hook_fn 

1335 if isinstance(hook_name_or_filter, str): 

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

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

1338 # message still names the caller's spelling. 

1339 actual_hook_name, hook_point = self._resolve_hook_point( 

1340 hook_name_or_filter, aliases, hook_dict 

1341 ) 

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

1343 if reason is not None: 

1344 raise ValueError( 

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

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

1347 ) 

1348 add_hook_to_point(hook_point, hook_fn, actual_hook_name, direction) 

1349 else: 

1350 seen_hooks: set = set() 

1351 gated_skipped: List[str] = [] 

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

1353 if hook_name_or_filter(n): 

1354 hook_id = id(hook_point) 

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

1356 continue 

1357 seen_hooks.add(hook_id) 

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

1359 gated_skipped.append(n) 

1360 continue 

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

1362 add_hook_to_point(hook_point, hook_fn, hook_name_to_use, direction) 

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

1364 

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

1366 self.context_level = context_level 

1367 try: 

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

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

1370 stop_at_layer = len(self.blocks) + stop_at_layer 

1371 if stop_at_layer >= 0 and stop_at_layer < len(self.blocks): 1371 ↛ anywhereline 1371 didn't jump anywhere: it always raised an exception.

1372 

1373 def stop_hook(tensor: Any, *, hook: Any) -> Any: 

1374 raise StopAtLayerException(tensor) 

1375 

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

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

1378 hook_dict = self.hook_dict 

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

1380 add_hook_to_point( 

1381 hook_dict[block_hook_name], stop_hook, block_hook_name, "fwd" 

1382 ) 

1383 

1384 apply_hooks(fwd_hooks, True) 

1385 apply_hooks(bwd_hooks, False) 

1386 if start_at_layer is not None: 

1387 kwargs["start_at_layer"] = start_at_layer 

1388 try: 

1389 output = self.forward( 

1390 input, return_type=return_type, stop_at_layer=stop_at_layer, **kwargs 

1391 ) 

1392 except StopAtLayerException as e: 

1393 output = e.layer_output 

1394 return output 

1395 finally: 

1396 try: 

1397 for hook_point, direction in added_hooks: 

1398 if reset_hooks_end: 

1399 # `level` keeps this to the hooks added above — hooks the caller 

1400 # attached before the call survive. 

1401 hook_point.remove_hooks(dir=direction, level=context_level) 

1402 if clear_contexts: 

1403 hook_point.clear_context() 

1404 finally: 

1405 self.context_level -= 1 

1406 

1407 # ---- high-level execution: run_with_cache ---- 

1408 

1409 @overload 

1410 def run_with_cache( 

1411 self, 

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

1413 return_cache_object: Literal[True] = True, 

1414 remove_batch_dim: bool = False, 

1415 **kwargs, 

1416 ) -> Tuple[Any, ActivationCache]: 

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

1418 pass 

1419 

1420 @overload 

1421 def run_with_cache( 

1422 self, 

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

1424 return_cache_object: Literal[False], 

1425 remove_batch_dim: bool = False, 

1426 **kwargs, 

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

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

1429 pass 

1430 

1431 def run_with_cache( 

1432 self, 

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

1434 return_cache_object: bool = True, 

1435 remove_batch_dim: bool = False, 

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

1437 stop_at_layer: Optional[int] = None, 

1438 start_at_layer: Optional[int] = None, 

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

1440 incl_bwd: bool = False, 

1441 reset_hooks_end: bool = True, 

1442 clear_contexts: bool = False, 

1443 **kwargs, 

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

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

1446 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1461 inputs stay where the caller put them. 

1462 """ 

1463 if incl_bwd and stop_at_layer is not None: 

1464 raise ValueError( 

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

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

1467 ) 

1468 if incl_bwd and not torch.is_grad_enabled(): 

1469 raise ValueError( 

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

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

1472 "Run the call with gradients enabled." 

1473 ) 

1474 pos_slice_obj = Slice.unwrap(pos_slice) 

1475 aliases = build_alias_to_canonical_map(self.hook_dict) 

1476 

1477 def create_names_filter_fn(filter_input): 

1478 if filter_input is None: 

1479 return lambda name: True 

1480 elif isinstance(filter_input, str): 

1481 mapped_name = aliases.get(filter_input, None) 

1482 if mapped_name: 

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

1484 else: 

1485 return lambda name: name == filter_input 

1486 elif isinstance(filter_input, list): 

1487 mapped_list = [] 

1488 for item in filter_input: 

1489 mapped_list.append(item) 

1490 mapped_name = aliases.get(item, None) 

1491 if mapped_name: 

1492 mapped_list.append(mapped_name) 

1493 return lambda name: name in mapped_list 

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

1495 return filter_input 

1496 else: 

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

1498 

1499 names_filter_fn = create_names_filter_fn(names_filter) 

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

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

1502 for name in requested: 

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

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

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

1506 visited: set[int] = set() 

1507 stop_hook_point: Optional[HookPoint] = None 

1508 stop_hook_fn: Optional[Callable] = None 

1509 

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

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

1512 

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

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

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

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

1517 # the unsuffixed name — see _pos_slice_dim. 

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

1519 cache[name + suffix] = stored 

1520 

1521 def make_cache_hook(name: str): 

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

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

1524 cache[name] = None 

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

1526 _store(name, tensor, hook) 

1527 elif isinstance(tensor, tuple): 

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

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

1530 else: 

1531 pass 

1532 else: 

1533 try: 

1534 if hasattr(tensor, "detach"): 

1535 _store(name, tensor, hook) 

1536 except: 

1537 pass 

1538 return tensor 

1539 

1540 return cache_hook 

1541 

1542 hook_dict = self.hook_dict 

1543 effective_stop_layer = None 

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

1545 if stop_at_layer < 0: 

1546 effective_stop_layer = len(self.blocks) + stop_at_layer 

1547 else: 

1548 effective_stop_layer = stop_at_layer 

1549 effective_start_layer = None 

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

1551 effective_start_layer = ( 

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

1553 ) 

1554 matched_any = False 

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

1556 if names_filter_fn(hook_name): 

1557 matched_any = True 

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

1559 continue 

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

1561 try: 

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

1563 except (IndexError, ValueError): 

1564 layer_num = None 

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

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

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

1568 # so their activations must not enter the cache. 

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

1570 continue 

1571 if effective_start_layer is not None and layer_num < effective_start_layer: 

1572 continue 

1573 hooks.append((hook, hook_name)) 

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

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

1576 raise KeyError( 

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

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

1579 ) 

1580 

1581 def make_grad_cache_hook(name: str): 

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

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

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

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

1586 return None 

1587 

1588 return grad_hook 

1589 

1590 processed_args = [input] 

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

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

1593 target_device = self._input_device() 

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

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

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

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

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

1599 input_ids = input_ids.to(target_device) 

1600 kwargs["input_ids"] = input_ids 

1601 processed_args = processed_args[1:] 

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

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

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

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

1606 if target_device is not None: 

1607 input_ids = input_ids.to(target_device) 

1608 kwargs["input_ids"] = input_ids 

1609 del kwargs["input"] 

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

1611 if stop_at_layer < 0: 

1612 stop_at_layer = len(self.blocks) + stop_at_layer 

1613 last_layer_to_process = stop_at_layer - 1 

1614 

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

1616 raise StopAtLayerException(tensor) 

1617 

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

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

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

1621 hook_dict = self.hook_dict 

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

1623 stop_hook_point = hook_dict[block_hook_name] 

1624 stop_hook_fn = stop_hook 

1625 filtered_kwargs = kwargs.copy() 

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

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

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

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

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

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

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

1633 warnings.warn( 

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

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

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

1637 stacklevel=2, 

1638 ) 

1639 if start_at_layer is not None: 

1640 filtered_kwargs["start_at_layer"] = start_at_layer 

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

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

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

1644 gated_names_skipped: List[str] = [] 

1645 if names_filter is not None: 

1646 kept = [] 

1647 for hp, name in hooks: 

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

1649 gated_names_skipped.append(name) 

1650 else: 

1651 kept.append((hp, name)) 

1652 hooks = kept 

1653 if gated_names_skipped: 

1654 warnings.warn( 

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

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

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

1658 stacklevel=2, 

1659 ) 

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

1661 self.context_level = context_level 

1662 try: 

1663 for hp, name in hooks: 

1664 hp.add_hook(make_cache_hook(name), level=context_level) 

1665 if incl_bwd: 

1666 hp.add_hook(make_grad_cache_hook(name), dir="bwd", level=context_level) 

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

1668 stop_hook_point.add_hook(stop_hook_fn, level=context_level) 

1669 except Exception: 

1670 try: 

1671 self.remove_all_hook_fns(level=context_level) 

1672 finally: 

1673 self.context_level -= 1 

1674 raise 

1675 try: 

1676 if ( 

1677 "output_attentions" not in filtered_kwargs 

1678 and self.adapter.supports_hf_output_attentions 

1679 ): 

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

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

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

1683 # keep the kwarg for them, as before. 

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

1685 if underlying is None: 

1686 filtered_kwargs["output_attentions"] = True 

1687 else: 

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

1689 if "output_attentions" in fwd_params or any( 

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

1691 ): 

1692 filtered_kwargs["output_attentions"] = True 

1693 if processed_args: 

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

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

1696 output = self.forward( 

1697 filtered_kwargs["input_ids"], 

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

1699 ) 

1700 else: 

1701 output = self.forward(**filtered_kwargs) 

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

1703 output = output.logits 

1704 if incl_bwd: 

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

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

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

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

1709 raise ValueError( 

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

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

1712 'Pass return_type="loss".' 

1713 ) 

1714 if not output.requires_grad: 

1715 raise ValueError( 

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

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

1718 ) 

1719 output.backward() 

1720 except StopAtLayerException as e: 

1721 output = e.layer_output 

1722 except Exception as e: 

1723 raise e 

1724 finally: 

1725 try: 

1726 if reset_hooks_end: 

1727 # `level` keeps this to the hooks added above — hooks the caller 

1728 # attached before the call survive. 

1729 self.remove_all_hook_fns(level=context_level) 

1730 if clear_contexts: 

1731 for hp, _ in hooks: 

1732 hp.clear_context() 

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

1734 stop_hook_point.clear_context() 

1735 finally: 

1736 self.context_level -= 1 

1737 if self.compatibility_mode == True: 

1738 reverse_aliases = {} 

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

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

1741 for single_new_name in new_name: 

1742 reverse_aliases[single_new_name] = old_name 

1743 else: 

1744 reverse_aliases[new_name] = old_name 

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

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

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

1748 cache_items_to_add = {} 

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

1750 base_name, suffix = ( 

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

1752 if cache_name.endswith("_grad") 

1753 else (cache_name, "") 

1754 ) 

1755 old_name = reverse_aliases.get(base_name) 

1756 if old_name is not None: 

1757 cache_items_to_add[old_name + suffix] = cached_value 

1758 cache.update(cache_items_to_add) 

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

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

1761 for suffix in suffixes: 

1762 if alias_name + suffix in cache: 

1763 continue 

1764 for single_target in targets: 

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

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

1767 break 

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

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

1770 batch_size = _input_batch_size( 

1771 processed_args[0] if processed_args else None, 

1772 filtered_kwargs.get("input_ids"), 

1773 filtered_kwargs.get("inputs_embeds"), 

1774 ) 

1775 if return_cache_object: 

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

1777 if remove_batch_dim: 

1778 activation_cache.remove_batch_dim(batch_size=batch_size) 

1779 return (output, activation_cache) 

1780 else: 

1781 if remove_batch_dim: 

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

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

1784 cache.update( 

1785 ActivationCache(tensors, self) 

1786 .remove_batch_dim(batch_size=batch_size) 

1787 .cache_dict 

1788 ) 

1789 return (output, cache)