Coverage for transformer_lens/model_bridge/bridge.py: 83%

2343 statements  

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

1"""Bridge module for connecting different model architectures. 

2 

3This module provides the bridge components that wrap remote model components and provide 

4a consistent interface for accessing their weights and performing operations. 

5""" 

6 

7import inspect 

8import logging 

9import re 

10import warnings 

11from collections.abc import Generator 

12from contextlib import contextmanager 

13from functools import lru_cache 

14from typing import ( 

15 TYPE_CHECKING, 

16 Any, 

17 Callable, 

18 Dict, 

19 FrozenSet, 

20 Iterable, 

21 Iterator, 

22 List, 

23 Literal, 

24 Optional, 

25 Tuple, 

26 Union, 

27 cast, 

28 overload, 

29) 

30 

31import einops 

32import numpy as np 

33import torch 

34import tqdm 

35from torch import nn 

36from torch.nn import functional as F 

37from transformers.tokenization_utils_base import PreTrainedTokenizerBase 

38 

39from transformer_lens import utilities as utils 

40from transformer_lens.ActivationCache import ActivationCache 

41from transformer_lens.config import TransformerBridgeConfig 

42from transformer_lens.FactoredMatrix import FactoredMatrix 

43from transformer_lens.hook_points import HookIntrospectionMixin, HookPoint 

44from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

45from transformer_lens.model_bridge.component_setup import ( 

46 refresh_container_state_owners, 

47 set_original_components, 

48) 

49from transformer_lens.model_bridge.composition_scores import CompositionScores 

50from transformer_lens.model_bridge.exceptions import StopAtLayerException 

51from transformer_lens.model_bridge.generalized_components.base import ( 

52 GeneralizedComponent, 

53 alias_generation, 

54) 

55from transformer_lens.model_bridge.generalized_components.block import ( 

56 _BLOCK_INTERNAL_MODULES, 

57 _NORM_PREFIXES, 

58 _VARIANT_SUBMODULE_SET, 

59 VARIANT_SUBMODULE_NAMES, 

60) 

61from transformer_lens.model_bridge.get_params_util import get_bridge_params 

62from transformer_lens.utilities.activation_functions import softcap_enabled 

63from transformer_lens.utilities.aliases import resolve_alias 

64from transformer_lens.utilities.devices import move_to_and_update_config 

65from transformer_lens.utilities.lm_utils import lm_cross_entropy_loss 

66from transformer_lens.utilities.quantization import require_readable_weight 

67from transformer_lens.utilities.slice import Slice, SliceInput 

68 

69if TYPE_CHECKING: 

70 from transformer_lens.ActivationCache import ActivationCache 

71 

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

73 

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

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

76 

77 

78def _resolve_attr_path(obj: nn.Module, attr_path: str) -> torch.Tensor: 

79 """Walk a dot-separated attribute path and return the final tensor.""" 

80 result = obj 

81 for attr in attr_path.split("."): 

82 result = getattr(result, attr) 

83 return cast(torch.Tensor, result) 

84 

85 

86def build_alias_to_canonical_map(hook_dict, prefix=""): 

87 """Build a mapping from alias hook names to their canonical names. 

88 

89 Args: 

90 hook_dict: Dictionary mapping hook names to HookPoint objects 

91 prefix: Prefix for nested keys 

92 

93 Returns: 

94 Dictionary mapping alias names to canonical names 

95 

96 Example: 

97 If hook_dict contains: 

98 - "blocks.0.hook_q" -> HookPoint(name="blocks.0.attn.q.hook_out") 

99 

100 Returns: 

101 - {"blocks.0.hook_q": "blocks.0.attn.q.hook_out"} 

102 """ 

103 aliases = {} 

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

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

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

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

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

109 if key != value.name: 

110 aliases[full_key] = value.name 

111 return aliases 

112 

113 

114def _pos_axis_for_hook(hook_name: str, hook_point: HookPoint) -> int: 

115 """Return the axis `pos_slice` applies to for a hook's cached activation. 

116 

117 Head-split tensors are [batch, pos, head, d_head], so their position axis is two from the 

118 end; everything else (residual stream, MLP, attention patterns keyed by destination 

119 position) has it one from the end. HookedTransformer-style names are recognised by 

120 suffix, bridge-native ones (`attn.q.hook_out`, `attn.o.hook_in`, ...) by the head-splitting 

121 conversion installed on their hook point. 

122 """ 

123 if hook_name.endswith(("hook_q", "hook_k", "hook_v", "hook_z", "hook_result")): 

124 return -3 

125 conversion = getattr(hook_point, "hook_conversion", None) 

126 if getattr(conversion, "splits_attention_heads", False): 

127 return -3 

128 return -2 

129 

130 

131class TransformerBridge(HookIntrospectionMixin, nn.Module): 

132 """Bridge between HuggingFace and TransformerLens models. 

133 

134 This class provides a standardized interface to access components of a transformer 

135 model, regardless of the underlying architecture. It uses an architecture adapter 

136 to map between the TransformerLens and HuggingFace model structures. 

137 

138 Tokenization notes 

139 ------------------ 

140 

141 :meth:`to_tokens`, :meth:`to_str_tokens`, :meth:`get_token_position`, 

142 :meth:`forward` (string input), and :meth:`generate` accept ``prepend_bos`` 

143 to control BOS prepending. Resolution: explicit arg → 

144 ``cfg.default_prepend_bos`` (defaults ``True``, even for non-BOS-trained 

145 models — attention heads tend to use position 0 as a resting state). 

146 **Pass ``prepend_bos=False`` when tokenizing a fragment of a larger 

147 prompt** — off-by-one position errors usually trace back here. 

148 

149 Reconciliation with ``cfg.tokenizer_prepends_bos`` (tokenizers that add 

150 BOS automatically) is handled internally — pass the value you want; 

151 the bridge adds or strips manually as needed. When 

152 ``cfg.tokenizer_appends_eos=True`` (OLMo, Apertus, etc.), 

153 :meth:`to_tokens` also strips trailing EOS tokens so the model receives 

154 a continuation rather than a terminated sequence; this path is 

155 bridge-specific. 

156 

157 BPE/SentencePiece tokenizers treat ``"hello"``, ``" hello"``, and 

158 ``"Hello"`` as distinct tokens. Concatenated prompts may not tokenize 

159 as the sum of parts — inspect with :meth:`to_str_tokens` when in doubt. 

160 

161 BOS token and chat templates 

162 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

163 

164 ``model.tokenizer`` is configured with ``add_bos_token=True`` and is 

165 **not** the stock HuggingFace tokenizer. Direct ``.encode()`` calls 

166 will prepend BOS automatically. 

167 

168 When passing pre-applied chat-template text (i.e., the output of 

169 ``tokenizer.apply_chat_template(..., tokenize=False)``), pass 

170 ``prepend_bos=False`` to :meth:`to_tokens` to avoid a double BOS:: 

171 

172 # Correct pattern for chat templates: 

173 text = model.tokenizer.apply_chat_template(messages, tokenize=False) 

174 tokens = model.to_tokens(text, prepend_bos=False) 

175 

176 The chat template already embeds the model's expected BOS token in 

177 the rendered text; letting :meth:`to_tokens` add another would produce 

178 a malformed sequence like ``[BOS, BOS, ...]``. 

179 

180 To inspect what tokens will actually be fed to the model during 

181 generation, use :meth:`to_tokens` directly or pass 

182 ``return_input_tokens=True`` to :meth:`generate`. 

183 """ 

184 

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

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

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

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

189 "hook_unembed": "unembed.hook_out", 

190 } 

191 

192 def __init__(self, model: nn.Module, adapter: ArchitectureAdapter, tokenizer: Any): 

193 """Initialize the bridge. 

194 

195 Args: 

196 model: The model to bridge (must be a PyTorch nn.Module or PreTrainedModel) 

197 adapter: The architecture adapter to use 

198 tokenizer: The tokenizer to use (required) 

199 """ 

200 super().__init__() 

201 self._n_params_total = sum(parameter.numel() for parameter in model.parameters()) 

202 self.__dict__["original_model"] = model 

203 self.adapter = adapter 

204 self.cfg = adapter.cfg 

205 self._tokenizer = None 

206 if tokenizer is not None: 

207 self.tokenizer = tokenizer # Use the property setter 

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

209 self.cfg.d_vocab_out = self.cfg.d_vocab 

210 self.compatibility_mode = False 

211 self._weights_processed = False 

212 self._hook_cache = None 

213 # Nesting depth of hook-adding contexts; hooks are tagged with it so teardown removes 

214 # only the ones this call/context added, leaving the caller's own hooks in place. 

215 self.context_level = 0 

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

217 self._hook_registry_initialized = False 

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

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

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

221 # real_components maps TL keys to (remote_path, actual_instance) tuples 

222 # For list components, actual_instance will be a list of component instances 

223 self.real_components: Dict[str, tuple] = {} 

224 if not hasattr(self.cfg, "device") or self.cfg.device is None: 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 try: 

226 self.cfg.device = str(next(self.original_model.parameters()).device) 

227 except StopIteration: 

228 self.cfg.device = "cpu" 

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

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

231 original_model = self.__dict__["original_model"] 

232 set_original_components(self, self.adapter, original_model) 

233 self._initialize_hook_registry() 

234 self._register_aliases() 

235 self._register_all_aliases_recursive() 

236 self._setup_hook_compatibility() 

237 self._initialize_hooks_to_cache() 

238 self.processor = None 

239 # Bridge wrappers are inserted into the HF module tree after 

240 # from_pretrained's eval(), and nn.Module defaults to training=True — 

241 # without re-syncing, reconstruction paths apply dropout at inference. 

242 # train() recurses, so this stamps the wrappers with the model's mode. 

243 original_model.train(original_model.training) 

244 self.train(original_model.training) 

245 self.cfg._bind_bridge(self) 

246 

247 def __setstate__(self, state: dict[str, Any]) -> None: 

248 """Restore runtime config routing after deepcopy or deserialization.""" 

249 super().__setstate__(state) 

250 self.cfg._bind_bridge(self) 

251 

252 @property 

253 def tokenizer(self) -> Any: 

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

255 return self._tokenizer 

256 

257 @tokenizer.setter 

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

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

260 

261 On initial assignment (during __init__), the boot path has already run 

262 setup_tokenizer, so we skip calling it again. However, we still infer 

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

264 

265 On reassignment, we re-run the tokenizer wiring and update d_vocab to 

266 keep cfg in sync with the new tokenizer. 

267 """ 

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

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

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

271 if is_reassignment: 

272 from transformer_lens.model_bridge.sources._bridge_builder import ( 

273 detect_tokenizer_bos_eos, 

274 ) 

275 from transformer_lens.model_bridge.sources.transformers import ( 

276 setup_tokenizer, 

277 ) 

278 

279 value = setup_tokenizer( 

280 value, default_padding_side=getattr(cfg, "default_padding_side", None) 

281 ) 

282 cfg.tokenizer_prepends_bos, cfg.tokenizer_appends_eos = detect_tokenizer_bos_eos( 

283 value 

284 ) 

285 

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

287 # on reassignment always update to match new tokenizer. 

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

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

290 if d_vocab == -1 or is_reassignment: 

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

292 vocab = value.get_vocab() 

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

294 elif hasattr(value, "vocab"): 

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

296 else: 

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

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

299 if d_vocab_out == -1 or is_reassignment: 

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

301 self._tokenizer = value 

302 

303 @classmethod 

304 def boot_transformers( 

305 cls, 

306 model_name: str, 

307 hf_config_overrides: Optional[dict] = None, 

308 device: Optional[Union[str, torch.device]] = None, 

309 dtype: torch.dtype = torch.float32, 

310 tokenizer: Optional[Any] = None, 

311 load_weights: bool = True, 

312 trust_remote_code: bool = False, 

313 model_class: Optional[type] = None, 

314 hf_model: Optional[Any] = None, 

315 device_map: Optional[Union[str, Dict[str, Union[str, int]]]] = None, 

316 n_devices: Optional[int] = None, 

317 max_memory: Optional[Dict[Union[str, int], Union[str, int]]] = None, 

318 n_ctx: Optional[int] = None, 

319 revision: Optional[str] = None, 

320 checkpoint_index: Optional[int] = None, 

321 checkpoint_value: Optional[int] = None, 

322 ) -> "TransformerBridge": 

323 """Boot a model from HuggingFace (alias for sources.transformers.boot). 

324 

325 Returns raw HF weights by default — logits/activations match HF, *not* 

326 legacy ``HookedTransformer`` (which folds LayerNorm + centers weights). 

327 Call ``enable_compatibility_mode()`` on the result for HookedTransformer- 

328 equivalent numerics. Generation, argmax, and CE loss are unaffected. 

329 

330 Attention implementation is forced to ``"eager"`` so hooks can capture scores 

331 and patterns. For an apples-to-apples HF comparison, load the HF model with 

332 ``attn_implementation="eager"`` too; comparing against the default ``"sdpa"`` 

333 shows ~1e-3 fp32 drift from kernel-level op reordering, not a bridge bug. 

334 

335 Args: 

336 model_name: The name of the model to load. 

337 hf_config_overrides: Optional overrides applied to the HuggingFace config before model load. 

338 device: The device to use. If None, will be determined automatically. Mutually exclusive 

339 with ``device_map``. 

340 dtype: The dtype to use for the model. 

341 tokenizer: Optional pre-initialized tokenizer to use; if not provided one will be created. 

342 load_weights: If False, load model without weights (on meta device) for config inspection only. 

343 trust_remote_code: Whether to trust remote code for custom model architectures. 

344 model_class: Optional HuggingFace model class to use instead of the default 

345 auto-detected class (e.g., BertForNextSentencePrediction). 

346 hf_model: Optional pre-loaded HuggingFace model to use instead of loading one. Useful 

347 for models loaded with custom configurations (e.g., quantization via 

348 BitsAndBytesConfig). When provided, load_weights is ignored. If the pre-loaded 

349 model was built with a ``device_map``, ``cfg.device`` and ``cfg.n_devices`` are 

350 derived from its ``hf_device_map`` automatically. 

351 device_map: HuggingFace-style device map for dispatched inference. Pass ``"auto"``, 

352 ``"balanced"``, ``"sequential"``, or an explicit ``{submodule_path: device}`` 

353 dict. Explicit maps may include CPU targets; disk / meta offload targets are 

354 still rejected because Bridge component wrappers need additional offload-hook 

355 routing work. Mutually exclusive with ``device``. 

356 n_devices: Convenience shortcut: split the model across this many CUDA devices. 

357 Translated to a ``max_memory`` dict over devices 0..n_devices-1 and passed as 

358 ``device_map`` to HF. Requires CUDA with at least this many visible devices. 

359 max_memory: Optional per-device memory budget, passed through to HF's dispatcher. 

360 Only used when ``device_map`` or ``n_devices`` is in effect. 

361 n_ctx: Optional context length override. Writes to the appropriate HF config field 

362 for this model automatically (callers don't need to know the field name). 

363 Warns if larger than the model's default context length. 

364 revision: Optional HF revision (branch, tag, or commit). Forwarded to the underlying 

365 ``AutoConfig.from_pretrained`` and ``AutoModelForCausalLM.from_pretrained`` calls. 

366 Mutually exclusive with ``checkpoint_index`` / ``checkpoint_value``. 

367 checkpoint_index: Index into the available training checkpoints for the model family 

368 (currently ``EleutherAI/pythia*`` and ``stanford-crfm/*``). Resolved to a revision 

369 string via known per-family naming conventions. 

370 checkpoint_value: Training step or token count of the desired checkpoint. Alternative 

371 to ``checkpoint_index``; must match an entry in the family's checkpoint label list. 

372 

373 Returns: 

374 The bridge to the loaded model. 

375 """ 

376 from transformer_lens.model_bridge.sources.transformers import boot 

377 

378 return boot( 

379 model_name=model_name, 

380 hf_config_overrides=hf_config_overrides, 

381 device=device, 

382 dtype=dtype, 

383 tokenizer=tokenizer, 

384 load_weights=load_weights, 

385 trust_remote_code=trust_remote_code, 

386 model_class=model_class, 

387 hf_model=hf_model, 

388 device_map=device_map, 

389 n_devices=n_devices, 

390 max_memory=max_memory, 

391 n_ctx=n_ctx, 

392 revision=revision, 

393 checkpoint_index=checkpoint_index, 

394 checkpoint_value=checkpoint_value, 

395 ) 

396 

397 @overload 

398 @classmethod 

399 def boot_native( 

400 cls, 

401 config: TransformerBridgeConfig, 

402 tokenizer: Optional[Any] = None, 

403 device: Optional[Union[str, torch.device]] = None, 

404 dtype: Optional[torch.dtype] = None, 

405 model_name: str = "native", 

406 ) -> "TransformerBridge": 

407 ... 

408 

409 @overload 

410 @classmethod 

411 def boot_native( 

412 cls, 

413 config: Dict[str, Any], 

414 tokenizer: Optional[Any] = None, 

415 device: Optional[Union[str, torch.device]] = None, 

416 dtype: Optional[torch.dtype] = None, 

417 model_name: str = "native", 

418 ) -> "TransformerBridge": 

419 ... 

420 

421 @classmethod 

422 def boot_native( 

423 cls, 

424 config: Any, 

425 tokenizer: Optional[Any] = None, 

426 device: Optional[Union[str, torch.device]] = None, 

427 dtype: Optional[torch.dtype] = None, 

428 model_name: str = "native", 

429 ) -> "TransformerBridge": 

430 """Build a bridge around a small, randomly-initialized TL-native model. 

431 

432 No HuggingFace Hub call, no ``transformers`` import. ``config.init_mode`` 

433 and ``config.seed`` control reproducibility. 

434 """ 

435 # Impl signature stays Any so this guard is reachable — a Union hint 

436 # would have beartype reject foreign configs with its own error first. 

437 if not isinstance(config, (TransformerBridgeConfig, dict)): 

438 raise TypeError( 

439 "boot_native expected a TransformerBridgeConfig or dict, " 

440 f"got {type(config).__name__}. Construct a TransformerBridgeConfig " 

441 "with the same fields." 

442 ) 

443 

444 import copy as _copy 

445 

446 from transformer_lens.config import TransformerBridgeConfig as _Cfg 

447 from transformer_lens.model_bridge.sources._bridge_builder import ( 

448 build_bridge_from_module, 

449 ) 

450 from transformer_lens.model_bridge.sources.native import ( 

451 NativeModel, 

452 initialize_native_model, 

453 ) 

454 

455 cfg: TransformerBridgeConfig 

456 if isinstance(config, dict): 

457 cfg = _Cfg.from_dict(config) 

458 else: 

459 # Deep-copy so NativeModel's default-resolution writes don't land 

460 # on the caller's config. 

461 cfg = _copy.deepcopy(config) 

462 

463 # Foreign architecture strings would dispatch to the wrong adapter and 

464 # crash deep in prepare_model. Refuse them with a pointing message. 

465 if cfg.architecture not in (None, "TransformerLensNative"): 

466 raise ValueError( 

467 f"boot_native cannot build a {cfg.architecture!r} model — " 

468 f"it only constructs the TL-native architecture. Either clear " 

469 f"config.architecture or set it to 'TransformerLensNative', " 

470 f"or use boot_transformers / build_bridge_from_module for " 

471 f"non-native architectures." 

472 ) 

473 architecture = "TransformerLensNative" 

474 

475 # Fork RNG around construction + init when seeded so neither nn.Linear's 

476 # default reset_parameters nor our scoped init perturb the caller's RNG. 

477 # When custom init is disabled, construction keeps PyTorch's normal global 

478 # RNG semantics and cfg.seed has no initialization work to control. 

479 if cfg.init_weights and cfg.seed is not None: 

480 with torch.random.fork_rng(devices=[]): 

481 model = NativeModel(cfg) 

482 initialize_native_model(model, cfg) 

483 else: 

484 model = NativeModel(cfg) 

485 if cfg.init_weights: 

486 initialize_native_model(model, cfg) 

487 

488 if device is not None: 

489 model = model.to(device) 

490 if dtype is not None: 490 ↛ 491line 490 didn't jump to line 491 because the condition on line 490 was never true

491 model = model.to(dtype=dtype) 

492 

493 return build_bridge_from_module( 

494 model, 

495 architecture=architecture, 

496 tl_config=cfg, 

497 tokenizer=tokenizer, 

498 dtype=dtype, 

499 device=device, 

500 model_name=model_name, 

501 ) 

502 

503 def init_weights(self) -> None: 

504 """Reinitialize a TL-native model in place using the bridge config.""" 

505 from transformer_lens.model_bridge.sources.native.init import ( 

506 initialize_native_model, 

507 ) 

508 from transformer_lens.model_bridge.sources.native.model import NativeModel 

509 

510 model = self.original_model 

511 if not isinstance(model, NativeModel): 

512 raise RuntimeError( 

513 "TransformerBridge.init_weights() is only supported for TL-native " 

514 "bridges created with TransformerBridge.boot_native(...); this bridge " 

515 f"wraps {type(model).__name__}." 

516 ) 

517 initialize_native_model(model, self.cfg) 

518 

519 @property 

520 def original_model(self) -> nn.Module: 

521 """Return the wrapped underlying model; raises AttributeError if it was never set.""" 

522 if "original_model" not in self.__dict__: 

523 raise AttributeError("original_model has not been set") 

524 return self.__dict__["original_model"] 

525 

526 @original_model.setter 

527 def original_model(self, value: nn.Module) -> None: 

528 """Store the model in __dict__ so nn.Module does not register it as a submodule.""" 

529 self.__dict__["original_model"] = value 

530 

531 def _register_aliases(self) -> None: 

532 """Register bridge-level aliases. 

533 

534 This is called at the END of __init__ when all components are set up. 

535 It registers the top-level bridge aliases (hook_embed, hook_pos_embed, etc.) 

536 and creates direct attribute references. 

537 """ 

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

539 self._hook_alias_registry.update(self.hook_aliases) 

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

541 try: 

542 if isinstance(target_path, list): 

543 for single_target in target_path: 

544 try: 

545 target_obj = self 

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

547 target_obj = getattr(target_obj, part) 

548 object.__setattr__(self, alias_name, target_obj) 

549 break 

550 except AttributeError: 

551 continue 

552 else: 

553 target_obj = self 

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

555 target_obj = getattr(target_obj, part) 

556 object.__setattr__(self, alias_name, target_obj) 

557 except AttributeError: 

558 pass 

559 

560 def _set_processed_weight_attributes(self) -> None: 

561 """Create 3D processed weight attributes for attention components. 

562 

563 For each attention component, if it has 2D weights (q.weight, k.weight, v.weight), 

564 reshape them to 3D format [n_heads, d_model, d_head] and set as: 

565 - _processed_W_Q 

566 - _processed_W_K 

567 - _processed_W_V 

568 - _processed_b_Q 

569 - _processed_b_K 

570 - _processed_b_V 

571 

572 This allows property aliases (W_Q, W_K, W_V) to return 3D format for 

573 HookedTransformer compatibility while keeping 2D format for calculations. 

574 """ 

575 

576 n_heads = self.cfg.n_heads 

577 d_head = self.cfg.d_head 

578 d_model = self.cfg.d_model 

579 blocks_iter = [] 

580 for bl_name in _BLOCK_LIST_ATTRS: 

581 if hasattr(self, bl_name): 

582 blocks_iter.append(getattr(self, bl_name)) 

583 if not blocks_iter: 

584 return 

585 for block in [b for bl in blocks_iter for b in bl]: 

586 if "attn" not in block._modules: 

587 continue 

588 attn = block.attn 

589 if not (hasattr(attn, "q") and hasattr(attn.q, "weight")): 

590 continue 

591 try: 

592 w_q_2d = attn.q.weight.data 

593 w_k_2d = attn.k.weight.data 

594 w_v_2d = attn.v.weight.data 

595 attn._processed_W_Q = einops.rearrange( 

596 w_q_2d, "m (i h) -> i m h", i=n_heads, h=d_head 

597 ) 

598 attn._processed_W_K = einops.rearrange( 

599 w_k_2d, "m (i h) -> i m h", i=n_heads, h=d_head 

600 ) 

601 attn._processed_W_V = einops.rearrange( 

602 w_v_2d, "m (i h) -> i m h", i=n_heads, h=d_head 

603 ) 

604 if hasattr(attn.q, "bias") and attn.q.bias is not None: 

605 b_q_2d = attn.q.bias.data 

606 b_k_2d = attn.k.bias.data 

607 b_v_2d = attn.v.bias.data 

608 attn._processed_b_Q = einops.rearrange( 

609 b_q_2d, "(i h) -> i h", i=n_heads, h=d_head 

610 ) 

611 attn._processed_b_K = einops.rearrange( 

612 b_k_2d, "(i h) -> i h", i=n_heads, h=d_head 

613 ) 

614 attn._processed_b_V = einops.rearrange( 

615 b_v_2d, "(i h) -> i h", i=n_heads, h=d_head 

616 ) 

617 if hasattr(attn, "o") and hasattr(attn.o, "weight"): 

618 w_o_2d = attn.o.weight.data 

619 w_o_transposed = w_o_2d.T 

620 attn._processed_W_O = einops.rearrange( 

621 w_o_transposed, "m (i h) -> i h m", i=n_heads, h=d_head 

622 ) 

623 if hasattr(attn.o, "bias") and attn.o.bias is not None: 

624 attn._processed_b_O = attn.o.bias.data 

625 except Exception: 

626 pass 

627 

628 def _register_all_aliases_recursive(self) -> None: 

629 """Recursively register aliases on all bridge components. 

630 

631 This walks through all components and calls _register_aliases() on each one. 

632 Used after weight processing to ensure aliases point to processed weights. 

633 """ 

634 if hasattr(self, "_register_aliases"): 634 ↛ 636line 634 didn't jump to line 636 because the condition on line 634 was always true

635 self._register_aliases() 

636 for module in self.modules(): 

637 if module is not self and hasattr(module, "_register_aliases"): 

638 getattr(module, "_register_aliases")() 

639 

640 def __setattr__(self, name: str, value: Any) -> None: 

641 """Override setattr to track HookPoint objects dynamically.""" 

642 super().__setattr__(name, value) 

643 if isinstance(value, HookPoint): 643 ↛ 644line 643 didn't jump to line 644 because the condition on line 643 was never true

644 value.name = name 

645 self._hook_registry[name] = value 

646 elif hasattr(value, "get_hooks") and callable(getattr(value, "get_hooks")): 

647 component_hooks = value.get_hooks() 

648 for hook_name, hook in component_hooks.items(): 

649 full_name = f"{name}.{hook_name}" 

650 hook.name = full_name 

651 self._hook_registry[full_name] = hook 

652 

653 def _initialize_hook_registry(self) -> None: 

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

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

656 return 

657 self._scan_existing_hooks(self, "") 

658 self._hook_registry_initialized = True 

659 

660 def _collect_component_aliases(self, component_mapping, prefix="", _ancestors=frozenset()): 

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

662 

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

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

665 contributes aliases under both names. 

666 """ 

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

668 if id(component_mapping) in _ancestors: 

669 return aliases 

670 _ancestors = _ancestors | {id(component_mapping)} 

671 if isinstance(component_mapping, dict): 

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

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

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

675 else: 

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

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

678 # Skip fallback-list targets: the lru_cached consumer 

679 # endswith-matches strings, so a list is neither matchable 

680 # nor hashable; lists resolve via the instance walks instead. 

681 if not isinstance(target, str): 

682 continue 

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

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

685 aliases[full_alias] = full_target 

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

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

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

689 aliases.update( 

690 self._collect_component_aliases(sub_component, sub_prefix, _ancestors) 

691 ) 

692 return aliases 

693 

694 @staticmethod 

695 @lru_cache(maxsize=128) 

696 def _compute_hook_aliases_cached( 

697 hook_names_tuple: Tuple[str, ...], component_aliases_tuple: Tuple[Tuple[str, str], ...] 

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

699 """Cached computation of hook aliases. Takes immutable inputs for caching.""" 

700 aliases = {} 

701 component_aliases = dict(component_aliases_tuple) 

702 for hook_name in hook_names_tuple: 

703 for alias_pattern, target_pattern in component_aliases.items(): 

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

705 block_match = _BLOCK_PATTERN.search(hook_name) 

706 if block_match: 706 ↛ 703line 706 didn't jump to line 703 because the condition on line 706 was always true

707 block_num = block_match.group(1) 

708 dynamic_alias_pattern = alias_pattern.replace( 

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

710 ) 

711 dynamic_target_pattern = target_pattern.replace( 

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

713 ) 

714 if hook_name.endswith(dynamic_target_pattern): 

715 target_len = len(dynamic_target_pattern) 

716 alias_name = hook_name[:-target_len] + dynamic_alias_pattern 

717 aliases[alias_name] = hook_name 

718 elif hook_name.endswith(target_pattern): 

719 target_len = len(target_pattern) 

720 alias_name = hook_name[:-target_len] + alias_pattern 

721 aliases[alias_name] = hook_name 

722 return tuple(aliases.items()) 

723 

724 def _collect_hook_aliases_from_registry(self): 

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

726 if hasattr(self.adapter, "component_mapping"): 726 ↛ 736line 726 didn't jump to line 736 because the condition on line 726 was always true

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

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

729 component_aliases_tuple = tuple(sorted(component_aliases.items())) # type: ignore[operator] 

730 aliases_tuple = self._compute_hook_aliases_cached( 

731 hook_names_tuple, component_aliases_tuple 

732 ) 

733 aliases = dict(aliases_tuple) 

734 aliases.update(self._collect_block_instance_aliases()) 

735 return aliases 

736 return {} 

737 

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

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

740 

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

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

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

744 """ 

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

746 cached = self._block_alias_cache 

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

748 return cached[1] 

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

750 unresolved: List[str] = [] 

751 for bl_name in _BLOCK_LIST_ATTRS: 

752 block_list = getattr(self, bl_name, None) 

753 if block_list is None: 

754 continue 

755 for i, block in enumerate(block_list): 

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

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

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

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

760 continue 

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

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

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

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

765 # references, which would otherwise hang boot. 

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

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

768 # under two names still contributes aliases at both. 

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

770 while stack: 

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

772 if id(component) in ancestors: 

773 continue 

774 ancestors = ancestors | {id(component)} 

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

776 if component_aliases: 

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

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

779 for single_target in targets: 

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

781 if full_target in self._hook_registry: 

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

783 break 

784 else: 

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

786 for nested_name, nested in ( 

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

788 ).items(): 

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

790 if unresolved: 

791 # Surface drops instead of silently swallowing, mirroring 

792 # GeneralizedComponent._register_aliases. 

793 warnings.warn( 

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

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

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

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

798 stacklevel=2, 

799 ) 

800 self._block_alias_cache = (cache_key, aliases) 

801 return aliases 

802 

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

804 """Add aliases to hooks in place.""" 

805 component_aliases = self._collect_hook_aliases_from_registry() 

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

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

808 return 

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

810 if isinstance(target, list): 

811 for single_target in target: 

812 try: 

813 target_hook = resolve_alias(self, alias_name, {alias_name: single_target}) 

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

815 hooks[alias_name] = target_hook 

816 break 

817 except AttributeError: 

818 continue 

819 else: 

820 try: 

821 target_hook = resolve_alias(self, alias_name, {alias_name: target}) 

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

823 hooks[alias_name] = target_hook 

824 except AttributeError: 

825 continue 

826 

827 def _scan_existing_hooks(self, module: nn.Module, prefix: str = "") -> None: 

828 """Scan existing modules for hooks and add them to registry.""" 

829 visited = set() 

830 # Protect canonical HookPoint names from alias overwrites 

831 named_hook_ids: set = set() 

832 

833 def scan_module(mod: nn.Module, path: str = "") -> None: 

834 obj_id = id(mod) 

835 if obj_id in visited: 

836 return 

837 visited.add(obj_id) 

838 if hasattr(mod, "get_hooks") and callable(getattr(mod, "get_hooks")): 

839 component_hooks = mod.get_hooks() # type: ignore[operator] 

840 if isinstance(component_hooks, dict): 840 ↛ 849line 840 didn't jump to line 849 because the condition on line 840 was always true

841 hooks_dict = cast(Dict[str, HookPoint], component_hooks) 

842 for hook_name, hook in hooks_dict.items(): 

843 full_name = f"{path}.{hook_name}" if path else hook_name 

844 hook_id = id(hook) 

845 if hook_id not in named_hook_ids: 

846 hook.name = full_name 

847 named_hook_ids.add(hook_id) 

848 self._hook_registry[full_name] = hook 

849 for attr_name in dir(mod): 

850 if attr_name.startswith("_"): 

851 continue 

852 if attr_name == "original_component" or attr_name == "original_model": 

853 continue 

854 if attr_name in [ 

855 "OV", 

856 "QK", 

857 "W_V", 

858 "W_O", 

859 "W_Q", 

860 "W_K", 

861 "W_in", 

862 "W_gate", 

863 "W_out", 

864 "b_V", 

865 "b_O", 

866 "b_Q", 

867 "b_K", 

868 "b_in", 

869 "b_out", 

870 ]: 

871 continue 

872 try: 

873 attr = getattr(mod, attr_name) 

874 except (AttributeError, NameError, RuntimeError, TypeError): 

875 continue 

876 name = f"{path}.{attr_name}" if path else attr_name 

877 if isinstance(attr, HookPoint): 

878 hook_id = id(attr) 

879 if hook_id not in named_hook_ids: 

880 attr.name = name 

881 named_hook_ids.add(hook_id) 

882 self._hook_registry[name] = attr 

883 for child_name, child_module in mod.named_children(): 

884 if ( 

885 child_name == "original_component" 

886 or child_name == "_original_component" 

887 or child_name == "original_model" 

888 ): 

889 continue 

890 child_path = f"{path}.{child_name}" if path else child_name 

891 scan_module(child_module, child_path) 

892 

893 scan_module(module, prefix) 

894 

895 @property 

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

897 """Get all HookPoint objects in the model for compatibility with TransformerLens.""" 

898 hooks = self._hook_registry.copy() 

899 self._add_aliases_to_hooks(hooks) 

900 return hooks 

901 

902 @property 

903 def n_params_total(self) -> int: 

904 """Number of parameters in the wrapped model before bridge instrumentation. 

905 

906 This follows PyTorch's parameter iteration semantics, counting tied 

907 parameters once. Bridge-created split views and synthetic zero tensors 

908 are excluded, so the result can differ from 

909 :attr:`HookedTransformer.n_params_total` and :meth:`tl_parameters`. 

910 

911 Returns: 

912 int: Parameter count of the uninstrumented wrapped model. 

913 """ 

914 return self._n_params_total 

915 

916 def clear_hook_registry(self) -> None: 

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

918 self._hook_registry.clear() 

919 self._hook_registry_initialized = False 

920 

921 def _initialize_hooks_to_cache(self) -> None: 

922 """Initialize the hooks to cache when running the model with cache.""" 

923 self.hooks_to_cache = {} 

924 default_cached_hooks_names = [ 

925 "embed.hook_in", 

926 "embed.hook_out", 

927 "pos_embed.hook_in", 

928 "pos_embed.hook_out", 

929 "rotary_embed.hook_in", 

930 "rotary_embed.hook_out", 

931 "ln_final.hook_in", 

932 "ln_final.hook_scale", 

933 "ln_final.hook_normalized", 

934 "ln_final.hook_out", 

935 "unembed.hook_in", 

936 "unembed.hook_out", 

937 ] 

938 for block_idx in range(self.cfg.n_layers): 

939 default_cached_hooks_names.append(f"blocks.{block_idx}.hook_in") 

940 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1.hook_in") 

941 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1.hook_scale") 

942 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1.hook_normalized") 

943 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1.hook_out") 

944 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1_post.hook_in") 

945 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1_post.hook_scale") 

946 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1_post.hook_normalized") 

947 default_cached_hooks_names.append(f"blocks.{block_idx}.ln1_post.hook_out") 

948 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.hook_in") 

949 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.q.hook_in") 

950 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.q.hook_out") 

951 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.q_norm.hook_in") 

952 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.q_norm.hook_out") 

953 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.k.hook_in") 

954 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.k.hook_out") 

955 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.k_norm.hook_in") 

956 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.k_norm.hook_out") 

957 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.v.hook_in") 

958 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.v.hook_out") 

959 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.o.hook_in") 

960 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.o.hook_out") 

961 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.hook_attn_scores") 

962 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.hook_pattern") # type: ignore[operator] 

963 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.hook_hidden_states") 

964 default_cached_hooks_names.append(f"blocks.{block_idx}.attn.hook_out") 

965 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2.hook_in") 

966 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2.hook_scale") 

967 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2.hook_normalized") 

968 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2.hook_out") 

969 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2_post.hook_in") # type: ignore[operator] 

970 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2_post.hook_scale") 

971 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2_post.hook_normalized") 

972 default_cached_hooks_names.append(f"blocks.{block_idx}.ln2_post.hook_out") 

973 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.hook_in") # type: ignore[operator] 

974 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.in.hook_in") 

975 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.in.hook_out") # type: ignore[operator] 

976 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.out.hook_in") 

977 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.out.hook_out") 

978 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.gate.hook_in") 

979 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.gate.hook_out") 

980 default_cached_hooks_names.append(f"blocks.{block_idx}.mlp.hook_out") 

981 default_cached_hooks_names.append(f"blocks.{block_idx}.hook_out") 

982 for hook_name in default_cached_hooks_names: 

983 if hook_name in self._hook_registry: 

984 self.hooks_to_cache[hook_name] = self._hook_registry[hook_name] # type: ignore[arg-type] 

985 

986 def __getattr__(self, name: str) -> Any: 

987 """Provide a clear error message for missing attributes.""" 

988 if name in self.__dict__: # type: ignore[arg-type] 988 ↛ 989line 988 didn't jump to line 989 because the condition on line 988 was never true

989 return self.__dict__[name] 

990 # Use __dict__ directly to avoid recursion 

991 if "_modules" in self.__dict__ and name in self.__dict__["_modules"]: # type: ignore[arg-type] 

992 return self.__dict__["_modules"][name] 

993 adapter = self.__dict__.get("adapter") 

994 component_mapping = getattr(adapter, "component_mapping", None) 

995 if component_mapping is not None and name in component_mapping: 

996 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") 

997 if "original_model" in self.__dict__ and self.__dict__["original_model"] is not None: 

998 try: 

999 name_split = name.split(".") 

1000 if len(name_split) > 1: 1000 ↛ 1001line 1000 didn't jump to line 1001 because the condition on line 1000 was never true

1001 current = getattr(self.__dict__["original_model"], name_split[0]) 

1002 for part in name_split[1:]: # type: ignore[operator] 

1003 current = getattr(current, part) 

1004 return current 

1005 else: 

1006 return getattr(self.__dict__["original_model"], name) 

1007 except AttributeError: 

1008 pass # type: ignore[operator,assignment] 

1009 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") 

1010 

1011 def __str__(self) -> str: 

1012 """Get a string representation of the bridge. 

1013 # type: ignore[operator] 

1014 Returns: 

1015 A string describing the bridge's components # type: ignore[operator] 

1016 """ 

1017 lines = ["TransformerBridge:"] 

1018 mapping = self.adapter.get_component_mapping() 

1019 lines.extend(self._format_component_mapping(mapping, indent=1)) 

1020 return "\n".join(lines) 

1021 

1022 def enable_compatibility_mode( 

1023 self, 

1024 disable_warnings: bool = False, 

1025 no_processing: bool = False, 

1026 fold_ln: bool = True, 

1027 center_writing_weights: bool = True, 

1028 center_unembed: bool = True, 

1029 fold_value_biases: bool = True, 

1030 refactor_factored_attn_matrices: bool = False, 

1031 ) -> None: 

1032 """Apply HookedTransformer-equivalent weight processing and legacy hook compatibility. 

1033 

1034 Defaults match HookedTransformer's load-time processing (fold_ln + weight 

1035 centering) — required for analyses that reason in HookedTransformer's 

1036 post-processed coordinate system: logit lens, direct logit attribution, 

1037 residual-stream norms. Also enables legacy hook/component name aliases. 

1038 

1039 Hook semantic parity (issue #1317): ``hook_q_input``, ``hook_k_input``, 

1040 ``hook_v_input``, ``hook_attn_in``, and ``hook_mlp_in`` fire on the 

1041 pre-norm residual. Carve-outs: post-norm architectures (OLMo 2, 

1042 BERT-style) read the post-attention residual instead, and MLA blocks 

1043 (DeepSeek V2/V3/R1) do not expose the split-qkv aliases. ``hook_mlp_in`` 

1044 is gated on ``cfg.use_hook_mlp_in``; toggle it via 

1045 :py:meth:`set_use_hook_mlp_in`. 

1046 

1047 Args: 

1048 disable_warnings: Whether to disable warnings about legacy components/hooks 

1049 no_processing: Whether to disable ALL pre-processing steps of the model. 

1050 If True, overrides fold_ln, center_writing_weights, and center_unembed to False. 

1051 fold_ln: Whether to fold layer norm weights into the subsequent linear layers. 

1052 Default: True. Ignored if no_processing=True. 

1053 center_writing_weights: Whether to center the writing weights (W_out in attention and MLPs). 

1054 Default: True. Ignored if no_processing=True. 

1055 center_unembed: Whether to center the unembedding matrix. 

1056 Default: True. Ignored if no_processing=True. 

1057 fold_value_biases: Whether to fold value biases into output bias. 

1058 Default: True. Ignored if no_processing=True. 

1059 refactor_factored_attn_matrices: Whether to refactor factored attention matrices. 

1060 Default: False. Ignored if no_processing=True. 

1061 """ 

1062 from transformer_lens.utilities.bridge_components import ( 

1063 apply_fn_to_all_components, 

1064 ) 

1065 

1066 if not getattr(self.adapter, "supports_compatibility_mode", True): 

1067 raise RuntimeError( 

1068 f"{type(self.adapter).__name__} does not support compatibility mode: " 

1069 "its stored-processed-weights is known to diverge from the " 

1070 "reference model. Use the default bridge forward instead." 

1071 ) 

1072 

1073 self.compatibility_mode = True 

1074 

1075 def set_compatibility_mode(component: Any) -> None: 

1076 """Set compatibility mode on a component.""" 

1077 component.compatibility_mode = True 

1078 component.disable_warnings = disable_warnings 

1079 

1080 apply_fn_to_all_components(self, set_compatibility_mode) 

1081 self.clear_hook_registry() 

1082 # Drop block capture-hook handles from any prior call so they don't accumulate. 

1083 if hasattr(self, "blocks"): 1083 ↛ 1087line 1083 didn't jump to line 1087 because the condition on line 1083 was always true

1084 for block in self.blocks: 

1085 if hasattr(block, "_teardown_capture_hooks"): 1085 ↛ 1084line 1085 didn't jump to line 1084 because the condition on line 1085 was always true

1086 block._teardown_capture_hooks() 

1087 try: 

1088 if not no_processing: 

1089 self.process_weights( 

1090 fold_ln=fold_ln, 

1091 center_writing_weights=center_writing_weights, 

1092 center_unembed=center_unembed, 

1093 fold_value_biases=fold_value_biases, 

1094 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

1095 ) 

1096 finally: 

1097 # Re-initialize hooks even on failure so bridge stays usable 

1098 self._initialize_hook_registry() 

1099 self._setup_hook_compatibility() 

1100 self._register_all_aliases_recursive() 

1101 

1102 def _setup_hook_compatibility(self) -> None: 

1103 """Setup hook compatibility transformations to match HookedTransformer behavior. 

1104 

1105 This method sets up hook conversions and wrappers that ensure Bridge hooks 

1106 have the same shapes and behavior as HookedTransformer hooks. This includes: 

1107 1. hook_z reshaping from [batch, seq, d_model] to [batch, seq, n_heads, d_head] 

1108 2. Wrapping HF attention forward to inject position embeddings/attention masks 

1109 3. Architecture-specific setup (e.g., rotary embedding references) 

1110 

1111 This is called during __init__ and should always be run, regardless of whether 

1112 compatibility mode or weight processing is enabled. 

1113 

1114 Note: This method is idempotent - can be called multiple times safely. 

1115 """ 

1116 if hasattr(self.adapter, "setup_hook_compatibility"): 

1117 self.adapter.setup_hook_compatibility(self) 

1118 elif hasattr(self.adapter, "setup_no_processing_hooks"): 1118 ↛ 1119line 1118 didn't jump to line 1119 because the condition on line 1118 was never true

1119 self.adapter.setup_no_processing_hooks(self) 

1120 blocks_to_process = [] 

1121 for block_list_name in ( 

1122 "blocks", 

1123 "encoder_blocks", 

1124 "decoder_blocks", 

1125 "L_blocks", 

1126 "H_blocks", 

1127 ): 

1128 if hasattr(self, block_list_name): 

1129 blocks_to_process.extend(getattr(self, block_list_name)) 

1130 for block in blocks_to_process: 

1131 for attn_name in ["attn", "self_attn", "cross_attn"]: 

1132 if hasattr(block, attn_name): 

1133 attn = getattr(block, attn_name) 

1134 if hasattr(attn, "setup_hook_compatibility"): 

1135 attn.setup_hook_compatibility() 

1136 elif hasattr(attn, "setup_no_processing_hooks"): 1136 ↛ 1137line 1136 didn't jump to line 1137 because the condition on line 1136 was never true

1137 attn.setup_no_processing_hooks() 

1138 

1139 def process_weights( 

1140 self, 

1141 verbose: bool = False, 

1142 fold_ln: bool = True, 

1143 center_writing_weights: bool = True, 

1144 center_unembed: bool = True, 

1145 fold_value_biases: bool = True, 

1146 refactor_factored_attn_matrices: bool = False, 

1147 ) -> None: 

1148 """Process weights directly using ProcessWeights and architecture adapter. 

1149 

1150 This method applies weight processing transformations to improve model interpretability 

1151 without requiring a reference HookedTransformer model. Works with all architectures 

1152 supported by TransformerBridge, including GPT-OSS and other new models. 

1153 

1154 Args: 

1155 verbose: If True, print detailed progress messages. Default: False 

1156 fold_ln: Fold LayerNorm weights/biases into subsequent layers. Default: True 

1157 center_writing_weights: Center weights that write to residual stream. Default: True 

1158 center_unembed: Center unembedding weights (translation invariant). Default: True 

1159 fold_value_biases: Fold value biases into output bias. Default: True 

1160 refactor_factored_attn_matrices: Experimental QK/OV factorization. Default: False 

1161 """ 

1162 # Folding and centering do arithmetic on raw weights, so packed or 

1163 # scale-separated storage would produce silent garbage. The forward 

1164 # path stays usable when quantized; only this transformation does not. 

1165 for name, param in self.original_model.named_parameters(): 

1166 # No name filter: modern batched-MoE experts are Parameters that do 

1167 # NOT end in .weight (mlp.experts.gate_up_proj), and those are the 

1168 # very tensors the converters had to guard. unreadable_weight_reason 

1169 # is dtype-driven, so every full-width float parameter still passes. 

1170 # Routed through the shared helper so meta gets its own "load with 

1171 # real weights" message instead of being silently skipped — folding 

1172 # on meta tensors yields meta tensors, which is the same 

1173 # silent-garbage failure this guard exists to stop. 

1174 require_readable_weight( 

1175 param, 

1176 operation=f"process weights ({name})", 

1177 owner=self.original_model, 

1178 remedy=( 

1179 "Load the model dequantized, or use the bridge without weight " 

1180 "processing (enable_compatibility_mode(no_processing=True))." 

1181 ), 

1182 ) 

1183 

1184 # A failed or partial processing attempt is no longer guaranteed to retain 

1185 # the raw HuggingFace basis, so invalidate that contract before any work. 

1186 self._weights_processed = True 

1187 from transformer_lens.weight_processing import ProcessWeights 

1188 

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

1190 print(f"Processing weights for {self.cfg.model_name}...") 

1191 

1192 # Soft capping (tanh) is not translation-invariant; centering would change output. 

1193 if center_unembed and softcap_enabled(getattr(self.cfg, "output_logits_soft_cap", None)): 1193 ↛ 1194line 1193 didn't jump to line 1194 because the condition on line 1193 was never true

1194 import logging 

1195 

1196 logging.warning( 

1197 "center_unembed=True is incompatible with logit softcapping " 

1198 "(output_logits_soft_cap=%.1f). Disabling center_unembed.", 

1199 self.cfg.output_logits_soft_cap, 

1200 ) 

1201 center_unembed = False 

1202 

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

1204 print(" Extracting state dict from existing model...") 

1205 state_dict = self.state_dict() 

1206 adapter = self.adapter 

1207 

1208 # Untie embed/unembed weights (GPT-2) so centering affects only unembed 

1209 embed_key = "embed.weight" 

1210 unembed_key = "unembed.weight" 

1211 

1212 if embed_key in state_dict and unembed_key in state_dict: 1212 ↛ 1220line 1212 didn't jump to line 1220 because the condition on line 1212 was always true

1213 # Check if they point to the same tensor (weight tying) 

1214 if state_dict[embed_key].data_ptr() == state_dict[unembed_key].data_ptr(): 

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

1216 print(" Breaking weight tying between embed and unembed in state dict...") 

1217 # Clone the unembed weight to break the tie 

1218 state_dict[unembed_key] = state_dict[unembed_key].clone() 

1219 

1220 if adapter and hasattr(adapter, "preprocess_weights"): 1220 ↛ 1226line 1220 didn't jump to line 1226 because the condition on line 1220 was always true

1221 adapter._fold_ln_requested = fold_ln # type: ignore[union-attr] 

1222 state_dict = adapter.preprocess_weights(state_dict) 

1223 

1224 # Use unified ProcessWeights.process_weights() like HookedTransformer does. 

1225 # Float32 upcasting for precision is handled centrally in process_weights(). 

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

1227 print(" Processing weights (fold_ln, center_writing_weights, etc.)...") 

1228 state_dict = ProcessWeights.process_weights( 

1229 state_dict, 

1230 self.cfg, 

1231 fold_ln=fold_ln, 

1232 center_writing_weights=center_writing_weights, 

1233 center_unembed=center_unembed, 

1234 fold_value_biases=fold_value_biases, 

1235 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

1236 adapter=adapter, 

1237 ) 

1238 

1239 # Normalize HF-prefix keys to TL format for weight routing 

1240 import re 

1241 

1242 hf_to_tl_prefix = {} 

1243 for tl_name, (remote_path, _component) in self.real_components.items(): 

1244 if remote_path and remote_path != tl_name: 1244 ↛ 1243line 1244 didn't jump to line 1243 because the condition on line 1244 was always true

1245 hf_to_tl_prefix[remote_path] = tl_name 

1246 

1247 normalized_state_dict = {} 

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

1249 new_key = key 

1250 for hf_prefix, tl_prefix in hf_to_tl_prefix.items(): 

1251 if key.startswith(hf_prefix + "."): 1251 ↛ 1252line 1251 didn't jump to line 1252 because the condition on line 1251 was never true

1252 suffix = key[len(hf_prefix) + 1 :] 

1253 new_key = f"{tl_prefix}.{suffix}" 

1254 break 

1255 normalized_state_dict[new_key] = value 

1256 state_dict = normalized_state_dict 

1257 

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

1259 print(" Distributing weights to generalized components...") 

1260 ProcessWeights.distribute_weights_to_components( 

1261 state_dict=state_dict, 

1262 component_mapping=self.real_components, 

1263 ) 

1264 

1265 def _calculate_loss(self, logits, tokens, loss_per_token=False): 

1266 """Calculate cross-entropy loss.""" 

1267 shift_logits = logits[..., :-1, :].contiguous() 

1268 shift_labels = tokens[..., 1:].contiguous() 

1269 loss_fct = torch.nn.CrossEntropyLoss(reduction="none" if loss_per_token else "mean") 

1270 flat_logits = shift_logits.view(-1, shift_logits.size(-1)) 

1271 flat_labels = shift_labels.view(-1) 

1272 loss = loss_fct(flat_logits, flat_labels) 

1273 if loss_per_token: 

1274 return loss.view(shift_labels.shape) 

1275 else: 

1276 return loss 

1277 

1278 def _extract_hf_weights(self): 

1279 """Extract weights from the original HuggingFace model.""" 

1280 hf_state_dict = self.state_dict() 

1281 for layer_idx in range(self.cfg.n_layers): 

1282 combined_qkv_key = f"transformer.h.{layer_idx}.attn.c_attn.weight" 

1283 combined_qkv_bias_key = f"transformer.h.{layer_idx}.attn.c_attn.bias" 

1284 if combined_qkv_key in hf_state_dict: 

1285 separate_keys_to_remove = [ 

1286 f"transformer.h.{layer_idx}.attn.q.weight", 

1287 f"transformer.h.{layer_idx}.attn.q.bias", 

1288 f"transformer.h.{layer_idx}.attn.k.weight", 

1289 f"transformer.h.{layer_idx}.attn.k.bias", 

1290 f"transformer.h.{layer_idx}.attn.v.weight", 

1291 f"transformer.h.{layer_idx}.attn.v.bias", 

1292 ] 

1293 for key_to_remove in separate_keys_to_remove: 

1294 if key_to_remove in hf_state_dict: 

1295 del hf_state_dict[key_to_remove] 

1296 return hf_state_dict 

1297 

1298 def to_tokens( 

1299 self, 

1300 input: Union[str, List[str]], 

1301 prepend_bos: Optional[bool] = None, 

1302 padding_side: Optional[str] = None, 

1303 move_to_device: bool = True, 

1304 truncate: bool = True, 

1305 ) -> torch.Tensor: 

1306 """Converts a string to a tensor of tokens. 

1307 

1308 See the class-level "Tokenization notes" for full ``prepend_bos`` 

1309 semantics, the ``default_prepend_bos`` / 

1310 ``tokenizer_prepends_bos`` interaction, and the whitespace- 

1311 sensitivity gotcha. **Pass ``prepend_bos=False`` whenever you're 

1312 tokenizing only part of a prompt.** 

1313 

1314 Args: 

1315 input: The input to tokenize. 

1316 prepend_bos: Overrides ``self.cfg.default_prepend_bos``. Defaults 

1317 to ``None`` (use the cfg setting). Pass ``True`` or ``False`` 

1318 to override locally. 

1319 padding_side: Which side to pad on when tokenizing multiple 

1320 strings of different lengths. Defaults to the tokenizer's 

1321 ``padding_side``. 

1322 move_to_device: Whether to move the result to ``cfg.device``. 

1323 truncate: Whether to truncate inputs longer than ``cfg.n_ctx``. 

1324 

1325 Returns: 

1326 Token tensor of shape ``[batch, pos]``. 

1327 """ 

1328 assert self.tokenizer is not None, "Cannot use to_tokens without a tokenizer" 

1329 if prepend_bos is None: 

1330 prepend_bos = getattr(self.cfg, "default_prepend_bos", True) 

1331 if padding_side is None: 

1332 padding_side = getattr(self.tokenizer, "padding_side", "right") 

1333 tokenizer_prepends_bos = getattr(self.cfg, "tokenizer_prepends_bos", True) 

1334 if prepend_bos and (not tokenizer_prepends_bos): 

1335 bos = self.tokenizer.bos_token 

1336 encodes_atomically = ( 

1337 bos is not None 

1338 and len(self.tokenizer(bos, add_special_tokens=False)["input_ids"]) == 1 

1339 ) 

1340 if encodes_atomically: 

1341 input = utils.get_input_with_manually_prepended_bos(bos, input) 

1342 # else: the fallback BOS is not an atom in this vocab (e.g. 

1343 # '<|endoftext|>' installed on BERT); prepending the string would 

1344 # tokenize to subword garbage, so skip rather than pollute the input. 

1345 if isinstance(input, str): 

1346 input = [input] 

1347 tokens = self.tokenizer( 

1348 input, 

1349 return_tensors="pt", 

1350 padding=True, 

1351 padding_side=padding_side, 

1352 truncation=truncate, 

1353 max_length=self.cfg.n_ctx if truncate else None, 

1354 )["input_ids"] 

1355 # Strip auto-appended EOS tokens (e.g., OLMo) 

1356 if ( 

1357 getattr(self.cfg, "tokenizer_appends_eos", False) 

1358 and self.tokenizer.eos_token_id is not None 

1359 ): 

1360 # Remove trailing EOS, keep at least 1 token 

1361 while tokens.shape[-1] > 1 and (tokens[:, -1] == self.tokenizer.eos_token_id).all(): 

1362 tokens = tokens[:, :-1] 

1363 if not prepend_bos and tokenizer_prepends_bos: 

1364 tokens = utils.get_tokens_with_bos_removed( 

1365 self.tokenizer, tokens, padding_side=padding_side 

1366 ) 

1367 if move_to_device: 

1368 tokens = tokens.to(self.cfg.device) 

1369 return tokens 

1370 

1371 def to_string( 

1372 self, tokens: Union[List[int], torch.Tensor, np.ndarray] 

1373 ) -> Union[str, List[str]]: 

1374 """Convert tokens to string(s). 

1375 

1376 Args: 

1377 tokens: Tokens to convert 

1378 

1379 Returns: 

1380 Decoded string(s) 

1381 """ 

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

1383 tokens = torch.tensor(tokens) 

1384 if len(tokens.shape) == 2: 

1385 return self.tokenizer.batch_decode(tokens, clean_up_tokenization_spaces=False) 

1386 elif len(tokens.shape) <= 1: 1386 ↛ 1389line 1386 didn't jump to line 1389 because the condition on line 1386 was always true

1387 return self.tokenizer.decode(tokens, clean_up_tokenization_spaces=False) 

1388 else: 

1389 raise ValueError(f"Invalid shape passed in: {tokens.shape}") 

1390 

1391 def to_str_tokens( 

1392 self, 

1393 input: Union[str, torch.Tensor, np.ndarray, List], 

1394 prepend_bos: Optional[bool] = None, 

1395 padding_side: Optional[str] = None, 

1396 ) -> Union[List[str], List[List[str]]]: 

1397 """Map text or tokens to a list of tokens as strings. 

1398 

1399 See the class-level "Tokenization notes" for full ``prepend_bos`` 

1400 semantics. **Pass ``prepend_bos=False`` whenever you're tokenizing 

1401 only part of a prompt.** When ``input`` is already a tensor or 

1402 array, ``prepend_bos`` and ``padding_side`` are ignored. 

1403 

1404 Args: 

1405 input: A string, list of strings, or tensor/array of token IDs. 

1406 prepend_bos: Overrides ``self.cfg.default_prepend_bos``. Only 

1407 applies when ``input`` is a string. Defaults to ``None`` 

1408 (use the cfg setting). 

1409 padding_side: Which side to pad on. Only applies when ``input`` 

1410 is a string. 

1411 

1412 Returns: 

1413 List of token strings. 

1414 """ 

1415 if isinstance(input, list): 1415 ↛ 1416line 1415 didn't jump to line 1416 because the condition on line 1415 was never true

1416 return cast( 

1417 List[List[str]], 

1418 [self.to_str_tokens(item, prepend_bos, padding_side) for item in input], 

1419 ) 

1420 elif isinstance(input, str): 1420 ↛ 1422line 1420 didn't jump to line 1422 because the condition on line 1420 was always true

1421 tokens = self.to_tokens(input, prepend_bos=prepend_bos, padding_side=padding_side)[0] 

1422 elif isinstance(input, torch.Tensor): 

1423 tokens = input.squeeze() 

1424 if tokens.dim() == 0: 

1425 tokens = tokens.unsqueeze(0) 

1426 assert ( 

1427 tokens.dim() == 1 

1428 ), f"Invalid tokens input to to_str_tokens, has shape: {tokens.shape}" 

1429 elif isinstance(input, np.ndarray): 

1430 tokens_np = input.squeeze() 

1431 if tokens_np.ndim == 0: 

1432 tokens_np = np.expand_dims(tokens_np, axis=0) 

1433 assert ( 

1434 tokens_np.ndim == 1 

1435 ), f"Invalid tokens input to to_str_tokens, has shape: {tokens_np.shape}" 

1436 tokens = torch.tensor(tokens_np) 

1437 else: 

1438 raise ValueError(f"Invalid input type to to_str_tokens: {type(input)}") 

1439 # v5 compat: wrap each token so batch_decode decodes them individually 

1440 tokens_list = [[int(t)] for t in tokens.tolist()] 

1441 str_tokens = self.tokenizer.batch_decode(tokens_list, clean_up_tokenization_spaces=False) 

1442 return str_tokens 

1443 

1444 def to_single_token(self, string: str) -> int: 

1445 """Map a string that makes up a single token to the id for that token. 

1446 

1447 Args: 

1448 string: The string to convert 

1449 

1450 Returns: 

1451 Token ID 

1452 

1453 Raises: 

1454 AssertionError: If string is not a single token 

1455 """ 

1456 token = self.to_tokens(string, prepend_bos=False).squeeze() 

1457 if token.numel() != 1: 1457 ↛ 1458line 1457 didn't jump to line 1458 because the condition on line 1457 was never true

1458 raise AssertionError(f"Input string: {string} is not a single token!") 

1459 return int(token.item()) 

1460 

1461 def get_token_position( 

1462 self, 

1463 single_token: Union[str, int], 

1464 input: Union[str, torch.Tensor], 

1465 mode="first", 

1466 prepend_bos: Optional[Union[bool, None]] = None, 

1467 padding_side: Optional[Union[Literal["left", "right"], None]] = None, 

1468 ): 

1469 """Get the position of a single_token in a string or sequence of tokens. 

1470 

1471 Raises an error if the token is not present. 

1472 

1473 When ``input`` is a string it's tokenized internally — see the 

1474 class-level "Tokenization notes" for ``prepend_bos`` semantics. 

1475 Off-by-one position errors usually mean ``prepend_bos`` is on 

1476 when it shouldn't be (or vice versa); pass ``prepend_bos=False`` 

1477 when ``input`` is a fragment of a larger prompt. 

1478 

1479 Args: 

1480 single_token (Union[str, int]): The token to search for. Can 

1481 be a token index, or a string (but the string must correspond to a single token). 

1482 input (Union[str, torch.Tensor]): The sequence to 

1483 search in. Can be a string or a rank 1 tensor of tokens or a rank 2 tensor of tokens 

1484 with a dummy batch dimension. 

1485 mode (str, optional): If there are multiple matches, which match to return. Supports 

1486 "first" or "last". Defaults to "first". 

1487 prepend_bos (bool, optional): Overrides ``self.cfg.default_prepend_bos``. Only 

1488 applies when ``input`` is a string. Defaults to ``None`` (use the cfg setting). 

1489 padding_side (Union[Literal["left", "right"], None], optional): Specifies which 

1490 side to pad when tokenizing multiple strings of different lengths. 

1491 """ 

1492 if isinstance(input, str): 

1493 tokens = self.to_tokens(input, prepend_bos=prepend_bos, padding_side=padding_side) 

1494 else: 

1495 tokens = input 

1496 if len(tokens.shape) == 2: 

1497 assert ( 

1498 tokens.shape[0] == 1 

1499 ), f"If tokens are rank two, they must have shape [1, seq_len], not {tokens.shape}" 

1500 tokens = tokens[0] 

1501 if isinstance(single_token, str): 

1502 single_token = self.to_single_token(single_token) 

1503 elif isinstance(single_token, torch.Tensor): 1503 ↛ 1504line 1503 didn't jump to line 1504 because the condition on line 1503 was never true

1504 single_token = single_token.item() 

1505 indices = torch.arange(len(tokens), device=tokens.device)[tokens == single_token] 

1506 assert len(indices) > 0, "The token does not occur in the prompt" 

1507 if mode == "first": 

1508 return indices[0].item() 

1509 elif mode == "last": 1509 ↛ 1512line 1509 didn't jump to line 1512 because the condition on line 1509 was always true

1510 return indices[-1].item() 

1511 else: 

1512 raise ValueError(f"mode must be 'first' or 'last', not {mode}") 

1513 

1514 def to_single_str_token(self, int_token: int) -> str: 

1515 """Get the single token corresponding to an int in string form. 

1516 

1517 Args: 

1518 int_token: The token ID 

1519 

1520 Returns: 

1521 The token string 

1522 """ 

1523 assert isinstance(int_token, int) 

1524 token = self.to_str_tokens(torch.tensor([int_token])) 

1525 if isinstance(token, list) and len(token) == 1: 

1526 return str(token[0]) 

1527 raise AssertionError("Expected a single string token.") 

1528 

1529 def blocks_with(self, submodule: str) -> List[Tuple[int, "GeneralizedComponent"]]: 

1530 """Return (index, block) pairs for blocks with the named bridged submodule. 

1531 

1532 Checks _modules (not hasattr) so HF-internal attrs don't match. 

1533 Use instead of assuming blocks[0] is representative on hybrid models. 

1534 """ 

1535 if not hasattr(self, "blocks"): 

1536 return [] 

1537 return [(i, block) for i, block in enumerate(self.blocks) if submodule in block._modules] 

1538 

1539 def stack_params_for( 

1540 self, submodule: str, attr_path: str, reshape_fn: Optional[Callable] = None 

1541 ) -> Tuple[List[int], torch.Tensor]: 

1542 """Stack a parameter across matching blocks only. Returns (layer_indices, tensor). 

1543 

1544 Use for hybrid models where not all blocks have the submodule. 

1545 """ 

1546 matching = self.blocks_with(submodule) 

1547 if not matching: 

1548 raise ValueError( 

1549 f"No blocks have submodule '{submodule}'. " 

1550 f"Available submodules can be checked with blocks_with()." 

1551 ) 

1552 indices: List[int] = [] 

1553 weights: List[torch.Tensor] = [] 

1554 for idx, block in matching: 

1555 w = _resolve_attr_path(block, attr_path) 

1556 if reshape_fn is not None: 

1557 w = reshape_fn(w) 

1558 weights.append(w) 

1559 indices.append(idx) 

1560 return indices, torch.stack(weights, dim=0) 

1561 

1562 def _stack_block_params( 

1563 self, attr_path: str, reshape_fn: Optional[Callable] = None 

1564 ) -> torch.Tensor: 

1565 """Stack a parameter across all blocks; falls back to matching-only on hybrids. 

1566 

1567 Filters on FULL-path resolution, not the first segment: on interleaved 

1568 MoE, every block has `mlp` but only dense layers expose `mlp.W_in`, so 

1569 a first-segment filter matched everything and the sparse layer's 

1570 AttributeError killed the accessor for the whole model. 

1571 """ 

1572 first_attr = attr_path.split(".")[0] 

1573 matching_blocks = [] 

1574 for i, block in enumerate(self.blocks): 

1575 if first_attr not in block._modules: 

1576 continue 

1577 try: 

1578 weight = _resolve_attr_path(block, attr_path) 

1579 except AttributeError: 

1580 continue 

1581 matching_blocks.append((i, weight)) 

1582 

1583 if len(matching_blocks) == 0: 

1584 raise AttributeError( 

1585 f"No blocks resolve '{attr_path}'. " 

1586 f"Use bridge.blocks_with('{first_attr}') to check availability." 

1587 ) 

1588 

1589 if len(matching_blocks) < len(self.blocks): 

1590 indices = [i for i, _ in matching_blocks] 

1591 logging.warning( 

1592 "Hybrid model: only %d/%d blocks resolve '%s'. Returning stacked tensor " 

1593 "for layers %s only. Tensor index i corresponds to original layer " 

1594 "indices[i], not layer i. For explicit index mapping, use " 

1595 "bridge.stack_params_for('%s', '%s').", 

1596 len(matching_blocks), 

1597 len(self.blocks), 

1598 attr_path, 

1599 indices, 

1600 first_attr, 

1601 attr_path, 

1602 ) 

1603 

1604 weights: List[torch.Tensor] = [] 

1605 for _, weight in matching_blocks: 

1606 if reshape_fn is not None: 

1607 weight = reshape_fn(weight) 

1608 weights.append(weight) 

1609 # Under a device_map split, per-block tensors live on different devices. 

1610 # torch.stack requires a common device; gather onto cfg.device (the embedding / 

1611 # input device — a natural "home" for cross-layer reductions). 

1612 if getattr(self.cfg, "n_devices", 1) > 1 and weights and self.cfg.device is not None: 

1613 target_device = torch.device(self.cfg.device) 

1614 weights = [w.to(target_device) for w in weights] 

1615 return torch.stack(weights, dim=0) 

1616 

1617 def _reshape_qkv(self, w: torch.Tensor) -> torch.Tensor: 

1618 """Reshape 2D [d_model, d_model] QKV weight to 3D [n_heads, d_model, d_head].""" 

1619 if w.shape == (self.cfg.d_model, self.cfg.d_model): 1619 ↛ 1620line 1619 didn't jump to line 1620 because the condition on line 1619 was never true

1620 d_head = self.cfg.d_model // self.cfg.n_heads 

1621 return w.reshape(self.cfg.n_heads, self.cfg.d_model, d_head) 

1622 return w 

1623 

1624 def _reshape_o(self, w: torch.Tensor) -> torch.Tensor: 

1625 """Reshape 2D [d_model, d_model] O weight to 3D [n_heads, d_head, d_model].""" 

1626 if w.shape == (self.cfg.d_model, self.cfg.d_model): 1626 ↛ 1627line 1626 didn't jump to line 1627 because the condition on line 1626 was never true

1627 d_head = self.cfg.d_model // self.cfg.n_heads 

1628 return w.reshape(self.cfg.n_heads, d_head, self.cfg.d_model) 

1629 return w 

1630 

1631 def _expand_kv_heads(self, w: torch.Tensor) -> torch.Tensor: 

1632 """Expand stacked grouped K/V weights along the head axis to n_heads. 

1633 

1634 GQA models store one K/V projection per key-value head while W_Q/W_O are 

1635 per-query-head, so weight circuits must repeat the grouped K/V up to 

1636 n_heads before factoring: query head h reads kv head 

1637 h // (n_heads // n_kv_heads), i.e. repeat_interleave — the same layout 

1638 GroupedQueryAttention.W_K/W_V expose on HookedTransformer. No-op for MHA, 

1639 where the head axes already match. 

1640 """ 

1641 if w.ndim != 4 or w.shape[1] == self.cfg.n_heads: 

1642 return w 

1643 n_kv_heads = w.shape[1] 

1644 if self.cfg.n_heads % n_kv_heads != 0: 

1645 raise ValueError( 

1646 f"Cannot expand {n_kv_heads} key-value heads to {self.cfg.n_heads} " 

1647 f"query heads: n_heads must be a multiple of n_kv_heads." 

1648 ) 

1649 return w.repeat_interleave(self.cfg.n_heads // n_kv_heads, dim=1) 

1650 

1651 @property 

1652 def W_K(self) -> torch.Tensor: 

1653 """Stack the key weights across all layers.""" 

1654 return self._stack_block_params("attn.W_K", self._reshape_qkv) 

1655 

1656 @property 

1657 def W_Q(self) -> torch.Tensor: 

1658 """Stack the query weights across all layers.""" 

1659 return self._stack_block_params("attn.W_Q", self._reshape_qkv) 

1660 

1661 @property 

1662 def W_V(self) -> torch.Tensor: 

1663 """Stack the value weights across all layers.""" 

1664 return self._stack_block_params("attn.W_V", self._reshape_qkv) 

1665 

1666 @property 

1667 def W_O(self) -> torch.Tensor: 

1668 """Stack the attn output weights across all layers.""" 

1669 return self._stack_block_params("attn.W_O", self._reshape_o) 

1670 

1671 @property 

1672 def W_in(self) -> torch.Tensor: 

1673 """Stack the MLP input weights across all layers.""" 

1674 return self._stack_block_params("mlp.W_in") 

1675 

1676 @property 

1677 def W_gate(self) -> Union[torch.Tensor, None]: 

1678 """Stack the MLP gate weights across all layers (gated MLPs only).""" 

1679 if getattr(self.cfg, "gated_mlp", False): 

1680 return self._stack_block_params("mlp.W_gate") 

1681 return None 

1682 

1683 @property 

1684 def W_out(self) -> torch.Tensor: 

1685 """Stack the MLP output weights across all layers.""" 

1686 return self._stack_block_params("mlp.W_out") 

1687 

1688 @property 

1689 def b_K(self) -> torch.Tensor: 

1690 """Stack the key biases across all layers.""" 

1691 return self._stack_block_params("attn.b_K") 

1692 

1693 @property 

1694 def b_Q(self) -> torch.Tensor: 

1695 """Stack the query biases across all layers.""" 

1696 return self._stack_block_params("attn.b_Q") 

1697 

1698 @property 

1699 def b_V(self) -> torch.Tensor: 

1700 """Stack the value biases across all layers.""" 

1701 return self._stack_block_params("attn.b_V") 

1702 

1703 @property 

1704 def b_O(self) -> torch.Tensor: 

1705 """Stack the attn output biases across all layers.""" 

1706 return self._stack_block_params("attn.b_O") 

1707 

1708 @property 

1709 def b_in(self) -> torch.Tensor: 

1710 """Stack the MLP input biases across all layers.""" 

1711 return self._stack_block_params("mlp.b_in") 

1712 

1713 @property 

1714 def b_out(self) -> torch.Tensor: 

1715 """Stack the MLP output biases across all layers.""" 

1716 return self._stack_block_params("mlp.b_out") 

1717 

1718 @property 

1719 def W_U(self) -> torch.Tensor: 

1720 """Unembedding matrix (d_model, d_vocab). Maps residual stream to logits.""" 

1721 return self.unembed.W_U 

1722 

1723 @property 

1724 def b_U(self) -> torch.Tensor: 

1725 """Unembedding bias (d_vocab).""" 

1726 return self.unembed.b_U 

1727 

1728 @property 

1729 def W_E(self) -> torch.Tensor: 

1730 """Token embedding matrix (d_vocab, d_model).""" 

1731 return self.embed.W_E 

1732 

1733 @property 

1734 def QK(self): 

1735 """QK circuit. On hybrids, returns attn layers only (with warning). See QK_for_attn_layers().""" 

1736 return FactoredMatrix(self.W_Q, self._expand_kv_heads(self.W_K).transpose(-2, -1)) 

1737 

1738 @property 

1739 def OV(self): 

1740 """OV circuit. On hybrids, returns attn layers only (with warning). See OV_for_attn_layers().""" 

1741 return FactoredMatrix(self._expand_kv_heads(self.W_V), self.W_O) 

1742 

1743 def QK_for_attn_layers(self) -> Tuple[List[int], FactoredMatrix]: 

1744 """QK circuit for attention layers only. Returns (layer_indices, FactoredMatrix).""" 

1745 q_indices, W_Q = self.stack_params_for("attn", "attn.W_Q", self._reshape_qkv) 

1746 _, W_K = self.stack_params_for("attn", "attn.W_K", self._reshape_qkv) 

1747 return q_indices, FactoredMatrix(W_Q, self._expand_kv_heads(W_K).transpose(-2, -1)) 

1748 

1749 def OV_for_attn_layers(self) -> Tuple[List[int], FactoredMatrix]: 

1750 """OV circuit for attention layers only. Returns (layer_indices, FactoredMatrix).""" 

1751 v_indices, W_V = self.stack_params_for("attn", "attn.W_V", self._reshape_qkv) 

1752 _, W_O = self.stack_params_for("attn", "attn.W_O", self._reshape_o) 

1753 return v_indices, FactoredMatrix(self._expand_kv_heads(W_V), W_O) 

1754 

1755 # ------------------------------------------------------------------ 

1756 # Mechanistic interpretability analysis methods 

1757 # ------------------------------------------------------------------ 

1758 

1759 def tokens_to_residual_directions( 

1760 self, 

1761 tokens: Union[str, int, torch.Tensor], 

1762 ) -> torch.Tensor: 

1763 """Map tokens to their unembedding vectors (residual stream directions). 

1764 

1765 Returns the columns of W_U corresponding to the given tokens — i.e. the 

1766 directions in the residual stream that the model dots with to produce the 

1767 logit for each token. 

1768 

1769 WARNING: If you use this without folding in LayerNorm (compatibility mode), 

1770 the results will be misleading because LN weights change the unembed map. 

1771 

1772 Args: 

1773 tokens: A single token (str, int, or scalar tensor), a 1-D tensor of 

1774 token IDs, or a 2-D batch of token IDs. 

1775 

1776 Returns: 

1777 Tensor of unembedding vectors with shape matching the input token shape 

1778 plus a trailing d_model dimension. 

1779 """ 

1780 if isinstance(tokens, torch.Tensor) and tokens.numel() > 1: 

1781 residual_directions = self.W_U[:, tokens] 

1782 residual_directions = einops.rearrange( 

1783 residual_directions, "d_model ... -> ... d_model" 

1784 ) 

1785 return residual_directions 

1786 else: 

1787 if isinstance(tokens, str): 

1788 token = self.to_single_token(tokens) 

1789 elif isinstance(tokens, int): 1789 ↛ 1791line 1789 didn't jump to line 1791 because the condition on line 1789 was always true

1790 token = tokens 

1791 elif isinstance(tokens, torch.Tensor) and tokens.numel() == 1: 

1792 token = int(tokens.item()) 

1793 else: 

1794 raise ValueError(f"Invalid token type: {type(tokens)}") 

1795 residual_direction = self.W_U[:, token] 

1796 return residual_direction 

1797 

1798 # Variant → attr paths for the output bias that feeds the residual stream. 

1799 _VARIANT_OUTPUT_BIAS_ATTRS: Dict[str, tuple] = { 

1800 "attn": ("b_O",), 

1801 "linear_attn": ("out_proj.bias",), 

1802 "mamba": ("out_proj.bias",), 

1803 "mixer": ("out_proj.bias",), 

1804 "ssm": ("out_proj.bias",), 

1805 } 

1806 

1807 def _get_block_variant_bias(self, block: "GeneralizedComponent") -> Optional[torch.Tensor]: 

1808 """Return the output bias from this block's variant submodule, or None.""" 

1809 for name in VARIANT_SUBMODULE_NAMES: 

1810 if name not in block._modules: 

1811 continue 

1812 variant = block._modules[name] 

1813 for attr_path in self._VARIANT_OUTPUT_BIAS_ATTRS.get(name, ()): 1813 ↛ 1809line 1813 didn't jump to line 1809 because the loop on line 1813 didn't complete

1814 obj = variant 

1815 try: 

1816 for attr in attr_path.split("."): 

1817 obj = getattr(obj, attr) 

1818 except AttributeError: 

1819 continue 

1820 if obj is not None and isinstance(obj, torch.Tensor): 1820 ↛ 1813line 1820 didn't jump to line 1813 because the condition on line 1820 was always true

1821 return obj 

1822 return None 

1823 

1824 def accumulated_bias( 

1825 self, 

1826 layer: int, 

1827 mlp_input: bool = False, 

1828 include_mlp_biases: bool = True, 

1829 ) -> torch.Tensor: 

1830 """Sum of variant + MLP output biases through the residual stream up to `layer`. 

1831 

1832 Includes all layer types (attn, SSM, linear-attn). Set mlp_input=True 

1833 to include the variant bias of the target layer itself. 

1834 """ 

1835 accumulated = torch.zeros(self.cfg.d_model, device=self.cfg.device) 

1836 for i in range(layer): 

1837 block = self.blocks[i] 

1838 b_O = self._get_block_variant_bias(block) 

1839 if b_O is not None: 

1840 accumulated = accumulated + b_O.to(accumulated.device) 

1841 if include_mlp_biases and "mlp" in block._modules: 

1842 b_out = getattr(block.mlp, "b_out", None) 

1843 if b_out is not None: 1843 ↛ 1836line 1843 didn't jump to line 1836 because the condition on line 1843 was always true

1844 accumulated = accumulated + b_out.to(accumulated.device) 

1845 if mlp_input: 

1846 assert layer < self.cfg.n_layers, "Cannot include attn_bias from beyond the final layer" 

1847 block = self.blocks[layer] 

1848 b_O = self._get_block_variant_bias(block) 

1849 if b_O is not None: 

1850 accumulated = accumulated + b_O.to(accumulated.device) 

1851 return accumulated 

1852 

1853 def all_composition_scores(self, mode: str) -> CompositionScores: 

1854 """Composition scores for all attention head pairs. Returns CompositionScores. 

1855 

1856 See https://transformer-circuits.pub/2021/framework/index.html 

1857 On hybrid models, only attention layers are included; layer_indices 

1858 maps tensor position i to original layer number. 

1859 """ 

1860 attn_blocks = self.blocks_with("attn") 

1861 if not attn_blocks: 1861 ↛ 1862line 1861 didn't jump to line 1862 because the condition on line 1861 was never true

1862 raise ValueError("No attention layers found — cannot compute composition scores.") 

1863 

1864 indices = [idx for idx, _ in attn_blocks] 

1865 blocks_list = [block for _, block in attn_blocks] 

1866 

1867 def _stack(attr_path: str, reshape_fn: Optional[Callable] = None) -> torch.Tensor: 

1868 weights: List[torch.Tensor] = [] 

1869 for block in blocks_list: 

1870 w = _resolve_attr_path(block, attr_path) 

1871 if reshape_fn is not None: 1871 ↛ 1873line 1871 didn't jump to line 1873 because the condition on line 1871 was always true

1872 w = reshape_fn(w) 

1873 weights.append(w) 

1874 # See _stack_block_params: gather per-block tensors onto cfg.device when split. 

1875 if getattr(self.cfg, "n_devices", 1) > 1 and weights and self.cfg.device is not None: 1875 ↛ 1876line 1875 didn't jump to line 1876 because the condition on line 1875 was never true

1876 target_device = torch.device(self.cfg.device) 

1877 weights = [w.to(target_device) for w in weights] 

1878 return torch.stack(weights, dim=0) 

1879 

1880 W_V = self._expand_kv_heads(_stack("attn.W_V", self._reshape_qkv)) 

1881 W_O = _stack("attn.W_O", self._reshape_o) 

1882 left = FactoredMatrix(W_V, W_O) 

1883 

1884 if mode == "Q": 

1885 W_Q = _stack("attn.W_Q", self._reshape_qkv) 

1886 W_K = self._expand_kv_heads(_stack("attn.W_K", self._reshape_qkv)) 

1887 right = FactoredMatrix(W_Q, W_K.transpose(-2, -1)) 

1888 elif mode == "K": 

1889 W_Q = _stack("attn.W_Q", self._reshape_qkv) 

1890 W_K = self._expand_kv_heads(_stack("attn.W_K", self._reshape_qkv)) 

1891 right = FactoredMatrix(W_Q, W_K.transpose(-2, -1)).T 

1892 elif mode == "V": 

1893 right = left 

1894 else: 

1895 raise ValueError(f"mode must be one of ['Q', 'K', 'V'] not {mode}") 

1896 

1897 scores = utils.composition_scores(left, right, broadcast_dims=True) 

1898 n_attn = len(indices) 

1899 idx_tensor = torch.arange(n_attn, device=self.cfg.device) 

1900 mask = idx_tensor[:, None, None, None] < idx_tensor[None, None, :, None] 

1901 scores = torch.where(mask, scores, torch.zeros_like(scores)) 

1902 

1903 labels = [f"L{l}H{h}" for l in indices for h in range(self.cfg.n_heads)] 

1904 return CompositionScores(scores=scores, layer_indices=indices, head_labels=labels) 

1905 

1906 def composition_layer_indices(self) -> List[int]: 

1907 """Original layer indices for attention layers (maps composition score positions).""" 

1908 return [idx for idx, _ in self.blocks_with("attn")] 

1909 

1910 def block_hooks(self, layer_idx: int) -> List[str]: 

1911 """Sorted hook names available on block `layer_idx` (block-relative paths).""" 

1912 prefix = f"blocks.{layer_idx}." 

1913 return sorted(name[len(prefix) :] for name in self.hook_dict if name.startswith(prefix)) 

1914 

1915 def block_submodules(self, layer_idx: int) -> List[str]: 

1916 """Return bridged submodule names on block `layer_idx`.""" 

1917 block = self.blocks[layer_idx] 

1918 return [name for name in block._modules if name not in _BLOCK_INTERNAL_MODULES] 

1919 

1920 def layer_types(self) -> List[str]: 

1921 """Per-block type labels, e.g. ["attn+mlp", "ssm+mlp", ...]. Deterministic order.""" 

1922 types = [] 

1923 for block in self.blocks: 

1924 variants = [n for n in VARIANT_SUBMODULE_NAMES if n in block._modules] 

1925 universals = sorted( 

1926 n 

1927 for n in block._modules 

1928 if n not in _VARIANT_SUBMODULE_SET 

1929 and n not in _BLOCK_INTERNAL_MODULES 

1930 and not n.startswith(_NORM_PREFIXES) 

1931 ) 

1932 parts = variants + universals 

1933 types.append("+".join(parts) if parts else "unknown") 

1934 return types 

1935 

1936 @property 

1937 def all_head_labels(self) -> list[str]: 

1938 """Human-readable labels for all attention heads, e.g. ['L0H0', 'L0H1', ...].""" 

1939 return [f"L{l}H{h}" for l in range(self.cfg.n_layers) for h in range(self.cfg.n_heads)] 

1940 

1941 @property 

1942 def attn_head_labels(self) -> list[str]: 

1943 """Head labels for attention layers only — matches all_composition_scores() dims.""" 

1944 return [ 

1945 f"L{l}H{h}" for l in self.composition_layer_indices() for h in range(self.cfg.n_heads) 

1946 ] 

1947 

1948 def parameters(self, recurse: bool = True) -> Iterator[nn.Parameter]: 

1949 """Returns parameters following standard PyTorch semantics. 

1950 

1951 This method delegates to the underlying HuggingFace model's parameters(). 

1952 For TransformerLens-style parameter generator, use tl_parameters() instead. 

1953 

1954 Args: 

1955 recurse: If True, yields parameters of this module and all submodules 

1956 

1957 Returns: 

1958 Iterator of nn.Parameter objects 

1959 """ 

1960 return self.original_model.parameters(recurse=recurse) 

1961 

1962 def named_parameters( 

1963 self, prefix: str = "", recurse: bool = True, remove_duplicate: bool = True 

1964 ) -> Iterator[tuple[str, nn.Parameter]]: 

1965 """Returns named parameters following standard PyTorch semantics. 

1966 

1967 This method delegates to the underlying HuggingFace model's named_parameters(). 

1968 For TransformerLens-style generator, use tl_named_parameters() instead. 

1969 

1970 Args: 

1971 prefix: Prefix to prepend to all parameter names 

1972 recurse: If True, yields parameters of this module and all submodules 

1973 remove_duplicate: If True, removes duplicate parameters 

1974 

1975 Returns: 

1976 Iterator of (name, parameter) tuples 

1977 """ 

1978 return self.original_model.named_parameters(prefix, recurse, remove_duplicate) 

1979 

1980 def tl_parameters(self) -> dict[str, torch.Tensor]: 

1981 """Returns TransformerLens-style parameter dictionary. 

1982 

1983 Parameter names follow TransformerLens conventions (e.g., 'blocks.0.attn.W_Q') and may 

1984 include processed weights (non-leaf tensors). This format is expected by SVDInterpreter 

1985 among other analysis tools. 

1986 

1987 Returns: 

1988 Dictionary mapping TransformerLens parameter names to tensors 

1989 

1990 Example: 

1991 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

1992 >>> tl_params = bridge.tl_parameters() 

1993 >>> W_Q = tl_params["blocks.0.attn.W_Q"] # Shape: [n_heads, d_model, d_head] 

1994 """ 

1995 return self.get_params() 

1996 

1997 def tl_named_parameters(self) -> Iterator[tuple[str, torch.Tensor]]: 

1998 """Returns iterator of TransformerLens-style named parameters. 

1999 

2000 This provides the same parameters as tl_parameters() but as an iterator 

2001 for consistency with PyTorch's named_parameters() API pattern. 

2002 

2003 Returns: 

2004 Iterator of (name, tensor) tuples with TransformerLens naming conventions 

2005 

2006 Example: 

2007 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

2008 >>> for name, param in bridge.tl_named_parameters(): 

2009 ... if "attn.W_Q" in name: 

2010 ... print(f"{name}: {param.shape}") # doctest: +ELLIPSIS 

2011 blocks.0.attn.W_Q: torch.Size([12, 768, 64]) 

2012 ... 

2013 """ 

2014 return iter(self.get_params().items()) 

2015 

2016 def _accepts_derived_position_ids(self) -> bool: 

2017 """Whether it is safe to hand the wrapped model a mask-derived ``position_ids``. 

2018 

2019 Two families of model must be left alone, so the injection below is 

2020 gated on the target the same way ``output_attentions`` is in 

2021 :meth:`run_with_cache`: 

2022 

2023 * **Fixed-signature models.** Remote-code forwards such as 

2024 ``LLaDAModelLM.forward`` take neither ``position_ids`` nor 

2025 ``**kwargs``, so passing it raises ``TypeError`` where the model 

2026 previously returned logits. 

2027 * **Models that own their position derivation.** mRoPE architectures 

2028 (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, GLM-4V) build a 3-D temporal / 

2029 height / width index in ``get_rope_index``, and only while 

2030 ``position_ids is None``; a supplied 2-D tensor is silently expanded 

2031 across all three streams instead. Their derivation already scatters 

2032 positions onto attended slots only, so it handles left padding 

2033 correctly on its own and needs no help from us. 

2034 * **Mask-consuming positional embeddings.** OPT's 

2035 ``OPTLearnedPositionalEmbedding.forward`` takes the mask and derives 

2036 the same positions we would, so injection buys nothing — but it does 

2037 replace the model's own padding-slot convention with ours, which 

2038 shows up as a whole-tensor diff. 

2039 """ 

2040 underlying = getattr(self, "original_model", None) 

2041 if underlying is None: 

2042 return False 

2043 

2044 cached = self.__dict__.get("_derived_position_ids_ok") 

2045 if cached is not None and cached[0] is underlying: 

2046 return bool(cached[1]) 

2047 

2048 def verdict() -> bool: 

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

2050 if "position_ids" not in fwd_params and not any( 

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

2052 ): 

2053 return False 

2054 

2055 # ``get_rope_index`` lives on the inner text model, not the 

2056 # ForConditionalGeneration wrapper that is usually original_model. 

2057 for module in ( 

2058 underlying, 

2059 getattr(underlying, "model", None), 

2060 getattr(underlying, "language_model", None), 

2061 ): 

2062 if module is not None and hasattr(module, "get_rope_index"): 

2063 return False 

2064 

2065 # Config-level backstop for mRoPE models that spell the derivation 

2066 # differently; the section list is what makes positions 3-D. 

2067 config = getattr(underlying, "config", None) 

2068 for candidate in (config, getattr(config, "text_config", None)): 

2069 scaling = getattr(candidate, "rope_scaling", None) 

2070 if isinstance(scaling, dict) and "mrope_section" in scaling: 

2071 return False 

2072 

2073 # A positional embedding that takes the mask derives positions for 

2074 # itself. Only embeddings that override nn.Embedding.forward are 

2075 # worth inspecting, which keeps this to a handful per model. 

2076 for module in underlying.modules(): 

2077 if not isinstance(module, nn.Embedding): 

2078 continue 

2079 if type(module).forward is nn.Embedding.forward: 

2080 continue 

2081 if "attention_mask" in inspect.signature(module.forward).parameters: 

2082 return False 

2083 return True 

2084 

2085 accepts = verdict() 

2086 self.__dict__["_derived_position_ids_ok"] = (underlying, accepts) 

2087 return accepts 

2088 

2089 def forward( 

2090 self, 

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

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

2093 loss_per_token: bool = False, 

2094 prepend_bos: Optional[bool] = None, 

2095 padding_side: Optional[str] = None, 

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

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

2098 start_at_layer: Optional[int] = None, 

2099 stop_at_layer: Optional[int] = None, 

2100 pixel_values: Optional[torch.Tensor] = None, 

2101 input_values: Optional[torch.Tensor] = None, 

2102 **kwargs, 

2103 ) -> Any: 

2104 """Forward pass through the model. 

2105 

2106 Args: 

2107 input: Input to the model 

2108 return_type: Type of output to return ('logits', 'loss', 'both', 'predictions', None) 

2109 loss_per_token: Whether to return loss per token 

2110 prepend_bos: Whether to prepend BOS token 

2111 padding_side: Which side to pad on 

2112 labels: Explicit language-model targets. Encoder-decoder models require 

2113 labels for loss; decoder-only models fall back to input IDs when omitted. 

2114 start_at_layer: Not implemented in TransformerBridge. The bridge delegates 

2115 to HuggingFace's model.forward() which owns the layer iteration loop, 

2116 making start_at_layer infeasible without monkey-patching HF internals 

2117 (fragile across HF versions) or exception-based layer skipping (corrupts 

2118 model state). Raises NotImplementedError if a non-None value is passed. 

2119 stop_at_layer: Layer to stop forward pass at 

2120 pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) 

2121 and vision models (eg. ViT, DeiT). 

2122 The tensor is passed directly to the underlying HuggingFace model. 

2123 Only valid when cfg.is_multimodal is True or cfg.is_visual_model is True. 

2124 input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). 

2125 The tensor is passed directly to the underlying HuggingFace model. 

2126 Only valid when cfg.is_audio_model is True. 

2127 **kwargs: Additional arguments passed to model 

2128 

2129 Returns: 

2130 Model output based on return_type 

2131 """ 

2132 

2133 model_config = getattr(self.original_model, "config", None) 

2134 is_encoder_decoder = bool(getattr(model_config, "is_encoder_decoder", False)) 

2135 if return_type in ("loss", "both") and is_encoder_decoder and labels is None: 

2136 raise ValueError( 

2137 "labels are required for seq2seq return_type='loss' or 'both'; " 

2138 "encoder input_ids are not decoder targets" 

2139 ) 

2140 if ( 

2141 return_type in ("loss", "both") 

2142 and not is_encoder_decoder 

2143 and not self.adapter.supports_causal_loss 

2144 ): 

2145 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2146 raise NotImplementedError( 

2147 f"{architecture} does not support TransformerBridge's shifted causal " 

2148 "loss. Request return_type='logits' and compute the architecture-specific " 

2149 "masked-token objective explicitly." 

2150 ) 

2151 

2152 if labels is not None: 

2153 kwargs["labels"] = labels 

2154 

2155 if start_at_layer is not None: 2155 ↛ 2156line 2155 didn't jump to line 2156 because the condition on line 2155 was never true

2156 raise NotImplementedError( 

2157 "start_at_layer is not supported in TransformerBridge. " 

2158 "The bridge delegates to HuggingFace's model.forward() which controls " 

2159 "the layer iteration loop. See the TransformerBridge review plan for a " 

2160 "detailed analysis of implementation approaches and their tradeoffs." 

2161 ) 

2162 

2163 # Set stop_at_layer flag on all blocks if requested 

2164 if stop_at_layer is not None: 

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

2166 hasattr(self, "L_blocks") 

2167 or hasattr(self, "H_blocks") 

2168 or hasattr(self, "encoder_blocks") 

2169 or hasattr(self, "decoder_blocks") 

2170 ): 

2171 raise NotImplementedError( 

2172 "stop_at_layer is not supported on non-standard block list " 

2173 "names (L_blocks, H_blocks, encoder_blocks, decoder_blocks). " 

2174 "The bridge only supports stop_at_layer on 'blocks'." 

2175 ) 

2176 if hasattr(self, "blocks"): 2176 ↛ 2184line 2176 didn't jump to line 2184 because the condition on line 2176 was always true

2177 effective_stop_at_layer = ( 

2178 len(self.blocks) + stop_at_layer if stop_at_layer < 0 else stop_at_layer 

2179 ) 

2180 for block in self.blocks: 

2181 block._stop_at_layer_idx = effective_stop_at_layer 

2182 

2183 # Map HookedEncoderDecoder-style kwargs to HF-compatible names 

2184 if "decoder_input" in kwargs: 

2185 kwargs["decoder_input_ids"] = kwargs.pop("decoder_input") 

2186 if "one_zero_attention_mask" in kwargs: 2186 ↛ 2187line 2186 didn't jump to line 2187 because the condition on line 2186 was never true

2187 if attention_mask is None: 

2188 attention_mask = kwargs.pop("one_zero_attention_mask") 

2189 else: 

2190 kwargs.pop("one_zero_attention_mask") 

2191 

2192 # Detect batched list input that may need padding. Forward follows the 

2193 # requested/tokenizer side; generation separately forces left-padding. 

2194 _is_batched_list = ( 

2195 isinstance(input, list) 

2196 and len(input) > 1 

2197 and not getattr(self.cfg, "is_audio_model", False) 

2198 and not getattr(self.cfg, "is_visual_model", False) 

2199 ) 

2200 _resolved_padding_side = padding_side 

2201 if _resolved_padding_side is None and self.tokenizer is not None: 

2202 _resolved_padding_side = getattr(self.tokenizer, "padding_side", "right") 

2203 

2204 try: 

2205 if isinstance(input, (str, list)): 

2206 if getattr(self.cfg, "is_audio_model", False): 2206 ↛ 2207line 2206 didn't jump to line 2207 because the condition on line 2206 was never true

2207 raise ValueError( 

2208 "Audio models require tensor input (raw waveform), not text. " 

2209 "Pass a torch.Tensor or use the input_values parameter." 

2210 ) 

2211 if getattr(self.cfg, "is_visual_model", False): 2211 ↛ 2212line 2211 didn't jump to line 2212 because the condition on line 2211 was never true

2212 raise ValueError( 

2213 "Visual models require tensor input (pixel values), not text. " 

2214 "Pass a torch.Tensor or use the pixel_values parameter." 

2215 ) 

2216 input_ids = self.to_tokens( 

2217 input, prepend_bos=prepend_bos, padding_side=padding_side 

2218 ) 

2219 else: 

2220 input_ids = input 

2221 # Promote 1D integer token tensors to 2D [batch=1, seq] to match 

2222 # HookedTransformer's contract. Float tensors (inputs_embeds, 

2223 # audio waveforms) are passed through unchanged. 

2224 if ( 

2225 isinstance(input_ids, torch.Tensor) 

2226 and input_ids.ndim == 1 

2227 and not input_ids.is_floating_point() 

2228 ): 

2229 input_ids = input_ids.unsqueeze(0) 

2230 

2231 # Detect inputs_embeds: if the tensor is floating point, it's pre-computed 

2232 # embeddings (e.g., from multimodal models) rather than token IDs. 

2233 _is_inputs_embeds = ( 

2234 isinstance(input_ids, torch.Tensor) and input_ids.is_floating_point() 

2235 ) 

2236 

2237 # Left padding needs a mask and corrected positions. Right padding is 

2238 # harmless for causal real-token positions and remains unmasked to 

2239 # match HookedTransformer; bidirectional/encoder inputs still need it. 

2240 if ( 

2241 _is_batched_list 

2242 and attention_mask is None 

2243 and self.tokenizer is not None 

2244 and self.tokenizer.pad_token_id is not None 

2245 and not _is_inputs_embeds 

2246 and ( 

2247 _resolved_padding_side == "left" 

2248 or is_encoder_decoder 

2249 or not self.adapter.supports_causal_loss 

2250 ) 

2251 ): 

2252 attention_mask = utils.get_attention_mask( 

2253 self.tokenizer, 

2254 input_ids, 

2255 prepend_bos=getattr(self.cfg, "default_prepend_bos", True), 

2256 padding_side=_resolved_padding_side, 

2257 ).to(self.cfg.device) 

2258 # Gated on the target for the same reason the derivation below is: 

2259 # a fixed-signature forward raises TypeError on the kwarg, and a 

2260 # model that owns its own position derivation is overridden by it 

2261 # (#1626). 

2262 if "position_ids" not in kwargs and self._accepts_derived_position_ids(): 

2263 position_ids = attention_mask.long().cumsum(-1) - 1 

2264 position_ids.masked_fill_(attention_mask == 0, 1) 

2265 kwargs["position_ids"] = position_ids 

2266 

2267 # Any masked-out token shifts the absolute position of every real token 

2268 # after it, so positions must be derived from the mask rather than left 

2269 # to HF's default arange. This is the same derivation HookedTransformer 

2270 # applies in pos_embed; without it the bridge silently returns wrong 

2271 # logits. An all-ones mask reduces to arange, so this is a no-op there. 

2272 # 

2273 # The mask spans any cached prefix as well as the new tokens, so it is 

2274 # offset back to just the tokens actually being passed — matching how 

2275 # AbstractAttention/PosEmbed use past_kv_pos_offset. 

2276 if ( 

2277 attention_mask is not None 

2278 and "position_ids" not in kwargs 

2279 and not _is_inputs_embeds 

2280 and attention_mask.ndim == 2 

2281 and isinstance(input_ids, torch.Tensor) 

2282 and input_ids.ndim == 2 

2283 and attention_mask.shape[1] >= input_ids.shape[1] 

2284 and self._accepts_derived_position_ids() 

2285 ): 

2286 # .long() because callers may hand in a float 0/1 mask, and 

2287 # positions index an embedding table. 

2288 _derived = utils.get_offset_position_ids(0, attention_mask.long()) 

2289 _arange = torch.arange(attention_mask.shape[1], device=_derived.device) 

2290 # Decide per row, not per batch. A row only needs the derived 

2291 # positions when its mask actually moves one of its attended 

2292 # tokens off the default position — i.e. a masked token precedes 

2293 # a real one (left padding, or an interior gap). Rows that are 

2294 # unpadded or purely right-padded keep arange verbatim, so one 

2295 # left-padded row in a batch cannot perturb its neighbours. 

2296 _needs = ((_derived != _arange) & (attention_mask != 0)).any(dim=1, keepdim=True) 

2297 if bool(_needs.any()): 

2298 _positions = torch.where(_needs, _derived, _arange.expand_as(_derived)) 

2299 kwargs["position_ids"] = _positions[ 

2300 :, attention_mask.shape[1] - input_ids.shape[1] : 

2301 ] 

2302 

2303 if attention_mask is not None: 

2304 kwargs["attention_mask"] = attention_mask 

2305 if kwargs.pop("use_past_kv_cache", False) or kwargs.get("use_cache", False): 

2306 kwargs["use_cache"] = True 

2307 # Auto-generate decoder_input_ids for encoder-decoder models 

2308 if "decoder_input_ids" not in kwargs and labels is None and is_encoder_decoder: 

2309 decoder_start_token_id = getattr( 

2310 self.original_model.config, "decoder_start_token_id", None 

2311 ) 

2312 if decoder_start_token_id is not None: 

2313 shifted = input_ids[:, :-1] 

2314 start_tokens = torch.full( 

2315 (input_ids.shape[0], 1), 

2316 decoder_start_token_id, 

2317 dtype=input_ids.dtype, 

2318 device=input_ids.device, 

2319 ) 

2320 kwargs["decoder_input_ids"] = torch.cat([start_tokens, shifted], dim=1) 

2321 else: 

2322 kwargs["decoder_input_ids"] = input_ids 

2323 

2324 # Tell PosEmbedBridge to expand batch=1 position_ids to full batch. 

2325 if hasattr(self, "pos_embed"): 

2326 self.pos_embed._current_batch_size = input_ids.shape[0] 

2327 

2328 # Handle pixel_values for multimodal models 

2329 if pixel_values is not None: 

2330 if not ( 

2331 getattr(self.cfg, "is_multimodal", False) 

2332 or getattr(self.cfg, "is_visual_model", False) 

2333 ): 

2334 raise ValueError( 

2335 "pixel_values can only be passed to multimodal or vision models " 

2336 "(cfg.is_multimodal or cfg.is_visual_model must be True)" 

2337 ) 

2338 kwargs["pixel_values"] = pixel_values 

2339 

2340 # Handle input_values for audio models 

2341 if input_values is not None: 2341 ↛ 2342line 2341 didn't jump to line 2342 because the condition on line 2341 was never true

2342 if not getattr(self.cfg, "is_audio_model", False): 

2343 raise ValueError( 

2344 "input_values can only be passed to audio models " 

2345 "(cfg.is_audio_model must be True)" 

2346 ) 

2347 kwargs["input_values"] = input_values 

2348 

2349 # Audio models use input_values (waveform), not input_ids 

2350 if getattr(self.cfg, "is_audio_model", False): 

2351 if input_values is not None: 2351 ↛ 2352line 2351 didn't jump to line 2352 because the condition on line 2351 was never true

2352 output = self.original_model(**kwargs) 

2353 elif isinstance(input, torch.Tensor): 2353 ↛ 2357line 2353 didn't jump to line 2357 because the condition on line 2353 was always true

2354 kwargs["input_values"] = input 

2355 output = self.original_model(**kwargs) 

2356 else: 

2357 raise ValueError( 

2358 "Audio models require tensor input (raw waveform). " 

2359 "Pass a torch.Tensor or use input_values parameter." 

2360 ) 

2361 elif getattr(self.cfg, "is_visual_model", False): 

2362 # "pixel_values" may already be in kwargs from the "if pixel_values is not None:" 

2363 # gate above (explicit pixel_values=... call); otherwise treat `input` itself as 

2364 # the image tensor, matching how the audio branch treats `input` as the waveform. 

2365 if "pixel_values" not in kwargs: 2365 ↛ 2373line 2365 didn't jump to line 2373 because the condition on line 2365 was always true

2366 if isinstance(input, torch.Tensor): 2366 ↛ 2369line 2366 didn't jump to line 2369 because the condition on line 2366 was always true

2367 kwargs["pixel_values"] = input 

2368 else: 

2369 raise ValueError( 

2370 "Visual models require tensor input (pixel values). " 

2371 "Pass a torch.Tensor as `input` or use the pixel_values parameter." 

2372 ) 

2373 output = self.original_model(**kwargs) 

2374 elif _is_inputs_embeds: 

2375 output = self.original_model(inputs_embeds=input_ids, **kwargs) 

2376 else: 

2377 output = self.original_model(input_ids, **kwargs) 

2378 # Stash only the cache object (not the full output) for generate(). 

2379 if getattr(self, "_capture_hf_cache", False): 

2380 self._last_hf_cache = getattr(output, "past_key_values", None) 

2381 if hasattr(output, "logits"): 

2382 logits = output.logits 

2383 elif isinstance(output, tuple) and len(output) > 0: 

2384 # With labels forwarded, HF tuple outputs are (loss, logits, ...). 

2385 if labels is not None and len(output) > 1: 2385 ↛ 2388line 2385 didn't jump to line 2388 because the condition on line 2385 was always true

2386 logits = output[1] 

2387 else: 

2388 logits = output[0] 

2389 elif hasattr(output, "last_hidden_state"): 

2390 # Bare encoder models (ViTModel, DeiTModel, BertModel, etc. without 

2391 # a task head) return e.g. BaseModelOutput/BaseModelOutputWithPooling, 

2392 # which has neither `.logits` nor tuple semantics. Fall back to 

2393 # `last_hidden_state` so return_type="logits" still yields a plain 

2394 # tensor rather than silently handing back the raw HF output object. 

2395 logits = output.last_hidden_state 

2396 else: 

2397 logits = output 

2398 if return_type == "logits": 

2399 return logits 

2400 elif return_type == "logits_and_cache": 

2401 past_key_values = getattr(output, "past_key_values", None) 

2402 return (logits, past_key_values) 

2403 elif is_encoder_decoder and return_type in ("loss", "both"): 

2404 assert isinstance( 

2405 logits, torch.Tensor 

2406 ), f"Expected seq2seq logits tensor, got {type(logits)}" 

2407 assert isinstance(labels, torch.Tensor) 

2408 return self._finalize_seq2seq_return( 

2409 return_type, 

2410 logits, 

2411 labels, 

2412 output, 

2413 loss_per_token=loss_per_token, 

2414 ) 

2415 elif return_type == "loss": 

2416 if getattr(self.cfg, "is_audio_model", False): 2416 ↛ 2417line 2416 didn't jump to line 2417 because the condition on line 2416 was never true

2417 raise ValueError( 

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

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

2420 ) 

2421 if getattr(self.cfg, "is_visual_model", False): 2421 ↛ 2422line 2421 didn't jump to line 2422 because the condition on line 2421 was never true

2422 raise ValueError( 

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

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

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

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

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

2428 ) 

2429 if _is_inputs_embeds and labels is None: 2429 ↛ 2430line 2429 didn't jump to line 2430 because the condition on line 2429 was never true

2430 raise ValueError( 

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

2432 ) 

2433 # Always use self.loss_fn for consistency with HT's formula 

2434 # (log_softmax + gather). HF's output.loss uses F.cross_entropy 

2435 # which gives different results in bfloat16. 

2436 assert isinstance( 

2437 logits, torch.Tensor 

2438 ), f"Expected logits tensor, got {type(logits)}" 

2439 if labels is not None: 

2440 return self._causal_labels_loss( 

2441 logits, 

2442 labels, 

2443 attention_mask=attention_mask, 

2444 per_token=loss_per_token, 

2445 ) 

2446 return self.loss_fn( 

2447 logits, 

2448 input_ids, 

2449 attention_mask=attention_mask, 

2450 per_token=loss_per_token, 

2451 ) 

2452 elif return_type == "both": 

2453 if getattr(self.cfg, "is_audio_model", False): 2453 ↛ 2454line 2453 didn't jump to line 2454 because the condition on line 2453 was never true

2454 raise ValueError( 

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

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

2457 ) 

2458 if _is_inputs_embeds and labels is None: 2458 ↛ 2459line 2458 didn't jump to line 2459 because the condition on line 2458 was never true

2459 raise ValueError( 

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

2461 ) 

2462 assert isinstance( 

2463 logits, torch.Tensor 

2464 ), f"Expected logits tensor, got {type(logits)}" 

2465 if labels is not None: 

2466 loss = self._causal_labels_loss( 

2467 logits, 

2468 labels, 

2469 attention_mask=attention_mask, 

2470 per_token=loss_per_token, 

2471 ) 

2472 else: 

2473 loss = self.loss_fn( 

2474 logits, 

2475 input_ids, 

2476 attention_mask=attention_mask, 

2477 per_token=loss_per_token, 

2478 ) 

2479 return (logits, loss) 

2480 elif return_type == "predictions": 2480 ↛ 2481line 2480 didn't jump to line 2481 because the condition on line 2480 was never true

2481 assert ( 

2482 self.tokenizer is not None 

2483 ), "Must have a tokenizer to use return_type='predictions'" 

2484 if logits.shape[-1] == 2: 

2485 # Next Sentence Prediction — 2-class output 

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

2487 predictions = [ 

2488 "The sentences are sequential", 

2489 "The sentences are NOT sequential", 

2490 ] 

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

2492 else: 

2493 # Masked Language Modeling — decode [MASK] tokens 

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

2495 predictions = self.tokenizer.decode(logprobs.argmax(dim=-1)) 

2496 if " " in predictions: 

2497 predictions = predictions.split(" ") 

2498 predictions = [f"Prediction {i}: {p}" for i, p in enumerate(predictions)] 

2499 return predictions 

2500 elif return_type is None: 2500 ↛ 2503line 2500 didn't jump to line 2503 because the condition on line 2500 was always true

2501 return None 

2502 else: 

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

2504 except StopAtLayerException as e: 

2505 # Execution stopped at the requested layer 

2506 return e.layer_output 

2507 finally: 

2508 # Clean up state that may be inconsistent after StopAtLayerException 

2509 if stop_at_layer is not None: 

2510 for bl_name in ( 

2511 "blocks", 

2512 "encoder_blocks", 

2513 "decoder_blocks", 

2514 "L_blocks", 

2515 "H_blocks", 

2516 ): 

2517 if hasattr(self, bl_name): 

2518 for block in getattr(self, bl_name): 

2519 block._stop_at_layer_idx = None 

2520 

2521 # Clear any stale KV cache — layers after the stop point didn't 

2522 # execute, so the cache is incomplete and would corrupt subsequent 

2523 # generate() calls that expect a full cache. 

2524 if hasattr(self, "_last_hf_cache"): 

2525 del self._last_hf_cache 

2526 

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

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

2529 if hook_name in self._hook_registry: 

2530 return self._hook_registry[hook_name] 

2531 try: 

2532 parts = hook_name.split(".") 

2533 current = self 

2534 for part in parts: 

2535 current = getattr(current, part) 

2536 if isinstance(current, HookPoint): 

2537 return current 

2538 except AttributeError: 

2539 pass 

2540 return None 

2541 

2542 def loss_fn( 

2543 self, 

2544 logits: torch.Tensor, 

2545 tokens: torch.Tensor, 

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

2547 per_token: bool = False, 

2548 ) -> torch.Tensor: 

2549 """Calculate cross-entropy loss. 

2550 

2551 Uses the same formula as HookedTransformer (log_softmax + gather) to ensure 

2552 numerically identical results when logits match. 

2553 

2554 Args: 

2555 logits: Model logits 

2556 tokens: Target tokens 

2557 attention_mask: Optional attention mask for padding 

2558 per_token: Whether to return per-token loss 

2559 

2560 Returns: 

2561 Loss tensor 

2562 """ 

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

2564 tokens = tokens.to(logits.device) 

2565 if attention_mask is not None: 

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

2567 attention_mask = attention_mask.to(logits.device) 

2568 attention_mask = self._prepare_loss_attention_mask(attention_mask, tokens) 

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

2570 

2571 def _causal_labels_loss( 

2572 self, 

2573 logits: torch.Tensor, 

2574 labels: torch.Tensor, 

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

2576 per_token: bool = False, 

2577 ) -> torch.Tensor: 

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

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

2580 labels = labels.to(logits.device) 

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

2582 raise ValueError( 

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

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

2585 ) 

2586 

2587 losses = F.cross_entropy( 

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

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

2590 reduction="none", 

2591 ignore_index=-100, 

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

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

2594 if attention_mask is not None: 

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

2596 attention_mask = attention_mask.to(logits.device) 

2597 token_mask = self._prepare_loss_attention_mask(attention_mask, labels) 

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

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

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

2601 

2602 @staticmethod 

2603 def _seq2seq_loss( 

2604 logits: torch.Tensor, 

2605 labels: torch.Tensor, 

2606 native_loss: Any, 

2607 *, 

2608 per_token: bool, 

2609 ) -> torch.Tensor: 

2610 """Return encoder-decoder loss without the causal LM token shift.""" 

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

2612 labels = labels.to(logits.device) 

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

2614 raise ValueError( 

2615 "seq2seq labels must match the decoder logits batch and position " 

2616 f"dimensions, got labels {tuple(labels.shape)} and logits " 

2617 f"{tuple(logits.shape)}" 

2618 ) 

2619 if not per_token and isinstance(native_loss, torch.Tensor): 

2620 return native_loss 

2621 

2622 losses = F.cross_entropy( 

2623 logits.flatten(0, 1), 

2624 labels.flatten(), 

2625 reduction="none" if per_token else "mean", 

2626 ignore_index=-100, 

2627 ) 

2628 return losses.view_as(labels) if per_token else losses 

2629 

2630 def _finalize_seq2seq_return( 

2631 self, 

2632 return_type: str, 

2633 logits: torch.Tensor, 

2634 labels: torch.Tensor, 

2635 native_output: Any, 

2636 *, 

2637 loss_per_token: bool, 

2638 ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: 

2639 loss = self._seq2seq_loss( 

2640 logits, 

2641 labels, 

2642 getattr(native_output, "loss", None), 

2643 per_token=loss_per_token, 

2644 ) 

2645 return (logits, loss) if return_type == "both" else loss 

2646 

2647 @staticmethod 

2648 def _prepare_loss_attention_mask( 

2649 attention_mask: torch.Tensor, tokens: torch.Tensor 

2650 ) -> torch.Tensor: 

2651 """Reduce a forward attention mask to the token window scored by the loss.""" 

2652 batch, pos = tokens.shape 

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

2654 raise ValueError( 

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

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

2657 ) 

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

2659 raise ValueError( 

2660 "attention_mask batch dimension must match tokens, " 

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

2662 ) 

2663 

2664 if attention_mask.ndim == 2: 

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

2666 raise ValueError( 

2667 "attention_mask must cover every scored token, " 

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

2669 ) 

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

2671 

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

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

2674 raise ValueError( 

2675 "attention_mask must cover every scored token, " 

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

2677 ) 

2678 

2679 blocked = attention_mask if attention_mask.dtype is torch.bool else attention_mask < -1.0 

2680 if query_pos == 1: 

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

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

2683 else: 

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

2685 raise ValueError( 

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

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

2688 ) 

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

2690 diagonal = torch.diagonal( 

2691 blocked, 

2692 offset=key_pos - query_pos, 

2693 dim1=-2, 

2694 dim2=-1, 

2695 ) 

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

2697 raise ValueError( 

2698 "attention_mask diagonal must cover every scored token, " 

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

2700 ) 

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

2702 

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

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

2705 

2706 @overload 

2707 def run_with_cache( 

2708 self, 

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

2710 return_cache_object: Literal[True] = True, 

2711 remove_batch_dim: bool = False, 

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

2713 stop_at_layer: Optional[int] = None, 

2714 incl_bwd: bool = False, 

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

2716 reset_hooks_end: bool = True, 

2717 clear_contexts: bool = False, 

2718 **kwargs, 

2719 ) -> Tuple[Any, ActivationCache]: 

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

2721 pass 

2722 

2723 @overload 

2724 def run_with_cache( 

2725 self, 

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

2727 return_cache_object: Literal[False], 

2728 remove_batch_dim: bool = False, 

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

2730 stop_at_layer: Optional[int] = None, 

2731 incl_bwd: bool = False, 

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

2733 reset_hooks_end: bool = True, 

2734 clear_contexts: bool = False, 

2735 **kwargs, 

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

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

2738 pass 

2739 

2740 def run_with_cache( 

2741 self, 

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

2743 return_cache_object: bool = True, 

2744 remove_batch_dim: bool = False, 

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

2746 stop_at_layer: Optional[int] = None, 

2747 incl_bwd: bool = False, 

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

2749 reset_hooks_end: bool = True, 

2750 clear_contexts: bool = False, 

2751 **kwargs, 

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

2753 """Run the model and cache all activations. 

2754 

2755 Args: 

2756 input: Input to the model 

2757 return_cache_object: Whether to return ActivationCache object 

2758 remove_batch_dim: Whether to remove batch dimension 

2759 names_filter: Filter for which activations to cache (str, list of str, or callable) 

2760 stop_at_layer: Layer to stop forward pass at (uses StopAtLayerException; cleans up KV cache on stop) 

2761 incl_bwd: If True, also caches gradients under a ``_grad`` suffix by calling 

2762 ``backward()`` on the output, matching HookedRootModule.run_with_cache. 

2763 Requires a scalar output, so pass ``return_type="loss"``. 

2764 pos_slice: Slice applied to the position axis of every cached tensor 

2765 (and of the gradients when ``incl_bwd``). Defaults to None, do nothing. 

2766 reset_hooks_end: If True, removes the hooks this call added when it finishes. 

2767 Hooks the caller added beforehand are left alone either way. 

2768 clear_contexts: If True, clears the hook contexts of the touched hook points 

2769 when the hooks are removed. 

2770 device: Where to store cached activations (matches ActivationCache.to; 

2771 does not move the model). Defaults to per-layer storage. 

2772 **kwargs: Additional arguments 

2773 # type: ignore[name-defined] 

2774 Returns: 

2775 Tuple of (output, cache) 

2776 """ 

2777 if incl_bwd and stop_at_layer is not None: 

2778 raise ValueError( 

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

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

2781 ) 

2782 if incl_bwd and not torch.is_grad_enabled(): 

2783 raise ValueError( 

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

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

2786 "Run the call with gradients enabled." 

2787 ) 

2788 

2789 aliases = build_alias_to_canonical_map(self.hook_dict) 

2790 

2791 def create_names_filter_fn(filter_input): 

2792 if filter_input is None: 

2793 return lambda name: True 

2794 elif isinstance(filter_input, str): 

2795 mapped_name = aliases.get(filter_input, None) 

2796 if mapped_name: 

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

2798 else: 

2799 return lambda name: name == filter_input 

2800 elif isinstance(filter_input, list): 

2801 mapped_list = [] 

2802 for item in filter_input: 

2803 mapped_list.append(item) 

2804 mapped_name = aliases.get(item, None) 

2805 if mapped_name: 

2806 mapped_list.append(mapped_name) 

2807 return lambda name: name in mapped_list 

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

2809 return filter_input 

2810 else: 

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

2812 

2813 names_filter_fn = create_names_filter_fn(names_filter) 

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

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

2816 

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

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

2819 resolved_pos_slice = Slice.unwrap(pos_slice) 

2820 

2821 def make_cache_hook(name: str, pos_dim: int = -2, is_backward: bool = False): 

2822 key = f"{name}_grad" if is_backward else name 

2823 

2824 def store(tensor: torch.Tensor) -> None: 

2825 tensor = tensor.detach().to(cache_device) 

2826 if pos_slice is not None and isinstance(tensor, torch.Tensor) and tensor.dim() >= 2: 

2827 # Tensors too flat for the hook's layout (2D token ids at embed.hook_in) 

2828 # still carry position on axis 1, right after batch. Deliberate divergence 

2829 # from HookedRootModule, which would slice such a tensor on batch. 

2830 axis = pos_dim if tensor.dim() >= 1 - pos_dim else 1 

2831 tensor = resolved_pos_slice.apply(tensor, dim=axis) 

2832 cache[key] = tensor 

2833 

2834 def cache_hook(tensor: torch.Tensor, *, hook: Any) -> Optional[torch.Tensor]: 

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

2836 cache[key] = None 

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

2838 store(tensor) 

2839 elif isinstance(tensor, tuple): 

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

2841 store(tensor[0]) 

2842 else: 

2843 pass 

2844 else: 

2845 try: 

2846 if hasattr(tensor, "detach"): 

2847 store(tensor) 

2848 except Exception: 

2849 pass 

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

2851 return None if is_backward else tensor 

2852 

2853 return cache_hook 

2854 

2855 hook_dict = self.hook_dict 

2856 effective_stop_layer = None 

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

2858 if stop_at_layer < 0: 

2859 effective_stop_layer = len(self.blocks) + stop_at_layer 

2860 else: 

2861 effective_stop_layer = stop_at_layer 

2862 gated_names_skipped: List[str] = [] 

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

2864 if names_filter_fn(hook_name): 

2865 if effective_stop_layer is not None: 

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

2867 try: 

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

2869 if layer_num >= effective_stop_layer: 

2870 continue 

2871 except (IndexError, ValueError): 

2872 pass 

2873 

2874 # Only validate gated hooks when the caller explicitly supplied 

2875 # a names_filter. The default filter matches every hook and must 

2876 # not cause gated hooks to be treated as explicitly requested. 

2877 if names_filter is not None: 

2878 try: 

2879 self.check_hooks_to_add(hook_name) 

2880 except ValueError: 

2881 gated_names_skipped.append(hook_name) 

2882 continue 

2883 

2884 hooks.append((hook, hook_name)) 

2885 

2886 if names_filter is not None and gated_names_skipped: 

2887 warnings.warn( 

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

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

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

2891 stacklevel=2, 

2892 ) 

2893 self.context_level += 1 

2894 context_level = self.context_level 

2895 try: 

2896 for hp, name in hooks: 

2897 pos_dim = _pos_axis_for_hook(name, hp) 

2898 hp.add_hook(make_cache_hook(name, pos_dim), level=context_level) 

2899 if incl_bwd: 

2900 hp.add_hook( 

2901 make_cache_hook(name, pos_dim, is_backward=True), 

2902 dir="bwd", 

2903 level=context_level, 

2904 ) 

2905 processed_args = [input] 

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

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

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

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

2910 input_ids = input_ids.to(next(self.original_model.parameters()).device) 

2911 kwargs["input_ids"] = input_ids 

2912 processed_args = processed_args[1:] 

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

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

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

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

2917 input_ids = input_ids.to(next(self.original_model.parameters()).device) 

2918 kwargs["input_ids"] = input_ids 

2919 del kwargs["input"] 

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

2921 if stop_at_layer < 0: 

2922 stop_at_layer = len(self.blocks) + stop_at_layer 

2923 last_layer_to_process = stop_at_layer - 1 

2924 

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

2926 raise StopAtLayerException(tensor) 

2927 

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

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

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

2931 hook_dict = self.hook_dict 

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

2933 hook_dict[block_hook_name].add_hook(stop_hook, level=context_level) 

2934 hooks.append((hook_dict[block_hook_name], block_hook_name)) 

2935 filtered_kwargs = kwargs.copy() 

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

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

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

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

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

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

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

2943 warnings.warn( 

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

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

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

2947 stacklevel=2, 

2948 ) 

2949 if ( 

2950 "output_attentions" not in filtered_kwargs 

2951 and self.adapter.supports_hf_output_attentions 

2952 ): 

2953 # Attention-free remote models (e.g. HyenaDNA) reject the kwarg 

2954 # outright; only pass it when the HF forward actually accepts it. 

2955 fwd_params = inspect.signature(self.original_model.forward).parameters 

2956 if "output_attentions" in fwd_params or any( 2956 ↛ 2960line 2956 didn't jump to line 2960 because the condition on line 2956 was always true

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

2958 ): 

2959 filtered_kwargs["output_attentions"] = True 

2960 if processed_args: 

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

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

2963 output = self.forward( 

2964 filtered_kwargs["input_ids"], 

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

2966 ) 

2967 else: 

2968 output = self.forward(**filtered_kwargs) 

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

2970 output = output.logits 

2971 if incl_bwd: 

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

2973 # so the backward pass has to happen inside this try. 

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

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

2976 raise ValueError( 

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

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

2979 'Pass return_type="loss".' 

2980 ) 

2981 if not output.requires_grad: 

2982 raise ValueError( 

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

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

2985 ) 

2986 output.backward() 

2987 except StopAtLayerException as e: 

2988 output = e.layer_output 

2989 except Exception as e: 

2990 raise e 

2991 finally: 

2992 if reset_hooks_end: 

2993 for hp, _ in hooks: 

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

2995 # attached before the call survive. 

2996 hp.remove_hooks(dir="fwd", level=context_level) 

2997 if incl_bwd: 

2998 hp.remove_hooks(dir="bwd", level=context_level) 

2999 if clear_contexts: 

3000 hp.clear_context() 

3001 self.context_level -= 1 

3002 if self.compatibility_mode == True: 

3003 reverse_aliases = {} 

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

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

3006 for single_new_name in new_name: 

3007 reverse_aliases[single_new_name] = old_name 

3008 else: 

3009 reverse_aliases[new_name] = old_name 

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

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

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

3013 cache_items_to_add = {} 

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

3015 base_name, suffix = ( 

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

3017 if cache_name.endswith("_grad") 

3018 else (cache_name, "") 

3019 ) 

3020 old_name = reverse_aliases.get(base_name) 

3021 if old_name is not None: 

3022 cache_items_to_add[old_name + suffix] = cached_value 

3023 cache.update(cache_items_to_add) 

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

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

3026 for suffix in suffixes: 

3027 if alias_name + suffix in cache: 

3028 continue 

3029 for single_target in targets: 

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

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

3032 break 

3033 if return_cache_object: 

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

3035 if remove_batch_dim: 3035 ↛ 3036line 3035 didn't jump to line 3036 because the condition on line 3035 was never true

3036 activation_cache.remove_batch_dim() 

3037 return (output, activation_cache) 

3038 else: 

3039 if remove_batch_dim: 

3040 for key in cache: 

3041 if cache[key] is not None and isinstance(cache[key], torch.Tensor): 3041 ↛ 3040line 3041 didn't jump to line 3040 because the condition on line 3041 was always true

3042 if cache[key].size(0) == 1: 3042 ↛ 3040line 3042 didn't jump to line 3040 because the condition on line 3042 was always true

3043 cache[key] = cache[key][0] 

3044 return (output, cache) 

3045 

3046 def run_with_hooks( 

3047 self, 

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

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

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

3051 reset_hooks_end: bool = True, 

3052 clear_contexts: bool = False, 

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

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

3055 stop_at_layer: Optional[int] = None, 

3056 remove_batch_dim: bool = False, 

3057 **kwargs, 

3058 ) -> Any: 

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

3060 

3061 Args: 

3062 input: Input to the model 

3063 fwd_hooks: Forward hooks to apply 

3064 bwd_hooks: Backward hooks to apply 

3065 reset_hooks_end: Whether to reset hooks at the end 

3066 clear_contexts: Whether to clear hook contexts 

3067 return_type: What to return ("logits", "loss", etc.) 

3068 names_filter: Filter for hook names (not used directly, for compatibility) 

3069 stop_at_layer: Layer to stop at (uses StopAtLayerException; cleans up KV cache on stop) 

3070 remove_batch_dim: Whether to remove batch dimension from hook inputs (only works for batch_size==1) 

3071 **kwargs: Additional arguments 

3072 

3073 Returns: 

3074 Model output 

3075 """ 

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

3077 # Claimed here for the closures below, but only committed to self inside the try that 

3078 # decrements it, so a raise in between can't leave the counter incremented. 

3079 context_level = self.context_level + 1 

3080 effective_stop_layer = None 

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

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

3083 effective_stop_layer = len(self.blocks) + stop_at_layer 

3084 else: 

3085 effective_stop_layer = stop_at_layer 

3086 

3087 def add_hook_to_point( 

3088 hook_point: HookPoint, 

3089 hook_fn: Callable, 

3090 name: str, 

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

3092 *, 

3093 is_explicit: bool = True, 

3094 ): 

3095 if effective_stop_layer is not None and name.startswith("blocks."): 

3096 try: 

3097 layer_num = int(name.split(".")[1]) 

3098 if layer_num >= effective_stop_layer: 

3099 return 

3100 except (IndexError, ValueError): 

3101 pass 

3102 if is_explicit: 

3103 self.check_hooks_to_add(name) 

3104 elif self._gated_hook_reason(name) is not None: 3104 ↛ 3105line 3104 didn't jump to line 3105 because the condition on line 3104 was never true

3105 warnings.warn( 

3106 f"run_with_hooks(): filter matched gated-off hook name '{name}', skipped. " 

3107 "Call the relevant set_use_*(True) setter first to enable it.", 

3108 stacklevel=2, 

3109 ) 

3110 return 

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

3112 alias_names_list: list[str] = [] 

3113 if hook_point.name is not None: 

3114 alias_names_list.append(hook_point.name) 

3115 alias_names_list.append(name) 

3116 hook_point.add_hook( 

3117 hook_fn, dir=dir, alias_names=alias_names_list, level=context_level 

3118 ) 

3119 else: 

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

3121 added_hooks.append((hook_point, dir)) 

3122 

3123 def apply_hooks(hooks: List[Tuple[Union[str, Callable], Callable]], is_fwd: bool): 

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

3125 aliases = build_alias_to_canonical_map(self.hook_dict) 

3126 for hook_name_or_filter, hook_fn in hooks: 

3127 if remove_batch_dim: 3127 ↛ 3128line 3127 didn't jump to line 3128 because the condition on line 3127 was never true

3128 original_hook_fn = hook_fn 

3129 

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

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

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

3133 tensor_no_batch = tensor.squeeze(0) 

3134 result = _orig_fn(tensor_no_batch, hook) 

3135 if result.dim() == tensor_no_batch.dim(): 

3136 result = result.unsqueeze(0) 

3137 return result 

3138 else: 

3139 return _orig_fn(tensor, hook) 

3140 

3141 hook_fn = wrapped_hook_fn 

3142 if isinstance(hook_name_or_filter, str): 

3143 hook_dict = self.hook_dict 

3144 actual_hook_name = hook_name_or_filter 

3145 if hook_name_or_filter in aliases: 

3146 actual_hook_name = aliases[hook_name_or_filter] 

3147 if actual_hook_name in hook_dict: 3147 ↛ 3126line 3147 didn't jump to line 3126 because the condition on line 3147 was always true

3148 add_hook_to_point( 

3149 hook_dict[actual_hook_name], 

3150 hook_fn, 

3151 actual_hook_name, 

3152 direction, 

3153 is_explicit=True, 

3154 ) 

3155 else: 

3156 hook_dict = self.hook_dict 

3157 seen_hooks = set() 

3158 for name, hook_point in hook_dict.items(): 

3159 if hook_name_or_filter(name): 

3160 hook_id = id(hook_point) 

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

3162 continue 

3163 seen_hooks.add(hook_id) 

3164 hook_name_to_use = hook_point.name if hook_point.name else name 

3165 add_hook_to_point( 

3166 hook_point, 

3167 hook_fn, 

3168 hook_name_to_use, 

3169 direction, 

3170 is_explicit=False, 

3171 ) 

3172 

3173 try: 

3174 self.context_level = context_level 

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

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

3177 stop_at_layer = len(self.blocks) + stop_at_layer 

3178 last_layer_to_process = stop_at_layer - 1 

3179 

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

3181 raise StopAtLayerException(tensor) 

3182 

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

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

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

3186 hook_dict = self.hook_dict 

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

3188 add_hook_to_point( 

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

3190 ) 

3191 

3192 apply_hooks(fwd_hooks, True) 

3193 apply_hooks(bwd_hooks, False) 

3194 try: 

3195 output = self.forward( 

3196 input, return_type=return_type, stop_at_layer=stop_at_layer, **kwargs 

3197 ) 

3198 except StopAtLayerException as e: 

3199 output = e.layer_output 

3200 return output 

3201 finally: 

3202 if reset_hooks_end: 3202 ↛ 3209line 3202 didn't jump to line 3209 because the condition on line 3202 was always true

3203 for hook_point, direction in added_hooks: 

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

3205 # attached before the call survive. 

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

3207 if clear_contexts: 

3208 hook_point.clear_context() 

3209 self.context_level -= 1 

3210 

3211 def _resolve_stopping_criteria( 

3212 self, 

3213 stop_strings: Optional[Union[str, List[str]]], 

3214 stopping_criteria: Optional[Any], 

3215 ) -> Optional[Any]: 

3216 """Combine ``stop_strings`` and ``stopping_criteria`` into one StoppingCriteriaList. 

3217 

3218 Returns ``None`` when neither is supplied (or both reduce to no-ops), 

3219 so callers can cheaply check whether any extra stop signal is active. 

3220 ``stop_strings`` is turned into a HuggingFace ``StopStringCriteria`` (which reproduces 

3221 HF's exact partial-token-aware, end-anchored matching: it fires when the stop string 

3222 ends the generated text, even if the string straddles token boundaries) and therefore 

3223 requires a tokenizer. 

3224 A user-supplied ``stopping_criteria`` may be a single ``StoppingCriteria``, 

3225 a list of them, or a ``StoppingCriteriaList``. 

3226 

3227 Raises: 

3228 ValueError: if ``stop_strings`` is supplied without a tokenizer. 

3229 TypeError: if ``stopping_criteria`` is not a ``StoppingCriteria``, a 

3230 list/tuple of them, or a ``StoppingCriteriaList``. 

3231 """ 

3232 if stop_strings is None and stopping_criteria is None: 

3233 return None 

3234 

3235 from transformers import ( # local import: matches the file's transformers usage 

3236 StoppingCriteria, 

3237 StoppingCriteriaList, 

3238 StopStringCriteria, 

3239 ) 

3240 

3241 criteria = StoppingCriteriaList() 

3242 

3243 if stop_strings is not None: 

3244 strings = [stop_strings] if isinstance(stop_strings, str) else list(stop_strings) 

3245 strings = [s for s in strings if s] # drop empty strings (HF errors on them) 

3246 if strings: 

3247 if self.tokenizer is None: 

3248 raise ValueError( 

3249 "stop_strings requires a tokenizer (stop strings are detected by " 

3250 "matching against the tokenizer vocabulary), but this TransformerBridge " 

3251 "has no tokenizer. Pass a stopping_criteria callable that operates on " 

3252 "token ids instead, or use hf_generate()." 

3253 ) 

3254 criteria.append(StopStringCriteria(tokenizer=self.tokenizer, stop_strings=strings)) 

3255 

3256 if stopping_criteria is not None: 

3257 if isinstance(stopping_criteria, StoppingCriteriaList): 

3258 criteria.extend(stopping_criteria) 

3259 elif isinstance(stopping_criteria, (list, tuple)): 

3260 criteria.extend(stopping_criteria) 

3261 elif isinstance(stopping_criteria, StoppingCriteria): 

3262 criteria.append(stopping_criteria) 

3263 else: 

3264 raise TypeError( 

3265 "stopping_criteria must be a transformers.StoppingCriteria, a list of " 

3266 f"them, or a StoppingCriteriaList, but got {type(stopping_criteria).__name__}." 

3267 ) 

3268 

3269 return criteria if len(criteria) > 0 else None 

3270 

3271 def _encdec_ngram_processor(self) -> Optional[Any]: 

3272 """generation_config.no_repeat_ngram_size as transformers' own 

3273 processor, or None. HF applies it by default; parity for models whose 

3274 greedy decode needs it to escape token attractors.""" 

3275 size = getattr( 

3276 getattr(self.original_model, "generation_config", None), 

3277 "no_repeat_ngram_size", 

3278 None, 

3279 ) 

3280 if not size: 

3281 return None 

3282 from transformers.generation.logits_process import NoRepeatNGramLogitsProcessor 

3283 

3284 return NoRepeatNGramLogitsProcessor(size) 

3285 

3286 def _generate_tokens( 

3287 self, 

3288 current_tokens: torch.Tensor, 

3289 input_tokens: torch.Tensor, 

3290 batch_size: int, 

3291 *, 

3292 max_new_tokens: int, 

3293 do_sample: bool, 

3294 top_k: Optional[int], 

3295 top_p: Optional[float], 

3296 temperature: float, 

3297 freq_penalty: float, 

3298 repetition_penalty: float, 

3299 stop_at_eos: bool, 

3300 stop_tokens: List[int], 

3301 eos_token_for_padding: int, 

3302 finished_sequences: torch.Tensor, 

3303 use_past_kv_cache: bool, 

3304 use_stateful_cache: bool, 

3305 mamba_cache: Any, 

3306 mamba_conv_kernel: int, 

3307 is_encoder_decoder: bool, 

3308 _is_batched_list: bool, 

3309 _generate_from_embeds: bool, 

3310 encoder_input: Optional[torch.Tensor], 

3311 decoder_tokens: Optional[torch.Tensor], 

3312 generated_token_ids: Optional[List[torch.Tensor]], 

3313 pixel_values: Optional[torch.Tensor], 

3314 multimodal_kwargs: Dict[str, Any], 

3315 verbose: bool, 

3316 stopping_criteria_list: Optional[Any] = None, 

3317 initial_attention_mask: Optional[torch.Tensor] = None, 

3318 min_decoder_length: Optional[int] = None, 

3319 ngram_processor: Optional[Any] = None, 

3320 encoder_attention_mask: Optional[torch.Tensor] = None, 

3321 ) -> Generator[Tuple[torch.Tensor, torch.Tensor, bool], None, None]: 

3322 """Core generation loop. Yields (sampled_tokens, final_logits, all_finished) per step. 

3323 

3324 Owns the forward pass, sampling, stop handling (EOS and any 

3325 ``stopping_criteria_list``), token accumulation, and KV cache management. Callers 

3326 are responsible for try/finally cleanup of ``_capture_hf_cache``. 

3327 

3328 ``stopping_criteria_list`` (from ``_resolve_stopping_criteria``) is evaluated on 

3329 the running sequence each step and folded into the finished-sequence mask alongside 

3330 EOS, so when it is ``None`` the loop runs the EOS-only path unchanged. 

3331 """ 

3332 _hf_kv_cache = None 

3333 # A row may finish via EOS and/or any of the configured stopping criteria. 

3334 any_stop_active = stop_at_eos or stopping_criteria_list is not None 

3335 

3336 # Models that own their position derivation (the gate refuses them) cache 

3337 # mRoPE deltas on the module between calls; a text-only prefill never 

3338 # refreshes them, so a stale delta from an earlier multimodal forward gets 

3339 # added to every cached-step position. HF's generate recomputes them at 

3340 # prefill via prepare_inputs_for_generation, which this loop bypasses — 

3341 # so match it by clearing before the prompt pass. A multimodal prefill 

3342 # recomputes its own fresh deltas regardless. 

3343 if not self._accepts_derived_position_ids(): 

3344 underlying = getattr(self, "original_model", None) 

3345 for module in ( 

3346 underlying, 

3347 getattr(underlying, "model", None), 

3348 getattr(underlying, "language_model", None), 

3349 ): 

3350 if module is not None and hasattr(module, "rope_deltas"): 

3351 module.rope_deltas = None 

3352 

3353 # Pure-SSM models (Mamba-1/2) take the stateful cache as `cache_params`; 

3354 # modern hybrids (Bamba, NemotronH, FalconH1) take `past_key_values` and 

3355 # would receive a duplicate cache_params via **kwargs cascade otherwise. 

3356 stateful_cache_kwarg = "cache_params" 

3357 if use_stateful_cache: 

3358 forward_params = inspect.signature(self.original_model.forward).parameters 

3359 if "cache_params" not in forward_params: 

3360 stateful_cache_kwarg = "past_key_values" 

3361 

3362 for gen_step_idx in tqdm.tqdm(range(max_new_tokens), disable=not verbose): 

3363 with torch.no_grad(): 

3364 if is_encoder_decoder: 

3365 assert encoder_input is not None 

3366 encdec_kwargs: Dict[str, Any] = {} 

3367 if encoder_attention_mask is not None: 

3368 encdec_kwargs["attention_mask"] = encoder_attention_mask.to( 

3369 encoder_input.device 

3370 ) 

3371 logits = self( 

3372 encoder_input, 

3373 return_type="logits", 

3374 decoder_input=decoder_tokens, 

3375 **encdec_kwargs, 

3376 ) 

3377 else: 

3378 forward_kwargs: Dict[str, Any] = {} 

3379 # A prompt mask covers only the prompt, so extend it by one 

3380 # attended column per token generated so far. position_ids are 

3381 # left to forward(), which derives them from the mask for the 

3382 # models that can take them. 

3383 running_attention_mask: Optional[torch.Tensor] = None 

3384 if initial_attention_mask is not None: 

3385 n_generated = current_tokens.shape[1] - initial_attention_mask.shape[1] 

3386 running_attention_mask = torch.cat( 

3387 [ 

3388 initial_attention_mask.to(current_tokens.device), 

3389 torch.ones( 

3390 (current_tokens.shape[0], n_generated), 

3391 dtype=initial_attention_mask.dtype, 

3392 device=current_tokens.device, 

3393 ), 

3394 ], 

3395 dim=1, 

3396 ) 

3397 forward_kwargs["attention_mask"] = running_attention_mask 

3398 # Compute attention mask and position_ids for batched 

3399 # inputs with padding. 

3400 if ( 

3401 initial_attention_mask is None 

3402 and _is_batched_list 

3403 and self.tokenizer is not None 

3404 and self.tokenizer.pad_token_id is not None 

3405 ): 

3406 _prev_side = self.tokenizer.padding_side 

3407 self.tokenizer.padding_side = "left" 

3408 attn_mask = utils.get_attention_mask( 

3409 self.tokenizer, 

3410 current_tokens, 

3411 prepend_bos=getattr(self.cfg, "default_prepend_bos", True), 

3412 ).to(self.cfg.device) 

3413 self.tokenizer.padding_side = _prev_side 

3414 forward_kwargs["attention_mask"] = attn_mask 

3415 # Same target gate as the forward() path: the mask is safe 

3416 # for every model, the derived positions are not (#1626). 

3417 if self._accepts_derived_position_ids(): 

3418 position_ids = attn_mask.long().cumsum(-1) - 1 

3419 position_ids.masked_fill_(attn_mask == 0, 1) 

3420 forward_kwargs["position_ids"] = position_ids 

3421 if gen_step_idx == 0: 

3422 if pixel_values is not None: 

3423 forward_kwargs["pixel_values"] = pixel_values 

3424 if multimodal_kwargs: 

3425 forward_kwargs.update(multimodal_kwargs) 

3426 if use_stateful_cache: 

3427 forward_kwargs[stateful_cache_kwarg] = mamba_cache 

3428 forward_kwargs["use_cache"] = True 

3429 if gen_step_idx == 0: 

3430 # Mamba's conv-window warmup positions vs standard 

3431 # full-prompt positions for past_key_values hybrids. 

3432 prefill_len = ( 

3433 mamba_conv_kernel 

3434 if stateful_cache_kwarg == "cache_params" 

3435 else current_tokens.shape[1] 

3436 ) 

3437 cache_position = torch.arange(0, prefill_len, device=self.cfg.device) 

3438 forward_kwargs["cache_position"] = cache_position 

3439 logits = self( 

3440 current_tokens, 

3441 return_type="logits", 

3442 **forward_kwargs, 

3443 ) 

3444 else: 

3445 input_seq_pos = input_tokens.shape[1] + gen_step_idx - 1 

3446 cache_position = torch.tensor([input_seq_pos], device=self.cfg.device) 

3447 forward_kwargs["cache_position"] = cache_position 

3448 if "position_ids" in forward_kwargs: 3448 ↛ 3449line 3448 didn't jump to line 3449 because the condition on line 3448 was never true

3449 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

3450 :, -1: 

3451 ] 

3452 logits = self( 

3453 current_tokens[:, -1:], 

3454 return_type="logits", 

3455 **forward_kwargs, 

3456 ) 

3457 elif use_past_kv_cache: 

3458 forward_kwargs["use_cache"] = True 

3459 if _hf_kv_cache is not None: 

3460 forward_kwargs["past_key_values"] = _hf_kv_cache 

3461 # HF v5 + macOS-arm64 NaNs when these are inferred 

3462 # from cache state alone. Mirror HF generate(): pass 

3463 # both an (batch, total_len) attention_mask and a 

3464 # (batch, 1) position_ids for the new token. 

3465 batch_size = current_tokens.shape[0] 

3466 total_len = current_tokens.shape[1] 

3467 device = current_tokens.device 

3468 if "attention_mask" not in forward_kwargs: 

3469 forward_kwargs["attention_mask"] = torch.ones( 

3470 (batch_size, total_len), 

3471 dtype=torch.long, 

3472 device=device, 

3473 ) 

3474 # Gated as a whole (#1626): every branch below supplies 

3475 # position_ids, so gating only the prompt derivation 

3476 # above would divert a refused model into the 

3477 # total_len - 1 fallback, which counts pad slots and is 

3478 # wrong per row for a left-padded batch. A model that 

3479 # owns its position derivation gets the mask alone, 

3480 # matching the uncached path. 

3481 if self._accepts_derived_position_ids(): 

3482 if "position_ids" in forward_kwargs: 

3483 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

3484 :, -1: 

3485 ] 

3486 elif running_attention_mask is not None: 

3487 # total_len - 1 counts pad slots, so it is wrong 

3488 # for a left-padded prompt. Derive the new token's 

3489 # position from the mask instead. 

3490 forward_kwargs["position_ids"] = utils.get_offset_position_ids( 

3491 0, running_attention_mask.long() 

3492 )[:, -1:] 

3493 else: 

3494 forward_kwargs["position_ids"] = torch.full( 

3495 (batch_size, 1), 

3496 total_len - 1, 

3497 dtype=torch.long, 

3498 device=device, 

3499 ) 

3500 logits = self( 

3501 current_tokens[:, -1:], 

3502 return_type="logits", 

3503 **forward_kwargs, 

3504 ) 

3505 else: 

3506 logits = self( 

3507 current_tokens, 

3508 return_type="logits", 

3509 **forward_kwargs, 

3510 ) 

3511 else: 

3512 logits = self(current_tokens, return_type="logits", **forward_kwargs) 

3513 if use_past_kv_cache and hasattr(self, "_last_hf_cache"): 

3514 _hf_kv_cache = self._last_hf_cache or _hf_kv_cache 

3515 del self._last_hf_cache 

3516 final_logits = logits[:, -1, :] 

3517 

3518 # Sample next token 

3519 penalty_tokens = ( 

3520 torch.stack(generated_token_ids, dim=1) 

3521 if _generate_from_embeds and generated_token_ids 

3522 else None 

3523 ) 

3524 # transformers' own NoRepeatNGramLogitsProcessor, honoring 

3525 # generation_config (bart-large-cnn pins 3; without it greedy 

3526 # decoding falls into a BOS attractor and emits nothing). 

3527 if ngram_processor is not None and decoder_tokens is not None: 

3528 final_logits = ngram_processor(decoder_tokens, final_logits) 

3529 # HF's generate() suppresses EOS below generation_config.min_length 

3530 # (bart-large-cnn pins 56); without this the loop can EOS on step 

3531 # one and emit an empty summary. 

3532 if ( 

3533 min_decoder_length is not None 

3534 and is_encoder_decoder 

3535 and decoder_tokens is not None 

3536 and decoder_tokens.shape[1] < min_decoder_length 

3537 and stop_tokens 

3538 ): 

3539 final_logits = final_logits.clone() 

3540 final_logits[:, stop_tokens] = float("-inf") 

3541 if do_sample: 

3542 sampled_tokens = utils.sample_logits( 

3543 final_logits, 

3544 top_k=top_k, 

3545 top_p=top_p, 

3546 temperature=temperature, 

3547 freq_penalty=freq_penalty, 

3548 repetition_penalty=repetition_penalty, 

3549 tokens=( 

3550 penalty_tokens 

3551 if _generate_from_embeds 

3552 else (decoder_tokens if is_encoder_decoder else current_tokens) 

3553 ), 

3554 ).to(self.cfg.device) 

3555 else: 

3556 sampled_tokens = utils.sample_logits( 

3557 final_logits, 

3558 temperature=0.0, 

3559 repetition_penalty=repetition_penalty, 

3560 tokens=( 

3561 penalty_tokens 

3562 if _generate_from_embeds 

3563 else (decoder_tokens if is_encoder_decoder else current_tokens) 

3564 ), 

3565 ).to(self.cfg.device) 

3566 

3567 # Freeze rows that finished on an earlier step so they stop emitting 

3568 # real tokens. Applies to every active stop mechanism, not just EOS. 

3569 if any_stop_active: 

3570 sampled_tokens[finished_sequences] = eos_token_for_padding 

3571 

3572 # Fold this step's EOS matches into the finished mask. 

3573 if stop_at_eos: 

3574 finished_sequences.logical_or_( 

3575 torch.isin( 

3576 sampled_tokens.to(self.cfg.device), 

3577 torch.tensor(stop_tokens).to(self.cfg.device), 

3578 ) 

3579 ) 

3580 

3581 # Update token sequences 

3582 if is_encoder_decoder: 

3583 assert decoder_tokens is not None 

3584 decoder_tokens = torch.cat([decoder_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

3585 elif _generate_from_embeds: 

3586 assert generated_token_ids is not None 

3587 generated_token_ids.append(sampled_tokens) 

3588 embed_fn = self.original_model.get_input_embeddings() # type: ignore[operator] 

3589 assert embed_fn is not None 

3590 new_embed = embed_fn(sampled_tokens.unsqueeze(1)).to(current_tokens.dtype) 

3591 current_tokens = torch.cat([current_tokens, new_embed], dim=1) 

3592 else: 

3593 current_tokens = torch.cat([current_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

3594 

3595 # Fold stop_strings / stopping_criteria into the finished mask. They are 

3596 # evaluated on the full running sequence (prompt + everything generated so 

3597 # far, including the token just appended) with this step's logits as the 

3598 # scores argument, matching transformers' StoppingCriteria contract. The 

3599 # combined list returns a per-row bool [batch] OR-ing every criterion. 

3600 # generate()/generate_stream() guarantee this is plain decoder-only token 

3601 # generation, so current_tokens is the running token sequence. 

3602 if stopping_criteria_list is not None: 

3603 criteria_finished = stopping_criteria_list(current_tokens, final_logits).to( 

3604 device=self.cfg.device, dtype=torch.bool 

3605 ) 

3606 if criteria_finished.shape != finished_sequences.shape: 

3607 raise ValueError( 

3608 "A stopping criterion returned shape " 

3609 f"{tuple(criteria_finished.shape)}, expected a per-row bool of " 

3610 f"shape {tuple(finished_sequences.shape)} (one entry per sequence)." 

3611 ) 

3612 finished_sequences.logical_or_(criteria_finished) 

3613 

3614 all_finished = bool(any_stop_active and finished_sequences.all().item()) 

3615 

3616 yield sampled_tokens, final_logits, all_finished 

3617 

3618 if all_finished: 3618 ↛ 3619line 3618 didn't jump to line 3619 because the condition on line 3618 was never true

3619 return 

3620 

3621 def _resolve_generation_caching(self, use_past_kv_cache: bool, batched: bool) -> bool: 

3622 """Honor adapter caching/batching limits (recurrent/conv decoders have no KV 

3623 cache; batching is rejected where padding can't be masked, not mis-generated).""" 

3624 if batched and not getattr(self.adapter, "supports_batched_generation", True): 

3625 architecture = self.cfg.architecture or type(self.adapter).__name__ 

3626 raise NotImplementedError( 

3627 f"Batched generation is not supported by {architecture}: its forward does not " 

3628 "apply an attention mask, so padded rows would corrupt the output. Generate one " 

3629 "sequence at a time, or pass equal-length inputs as a tensor." 

3630 ) 

3631 if not getattr(self.adapter, "supports_kv_cache", True): 

3632 return False 

3633 return use_past_kv_cache 

3634 

3635 def _ensure_generation_supported(self, api_name: str) -> None: 

3636 """Reject autoregressive generation for forward-only architectures.""" 

3637 if not self.adapter.supports_generation: 

3638 architecture = self.cfg.architecture or type(self.adapter).__name__ 

3639 hint = ( 

3640 " Use diffusion_generate() — this architecture samples by iterative denoising, " 

3641 "not left-to-right." 

3642 if getattr(self.adapter, "native_sampler", None) 

3643 else "" 

3644 ) 

3645 raise NotImplementedError( 

3646 f"TransformerBridge.{api_name}() generation is not supported by " 

3647 f"the {architecture} architecture.{hint}" 

3648 ) 

3649 

3650 def generate( 

3651 self, 

3652 input: Union[str, List[str], torch.Tensor] = "", 

3653 max_new_tokens: int = 10, 

3654 stop_at_eos: bool = True, 

3655 eos_token_id: Optional[int] = None, 

3656 do_sample: bool = True, 

3657 top_k: Optional[int] = None, 

3658 top_p: Optional[float] = None, 

3659 temperature: float = 1.0, 

3660 freq_penalty: float = 0.0, 

3661 repetition_penalty: float = 1.0, 

3662 use_past_kv_cache: bool = True, 

3663 prepend_bos: Optional[bool] = None, 

3664 padding_side: Optional[str] = None, 

3665 return_type: Optional[str] = "input", 

3666 verbose: bool = True, 

3667 output_logits: bool = False, 

3668 return_cache: bool = False, 

3669 return_input_tokens: bool = False, 

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

3671 device: Optional[Union[str, torch.device]] = None, 

3672 pixel_values: Optional[torch.Tensor] = None, 

3673 stop_strings: Optional[Union[str, List[str]]] = None, 

3674 stopping_criteria: Optional[Any] = None, 

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

3676 forced_bos_token_id: Optional[int] = None, 

3677 **multimodal_kwargs, 

3678 ) -> ( 

3679 str 

3680 | list[str] 

3681 | torch.Tensor 

3682 | Any 

3683 | tuple[Any, ActivationCache] 

3684 | tuple[Any, torch.Tensor] 

3685 ): # Any for transformers.utils.ModelOutput 

3686 # Any: beartype forward ref limitation (beartype#546) 

3687 """Sample tokens from the model. 

3688 

3689 Sample tokens from the model until the model outputs eos_token or max_new_tokens is reached. 

3690 This implementation is based on HookedTransformer.generate() to ensure consistent behavior. 

3691 

3692 Args: 

3693 input: Text string, list of strings, or tensor of tokens 

3694 max_new_tokens: Maximum number of tokens to generate 

3695 stop_at_eos: If True, stop generating tokens when the model outputs eos_token 

3696 eos_token_id: The token ID to use for end of sentence 

3697 do_sample: If True, sample from the model's output distribution. Otherwise, use greedy search 

3698 top_k: Number of tokens to sample from. If None, sample from all tokens 

3699 top_p: Probability mass to sample from. If 1.0, sample from all tokens 

3700 temperature: Temperature for sampling. Higher values will make the model more random 

3701 freq_penalty: Frequency penalty for sampling - how much to penalise previous tokens 

3702 repetition_penalty: HuggingFace-style repetition penalty. Values > 1.0 discourage 

3703 repetition by dividing positive logits and multiplying negative logits for 

3704 previously seen tokens. Default 1.0 (no penalty). 

3705 use_past_kv_cache: If True, use KV caching for faster generation 

3706 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

3707 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

3708 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

3709 text that already contains the BOS token to avoid double-BOS. 

3710 Ignored when input is already a token tensor. 

3711 padding_side: Which side to pad when tokenizing multiple strings of different 

3712 lengths. For batched list inputs, left-padding is forced internally for 

3713 correct generation behavior. Defaults to None (tokenizer default). 

3714 return_type: The type of output to return - 'input', 'str', or 'tokens' 

3715 verbose: Not used in Bridge (kept for API compatibility) 

3716 output_logits: If True, return a ModelOutput with sequences and logits tuple 

3717 return_cache: If True, also return an ActivationCache for the full prompt + 

3718 generated sequence, identical to ``run_with_cache(output)``, and the call 

3719 returns an ``(output, cache)`` tuple. Implemented as one extra clean forward 

3720 over the output, so the cache includes every hook point (attention patterns 

3721 included). Supported only for single-sequence, decoder-only text generation; 

3722 encoder-decoder, SSM, multimodal, batched, and inputs_embeds inputs raise 

3723 NotImplementedError. The cache spans prompt + max_new_tokens and can be large, 

3724 use ``names_filter`` to scope it and/or ``device`` to offload it. 

3725 return_input_tokens: If True, return an ``(output, input_tokens)`` tuple where 

3726 ``input_tokens`` is the token tensor that was actually fed to the model 

3727 (after BOS handling). Useful for debugging tokenization, especially when 

3728 using chat templates where BOS handling can be subtle. Can be combined 

3729 with ``return_cache`` to get ``(output, cache, input_tokens)``. 

3730 names_filter: Passed to ``run_with_cache`` when ``return_cache=True``; restricts 

3731 which activations are cached (str, list of str, or callable). 

3732 device: Passed through when ``return_cache=True`` to offload the cached tensors 

3733 to this device (e.g. "cpu") to save accelerator memory. 

3734 pixel_values: Optional image tensor for multimodal models. Only passed on the 

3735 first generation step (the vision encoder processes the image once, then 

3736 embeddings are part of the token sequence for subsequent steps). 

3737 stop_strings: Optional string or list of strings. A sequence stops once its 

3738 generated text ends with one of these strings, using HuggingFace's 

3739 StopStringCriteria (partial-token-aware, end-anchored) matching. 

3740 Requires a tokenizer (raises ValueError otherwise). 

3741 Independent of stop_at_eos: either can stop a sequence. 

3742 stopping_criteria: Optional HuggingFace stopping criteria, a single 

3743 transformers.StoppingCriteria, a list of them, or a StoppingCriteriaList. 

3744 Each is called as criterion(input_ids, scores) after every step and ORed 

3745 with the other stop signals, where input_ids is the running sequence and 

3746 scores is this step's logits ([batch, d_vocab]). Each criterion must return 

3747 a per-row bool [batch] (or a scalar bool). stop_strings and stopping_criteria 

3748 are supported only for standard decoder-only text generation. Encoder-decoder, 

3749 inputs_embeds, and multimodal generation always raise NotImplementedError. 

3750 Stateful/SSM models raise only when run with use_past_kv_cache=False (the 

3751 default keeps them on the hooked loop). Each error names the supported 

3752 alternative. 

3753 attention_mask: Optional ``[batch, pos]`` 0/1 mask over the prompt, marking 

3754 which prompt tokens are real. Required to generate correctly from an 

3755 already-padded token tensor: without it the pad tokens are treated as 

3756 real context and every real token's position is shifted, so the 

3757 continuation differs from the same prompt unpadded. The mask is extended 

3758 by one attended column per generated token. Takes precedence over the 

3759 ``padding_side`` heuristic, and unlike it can express an interior gap or 

3760 a pad id that also occurs as a real token. Passing ``padding_side`` 

3761 instead reads the padding off the pad token, which is enough for the 

3762 common single-edge case, and raises if this bridge has no tokenizer 

3763 or pad id to read it from. On the encoder-decoder and inputs_embeds 

3764 paths the mask is forwarded to the model as-is rather than grown per 

3765 step, which is what processors emitting one alongside 

3766 ``pixel_values`` expect. 

3767 forced_bos_token_id: Optional token id seeded as the first decoder token 

3768 after ``decoder_start`` on encoder-decoder models. Multilingual 

3769 translators (M2M100/MBart/NLLB) select their target language this way. 

3770 Raises ValueError on decoder-only models. 

3771 

3772 Returns: 

3773 Generated sequence as string, list of strings, or tensor depending on input type and return_type. 

3774 If output_logits=True, returns a ModelOutput-like object with 'sequences' and 'logits' attributes. 

3775 If return_cache=True, returns an ``(output, ActivationCache)`` tuple where ``output`` is the 

3776 value that would otherwise be returned and the cache equals ``run_with_cache(output)``. 

3777 If return_input_tokens=True, returns an ``(output, input_tokens)`` tuple. 

3778 If both return_cache and return_input_tokens are True, returns ``(output, cache, input_tokens)``. 

3779 

3780 Example: 

3781 ``out, cache = model.generate(prompt, max_new_tokens=20, return_cache=True)`` returns a 

3782 normal ActivationCache over the full prompt + generated sequence (equivalent to 

3783 ``run_with_cache(out)``). 

3784 

3785 ``out, input_tokens = model.generate(prompt, return_input_tokens=True)`` returns 

3786 the tokens that were fed to the model, useful for verifying BOS handling with 

3787 chat templates. 

3788 """ 

3789 self._ensure_generation_supported("generate") 

3790 # padding_side is handled internally: for batched list inputs, left-padding 

3791 # is forced to ensure correct generation. See _is_batched_list logic below. 

3792 

3793 # Stateful dispatch is decided after input parsing so we can fall back 

3794 # to hf_generate() for input types the stateful loop doesn't handle. 

3795 is_stateful_model = getattr(self.cfg, "is_stateful", False) 

3796 

3797 _is_batched_list = isinstance(input, list) and len(input) > 1 

3798 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

3799 

3800 _generate_from_embeds = False 

3801 _encdec_early = hasattr(self.original_model, "config") and getattr( 

3802 self.original_model.config, "is_encoder_decoder", False 

3803 ) 

3804 if isinstance(input, str): 

3805 if _encdec_early: 

3806 # Deliberate divergence: prepend_bos is IGNORED for enc-dec 

3807 # string/list input. Encoder input follows the tokenizer's own 

3808 # recipe (lang token + trailing </s>); to_tokens' decoder-style 

3809 # BOS policy corrupts it — m2m100 degenerates to loops. 

3810 input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to( 

3811 self.cfg.device 

3812 ) 

3813 else: 

3814 input_tokens = self.to_tokens( 

3815 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3816 ) 

3817 input_type = "str" 

3818 elif isinstance(input, list): 

3819 if _encdec_early: 

3820 # Same native-recipe rule as the str branch: to_tokens' BOS 

3821 # policy corrupts encoder inputs (stray <s>, dropped </s>). 

3822 # Keep the tokenizer's mask too — unequal rows otherwise 

3823 # attend over pads in the encoder. 

3824 _enc_batch = self.tokenizer(input, return_tensors="pt", padding=True) 

3825 input_tokens = _enc_batch["input_ids"].to(self.cfg.device) 

3826 if attention_mask is None and "attention_mask" in _enc_batch: 3826 ↛ 3839line 3826 didn't jump to line 3839 because the condition on line 3826 was always true

3827 attention_mask = _enc_batch["attention_mask"].to(self.cfg.device) 

3828 else: 

3829 # Force left-padding for batched generation so real tokens are 

3830 # flush-right and logits[:, -1, :] is always the last real token. 

3831 if _is_batched_list: 3831 ↛ 3834line 3831 didn't jump to line 3834 because the condition on line 3831 was always true

3832 _orig_padding_side = self.tokenizer.padding_side 

3833 self.tokenizer.padding_side = "left" 

3834 input_tokens = self.to_tokens( 

3835 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3836 ) 

3837 if _is_batched_list: 3837 ↛ 3839line 3837 didn't jump to line 3839 because the condition on line 3837 was always true

3838 self.tokenizer.padding_side = _orig_padding_side 

3839 input_type = "list" 

3840 elif isinstance(input, torch.Tensor) and input.is_floating_point(): 

3841 # inputs_embeds: pre-computed embeddings (e.g., from multimodal models) 

3842 input_tokens = input.to(self.cfg.device) 

3843 input_type = "embeds" 

3844 _generate_from_embeds = True 

3845 else: 

3846 input_tokens = input.to(self.cfg.device) 

3847 input_type = "tokens" 

3848 

3849 # Without one of these a pre-padded tensor generates as though its pads were 

3850 # real context, shifting every real token's position (#1612). An explicit 

3851 # mask wins; otherwise the padding is read off the tokens, but only when the 

3852 # caller asked for that by passing padding_side. Deriving a mask on the 

3853 # default path would silently change behaviour for every existing caller, 

3854 # and would demand a real tokenizer where today none is required. 

3855 initial_attention_mask: Optional[torch.Tensor] = attention_mask 

3856 if initial_attention_mask is not None and ( 

3857 _generate_from_embeds 

3858 or getattr(getattr(self.original_model, "config", None), "is_encoder_decoder", False) 

3859 ): 

3860 # Growing the mask per step only means something for decoder-only token 

3861 # generation. On these paths the mask used to arrive via 

3862 # **multimodal_kwargs and be forwarded to the model untouched — as 

3863 # processors emit it alongside pixel_values — so keep doing that rather 

3864 # than reject a call that worked before this parameter existed. 

3865 multimodal_kwargs = {**multimodal_kwargs, "attention_mask": initial_attention_mask} 

3866 initial_attention_mask = None 

3867 if initial_attention_mask is not None: 

3868 if initial_attention_mask.shape != input_tokens.shape: 

3869 raise ValueError( 

3870 f"attention_mask shape {tuple(initial_attention_mask.shape)} does not " 

3871 f"match the prompt shape {tuple(input_tokens.shape)}. Pass a 0/1 mask " 

3872 "covering exactly the prompt tokens; generate() extends it itself." 

3873 ) 

3874 initial_attention_mask = initial_attention_mask.to(self.cfg.device) 

3875 elif padding_side is not None and input_type == "tokens": 

3876 # Reading the padding off the tokens needs a tokenizer with a pad id. 

3877 # Without one the argument would be inert, leaving exactly the bug this 

3878 # fixes — silently, on a bridge booted without a tokenizer. Say so 

3879 # rather than generate something quietly wrong. 

3880 if not isinstance(self.tokenizer, PreTrainedTokenizerBase): 

3881 raise ValueError( 

3882 "generate(padding_side=...) reads the padding off the pad token, " 

3883 "which needs a tokenizer; this bridge has none. Pass " 

3884 "attention_mask=... to state the padding directly instead." 

3885 ) 

3886 if self.tokenizer.pad_token_id is None: 

3887 raise ValueError( 

3888 "generate(padding_side=...) reads the padding off the pad token, " 

3889 "but this tokenizer has no pad_token_id. Set one, or pass " 

3890 "attention_mask=... to state the padding directly instead." 

3891 ) 

3892 _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos 

3893 _orig_side = self.tokenizer.padding_side 

3894 self.tokenizer.padding_side = padding_side 

3895 try: 

3896 initial_attention_mask = utils.get_attention_mask( 

3897 self.tokenizer, input_tokens, _prepend 

3898 ).to(self.cfg.device) 

3899 finally: 

3900 self.tokenizer.padding_side = _orig_side 

3901 # An all-ones mask is what the model assumes anyway; skipping it keeps 

3902 # the unpadded path byte-identical to before. 

3903 if initial_attention_mask is not None and bool(initial_attention_mask.all()): 3903 ↛ 3904line 3903 didn't jump to line 3904 because the condition on line 3903 was never true

3904 initial_attention_mask = None 

3905 

3906 # Determine return type 

3907 if return_type == "input": 

3908 if input_type in ["str", "list"]: 

3909 return_type = "str" 

3910 elif input_type == "embeds": 

3911 return_type = "tokens" 

3912 else: 

3913 return_type = "tokens" 

3914 

3915 batch_size = input_tokens.shape[0] 

3916 

3917 # Setup EOS token handling 

3918 stop_tokens = [] 

3919 eos_token_for_padding = 0 

3920 if stop_at_eos: 

3921 tokenizer_has_eos_token = ( 

3922 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

3923 ) 

3924 if eos_token_id is None: 

3925 # Some chat models use a turn-end token that differs from the 

3926 # tokenizer's primary EOS. Let adapters provide the full stop 

3927 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

3928 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

3929 if eos_token_id is None: 

3930 assert ( 

3931 tokenizer_has_eos_token 

3932 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

3933 assert self.tokenizer is not None 

3934 eos_token_id = self.tokenizer.eos_token_id 

3935 

3936 if isinstance(eos_token_id, int): 

3937 stop_tokens = [eos_token_id] 

3938 eos_token_for_padding = eos_token_id 

3939 else: 

3940 stop_tokens = list(eos_token_id) 

3941 if tokenizer_has_eos_token: 

3942 assert self.tokenizer is not None 

3943 eos_token_for_padding = self.tokenizer.eos_token_id 

3944 else: 

3945 eos_token_for_padding = eos_token_id[0] 

3946 

3947 # Track which sequences have finished 

3948 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

3949 

3950 # Optionally collect logits at each generation step for downstream tooling/tests 

3951 logits_seq_list: list[torch.Tensor] | None = [] if output_logits else None 

3952 

3953 # Detect encoder-decoder models (T5, BART, etc.) 

3954 is_encoder_decoder = hasattr(self.original_model, "config") and getattr( 

3955 self.original_model.config, "is_encoder_decoder", False 

3956 ) 

3957 if forced_bos_token_id is None and is_encoder_decoder: 

3958 # HF's generate() applies generation_config defaults; bart-large-cnn 

3959 # pins forced_bos_token_id=0 there and degrades without it. 

3960 forced_bos_token_id = getattr( 

3961 getattr(self.original_model, "generation_config", None), 

3962 "forced_bos_token_id", 

3963 None, 

3964 ) 

3965 if forced_bos_token_id is not None and not is_encoder_decoder: 3965 ↛ 3968line 3965 didn't jump to line 3968 because the condition on line 3965 was never true

3966 # Raise before any state mutation (_capture_hf_cache) and before 

3967 # the stateful hf_generate early-return would drop the kwarg. 

3968 raise ValueError("forced_bos_token_id is only meaningful for encoder-decoder models") 

3969 

3970 # return_cache recomputes run_with_cache on the generated output (see issue #697). 

3971 # That is well-defined only for single-sequence, decoder-only text generation, so 

3972 # reject the paths whose cache would be wrong/undefined, with a clear pointer to the 

3973 # run_with_cache workaround. Fail fast here, before any generation work. 

3974 if return_cache: 

3975 if is_encoder_decoder: 3975 ↛ 3976line 3975 didn't jump to line 3976 because the condition on line 3975 was never true

3976 raise NotImplementedError( 

3977 "generate(return_cache=True) is not supported for encoder-decoder " 

3978 "models yet. Run run_with_cache on the generated output instead." 

3979 ) 

3980 if is_stateful_model: 3980 ↛ 3981line 3980 didn't jump to line 3981 because the condition on line 3980 was never true

3981 raise NotImplementedError( 

3982 "generate(return_cache=True) is not supported for stateful/SSM models " 

3983 "(e.g. Mamba); they do not expose standard transformer hook points." 

3984 ) 

3985 if pixel_values is not None or multimodal_kwargs: 3985 ↛ 3986line 3985 didn't jump to line 3986 because the condition on line 3985 was never true

3986 raise NotImplementedError( 

3987 "generate(return_cache=True) is not supported for multimodal generation " 

3988 "yet. Run run_with_cache on the generated output instead." 

3989 ) 

3990 if _generate_from_embeds: 

3991 raise NotImplementedError( 

3992 "generate(return_cache=True) requires token input, not inputs_embeds." 

3993 ) 

3994 if batch_size > 1: 

3995 raise NotImplementedError( 

3996 "generate(return_cache=True) is not supported for batched/multi-prompt " 

3997 "generation yet. Pass a single prompt, or run run_with_cache on each " 

3998 "output sequence." 

3999 ) 

4000 

4001 # HF cache flows opaquely through the component chain via 

4002 # _reconstruct_attention() → _update_kv_cache() on each layer. 

4003 _hf_kv_cache = None 

4004 if use_past_kv_cache and is_encoder_decoder: 

4005 # Encoder-decoder models (T5, BART) don't support the opaque 

4006 # cache path — silently disable rather than crash, since 

4007 # use_past_kv_cache=True is the default. 

4008 use_past_kv_cache = False 

4009 

4010 # SSMs (Mamba/Mamba-2) run through a dedicated cache path so hooks 

4011 # fire on every step. Unsupported input types fall back to hf_generate(). 

4012 use_stateful_cache = ( 

4013 is_stateful_model 

4014 and use_past_kv_cache 

4015 and not is_encoder_decoder 

4016 and not _generate_from_embeds 

4017 and pixel_values is None 

4018 and not multimodal_kwargs 

4019 ) 

4020 

4021 # stop_strings / stopping_criteria are applied inside the hooked _generate_tokens 

4022 # loop, so they are supported only on the standard decoder-only text path. Reject 

4023 # the paths that route around that loop with a clear error rather than silently 

4024 # dropping the kwargs. This must run before the stateful delegation below. 

4025 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

4026 if stopping_criteria_list is not None: 

4027 if is_encoder_decoder: 

4028 _unsupported = "encoder-decoder models" 

4029 elif _generate_from_embeds: 

4030 _unsupported = "inputs_embeds generation" 

4031 elif pixel_values is not None or multimodal_kwargs: 

4032 _unsupported = "multimodal (pixel_values) generation" 

4033 else: 

4034 _unsupported = None 

4035 if _unsupported is not None: 

4036 raise NotImplementedError( 

4037 f"stop_strings/stopping_criteria are not supported for {_unsupported} in " 

4038 "TransformerBridge.generate(). Call hf_generate(...), which runs " 

4039 "HuggingFace's own generation loop and supports HF-native stopping on " 

4040 "those inputs." 

4041 ) 

4042 if is_stateful_model and not use_stateful_cache: 

4043 # Reached only for a stateful/SSM model with use_past_kv_cache=False: the 

4044 # hooked loop needs the stateful cache, so generate() would otherwise fall 

4045 # back to hf_generate() and drop these kwargs. The default cache setting 

4046 # keeps generation on the hooked loop, where stopping is applied. 

4047 raise NotImplementedError( 

4048 "stop_strings/stopping_criteria on a stateful/SSM model require the " 

4049 "stateful cache path, which runs only with use_past_kv_cache=True (the " 

4050 "default). With use_past_kv_cache=False generate() falls back to " 

4051 "hf_generate(). Set use_past_kv_cache=True to keep stopping on the hooked " 

4052 "loop, or call hf_generate(...) directly for HF-native stopping." 

4053 ) 

4054 # Finished rows are overwritten with this id so they stop emitting real tokens 

4055 # while the rest of a batch keeps going. stop_at_eos already set a sensible 

4056 # value, otherwise fall back to the tokenizer pad/eos id. (For a single 

4057 # sequence this id is never read: the loop exits when the row finishes.) 

4058 if not stop_at_eos: 

4059 _pad_id = None 

4060 if self.tokenizer is not None: 

4061 _pad_id = ( 

4062 self.tokenizer.pad_token_id 

4063 if self.tokenizer.pad_token_id is not None 

4064 else self.tokenizer.eos_token_id 

4065 ) 

4066 if _pad_id is not None: 

4067 eos_token_for_padding = _pad_id 

4068 elif batch_size > 1: 

4069 raise ValueError( 

4070 "Batched generation with stopping_criteria and stop_at_eos=False " 

4071 "needs a padding token to freeze finished rows, but no tokenizer " 

4072 "pad/eos id is available. Set stop_at_eos=True, use a tokenizer with " 

4073 "a pad or eos token, or generate one sequence at a time." 

4074 ) 

4075 

4076 if is_stateful_model and not use_stateful_cache: 4076 ↛ 4077line 4076 didn't jump to line 4077 because the condition on line 4076 was never true

4077 hf_kwargs: dict[str, Any] = { 

4078 "max_new_tokens": max_new_tokens, 

4079 "do_sample": do_sample, 

4080 "temperature": temperature, 

4081 } 

4082 if top_k is not None: 

4083 hf_kwargs["top_k"] = top_k 

4084 if top_p is not None: 

4085 hf_kwargs["top_p"] = top_p 

4086 if eos_token_id is not None: 

4087 hf_kwargs["eos_token_id"] = eos_token_id 

4088 return self.hf_generate(input, **hf_kwargs) 

4089 

4090 # SSM cache is built once and mutated in place across forward calls. 

4091 # Adapter owns the cache-type choice; new SSMs just override 

4092 # create_stateful_cache(). 

4093 mamba_cache: Any = None 

4094 mamba_conv_kernel: int = 0 

4095 if use_stateful_cache: 

4096 hf_model: Any = self.original_model 

4097 mamba_conv_kernel = int(getattr(hf_model.config, "conv_kernel", 4)) 

4098 cache_dtype = self.cfg.dtype or torch.float32 

4099 mamba_cache = self.adapter.create_stateful_cache( 

4100 hf_model=hf_model, 

4101 batch_size=batch_size, 

4102 device=self.cfg.device, 

4103 dtype=cache_dtype, 

4104 ) 

4105 

4106 if use_past_kv_cache and not use_stateful_cache: 

4107 self._capture_hf_cache = True # Signal forward() to stash cache 

4108 

4109 # Generate tokens 

4110 current_tokens = input_tokens.clone() 

4111 # For inputs_embeds generation, also track generated token IDs for decoding 

4112 if _generate_from_embeds: 

4113 generated_token_ids: list[torch.Tensor] = [] 

4114 sampled_tokens_list = [] 

4115 

4116 # For encoder-decoder models, keep encoder input fixed and grow decoder input 

4117 if is_encoder_decoder: 

4118 encoder_input = input_tokens.clone() 

4119 decoder_start_token_id = getattr( 

4120 self.original_model.config, "decoder_start_token_id", None 

4121 ) 

4122 if decoder_start_token_id is None: 

4123 # HF's fallback chain: bos, then eos (MBart-family checkpoints 

4124 # like IndicBART leave decoder_start unset and start from EOS). 

4125 fallback = getattr(self.original_model.config, "bos_token_id", None) 

4126 if fallback is None: 

4127 fallback = getattr(self.original_model.config, "eos_token_id", None) 

4128 if isinstance(fallback, (list, tuple)): 4128 ↛ 4129line 4128 didn't jump to line 4129 because the condition on line 4128 was never true

4129 fallback = fallback[0] 

4130 decoder_start_token_id = fallback if fallback is not None else 0 

4131 decoder_tokens = torch.full( 

4132 (batch_size, 1), 

4133 decoder_start_token_id, 

4134 dtype=input_tokens.dtype, 

4135 device=self.cfg.device, 

4136 ) 

4137 if forced_bos_token_id is not None: 

4138 # Multilingual seq2seq (M2M100/MBart/NLLB) selects the target 

4139 # language via the first decoder token after decoder_start. 

4140 forced = torch.full( 

4141 (batch_size, 1), 

4142 forced_bos_token_id, 

4143 dtype=input_tokens.dtype, 

4144 device=self.cfg.device, 

4145 ) 

4146 decoder_tokens = torch.cat([decoder_tokens, forced], dim=1) 

4147 

4148 try: 

4149 for sampled_tokens, final_logits, all_finished in self._generate_tokens( 

4150 current_tokens, 

4151 input_tokens, 

4152 batch_size, 

4153 max_new_tokens=max_new_tokens, 

4154 do_sample=do_sample, 

4155 top_k=top_k, 

4156 top_p=top_p, 

4157 temperature=temperature, 

4158 freq_penalty=freq_penalty, 

4159 repetition_penalty=repetition_penalty, 

4160 stop_at_eos=stop_at_eos, 

4161 stop_tokens=stop_tokens, 

4162 eos_token_for_padding=eos_token_for_padding, 

4163 finished_sequences=finished_sequences, 

4164 use_past_kv_cache=use_past_kv_cache, 

4165 use_stateful_cache=use_stateful_cache, 

4166 mamba_cache=mamba_cache, 

4167 mamba_conv_kernel=mamba_conv_kernel, 

4168 is_encoder_decoder=is_encoder_decoder, 

4169 _is_batched_list=_is_batched_list, 

4170 _generate_from_embeds=_generate_from_embeds, 

4171 encoder_input=encoder_input if is_encoder_decoder else None, 

4172 decoder_tokens=decoder_tokens if is_encoder_decoder else None, 

4173 generated_token_ids=generated_token_ids if _generate_from_embeds else None, 

4174 pixel_values=pixel_values, 

4175 multimodal_kwargs=multimodal_kwargs if multimodal_kwargs else {}, 

4176 verbose=verbose, 

4177 stopping_criteria_list=stopping_criteria_list, 

4178 initial_attention_mask=initial_attention_mask, 

4179 min_decoder_length=( 

4180 getattr( 

4181 getattr(self.original_model, "generation_config", None), 

4182 "min_length", 

4183 None, 

4184 ) 

4185 if is_encoder_decoder 

4186 else None 

4187 ), 

4188 ngram_processor=(self._encdec_ngram_processor() if is_encoder_decoder else None), 

4189 encoder_attention_mask=(attention_mask if is_encoder_decoder else None), 

4190 ): 

4191 sampled_tokens_list.append(sampled_tokens.unsqueeze(1)) 

4192 if logits_seq_list is not None: 

4193 logits_seq_list.append(final_logits.clone()) 

4194 if all_finished: 

4195 break 

4196 finally: 

4197 self._capture_hf_cache = False 

4198 if hasattr(self, "_last_hf_cache"): 4198 ↛ 4199line 4198 didn't jump to line 4199 because the condition on line 4198 was never true

4199 del self._last_hf_cache 

4200 

4201 # Concatenate all sampled tokens 

4202 sampled_tokens = torch.cat(sampled_tokens_list, dim=1) 

4203 if is_encoder_decoder: 

4204 # Reconstruct full decoder sequence: start token + generated tokens 

4205 decoder_seed_len = 2 if forced_bos_token_id is not None else 1 

4206 output_tokens = torch.cat([decoder_tokens[:, :decoder_seed_len], sampled_tokens], dim=1) 

4207 elif _generate_from_embeds: 

4208 # For inputs_embeds, we only have the generated token IDs (no input token IDs) 

4209 output_tokens = sampled_tokens 

4210 else: 

4211 output_tokens = torch.cat([input_tokens, sampled_tokens], dim=1) 

4212 

4213 # Build the formatted output (shape unchanged: ModelOutput / str / list[str] / tokens). 

4214 result: Any 

4215 if output_logits and logits_seq_list is not None: 

4216 from transformers.utils import ModelOutput # type: ignore 

4217 

4218 def _logits_to_tuple(logits_list: list[torch.Tensor]) -> tuple[torch.Tensor, ...]: 

4219 assert logits_list is not None 

4220 # Convert list of [batch, vocab] tensors to tuple 

4221 return tuple(logits_list) 

4222 

4223 try: 

4224 from transformers.generation.utils import GenerateDecoderOnlyOutput 

4225 

4226 # HF-compatible ModelOutput structure. 

4227 # GenerateDecoderOnlyOutput expects: sequences, scores (optional), logits (optional) 

4228 result = GenerateDecoderOnlyOutput( 

4229 sequences=cast(torch.LongTensor, output_tokens), 

4230 # HF's type hint says tuple[FloatTensor] but should be tuple[FloatTensor, ...] 

4231 # (variable-length tuple with one element per generated token) 

4232 logits=_logits_to_tuple(logits_seq_list), # type: ignore[arg-type] 

4233 ) 

4234 except (ImportError, AttributeError): 

4235 # Fallback if GenerateDecoderOnlyOutput not available in this transformers version 

4236 result = ModelOutput( 

4237 sequences=output_tokens, 

4238 logits=_logits_to_tuple(logits_seq_list), 

4239 ) 

4240 elif return_type == "str": 

4241 assert self.tokenizer is not None 

4242 if input_type == "str": 

4243 result = self.tokenizer.decode(output_tokens[0], skip_special_tokens=True) 

4244 else: 

4245 decoded_texts = [ 

4246 self.tokenizer.decode(tokens, skip_special_tokens=True) 

4247 for tokens in output_tokens 

4248 ] 

4249 result = decoded_texts[0] if len(decoded_texts) == 1 else decoded_texts 

4250 else: # return_type == "tokens" 

4251 result = output_tokens 

4252 

4253 if not return_cache and not return_input_tokens: 

4254 return result 

4255 

4256 if return_cache: 

4257 # return_cache: recompute one clean forward over the full generated sequence so the 

4258 # cache is identical to run_with_cache(output_tokens) - all hook points, including 

4259 # attention patterns. The guards above restrict this to single-sequence, decoder-only 

4260 # text generation (see issue #697). 

4261 _, cache = self.run_with_cache(output_tokens, names_filter=names_filter, device=device) 

4262 if return_input_tokens: 

4263 return result, cache, input_tokens 

4264 return result, cache 

4265 

4266 # return_input_tokens only (no cache) 

4267 return result, input_tokens 

4268 

4269 @torch.no_grad() 

4270 def diffusion_generate( 

4271 self, 

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

4273 max_new_tokens: int = 32, 

4274 prepend_bos: Optional[bool] = None, 

4275 **kwargs: Any, 

4276 ) -> Union[str, torch.Tensor]: 

4277 """Sample from a non-autoregressive (diffusion) architecture. 

4278 

4279 Delegates to the model's own sampler, which calls the model through 

4280 ``__call__`` so bridge hooks fire on every denoising step. 

4281 """ 

4282 sampler_name = getattr(self.adapter, "native_sampler", None) 

4283 architecture = self.cfg.architecture or type(self.adapter).__name__ 

4284 if sampler_name is None: 4284 ↛ 4285line 4284 didn't jump to line 4285 because the condition on line 4284 was never true

4285 raise NotImplementedError( 

4286 f"{architecture} has no native sampler; use generate() for autoregressive " 

4287 "architectures." 

4288 ) 

4289 sampler = getattr(self.original_model, sampler_name, None) 

4290 if sampler is None: 4290 ↛ 4291line 4290 didn't jump to line 4291 because the condition on line 4290 was never true

4291 raise NotImplementedError( 

4292 f"{architecture} declares native_sampler={sampler_name!r} but the loaded model " 

4293 "has no such method." 

4294 ) 

4295 

4296 was_string = isinstance(input, str) 

4297 if isinstance(input, list) and len(input) > 1: 4297 ↛ 4301line 4297 didn't jump to line 4301 because the condition on line 4297 was never true

4298 # Unequal prompts would be right-padded into the sampler's canvas, 

4299 # where pad tokens read as real context. generate() gates batching 

4300 # for the same reason; do not silently corrupt rows here. 

4301 raise NotImplementedError( 

4302 f"diffusion_generate() does not support batched prompts for {architecture}: " 

4303 "the native samplers condition on a padded canvas. Sample one prompt at a time." 

4304 ) 

4305 if isinstance(input, torch.Tensor): 4305 ↛ 4310line 4305 didn't jump to line 4310 because the condition on line 4305 was always true

4306 tokens = input.to(self.cfg.device) 

4307 else: 

4308 # Tokenization is the bridge's concern, not the sampler's — absorb 

4309 # prepend_bos here rather than forwarding it into native kwargs. 

4310 tokens = self.to_tokens( 

4311 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

4312 ) 

4313 

4314 sampler_kwargs = self.adapter.native_sampler_kwargs(max_new_tokens, tokens.shape[-1]) 

4315 sampler_kwargs.update(kwargs) 

4316 output = sampler(tokens, **sampler_kwargs) 

4317 # Samplers return either bare ids or a generation output object. 

4318 sequences = getattr(output, "sequences", output) 

4319 # They also disagree on whether the prompt is included: Dream and Gidd 

4320 # return the whole canvas, LLaDA2 slices it off. Normalize to 

4321 # generate()'s contract (prompt + continuation). 

4322 if isinstance(sequences, torch.Tensor) and sequences.ndim == tokens.ndim: 4322 ↛ 4333line 4322 didn't jump to line 4333 because the condition on line 4322 was always true

4323 prompt_len = tokens.shape[-1] 

4324 # Compare on one device: torch.equal raises on a device mismatch, 

4325 # which some samplers produce by assembling output on CPU. 

4326 sequences = sequences.to(tokens.device) 

4327 includes_prompt = sequences.shape[-1] >= prompt_len and torch.equal( 

4328 sequences[..., :prompt_len], tokens 

4329 ) 

4330 if not includes_prompt: 

4331 sequences = torch.cat([tokens, sequences], dim=-1) 

4332 

4333 if was_string and self.tokenizer is not None: 4333 ↛ 4334line 4333 didn't jump to line 4334 because the condition on line 4333 was never true

4334 return self.tokenizer.decode(sequences[0], skip_special_tokens=True) 

4335 return sequences 

4336 

4337 @torch.no_grad() 

4338 def generate_stream( 

4339 self, 

4340 input: Union[str, List[str], torch.Tensor] = "", 

4341 max_new_tokens: int = 10, 

4342 max_tokens_per_yield: int = 25, 

4343 stop_at_eos: bool = True, 

4344 eos_token_id: Optional[int] = None, 

4345 do_sample: bool = True, 

4346 top_k: Optional[int] = None, 

4347 top_p: Optional[float] = None, 

4348 temperature: float = 1.0, 

4349 freq_penalty: float = 0.0, 

4350 repetition_penalty: float = 1.0, 

4351 use_past_kv_cache: bool = True, 

4352 prepend_bos: Optional[bool] = None, 

4353 padding_side: Optional[str] = None, 

4354 return_type: Optional[str] = "input", 

4355 verbose: bool = True, 

4356 stop_strings: Optional[Union[str, List[str]]] = None, 

4357 stopping_criteria: Optional[Any] = None, 

4358 ) -> Generator[Union[torch.Tensor, str], None, None]: 

4359 """Stream tokens from the model as they are generated. 

4360 

4361 Yields batches of tokens progressively during generation rather than 

4362 waiting for the entire sequence. Uses the same core loop as generate(). 

4363 

4364 Args: 

4365 input: Text string, list of strings, or tensor of tokens. 

4366 max_new_tokens: Maximum number of tokens to generate. 

4367 max_tokens_per_yield: Yield accumulated tokens every this many steps. 

4368 stop_at_eos: If True, stop when eos_token is produced. 

4369 eos_token_id: Token ID(s) for end of sentence. Defaults to tokenizer's. 

4370 do_sample: If True, sample; otherwise greedy. 

4371 top_k: Top-k sampling. None means no filtering. 

4372 top_p: Nucleus sampling threshold. 

4373 temperature: Sampling temperature. 

4374 freq_penalty: Frequency penalty for previous tokens. 

4375 repetition_penalty: HF-style repetition penalty (>1.0 discourages repeats). 

4376 use_past_kv_cache: Use KV caching for faster generation. 

4377 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

4378 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

4379 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

4380 text that already contains the BOS token to avoid double-BOS. 

4381 Ignored when input is already a token tensor. 

4382 padding_side: Which side to pad for batched list inputs. Left-padding 

4383 is forced internally for batched generation. 

4384 return_type: 'input' (match input type), 'str', or 'tokens'. 

4385 verbose: Show progress bar. 

4386 stop_strings: Optional string or list of strings. A sequence stops once its 

4387 generated text ends with one of them (HF StopStringCriteria). Requires a 

4388 tokenizer. See generate() for details. 

4389 stopping_criteria: Optional transformers StoppingCriteria, list, or 

4390 StoppingCriteriaList, called as criterion(input_ids, scores) each step 

4391 (scores is the step's logits). See generate() for the full contract. 

4392 

4393 Yields: 

4394 Token tensors [batch, seq_len] or strings, accumulated up to 

4395 max_tokens_per_yield tokens between yields. First yield includes 

4396 the input tokens; subsequent yields contain only new tokens. 

4397 """ 

4398 self._ensure_generation_supported("generate_stream") 

4399 # --- Input parsing (mirrors generate()) --- 

4400 _is_batched_list = isinstance(input, list) and len(input) > 1 

4401 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

4402 

4403 _encdec_early = hasattr(self.original_model, "config") and getattr( 

4404 self.original_model.config, "is_encoder_decoder", False 

4405 ) 

4406 if isinstance(input, str): 

4407 if _encdec_early: 4407 ↛ 4409line 4407 didn't jump to line 4409 because the condition on line 4407 was never true

4408 # Native recipe: to_tokens' BOS policy corrupts encoder inputs. 

4409 input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to( 

4410 self.cfg.device 

4411 ) 

4412 else: 

4413 input_tokens = self.to_tokens( 

4414 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

4415 ) 

4416 input_type = "str" 

4417 elif isinstance(input, list): 4417 ↛ 4418line 4417 didn't jump to line 4418 because the condition on line 4417 was never true

4418 if _encdec_early: 

4419 input_tokens = self.tokenizer(input, return_tensors="pt", padding=True)[ 

4420 "input_ids" 

4421 ].to(self.cfg.device) 

4422 elif _is_batched_list: 

4423 _orig_ps = self.tokenizer.padding_side 

4424 self.tokenizer.padding_side = "left" 

4425 try: 

4426 input_tokens = self.to_tokens( 

4427 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

4428 ) 

4429 finally: 

4430 self.tokenizer.padding_side = _orig_ps 

4431 else: 

4432 input_tokens = self.to_tokens( 

4433 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

4434 ) 

4435 input_type = "list" 

4436 else: 

4437 input_tokens = input.to(self.cfg.device) 

4438 input_type = "tokens" 

4439 

4440 if return_type == "input": 4440 ↛ 4441line 4440 didn't jump to line 4441 because the condition on line 4440 was never true

4441 return_type = "str" if input_type in ["str", "list"] else "tokens" 

4442 

4443 batch_size = input_tokens.shape[0] 

4444 

4445 # --- EOS setup --- 

4446 stop_tokens: List[int] = [] 

4447 eos_token_for_padding = 0 

4448 if stop_at_eos: 

4449 tokenizer_has_eos_token = ( 

4450 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

4451 ) 

4452 if eos_token_id is None: 

4453 # Some chat models use a turn-end token that differs from the 

4454 # tokenizer's primary EOS. Let adapters provide the full stop 

4455 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

4456 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

4457 if eos_token_id is None: 

4458 assert ( 

4459 tokenizer_has_eos_token 

4460 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

4461 assert self.tokenizer is not None 

4462 eos_token_id = self.tokenizer.eos_token_id 

4463 if isinstance(eos_token_id, int): 

4464 stop_tokens = [eos_token_id] 

4465 eos_token_for_padding = eos_token_id 

4466 else: 

4467 stop_tokens = list(eos_token_id) 

4468 if tokenizer_has_eos_token: 4468 ↛ 4469line 4468 didn't jump to line 4469 because the condition on line 4468 was never true

4469 assert self.tokenizer is not None 

4470 eos_token_for_padding = self.tokenizer.eos_token_id 

4471 else: 

4472 eos_token_for_padding = eos_token_id[0] 

4473 

4474 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

4475 

4476 # stop_strings / stopping_criteria: build the combined criteria list (validates 

4477 # tokenizer for stop_strings). generate_stream only runs the decoder-only text 

4478 # path, so no path guards are needed here. 

4479 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

4480 if stopping_criteria_list is not None and not stop_at_eos: 

4481 _pad_id = None 

4482 if self.tokenizer is not None: 

4483 _pad_id = ( 

4484 self.tokenizer.pad_token_id 

4485 if self.tokenizer.pad_token_id is not None 

4486 else self.tokenizer.eos_token_id 

4487 ) 

4488 if _pad_id is not None: 

4489 eos_token_for_padding = _pad_id 

4490 elif batch_size > 1: 4490 ↛ 4499line 4490 didn't jump to line 4499 because the condition on line 4490 was always true

4491 raise ValueError( 

4492 "Batched generate_stream with stopping_criteria and stop_at_eos=False " 

4493 "needs a padding token to freeze finished rows, but no tokenizer pad/eos " 

4494 "id is available. Set stop_at_eos=True or use a tokenizer with a pad/eos " 

4495 "token." 

4496 ) 

4497 

4498 # --- Cache setup --- 

4499 if use_past_kv_cache: 

4500 self._capture_hf_cache = True 

4501 

4502 current_tokens = input_tokens.clone() 

4503 

4504 # --- Streaming loop --- 

4505 # All yields are token tensors [batch, seq_len]. Each yield contains 

4506 # only the newly generated tokens since the previous yield (the first 

4507 # yield additionally prepends the input tokens for context). 

4508 accumulated_tokens: Optional[torch.Tensor] = None 

4509 tokens_since_last_yield = 0 

4510 

4511 def _maybe_decode( 

4512 tokens: torch.Tensor, 

4513 ) -> Union[torch.Tensor, str]: 

4514 if return_type == "str": 

4515 assert self.tokenizer is not None 

4516 return self.tokenizer.decode(tokens[0], skip_special_tokens=True) 

4517 return tokens 

4518 

4519 try: 

4520 for step_idx, (sampled_tokens, _, all_finished) in enumerate( 

4521 self._generate_tokens( 

4522 current_tokens, 

4523 input_tokens, 

4524 batch_size, 

4525 max_new_tokens=max_new_tokens, 

4526 do_sample=do_sample, 

4527 top_k=top_k, 

4528 top_p=top_p, 

4529 temperature=temperature, 

4530 freq_penalty=freq_penalty, 

4531 repetition_penalty=repetition_penalty, 

4532 stop_at_eos=stop_at_eos, 

4533 stop_tokens=stop_tokens, 

4534 eos_token_for_padding=eos_token_for_padding, 

4535 finished_sequences=finished_sequences, 

4536 use_past_kv_cache=use_past_kv_cache, 

4537 use_stateful_cache=False, 

4538 mamba_cache=None, 

4539 mamba_conv_kernel=0, 

4540 is_encoder_decoder=False, 

4541 _is_batched_list=_is_batched_list, 

4542 _generate_from_embeds=False, 

4543 encoder_input=None, 

4544 decoder_tokens=None, 

4545 generated_token_ids=None, 

4546 pixel_values=None, 

4547 multimodal_kwargs={}, 

4548 verbose=verbose, 

4549 stopping_criteria_list=stopping_criteria_list, 

4550 ) 

4551 ): 

4552 new_tokens = sampled_tokens.unsqueeze(-1) 

4553 

4554 if step_idx == 0: 

4555 accumulated_tokens = torch.cat([input_tokens, new_tokens], dim=-1) 

4556 tokens_since_last_yield = accumulated_tokens.shape[1] 

4557 else: 

4558 if accumulated_tokens is None: 

4559 accumulated_tokens = new_tokens 

4560 else: 

4561 accumulated_tokens = torch.cat([accumulated_tokens, new_tokens], dim=-1) 

4562 tokens_since_last_yield += 1 

4563 

4564 if tokens_since_last_yield >= max_tokens_per_yield: 

4565 yield _maybe_decode(accumulated_tokens) 

4566 tokens_since_last_yield = 0 

4567 accumulated_tokens = None 

4568 

4569 if all_finished: 

4570 if accumulated_tokens is not None: 4570 ↛ 4573line 4570 didn't jump to line 4573 because the condition on line 4570 was always true

4571 yield _maybe_decode(accumulated_tokens) 

4572 accumulated_tokens = None 

4573 break 

4574 

4575 # Yield remainder after loop completes without break 

4576 if accumulated_tokens is not None: 

4577 yield _maybe_decode(accumulated_tokens) 

4578 finally: 

4579 self._capture_hf_cache = False 

4580 if hasattr(self, "_last_hf_cache"): 4580 ↛ 4581line 4580 didn't jump to line 4581 because the condition on line 4580 was never true

4581 del self._last_hf_cache 

4582 

4583 def hf_generate( 

4584 self, 

4585 input: str | list[str] | torch.Tensor = "", 

4586 max_new_tokens: int = 10, 

4587 stop_at_eos: bool = True, 

4588 eos_token_id: int | None = None, 

4589 do_sample: bool = True, 

4590 top_k: int | None = None, 

4591 top_p: float | None = None, 

4592 temperature: float = 1.0, 

4593 use_past_kv_cache: bool = True, 

4594 return_type: str | None = "input", 

4595 pixel_values: torch.Tensor | None = None, 

4596 **generation_kwargs, 

4597 ) -> str | list[str] | torch.Tensor | Any: # Any for HF ModelOutput types 

4598 # Any: beartype forward ref limitation (beartype#546) 

4599 """Generate text using the underlying HuggingFace model with full HF API support. 

4600 

4601 This method provides direct access to HuggingFace's generation API, forwarding all 

4602 generation parameters (including output_scores, output_logits, output_attentions, 

4603 output_hidden_states) directly to the underlying HF model. Use this when you need 

4604 full HuggingFace generation features not supported by the standard generate() method. 

4605 

4606 For standard generation compatible with HookedTransformer, use generate() instead. 

4607 

4608 Args: 

4609 input: Text string, list of strings, or tensor of tokens 

4610 max_new_tokens: Maximum number of tokens to generate 

4611 stop_at_eos: If True, stop generating tokens when the model outputs eos_token 

4612 eos_token_id: The token ID to use for end of sentence 

4613 do_sample: If True, sample from the model's output distribution 

4614 top_k: Number of tokens to sample from 

4615 top_p: Probability mass to sample from 

4616 temperature: Temperature for sampling 

4617 use_past_kv_cache: If True, use KV caching for faster generation 

4618 return_type: The type of output to return - 'input', 'str', or 'tokens' 

4619 **generation_kwargs: Additional HuggingFace generation parameters including: 

4620 - output_scores: Return generation scores 

4621 - output_logits: Return generation logits 

4622 - output_attentions: Return attention weights 

4623 - output_hidden_states: Return hidden states 

4624 - return_dict_in_generate: Return ModelOutput object 

4625 - And any other HF generation parameters 

4626 

4627 Returns: 

4628 Generated sequence as string, list of strings, tensor, or HF ModelOutput 

4629 depending on input type, return_type, and generation_kwargs. 

4630 

4631 Example:: 

4632 

4633 # Get full HF ModelOutput with logits and attentions 

4634 from transformer_lens import HookedTransformer 

4635 model = HookedTransformer.from_pretrained("tiny-stories-1M") 

4636 result = model.hf_generate( 

4637 "Hello world", 

4638 max_new_tokens=5, 

4639 output_logits=True, 

4640 output_attentions=True, 

4641 return_dict_in_generate=True 

4642 ) 

4643 print(result.sequences) # Generated tokens 

4644 print(result.logits) # Logits for each generation step 

4645 print(result.attentions) # Attention weights 

4646 """ 

4647 self._ensure_generation_supported("hf_generate") 

4648 # Handle string input by tokenizing it 

4649 if isinstance(input, str): 

4650 inputs = self.tokenizer(input, return_tensors="pt", padding=False, truncation=False).to( 

4651 self.cfg.device 

4652 ) 

4653 input_ids = inputs["input_ids"] 

4654 input_type = "str" 

4655 elif isinstance(input, list): 4655 ↛ 4662line 4655 didn't jump to line 4662 because the condition on line 4655 was always true

4656 inputs = self.tokenizer(input, return_tensors="pt", padding=True, truncation=False).to( 

4657 self.cfg.device 

4658 ) 

4659 input_ids = inputs["input_ids"] 

4660 input_type = "list" 

4661 else: 

4662 input_ids = input 

4663 if input_ids.device != self.cfg.device: 

4664 input_ids = input_ids.to(self.cfg.device) 

4665 input_type = "tokens" 

4666 

4667 # Build generation_kwargs from explicit args and kwargs 

4668 generation_kwargs = dict(generation_kwargs) if generation_kwargs is not None else {} 

4669 generation_kwargs.update( 

4670 { 

4671 "max_new_tokens": max_new_tokens, 

4672 "do_sample": do_sample, 

4673 "temperature": temperature, 

4674 "pad_token_id": self.tokenizer.eos_token_id, 

4675 } 

4676 ) 

4677 

4678 if top_k is not None: 4678 ↛ 4679line 4678 didn't jump to line 4679 because the condition on line 4678 was never true

4679 generation_kwargs["top_k"] = top_k 

4680 if top_p is not None: 4680 ↛ 4681line 4680 didn't jump to line 4681 because the condition on line 4680 was never true

4681 generation_kwargs["top_p"] = top_p 

4682 if eos_token_id is not None: 4682 ↛ 4683line 4682 didn't jump to line 4683 because the condition on line 4682 was never true

4683 generation_kwargs["eos_token_id"] = eos_token_id 

4684 elif stop_at_eos and self.tokenizer.eos_token_id is not None: 4684 ↛ 4687line 4684 didn't jump to line 4687 because the condition on line 4684 was always true

4685 generation_kwargs["eos_token_id"] = self.tokenizer.eos_token_id 

4686 

4687 if pixel_values is not None: 4687 ↛ 4688line 4687 didn't jump to line 4688 because the condition on line 4687 was never true

4688 generation_kwargs["pixel_values"] = pixel_values 

4689 

4690 if use_past_kv_cache: 4690 ↛ 4694line 4690 didn't jump to line 4694 because the condition on line 4690 was always true

4691 generation_kwargs["use_cache"] = True 

4692 

4693 # HF dict flags that trigger ModelOutput returns 

4694 hf_dict_flags = ( 

4695 "output_scores", 

4696 "output_logits", 

4697 "output_attentions", 

4698 "output_hidden_states", 

4699 ) 

4700 

4701 # If any HF-style output flags are provided, ensure return_dict_in_generate is set 

4702 any_flag_set = False 

4703 for f in hf_dict_flags: 

4704 if generation_kwargs.get(f) is not None: 

4705 generation_kwargs[f] = bool(generation_kwargs[f]) 

4706 any_flag_set = True 

4707 

4708 if any_flag_set: 4708 ↛ 4712line 4708 didn't jump to line 4712 because the condition on line 4708 was always true

4709 generation_kwargs.setdefault("return_dict_in_generate", True) 

4710 

4711 # Generate using the original HuggingFace model 

4712 with torch.no_grad(): 

4713 outputs = self.original_model.generate(input_ids, **generation_kwargs) # type: ignore[operator] 

4714 

4715 # Check if output is a ModelOutput 

4716 try: 

4717 from transformers.utils import ModelOutput # type: ignore 

4718 

4719 is_model_output = isinstance(outputs, ModelOutput) 

4720 except Exception: 

4721 is_model_output = False 

4722 

4723 # Return based on return_type and input format 

4724 if return_type == "input" or return_type is None: 

4725 if input_type == "str": 

4726 # Decode the full output back to string 

4727 if is_model_output and hasattr(outputs, "sequences"): 4727 ↛ 4729line 4727 didn't jump to line 4729 because the condition on line 4727 was always true

4728 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4729 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4730 elif input_type == "list": 4730 ↛ 4740line 4730 didn't jump to line 4740 because the condition on line 4730 was always true

4731 # Decode each sequence in the batch 

4732 if is_model_output and hasattr(outputs, "sequences"): 4732 ↛ 4737line 4732 didn't jump to line 4737 because the condition on line 4732 was always true

4733 return [ 

4734 self.tokenizer.decode(seq, skip_special_tokens=True) 

4735 for seq in outputs.sequences 

4736 ] 

4737 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4738 else: 

4739 # Return the full token sequence including input 

4740 return outputs 

4741 elif return_type == "tokens": 4741 ↛ 4745line 4741 didn't jump to line 4745 because the condition on line 4741 was always true

4742 return outputs 

4743 else: 

4744 # For other return types, default to the decoded text 

4745 if input_type == "str": 

4746 if is_model_output and hasattr(outputs, "sequences"): 

4747 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4748 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4749 elif input_type == "list": 

4750 if is_model_output and hasattr(outputs, "sequences"): 

4751 return [ 

4752 self.tokenizer.decode(seq, skip_special_tokens=True) 

4753 for seq in outputs.sequences 

4754 ] 

4755 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4756 else: 

4757 return outputs 

4758 

4759 def prepare_multimodal_inputs( 

4760 self, 

4761 text: Union[str, List[str]], 

4762 images: Optional[Any] = None, 

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

4764 """Prepare multimodal inputs using the model's processor. 

4765 

4766 Converts text and images into model-ready tensors (input_ids, pixel_values, 

4767 attention_mask, etc.) using the HuggingFace processor loaded during boot(). 

4768 

4769 Args: 

4770 text: Text prompt(s), typically containing image placeholder tokens 

4771 (e.g., "<image>" for LLaVA). 

4772 images: PIL Image or list of PIL Images to process. Pass None for 

4773 text-only inputs on a multimodal model. 

4774 

4775 Returns: 

4776 Dictionary with 'input_ids', 'pixel_values', 'attention_mask', etc. 

4777 All tensors are moved to the model's device. 

4778 

4779 Raises: 

4780 ValueError: If model is not multimodal or processor is not available. 

4781 """ 

4782 if not getattr(self.cfg, "is_multimodal", False): 

4783 raise ValueError( 

4784 "prepare_multimodal_inputs() requires a multimodal model " 

4785 "(cfg.is_multimodal must be True)" 

4786 ) 

4787 if self.processor is None: 

4788 raise ValueError( 

4789 "No processor available. Load model with boot_transformers() or " 

4790 "set bridge.processor = AutoProcessor.from_pretrained(...) manually." 

4791 ) 

4792 inputs = self.processor(text=text, images=images, return_tensors="pt") 

4793 return {k: v.to(self.cfg.device) if hasattr(v, "to") else v for k, v in inputs.items()} 

4794 

4795 def to(self, *args, **kwargs) -> "TransformerBridge": 

4796 """Move model to device and/or change dtype. 

4797 

4798 Args: 

4799 args: Positional arguments for nn.Module.to 

4800 kwargs: Keyword arguments for nn.Module.to 

4801 print_details: Whether to print details about device/dtype changes (default: True) 

4802 

4803 Returns: 

4804 Self for chaining 

4805 """ 

4806 # Extract print_details if provided 

4807 print_details = kwargs.pop("print_details", True) 

4808 

4809 # Handle both device and dtype changes 

4810 # torch.nn.Module.to() supports: to(device), to(dtype), to(device, dtype), 

4811 # to(device=...), to(dtype=...), to(device=..., dtype=...) 

4812 target_device, target_dtype = None, None 

4813 

4814 if len(args) >= 1: 

4815 first_arg = args[0] 

4816 if isinstance(first_arg, (torch.device, str)): 

4817 target_device = first_arg 

4818 elif isinstance(first_arg, torch.dtype): 4818 ↛ 4820line 4818 didn't jump to line 4820 because the condition on line 4818 was always true

4819 target_dtype = first_arg 

4820 if len(args) >= 2: 

4821 second_arg = args[1] 

4822 if isinstance(second_arg, torch.dtype): 4822 ↛ 4826line 4822 didn't jump to line 4826 because the condition on line 4822 was always true

4823 target_dtype = second_arg 

4824 

4825 # these override positional args 

4826 if "device" in kwargs: 4826 ↛ 4827line 4826 didn't jump to line 4827 because the condition on line 4826 was never true

4827 target_device = kwargs["device"] 

4828 if "dtype" in kwargs: 

4829 target_dtype = kwargs["dtype"] 

4830 

4831 # Moving a multi-device (device_map-dispatched) model to a single device would 

4832 # collapse the split and break accelerate's hook routing. Warn and drop the 

4833 # device move; still honor dtype changes. 

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

4835 warnings.warn( 

4836 f"TransformerBridge.to({target_device!r}) ignored: model is dispatched " 

4837 f"across {self.cfg.n_devices} devices via device_map. Reload with " 

4838 "device=... (and no device_map/n_devices) to move to a single device.", 

4839 stacklevel=2, 

4840 ) 

4841 target_device = None 

4842 

4843 if target_device is not None: 

4844 move_to_and_update_config(self, target_device, print_details) 

4845 if target_dtype is not None: 

4846 move_to_and_update_config(self, target_dtype, print_details) 

4847 

4848 # Move the original model with all original args/kwargs (with print_details removed). 

4849 # When we've nulled target_device for multi-GPU safety, strip device args so the 

4850 # underlying module isn't moved either. 

4851 if target_device is None and (len(args) > 0 or "device" in kwargs): 

4852 kwargs.pop("device", None) 

4853 # Filter positional args: drop devices/strings, keep dtypes. 

4854 args = tuple(a for a in args if not isinstance(a, (torch.device, str))) 

4855 self.original_model = self.original_model.to(*args, **kwargs) 

4856 return self 

4857 

4858 def cuda(self, device: Optional[Union[int, torch.device]] = None) -> "TransformerBridge": 

4859 """Move model to CUDA. 

4860 

4861 Args: 

4862 device: CUDA device 

4863 

4864 Returns: 

4865 Self for chaining 

4866 """ 

4867 if isinstance(device, int): 

4868 return self.to(f"cuda:{device}") 

4869 elif device is None: 

4870 return self.to("cuda") 

4871 else: 

4872 return self.to(device) 

4873 

4874 def cpu(self) -> "TransformerBridge": 

4875 """Move model to CPU. 

4876 

4877 Returns: 

4878 Self for chaining 

4879 """ 

4880 return self.to(torch.device("cpu")) 

4881 

4882 def mps(self) -> "TransformerBridge": 

4883 """Move model to MPS. 

4884 

4885 Returns: 

4886 Self for chaining 

4887 """ 

4888 return self.to(torch.device("mps")) 

4889 

4890 def train(self, mode: bool = True) -> "TransformerBridge": 

4891 """Set training mode, propagating to the wrapped source model.""" 

4892 super().train(mode) 

4893 original = getattr(self, "original_model", None) 

4894 if isinstance(original, torch.nn.Module): 4894 ↛ 4896line 4894 didn't jump to line 4896 because the condition on line 4894 was always true

4895 original.train(mode) 

4896 return self 

4897 

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

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

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

4901 return "use_attn_result" 

4902 if ( 

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

4904 and not self.cfg.use_split_qkv_input 

4905 ): 

4906 return "use_split_qkv_input" 

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

4908 return "use_hook_mlp_in" 

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

4910 return "use_attn_in" 

4911 return None 

4912 

4913 def check_hooks_to_add(self, hook_point_name: str) -> None: 

4914 """Raise a clear error if a hook is being explicitly added to a gated-off hook point. 

4915 

4916 Mirrors HookedTransformer.check_hooks_to_add, but raises a ValueError 

4917 naming the setter to call, instead of a bare assert. Only for explicit, 

4918 user-named hook points — a filter/callable matching a gated name uses 

4919 _gated_hook_reason directly and skips with a warning instead, since the 

4920 filter was not necessarily targeting that name on purpose. 

4921 """ 

4922 reason = self._gated_hook_reason(hook_point_name) 

4923 if reason is not None: 

4924 raise ValueError( 

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

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

4927 ) 

4928 

4929 def add_hook( 

4930 self, 

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

4932 hook_fn, 

4933 dir="fwd", 

4934 is_permanent=False, 

4935 ): 

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

4937 

4938 Args: 

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

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

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

4942 returns True. 

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

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

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

4946 """ 

4947 if callable(name) and not isinstance(name, str): 4947 ↛ 4948line 4947 didn't jump to line 4948 because the condition on line 4947 was never true

4948 hook_dict = self.hook_dict 

4949 seen_hooks: set[int] = set() 

4950 gated_names_skipped: List[str] = [] 

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

4952 if name(hook_name): 

4953 hook_id = id(hook_point) 

4954 if hook_id in seen_hooks: 

4955 continue 

4956 seen_hooks.add(hook_id) 

4957 if self._gated_hook_reason(hook_name) is not None: 

4958 gated_names_skipped.append(hook_name) 

4959 continue 

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

4961 if gated_names_skipped: 

4962 warnings.warn( 

4963 f"add_hook: filter matched {len(gated_names_skipped)} gated-off hook " 

4964 f"name(s) that were skipped: {gated_names_skipped}. Call the relevant " 

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

4966 stacklevel=2, 

4967 ) 

4968 return 

4969 

4970 component = self 

4971 parts = name.split(".") 

4972 for part in parts[:-1]: 

4973 if hasattr(component, part): 4973 ↛ 4976line 4973 didn't jump to line 4976 because the condition on line 4973 was always true

4974 component = getattr(component, part) 

4975 else: 

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

4977 hook_name = parts[-1] 

4978 if hasattr(component, hook_name): 4978 ↛ 4988line 4978 didn't jump to line 4988 because the condition on line 4978 was always true

4979 hook_point = getattr(component, hook_name) 

4980 if isinstance(hook_point, HookPoint): 4980 ↛ 4984line 4980 didn't jump to line 4984 because the condition on line 4980 was always true

4981 self.check_hooks_to_add(name) 

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

4983 else: 

4984 raise AttributeError( 

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

4986 ) 

4987 else: 

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

4989 

4990 def add_perma_hook( 

4991 self, 

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

4993 hook_fn, 

4994 dir="fwd", 

4995 ) -> None: 

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

4997 

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

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

5000 underlying ``HookPoint`` directly. 

5001 """ 

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

5003 

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

5005 """All registered :class:`HookPoint` instances.""" 

5006 return self._hook_registry.values() 

5007 

5008 def clear_contexts(self) -> None: 

5009 """Clear the stored ``ctx`` on every registered hook point.""" 

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

5011 hp.clear_context() 

5012 

5013 def remove_all_hook_fns( 

5014 self, 

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

5016 including_permanent: bool = False, 

5017 level: Optional[int] = None, 

5018 ) -> None: 

5019 """Remove hook functions from every registered hook point.""" 

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

5021 hp.remove_hooks(dir=direction, including_permanent=including_permanent, level=level) 

5022 

5023 def reset_hooks( 

5024 self, 

5025 clear_contexts: bool = True, 

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

5027 including_permanent: bool = False, 

5028 level: Optional[int] = None, 

5029 ) -> None: 

5030 """Remove hooks from the model; mirrors ``HookedRootModule.reset_hooks``. 

5031 

5032 Clears through the hook registry (which holds hook points the component 

5033 walk cannot reach, e.g. alias-registered points) and, on a full reset, 

5034 additionally walks the component tree — dev's registry is not asserted 

5035 canonical, so both passes run belt-and-suspenders. 

5036 """ 

5037 if clear_contexts: 5037 ↛ 5039line 5037 didn't jump to line 5039 because the condition on line 5037 was always true

5038 self.clear_contexts() 

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

5040 

5041 if direction == "both" and level is None: 5041 ↛ exitline 5041 didn't return from function 'reset_hooks' because the condition on line 5041 was always true

5042 

5043 def remove_hooks_recursive(module): 

5044 if isinstance(module, GeneralizedComponent): 

5045 module.remove_hooks() 

5046 for child in module.children(): 

5047 remove_hooks_recursive(child) 

5048 

5049 remove_hooks_recursive(self) 

5050 

5051 def hooks(self, fwd_hooks=[], bwd_hooks=[], reset_hooks_end=True, clear_contexts=False): 

5052 """Context manager for temporarily adding hooks. 

5053 

5054 Args: 

5055 fwd_hooks: List of (hook_name, hook_fn) tuples for forward hooks 

5056 bwd_hooks: List of (hook_name, hook_fn) tuples for backward hooks 

5057 reset_hooks_end: If True, removes the hooks this context added when it exits. 

5058 Hooks the caller added beforehand are left alone either way. 

5059 clear_contexts: If True, clears the hook contexts of the touched hook points 

5060 when the hooks are removed 

5061 

5062 Example: 

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

5064 output = model("Hello world") 

5065 """ 

5066 

5067 @contextmanager 

5068 def _hooks_context(): 

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

5070 # Claimed here for the closures below, but only committed to self inside the try 

5071 # that decrements it, so a raise in between can't leave the counter incremented. 

5072 context_level = self.context_level + 1 

5073 

5074 def add_hook_to_point( 

5075 hook_point: HookPoint, 

5076 hook_fn: Callable, 

5077 name: str, 

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

5079 *, 

5080 is_explicit: bool = True, 

5081 ): 

5082 if is_explicit: 5082 ↛ 5084line 5082 didn't jump to line 5084 because the condition on line 5082 was always true

5083 self.check_hooks_to_add(name) 

5084 elif self._gated_hook_reason(name) is not None: 

5085 warnings.warn( 

5086 f"hooks(): filter matched gated-off hook name '{name}', skipped. " 

5087 "Call the relevant set_use_*(True) setter first to enable it.", 

5088 stacklevel=2, 

5089 ) 

5090 return 

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

5092 alias_names_list: list[str] = [] 

5093 if hook_point.name is not None: 

5094 alias_names_list.append(hook_point.name) 

5095 alias_names_list.append(name) 

5096 hook_point.add_hook( 

5097 hook_fn, dir=dir, alias_names=alias_names_list, level=context_level 

5098 ) 

5099 else: 

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

5101 added_hooks.append((hook_point, dir)) 

5102 

5103 def apply_hooks(hooks: List[Tuple[Union[str, Callable], Callable]], is_fwd: bool): 

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

5105 aliases = build_alias_to_canonical_map(self.hook_dict) 

5106 for hook_name_or_filter, hook_fn in hooks: 

5107 if isinstance(hook_name_or_filter, str): 5107 ↛ 5121line 5107 didn't jump to line 5121 because the condition on line 5107 was always true

5108 hook_dict = self.hook_dict 

5109 actual_hook_name = hook_name_or_filter 

5110 if hook_name_or_filter in aliases: 

5111 actual_hook_name = aliases[hook_name_or_filter] 

5112 if actual_hook_name in hook_dict: 5112 ↛ 5106line 5112 didn't jump to line 5106 because the condition on line 5112 was always true

5113 add_hook_to_point( 

5114 hook_dict[actual_hook_name], 

5115 hook_fn, 

5116 actual_hook_name, 

5117 direction, 

5118 is_explicit=True, 

5119 ) 

5120 else: 

5121 hook_dict = self.hook_dict 

5122 seen_hooks = set() 

5123 for name, hook_point in hook_dict.items(): 

5124 if hook_name_or_filter(name): 

5125 hook_id = id(hook_point) 

5126 if hook_id in seen_hooks: 

5127 continue 

5128 seen_hooks.add(hook_id) 

5129 hook_name_to_use = hook_point.name if hook_point.name else name 

5130 add_hook_to_point( 

5131 hook_point, 

5132 hook_fn, 

5133 hook_name_to_use, 

5134 direction, 

5135 is_explicit=False, 

5136 ) 

5137 

5138 try: 

5139 self.context_level = context_level 

5140 apply_hooks(fwd_hooks, True) 

5141 apply_hooks(bwd_hooks, False) 

5142 yield self 

5143 finally: 

5144 if reset_hooks_end: 5144 ↛ 5151line 5144 didn't jump to line 5151 because the condition on line 5144 was always true

5145 for hook_point, direction in added_hooks: 

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

5147 # attached before the context survive. 

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

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

5150 hook_point.clear_context() 

5151 self.context_level -= 1 

5152 

5153 return _hooks_context() 

5154 

5155 def set_use_attn_result(self, use_attn_result: bool): 

5156 """Toggle whether to explicitly calculate and expose the result for each attention head. 

5157 

5158 Useful for interpretability but can easily burn through GPU memory. 

5159 """ 

5160 if use_attn_result: 

5161 self._validate_attention_fork_supported("use_attn_result") 

5162 self.cfg._set_bridge_managed_hook_flag("use_attn_result", use_attn_result) 

5163 self._propagate_attention_flag("use_attn_result", use_attn_result) 

5164 

5165 def set_use_split_qkv_input(self, use_split_qkv_input: bool): 

5166 """Toggle independent residual copies for Q/K/V so each path can be patched alone. 

5167 

5168 Mutually exclusive with `use_attn_in` — set that flag off first if it's on. 

5169 """ 

5170 if use_split_qkv_input: 

5171 if bool(getattr(self.cfg, "use_attn_in", False)): 

5172 raise ValueError( 

5173 "use_split_qkv_input and use_attn_in are mutually exclusive. " 

5174 "Call set_use_attn_in(False) before enabling use_split_qkv_input." 

5175 ) 

5176 self._validate_attention_fork_supported("use_split_qkv_input") 

5177 self.cfg._set_bridge_managed_hook_flag("use_split_qkv_input", use_split_qkv_input) 

5178 self._propagate_attention_flag("use_split_qkv_input", use_split_qkv_input) 

5179 

5180 def set_use_attn_in(self, use_attn_in: bool): 

5181 """Toggle a single 4D residual copy feeding all three Q/K/V projections. 

5182 

5183 Mutually exclusive with `use_split_qkv_input` — set that flag off first 

5184 if it's on. When on, `hook_attn_in` fires at 

5185 `[batch, pos, n_heads, d_model]`, enabling coarse-grained interventions 

5186 on the residual-stream copy shared across Q/K/V. 

5187 """ 

5188 if use_attn_in: 

5189 if bool(getattr(self.cfg, "use_split_qkv_input", False)): 

5190 raise ValueError( 

5191 "use_attn_in and use_split_qkv_input are mutually exclusive. " 

5192 "Call set_use_split_qkv_input(False) before enabling use_attn_in." 

5193 ) 

5194 self._validate_attention_fork_supported("use_attn_in") 

5195 self.cfg._set_bridge_managed_hook_flag("use_attn_in", use_attn_in) 

5196 self._propagate_attention_flag("use_attn_in", use_attn_in) 

5197 

5198 def set_use_hook_mlp_in(self, use_hook_mlp_in: bool) -> None: 

5199 """Toggle the ``hook_mlp_in`` HookPoint (the MLP-branch entry: pre-ln2, or 

5200 the MLP input on post-norm blocks), matching legacy semantics. 

5201 

5202 See :py:meth:`HookedTransformer.set_use_hook_mlp_in`. 

5203 """ 

5204 self.cfg._set_bridge_managed_hook_flag("use_hook_mlp_in", use_hook_mlp_in) 

5205 if not hasattr(self, "blocks"): 5205 ↛ 5206line 5205 didn't jump to line 5206 because the condition on line 5205 was never true

5206 return 

5207 for block in self.blocks: 

5208 block_cfg = getattr(block, "config", None) 

5209 if block_cfg is not None and block_cfg is not self.cfg: 

5210 try: 

5211 self._write_propagated_hook_flag(block_cfg, "use_hook_mlp_in", use_hook_mlp_in) 

5212 except (AttributeError, TypeError): 

5213 pass 

5214 block._use_hook_mlp_in = use_hook_mlp_in 

5215 

5216 @staticmethod 

5217 def _write_propagated_hook_flag(config: Any, flag_name: str, value: bool) -> None: 

5218 """Write a cloned config flag without dispatching through its live Bridge.""" 

5219 if isinstance(config, TransformerBridgeConfig): 5219 ↛ 5222line 5219 didn't jump to line 5222 because the condition on line 5219 was always true

5220 config._set_bridge_managed_hook_flag(flag_name, value) 

5221 else: 

5222 object.__setattr__(config, flag_name, value) 

5223 

5224 def _propagate_attention_flag(self, flag_name: str, value: bool) -> None: 

5225 """Mirror `bridge.cfg.<flag>` onto every block's attention config. 

5226 

5227 Some adapters (Llama family) deep-copy the block template during 

5228 `setup_blocks_bridge`, cloning the attention bridge's config along 

5229 with it. Others (Pythia, GPT-2) override `__deepcopy__` to share the 

5230 config. Setting the flag only on `self.cfg` silently misses the 

5231 cloned-config case. Propagating explicitly keeps both patterns 

5232 honest — a no-op when configs are shared, a correctness fix when 

5233 they aren't. 

5234 """ 

5235 if not hasattr(self, "blocks"): 5235 ↛ 5236line 5235 didn't jump to line 5236 because the condition on line 5235 was never true

5236 return 

5237 for block in self.blocks: 

5238 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

5239 if attn is None: 5239 ↛ 5240line 5239 didn't jump to line 5240 because the condition on line 5239 was never true

5240 continue 

5241 attn_cfg = getattr(attn, "config", None) 

5242 if attn_cfg is not None and attn_cfg is not self.cfg: 

5243 try: 

5244 self._write_propagated_hook_flag(attn_cfg, flag_name, value) 

5245 except (AttributeError, TypeError): 

5246 # Some config-like objects reject attributes even when 

5247 # bypassing their custom __setattr__ implementation. 

5248 pass 

5249 

5250 def _validate_attention_fork_supported(self, flag_name: str) -> None: 

5251 """Raise / warn if the model can't honor a fine-grained attention flag. 

5252 

5253 The post-ln1 fork path lives on JointQKVAttentionBridge and 

5254 PositionEmbeddingsAttentionBridge. Plain AttentionBridge delegates to 

5255 HF and exposes no fork point; we raise rather than setting the flag 

5256 silently. For hybrid models (some attention layers, some not), we warn 

5257 and list which layers will honor the flag. 

5258 """ 

5259 # Deferred imports: tight circular dependency with bridge setup. 

5260 from transformer_lens.model_bridge.generalized_components.joint_qkv_attention import ( 

5261 JointQKVAttentionBridge, 

5262 ) 

5263 from transformer_lens.model_bridge.generalized_components.position_embeddings_attention import ( 

5264 PositionEmbeddingsAttentionBridge, 

5265 ) 

5266 

5267 if not hasattr(self, "blocks"): 5267 ↛ 5268line 5267 didn't jump to line 5268 because the condition on line 5267 was never true

5268 raise NotImplementedError( 

5269 f"{flag_name}: this bridge has no `blocks` attribute, so no " 

5270 "attention bridges to apply the flag to." 

5271 ) 

5272 supported_classes = (JointQKVAttentionBridge, PositionEmbeddingsAttentionBridge) 

5273 supporting_layers: list[int] = [] 

5274 attn_classes: set[str] = set() 

5275 total_with_attn = 0 

5276 for idx, block in enumerate(self.blocks): 

5277 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

5278 if attn is None: 5278 ↛ 5279line 5278 didn't jump to line 5279 because the condition on line 5278 was never true

5279 continue 

5280 total_with_attn += 1 

5281 attn_classes.add(type(attn).__name__) 

5282 supports_flag = ( 

5283 bool(getattr(attn, "supports_attn_result", False)) 

5284 if flag_name == "use_attn_result" 

5285 else isinstance(attn, supported_classes) 

5286 ) 

5287 if supports_flag: 

5288 supporting_layers.append(idx) 

5289 if total_with_attn == 0: 5289 ↛ 5290line 5289 didn't jump to line 5290 because the condition on line 5289 was never true

5290 raise NotImplementedError(f"{flag_name}: no attention bridges found on self.blocks.") 

5291 if not supporting_layers: 

5292 if flag_name == "use_attn_result": 

5293 capability_detail = "Per-head result computation is unavailable." 

5294 else: 

5295 capability_detail = "No hook point is available before the Q/K/V projections." 

5296 raise NotImplementedError( 

5297 f"{flag_name}: none of this model's attention bridges support " 

5298 "the requested fine-grained attention hook. Found attention classes: " 

5299 f"{sorted(attn_classes)}. Supported classes: " 

5300 f"{[c.__name__ for c in supported_classes]}. {capability_detail}" 

5301 ) 

5302 if len(supporting_layers) < total_with_attn: 5302 ↛ 5303line 5302 didn't jump to line 5303 because the condition on line 5302 was never true

5303 skipped = total_with_attn - len(supporting_layers) 

5304 warnings.warn( 

5305 f"{flag_name}: {skipped} of {total_with_attn} attention layers " 

5306 "use an attention-bridge class that cannot honor this flag " 

5307 f"(attention classes present: {sorted(attn_classes)}). " 

5308 f"The flag will affect layers: {supporting_layers}.", 

5309 stacklevel=3, 

5310 ) 

5311 

5312 def _is_valid_bridge_path(self, hf_path: str) -> bool: 

5313 """Check if a HuggingFace path corresponds to a valid bridge component. 

5314 

5315 This validates that the path follows the bridge component structure and doesn't 

5316 contain nested HuggingFace components that should have been wrapped. 

5317 

5318 Args: 

5319 hf_path: HuggingFace path after removing _original_component 

5320 

5321 Returns: 

5322 True if the path is valid, False if it contains nested HF components 

5323 """ 

5324 # Split the path into parts 

5325 parts = hf_path.split(".") 

5326 

5327 # Get the component mapping for validation 

5328 component_mapping = self.adapter.component_mapping 

5329 if not component_mapping: 5329 ↛ 5330line 5329 didn't jump to line 5330 because the condition on line 5329 was never true

5330 return True # If no mapping, accept all keys 

5331 

5332 # Walk through the path and check if each level is a registered bridge component 

5333 # For example, transformer.h.0.mlp.in.weight should be valid 

5334 # but transformer.h.0.mlp.c_fc.weight should be invalid (c_fc is nested HF component) 

5335 

5336 # Start from the root 

5337 current_component = None 

5338 idx = 0 

5339 

5340 # Find which top-level component this belongs to 

5341 for tl_name, component in component_mapping.items(): 

5342 if component.name and hf_path.startswith(component.name + "."): 

5343 current_component = component 

5344 # Skip past the HF prefix 

5345 remaining_path = hf_path[len(component.name) + 1 :] 

5346 parts = remaining_path.split(".") 

5347 idx = 0 

5348 break 

5349 

5350 if current_component is None: 

5351 return True # Path doesn't match any component, let it through 

5352 

5353 # Special handling for blocks 

5354 if hasattr(current_component, "is_list_item") and current_component.is_list_item: 

5355 # Skip the layer index 

5356 if idx < len(parts) and parts[idx].isdigit(): 5356 ↛ 5360line 5356 didn't jump to line 5360 because the condition on line 5356 was always true

5357 idx += 1 

5358 

5359 # Now validate the rest of the path against submodules 

5360 while idx < len(parts): 5360 ↛ 5387line 5360 didn't jump to line 5387 because the condition on line 5360 was always true

5361 part = parts[idx] 

5362 

5363 # If we hit 'weight' or 'bias', we're at a parameter - this is valid 

5364 if part in ("weight", "bias"): 

5365 return True 

5366 

5367 # Check if this part is a registered submodule 

5368 if hasattr(current_component, "submodules") and current_component.submodules: 

5369 if part in current_component.submodules: 

5370 current_component = current_component.submodules[part] 

5371 idx += 1 

5372 continue 

5373 else: 

5374 # This part is not a registered bridge component 

5375 # It's likely a nested HF component (like c_fc, c_proj, c_attn) 

5376 return False 

5377 else: 

5378 # No submodules to check, but not at a parameter yet 

5379 # Check if next is weight/bias 

5380 if idx + 1 < len(parts) and parts[idx + 1] in ("weight", "bias"): 

5381 return True 

5382 # Otherwise this is likely a nested HF component 

5383 return False 

5384 

5385 idx += 1 

5386 

5387 return True 

5388 

5389 def _normalize_bridge_key_to_hf(self, key: str) -> str: 

5390 """Normalize a key that uses bridge attribute names to use HF module names. 

5391 

5392 PyTorch's state_dict uses the Python attribute names (e.g., 'ln1') 

5393 but the conversion logic expects HF module names (e.g., 'ln_1'). This 

5394 function only replaces non-nested component names, leaving bridge 

5395 subcomponents (like 'in', 'out', 'q', 'k', 'v') unchanged since they're 

5396 handled by the component structure. 

5397 

5398 Args: 

5399 key: Key that may use bridge attribute names 

5400 

5401 Returns: 

5402 Key with attribute names replaced by module names where needed 

5403 """ 

5404 component_mapping = self.adapter.component_mapping 

5405 if not component_mapping: 5405 ↛ 5406line 5405 didn't jump to line 5406 because the condition on line 5405 was never true

5406 return key 

5407 

5408 # Build a mapping of only the direct module attribute names to HF names 

5409 # We only care about top-level and block-level component names, NOT subcomponents 

5410 attr_to_hf = {} 

5411 

5412 # Map top-level components 

5413 block_list_names = {"blocks", "L_blocks", "H_blocks", "encoder_blocks", "decoder_blocks"} 

5414 for tl_name, component in component_mapping.items(): 

5415 if component.name and tl_name not in block_list_names: 

5416 # Skip if TL name is already a segment of its HF path (avoids doubling). 

5417 if tl_name != component.name and tl_name not in component.name.split("."): 

5418 attr_to_hf[tl_name] = component.name 

5419 

5420 # Map block-level components (ln1, ln2, attn, mlp) for all block lists 

5421 for bl_name in block_list_names: 

5422 blocks_component = component_mapping.get(bl_name) 

5423 if blocks_component and hasattr(blocks_component, "submodules"): 

5424 for tl_subname, subcomponent in blocks_component.submodules.items(): 

5425 if subcomponent.name: 

5426 # Only map if the names differ (e.g., ln1 -> ln_1, but attn -> attn) 

5427 if tl_subname != subcomponent.name: 

5428 attr_to_hf[tl_subname] = subcomponent.name 

5429 

5430 # Replace only these specific attribute names in the key 

5431 # We need to be careful to only replace whole path components, not substrings 

5432 parts = key.split(".") 

5433 result_parts = [] 

5434 

5435 for part in parts: 

5436 if part in attr_to_hf: 

5437 result_parts.append(attr_to_hf[part]) 

5438 else: 

5439 result_parts.append(part) 

5440 

5441 return ".".join(result_parts) 

5442 

5443 def state_dict(self, destination=None, prefix="", keep_vars=False): 

5444 """Get state dict with TransformerLens format keys. 

5445 

5446 Converts HuggingFace format keys to TransformerLens format and filters out 

5447 _original_component references and nested HuggingFace components. 

5448 

5449 A direct no-argument call returns a clean state dict with bridge component 

5450 paths converted to TL format. Calls that supply ``destination`` or 

5451 ``prefix`` use standard ``nn.Module`` recursive semantics so a Bridge can 

5452 compose inside a parent module. 

5453 

5454 Args: 

5455 destination: Optional dict to store state dict in 

5456 prefix: Optional prefix to add to all keys 

5457 keep_vars: Whether to keep variables as Variables instead of tensors 

5458 

5459 Returns: 

5460 Direct calls return TransformerLens-format keys; recursive calls 

5461 return the supplied destination with standard module-tree keys. 

5462 """ 

5463 if destination is not None or prefix: 

5464 return super().state_dict( 

5465 destination=destination, 

5466 prefix=prefix, 

5467 keep_vars=keep_vars, 

5468 ) 

5469 

5470 raw_state_dict = self.original_model.state_dict(keep_vars=keep_vars) 

5471 

5472 # Clean _original_component references and convert to TL format 

5473 # Also filter out nested HuggingFace components that are wrapped by bridge components 

5474 tl_state_dict = {} 

5475 

5476 for key, value in raw_state_dict.items(): 

5477 # Skip _original_component keys 

5478 if key == "_original_component" or key.startswith("_original_component."): 5478 ↛ 5479line 5478 didn't jump to line 5479 because the condition on line 5478 was never true

5479 continue 

5480 

5481 # Remove all _original_component from the key 

5482 clean_key = key.replace("._original_component", "") 

5483 

5484 # Check if this is a valid bridge path (not a nested HF component) 

5485 if not self._is_valid_bridge_path(clean_key): 

5486 continue 

5487 

5488 # Normalize bridge component names to HF names for conversion 

5489 # (e.g., 'ln1' -> 'ln_1', 'mlp.in' -> 'mlp.c_fc') 

5490 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

5491 

5492 # Convert to TL format - this uses the adapter's component_mapping 

5493 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

5494 

5495 # Only add if we haven't seen this TL key yet (handles duplicates) 

5496 if tl_key not in tl_state_dict: 

5497 tl_state_dict[tl_key] = value 

5498 

5499 return tl_state_dict 

5500 

5501 def _tl_key_to_actual_keys(self) -> dict[str, list[str]]: 

5502 """Inverse of the renaming state_dict() applies: map each TL-format key 

5503 back to every raw parameter/buffer path that represents it. 

5504 

5505 Mirrors the filtering and key-conversion in state_dict() exactly, except 

5506 it keeps every raw key for a given TL key instead of only the first-seen 

5507 one. Bridge components frequently expose the same underlying parameter 

5508 through more than one attribute path (e.g. GPT-2's split q/k/v weights 

5509 are views into the wrapped module's combined c_attn weight, reachable 

5510 both via a block-level shortcut and via the nested _original_component 

5511 chain) - all of those aliases must be written for the round trip to 

5512 actually change what forward() reads, not just what state_dict() shows. 

5513 """ 

5514 mapping: dict[str, list[str]] = {} 

5515 for actual_key in self.original_model.state_dict(): 

5516 if actual_key == "_original_component" or actual_key.startswith("_original_component."): 5516 ↛ 5517line 5516 didn't jump to line 5517 because the condition on line 5516 was never true

5517 continue 

5518 clean_key = actual_key.replace("._original_component", "") 

5519 if not self._is_valid_bridge_path(clean_key): 

5520 continue 

5521 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

5522 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

5523 mapping.setdefault(tl_key, []).append(actual_key) 

5524 return mapping 

5525 

5526 def load_state_dict(self, state_dict, strict=True, assign=False): 

5527 """Load state dict into the model, handling both clean keys and original keys with _original_component references. 

5528 

5529 Accepts three key formats: TL-format keys as emitted by state_dict() 

5530 (e.g. "blocks.0.attn.q.weight"), raw native parameter paths (e.g. for 

5531 ``boot_native`` / tracr-style loading), and raw paths with 

5532 "_original_component" segments stripped. 

5533 

5534 Args: 

5535 state_dict: Dictionary containing a whole state of the module 

5536 strict: Whether to strictly enforce that the keys in state_dict match the keys returned by this module's state_dict() function 

5537 assign: Whether to assign items in the state dictionary to their corresponding keys in the module instead of copying them 

5538 

5539 Returns: 

5540 NamedTuple with missing_keys and unexpected_keys fields 

5541 """ 

5542 current_state_dict = self.original_model.state_dict() 

5543 clean_to_actual = {} 

5544 for actual_key in current_state_dict.keys(): 

5545 if actual_key != "_original_component": 5545 ↛ 5544line 5545 didn't jump to line 5544 because the condition on line 5545 was always true

5546 clean_to_actual[actual_key.replace("._original_component", "")] = actual_key 

5547 

5548 tl_to_actual = self._tl_key_to_actual_keys() 

5549 

5550 mapped_state_dict = {} 

5551 unexpected_keys = [] 

5552 for input_key, value in state_dict.items(): 

5553 if input_key in current_state_dict: 

5554 mapped_state_dict[input_key] = value 

5555 elif input_key in clean_to_actual: 

5556 mapped_state_dict[clean_to_actual[input_key]] = value 

5557 elif input_key in tl_to_actual: 

5558 for actual_key in tl_to_actual[input_key]: 

5559 mapped_state_dict[actual_key] = value 

5560 else: 

5561 unexpected_keys.append(input_key) 

5562 

5563 # A TL key's actual-key aliases share the same underlying storage (see 

5564 # _tl_key_to_actual_keys), so writing any one of them already updates 

5565 # what forward() reads for all of them. Treat the group as satisfied 

5566 # if any alias was written -- e.g. a caller supplying clean/raw keys 

5567 # (the branch above maps each clean key to exactly one actual key) 

5568 # shouldn't have the *other*, unwritten aliases reported as missing. 

5569 missing_keys = sorted( 

5570 actual_key 

5571 for actual_keys in tl_to_actual.values() 

5572 if not any(k in mapped_state_dict for k in actual_keys) 

5573 for actual_key in actual_keys 

5574 ) 

5575 

5576 if strict and (missing_keys or unexpected_keys): 

5577 error_msgs = [] 

5578 if unexpected_keys: 

5579 error_msgs.append( 

5580 "Unexpected key(s) in state_dict: " 

5581 + ", ".join(f'"{k}"' for k in sorted(unexpected_keys)) 

5582 ) 

5583 if missing_keys: 

5584 error_msgs.append( 

5585 "Missing key(s) in state_dict: " + ", ".join(f'"{k}"' for k in missing_keys) 

5586 ) 

5587 raise RuntimeError( 

5588 "Error(s) in loading state_dict for {}:\n\t{}".format( 

5589 type(self.original_model).__name__, "\n\t".join(error_msgs) 

5590 ) 

5591 ) 

5592 

5593 result = self.original_model.load_state_dict(mapped_state_dict, strict=False, assign=assign) 

5594 if assign: 

5595 refresh_container_state_owners(self) 

5596 return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys) 

5597 

5598 def get_params(self): 

5599 """Access to model parameters in the format expected by SVDInterpreter. 

5600 

5601 For missing weights, returns zero tensors of appropriate shape instead of raising exceptions. 

5602 This ensures compatibility across different model architectures. 

5603 

5604 Returns: 

5605 dict: Dictionary of parameter tensors with TransformerLens naming convention 

5606 

5607 Raises: 

5608 ValueError: If configuration is inconsistent (e.g., cfg.n_layers != len(blocks)) 

5609 """ 

5610 return get_bridge_params(self) 

5611 

5612 # NOTE: list_supported_models and check_model_support are attached to this class 

5613 # dynamically by transformer_lens.model_bridge.sources.transformers module. 

5614 # These are HuggingFace-specific methods that belong in the transformers source module.