Coverage for transformer_lens/model_bridge/transformer_bridge.py: 87%

1839 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +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 typing import ( 

13 TYPE_CHECKING, 

14 Any, 

15 Callable, 

16 ClassVar, 

17 Dict, 

18 Iterator, 

19 List, 

20 Literal, 

21 Optional, 

22 Tuple, 

23 Union, 

24 cast, 

25) 

26 

27import einops 

28import numpy as np 

29import torch 

30import tqdm 

31from torch import nn 

32from torch.nn import functional as F 

33from transformers.tokenization_utils_base import PreTrainedTokenizerBase 

34 

35from transformer_lens import utilities as utils 

36from transformer_lens.ActivationCache import ActivationCache 

37from transformer_lens.config import TransformerBridgeConfig 

38from transformer_lens.FactoredMatrix import FactoredMatrix 

39from transformer_lens.hook_points import HookIntrospectionMixin, HookPoint 

40from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

41from transformer_lens.model_bridge.bridge_core import ( 

42 _BLOCK_LIST_ATTRS, 

43 _SELF_ATTENTION_NAMES, 

44 BridgeCore, 

45) 

46from transformer_lens.model_bridge.component_setup import ( 

47 refresh_container_state_owners, 

48 set_original_components, 

49) 

50from transformer_lens.model_bridge.composition_scores import CompositionScores 

51from transformer_lens.model_bridge.driver_protocol import ( 

52 TensorLike, 

53 to_torch, 

54 validate_driver, 

55) 

56from transformer_lens.model_bridge.exceptions import StopAtLayerException 

57from transformer_lens.model_bridge.generalized_components.base import ( 

58 GeneralizedComponent, 

59) 

60from transformer_lens.model_bridge.generalized_components.block import ( 

61 _BLOCK_INTERNAL_MODULES, 

62 _NORM_PREFIXES, 

63 _VARIANT_SUBMODULE_SET, 

64 VARIANT_SUBMODULE_NAMES, 

65) 

66from transformer_lens.model_bridge.generalized_components.moe import ( 

67 fold_scale_into_moe_block, 

68 has_batched_experts, 

69) 

70from transformer_lens.model_bridge.get_params_util import get_bridge_params 

71from transformer_lens.utilities.activation_functions import softcap_enabled 

72from transformer_lens.utilities.devices import move_to_and_update_config 

73from transformer_lens.utilities.quantization import require_readable_weight 

74 

75if TYPE_CHECKING: 

76 pass 

77 

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

79 

80 

81def _resolve_attr_path(obj: nn.Module, attr_path: str) -> Optional[torch.Tensor]: 

82 """Walk a dot-separated attribute path and return the final tensor (None if bias-free).""" 

83 result = obj 

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

85 result = getattr(result, attr) 

86 return cast(torch.Tensor, result) 

87 

88 

89def _storage_group_keys(state_dict: dict[str, torch.Tensor]) -> set[str]: 

90 """Keys whose current tensor shares underlying storage with another key's. 

91 

92 Covers every direction of the desync #1637 reported: a partial view onto 

93 a combined weight (e.g. a split QKV/gate-up component's ``torch.tensor_split`` 

94 view into ``c_attn``/``gate_up_proj``), the combined weight itself (writing 

95 it directly would silently orphan the views that share its storage), and a 

96 same-size tied pair (e.g. tied embed/unembed weights, #1725) -- none of 

97 which reliably show up via ``Tensor._base``, since wrapping in 

98 ``nn.Parameter`` doesn't preserve that tracking here. 

99 

100 Meta tensors are excluded from the comparison: every meta tensor reports 

101 ``untyped_storage().data_ptr() == 0`` (it has no real backing memory), so 

102 comparing meta tensors by data pointer would spuriously group every 

103 unrelated offloaded parameter in the model together. A key whose current 

104 target is meta falls through to ordinary ``assign=True`` handling instead 

105 (the standard way to materialize a meta tensor from a real one). 

106 """ 

107 by_storage: dict[int, list[str]] = {} 

108 for key, tensor in state_dict.items(): 

109 if tensor.is_meta: 

110 continue 

111 by_storage.setdefault(tensor.untyped_storage().data_ptr(), []).append(key) 

112 return {key for keys in by_storage.values() if len(keys) > 1 for key in keys} 

113 

114 

115class TransformerBridge(BridgeCore, HookIntrospectionMixin, nn.Module): 

116 """Torch-backed bridge: HF, vLLM-via-torch, anything that wraps an ``nn.Module``. 

117 

118 Provides a standardized interface to access components of a transformer 

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

120 to map between the TransformerLens and HuggingFace model structures. 

121 

122 Stateless reparametrization is unsupported 

123 ------------------------------------------ 

124 

125 ``torch.func.functional_call`` (and ``torch.nn.utils.stateless``) fails to 

126 restore parameters through this tree: each replaced component is registered 

127 both inside the wrapped HF model and as a bridge submodule, and torch's 

128 tied-weight handling double-swaps the shared slot, leaving the override 

129 installed. For temporary weight edits use 

130 :func:`transformer_lens.utilities.temporarily_swap_parameter`. 

131 

132 Tokenization notes 

133 ------------------ 

134 

135 :meth:`to_tokens`, :meth:`to_str_tokens`, :meth:`get_token_position`, 

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

137 to control BOS prepending. Resolution: explicit arg → 

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

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

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

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

142 

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

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

145 the bridge adds or strips manually as needed. When 

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

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

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

149 bridge-specific. 

150 

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

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

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

154 """ 

155 

156 # hook_aliases inherited from BridgeCore 

157 

158 def __init__( 

159 self, 

160 model: nn.Module, 

161 adapter: ArchitectureAdapter, 

162 tokenizer: Any, 

163 *, 

164 driver: Any = None, 

165 ): 

166 """Initialize the bridge. 

167 

168 Args: 

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

170 adapter: The architecture adapter to use 

171 tokenizer: The tokenizer to use (required) 

172 driver: Optional pre-built :class:`Driver`. Sources that construct 

173 exotic drivers (vLLM, Inspect) pass them here. When ``None``, 

174 a :class:`TransformersDriver` is built from the supplied 

175 ``model``/``adapter``/``tokenizer`` — kept for backward 

176 compatibility with direct ``TransformerBridge(...)`` callers. 

177 """ 

178 nn.Module.__init__(self) 

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

180 self.__dict__["original_model"] = model 

181 # Production sources construct their Driver and pass it via ``driver=``. 

182 # The fallback covers tests / direct callers with a hand-rolled triple. 

183 if driver is None: 

184 from transformer_lens.model_bridge.sources.transformers_driver import ( 

185 TransformersDriver, 

186 ) 

187 

188 driver = TransformersDriver(model, adapter, tokenizer) 

189 BridgeCore.__init__(self, adapter, tokenizer, driver) 

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

191 # for list components, actual_instance is a list of instances. 

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

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

194 try: 

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

196 except StopIteration: 

197 self.cfg.device = "cpu" 

198 set_original_components(self, self.adapter, self.__dict__["original_model"]) 

199 self._initialize_hook_registry() 

200 self._register_aliases() 

201 self._register_all_aliases_recursive() 

202 # Re-scan after alias registration so alias names (hook_resid_pre, …) 

203 # join the registry alongside their canonical targets. Shared HookPoint 

204 # instances, so no double-firing. 

205 self._scan_existing_hooks(self, "") 

206 self._setup_hook_compatibility() 

207 # Backfill supported_hook_points = registry − non_fireable. Whitelist- 

208 # semantic drivers (Inspect) declared their own non-empty set and skip. 

209 if not self._driver.supported_hook_points: 

210 self._driver.supported_hook_points = ( 

211 frozenset(self._hook_registry) - self._driver.non_fireable_hook_points 

212 ) 

213 # Fail fast on a misshapen driver here, not at first capture. 

214 validate_driver(self._driver, after_bridge_construction=True) 

215 self.processor = None 

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

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

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

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

220 model.train(model.training) 

221 self.train(model.training) 

222 self.cfg._bind_bridge(self) 

223 

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

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

226 super().__setstate__(state) 

227 self.cfg._bind_bridge(self) 

228 

229 # boot_transformers / list_supported_models / check_model_support are 

230 # attached by sources.transformers.__init__ (setattr on this class) so the 

231 # source package owns its own boot entry point. 

232 boot_transformers: ClassVar[Callable[..., "TransformerBridge"]] 

233 

234 @property 

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

236 """The wrapped ``nn.Module``. Raises :class:`AttributeError` for 

237 non-torch drivers (vLLM, Inspect) that don't expose a local module.""" 

238 driver = getattr(self, "_driver", None) 

239 if driver is None: 

240 # Bridges assembled without __init__ (object.__new__ scaffolds) keep the 

241 # module in the __dict__ mirror the setter maintains. 

242 model = self.__dict__.get("original_model") 

243 if model is None: 

244 raise AttributeError(f"'{type(self).__name__}' has no driver and no original_model") 

245 return model 

246 underlying = getattr(driver, "underlying_model", None) 

247 if underlying is None: 

248 raise AttributeError( 

249 f"{type(driver).__name__} does not expose an nn.Module — " 

250 "non-torch drivers (vLLM, Inspect) operate without a local module." 

251 ) 

252 return underlying 

253 

254 @original_model.setter 

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

256 """Used by weight-processing paths that move the model across devices.""" 

257 self.__dict__["original_model"] = value 

258 # Sync via the driver's public API; non-torch drivers don't implement it. 

259 setter = getattr(getattr(self, "_driver", None), "set_underlying_model", None) 

260 if callable(setter): 

261 setter(value) 

262 

263 def init_weights(self) -> None: 

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

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

266 initialize_native_model, 

267 ) 

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

269 

270 model = self.original_model 

271 if not isinstance(model, NativeModel): 

272 raise RuntimeError( 

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

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

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

276 ) 

277 initialize_native_model(model, self.cfg) 

278 

279 def _set_processed_weight_attributes(self) -> None: 

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

281 

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

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

284 - _processed_W_Q 

285 - _processed_W_K 

286 - _processed_W_V 

287 - _processed_b_Q 

288 - _processed_b_K 

289 - _processed_b_V 

290 

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

292 HookedTransformer compatibility while keeping 2D format for calculations. 

293 """ 

294 

295 n_heads = self.cfg.n_heads 

296 d_head = self.cfg.d_head 

297 d_model = self.cfg.d_model 

298 blocks_iter = [] 

299 for bl_name in _BLOCK_LIST_ATTRS: 

300 if hasattr(self, bl_name): 

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

302 if not blocks_iter: 

303 return 

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

305 if "attn" not in block._modules: 

306 continue 

307 attn = block.attn 

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

309 continue 

310 try: 

311 w_q_2d = attn.q.weight.data 

312 w_k_2d = attn.k.weight.data 

313 w_v_2d = attn.v.weight.data 

314 attn._processed_W_Q = einops.rearrange( 

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

316 ) 

317 attn._processed_W_K = einops.rearrange( 

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

319 ) 

320 attn._processed_W_V = einops.rearrange( 

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

322 ) 

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

324 b_q_2d = attn.q.bias.data 

325 b_k_2d = attn.k.bias.data 

326 b_v_2d = attn.v.bias.data 

327 attn._processed_b_Q = einops.rearrange( 

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

329 ) 

330 attn._processed_b_K = einops.rearrange( 

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

332 ) 

333 attn._processed_b_V = einops.rearrange( 

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

335 ) 

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

337 w_o_2d = attn.o.weight.data 

338 w_o_transposed = w_o_2d.T 

339 attn._processed_W_O = einops.rearrange( 

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

341 ) 

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

343 attn._processed_b_O = attn.o.bias.data 

344 except Exception: 

345 pass 

346 

347 def _register_all_aliases_recursive(self) -> None: 

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

349 

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

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

352 """ 

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

354 self._register_aliases() 

355 for module in self.modules(): 

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

357 getattr(module, "_register_aliases")() 

358 

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

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

361 # nn.Module.__setattr__ claims Module values before any data descriptor runs, so the 

362 # original_model property setter would never fire. object.__setattr__ invokes it: 

363 # a registered copy aliases every HF weight into state_dict and makes .to() move twice. 

364 if name == "original_model": 

365 object.__setattr__(self, name, value) 

366 return 

367 super().__setattr__(name, value) 

368 if isinstance(value, HookPoint): 

369 value.name = name 

370 self._hook_registry[name] = value 

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

372 component_hooks = value.get_hooks() 

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

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

375 hook.name = full_name 

376 self._hook_registry[full_name] = hook 

377 

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

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

380 visited = set() 

381 # Protect canonical HookPoint names from alias overwrites. Seeded with 

382 # already-registered hooks so the post-alias-registration re-scan adds 

383 # alias registry entries without renaming shared HookPoint instances 

384 # (dir() sorts alphabetically, so block-level alias attributes like 

385 # hook_mlp_out are visited before the mlp child they point into). 

386 named_hook_ids: set = {id(hp) for hp in self._hook_registry.values() if hp.name is not None} 

387 

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

389 obj_id = id(mod) 

390 if obj_id in visited: 

391 return 

392 visited.add(obj_id) 

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

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

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

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

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

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

399 hook_id = id(hook) 

400 if hook_id not in named_hook_ids: 

401 hook.name = full_name 

402 named_hook_ids.add(hook_id) 

403 self._hook_registry[full_name] = hook 

404 for attr_name in dir(mod): 

405 if attr_name.startswith("_"): 

406 continue 

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

408 continue 

409 if attr_name in [ 

410 "OV", 

411 "QK", 

412 "W_V", 

413 "W_O", 

414 "W_Q", 

415 "W_K", 

416 "W_in", 

417 "W_gate", 

418 "W_out", 

419 "b_V", 

420 "b_O", 

421 "b_Q", 

422 "b_K", 

423 "b_in", 

424 "b_out", 

425 ]: 

426 continue 

427 try: 

428 attr = getattr(mod, attr_name) 

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

430 continue 

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

432 if isinstance(attr, HookPoint): 

433 hook_id = id(attr) 

434 if hook_id not in named_hook_ids: 

435 attr.name = name 

436 named_hook_ids.add(hook_id) 

437 self._hook_registry[name] = attr 

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

439 if ( 

440 child_name == "original_component" 

441 or child_name == "_original_component" 

442 or child_name == "original_model" 

443 ): 

444 continue 

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

446 scan_module(child_module, child_path) 

447 

448 scan_module(module, prefix) 

449 

450 @property 

451 def n_params_total(self) -> int: 

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

453 

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

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

456 are excluded, so the result can differ from 

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

458 

459 Returns: 

460 int: Parameter count of the uninstrumented wrapped model. 

461 """ 

462 return self._n_params_total 

463 

464 def _has_registered_blocks(self) -> bool: 

465 """Whether a ``blocks`` stack is registered as a submodule on this bridge. 

466 

467 Checks ``_modules`` directly rather than ``hasattr``: ``__getattr__`` falls 

468 through to the wrapped HF model, so ``hasattr(self, "blocks")`` can be True 

469 for a model that merely exposes its own ``.blocks`` attribute. 

470 """ 

471 modules = self.__dict__.get("_modules") or {} 

472 return "blocks" in modules 

473 

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

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

476 # Re-invoke original_model's property so its descriptive AttributeError 

477 # for non-torch drivers isn't shadowed by the __dict__ fallback below. 

478 if name == "original_model": 

479 prop = type(self).__dict__.get("original_model") 

480 if isinstance(prop, property) and prop.fget is not None: 

481 return prop.fget(self) 

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

483 return self.__dict__[name] 

484 # Use __dict__ directly to avoid recursion 

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

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

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

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

489 if component_mapping is not None and name in component_mapping: 

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

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

492 try: 

493 name_split = name.split(".") 

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

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

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

497 current = getattr(current, part) 

498 return current 

499 else: 

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

501 except AttributeError: 

502 pass # type: ignore[operator,assignment] 

503 # A class property whose fget raised AttributeError lands here with the 

504 # informative message discarded (CPython drops it before __getattr__). 

505 # Re-invoke the property so its own diagnostic (e.g. "bias-free 

506 # projection") surfaces instead of a generic missing-attribute error. 

507 descriptor = getattr(type(self), name, None) 

508 if isinstance(descriptor, property) and descriptor.fget is not None: 

509 descriptor.fget(self) 

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

511 

512 def __str__(self) -> str: 

513 """One-line-per-component summary of the bridge. 

514 

515 Returns: 

516 A string describing the bridge's components. 

517 """ 

518 lines = ["TransformerBridge:"] 

519 mapping = self.adapter.get_component_mapping() 

520 

521 def _describe(component_mapping, indent): 

522 pad = " " * indent 

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

524 lines.append(f"{pad}{name}: {type(component).__name__}") 

525 submodules = getattr(component, "submodules", None) 

526 if submodules: 

527 _describe(submodules, indent + 1) 

528 

529 _describe(mapping, 1) 

530 return "\n".join(lines) 

531 

532 def enable_compatibility_mode( 

533 self, 

534 disable_warnings: bool = False, 

535 no_processing: bool = False, 

536 fold_ln: bool = True, 

537 center_writing_weights: bool = True, 

538 center_unembed: bool = True, 

539 fold_value_biases: bool = True, 

540 refactor_factored_attn_matrices: bool = False, 

541 ) -> None: 

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

543 

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

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

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

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

548 

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

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

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

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

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

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

555 :py:meth:`set_use_hook_mlp_in`. 

556 

557 Args: 

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

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

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

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

562 Default: True. Ignored if no_processing=True. 

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

564 Default: True. Ignored if no_processing=True. 

565 center_unembed: Whether to center the unembedding matrix. 

566 Default: True. Ignored if no_processing=True. 

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

568 Default: True. Ignored if no_processing=True. 

569 refactor_factored_attn_matrices: Whether to refactor factored attention matrices. 

570 Default: False. Ignored if no_processing=True. 

571 """ 

572 from transformer_lens.utilities.bridge_components import ( 

573 apply_fn_to_all_components, 

574 ) 

575 

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

577 raise RuntimeError( 

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

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

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

581 ) 

582 

583 hf_device_map = getattr(self.original_model, "hf_device_map", None) 

584 if hf_device_map and not no_processing: 

585 offloaded = {k for k, v in hf_device_map.items() if str(v).lower() in ("cpu", "disk")} 

586 if offloaded: 586 ↛ 600line 586 didn't jump to line 600 because the condition on line 586 was always true

587 raise RuntimeError( 

588 "enable_compatibility_mode() with weight processing " 

589 "(fold_ln/center_writing_weights/center_unembed/fold_value_biases) is not " 

590 "supported on a bridge with an offloaded device_map " 

591 f"({sorted(offloaded)} are CPU/disk-offloaded). Weight processing reads and " 

592 "rewrites parameters directly across many components at once, not through a " 

593 "single component's own forward() call the way the default (non-compat-mode) " 

594 "bridge forward does, so it isn't covered by GeneralizedComponent's per-call " 

595 "materialization and hits raw meta tensors. Load without a CPU/disk device_map " 

596 "for compatibility mode, or call enable_compatibility_mode(no_processing=True) " 

597 "for the hook/component compatibility layer without the weight transforms." 

598 ) 

599 

600 if getattr(self.cfg, "is_audio_model", False): 

601 # Audio encoders have no text embed/unembed for the legacy weight 

602 # processing to operate on; without this guard the processing path 

603 # dies later with an opaque KeyError ('embed.weight'). 

604 raise NotImplementedError( 

605 "enable_compatibility_mode() is not supported for audio encoder models: " 

606 "the legacy weight processing (fold_ln/centering) assumes a text " 

607 "embed/unembed, which audio encoders do not have. Use the bridge's " 

608 "native hooks (run_with_cache / run_with_hooks) directly." 

609 ) 

610 

611 self.compatibility_mode = True 

612 

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

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

615 component.compatibility_mode = True 

616 component.disable_warnings = disable_warnings 

617 

618 apply_fn_to_all_components(self, set_compatibility_mode) 

619 self.clear_hook_registry() 

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

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

622 for block in self.blocks: 

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

624 block._teardown_capture_hooks() 

625 try: 

626 if not no_processing: 

627 self.process_weights( 

628 fold_ln=fold_ln, 

629 center_writing_weights=center_writing_weights, 

630 center_unembed=center_unembed, 

631 fold_value_biases=fold_value_biases, 

632 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

633 ) 

634 finally: 

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

636 self._initialize_hook_registry() 

637 self._setup_hook_compatibility() 

638 self._register_all_aliases_recursive() 

639 

640 def _setup_hook_compatibility(self) -> None: 

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

642 

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

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

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

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

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

648 

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

650 compatibility mode or weight processing is enabled. 

651 

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

653 """ 

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

655 self.adapter.setup_hook_compatibility(self) 

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

657 self.adapter.setup_no_processing_hooks(self) 

658 blocks_to_process = [] 

659 for block_list_name in ( 

660 "blocks", 

661 "encoder_blocks", 

662 "decoder_blocks", 

663 "L_blocks", 

664 "H_blocks", 

665 ): 

666 if hasattr(self, block_list_name): 

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

668 # A vision tower's layers are not a top-level block list, so they were 

669 # skipped here and their attention never had q/k/v/z reshaping set up -- 

670 # those hooks fired flat, ignoring the tower's head count. 

671 vision_encoder = getattr(self, "vision_encoder", None) 

672 vision_layers = getattr(vision_encoder, "encoder_layers", None) 

673 if vision_layers is not None: 

674 blocks_to_process.extend(vision_layers) 

675 for block in blocks_to_process: 

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

677 if hasattr(block, attn_name): 

678 attn = getattr(block, attn_name) 

679 if hasattr(attn, "setup_hook_compatibility"): 

680 attn.setup_hook_compatibility() 

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

682 attn.setup_no_processing_hooks() 

683 

684 def process_weights( 

685 self, 

686 verbose: bool = False, 

687 fold_ln: bool = True, 

688 center_writing_weights: bool = True, 

689 center_unembed: bool = True, 

690 fold_value_biases: bool = True, 

691 refactor_factored_attn_matrices: bool = False, 

692 ) -> None: 

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

694 

695 This method applies weight processing transformations to improve model interpretability 

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

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

698 

699 Args: 

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

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

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

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

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

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

706 """ 

707 # Match HookedTransformer: warn and skip rather than raise. Folding and centering 

708 # read their own factors out of the weights and neutralize them, so a repeat pass 

709 # is a no-op for every architecture — but adapters that fold a factor held outside 

710 # the weights would double-apply it, and notebook cell re-runs make that routine. 

711 if self._weights_processed: 

712 logging.warning( 

713 "process_weights was already applied to this bridge. Skipping: re-running " 

714 "it would re-apply adapter weight folds. Boot a fresh bridge with " 

715 "TransformerBridge.boot_transformers(...) to process with different options." 

716 ) 

717 return 

718 

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

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

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

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

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

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

725 # very tensors the converters had to guard. unreadable_weight_reason 

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

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

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

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

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

731 require_readable_weight( 

732 param, 

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

734 owner=self.original_model, 

735 remedy=( 

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

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

738 ), 

739 ) 

740 

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

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

743 self._weights_processed = True 

744 from transformer_lens.weight_processing import ProcessWeights 

745 

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

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

748 

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

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

751 logging.warning( 

752 "center_unembed=True is incompatible with logit softcapping " 

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

754 self.cfg.output_logits_soft_cap, 

755 ) 

756 center_unembed = False 

757 

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

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

760 state_dict = self.state_dict() 

761 adapter = self.adapter 

762 

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

764 embed_key = "embed.weight" 

765 unembed_key = "unembed.weight" 

766 

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

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

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

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

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

772 # Clone the unembed weight to break the tie 

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

774 

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

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

777 state_dict = adapter.preprocess_weights(state_dict) 

778 

779 # Use the unified ProcessWeights.process_weights() pipeline. 

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

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

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

783 state_dict = ProcessWeights.process_weights( 

784 state_dict, 

785 self.cfg, 

786 fold_ln=fold_ln, 

787 center_writing_weights=center_writing_weights, 

788 center_unembed=center_unembed, 

789 fold_value_biases=fold_value_biases, 

790 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

791 adapter=adapter, 

792 ) 

793 

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

795 import re 

796 

797 hf_to_tl_prefix = {} 

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

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

800 hf_to_tl_prefix[remote_path] = tl_name 

801 

802 normalized_state_dict = {} 

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

804 new_key = key 

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

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

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

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

809 break 

810 normalized_state_dict[new_key] = value 

811 state_dict = normalized_state_dict 

812 

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

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

815 ProcessWeights.distribute_weights_to_components( 

816 state_dict=state_dict, 

817 component_mapping=self.real_components, 

818 ) 

819 if fold_ln: 

820 self._fold_layer_norms_into_batched_experts() 

821 if adapter is not None: 821 ↛ exitline 821 didn't return from function 'process_weights' because the condition on line 821 was always true

822 adapter.postprocess_weights(self) 

823 

824 def _fold_layer_norms_into_batched_experts(self) -> None: 

825 """Fold each batched-expert MoE layer's ln2 into the weights that read it. 

826 

827 transformers stores a whole expert stack in one 3-D Parameter, which is not a 

828 ``weight``/``bias`` leaf of a declared bridge submodule and so never appears in 

829 the state dict ProcessWeights folds. Left alone those layers keep their learned 

830 FFN gains while attention and dense layers of the same model lose theirs, and 

831 the mixed basis surfaces only in DLA / logit-lens / factored-matrix reads. 

832 

833 Runs on the live model because the readers include tensors the state dict 

834 cannot carry: batched experts, and the shared-expert MLPs that Qwen2-MoE and 

835 GLM4-MoE hang off the same norm. 

836 """ 

837 # An adapter that declares fold_ln unsupported cannot have its norms folded 

838 # weight-preservingly. ProcessWeights already warns and skips for these; folding 

839 # the expert stack anyway leaves the model diverging from the reference. 

840 if not getattr(self.adapter, "supports_fold_ln", True): 

841 return 

842 

843 uses_offset = bool(getattr(self.cfg, "rmsnorm_uses_offset", False)) 

844 skipped: list[str] = [] 

845 for list_name in ("blocks", "encoder_blocks", "decoder_blocks"): 

846 for index, block in enumerate(getattr(self, list_name, None) or []): 

847 mlp = getattr(block, "mlp", None) 

848 if mlp is None or not has_batched_experts(mlp): 

849 continue 

850 label = f"{list_name}.{index}" 

851 norm = getattr(block, "ln2", None) 

852 weight = getattr(norm, "weight", None) if norm is not None else None 

853 if not isinstance(weight, torch.Tensor) or weight.ndim != 1: 853 ↛ 854line 853 didn't jump to line 854 because the condition on line 853 was never true

854 skipped.append(label) 

855 continue 

856 if getattr(norm, "bias", None) is not None: 856 ↛ 858line 856 didn't jump to line 858 because the condition on line 856 was never true

857 # Folding the gain but not the shift would change the block's output. 

858 skipped.append(label) 

859 continue 

860 scale = weight.detach().clone() 

861 if uses_offset: 861 ↛ 862line 861 didn't jump to line 862 because the condition on line 861 was never true

862 scale += 1.0 

863 if not fold_scale_into_moe_block(mlp, scale): 863 ↛ 864line 863 didn't jump to line 864 because the condition on line 863 was never true

864 skipped.append(label) 

865 continue 

866 with torch.no_grad(): 

867 weight.fill_(0.0 if uses_offset else 1.0) 

868 if skipped: 868 ↛ 869line 868 didn't jump to line 869 because the condition on line 868 was never true

869 logging.warning( 

870 "fold_ln could not reach the expert weights of %s, so those layers keep " 

871 "their FFN norm gains while the rest of the model is folded. Direct " 

872 "logit attribution and logit lens will mix two bases.", 

873 ", ".join(skipped), 

874 ) 

875 

876 def to_tokens( 

877 self, 

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

879 prepend_bos: Optional[bool] = None, 

880 padding_side: Optional[str] = None, 

881 move_to_device: bool = True, 

882 truncate: bool = True, 

883 ) -> torch.Tensor: 

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

885 

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

887 semantics, the ``default_prepend_bos`` / 

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

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

890 tokenizing only part of a prompt.** 

891 

892 Args: 

893 input: The input to tokenize. 

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

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

896 to override locally. 

897 padding_side: Which side to pad on when tokenizing multiple 

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

899 ``padding_side``. 

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

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

902 

903 Returns: 

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

905 """ 

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

907 if prepend_bos is None: 

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

909 if padding_side is None: 

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

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

912 if prepend_bos and (not tokenizer_prepends_bos): 

913 bos = self.tokenizer.bos_token 

914 encodes_atomically = ( 

915 bos is not None 

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

917 ) 

918 if encodes_atomically: 

919 input = utils.get_input_with_manually_prepended_bos(bos, input) 

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

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

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

923 if isinstance(input, str): 

924 input = [input] 

925 tokens = self.tokenizer( 

926 input, 

927 return_tensors="pt", 

928 padding=True, 

929 padding_side=padding_side, 

930 truncation=truncate, 

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

932 )["input_ids"] 

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

934 if ( 

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

936 and self.tokenizer.eos_token_id is not None 

937 ): 

938 # Remove trailing EOS, keep at least 1 token 

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

940 tokens = tokens[:, :-1] 

941 if not prepend_bos and tokenizer_prepends_bos: 

942 tokens = utils.get_tokens_with_bos_removed( 

943 self.tokenizer, tokens, padding_side=padding_side 

944 ) 

945 if move_to_device: 

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

947 return tokens 

948 

949 def encoder_output( 

950 self, 

951 frames: torch.Tensor, 

952 one_zero_attention_mask: Optional[torch.Tensor] = None, 

953 ) -> torch.Tensor: 

954 """Run the audio encoder from precomputed frames, skipping feature extraction. 

955 

956 The audio-path analogue of ``start_at_layer``: ``frames`` is 

957 ``[batch, frames, d_model]``, the tensor the conv front end would have 

958 produced (observable at ``feat_proj.hook_out``), so callers can inject or 

959 reuse frames without re-running the waveform convolutions. Positional 

960 convolution and the encoder layer norm are applied first, exactly as the 

961 full path does, then the blocks run. Mirrors 

962 ``HookedAudioEncoder.encoder_output``. 

963 

964 Hooks on the bridged components fire as usual, so this composes with 

965 ``add_hook`` / ``get_caching_hooks``. 

966 

967 Args: 

968 frames: ``[batch, frames, d_model]`` precomputed encoder frames. 

969 one_zero_attention_mask: Optional ``[batch, frames]`` mask, 1 for 

970 real frames and 0 for padding. 

971 

972 Returns: 

973 The residual stream leaving the final block, ``[batch, frames, d_model]``. 

974 """ 

975 self._require_frame_entry_support() 

976 if frames.ndim != 3: 

977 raise ValueError( 

978 "encoder_output expects precomputed frames [batch, frames, d_model]; " 

979 f"got a {frames.ndim}D tensor. Pass a waveform to forward() instead." 

980 ) 

981 frames = frames.to(self.cfg.device) 

982 

983 if one_zero_attention_mask is not None: 

984 # HF zeroes pad frames before the positional conv (kernel 128 smears 

985 # pad content into real frames otherwise); masked_fill, not HF's 

986 # in-place write, so the caller's tensor survives. 

987 frames = frames.masked_fill( 

988 ~one_zero_attention_mask.to(frames.device).bool().unsqueeze(-1), 0.0 

989 ) 

990 

991 resid = frames + self.conv_pos_embed(frames) 

992 # Post-LN encoders (do_stable_layer_norm=False: hubert-base, 

993 # wav2vec2-base) normalize before the blocks; stable-LN encoders 

994 # (wav2vec2-large and kin) apply this same module AFTER the blocks — 

995 # HF's Wav2Vec2EncoderStableLayerNorm.forward. Mirror the real order or 

996 # every stable-LN activation is silently wrong. 

997 stable_ln = self._audio_encoder_is_stable_layer_norm() 

998 if not stable_ln: 

999 resid = self.embed_ln(resid) 

1000 

1001 additive_attention_mask = None 

1002 if one_zero_attention_mask is not None: 

1003 mask = one_zero_attention_mask.to(self.cfg.device) 

1004 additive_attention_mask = torch.where( 

1005 mask[:, None, None, :] == 0, 

1006 torch.tensor(float("-inf"), dtype=resid.dtype, device=resid.device), 

1007 torch.tensor(0.0, dtype=resid.dtype, device=resid.device), 

1008 ) 

1009 

1010 for block in self.blocks: 

1011 output = block(resid, attention_mask=additive_attention_mask) 

1012 resid = output[0] if isinstance(output, tuple) else output 

1013 if stable_ln: 

1014 resid = self.embed_ln(resid) 

1015 return resid 

1016 

1017 def _audio_encoder_is_stable_layer_norm(self) -> bool: 

1018 """Whether the wrapped audio encoder is the pre-LN ("stable") variant. 

1019 

1020 Structural, not config-driven: build_bridge_from_module never runs the 

1021 adapter's prepare_loading, so cfg.do_stable_layer_norm can be absent 

1022 on that path while the module class is authoritative on both. 

1023 """ 

1024 model = self.original_model 

1025 for attr in ("encoder", "wav2vec2", "hubert", "model"): 1025 ↛ 1032line 1025 didn't jump to line 1032 because the loop on line 1025 didn't complete

1026 candidate = getattr(model, attr, None) 

1027 if candidate is None: 1027 ↛ 1028line 1027 didn't jump to line 1028 because the condition on line 1027 was never true

1028 continue 

1029 encoder = candidate if attr == "encoder" else getattr(candidate, "encoder", None) 

1030 if encoder is not None: 1030 ↛ 1025line 1030 didn't jump to line 1025 because the condition on line 1030 was always true

1031 return type(encoder).__name__.endswith("StableLayerNorm") 

1032 return bool(getattr(self.cfg, "do_stable_layer_norm", False)) 

1033 

1034 def _require_frame_entry_support(self) -> None: 

1035 """Reject models with no conv-frame stage to re-enter.""" 

1036 if not getattr(self.cfg, "is_audio_model", False): 

1037 raise NotImplementedError( 

1038 "encoder_output is an audio-encoder entry point; this bridge is not an " 

1039 "audio model. Use start_at_layer for residual re-entry on text models." 

1040 ) 

1041 missing = [ 

1042 name 

1043 for name in ("conv_pos_embed", "embed_ln", "blocks") 

1044 if self._modules.get(name) is None 

1045 ] 

1046 if missing: 

1047 raise NotImplementedError( 

1048 "encoder_output needs a waveform encoder with a convolutional front end " 

1049 f"(missing {missing}). Spectrogram encoders such as AST have no " 

1050 "precomputed-frame stage to re-enter, so there is nothing to bypass." 

1051 ) 

1052 

1053 def to_sentence_pair_tokens( 

1054 self, 

1055 sentence_a: str, 

1056 sentence_b: str, 

1057 move_to_device: bool = True, 

1058 truncate: bool = True, 

1059 ) -> Dict[str, torch.Tensor]: 

1060 """Pair-tokenize two sentences as ``[CLS] a [SEP] b [SEP]``. 

1061 

1062 Returns ``input_ids``, ``token_type_ids`` and ``attention_mask``. The 

1063 segment ids are not decorative: without them a BERT NSP head sees both 

1064 sentences as one segment and its logits collapse. Mirrors 

1065 ``BertNextSentencePrediction.to_tokens``. 

1066 

1067 Args: 

1068 sentence_a: First sentence of the pair. 

1069 sentence_b: Second sentence of the pair. 

1070 move_to_device: Move the returned tensors to ``cfg.device``. 

1071 truncate: Truncate to the model's context window. 

1072 """ 

1073 assert self.tokenizer is not None, "Cannot pair-tokenize without a tokenizer" 

1074 encodings = self.tokenizer( 

1075 sentence_a, 

1076 sentence_b, 

1077 return_tensors="pt", 

1078 padding=True, 

1079 truncation=truncate, 

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

1081 ) 

1082 if "token_type_ids" not in encodings: 1082 ↛ 1083line 1082 didn't jump to line 1083 because the condition on line 1082 was never true

1083 raise ValueError( 

1084 f"{type(self.tokenizer).__name__} emits no token_type_ids, so it cannot " 

1085 "express a sentence pair. Next-sentence prediction needs a " 

1086 "segment-aware tokenizer (e.g. BERT's)." 

1087 ) 

1088 keys = ("input_ids", "token_type_ids", "attention_mask") 

1089 tokens = {key: encodings[key] for key in keys if key in encodings} 

1090 if move_to_device: 1090 ↛ 1092line 1090 didn't jump to line 1092 because the condition on line 1090 was always true

1091 tokens = {key: value.to(self.cfg.device) for key, value in tokens.items()} 

1092 return tokens 

1093 

1094 def predict_next_sentence( 

1095 self, 

1096 sentence_a: str, 

1097 sentence_b: str, 

1098 return_type: Optional[str] = "predictions", 

1099 truncate: bool = True, 

1100 ) -> Any: 

1101 """Run next-sentence prediction over a sentence pair given as strings. 

1102 

1103 Owns the ``token_type_ids`` plumbing that a hand-rolled pair forward has 

1104 to remember. Requires a bridge booted onto an NSP head — otherwise the 

1105 model has no 2-class output to decode. Mirrors 

1106 ``BertNextSentencePrediction.forward``. 

1107 

1108 Args: 

1109 sentence_a: First sentence of the pair. 

1110 sentence_b: Second sentence of the pair. 

1111 return_type: ``"predictions"`` for the decoded verdict, or 

1112 ``"logits"`` for the raw 2-class scores. 

1113 truncate: Truncate to the model's context window. 

1114 """ 

1115 tokens = self.to_sentence_pair_tokens(sentence_a, sentence_b, truncate=truncate) 

1116 forward_kwargs: Dict[str, Any] = { 

1117 key: value for key, value in tokens.items() if key != "input_ids" 

1118 } 

1119 logits = self(tokens["input_ids"], return_type="logits", **forward_kwargs) 

1120 if logits.shape[-1] != 2: 

1121 raise ValueError( 

1122 "predict_next_sentence needs a next-sentence-prediction head, but this " 

1123 f"bridge produces {logits.shape[-1]} output classes. Boot it with " 

1124 "model_class=BertForNextSentencePrediction." 

1125 ) 

1126 if return_type == "logits": 

1127 return logits 

1128 return self._finalize_return(return_type, logits, tokens["input_ids"]) 

1129 

1130 def to_string( 

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

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

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

1134 

1135 Args: 

1136 tokens: Tokens to convert 

1137 

1138 Returns: 

1139 Decoded string(s) 

1140 """ 

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

1142 tokens = torch.tensor(tokens) 

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

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

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

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

1147 else: 

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

1149 

1150 def to_str_tokens( 

1151 self, 

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

1153 prepend_bos: Optional[bool] = None, 

1154 padding_side: Optional[str] = None, 

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

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

1157 

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

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

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

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

1162 

1163 Args: 

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

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

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

1167 (use the cfg setting). 

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

1169 is a string. 

1170 

1171 Returns: 

1172 List of token strings. 

1173 """ 

1174 if isinstance(input, list): 

1175 return cast( 

1176 List[List[str]], 

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

1178 ) 

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

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

1181 elif isinstance(input, torch.Tensor): 

1182 tokens = input.squeeze() 

1183 if tokens.dim() == 0: 

1184 tokens = tokens.unsqueeze(0) 

1185 assert ( 

1186 tokens.dim() == 1 

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

1188 elif isinstance(input, np.ndarray): 

1189 tokens_np = input.squeeze() 

1190 if tokens_np.ndim == 0: 

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

1192 assert ( 

1193 tokens_np.ndim == 1 

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

1195 tokens = torch.tensor(tokens_np) 

1196 else: 

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

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

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

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

1201 return str_tokens 

1202 

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

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

1205 

1206 Args: 

1207 string: The string to convert 

1208 

1209 Returns: 

1210 Token ID 

1211 

1212 Raises: 

1213 AssertionError: If string is not a single token 

1214 """ 

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

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

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

1218 return int(token.item()) 

1219 

1220 def get_token_position( 

1221 self, 

1222 single_token: Union[str, int], 

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

1224 mode="first", 

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

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

1227 ): 

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

1229 

1230 Raises an error if the token is not present. 

1231 

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

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

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

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

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

1237 

1238 Args: 

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

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

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

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

1243 with a dummy batch dimension. 

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

1245 "first" or "last". Defaults to "first". 

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

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

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

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

1250 """ 

1251 if isinstance(input, str): 

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

1253 else: 

1254 tokens = input 

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

1256 assert ( 

1257 tokens.shape[0] == 1 

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

1259 tokens = tokens[0] 

1260 if isinstance(single_token, str): 

1261 single_token = self.to_single_token(single_token) 

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

1263 single_token = single_token.item() 

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

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

1266 if mode == "first": 

1267 return indices[0].item() 

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

1269 return indices[-1].item() 

1270 else: 

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

1272 

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

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

1275 

1276 Args: 

1277 int_token: The token ID 

1278 

1279 Returns: 

1280 The token string 

1281 """ 

1282 assert isinstance(int_token, int) 

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

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

1285 return str(token[0]) 

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

1287 

1288 def _enumerate_blocks(self) -> List[Tuple[int, Any]]: 

1289 """(index, block) over every registered block list, encoder before decoder. 

1290 

1291 Decoder-only models register a single ``blocks``, so the indices are the 

1292 plain layer indices. Encoder-decoder models register ``encoder_blocks`` 

1293 and ``decoder_blocks`` instead; those are concatenated into one index 

1294 space, matching ``HookedEncoderDecoder``'s ``chain(encoder, decoder)``. 

1295 """ 

1296 pairs: List[Tuple[int, Any]] = [] 

1297 for list_name in _BLOCK_LIST_ATTRS: 

1298 block_list = self._modules.get(list_name) 

1299 if not isinstance(block_list, nn.ModuleList): 

1300 continue 

1301 for block in block_list: 

1302 pairs.append((len(pairs), block)) 

1303 return pairs 

1304 

1305 def _resolve_submodule_name(self, block: Any, submodule: str) -> Optional[str]: 

1306 """The block's actual name for ``submodule``. 

1307 

1308 Encoder blocks name self-attention ``attn`` while decoder blocks name it 

1309 ``self_attn``, so a caller asking for ``attn`` means "this block's 

1310 self-attention" on either side. Cross-attention is never resolved here: 

1311 ``HookedEncoderDecoder`` omits it from stacked weights, and this mirrors 

1312 that so the two stacks line up layer for layer. 

1313 """ 

1314 for candidate in _SELF_ATTENTION_NAMES.get(submodule, (submodule,)): 

1315 if candidate in block._modules: 

1316 return candidate 

1317 return None 

1318 

1319 def _rewrite_submodule_path(self, attr_path: str, submodule: str, actual: Optional[str]) -> str: 

1320 """Re-point ``attr_path``'s leading segment at the block's actual submodule.""" 

1321 if actual is None or actual == submodule: 

1322 return attr_path 

1323 if attr_path.split(".")[0] != submodule: 1323 ↛ 1324line 1323 didn't jump to line 1324 because the condition on line 1323 was never true

1324 return attr_path 

1325 return actual + attr_path[len(submodule) :] 

1326 

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

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

1329 

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

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

1332 On encoder-decoder models the indices span encoder then decoder blocks, 

1333 and ``"attn"`` matches the decoder's ``self_attn`` too. 

1334 """ 

1335 return [ 

1336 (index, block) 

1337 for index, block in self._enumerate_blocks() 

1338 if self._resolve_submodule_name(block, submodule) is not None 

1339 ] 

1340 

1341 def stack_params_for( 

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

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

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

1345 

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

1347 """ 

1348 matching = self.blocks_with(submodule) 

1349 if not matching: 

1350 raise ValueError( 

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

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

1353 ) 

1354 indices: List[int] = [] 

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

1356 for idx, block in matching: 

1357 resolved = self._resolve_submodule_name(block, submodule) 

1358 w = _resolve_attr_path( 

1359 block, self._rewrite_submodule_path(attr_path, submodule, resolved) 

1360 ) 

1361 if w is None: 1361 ↛ 1362line 1361 didn't jump to line 1362 because the condition on line 1361 was never true

1362 raise AttributeError( 

1363 f"blocks[{idx}].{attr_path} is None — this checkpoint has no such " 

1364 f"parameter (bias-free projection)." 

1365 ) 

1366 if reshape_fn is not None: 

1367 w = reshape_fn(w) 

1368 weights.append(w) 

1369 indices.append(idx) 

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

1371 

1372 def _stack_block_params( 

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

1374 ) -> torch.Tensor: 

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

1376 

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

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

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

1380 AttributeError killed the accessor for the whole model. 

1381 """ 

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

1383 all_blocks = self._enumerate_blocks() 

1384 matching_blocks: List[Tuple[int, torch.Tensor]] = [] 

1385 for i, block in all_blocks: 

1386 resolved = self._resolve_submodule_name(block, first_attr) 

1387 if resolved is None: 

1388 continue 

1389 try: 

1390 weight = _resolve_attr_path( 

1391 block, self._rewrite_submodule_path(attr_path, first_attr, resolved) 

1392 ) 

1393 except AttributeError: 

1394 continue 

1395 if weight is None: 

1396 # Bias-free checkpoints (e.g. qkv_bias=False) expose None here; 

1397 # stacking would raise an opaque TypeError. 

1398 raise AttributeError( 

1399 f"blocks[{i}].{attr_path} is None — this checkpoint has no " 

1400 f"such parameter (bias-free projection). Bias-free models expose no " 

1401 f"stacked {attr_path.rsplit('.', 1)[-1]}; construct zeros explicitly " 

1402 f"if your analysis needs one." 

1403 ) 

1404 matching_blocks.append((i, weight)) 

1405 

1406 if len(matching_blocks) == 0: 

1407 raise AttributeError( 

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

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

1410 ) 

1411 

1412 if len(matching_blocks) < len(all_blocks): 

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

1414 logging.warning( 

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

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

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

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

1419 len(matching_blocks), 

1420 len(all_blocks), 

1421 attr_path, 

1422 indices, 

1423 first_attr, 

1424 attr_path, 

1425 ) 

1426 

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

1428 for _, weight in matching_blocks: 

1429 w = reshape_fn(weight) if reshape_fn is not None else weight 

1430 weights.append(w) 

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

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

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

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

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

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

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

1438 

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

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

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

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

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

1444 return w 

1445 

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

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

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

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

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

1451 return w 

1452 

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

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

1455 

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

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

1458 n_heads before factoring: query head h reads kv head 

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

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

1461 where the head axes already match. 

1462 """ 

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

1464 return w 

1465 n_kv_heads = w.shape[1] 

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

1467 raise ValueError( 

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

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

1470 ) 

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

1472 

1473 @property 

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

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

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

1477 

1478 @property 

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

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

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

1482 

1483 @property 

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

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

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

1487 

1488 @property 

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

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

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

1492 

1493 @property 

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

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

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

1497 

1498 @property 

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

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

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

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

1503 return None 

1504 

1505 @property 

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

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

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

1509 

1510 @property 

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

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

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

1514 

1515 @property 

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

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

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

1519 

1520 @property 

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

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

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

1524 

1525 @property 

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

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

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

1529 

1530 @property 

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

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

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

1534 

1535 @property 

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

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

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

1539 

1540 @property 

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

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

1543 return self.unembed.W_U 

1544 

1545 @property 

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

1547 """Unembedding bias (d_vocab).""" 

1548 return self.unembed.b_U 

1549 

1550 @property 

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

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

1553 return self.embed.W_E 

1554 

1555 @property 

1556 def W_pos(self) -> torch.Tensor: 

1557 """Positional embedding matrix (n_ctx, d_model). 

1558 

1559 Only defined for models with learned absolute positional embeddings; 

1560 rotary/ALiBi models have no such matrix. Reflects the weights as 

1561 currently processed, like every other accessor — equal to 

1562 ``HookedTransformer.W_pos`` only under matching processing 

1563 (``from_pretrained_no_processing`` vs the bridge default, or 

1564 compatibility mode vs HT defaults). 

1565 """ 

1566 pos_embed = getattr(self, "pos_embed", None) 

1567 pos_type = getattr(self.cfg, "positional_embedding_type", "unknown") 

1568 if pos_embed is None or not hasattr(pos_embed, "W_pos"): 

1569 raise AttributeError( 

1570 "W_pos is only defined for models with a learned absolute positional " 

1571 f"embedding component; this model exposes none " 

1572 f"(positional_embedding_type={pos_type!r})." 

1573 ) 

1574 w = pos_embed.W_pos 

1575 # T5-family adapters map pos_embed to the relative attention bias — a 

1576 # [num_buckets, n_heads] table, not a positional matrix. The legacy 

1577 # accessor refused these models; keep refusing rather than hand back a 

1578 # wrong-shaped tensor. 

1579 if w.ndim != 2 or w.shape[-1] != self.cfg.d_model: 

1580 raise AttributeError( 

1581 "W_pos is only defined for learned absolute positional embeddings; " 

1582 f"this model's pos_embed holds a {tuple(w.shape)} table " 

1583 f"(positional_embedding_type={pos_type!r})." 

1584 ) 

1585 # OPT/BART-family checkpoints allocate max_position_embeddings + 2 rows 

1586 # (positions are offset by 2 in HF's forward). HookedTransformer's 

1587 # converters slice those rows off; mirror that so shapes and values agree. 

1588 if w.shape[0] == self.cfg.n_ctx + 2: 

1589 return w[2:] 

1590 return w 

1591 

1592 @property 

1593 def W_E_pos(self) -> torch.Tensor: 

1594 """Concatenated ``[W_E; W_pos]`` (d_vocab + n_ctx, d_model). 

1595 

1596 A full (overcomplete) basis of the input space, used for full QK/OV 

1597 circuits. Mirrors ``HookedTransformer.W_E_pos``. 

1598 """ 

1599 return torch.cat([self.W_E, self.W_pos], dim=0) 

1600 

1601 @property 

1602 def QK(self): 

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

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

1605 

1606 @property 

1607 def OV(self): 

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

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

1610 

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

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

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

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

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

1616 

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

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

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

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

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

1622 

1623 # ------------------------------------------------------------------ 

1624 # Mechanistic interpretability analysis methods 

1625 # ------------------------------------------------------------------ 

1626 

1627 def tokens_to_residual_directions( 

1628 self, 

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

1630 ) -> torch.Tensor: 

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

1632 

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

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

1635 logit for each token. 

1636 

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

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

1639 

1640 Args: 

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

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

1643 

1644 Returns: 

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

1646 plus a trailing d_model dimension. 

1647 """ 

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

1649 residual_directions = self.W_U[:, tokens] 

1650 residual_directions = einops.rearrange( 

1651 residual_directions, "d_model ... -> ... d_model" 

1652 ) 

1653 return residual_directions 

1654 else: 

1655 if isinstance(tokens, str): 

1656 token = self.to_single_token(tokens) 

1657 elif isinstance(tokens, int): 

1658 token = tokens 

1659 elif isinstance(tokens, torch.Tensor) and tokens.numel() == 1: 1659 ↛ 1662line 1659 didn't jump to line 1662 because the condition on line 1659 was always true

1660 token = int(tokens.item()) 

1661 else: 

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

1663 residual_direction = self.W_U[:, token] 

1664 return residual_direction 

1665 

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

1667 _VARIANT_OUTPUT_BIAS_ATTRS: Dict[str, tuple] = { 

1668 "attn": ("b_O",), 

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

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

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

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

1673 } 

1674 

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

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

1677 for name in VARIANT_SUBMODULE_NAMES: 

1678 if name not in block._modules: 

1679 continue 

1680 variant = block._modules[name] 

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

1682 obj = variant 

1683 try: 

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

1685 obj = getattr(obj, attr) 

1686 except AttributeError: 

1687 continue 

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

1689 return obj 

1690 return None 

1691 

1692 def accumulated_bias( 

1693 self, 

1694 layer: int, 

1695 mlp_input: bool = False, 

1696 include_mlp_biases: bool = True, 

1697 ) -> torch.Tensor: 

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

1699 

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

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

1702 """ 

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

1704 for i in range(layer): 

1705 block = self.blocks[i] 

1706 b_O = self._get_block_variant_bias(block) 

1707 if b_O is not None: 

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

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

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

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

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

1713 if mlp_input: 

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

1715 block = self.blocks[layer] 

1716 b_O = self._get_block_variant_bias(block) 

1717 if b_O is not None: 

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

1719 return accumulated 

1720 

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

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

1723 

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

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

1726 maps tensor position i to original layer number. 

1727 """ 

1728 self._reject_encoder_decoder_composition() 

1729 attn_blocks = self.blocks_with("attn") 

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

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

1732 

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

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

1735 

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

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

1738 for block_idx, block in zip(indices, blocks_list): 

1739 w = _resolve_attr_path(block, attr_path) 

1740 if w is None: 1740 ↛ 1741line 1740 didn't jump to line 1741 because the condition on line 1740 was never true

1741 raise AttributeError( 

1742 f"blocks[{block_idx}].{attr_path} is None — this checkpoint has " 

1743 f"no such parameter (bias-free projection)." 

1744 ) 

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

1746 w = reshape_fn(w) 

1747 weights.append(w) 

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

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

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

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

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

1753 

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

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

1756 left = FactoredMatrix(W_V, W_O) 

1757 

1758 if mode == "Q": 

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

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

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

1762 elif mode == "K": 

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

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

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

1766 elif mode == "V": 

1767 right = left 

1768 else: 

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

1770 

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

1772 n_attn = len(indices) 

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

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

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

1776 

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

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

1779 

1780 def _reject_encoder_decoder_composition(self) -> None: 

1781 """Composition scores live in one residual stream; enc-dec models have two.""" 

1782 if any(hasattr(self, a) for a in ("encoder_blocks", "decoder_blocks")): 

1783 raise NotImplementedError( 

1784 "Composition scores are not defined across an encoder-decoder " 

1785 "model's two residual streams (HookedTransformer never " 

1786 "supported them there either)." 

1787 ) 

1788 

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

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

1791 self._reject_encoder_decoder_composition() 

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

1793 

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

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

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

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

1798 

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

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

1801 block = self.blocks[layer_idx] 

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

1803 

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

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

1806 types = [] 

1807 for block in self.blocks: 

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

1809 universals = sorted( 

1810 n 

1811 for n in block._modules 

1812 if n not in _VARIANT_SUBMODULE_SET 

1813 and n not in _BLOCK_INTERNAL_MODULES 

1814 and not n.startswith(_NORM_PREFIXES) 

1815 ) 

1816 parts = variants + universals 

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

1818 return types 

1819 

1820 @property 

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

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

1823 

1824 Encoder-decoder models use ``HookedEncoderDecoder``'s ``EL{l}H{h}`` / 

1825 ``DL{l}H{h}`` scheme so encoder and decoder heads stay distinguishable; 

1826 a plain ``L{l}H{h}`` list would name only half of them. 

1827 """ 

1828 encoder_blocks = self._modules.get("encoder_blocks") 

1829 decoder_blocks = self._modules.get("decoder_blocks") 

1830 if isinstance(encoder_blocks, nn.ModuleList) and isinstance(decoder_blocks, nn.ModuleList): 

1831 heads = range(self.cfg.n_heads) 

1832 return [f"EL{l}H{h}" for l in range(len(encoder_blocks)) for h in heads] + [ 

1833 f"DL{l}H{h}" for l in range(len(decoder_blocks)) for h in heads 

1834 ] 

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

1836 

1837 @property 

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

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

1840 self._reject_encoder_decoder_composition() 

1841 return [ 

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

1843 ] 

1844 

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

1846 """Returns parameters following standard PyTorch semantics. 

1847 

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

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

1850 

1851 Args: 

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

1853 

1854 Returns: 

1855 Iterator of nn.Parameter objects 

1856 """ 

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

1858 

1859 def named_parameters( 

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

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

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

1863 

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

1865 For TransformerLens-style generator, use tl_named_parameters() instead. 

1866 

1867 Args: 

1868 prefix: Prefix to prepend to all parameter names 

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

1870 remove_duplicate: If True, removes duplicate parameters 

1871 

1872 Returns: 

1873 Iterator of (name, parameter) tuples 

1874 """ 

1875 return self.original_model.named_parameters(prefix, recurse, remove_duplicate) 

1876 

1877 def tl_parameters(self) -> dict[str, torch.Tensor]: 

1878 """Returns TransformerLens-style parameter dictionary. 

1879 

1880 Parameter names follow TransformerLens conventions (e.g., 'blocks.0.attn.W_Q') and may 

1881 include processed weights (non-leaf tensors). This format is expected by SVDInterpreter 

1882 among other analysis tools. 

1883 

1884 Returns: 

1885 Dictionary mapping TransformerLens parameter names to tensors 

1886 

1887 Example: 

1888 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

1889 >>> tl_params = bridge.tl_parameters() 

1890 >>> W_Q = tl_params["blocks.0.attn.W_Q"] # Shape: [n_heads, d_model, d_head] 

1891 """ 

1892 return self.get_params() 

1893 

1894 def tl_named_parameters(self) -> Iterator[tuple[str, torch.Tensor]]: 

1895 """Returns iterator of TransformerLens-style named parameters. 

1896 

1897 This provides the same parameters as tl_parameters() but as an iterator 

1898 for consistency with PyTorch's named_parameters() API pattern. 

1899 

1900 Returns: 

1901 Iterator of (name, tensor) tuples with TransformerLens naming conventions 

1902 

1903 Example: 

1904 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

1905 >>> for name, param in bridge.tl_named_parameters(): 

1906 ... if "attn.W_Q" in name: 

1907 ... print(f"{name}: {param.shape}") # doctest: +ELLIPSIS 

1908 blocks.0.attn.W_Q: torch.Size([12, 768, 64]) 

1909 ... 

1910 """ 

1911 return iter(self.get_params().items()) 

1912 

1913 def input_to_embed( 

1914 self, 

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

1916 prepend_bos: Optional[bool] = None, 

1917 padding_side: Optional[str] = None, 

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

1919 ) -> Tuple[torch.Tensor, torch.Tensor, None, Optional[torch.Tensor]]: 

1920 """Convert input to the residual stream entering block 0 (``resid_pre[0]``). 

1921 

1922 Bridge analog of :meth:`HookedTransformer.input_to_embed`. Returns 

1923 ``(residual, tokens, shortformer_pos_embed, attention_mask)``; feed the 

1924 residual to ``forward(..., start_at_layer=0)`` to resume the pass. 

1925 

1926 ``shortformer_pos_embed`` is always ``None``: for the models the bridge 

1927 supports the residual already carries positional information, so there is 

1928 no separate positional stream to return. 

1929 """ 

1930 if isinstance(input, (str, list)): 

1931 assert self.tokenizer is not None, "Must provide a tokenizer for string input." 

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

1933 else: 

1934 tokens = input 

1935 if isinstance(tokens, torch.Tensor) and tokens.ndim == 1: 1935 ↛ 1936line 1935 didn't jump to line 1936 because the condition on line 1935 was never true

1936 tokens = tokens.unsqueeze(0) 

1937 if ( 1937 ↛ 1942line 1937 didn't jump to line 1942 because the condition on line 1937 was never true

1938 attention_mask is None 

1939 and self.tokenizer is not None 

1940 and self.tokenizer.padding_side == "left" 

1941 ): 

1942 _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos 

1943 attention_mask = utils.get_attention_mask(self.tokenizer, tokens, _prepend) 

1944 residual = self.forward(tokens, stop_at_layer=0, attention_mask=attention_mask) 

1945 return residual, tokens, None, attention_mask 

1946 

1947 def _accepts_derived_position_ids(self) -> bool: 

1948 """Whether it is safe to hand the wrapped model a mask-derived ``position_ids``. 

1949 

1950 Two families of model must be left alone, so the injection below is 

1951 gated on the target the same way ``output_attentions`` is in 

1952 :meth:`BridgeCore.run_with_cache`: 

1953 

1954 * **Fixed-signature models.** Remote-code forwards such as 

1955 ``LLaDAModelLM.forward`` take neither ``position_ids`` nor 

1956 ``**kwargs``, so passing it raises ``TypeError`` where the model 

1957 previously returned logits. 

1958 * **Models that own their position derivation.** mRoPE architectures 

1959 (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, GLM-4V) build a 3-D temporal / 

1960 height / width index in ``get_rope_index``, and only while 

1961 ``position_ids is None``; a supplied 2-D tensor is silently expanded 

1962 across all three streams instead. Their derivation already scatters 

1963 positions onto attended slots only, so it handles left padding 

1964 correctly on its own and needs no help from us. 

1965 * **Mask-consuming positional embeddings.** OPT's 

1966 ``OPTLearnedPositionalEmbedding.forward`` takes the mask and derives 

1967 the same positions we would, so injection buys nothing — but it does 

1968 replace the model's own padding-slot convention with ours, which 

1969 shows up as a whole-tensor diff. 

1970 

1971 Non-torch drivers (vLLM, Inspect) expose no module to introspect and 

1972 manage positions internally, so they are excluded as well. 

1973 """ 

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

1975 if underlying is None: 

1976 return False 

1977 

1978 cached = self.__dict__.get("_derived_position_ids_ok") 

1979 if cached is not None and cached[0] is underlying: 1979 ↛ 1982line 1979 didn't jump to line 1982 because the condition on line 1979 was always true

1980 return bool(cached[1]) 

1981 

1982 def verdict() -> bool: 

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

1984 if "position_ids" not in fwd_params and not any( 

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

1986 ): 

1987 return False 

1988 

1989 # ``get_rope_index`` lives on the inner text model, not the 

1990 # ForConditionalGeneration wrapper that is usually original_model. 

1991 for module in ( 

1992 underlying, 

1993 getattr(underlying, "model", None), 

1994 getattr(underlying, "language_model", None), 

1995 ): 

1996 if module is not None and hasattr(module, "get_rope_index"): 

1997 return False 

1998 

1999 # Config-level backstop for mRoPE models that spell the derivation 

2000 # differently; the section list is what makes positions 3-D. 

2001 config = getattr(underlying, "config", None) 

2002 for candidate in (config, getattr(config, "text_config", None)): 

2003 scaling = getattr(candidate, "rope_scaling", None) 

2004 if isinstance(scaling, dict) and "mrope_section" in scaling: 

2005 return False 

2006 

2007 # A positional embedding that takes the mask derives positions for 

2008 # itself. Only embeddings that override nn.Embedding.forward are 

2009 # worth inspecting, which keeps this to a handful per model. 

2010 for module in underlying.modules(): 

2011 if not isinstance(module, nn.Embedding): 

2012 continue 

2013 if type(module).forward is nn.Embedding.forward: 

2014 continue 

2015 if "attention_mask" in inspect.signature(module.forward).parameters: 

2016 return False 

2017 return True 

2018 

2019 accepts = verdict() 

2020 self.__dict__["_derived_position_ids_ok"] = (underlying, accepts) 

2021 return accepts 

2022 

2023 @staticmethod 

2024 def _seq2seq_loss( 

2025 logits: torch.Tensor, 

2026 labels: torch.Tensor, 

2027 native_loss: Any, 

2028 *, 

2029 per_token: bool, 

2030 ) -> torch.Tensor: 

2031 """Return encoder-decoder loss without the causal LM token shift.""" 

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

2033 labels = labels.to(logits.device) 

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

2035 raise ValueError( 

2036 "seq2seq labels must match the decoder logits batch and position " 

2037 f"dimensions, got labels {tuple(labels.shape)} and logits " 

2038 f"{tuple(logits.shape)}" 

2039 ) 

2040 if not per_token and isinstance(native_loss, torch.Tensor): 

2041 return native_loss 

2042 

2043 losses = F.cross_entropy( 

2044 logits.flatten(0, 1), 

2045 labels.flatten(), 

2046 reduction="none" if per_token else "mean", 

2047 ignore_index=-100, 

2048 ) 

2049 return losses.view_as(labels) if per_token else losses 

2050 

2051 def _finalize_seq2seq_return( 

2052 self, 

2053 return_type: str, 

2054 logits: torch.Tensor, 

2055 labels: torch.Tensor, 

2056 native_output: Any, 

2057 *, 

2058 loss_per_token: bool, 

2059 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: 

2060 loss = self._seq2seq_loss( 

2061 logits, 

2062 labels, 

2063 getattr(native_output, "loss", None), 

2064 per_token=loss_per_token, 

2065 ) 

2066 return (logits, loss) if return_type == "both" else loss 

2067 

2068 def forward( 

2069 self, 

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

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

2072 loss_per_token: bool = False, 

2073 prepend_bos: Optional[bool] = None, 

2074 padding_side: Optional[str] = None, 

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

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

2077 start_at_layer: Optional[int] = None, 

2078 stop_at_layer: Optional[int] = None, 

2079 pixel_values: Optional[torch.Tensor] = None, 

2080 input_values: Optional[torch.Tensor] = None, 

2081 past_key_values: Optional[Any] = None, 

2082 **kwargs, 

2083 ) -> Any: 

2084 """Forward pass through the model. 

2085 

2086 Args: 

2087 input: Input to the model 

2088 return_type: Type of output to return ('logits', 'loss', 'both', 'predictions', 

2089 'logits_and_cache', None). 'logits_and_cache' returns 

2090 ``(logits, past_key_values)`` — the HuggingFace cache after this step, to 

2091 feed back on the next call for incremental decoding. 

2092 loss_per_token: Whether to return loss per token 

2093 prepend_bos: Whether to prepend BOS token 

2094 padding_side: Which side to pad on 

2095 labels: Explicit language-model targets. Encoder-decoder models require 

2096 labels for loss; decoder-only models fall back to input IDs when omitted. 

2097 past_key_values: HuggingFace KV cache from a prior ``use_cache=True`` step 

2098 (e.g. the second element of a ``return_type='logits_and_cache'`` return). 

2099 When provided, KV caching is enabled automatically and only the new 

2100 tokens' keys/values are computed; HF derives the position offset from the 

2101 cache length. This is the bridge's manual KV-cache entry point — it uses 

2102 HF's native cache object rather than a TransformerLens cache. 

2103 start_at_layer: Resume the forward from block ``k``, treating ``input`` as 

2104 the residual-stream tensor ``[batch, pos, d_model]`` entering that block 

2105 (mirrors HookedTransformer). Blocks 0..k-1 still execute internally (their 

2106 output is discarded when block k swaps in the residual) but are excluded 

2107 from ``run_with_cache`` output. Requires an HF model that accepts 

2108 ``inputs_embeds``; only supported on the standard ``blocks`` stack. 

2109 stop_at_layer: Layer to stop forward pass at. Only supported on the 

2110 standard ``blocks`` stack; architectures that register no ``blocks`` 

2111 (e.g. Raven's ``prelude``/``core_block``/``coda``) raise 

2112 ``NotImplementedError`` rather than running to completion. 

2113 pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) 

2114 and vision models (eg. ViT, DeiT). 

2115 The tensor is passed directly to the underlying HuggingFace model. 

2116 Only valid when cfg.is_multimodal is True or cfg.is_visual_model is True. 

2117 input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). 

2118 The tensor is passed directly to the underlying HuggingFace model. 

2119 Only valid when cfg.is_audio_model is True. 

2120 **kwargs: Additional arguments passed to model 

2121 

2122 Returns: 

2123 Model output based on return_type 

2124 """ 

2125 

2126 underlying_model = getattr(getattr(self, "_driver", None), "underlying_model", None) 

2127 model_config = getattr(underlying_model, "config", None) 

2128 is_encoder_decoder = bool(getattr(model_config, "is_encoder_decoder", False)) 

2129 if return_type in ("loss", "both"): 

2130 if is_encoder_decoder and labels is None: 

2131 raise ValueError( 

2132 "labels are required for seq2seq return_type='loss' or 'both'; " 

2133 "encoder input_ids are not decoder targets" 

2134 ) 

2135 if ( 

2136 return_type in ("loss", "both") 

2137 and not is_encoder_decoder 

2138 and not self.adapter.supports_causal_loss 

2139 ): 

2140 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2141 raise NotImplementedError( 

2142 f"{architecture} does not support TransformerBridge's shifted causal " 

2143 "loss. Request return_type='logits' and compute the architecture-specific " 

2144 "masked-token objective explicitly." 

2145 ) 

2146 

2147 if labels is not None: 

2148 kwargs["labels"] = labels 

2149 

2150 if start_at_layer is not None: 

2151 input = self._setup_start_at_layer(input, start_at_layer) 

2152 # Set stop_at_layer flag on all blocks if requested 

2153 if stop_at_layer is not None: 

2154 if not self._has_registered_blocks(): 

2155 raise NotImplementedError( 

2156 "stop_at_layer requires a 'blocks' stack; this architecture " 

2157 "does not register one." 

2158 ) 

2159 effective_stop_at_layer = ( 

2160 len(self.blocks) + stop_at_layer if stop_at_layer < 0 else stop_at_layer 

2161 ) 

2162 for block in self.blocks: 

2163 block._stop_at_layer_idx = effective_stop_at_layer 

2164 

2165 # Map HookedEncoderDecoder-style kwargs to HF-compatible names 

2166 if "decoder_input" in kwargs: 

2167 kwargs["decoder_input_ids"] = kwargs.pop("decoder_input") 

2168 if "one_zero_attention_mask" in kwargs: 2168 ↛ 2169line 2168 didn't jump to line 2169 because the condition on line 2168 was never true

2169 if attention_mask is None: 

2170 attention_mask = kwargs.pop("one_zero_attention_mask") 

2171 else: 

2172 kwargs.pop("one_zero_attention_mask") 

2173 

2174 # Detect batched list input that may need padding. Forward follows the 

2175 # requested/tokenizer side; generation separately forces left-padding. 

2176 _is_batched_list = ( 

2177 isinstance(input, list) 

2178 and len(input) > 1 

2179 and not getattr(self.cfg, "is_audio_model", False) 

2180 and not getattr(self.cfg, "is_visual_model", False) 

2181 ) 

2182 _resolved_padding_side = padding_side 

2183 if _resolved_padding_side is None and self.tokenizer is not None: 

2184 _resolved_padding_side = getattr(self.tokenizer, "padding_side", "right") 

2185 

2186 try: 

2187 if isinstance(input, (str, list)): 

2188 if getattr(self.cfg, "is_audio_model", False): 2188 ↛ 2189line 2188 didn't jump to line 2189 because the condition on line 2188 was never true

2189 raise ValueError( 

2190 "Audio models require tensor input (raw waveform), not text. " 

2191 "Pass a torch.Tensor or use the input_values parameter." 

2192 ) 

2193 if getattr(self.cfg, "is_visual_model", False): 2193 ↛ 2194line 2193 didn't jump to line 2194 because the condition on line 2193 was never true

2194 raise ValueError( 

2195 "Visual models require tensor input (pixel values), not text. " 

2196 "Pass a torch.Tensor or use the pixel_values parameter." 

2197 ) 

2198 input_ids = self.to_tokens( 

2199 input, prepend_bos=prepend_bos, padding_side=padding_side 

2200 ) 

2201 else: 

2202 input_ids = input 

2203 # Promote 1D integer token tensors to 2D [batch=1, seq] to match 

2204 # HookedTransformer's contract. Float tensors (inputs_embeds, 

2205 # audio waveforms) are passed through unchanged. 

2206 if ( 

2207 isinstance(input_ids, torch.Tensor) 

2208 and input_ids.ndim == 1 

2209 and not input_ids.is_floating_point() 

2210 ): 

2211 input_ids = input_ids.unsqueeze(0) 

2212 

2213 # Detect inputs_embeds: if the tensor is floating point, it's pre-computed 

2214 # embeddings (e.g., from multimodal models) rather than token IDs. 

2215 _is_inputs_embeds = ( 

2216 isinstance(input_ids, torch.Tensor) and input_ids.is_floating_point() 

2217 ) 

2218 

2219 # Left padding needs a mask and corrected positions. Right padding is 

2220 # harmless for causal real-token positions and remains unmasked to 

2221 # match HookedTransformer; bidirectional/encoder inputs still need it. 

2222 if ( 

2223 _is_batched_list 

2224 and attention_mask is None 

2225 and self.tokenizer is not None 

2226 and self.tokenizer.pad_token_id is not None 

2227 and not _is_inputs_embeds 

2228 and ( 

2229 _resolved_padding_side == "left" 

2230 or is_encoder_decoder 

2231 or not self.adapter.supports_causal_loss 

2232 ) 

2233 ): 

2234 attention_mask = utils.get_attention_mask( 

2235 self.tokenizer, 

2236 input_ids, 

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

2238 padding_side=_resolved_padding_side, 

2239 ).to(self.cfg.device) 

2240 # Gated on the target for the same reason the derivation below is: 

2241 # a fixed-signature forward raises TypeError on the kwarg, and a 

2242 # model that owns its own position derivation is overridden by it 

2243 # (#1626). 

2244 if "position_ids" not in kwargs and self._accepts_derived_position_ids(): 

2245 position_ids = attention_mask.long().cumsum(-1) - 1 

2246 position_ids.masked_fill_(attention_mask == 0, 1) 

2247 kwargs["position_ids"] = position_ids 

2248 

2249 # Any masked-out token shifts the absolute position of every real token 

2250 # after it, so positions must be derived from the mask rather than left 

2251 # to HF's default arange. This is the same derivation HookedTransformer 

2252 # applies in pos_embed; without it the bridge silently returns wrong 

2253 # logits. An all-ones mask reduces to arange, so this is a no-op there. 

2254 # 

2255 # The mask spans any cached prefix as well as the new tokens, so it is 

2256 # offset back to just the tokens actually being passed — matching how 

2257 # AbstractAttention/PosEmbed use past_kv_pos_offset. 

2258 if ( 

2259 attention_mask is not None 

2260 and "position_ids" not in kwargs 

2261 and not _is_inputs_embeds 

2262 and attention_mask.ndim == 2 

2263 and isinstance(input_ids, torch.Tensor) 

2264 and input_ids.ndim == 2 

2265 and attention_mask.shape[1] >= input_ids.shape[1] 

2266 and self._accepts_derived_position_ids() 

2267 ): 

2268 # .long() because callers may hand in a float 0/1 mask, and 

2269 # positions index an embedding table. 

2270 _derived = utils.get_offset_position_ids(0, attention_mask.long()) 

2271 _arange = torch.arange(attention_mask.shape[1], device=_derived.device) 

2272 # Decide per row, not per batch. A row only needs the derived 

2273 # positions when its mask actually moves one of its attended 

2274 # tokens off the default position — i.e. a masked token precedes 

2275 # a real one (left padding, or an interior gap). Rows that are 

2276 # unpadded or purely right-padded keep arange verbatim, so one 

2277 # left-padded row in a batch cannot perturb its neighbours. 

2278 _needs = ((_derived != _arange) & (attention_mask != 0)).any(dim=1, keepdim=True) 

2279 if bool(_needs.any()): 

2280 _positions = torch.where(_needs, _derived, _arange.expand_as(_derived)) 

2281 kwargs["position_ids"] = _positions[ 

2282 :, attention_mask.shape[1] - input_ids.shape[1] : 

2283 ] 

2284 

2285 if attention_mask is not None: 

2286 kwargs["attention_mask"] = attention_mask 

2287 if past_key_values is not None: 

2288 # Manual KV-cache injection: hand HF its own cache back and let it 

2289 # extend it, computing only the new tokens' keys/values. 

2290 kwargs["past_key_values"] = past_key_values 

2291 kwargs["use_cache"] = True 

2292 if kwargs.pop("use_past_kv_cache", False) or kwargs.get("use_cache", False): 

2293 kwargs["use_cache"] = True 

2294 # Auto-generate decoder_input_ids for encoder-decoder models 

2295 if "decoder_input_ids" not in kwargs and labels is None and is_encoder_decoder: 

2296 decoder_start_token_id = getattr( 

2297 self.original_model.config, "decoder_start_token_id", None 

2298 ) 

2299 if decoder_start_token_id is not None: 

2300 shifted = input_ids[:, :-1] 

2301 start_tokens = torch.full( 

2302 (input_ids.shape[0], 1), 

2303 decoder_start_token_id, 

2304 dtype=input_ids.dtype, 

2305 device=input_ids.device, 

2306 ) 

2307 kwargs["decoder_input_ids"] = torch.cat([start_tokens, shifted], dim=1) 

2308 else: 

2309 kwargs["decoder_input_ids"] = input_ids 

2310 

2311 # Tell PosEmbedBridge to expand batch=1 position_ids to full batch. 

2312 if hasattr(self, "pos_embed"): 

2313 self.pos_embed._current_batch_size = input_ids.shape[0] 

2314 

2315 # Handle pixel_values for multimodal and vision models 

2316 if pixel_values is not None: 

2317 if not ( 

2318 getattr(self.cfg, "is_multimodal", False) 

2319 or getattr(self.cfg, "is_visual_model", False) 

2320 ): 

2321 raise ValueError( 

2322 "pixel_values can only be passed to multimodal or vision models " 

2323 "(cfg.is_multimodal or cfg.is_visual_model must be True)" 

2324 ) 

2325 kwargs["pixel_values"] = pixel_values 

2326 

2327 # Handle input_values for audio models 

2328 if input_values is not None: 2328 ↛ 2329line 2328 didn't jump to line 2329 because the condition on line 2328 was never true

2329 if not getattr(self.cfg, "is_audio_model", False): 

2330 raise ValueError( 

2331 "input_values can only be passed to audio models " 

2332 "(cfg.is_audio_model must be True)" 

2333 ) 

2334 kwargs["input_values"] = input_values 

2335 

2336 # Audio models use input_values (waveform), not input_ids 

2337 if getattr(self.cfg, "is_audio_model", False): 

2338 if input_values is not None: 2338 ↛ 2339line 2338 didn't jump to line 2339 because the condition on line 2338 was never true

2339 result = self._driver.forward(**kwargs) 

2340 elif isinstance(input, torch.Tensor): 2340 ↛ 2344line 2340 didn't jump to line 2344 because the condition on line 2340 was always true

2341 kwargs["input_values"] = input 

2342 result = self._driver.forward(**kwargs) 

2343 else: 

2344 raise ValueError( 

2345 "Audio models require tensor input (raw waveform). " 

2346 "Pass a torch.Tensor or use input_values parameter." 

2347 ) 

2348 elif getattr(self.cfg, "is_visual_model", False): 

2349 # "pixel_values" may already be in kwargs from the "if pixel_values is not None:" 

2350 # gate above (explicit pixel_values=... call); otherwise treat `input` itself as 

2351 # the image tensor, matching how the audio branch treats `input` as the waveform. 

2352 if "pixel_values" not in kwargs: 2352 ↛ 2360line 2352 didn't jump to line 2360 because the condition on line 2352 was always true

2353 if isinstance(input, torch.Tensor): 2353 ↛ 2356line 2353 didn't jump to line 2356 because the condition on line 2353 was always true

2354 kwargs["pixel_values"] = input 

2355 else: 

2356 raise ValueError( 

2357 "Visual models require tensor input (pixel values). " 

2358 "Pass a torch.Tensor as `input` or use the pixel_values parameter." 

2359 ) 

2360 result = self._driver.forward(**kwargs) 

2361 elif _is_inputs_embeds: 

2362 result = self._driver.forward(inputs_embeds=input_ids, **kwargs) 

2363 else: 

2364 # By keyword so kw-only ``input_ids`` drivers don't TypeError. 

2365 result = self._driver.forward(input_ids=input_ids, **kwargs) 

2366 output = result.raw_output 

2367 # No-op for HF (its hooks already fired); load-bearing for vLLM/Inspect. 

2368 if result.captured: 2368 ↛ 2369line 2368 didn't jump to line 2369 because the condition on line 2368 was never true

2369 self._replay_captures(result.captured) 

2370 # Convert TensorLike to torch at the boundary; let weird shapes 

2371 # (audio/CTC dataclasses, tuple-of-tuples) pass through unchanged — 

2372 # downstream return_type branches catch them with specific errors. 

2373 logits = result.logits 

2374 if isinstance(logits, torch.Tensor): 2374 ↛ 2376line 2374 didn't jump to line 2376 because the condition on line 2374 was always true

2375 pass 

2376 elif logits is not None and isinstance(logits, TensorLike): 

2377 logits = to_torch(logits) 

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 return_type == "logits_and_cache": 

2382 past_key_values = getattr(output, "past_key_values", None) 

2383 return (logits, past_key_values) 

2384 if is_encoder_decoder and return_type in ("loss", "both"): 

2385 assert isinstance( 

2386 logits, torch.Tensor 

2387 ), f"Expected seq2seq logits tensor, got {type(logits)}" 

2388 assert isinstance(labels, torch.Tensor) 

2389 return self._finalize_seq2seq_return( 

2390 return_type, 

2391 logits, 

2392 labels, 

2393 output, 

2394 loss_per_token=loss_per_token, 

2395 ) 

2396 return self._finalize_return( 

2397 return_type, 

2398 logits, 

2399 input_ids, 

2400 attention_mask=attention_mask, 

2401 labels=labels, 

2402 is_audio_model=getattr(self.cfg, "is_audio_model", False), 

2403 is_visual_model=getattr(self.cfg, "is_visual_model", False), 

2404 inputs_embeds_was_used=_is_inputs_embeds, 

2405 loss_per_token=loss_per_token, 

2406 ) 

2407 except StopAtLayerException as e: 

2408 # Execution stopped at the requested layer 

2409 return e.layer_output 

2410 finally: 

2411 # Clean up state that may be inconsistent after StopAtLayerException 

2412 if stop_at_layer is not None: 

2413 for bl_name in ( 

2414 "blocks", 

2415 "encoder_blocks", 

2416 "decoder_blocks", 

2417 "L_blocks", 

2418 "H_blocks", 

2419 ): 

2420 if hasattr(self, bl_name): 

2421 for block in getattr(self, bl_name): 

2422 block._stop_at_layer_idx = None 

2423 

2424 # Clear any stale KV cache — layers after the stop point didn't 

2425 # execute, so the cache is incomplete and would corrupt subsequent 

2426 # generate() calls that expect a full cache. 

2427 if hasattr(self, "_last_hf_cache"): 2427 ↛ 2428line 2427 didn't jump to line 2428 because the condition on line 2427 was never true

2428 del self._last_hf_cache 

2429 

2430 if start_at_layer is not None: 

2431 self._teardown_start_at_layer() 

2432 

2433 def _setup_start_at_layer(self, input: Any, start_at_layer: int) -> torch.Tensor: 

2434 """Arm residual-stream injection at block ``start_at_layer``. 

2435 

2436 ``input`` is the residual entering that block. It is returned (batch-promoted) 

2437 to drive the HF forward as ``inputs_embeds`` so position ids / attention mask 

2438 are computed for the right sequence length; block ``start_at_layer`` then swaps 

2439 it back in for its own input, discarding whatever blocks 0..k-1 produced. 

2440 """ 

2441 if getattr(self.cfg, "is_visual_model", False): 

2442 raise NotImplementedError( 

2443 "start_at_layer is not supported for vision models: the residual " 

2444 "re-entry path assumes token inputs_embeds." 

2445 ) 

2446 if getattr(self.cfg, "is_audio_model", False): 

2447 raise NotImplementedError( 

2448 "start_at_layer is not supported for audio models: audio encoders " 

2449 "process waveforms through convolutional feature extraction before " 

2450 "the transformer blocks, making residual-stream injection infeasible." 

2451 ) 

2452 for alt in ("encoder_blocks", "decoder_blocks", "L_blocks", "H_blocks"): 

2453 if hasattr(self, alt): 2453 ↛ 2454line 2453 didn't jump to line 2454 because the condition on line 2453 was never true

2454 raise NotImplementedError( 

2455 "start_at_layer is only supported on the standard 'blocks' stack, " 

2456 f"not {alt!r}." 

2457 ) 

2458 if not self._has_registered_blocks(): 

2459 raise NotImplementedError("start_at_layer requires a 'blocks' stack.") 

2460 

2461 if not (isinstance(input, torch.Tensor) and input.is_floating_point()): 

2462 raise ValueError( 

2463 "start_at_layer requires a residual-stream tensor [batch, pos, d_model]; " 

2464 f"got {type(input).__name__}. Capture it from a prior run_with_cache " 

2465 "(e.g. cache['blocks.k.hook_in'])." 

2466 ) 

2467 residual = input if input.ndim == 3 else input.unsqueeze(0) 

2468 n_blocks = len(self.blocks) 

2469 if start_at_layer < 0: 

2470 start_at_layer += n_blocks 

2471 if not 0 <= start_at_layer < n_blocks: 

2472 raise ValueError(f"start_at_layer={start_at_layer} out of range [0, {n_blocks}).") 

2473 # The returned tensor is fed to HF as inputs_embeds; stash a clone so an 

2474 # architecture that mutates inputs_embeds in place can't corrupt the value 

2475 # block ``start_at_layer`` swaps back in. 

2476 injected = residual.clone() 

2477 for block in self.blocks: 

2478 block._start_at_layer_idx = start_at_layer 

2479 block._start_residual = injected 

2480 return residual 

2481 

2482 def _teardown_start_at_layer(self) -> None: 

2483 """Clear the residual-injection state set by ``_setup_start_at_layer``.""" 

2484 if hasattr(self, "blocks"): 2484 ↛ exitline 2484 didn't return from function '_teardown_start_at_layer' because the condition on line 2484 was always true

2485 for block in self.blocks: 

2486 block._start_at_layer_idx = None 

2487 block._start_residual = None 

2488 

2489 # loss_fn inherited from BridgeCore 

2490 

2491 def _resolve_stopping_criteria( 

2492 self, 

2493 stop_strings: Optional[Union[str, List[str]]], 

2494 stopping_criteria: Optional[Any], 

2495 ) -> Optional[Any]: 

2496 """Combine ``stop_strings`` and ``stopping_criteria`` into one StoppingCriteriaList. 

2497 

2498 Returns ``None`` when neither is supplied (or both reduce to no-ops), 

2499 so callers can cheaply check whether any extra stop signal is active. 

2500 ``stop_strings`` is turned into a HuggingFace ``StopStringCriteria`` (which reproduces 

2501 HF's exact partial-token-aware, end-anchored matching: it fires when the stop string 

2502 ends the generated text, even if the string straddles token boundaries) and therefore 

2503 requires a tokenizer. 

2504 A user-supplied ``stopping_criteria`` may be a single ``StoppingCriteria``, 

2505 a list of them, or a ``StoppingCriteriaList``. 

2506 

2507 Raises: 

2508 ValueError: if ``stop_strings`` is supplied without a tokenizer. 

2509 TypeError: if ``stopping_criteria`` is not a ``StoppingCriteria``, a 

2510 list/tuple of them, or a ``StoppingCriteriaList``. 

2511 """ 

2512 if stop_strings is None and stopping_criteria is None: 

2513 return None 

2514 

2515 from transformers import ( # local import: matches the file's transformers usage 

2516 StoppingCriteria, 

2517 StoppingCriteriaList, 

2518 StopStringCriteria, 

2519 ) 

2520 

2521 criteria = StoppingCriteriaList() 

2522 

2523 if stop_strings is not None: 

2524 strings = [stop_strings] if isinstance(stop_strings, str) else list(stop_strings) 

2525 strings = [s for s in strings if s] # drop empty strings (HF errors on them) 

2526 if strings: 

2527 if self.tokenizer is None: 

2528 raise ValueError( 

2529 "stop_strings requires a tokenizer (stop strings are detected by " 

2530 "matching against the tokenizer vocabulary), but this TransformerBridge " 

2531 "has no tokenizer. Pass a stopping_criteria callable that operates on " 

2532 "token ids instead, or use hf_generate()." 

2533 ) 

2534 criteria.append(StopStringCriteria(tokenizer=self.tokenizer, stop_strings=strings)) 

2535 

2536 if stopping_criteria is not None: 

2537 if isinstance(stopping_criteria, StoppingCriteriaList): 

2538 criteria.extend(stopping_criteria) 

2539 elif isinstance(stopping_criteria, (list, tuple)): 

2540 criteria.extend(stopping_criteria) 

2541 elif isinstance(stopping_criteria, StoppingCriteria): 

2542 criteria.append(stopping_criteria) 

2543 else: 

2544 raise TypeError( 

2545 "stopping_criteria must be a transformers.StoppingCriteria, a list of " 

2546 f"them, or a StoppingCriteriaList, but got {type(stopping_criteria).__name__}." 

2547 ) 

2548 

2549 return criteria if len(criteria) > 0 else None 

2550 

2551 def _encdec_ngram_processor(self) -> Optional[Any]: 

2552 """generation_config.no_repeat_ngram_size as transformers' own 

2553 processor, or None. HF applies it by default; parity for models whose 

2554 greedy decode needs it to escape token attractors.""" 

2555 size = getattr( 

2556 getattr(self.original_model, "generation_config", None), 

2557 "no_repeat_ngram_size", 

2558 None, 

2559 ) 

2560 if not size: 

2561 return None 

2562 from transformers.generation.logits_process import NoRepeatNGramLogitsProcessor 

2563 

2564 return NoRepeatNGramLogitsProcessor(size) 

2565 

2566 def _generate_tokens( 

2567 self, 

2568 current_tokens: torch.Tensor, 

2569 input_tokens: torch.Tensor, 

2570 batch_size: int, 

2571 *, 

2572 max_new_tokens: int, 

2573 do_sample: bool, 

2574 top_k: Optional[int], 

2575 top_p: Optional[float], 

2576 temperature: float, 

2577 freq_penalty: float, 

2578 repetition_penalty: float, 

2579 stop_at_eos: bool, 

2580 stop_tokens: List[int], 

2581 eos_token_for_padding: int, 

2582 finished_sequences: torch.Tensor, 

2583 use_past_kv_cache: bool, 

2584 use_stateful_cache: bool, 

2585 mamba_cache: Any, 

2586 mamba_conv_kernel: int, 

2587 is_encoder_decoder: bool, 

2588 _is_batched_list: bool, 

2589 _generate_from_embeds: bool, 

2590 encoder_input: Optional[torch.Tensor], 

2591 decoder_tokens: Optional[torch.Tensor], 

2592 generated_token_ids: Optional[List[torch.Tensor]], 

2593 pixel_values: Optional[torch.Tensor], 

2594 multimodal_kwargs: Dict[str, Any], 

2595 verbose: bool, 

2596 stopping_criteria_list: Optional[Any] = None, 

2597 initial_attention_mask: Optional[torch.Tensor] = None, 

2598 min_decoder_length: Optional[int] = None, 

2599 ngram_processor: Optional[Any] = None, 

2600 encoder_attention_mask: Optional[torch.Tensor] = None, 

2601 ) -> Generator[Tuple[torch.Tensor, torch.Tensor, bool], None, None]: 

2602 """Core generation loop. Yields (sampled_tokens, final_logits, all_finished) per step. 

2603 

2604 Owns the forward pass, sampling, stop handling (EOS and any 

2605 ``stopping_criteria_list``), token accumulation, and KV cache management. Callers 

2606 are responsible for try/finally cleanup of ``_capture_hf_cache``. 

2607 

2608 ``stopping_criteria_list`` (from ``_resolve_stopping_criteria``) is evaluated on 

2609 the running sequence each step and folded into the finished-sequence mask alongside 

2610 EOS, so when it is ``None`` the loop runs the EOS-only path unchanged. 

2611 """ 

2612 _hf_kv_cache = None 

2613 # A prior multimodal forward caches mrope deltas on the wrapped model, and a 

2614 # text-only generation would inherit them through the KV-cache position path 

2615 # (HF's own generate recomputes them at prefill; driving forward() directly 

2616 # makes that reset this loop's job). 

2617 base = getattr(self.original_model, "base_model", self.original_model) 

2618 if getattr(base, "rope_deltas", None) is not None: 

2619 base.rope_deltas = None 

2620 # A row may finish via EOS and/or any of the configured stopping criteria. 

2621 any_stop_active = stop_at_eos or stopping_criteria_list is not None 

2622 

2623 # Models that own their position derivation (the gate refuses them) cache 

2624 # mRoPE deltas on the module between calls; a text-only prefill never 

2625 # refreshes them, so a stale delta from an earlier multimodal forward gets 

2626 # added to every cached-step position. HF's generate recomputes them at 

2627 # prefill via prepare_inputs_for_generation, which this loop bypasses — 

2628 # so match it by clearing before the prompt pass. A multimodal prefill 

2629 # recomputes its own fresh deltas regardless. 

2630 if not self._accepts_derived_position_ids(): 

2631 underlying = getattr(self, "original_model", None) 

2632 for module in ( 

2633 underlying, 

2634 getattr(underlying, "model", None), 

2635 getattr(underlying, "language_model", None), 

2636 ): 

2637 if module is not None and hasattr(module, "rope_deltas"): 

2638 module.rope_deltas = None 

2639 

2640 # Pure-SSM models (Mamba-1/2) take the stateful cache as `cache_params`; 

2641 # modern hybrids (Bamba, NemotronH, FalconH1) take `past_key_values` and 

2642 # would receive a duplicate cache_params via **kwargs cascade otherwise. 

2643 stateful_cache_kwarg = "cache_params" 

2644 if use_stateful_cache: 

2645 forward_params = inspect.signature(self.original_model.forward).parameters 

2646 if "cache_params" not in forward_params: 

2647 stateful_cache_kwarg = "past_key_values" 

2648 

2649 for gen_step_idx in tqdm.tqdm(range(max_new_tokens), disable=not verbose): 

2650 with torch.no_grad(): 

2651 if is_encoder_decoder: 

2652 assert encoder_input is not None 

2653 encdec_kwargs: Dict[str, Any] = {} 

2654 if encoder_attention_mask is not None: 

2655 encdec_kwargs["attention_mask"] = encoder_attention_mask.to( 

2656 encoder_input.device 

2657 ) 

2658 logits = self( 

2659 encoder_input, 

2660 return_type="logits", 

2661 decoder_input=decoder_tokens, 

2662 **encdec_kwargs, 

2663 ) 

2664 else: 

2665 forward_kwargs: Dict[str, Any] = {} 

2666 # A prompt mask covers only the prompt, so extend it by one 

2667 # attended column per token generated so far. position_ids are 

2668 # left to forward(), which derives them from the mask for the 

2669 # models that can take them. 

2670 running_attention_mask: Optional[torch.Tensor] = None 

2671 if initial_attention_mask is not None: 

2672 n_generated = current_tokens.shape[1] - initial_attention_mask.shape[1] 

2673 running_attention_mask = torch.cat( 

2674 [ 

2675 initial_attention_mask.to(current_tokens.device), 

2676 torch.ones( 

2677 (current_tokens.shape[0], n_generated), 

2678 dtype=initial_attention_mask.dtype, 

2679 device=current_tokens.device, 

2680 ), 

2681 ], 

2682 dim=1, 

2683 ) 

2684 forward_kwargs["attention_mask"] = running_attention_mask 

2685 # Compute attention mask and position_ids for batched 

2686 # inputs with padding. 

2687 if ( 

2688 initial_attention_mask is None 

2689 and _is_batched_list 

2690 and self.tokenizer is not None 

2691 and self.tokenizer.pad_token_id is not None 

2692 ): 

2693 attn_mask = utils.get_attention_mask( 

2694 self.tokenizer, 

2695 current_tokens, 

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

2697 padding_side="left", 

2698 ).to(self.cfg.device) 

2699 forward_kwargs["attention_mask"] = attn_mask 

2700 # Same target gate as the forward() path: the mask is safe 

2701 # for every model, the derived positions are not (#1626). 

2702 if self._accepts_derived_position_ids(): 

2703 position_ids = attn_mask.long().cumsum(-1) - 1 

2704 position_ids.masked_fill_(attn_mask == 0, 1) 

2705 forward_kwargs["position_ids"] = position_ids 

2706 if gen_step_idx == 0: 

2707 if pixel_values is not None: 

2708 forward_kwargs["pixel_values"] = pixel_values 

2709 if multimodal_kwargs: 

2710 forward_kwargs.update(multimodal_kwargs) 

2711 if use_stateful_cache: 

2712 forward_kwargs[stateful_cache_kwarg] = mamba_cache 

2713 forward_kwargs["use_cache"] = True 

2714 if gen_step_idx == 0: 

2715 # Mamba's conv-window warmup positions vs standard 

2716 # full-prompt positions for past_key_values hybrids. 

2717 prefill_len = ( 

2718 mamba_conv_kernel 

2719 if stateful_cache_kwarg == "cache_params" 

2720 else current_tokens.shape[1] 

2721 ) 

2722 cache_position = torch.arange(0, prefill_len, device=self.cfg.device) 

2723 forward_kwargs["cache_position"] = cache_position 

2724 logits = self( 

2725 current_tokens, 

2726 return_type="logits", 

2727 **forward_kwargs, 

2728 ) 

2729 else: 

2730 input_seq_pos = input_tokens.shape[1] + gen_step_idx - 1 

2731 cache_position = torch.tensor([input_seq_pos], device=self.cfg.device) 

2732 forward_kwargs["cache_position"] = cache_position 

2733 if "position_ids" in forward_kwargs: 2733 ↛ 2734line 2733 didn't jump to line 2734 because the condition on line 2733 was never true

2734 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

2735 :, -1: 

2736 ] 

2737 logits = self( 

2738 current_tokens[:, -1:], 

2739 return_type="logits", 

2740 **forward_kwargs, 

2741 ) 

2742 elif use_past_kv_cache: 

2743 forward_kwargs["use_cache"] = True 

2744 if _hf_kv_cache is not None: 

2745 forward_kwargs["past_key_values"] = _hf_kv_cache 

2746 # HF v5 + macOS-arm64 NaNs when these are inferred 

2747 # from cache state alone. Mirror HF generate(): pass 

2748 # both an (batch, total_len) attention_mask and a 

2749 # (batch, 1) position_ids for the new token. 

2750 batch_size = current_tokens.shape[0] 

2751 total_len = current_tokens.shape[1] 

2752 device = current_tokens.device 

2753 if "attention_mask" not in forward_kwargs: 

2754 forward_kwargs["attention_mask"] = torch.ones( 

2755 (batch_size, total_len), 

2756 dtype=torch.long, 

2757 device=device, 

2758 ) 

2759 # Gated as a whole (#1626): every branch below supplies 

2760 # position_ids, so gating only the prompt derivation 

2761 # above would divert a refused model into the 

2762 # total_len - 1 fallback, which counts pad slots and is 

2763 # wrong per row for a left-padded batch. A model that 

2764 # owns its position derivation gets the mask alone, 

2765 # matching the uncached path. 

2766 if self._accepts_derived_position_ids(): 

2767 if "position_ids" in forward_kwargs: 

2768 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

2769 :, -1: 

2770 ] 

2771 elif running_attention_mask is not None: 

2772 # total_len - 1 counts pad slots, so it is wrong 

2773 # for a left-padded prompt. Derive the new token's 

2774 # position from the mask instead. 

2775 forward_kwargs["position_ids"] = utils.get_offset_position_ids( 

2776 0, running_attention_mask.long() 

2777 )[:, -1:] 

2778 else: 

2779 forward_kwargs["position_ids"] = torch.full( 

2780 (batch_size, 1), 

2781 total_len - 1, 

2782 dtype=torch.long, 

2783 device=device, 

2784 ) 

2785 logits = self( 

2786 current_tokens[:, -1:], 

2787 return_type="logits", 

2788 **forward_kwargs, 

2789 ) 

2790 else: 

2791 logits = self( 

2792 current_tokens, 

2793 return_type="logits", 

2794 **forward_kwargs, 

2795 ) 

2796 else: 

2797 logits = self(current_tokens, return_type="logits", **forward_kwargs) 

2798 if use_past_kv_cache and hasattr(self, "_last_hf_cache"): 

2799 _hf_kv_cache = self._last_hf_cache or _hf_kv_cache 

2800 del self._last_hf_cache 

2801 final_logits = logits[:, -1, :] 

2802 

2803 # Sample next token 

2804 penalty_tokens = ( 

2805 torch.stack(generated_token_ids, dim=1) 

2806 if _generate_from_embeds and generated_token_ids 

2807 else None 

2808 ) 

2809 # transformers' own NoRepeatNGramLogitsProcessor, honoring 

2810 # generation_config (bart-large-cnn pins 3; without it greedy 

2811 # decoding falls into a BOS attractor and emits nothing). 

2812 if ngram_processor is not None and decoder_tokens is not None: 

2813 final_logits = ngram_processor(decoder_tokens, final_logits) 

2814 # HF's generate() suppresses EOS below generation_config.min_length 

2815 # (bart-large-cnn pins 56); without this the loop can EOS on step 

2816 # one and emit an empty summary. 

2817 if ( 

2818 min_decoder_length is not None 

2819 and is_encoder_decoder 

2820 and decoder_tokens is not None 

2821 and decoder_tokens.shape[1] < min_decoder_length 

2822 and stop_tokens 

2823 ): 

2824 final_logits = final_logits.clone() 

2825 final_logits[:, stop_tokens] = float("-inf") 

2826 if do_sample: 

2827 sampled_tokens = utils.sample_logits( 

2828 final_logits, 

2829 top_k=top_k, 

2830 top_p=top_p, 

2831 temperature=temperature, 

2832 freq_penalty=freq_penalty, 

2833 repetition_penalty=repetition_penalty, 

2834 tokens=( 

2835 penalty_tokens 

2836 if _generate_from_embeds 

2837 else (decoder_tokens if is_encoder_decoder else current_tokens) 

2838 ), 

2839 ).to(self.cfg.device) 

2840 else: 

2841 sampled_tokens = utils.sample_logits( 

2842 final_logits, 

2843 temperature=0.0, 

2844 repetition_penalty=repetition_penalty, 

2845 tokens=( 

2846 penalty_tokens 

2847 if _generate_from_embeds 

2848 else (decoder_tokens if is_encoder_decoder else current_tokens) 

2849 ), 

2850 ).to(self.cfg.device) 

2851 

2852 # Freeze rows that finished on an earlier step so they stop emitting 

2853 # real tokens. Applies to every active stop mechanism, not just EOS. 

2854 if any_stop_active: 

2855 sampled_tokens[finished_sequences] = eos_token_for_padding 

2856 

2857 # Fold this step's EOS matches into the finished mask. 

2858 if stop_at_eos: 

2859 finished_sequences.logical_or_( 

2860 torch.isin( 

2861 sampled_tokens.to(self.cfg.device), 

2862 torch.tensor(stop_tokens).to(self.cfg.device), 

2863 ) 

2864 ) 

2865 

2866 # Update token sequences 

2867 if is_encoder_decoder: 

2868 assert decoder_tokens is not None 

2869 decoder_tokens = torch.cat([decoder_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

2870 elif _generate_from_embeds: 

2871 assert generated_token_ids is not None 

2872 generated_token_ids.append(sampled_tokens) 

2873 embed_fn = self.original_model.get_input_embeddings() # type: ignore[operator] 

2874 assert embed_fn is not None 

2875 new_embed = embed_fn(sampled_tokens.unsqueeze(1)).to(current_tokens.dtype) 

2876 current_tokens = torch.cat([current_tokens, new_embed], dim=1) 

2877 else: 

2878 current_tokens = torch.cat([current_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

2879 

2880 # Fold stop_strings / stopping_criteria into the finished mask. They are 

2881 # evaluated on the full running sequence (prompt + everything generated so 

2882 # far, including the token just appended) with this step's logits as the 

2883 # scores argument, matching transformers' StoppingCriteria contract. The 

2884 # combined list returns a per-row bool [batch] OR-ing every criterion. 

2885 # generate()/generate_stream() guarantee this is plain decoder-only token 

2886 # generation, so current_tokens is the running token sequence. 

2887 if stopping_criteria_list is not None: 

2888 criteria_finished = stopping_criteria_list(current_tokens, final_logits).to( 

2889 device=self.cfg.device, dtype=torch.bool 

2890 ) 

2891 if criteria_finished.shape != finished_sequences.shape: 

2892 raise ValueError( 

2893 "A stopping criterion returned shape " 

2894 f"{tuple(criteria_finished.shape)}, expected a per-row bool of " 

2895 f"shape {tuple(finished_sequences.shape)} (one entry per sequence)." 

2896 ) 

2897 finished_sequences.logical_or_(criteria_finished) 

2898 

2899 all_finished = bool(any_stop_active and finished_sequences.all().item()) 

2900 

2901 yield sampled_tokens, final_logits, all_finished 

2902 

2903 if all_finished: 2903 ↛ 2904line 2903 didn't jump to line 2904 because the condition on line 2903 was never true

2904 return 

2905 

2906 def _resolve_generation_caching(self, use_past_kv_cache: bool, batched: bool) -> bool: 

2907 """Honor adapter caching/batching limits (recurrent/conv decoders have no KV 

2908 cache; batching is rejected where padding can't be masked, not mis-generated).""" 

2909 if batched and not getattr(self.adapter, "supports_batched_generation", True): 

2910 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2911 raise NotImplementedError( 

2912 f"Batched generation is not supported by {architecture}: its forward does not " 

2913 "apply an attention mask, so padded rows would corrupt the output. Generate one " 

2914 "sequence at a time, or pass equal-length inputs as a tensor." 

2915 ) 

2916 if not getattr(self.adapter, "supports_kv_cache", True): 

2917 return False 

2918 return use_past_kv_cache 

2919 

2920 def _ensure_generation_supported(self, api_name: str) -> None: 

2921 """Reject autoregressive generation for forward-only architectures.""" 

2922 if not self.adapter.supports_generation: 

2923 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2924 hint = ( 

2925 " Use diffusion_generate() — this architecture samples by iterative denoising, " 

2926 "not left-to-right." 

2927 if getattr(self.adapter, "native_sampler", None) 

2928 else "" 

2929 ) 

2930 raise NotImplementedError( 

2931 f"TransformerBridge.{api_name}() generation is not supported by " 

2932 f"the {architecture} architecture.{hint}" 

2933 ) 

2934 

2935 def generate( 

2936 self, 

2937 input: Union[str, List[str], torch.Tensor] = "", 

2938 max_new_tokens: int = 10, 

2939 stop_at_eos: bool = True, 

2940 eos_token_id: Optional[int] = None, 

2941 do_sample: bool = True, 

2942 top_k: Optional[int] = None, 

2943 top_p: Optional[float] = None, 

2944 temperature: float = 1.0, 

2945 freq_penalty: float = 0.0, 

2946 repetition_penalty: float = 1.0, 

2947 use_past_kv_cache: bool = True, 

2948 prepend_bos: Optional[bool] = None, 

2949 padding_side: Optional[str] = None, 

2950 return_type: Optional[str] = "input", 

2951 verbose: bool = True, 

2952 output_logits: bool = False, 

2953 return_cache: bool = False, 

2954 return_input_tokens: bool = False, 

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

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

2957 pixel_values: Optional[torch.Tensor] = None, 

2958 stop_strings: Optional[Union[str, List[str]]] = None, 

2959 stopping_criteria: Optional[Any] = None, 

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

2961 forced_bos_token_id: Optional[int] = None, 

2962 **multimodal_kwargs, 

2963 ) -> ( 

2964 str 

2965 | list[str] 

2966 | torch.Tensor 

2967 | Any 

2968 | tuple[Any, ActivationCache] 

2969 | tuple[Any, torch.Tensor] 

2970 ): # Any for transformers.utils.ModelOutput 

2971 # Any: beartype forward ref limitation (beartype#546) 

2972 """Sample tokens from the model. 

2973 

2974 Sample tokens from the model until the model outputs eos_token or max_new_tokens is reached. 

2975 This implementation is based on HookedTransformer.generate() to ensure consistent behavior. 

2976 

2977 Args: 

2978 input: Text string, list of strings, or tensor of tokens 

2979 max_new_tokens: Maximum number of tokens to generate 

2980 stop_at_eos: If True, stop generating tokens when the model outputs eos_token 

2981 eos_token_id: The token ID to use for end of sentence 

2982 do_sample: If True, sample from the model's output distribution. Otherwise, use greedy search 

2983 top_k: Number of tokens to sample from. If None, sample from all tokens 

2984 top_p: Probability mass to sample from. If 1.0, sample from all tokens 

2985 temperature: Temperature for sampling. Higher values will make the model more random 

2986 freq_penalty: Frequency penalty for sampling - how much to penalise previous tokens 

2987 repetition_penalty: HuggingFace-style repetition penalty. Values > 1.0 discourage 

2988 repetition by dividing positive logits and multiplying negative logits for 

2989 previously seen tokens. Default 1.0 (no penalty). 

2990 use_past_kv_cache: If True, use KV caching for faster generation 

2991 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

2992 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

2993 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

2994 text that already contains the BOS token to avoid double-BOS. 

2995 Ignored when input is already a token tensor. 

2996 padding_side: Which side to pad when tokenizing multiple strings of different 

2997 lengths. For batched list inputs, left-padding is forced internally for 

2998 correct generation behavior. Defaults to None (tokenizer default). 

2999 return_type: The type of output to return - 'input', 'str', or 'tokens' 

3000 verbose: Not used in Bridge (kept for API compatibility) 

3001 output_logits: If True, return a ModelOutput with sequences and logits tuple 

3002 return_cache: If True, also return an ActivationCache for the full prompt + 

3003 generated sequence, identical to ``run_with_cache(output)``, and the call 

3004 returns an ``(output, cache)`` tuple. Implemented as one extra clean forward 

3005 over the output, so the cache includes every hook point (attention patterns 

3006 included). Supported only for single-sequence, decoder-only text generation; 

3007 encoder-decoder, SSM, multimodal, batched, and inputs_embeds inputs raise 

3008 NotImplementedError. The cache spans prompt + max_new_tokens and can be large, 

3009 use ``names_filter`` to scope it and/or ``device`` to offload it. 

3010 return_input_tokens: If True, return an ``(output, input_tokens)`` tuple where 

3011 ``input_tokens`` is the token tensor that was actually fed to the model 

3012 (after BOS handling). Useful for debugging tokenization, especially when 

3013 using chat templates where BOS handling can be subtle. Can be combined 

3014 with ``return_cache`` to get ``(output, cache, input_tokens)``. 

3015 names_filter: Passed to ``run_with_cache`` when ``return_cache=True``; restricts 

3016 which activations are cached (str, list of str, or callable). 

3017 device: Passed through when ``return_cache=True`` to offload the cached tensors 

3018 to this device (e.g. "cpu") to save accelerator memory. 

3019 pixel_values: Optional image tensor for multimodal models. Only passed on the 

3020 first generation step (the vision encoder processes the image once, then 

3021 embeddings are part of the token sequence for subsequent steps). 

3022 stop_strings: Optional string or list of strings. A sequence stops once its 

3023 generated text ends with one of these strings, using HuggingFace's 

3024 StopStringCriteria (partial-token-aware, end-anchored) matching. 

3025 Requires a tokenizer (raises ValueError otherwise). 

3026 Independent of stop_at_eos: either can stop a sequence. 

3027 stopping_criteria: Optional HuggingFace stopping criteria, a single 

3028 transformers.StoppingCriteria, a list of them, or a StoppingCriteriaList. 

3029 Each is called as criterion(input_ids, scores) after every step and ORed 

3030 with the other stop signals, where input_ids is the running sequence and 

3031 scores is this step's logits ([batch, d_vocab]). Each criterion must return 

3032 a per-row bool [batch] (or a scalar bool). stop_strings and stopping_criteria 

3033 are supported only for standard decoder-only text generation. Encoder-decoder, 

3034 inputs_embeds, and multimodal generation always raise NotImplementedError. 

3035 Stateful/SSM models raise only when run with use_past_kv_cache=False (the 

3036 default keeps them on the hooked loop). Each error names the supported 

3037 alternative. 

3038 attention_mask: Optional ``[batch, pos]`` 0/1 mask over the prompt, marking 

3039 which prompt tokens are real. Required to generate correctly from an 

3040 already-padded token tensor: without it the pad tokens are treated as 

3041 real context and every real token's position is shifted, so the 

3042 continuation differs from the same prompt unpadded. The mask is extended 

3043 by one attended column per generated token. Takes precedence over the 

3044 ``padding_side`` heuristic, and unlike it can express an interior gap or 

3045 a pad id that also occurs as a real token. Passing ``padding_side`` 

3046 instead reads the padding off the pad token, which is enough for the 

3047 common single-edge case, and raises if this bridge has no tokenizer 

3048 or pad id to read it from. On the encoder-decoder and inputs_embeds 

3049 paths the mask is forwarded to the model as-is rather than grown per 

3050 step, which is what processors emitting one alongside 

3051 ``pixel_values`` expect. 

3052 forced_bos_token_id: Optional token id seeded as the first decoder token 

3053 after ``decoder_start`` on encoder-decoder models. Multilingual 

3054 translators (M2M100/MBart/NLLB) select their target language this way. 

3055 Raises ValueError on decoder-only models. 

3056 

3057 Returns: 

3058 Generated sequence as string, list of strings, or tensor depending on input type and return_type. 

3059 If output_logits=True, returns a ModelOutput-like object with 'sequences' and 'logits' attributes. 

3060 If return_cache=True, returns an ``(output, ActivationCache)`` tuple where ``output`` is the 

3061 value that would otherwise be returned and the cache equals ``run_with_cache(output)``. 

3062 If return_input_tokens=True, returns an ``(output, input_tokens)`` tuple. 

3063 If both return_cache and return_input_tokens are True, returns ``(output, cache, input_tokens)``. 

3064 

3065 Example: 

3066 ``out, cache = model.generate(prompt, max_new_tokens=20, return_cache=True)`` returns a 

3067 normal ActivationCache over the full prompt + generated sequence (equivalent to 

3068 ``run_with_cache(out)``). 

3069 

3070 ``out, input_tokens = model.generate(prompt, return_input_tokens=True)`` returns 

3071 the tokens that were fed to the model, useful for verifying BOS handling with 

3072 chat templates. 

3073 """ 

3074 self._ensure_generation_supported("generate") 

3075 # padding_side is handled internally: for batched list inputs, left-padding 

3076 # is forced to ensure correct generation. See _is_batched_list logic below. 

3077 

3078 # Stateful dispatch is decided after input parsing so we can fall back 

3079 # to hf_generate() for input types the stateful loop doesn't handle. 

3080 is_stateful_model = getattr(self.cfg, "is_stateful", False) 

3081 

3082 _is_batched_list = isinstance(input, list) and len(input) > 1 

3083 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

3084 

3085 _generate_from_embeds = False 

3086 _encdec_early = hasattr(self.original_model, "config") and getattr( 

3087 self.original_model.config, "is_encoder_decoder", False 

3088 ) 

3089 if isinstance(input, str): 

3090 if _encdec_early: 

3091 # Deliberate divergence: prepend_bos is IGNORED for enc-dec 

3092 # string/list input. Encoder input follows the tokenizer's own 

3093 # recipe (lang token + trailing </s>); to_tokens' decoder-style 

3094 # BOS policy corrupts it — m2m100 degenerates to loops. 

3095 input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to( 

3096 self.cfg.device 

3097 ) 

3098 else: 

3099 input_tokens = self.to_tokens( 

3100 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3101 ) 

3102 input_type = "str" 

3103 elif isinstance(input, list): 

3104 if _encdec_early: 

3105 # Same native-recipe rule as the str branch: to_tokens' BOS 

3106 # policy corrupts encoder inputs (stray <s>, dropped </s>). 

3107 # Keep the tokenizer's mask too — unequal rows otherwise 

3108 # attend over pads in the encoder. 

3109 _enc_batch = self.tokenizer(input, return_tensors="pt", padding=True) 

3110 input_tokens = _enc_batch["input_ids"].to(self.cfg.device) 

3111 if attention_mask is None and "attention_mask" in _enc_batch: 3111 ↛ 3125line 3111 didn't jump to line 3125 because the condition on line 3111 was always true

3112 attention_mask = _enc_batch["attention_mask"].to(self.cfg.device) 

3113 else: 

3114 # Force left-padding for batched generation so real tokens are 

3115 # flush-right and logits[:, -1, :] is always the last real token. 

3116 # Passed as a kwarg rather than assigned: a raise between assignment 

3117 # and restore would pin the shared tokenizer left for the session. 

3118 input_tokens = self.to_tokens( 

3119 input, 

3120 prepend_bos=prepend_bos, 

3121 padding_side="left" if _is_batched_list else None, 

3122 move_to_device=True, 

3123 truncate=False, 

3124 ) 

3125 input_type = "list" 

3126 elif isinstance(input, torch.Tensor) and input.is_floating_point(): 

3127 # inputs_embeds: pre-computed embeddings (e.g., from multimodal models) 

3128 input_tokens = input.to(self.cfg.device) 

3129 input_type = "embeds" 

3130 _generate_from_embeds = True 

3131 else: 

3132 input_tokens = input.to(self.cfg.device) 

3133 input_type = "tokens" 

3134 

3135 # Without one of these a pre-padded tensor generates as though its pads were 

3136 # real context, shifting every real token's position (#1612). An explicit 

3137 # mask wins; otherwise the padding is read off the tokens, but only when the 

3138 # caller asked for that by passing padding_side. Deriving a mask on the 

3139 # default path would silently change behaviour for every existing caller, 

3140 # and would demand a real tokenizer where today none is required. 

3141 initial_attention_mask: Optional[torch.Tensor] = attention_mask 

3142 if initial_attention_mask is not None and ( 

3143 _generate_from_embeds 

3144 or getattr(getattr(self.original_model, "config", None), "is_encoder_decoder", False) 

3145 ): 

3146 # Growing the mask per step only means something for decoder-only token 

3147 # generation. On these paths the mask used to arrive via 

3148 # **multimodal_kwargs and be forwarded to the model untouched — as 

3149 # processors emit it alongside pixel_values — so keep doing that rather 

3150 # than reject a call that worked before this parameter existed. 

3151 multimodal_kwargs = {**multimodal_kwargs, "attention_mask": initial_attention_mask} 

3152 initial_attention_mask = None 

3153 if initial_attention_mask is not None: 

3154 if initial_attention_mask.shape != input_tokens.shape: 

3155 raise ValueError( 

3156 f"attention_mask shape {tuple(initial_attention_mask.shape)} does not " 

3157 f"match the prompt shape {tuple(input_tokens.shape)}. Pass a 0/1 mask " 

3158 "covering exactly the prompt tokens; generate() extends it itself." 

3159 ) 

3160 initial_attention_mask = initial_attention_mask.to(self.cfg.device) 

3161 elif padding_side is not None and input_type == "tokens": 

3162 # Reading the padding off the tokens needs a tokenizer with a pad id. 

3163 # Without one the argument would be inert, leaving exactly the bug this 

3164 # fixes — silently, on a bridge booted without a tokenizer. Say so 

3165 # rather than generate something quietly wrong. 

3166 if not isinstance(self.tokenizer, PreTrainedTokenizerBase): 

3167 raise ValueError( 

3168 "generate(padding_side=...) reads the padding off the pad token, " 

3169 "which needs a tokenizer; this bridge has none. Pass " 

3170 "attention_mask=... to state the padding directly instead." 

3171 ) 

3172 if self.tokenizer.pad_token_id is None: 

3173 raise ValueError( 

3174 "generate(padding_side=...) reads the padding off the pad token, " 

3175 "but this tokenizer has no pad_token_id. Set one, or pass " 

3176 "attention_mask=... to state the padding directly instead." 

3177 ) 

3178 _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos 

3179 _orig_side = self.tokenizer.padding_side 

3180 self.tokenizer.padding_side = padding_side 

3181 try: 

3182 initial_attention_mask = utils.get_attention_mask( 

3183 self.tokenizer, input_tokens, _prepend 

3184 ).to(self.cfg.device) 

3185 finally: 

3186 self.tokenizer.padding_side = _orig_side 

3187 # An all-ones mask is what the model assumes anyway; skipping it keeps 

3188 # the unpadded path byte-identical to before. 

3189 if initial_attention_mask is not None and bool(initial_attention_mask.all()): 3189 ↛ 3190line 3189 didn't jump to line 3190 because the condition on line 3189 was never true

3190 initial_attention_mask = None 

3191 

3192 # Determine return type 

3193 if return_type == "input": 

3194 if input_type in ["str", "list"]: 

3195 return_type = "str" 

3196 elif input_type == "embeds": 

3197 return_type = "tokens" 

3198 else: 

3199 return_type = "tokens" 

3200 

3201 batch_size = input_tokens.shape[0] 

3202 

3203 # Setup EOS token handling 

3204 stop_tokens = [] 

3205 eos_token_for_padding = 0 

3206 if stop_at_eos: 

3207 tokenizer_has_eos_token = ( 

3208 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

3209 ) 

3210 if eos_token_id is None: 

3211 # Some chat models use a turn-end token that differs from the 

3212 # tokenizer's primary EOS. Let adapters provide the full stop 

3213 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

3214 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

3215 if eos_token_id is None: 

3216 assert ( 

3217 tokenizer_has_eos_token 

3218 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

3219 assert self.tokenizer is not None 

3220 eos_token_id = self.tokenizer.eos_token_id 

3221 

3222 if isinstance(eos_token_id, int): 

3223 stop_tokens = [eos_token_id] 

3224 eos_token_for_padding = eos_token_id 

3225 else: 

3226 stop_tokens = list(eos_token_id) 

3227 if tokenizer_has_eos_token: 

3228 assert self.tokenizer is not None 

3229 eos_token_for_padding = self.tokenizer.eos_token_id 

3230 else: 

3231 eos_token_for_padding = eos_token_id[0] 

3232 

3233 # Track which sequences have finished 

3234 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

3235 

3236 # Optionally collect logits at each generation step for downstream tooling/tests 

3237 logits_seq_list: list[torch.Tensor] | None = [] if output_logits else None 

3238 

3239 # Detect encoder-decoder models (T5, BART, etc.) 

3240 is_encoder_decoder = hasattr(self.original_model, "config") and getattr( 

3241 self.original_model.config, "is_encoder_decoder", False 

3242 ) 

3243 if forced_bos_token_id is None and is_encoder_decoder: 

3244 # HF's generate() applies generation_config defaults; bart-large-cnn 

3245 # pins forced_bos_token_id=0 there and degrades without it. 

3246 forced_bos_token_id = getattr( 

3247 getattr(self.original_model, "generation_config", None), 

3248 "forced_bos_token_id", 

3249 None, 

3250 ) 

3251 if forced_bos_token_id is not None and not is_encoder_decoder: 3251 ↛ 3254line 3251 didn't jump to line 3254 because the condition on line 3251 was never true

3252 # Raise before any state mutation (_capture_hf_cache) and before 

3253 # the stateful hf_generate early-return would drop the kwarg. 

3254 raise ValueError("forced_bos_token_id is only meaningful for encoder-decoder models") 

3255 

3256 # return_cache recomputes run_with_cache on the generated output (see issue #697). 

3257 # That is well-defined only for single-sequence, decoder-only text generation, so 

3258 # reject the paths whose cache would be wrong/undefined, with a clear pointer to the 

3259 # run_with_cache workaround. Fail fast here, before any generation work. 

3260 if return_cache: 

3261 if is_encoder_decoder: 3261 ↛ 3262line 3261 didn't jump to line 3262 because the condition on line 3261 was never true

3262 raise NotImplementedError( 

3263 "generate(return_cache=True) is not supported for encoder-decoder " 

3264 "models yet. Run run_with_cache on the generated output instead." 

3265 ) 

3266 if is_stateful_model: 3266 ↛ 3267line 3266 didn't jump to line 3267 because the condition on line 3266 was never true

3267 raise NotImplementedError( 

3268 "generate(return_cache=True) is not supported for stateful/SSM models " 

3269 "(e.g. Mamba); they do not expose standard transformer hook points." 

3270 ) 

3271 if pixel_values is not None or multimodal_kwargs: 3271 ↛ 3272line 3271 didn't jump to line 3272 because the condition on line 3271 was never true

3272 raise NotImplementedError( 

3273 "generate(return_cache=True) is not supported for multimodal generation " 

3274 "yet. Run run_with_cache on the generated output instead." 

3275 ) 

3276 if _generate_from_embeds: 

3277 raise NotImplementedError( 

3278 "generate(return_cache=True) requires token input, not inputs_embeds." 

3279 ) 

3280 if batch_size > 1: 

3281 raise NotImplementedError( 

3282 "generate(return_cache=True) is not supported for batched/multi-prompt " 

3283 "generation yet. Pass a single prompt, or run run_with_cache on each " 

3284 "output sequence." 

3285 ) 

3286 

3287 # HF cache flows opaquely through the component chain via 

3288 # _reconstruct_attention() → _update_kv_cache() on each layer. 

3289 _hf_kv_cache = None 

3290 if use_past_kv_cache and is_encoder_decoder: 

3291 # Encoder-decoder models (T5, BART) don't support the opaque 

3292 # cache path — silently disable rather than crash, since 

3293 # use_past_kv_cache=True is the default. 

3294 use_past_kv_cache = False 

3295 

3296 # SSMs (Mamba/Mamba-2) run through a dedicated cache path so hooks 

3297 # fire on every step. Unsupported input types fall back to hf_generate(). 

3298 use_stateful_cache = ( 

3299 is_stateful_model 

3300 and use_past_kv_cache 

3301 and not is_encoder_decoder 

3302 and not _generate_from_embeds 

3303 and pixel_values is None 

3304 and not multimodal_kwargs 

3305 ) 

3306 

3307 # stop_strings / stopping_criteria are applied inside the hooked _generate_tokens 

3308 # loop, so they are supported only on the standard decoder-only text path. Reject 

3309 # the paths that route around that loop with a clear error rather than silently 

3310 # dropping the kwargs. This must run before the stateful delegation below. 

3311 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

3312 if stopping_criteria_list is not None: 

3313 if is_encoder_decoder: 

3314 _unsupported = "encoder-decoder models" 

3315 elif _generate_from_embeds: 

3316 _unsupported = "inputs_embeds generation" 

3317 elif pixel_values is not None or multimodal_kwargs: 

3318 _unsupported = "multimodal (pixel_values) generation" 

3319 else: 

3320 _unsupported = None 

3321 if _unsupported is not None: 

3322 raise NotImplementedError( 

3323 f"stop_strings/stopping_criteria are not supported for {_unsupported} in " 

3324 "TransformerBridge.generate(). Call hf_generate(...), which runs " 

3325 "HuggingFace's own generation loop and supports HF-native stopping on " 

3326 "those inputs." 

3327 ) 

3328 if is_stateful_model and not use_stateful_cache: 

3329 # Reached only for a stateful/SSM model with use_past_kv_cache=False: the 

3330 # hooked loop needs the stateful cache, so generate() would otherwise fall 

3331 # back to hf_generate() and drop these kwargs. The default cache setting 

3332 # keeps generation on the hooked loop, where stopping is applied. 

3333 raise NotImplementedError( 

3334 "stop_strings/stopping_criteria on a stateful/SSM model require the " 

3335 "stateful cache path, which runs only with use_past_kv_cache=True (the " 

3336 "default). With use_past_kv_cache=False generate() falls back to " 

3337 "hf_generate(). Set use_past_kv_cache=True to keep stopping on the hooked " 

3338 "loop, or call hf_generate(...) directly for HF-native stopping." 

3339 ) 

3340 # Finished rows are overwritten with this id so they stop emitting real tokens 

3341 # while the rest of a batch keeps going. stop_at_eos already set a sensible 

3342 # value, otherwise fall back to the tokenizer pad/eos id. (For a single 

3343 # sequence this id is never read: the loop exits when the row finishes.) 

3344 if not stop_at_eos: 

3345 _pad_id = None 

3346 if self.tokenizer is not None: 

3347 _pad_id = ( 

3348 self.tokenizer.pad_token_id 

3349 if self.tokenizer.pad_token_id is not None 

3350 else self.tokenizer.eos_token_id 

3351 ) 

3352 if _pad_id is not None: 

3353 eos_token_for_padding = _pad_id 

3354 elif batch_size > 1: 

3355 raise ValueError( 

3356 "Batched generation with stopping_criteria and stop_at_eos=False " 

3357 "needs a padding token to freeze finished rows, but no tokenizer " 

3358 "pad/eos id is available. Set stop_at_eos=True, use a tokenizer with " 

3359 "a pad or eos token, or generate one sequence at a time." 

3360 ) 

3361 

3362 if is_stateful_model and not use_stateful_cache: 

3363 hf_kwargs: dict[str, Any] = { 

3364 "max_new_tokens": max_new_tokens, 

3365 "do_sample": do_sample, 

3366 "temperature": temperature, 

3367 } 

3368 if top_k is not None: 3368 ↛ 3369line 3368 didn't jump to line 3369 because the condition on line 3368 was never true

3369 hf_kwargs["top_k"] = top_k 

3370 if top_p is not None: 3370 ↛ 3371line 3370 didn't jump to line 3371 because the condition on line 3370 was never true

3371 hf_kwargs["top_p"] = top_p 

3372 if eos_token_id is not None: 3372 ↛ 3376line 3372 didn't jump to line 3376 because the condition on line 3372 was always true

3373 hf_kwargs["eos_token_id"] = eos_token_id 

3374 # generate() already derived and shape-validated this mask; dropping it here 

3375 # would let the model attend to pad positions on the stateful fallback. 

3376 if initial_attention_mask is not None: 3376 ↛ 3378line 3376 didn't jump to line 3378 because the condition on line 3376 was always true

3377 hf_kwargs["attention_mask"] = initial_attention_mask 

3378 return self.hf_generate(input, **hf_kwargs) 

3379 

3380 # SSM cache is built once and mutated in place across forward calls. 

3381 # Adapter owns the cache-type choice; new SSMs just override 

3382 # create_stateful_cache(). 

3383 mamba_cache: Any = None 

3384 mamba_conv_kernel: int = 0 

3385 if use_stateful_cache: 

3386 hf_model: Any = self.original_model 

3387 mamba_conv_kernel = int(getattr(hf_model.config, "conv_kernel", 4)) 

3388 cache_dtype = self.cfg.dtype or torch.float32 

3389 mamba_cache = self.adapter.create_stateful_cache( 

3390 hf_model=hf_model, 

3391 batch_size=batch_size, 

3392 device=self.cfg.device, 

3393 dtype=cache_dtype, 

3394 ) 

3395 

3396 if use_past_kv_cache and not use_stateful_cache: 

3397 self._capture_hf_cache = True # Signal forward() to stash cache 

3398 

3399 # Generate tokens 

3400 current_tokens = input_tokens.clone() 

3401 # For inputs_embeds generation, also track generated token IDs for decoding 

3402 if _generate_from_embeds: 

3403 generated_token_ids: list[torch.Tensor] = [] 

3404 sampled_tokens_list = [] 

3405 

3406 # For encoder-decoder models, keep encoder input fixed and grow decoder input 

3407 if is_encoder_decoder: 

3408 encoder_input = input_tokens.clone() 

3409 decoder_start_token_id = getattr( 

3410 self.original_model.config, "decoder_start_token_id", None 

3411 ) 

3412 if decoder_start_token_id is None: 

3413 # HF's fallback chain: bos, then eos (MBart-family checkpoints 

3414 # like IndicBART leave decoder_start unset and start from EOS). 

3415 fallback = getattr(self.original_model.config, "bos_token_id", None) 

3416 if fallback is None: 

3417 fallback = getattr(self.original_model.config, "eos_token_id", None) 

3418 if isinstance(fallback, (list, tuple)): 3418 ↛ 3419line 3418 didn't jump to line 3419 because the condition on line 3418 was never true

3419 fallback = fallback[0] 

3420 decoder_start_token_id = fallback if fallback is not None else 0 

3421 decoder_tokens = torch.full( 

3422 (batch_size, 1), 

3423 decoder_start_token_id, 

3424 dtype=input_tokens.dtype, 

3425 device=self.cfg.device, 

3426 ) 

3427 if forced_bos_token_id is not None: 

3428 # Multilingual seq2seq (M2M100/MBart/NLLB) selects the target 

3429 # language via the first decoder token after decoder_start. 

3430 forced = torch.full( 

3431 (batch_size, 1), 

3432 forced_bos_token_id, 

3433 dtype=input_tokens.dtype, 

3434 device=self.cfg.device, 

3435 ) 

3436 decoder_tokens = torch.cat([decoder_tokens, forced], dim=1) 

3437 

3438 try: 

3439 for sampled_tokens, final_logits, all_finished in self._generate_tokens( 

3440 current_tokens, 

3441 input_tokens, 

3442 batch_size, 

3443 max_new_tokens=max_new_tokens, 

3444 do_sample=do_sample, 

3445 top_k=top_k, 

3446 top_p=top_p, 

3447 temperature=temperature, 

3448 freq_penalty=freq_penalty, 

3449 repetition_penalty=repetition_penalty, 

3450 stop_at_eos=stop_at_eos, 

3451 stop_tokens=stop_tokens, 

3452 eos_token_for_padding=eos_token_for_padding, 

3453 finished_sequences=finished_sequences, 

3454 use_past_kv_cache=use_past_kv_cache, 

3455 use_stateful_cache=use_stateful_cache, 

3456 mamba_cache=mamba_cache, 

3457 mamba_conv_kernel=mamba_conv_kernel, 

3458 is_encoder_decoder=is_encoder_decoder, 

3459 _is_batched_list=_is_batched_list, 

3460 _generate_from_embeds=_generate_from_embeds, 

3461 encoder_input=encoder_input if is_encoder_decoder else None, 

3462 decoder_tokens=decoder_tokens if is_encoder_decoder else None, 

3463 generated_token_ids=generated_token_ids if _generate_from_embeds else None, 

3464 pixel_values=pixel_values, 

3465 multimodal_kwargs=multimodal_kwargs if multimodal_kwargs else {}, 

3466 verbose=verbose, 

3467 stopping_criteria_list=stopping_criteria_list, 

3468 initial_attention_mask=initial_attention_mask, 

3469 min_decoder_length=( 

3470 getattr( 

3471 getattr(self.original_model, "generation_config", None), 

3472 "min_length", 

3473 None, 

3474 ) 

3475 if is_encoder_decoder 

3476 else None 

3477 ), 

3478 ngram_processor=(self._encdec_ngram_processor() if is_encoder_decoder else None), 

3479 encoder_attention_mask=(attention_mask if is_encoder_decoder else None), 

3480 ): 

3481 sampled_tokens_list.append(sampled_tokens.unsqueeze(1)) 

3482 if logits_seq_list is not None: 

3483 logits_seq_list.append(final_logits.clone()) 

3484 if all_finished: 

3485 break 

3486 finally: 

3487 self._capture_hf_cache = False 

3488 if hasattr(self, "_last_hf_cache"): 3488 ↛ 3489line 3488 didn't jump to line 3489 because the condition on line 3488 was never true

3489 del self._last_hf_cache 

3490 

3491 sampled_tokens = torch.cat(sampled_tokens_list, dim=1) 

3492 if is_encoder_decoder: 

3493 # Reconstruct full decoder sequence: start token + generated tokens 

3494 decoder_seed_len = 2 if forced_bos_token_id is not None else 1 

3495 output_tokens = torch.cat([decoder_tokens[:, :decoder_seed_len], sampled_tokens], dim=1) 

3496 elif _generate_from_embeds: 

3497 # For inputs_embeds, we only have the generated token IDs (no input token IDs) 

3498 output_tokens = sampled_tokens 

3499 else: 

3500 output_tokens = torch.cat([input_tokens, sampled_tokens], dim=1) 

3501 

3502 # Build the formatted output (shape unchanged: ModelOutput / str / list[str] / tokens). 

3503 result: Any 

3504 if output_logits and logits_seq_list is not None: 

3505 from transformers.utils import ModelOutput # type: ignore 

3506 

3507 def _logits_to_tuple(logits_list: list[torch.Tensor]) -> tuple[torch.Tensor, ...]: 

3508 assert logits_list is not None 

3509 # Convert list of [batch, vocab] tensors to tuple 

3510 return tuple(logits_list) 

3511 

3512 try: 

3513 from transformers.generation.utils import GenerateDecoderOnlyOutput 

3514 

3515 # HF-compatible ModelOutput structure. 

3516 # GenerateDecoderOnlyOutput expects: sequences, scores (optional), logits (optional) 

3517 result = GenerateDecoderOnlyOutput( 

3518 sequences=cast(torch.LongTensor, output_tokens), 

3519 # HF's type hint says tuple[FloatTensor] but should be tuple[FloatTensor, ...] 

3520 # (variable-length tuple with one element per generated token) 

3521 logits=_logits_to_tuple(logits_seq_list), # type: ignore[arg-type] 

3522 ) 

3523 except (ImportError, AttributeError): 

3524 # Fallback if GenerateDecoderOnlyOutput not available in this transformers version 

3525 result = ModelOutput( 

3526 sequences=output_tokens, 

3527 logits=_logits_to_tuple(logits_seq_list), 

3528 ) 

3529 elif return_type == "str": 

3530 assert self.tokenizer is not None 

3531 if input_type == "str": 

3532 result = self.tokenizer.decode(output_tokens[0], skip_special_tokens=True) 

3533 else: 

3534 decoded_texts = [ 

3535 self.tokenizer.decode(tokens, skip_special_tokens=True) 

3536 for tokens in output_tokens 

3537 ] 

3538 result = decoded_texts[0] if len(decoded_texts) == 1 else decoded_texts 

3539 else: # return_type == "tokens" 

3540 result = output_tokens 

3541 

3542 if not return_cache and not return_input_tokens: 

3543 return result 

3544 

3545 if return_cache: 

3546 # return_cache: recompute one clean forward over the full generated sequence so the 

3547 # cache is identical to run_with_cache(output_tokens) - all hook points, including 

3548 # attention patterns. The guards above restrict this to single-sequence, decoder-only 

3549 # text generation (see issue #697). 

3550 _, cache = self.run_with_cache(output_tokens, names_filter=names_filter, device=device) 

3551 if return_input_tokens: 

3552 return result, cache, input_tokens 

3553 return result, cache 

3554 

3555 # return_input_tokens only (no cache) 

3556 return result, input_tokens 

3557 

3558 @torch.no_grad() 

3559 def diffusion_generate( 

3560 self, 

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

3562 max_new_tokens: int = 32, 

3563 prepend_bos: Optional[bool] = None, 

3564 **kwargs: Any, 

3565 ) -> Union[str, torch.Tensor]: 

3566 """Sample from a non-autoregressive (diffusion) architecture. 

3567 

3568 Delegates to the model's own sampler, which calls the model through 

3569 ``__call__`` so bridge hooks fire on every denoising step. 

3570 """ 

3571 sampler_name = getattr(self.adapter, "native_sampler", None) 

3572 architecture = self.cfg.architecture or type(self.adapter).__name__ 

3573 if sampler_name is None: 3573 ↛ 3574line 3573 didn't jump to line 3574 because the condition on line 3573 was never true

3574 raise NotImplementedError( 

3575 f"{architecture} has no native sampler; use generate() for autoregressive " 

3576 "architectures." 

3577 ) 

3578 sampler = getattr(self.original_model, sampler_name, None) 

3579 if sampler is None: 3579 ↛ 3580line 3579 didn't jump to line 3580 because the condition on line 3579 was never true

3580 raise NotImplementedError( 

3581 f"{architecture} declares native_sampler={sampler_name!r} but the loaded model " 

3582 "has no such method." 

3583 ) 

3584 

3585 was_string = isinstance(input, str) 

3586 if isinstance(input, list) and len(input) > 1: 3586 ↛ 3590line 3586 didn't jump to line 3590 because the condition on line 3586 was never true

3587 # Unequal prompts would be right-padded into the sampler's canvas, 

3588 # where pad tokens read as real context. generate() gates batching 

3589 # for the same reason; do not silently corrupt rows here. 

3590 raise NotImplementedError( 

3591 f"diffusion_generate() does not support batched prompts for {architecture}: " 

3592 "the native samplers condition on a padded canvas. Sample one prompt at a time." 

3593 ) 

3594 if isinstance(input, torch.Tensor): 3594 ↛ 3599line 3594 didn't jump to line 3599 because the condition on line 3594 was always true

3595 tokens = input.to(self.cfg.device) 

3596 else: 

3597 # Tokenization is the bridge's concern, not the sampler's — absorb 

3598 # prepend_bos here rather than forwarding it into native kwargs. 

3599 tokens = self.to_tokens( 

3600 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3601 ) 

3602 

3603 sampler_kwargs = self.adapter.native_sampler_kwargs(max_new_tokens, tokens.shape[-1]) 

3604 sampler_kwargs.update(kwargs) 

3605 output = sampler(tokens, **sampler_kwargs) 

3606 # Samplers return either bare ids or a generation output object. 

3607 sequences = getattr(output, "sequences", output) 

3608 # They also disagree on whether the prompt is included: Dream and Gidd 

3609 # return the whole canvas, LLaDA2 slices it off. Normalize to 

3610 # generate()'s contract (prompt + continuation). 

3611 if isinstance(sequences, torch.Tensor) and sequences.ndim == tokens.ndim: 3611 ↛ 3622line 3611 didn't jump to line 3622 because the condition on line 3611 was always true

3612 prompt_len = tokens.shape[-1] 

3613 # Compare on one device: torch.equal raises on a device mismatch, 

3614 # which some samplers produce by assembling output on CPU. 

3615 sequences = sequences.to(tokens.device) 

3616 includes_prompt = sequences.shape[-1] >= prompt_len and torch.equal( 

3617 sequences[..., :prompt_len], tokens 

3618 ) 

3619 if not includes_prompt: 

3620 sequences = torch.cat([tokens, sequences], dim=-1) 

3621 

3622 if was_string and self.tokenizer is not None: 3622 ↛ 3623line 3622 didn't jump to line 3623 because the condition on line 3622 was never true

3623 return self.tokenizer.decode(sequences[0], skip_special_tokens=True) 

3624 return sequences 

3625 

3626 @torch.no_grad() 

3627 def generate_stream( 

3628 self, 

3629 input: Union[str, List[str], torch.Tensor] = "", 

3630 max_new_tokens: int = 10, 

3631 max_tokens_per_yield: int = 25, 

3632 stop_at_eos: bool = True, 

3633 eos_token_id: Optional[int] = None, 

3634 do_sample: bool = True, 

3635 top_k: Optional[int] = None, 

3636 top_p: Optional[float] = None, 

3637 temperature: float = 1.0, 

3638 freq_penalty: float = 0.0, 

3639 repetition_penalty: float = 1.0, 

3640 use_past_kv_cache: bool = True, 

3641 prepend_bos: Optional[bool] = None, 

3642 padding_side: Optional[str] = None, 

3643 return_type: Optional[str] = "input", 

3644 verbose: bool = True, 

3645 stop_strings: Optional[Union[str, List[str]]] = None, 

3646 stopping_criteria: Optional[Any] = None, 

3647 ) -> Generator[Union[torch.Tensor, str, List[str]], None, None]: 

3648 """Stream tokens from the model as they are generated. 

3649 

3650 Yields batches of tokens progressively during generation rather than 

3651 waiting for the entire sequence. Uses the same core loop as generate(). 

3652 

3653 Args: 

3654 input: Text string, list of strings, or tensor of tokens. 

3655 max_new_tokens: Maximum number of tokens to generate. 

3656 max_tokens_per_yield: Yield accumulated tokens every this many steps. 

3657 stop_at_eos: If True, stop when eos_token is produced. 

3658 eos_token_id: Token ID(s) for end of sentence. Defaults to tokenizer's. 

3659 do_sample: If True, sample; otherwise greedy. 

3660 top_k: Top-k sampling. None means no filtering. 

3661 top_p: Nucleus sampling threshold. 

3662 temperature: Sampling temperature. 

3663 freq_penalty: Frequency penalty for previous tokens. 

3664 repetition_penalty: HF-style repetition penalty (>1.0 discourages repeats). 

3665 use_past_kv_cache: Use KV caching for faster generation. 

3666 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

3667 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

3668 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

3669 text that already contains the BOS token to avoid double-BOS. 

3670 Ignored when input is already a token tensor. 

3671 padding_side: Which side to pad for batched list inputs. Left-padding 

3672 is forced internally for batched generation. 

3673 return_type: 'input' (match input type), 'str', or 'tokens'. 

3674 verbose: Show progress bar. 

3675 stop_strings: Optional string or list of strings. A sequence stops once its 

3676 generated text ends with one of them (HF StopStringCriteria). Requires a 

3677 tokenizer. See generate() for details. 

3678 stopping_criteria: Optional transformers StoppingCriteria, list, or 

3679 StoppingCriteriaList, called as criterion(input_ids, scores) each step 

3680 (scores is the step's logits). See generate() for the full contract. 

3681 

3682 Yields: 

3683 Token tensors [batch, seq_len], or decoded text when return_type='str' - 

3684 a bare string for a single sequence and one string per batch row for a 

3685 larger batch, matching generate(). Chunks accumulate up to 

3686 max_tokens_per_yield tokens between yields; the first yield includes the 

3687 input tokens and subsequent yields contain only new tokens. 

3688 """ 

3689 self._ensure_generation_supported("generate_stream") 

3690 # --- Input parsing (mirrors generate()) --- 

3691 _is_batched_list = isinstance(input, list) and len(input) > 1 

3692 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

3693 

3694 _encdec_early = hasattr(self.original_model, "config") and getattr( 

3695 self.original_model.config, "is_encoder_decoder", False 

3696 ) 

3697 if isinstance(input, str): 

3698 if _encdec_early: 3698 ↛ 3700line 3698 didn't jump to line 3700 because the condition on line 3698 was never true

3699 # Native recipe: to_tokens' BOS policy corrupts encoder inputs. 

3700 input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to( 

3701 self.cfg.device 

3702 ) 

3703 else: 

3704 input_tokens = self.to_tokens( 

3705 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3706 ) 

3707 input_type = "str" 

3708 elif isinstance(input, list): 

3709 if _encdec_early: 3709 ↛ 3710line 3709 didn't jump to line 3710 because the condition on line 3709 was never true

3710 input_tokens = self.tokenizer(input, return_tensors="pt", padding=True)[ 

3711 "input_ids" 

3712 ].to(self.cfg.device) 

3713 elif _is_batched_list: 

3714 _orig_ps = self.tokenizer.padding_side 

3715 self.tokenizer.padding_side = "left" 

3716 try: 

3717 input_tokens = self.to_tokens( 

3718 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3719 ) 

3720 finally: 

3721 self.tokenizer.padding_side = _orig_ps 

3722 else: 

3723 input_tokens = self.to_tokens( 

3724 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3725 ) 

3726 input_type = "list" 

3727 else: 

3728 input_tokens = input.to(self.cfg.device) 

3729 input_type = "tokens" 

3730 

3731 if return_type == "input": 

3732 return_type = "str" if input_type in ["str", "list"] else "tokens" 

3733 

3734 batch_size = input_tokens.shape[0] 

3735 

3736 # --- EOS setup --- 

3737 stop_tokens: List[int] = [] 

3738 eos_token_for_padding = 0 

3739 if stop_at_eos: 

3740 tokenizer_has_eos_token = ( 

3741 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

3742 ) 

3743 if eos_token_id is None: 

3744 # Some chat models use a turn-end token that differs from the 

3745 # tokenizer's primary EOS. Let adapters provide the full stop 

3746 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

3747 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

3748 if eos_token_id is None: 

3749 assert ( 

3750 tokenizer_has_eos_token 

3751 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

3752 assert self.tokenizer is not None 

3753 eos_token_id = self.tokenizer.eos_token_id 

3754 if isinstance(eos_token_id, int): 

3755 stop_tokens = [eos_token_id] 

3756 eos_token_for_padding = eos_token_id 

3757 else: 

3758 stop_tokens = list(eos_token_id) 

3759 if tokenizer_has_eos_token: 3759 ↛ 3760line 3759 didn't jump to line 3760 because the condition on line 3759 was never true

3760 assert self.tokenizer is not None 

3761 eos_token_for_padding = self.tokenizer.eos_token_id 

3762 else: 

3763 eos_token_for_padding = eos_token_id[0] 

3764 

3765 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

3766 

3767 # stop_strings / stopping_criteria: build the combined criteria list (validates 

3768 # tokenizer for stop_strings). generate_stream only runs the decoder-only text 

3769 # path, so no path guards are needed here. 

3770 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

3771 if stopping_criteria_list is not None and not stop_at_eos: 

3772 _pad_id = None 

3773 if self.tokenizer is not None: 

3774 _pad_id = ( 

3775 self.tokenizer.pad_token_id 

3776 if self.tokenizer.pad_token_id is not None 

3777 else self.tokenizer.eos_token_id 

3778 ) 

3779 if _pad_id is not None: 

3780 eos_token_for_padding = _pad_id 

3781 elif batch_size > 1: 3781 ↛ 3790line 3781 didn't jump to line 3790 because the condition on line 3781 was always true

3782 raise ValueError( 

3783 "Batched generate_stream with stopping_criteria and stop_at_eos=False " 

3784 "needs a padding token to freeze finished rows, but no tokenizer pad/eos " 

3785 "id is available. Set stop_at_eos=True or use a tokenizer with a pad/eos " 

3786 "token." 

3787 ) 

3788 

3789 # --- Cache setup --- 

3790 if use_past_kv_cache: 

3791 self._capture_hf_cache = True 

3792 

3793 current_tokens = input_tokens.clone() 

3794 

3795 # --- Streaming loop --- 

3796 # All yields are token tensors [batch, seq_len]. Each yield contains 

3797 # only the newly generated tokens since the previous yield (the first 

3798 # yield additionally prepends the input tokens for context). 

3799 accumulated_tokens: Optional[torch.Tensor] = None 

3800 tokens_since_last_yield = 0 

3801 

3802 # Decoding each chunk alone splits any character whose bytes straddle a yield 

3803 # boundary into U+FFFD, and concatenating chunks cannot rebuild it. Decode the 

3804 # whole stream each time, emit only the new text, and hold back a trailing 

3805 # incomplete character until the tokens that finish it arrive. 

3806 decoded_history: Optional[torch.Tensor] = None 

3807 emitted_chars = [0] * batch_size 

3808 

3809 def _decode_delta(hold_incomplete: bool) -> List[str]: 

3810 assert self.tokenizer is not None and decoded_history is not None 

3811 deltas = [] 

3812 for row_idx, row in enumerate(decoded_history): 

3813 text = self.tokenizer.decode(row, skip_special_tokens=True) 

3814 if hold_incomplete: 

3815 text = text.rstrip("�") 

3816 deltas.append(text[emitted_chars[row_idx] :]) 

3817 emitted_chars[row_idx] = len(text) 

3818 return deltas 

3819 

3820 def _maybe_decode(tokens: torch.Tensor) -> Union[torch.Tensor, str, List[str]]: 

3821 nonlocal decoded_history 

3822 if return_type != "str": 

3823 return tokens 

3824 decoded_history = ( 

3825 tokens if decoded_history is None else torch.cat([decoded_history, tokens], dim=-1) 

3826 ) 

3827 deltas = _decode_delta(hold_incomplete=True) 

3828 return deltas[0] if len(deltas) == 1 else deltas 

3829 

3830 def _flush_held() -> Optional[Union[str, List[str]]]: 

3831 """Emit a withheld partial character at stream end rather than dropping it.""" 

3832 if return_type != "str" or decoded_history is None: 

3833 return None 

3834 deltas = _decode_delta(hold_incomplete=False) 

3835 if not any(deltas): 

3836 return None 

3837 return deltas[0] if len(deltas) == 1 else deltas 

3838 

3839 try: 

3840 for step_idx, (sampled_tokens, _, all_finished) in enumerate( 

3841 self._generate_tokens( 

3842 current_tokens, 

3843 input_tokens, 

3844 batch_size, 

3845 max_new_tokens=max_new_tokens, 

3846 do_sample=do_sample, 

3847 top_k=top_k, 

3848 top_p=top_p, 

3849 temperature=temperature, 

3850 freq_penalty=freq_penalty, 

3851 repetition_penalty=repetition_penalty, 

3852 stop_at_eos=stop_at_eos, 

3853 stop_tokens=stop_tokens, 

3854 eos_token_for_padding=eos_token_for_padding, 

3855 finished_sequences=finished_sequences, 

3856 use_past_kv_cache=use_past_kv_cache, 

3857 use_stateful_cache=False, 

3858 mamba_cache=None, 

3859 mamba_conv_kernel=0, 

3860 is_encoder_decoder=False, 

3861 _is_batched_list=_is_batched_list, 

3862 _generate_from_embeds=False, 

3863 encoder_input=None, 

3864 decoder_tokens=None, 

3865 generated_token_ids=None, 

3866 pixel_values=None, 

3867 multimodal_kwargs={}, 

3868 verbose=verbose, 

3869 stopping_criteria_list=stopping_criteria_list, 

3870 ) 

3871 ): 

3872 new_tokens = sampled_tokens.unsqueeze(-1) 

3873 

3874 if step_idx == 0: 

3875 accumulated_tokens = torch.cat([input_tokens, new_tokens], dim=-1) 

3876 tokens_since_last_yield = accumulated_tokens.shape[1] 

3877 else: 

3878 if accumulated_tokens is None: 

3879 accumulated_tokens = new_tokens 

3880 else: 

3881 accumulated_tokens = torch.cat([accumulated_tokens, new_tokens], dim=-1) 

3882 tokens_since_last_yield += 1 

3883 

3884 if tokens_since_last_yield >= max_tokens_per_yield: 

3885 yield _maybe_decode(accumulated_tokens) 

3886 tokens_since_last_yield = 0 

3887 accumulated_tokens = None 

3888 

3889 if all_finished: 

3890 if accumulated_tokens is not None: 

3891 yield _maybe_decode(accumulated_tokens) 

3892 accumulated_tokens = None 

3893 break 

3894 

3895 # Yield remainder after loop completes without break 

3896 if accumulated_tokens is not None: 

3897 yield _maybe_decode(accumulated_tokens) 

3898 held_back = _flush_held() 

3899 if held_back is not None: 

3900 yield held_back 

3901 finally: 

3902 self._capture_hf_cache = False 

3903 if hasattr(self, "_last_hf_cache"): 3903 ↛ 3904line 3903 didn't jump to line 3904 because the condition on line 3903 was never true

3904 del self._last_hf_cache 

3905 

3906 def hf_generate( 

3907 self, 

3908 input: str | list[str] | torch.Tensor = "", 

3909 max_new_tokens: int = 10, 

3910 stop_at_eos: bool = True, 

3911 eos_token_id: int | None = None, 

3912 do_sample: bool = True, 

3913 top_k: int | None = None, 

3914 top_p: float | None = None, 

3915 temperature: float = 1.0, 

3916 use_past_kv_cache: bool = True, 

3917 return_type: str | None = "input", 

3918 pixel_values: torch.Tensor | None = None, 

3919 **generation_kwargs, 

3920 ) -> str | list[str] | torch.Tensor | Any: # Any for HF ModelOutput types 

3921 # Any: beartype forward ref limitation (beartype#546) 

3922 """Generate text using the underlying HuggingFace model with full HF API support. 

3923 

3924 This method provides direct access to HuggingFace's generation API, forwarding all 

3925 generation parameters (including output_scores, output_logits, output_attentions, 

3926 output_hidden_states) directly to the underlying HF model. Use this when you need 

3927 full HuggingFace generation features not supported by the standard generate() method. 

3928 

3929 For the standard TransformerLens generation interface, use generate() instead. 

3930 

3931 Args: 

3932 input: Text string, list of strings, or tensor of tokens 

3933 max_new_tokens: Maximum number of tokens to generate 

3934 stop_at_eos: If True, stop generating tokens when the model outputs eos_token 

3935 eos_token_id: The token ID to use for end of sentence 

3936 do_sample: If True, sample from the model's output distribution 

3937 top_k: Number of tokens to sample from 

3938 top_p: Probability mass to sample from 

3939 temperature: Temperature for sampling 

3940 use_past_kv_cache: If True, use KV caching for faster generation 

3941 return_type: The type of output to return - 'input', 'str', or 'tokens' 

3942 **generation_kwargs: Additional HuggingFace generation parameters including: 

3943 - output_scores: Return generation scores 

3944 - output_logits: Return generation logits 

3945 - output_attentions: Return attention weights 

3946 - output_hidden_states: Return hidden states 

3947 - return_dict_in_generate: Return ModelOutput object 

3948 - And any other HF generation parameters 

3949 

3950 Returns: 

3951 Generated sequence as string, list of strings, tensor, or HF ModelOutput 

3952 depending on input type, return_type, and generation_kwargs. 

3953 

3954 Example:: 

3955 

3956 # Get full HF ModelOutput with logits and attentions 

3957 from transformer_lens import TransformerBridge 

3958 model = TransformerBridge.boot_transformers("tiny-stories-1M") 

3959 result = model.hf_generate( 

3960 "Hello world", 

3961 max_new_tokens=5, 

3962 output_logits=True, 

3963 output_attentions=True, 

3964 return_dict_in_generate=True 

3965 ) 

3966 print(result.sequences) # Generated tokens 

3967 print(result.logits) # Logits for each generation step 

3968 print(result.attentions) # Attention weights 

3969 """ 

3970 self._ensure_generation_supported("hf_generate") 

3971 input_attention_mask: torch.Tensor | None = None 

3972 # Handle string input by tokenizing it 

3973 if isinstance(input, str): 

3974 inputs = self.tokenizer(input, return_tensors="pt", padding=False, truncation=False).to( 

3975 self.cfg.device 

3976 ) 

3977 input_ids = inputs["input_ids"] 

3978 input_type = "str" 

3979 elif isinstance(input, list): 

3980 is_encoder_decoder = getattr( 

3981 getattr(self.original_model, "config", None), "is_encoder_decoder", False 

3982 ) 

3983 tokenizer_kwargs = {} if is_encoder_decoder else {"padding_side": "left"} 

3984 inputs = self.tokenizer( 

3985 input, 

3986 return_tensors="pt", 

3987 padding=True, 

3988 truncation=False, 

3989 **tokenizer_kwargs, 

3990 ).to(self.cfg.device) 

3991 input_ids = inputs["input_ids"] 

3992 input_attention_mask = inputs["attention_mask"] 

3993 input_type = "list" 

3994 else: 

3995 input_ids = input 

3996 if input_ids.device != self.cfg.device: 3996 ↛ 3997line 3996 didn't jump to line 3997 because the condition on line 3996 was never true

3997 input_ids = input_ids.to(self.cfg.device) 

3998 input_type = "tokens" 

3999 

4000 # Build generation_kwargs from explicit args and kwargs 

4001 generation_kwargs = dict(generation_kwargs) if generation_kwargs is not None else {} 

4002 if input_attention_mask is not None: 

4003 generation_kwargs["attention_mask"] = input_attention_mask 

4004 generation_kwargs.update( 

4005 { 

4006 "max_new_tokens": max_new_tokens, 

4007 "do_sample": do_sample, 

4008 "temperature": temperature, 

4009 "pad_token_id": self.tokenizer.eos_token_id, 

4010 } 

4011 ) 

4012 

4013 if top_k is not None: 4013 ↛ 4014line 4013 didn't jump to line 4014 because the condition on line 4013 was never true

4014 generation_kwargs["top_k"] = top_k 

4015 if top_p is not None: 4015 ↛ 4016line 4015 didn't jump to line 4016 because the condition on line 4015 was never true

4016 generation_kwargs["top_p"] = top_p 

4017 if eos_token_id is not None: 

4018 generation_kwargs["eos_token_id"] = eos_token_id 

4019 elif stop_at_eos and self.tokenizer.eos_token_id is not None: 4019 ↛ 4022line 4019 didn't jump to line 4022 because the condition on line 4019 was always true

4020 generation_kwargs["eos_token_id"] = self.tokenizer.eos_token_id 

4021 

4022 if pixel_values is not None: 4022 ↛ 4023line 4022 didn't jump to line 4023 because the condition on line 4022 was never true

4023 generation_kwargs["pixel_values"] = pixel_values 

4024 

4025 if use_past_kv_cache: 4025 ↛ 4029line 4025 didn't jump to line 4029 because the condition on line 4025 was always true

4026 generation_kwargs["use_cache"] = True 

4027 

4028 # HF dict flags that trigger ModelOutput returns 

4029 hf_dict_flags = ( 

4030 "output_scores", 

4031 "output_logits", 

4032 "output_attentions", 

4033 "output_hidden_states", 

4034 ) 

4035 

4036 # If any HF-style output flags are provided, ensure return_dict_in_generate is set 

4037 any_flag_set = False 

4038 for f in hf_dict_flags: 

4039 if generation_kwargs.get(f) is not None: 

4040 generation_kwargs[f] = bool(generation_kwargs[f]) 

4041 any_flag_set = True 

4042 

4043 if any_flag_set: 

4044 generation_kwargs.setdefault("return_dict_in_generate", True) 

4045 

4046 with torch.no_grad(): 

4047 outputs = self.original_model.generate(input_ids, **generation_kwargs) # type: ignore[operator] 

4048 

4049 # Check if output is a ModelOutput 

4050 try: 

4051 from transformers.utils import ModelOutput # type: ignore 

4052 

4053 is_model_output = isinstance(outputs, ModelOutput) 

4054 except Exception: 

4055 is_model_output = False 

4056 

4057 # Return based on return_type and input format 

4058 if return_type == "input" or return_type is None: 

4059 if input_type == "str": 

4060 # Decode the full output back to string 

4061 if is_model_output and hasattr(outputs, "sequences"): 4061 ↛ 4063line 4061 didn't jump to line 4063 because the condition on line 4061 was always true

4062 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4063 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4064 elif input_type == "list": 

4065 # Decode each sequence in the batch 

4066 if is_model_output and hasattr(outputs, "sequences"): 4066 ↛ 4071line 4066 didn't jump to line 4071 because the condition on line 4066 was always true

4067 return [ 

4068 self.tokenizer.decode(seq, skip_special_tokens=True) 

4069 for seq in outputs.sequences 

4070 ] 

4071 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4072 else: 

4073 # Return the full token sequence including input 

4074 return outputs 

4075 elif return_type == "tokens": 4075 ↛ 4079line 4075 didn't jump to line 4079 because the condition on line 4075 was always true

4076 return outputs 

4077 else: 

4078 # For other return types, default to the decoded text 

4079 if input_type == "str": 

4080 if is_model_output and hasattr(outputs, "sequences"): 

4081 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4082 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4083 elif input_type == "list": 

4084 if is_model_output and hasattr(outputs, "sequences"): 

4085 return [ 

4086 self.tokenizer.decode(seq, skip_special_tokens=True) 

4087 for seq in outputs.sequences 

4088 ] 

4089 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4090 else: 

4091 return outputs 

4092 

4093 def prepare_multimodal_inputs( 

4094 self, 

4095 text: Union[str, List[str]], 

4096 images: Optional[Any] = None, 

4097 ) -> Dict[str, torch.Tensor]: 

4098 """Prepare multimodal inputs using the model's processor. 

4099 

4100 Converts text and images into model-ready tensors (input_ids, pixel_values, 

4101 attention_mask, etc.) using the HuggingFace processor loaded during boot(). 

4102 

4103 Args: 

4104 text: Text prompt(s), typically containing image placeholder tokens 

4105 (e.g., "<image>" for LLaVA). 

4106 images: PIL Image or list of PIL Images to process. Pass None for 

4107 text-only inputs on a multimodal model. 

4108 

4109 Returns: 

4110 Dictionary with 'input_ids', 'pixel_values', 'attention_mask', etc. 

4111 All tensors are moved to the model's device. 

4112 

4113 Raises: 

4114 ValueError: If model is not multimodal or processor is not available. 

4115 """ 

4116 if not getattr(self.cfg, "is_multimodal", False): 

4117 raise ValueError( 

4118 "prepare_multimodal_inputs() requires a multimodal model " 

4119 "(cfg.is_multimodal must be True)" 

4120 ) 

4121 if self.processor is None: 

4122 raise ValueError( 

4123 "No processor available. Load model with boot_transformers() or " 

4124 "set bridge.processor = AutoProcessor.from_pretrained(...) manually." 

4125 ) 

4126 inputs = self.processor(text=text, images=images, return_tensors="pt") 

4127 return {k: v.to(self.cfg.device) if hasattr(v, "to") else v for k, v in inputs.items()} 

4128 

4129 def to(self, *args, **kwargs) -> "TransformerBridge": 

4130 """Move model to device and/or change dtype. 

4131 

4132 Args: 

4133 args: Positional arguments for nn.Module.to 

4134 kwargs: Keyword arguments for nn.Module.to 

4135 print_details: Whether to print details about device/dtype changes (default: True) 

4136 

4137 Returns: 

4138 Self for chaining 

4139 """ 

4140 # Extract print_details if provided 

4141 print_details = kwargs.pop("print_details", True) 

4142 

4143 # Handle both device and dtype changes 

4144 # torch.nn.Module.to() supports: to(device), to(dtype), to(device, dtype), 

4145 # to(device=...), to(dtype=...), to(device=..., dtype=...) 

4146 target_device, target_dtype = None, None 

4147 

4148 if len(args) >= 1: 

4149 first_arg = args[0] 

4150 if isinstance(first_arg, (torch.device, str)): 

4151 target_device = first_arg 

4152 elif isinstance(first_arg, torch.dtype): 4152 ↛ 4154line 4152 didn't jump to line 4154 because the condition on line 4152 was always true

4153 target_dtype = first_arg 

4154 if len(args) >= 2: 

4155 second_arg = args[1] 

4156 if isinstance(second_arg, torch.dtype): 4156 ↛ 4160line 4156 didn't jump to line 4160 because the condition on line 4156 was always true

4157 target_dtype = second_arg 

4158 

4159 # these override positional args 

4160 if "device" in kwargs: 4160 ↛ 4161line 4160 didn't jump to line 4161 because the condition on line 4160 was never true

4161 target_device = kwargs["device"] 

4162 if "dtype" in kwargs: 

4163 target_dtype = kwargs["dtype"] 

4164 

4165 # Moving a multi-device (device_map-dispatched) model to a single device would 

4166 # collapse the split and break accelerate's hook routing. Warn and drop the 

4167 # device move; still honor dtype changes. 

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

4169 warnings.warn( 

4170 f"TransformerBridge.to({target_device!r}) ignored: model is dispatched " 

4171 f"across {self.cfg.n_devices} devices via device_map. Reload with " 

4172 "device=... (and no device_map/n_devices) to move to a single device.", 

4173 stacklevel=2, 

4174 ) 

4175 target_device = None 

4176 

4177 if target_device is not None: 

4178 move_to_and_update_config(self, target_device, print_details) 

4179 if target_dtype is not None: 

4180 move_to_and_update_config(self, target_dtype, print_details) 

4181 

4182 # Move the original model with all original args/kwargs (with print_details removed). 

4183 # When we've nulled target_device for multi-GPU safety, strip device args so the 

4184 # underlying module isn't moved either. 

4185 if target_device is None and (len(args) > 0 or "device" in kwargs): 

4186 kwargs.pop("device", None) 

4187 # Filter positional args: drop devices/strings, keep dtypes. 

4188 args = tuple(a for a in args if not isinstance(a, (torch.device, str))) 

4189 self.original_model = self.original_model.to(*args, **kwargs) 

4190 return self 

4191 

4192 def cuda(self, device: Optional[Union[int, torch.device]] = None) -> "TransformerBridge": 

4193 """Move model to CUDA. 

4194 

4195 Args: 

4196 device: CUDA device 

4197 

4198 Returns: 

4199 Self for chaining 

4200 """ 

4201 if isinstance(device, int): 

4202 return self.to(f"cuda:{device}") 

4203 elif device is None: 

4204 return self.to("cuda") 

4205 else: 

4206 return self.to(device) 

4207 

4208 def cpu(self) -> "TransformerBridge": 

4209 """Move model to CPU. 

4210 

4211 Returns: 

4212 Self for chaining 

4213 """ 

4214 return self.to(torch.device("cpu")) 

4215 

4216 def mps(self) -> "TransformerBridge": 

4217 """Move model to MPS. 

4218 

4219 Returns: 

4220 Self for chaining 

4221 """ 

4222 return self.to(torch.device("mps")) 

4223 

4224 def train(self, mode: bool = True) -> "TransformerBridge": 

4225 """Set training mode, propagating to the wrapped source model. 

4226 

4227 ``original_model`` lives in ``__dict__`` rather than the registered 

4228 module tree, so the inherited ``nn.Module.train()`` recursion does not 

4229 reach it. 

4230 """ 

4231 super().train(mode) 

4232 original = getattr(self, "original_model", None) 

4233 if isinstance(original, torch.nn.Module): 

4234 original.train(mode) 

4235 return self 

4236 

4237 def set_use_attn_result(self, use_attn_result: bool): 

4238 """Toggle whether to explicitly calculate and expose the result for each attention head. 

4239 

4240 Useful for interpretability but can easily burn through GPU memory. 

4241 """ 

4242 if use_attn_result: 

4243 self._validate_attention_fork_supported("use_attn_result") 

4244 self.cfg._set_bridge_managed_hook_flag("use_attn_result", use_attn_result) 

4245 self._propagate_attention_flag("use_attn_result", use_attn_result) 

4246 

4247 def set_use_split_qkv_input(self, use_split_qkv_input: bool): 

4248 """Toggle independent residual copies for Q/K/V so each path can be patched alone. 

4249 

4250 Mutually exclusive with `use_attn_in` — set that flag off first if it's on. 

4251 """ 

4252 if use_split_qkv_input: 

4253 if bool(getattr(self.cfg, "use_attn_in", False)): 

4254 raise ValueError( 

4255 "use_split_qkv_input and use_attn_in are mutually exclusive. " 

4256 "Call set_use_attn_in(False) before enabling use_split_qkv_input." 

4257 ) 

4258 self._validate_attention_fork_supported("use_split_qkv_input") 

4259 self.cfg._set_bridge_managed_hook_flag("use_split_qkv_input", use_split_qkv_input) 

4260 self._propagate_attention_flag("use_split_qkv_input", use_split_qkv_input) 

4261 

4262 def set_use_attn_in(self, use_attn_in: bool): 

4263 """Toggle a single 4D residual copy feeding all three Q/K/V projections. 

4264 

4265 Mutually exclusive with `use_split_qkv_input` — set that flag off first 

4266 if it's on. When on, `hook_attn_in` fires at 

4267 `[batch, pos, n_heads, d_model]`, enabling coarse-grained interventions 

4268 on the residual-stream copy shared across Q/K/V. 

4269 """ 

4270 if use_attn_in: 

4271 if bool(getattr(self.cfg, "use_split_qkv_input", False)): 

4272 raise ValueError( 

4273 "use_attn_in and use_split_qkv_input are mutually exclusive. " 

4274 "Call set_use_split_qkv_input(False) before enabling use_attn_in." 

4275 ) 

4276 self._validate_attention_fork_supported("use_attn_in") 

4277 self.cfg._set_bridge_managed_hook_flag("use_attn_in", use_attn_in) 

4278 self._propagate_attention_flag("use_attn_in", use_attn_in) 

4279 

4280 def set_use_hook_mlp_in(self, use_hook_mlp_in: bool) -> None: 

4281 """Toggle the ``hook_mlp_in`` HookPoint (the MLP-branch entry: pre-ln2, or 

4282 the MLP input on post-norm blocks), matching legacy semantics. 

4283 """ 

4284 self.cfg._set_bridge_managed_hook_flag("use_hook_mlp_in", use_hook_mlp_in) 

4285 if not hasattr(self, "blocks"): 4285 ↛ 4286line 4285 didn't jump to line 4286 because the condition on line 4285 was never true

4286 return 

4287 for block in self.blocks: 

4288 block_cfg = getattr(block, "config", None) 

4289 if block_cfg is not None and block_cfg is not self.cfg: 

4290 try: 

4291 self._write_propagated_hook_flag(block_cfg, "use_hook_mlp_in", use_hook_mlp_in) 

4292 except (AttributeError, TypeError): 

4293 pass 

4294 block._use_hook_mlp_in = use_hook_mlp_in 

4295 

4296 @staticmethod 

4297 def _write_propagated_hook_flag(config: Any, flag_name: str, value: bool) -> None: 

4298 """Write a cloned config flag without dispatching through its live Bridge.""" 

4299 if isinstance(config, TransformerBridgeConfig): 4299 ↛ 4302line 4299 didn't jump to line 4302 because the condition on line 4299 was always true

4300 config._set_bridge_managed_hook_flag(flag_name, value) 

4301 else: 

4302 object.__setattr__(config, flag_name, value) 

4303 

4304 def _propagate_attention_flag(self, flag_name: str, value: bool) -> None: 

4305 """Mirror `bridge.cfg.<flag>` onto every block's attention config. 

4306 

4307 Some adapters (Llama family) deep-copy the block template during 

4308 `setup_blocks_bridge`, cloning the attention bridge's config along 

4309 with it. Others (Pythia, GPT-2) override `__deepcopy__` to share the 

4310 config. Setting the flag only on `self.cfg` silently misses the 

4311 cloned-config case. Propagating explicitly keeps both patterns 

4312 honest — a no-op when configs are shared, a correctness fix when 

4313 they aren't. 

4314 """ 

4315 if not hasattr(self, "blocks"): 4315 ↛ 4316line 4315 didn't jump to line 4316 because the condition on line 4315 was never true

4316 return 

4317 for block in self.blocks: 

4318 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

4319 if attn is None: 4319 ↛ 4320line 4319 didn't jump to line 4320 because the condition on line 4319 was never true

4320 continue 

4321 attn_cfg = getattr(attn, "config", None) 

4322 if attn_cfg is not None and attn_cfg is not self.cfg: 

4323 try: 

4324 self._write_propagated_hook_flag(attn_cfg, flag_name, value) 

4325 except (AttributeError, TypeError): 

4326 # Some config-like objects reject attributes even when 

4327 # bypassing their custom __setattr__ implementation. 

4328 pass 

4329 

4330 def _validate_attention_fork_supported(self, flag_name: str) -> None: 

4331 """Raise / warn if the model can't honor a fine-grained attention flag. 

4332 

4333 The post-ln1 fork path lives on JointQKVAttentionBridge and 

4334 PositionEmbeddingsAttentionBridge. Plain AttentionBridge delegates to 

4335 HF and exposes no fork point; we raise rather than setting the flag 

4336 silently. For hybrid models (some attention layers, some not), we warn 

4337 and list which layers will honor the flag. 

4338 """ 

4339 # Deferred imports: tight circular dependency with bridge setup. 

4340 from transformer_lens.model_bridge.generalized_components.joint_qkv_attention import ( 

4341 JointQKVAttentionBridge, 

4342 ) 

4343 from transformer_lens.model_bridge.generalized_components.position_embeddings_attention import ( 

4344 PositionEmbeddingsAttentionBridge, 

4345 ) 

4346 

4347 if not hasattr(self, "blocks"): 4347 ↛ 4348line 4347 didn't jump to line 4348 because the condition on line 4347 was never true

4348 raise NotImplementedError( 

4349 f"{flag_name}: this bridge has no `blocks` attribute, so no " 

4350 "attention bridges to apply the flag to." 

4351 ) 

4352 supported_classes = (JointQKVAttentionBridge, PositionEmbeddingsAttentionBridge) 

4353 supporting_layers: list[int] = [] 

4354 attn_classes: set[str] = set() 

4355 total_with_attn = 0 

4356 for idx, block in enumerate(self.blocks): 

4357 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

4358 if attn is None: 4358 ↛ 4359line 4358 didn't jump to line 4359 because the condition on line 4358 was never true

4359 continue 

4360 total_with_attn += 1 

4361 attn_classes.add(type(attn).__name__) 

4362 supports_flag = ( 

4363 bool(getattr(attn, "supports_attn_result", False)) 

4364 if flag_name == "use_attn_result" 

4365 else isinstance(attn, supported_classes) 

4366 ) 

4367 if supports_flag: 

4368 supporting_layers.append(idx) 

4369 if total_with_attn == 0: 4369 ↛ 4370line 4369 didn't jump to line 4370 because the condition on line 4369 was never true

4370 raise NotImplementedError(f"{flag_name}: no attention bridges found on self.blocks.") 

4371 if not supporting_layers: 

4372 if flag_name == "use_attn_result": 

4373 capability_detail = "Per-head result computation is unavailable." 

4374 else: 

4375 capability_detail = "No hook point is available before the Q/K/V projections." 

4376 raise NotImplementedError( 

4377 f"{flag_name}: none of this model's attention bridges support " 

4378 "the requested fine-grained attention hook. Found attention classes: " 

4379 f"{sorted(attn_classes)}. Supported classes: " 

4380 f"{[c.__name__ for c in supported_classes]}. {capability_detail}" 

4381 ) 

4382 if len(supporting_layers) < total_with_attn: 4382 ↛ 4383line 4382 didn't jump to line 4383 because the condition on line 4382 was never true

4383 skipped = total_with_attn - len(supporting_layers) 

4384 warnings.warn( 

4385 f"{flag_name}: {skipped} of {total_with_attn} attention layers " 

4386 "use an attention-bridge class that cannot honor this flag " 

4387 f"(attention classes present: {sorted(attn_classes)}). " 

4388 f"The flag will affect layers: {supporting_layers}.", 

4389 stacklevel=3, 

4390 ) 

4391 

4392 def _is_valid_bridge_path(self, hf_path: str) -> bool: 

4393 """Check if a HuggingFace path corresponds to a valid bridge component. 

4394 

4395 This validates that the path follows the bridge component structure and doesn't 

4396 contain nested HuggingFace components that should have been wrapped. 

4397 

4398 Args: 

4399 hf_path: HuggingFace path after removing _original_component 

4400 

4401 Returns: 

4402 True if the path is valid, False if it contains nested HF components 

4403 """ 

4404 parts = hf_path.split(".") 

4405 

4406 # Get the component mapping for validation 

4407 component_mapping = self.adapter.component_mapping 

4408 if not component_mapping: 4408 ↛ 4409line 4408 didn't jump to line 4409 because the condition on line 4408 was never true

4409 return True # If no mapping, accept all keys 

4410 

4411 # Walk through the path and check if each level is a registered bridge component 

4412 # For example, transformer.h.0.mlp.in.weight should be valid 

4413 # but transformer.h.0.mlp.c_fc.weight should be invalid (c_fc is nested HF component) 

4414 

4415 # Start from the root 

4416 current_component = None 

4417 idx = 0 

4418 

4419 # Find which top-level component this belongs to 

4420 for tl_name, component in component_mapping.items(): 

4421 if component.name and hf_path.startswith(component.name + "."): 

4422 current_component = component 

4423 # Skip past the HF prefix 

4424 remaining_path = hf_path[len(component.name) + 1 :] 

4425 parts = remaining_path.split(".") 

4426 idx = 0 

4427 break 

4428 

4429 if current_component is None: 

4430 return True # Path doesn't match any component, let it through 

4431 

4432 # Special handling for blocks 

4433 if hasattr(current_component, "is_list_item") and current_component.is_list_item: 

4434 # Skip the layer index 

4435 if idx < len(parts) and parts[idx].isdigit(): 4435 ↛ 4439line 4435 didn't jump to line 4439 because the condition on line 4435 was always true

4436 idx += 1 

4437 

4438 # Now validate the rest of the path against submodules 

4439 while idx < len(parts): 4439 ↛ 4466line 4439 didn't jump to line 4466 because the condition on line 4439 was always true

4440 part = parts[idx] 

4441 

4442 # If we hit 'weight' or 'bias', we're at a parameter - this is valid 

4443 if part in ("weight", "bias"): 

4444 return True 

4445 

4446 # Check if this part is a registered submodule 

4447 if hasattr(current_component, "submodules") and current_component.submodules: 

4448 if part in current_component.submodules: 

4449 current_component = current_component.submodules[part] 

4450 idx += 1 

4451 continue 

4452 else: 

4453 # This part is not a registered bridge component 

4454 # It's likely a nested HF component (like c_fc, c_proj, c_attn) 

4455 return False 

4456 else: 

4457 # No submodules to check, but not at a parameter yet 

4458 # Check if next is weight/bias 

4459 if idx + 1 < len(parts) and parts[idx + 1] in ("weight", "bias"): 

4460 return True 

4461 # Otherwise this is likely a nested HF component 

4462 return False 

4463 

4464 idx += 1 

4465 

4466 return True 

4467 

4468 def _normalize_bridge_key_to_hf(self, key: str) -> str: 

4469 """Normalize a key that uses bridge attribute names to use HF module names. 

4470 

4471 PyTorch's state_dict uses the Python attribute names (e.g., 'ln1') 

4472 but the conversion logic expects HF module names (e.g., 'ln_1'). This 

4473 function only replaces non-nested component names, leaving bridge 

4474 subcomponents (like 'in', 'out', 'q', 'k', 'v') unchanged since they're 

4475 handled by the component structure. 

4476 

4477 Args: 

4478 key: Key that may use bridge attribute names 

4479 

4480 Returns: 

4481 Key with attribute names replaced by module names where needed 

4482 """ 

4483 component_mapping = self.adapter.component_mapping 

4484 if not component_mapping: 4484 ↛ 4485line 4484 didn't jump to line 4485 because the condition on line 4484 was never true

4485 return key 

4486 

4487 # Build a mapping of only the direct module attribute names to HF names 

4488 # We only care about top-level and block-level component names, NOT subcomponents 

4489 attr_to_hf = {} 

4490 

4491 # Map top-level components 

4492 block_list_names = {"blocks", "L_blocks", "H_blocks", "encoder_blocks", "decoder_blocks"} 

4493 for tl_name, component in component_mapping.items(): 

4494 if component.name and tl_name not in block_list_names: 

4495 # Skip if TL name is already a segment of its HF path (avoids doubling). 

4496 if tl_name != component.name and tl_name not in component.name.split("."): 

4497 attr_to_hf[tl_name] = component.name 

4498 

4499 # Map block-level components (ln1, ln2, attn, mlp) for all block lists 

4500 for bl_name in block_list_names: 

4501 blocks_component = component_mapping.get(bl_name) 

4502 if blocks_component and hasattr(blocks_component, "submodules"): 

4503 for tl_subname, subcomponent in blocks_component.submodules.items(): 

4504 if subcomponent.name: 

4505 # Only map if the names differ (e.g., ln1 -> ln_1, but attn -> attn) 

4506 if tl_subname != subcomponent.name: 

4507 attr_to_hf[tl_subname] = subcomponent.name 

4508 

4509 # Replace only these specific attribute names in the key 

4510 # We need to be careful to only replace whole path components, not substrings 

4511 parts = key.split(".") 

4512 result_parts = [] 

4513 

4514 for part in parts: 

4515 if part in attr_to_hf: 

4516 result_parts.append(attr_to_hf[part]) 

4517 else: 

4518 result_parts.append(part) 

4519 

4520 return ".".join(result_parts) 

4521 

4522 def state_dict(self, destination=None, prefix="", keep_vars=False): 

4523 """Get state dict with TransformerLens format keys. 

4524 

4525 Converts HuggingFace format keys to TransformerLens format and filters out 

4526 _original_component references and nested HuggingFace components. 

4527 

4528 A direct no-argument call returns a clean state dict with bridge component 

4529 paths converted to TL format. Calls that supply ``destination`` or 

4530 ``prefix`` use standard ``nn.Module`` recursive semantics so a Bridge can 

4531 compose inside a parent module. 

4532 

4533 Args: 

4534 destination: Optional dict to store state dict in 

4535 prefix: Optional prefix to add to all keys 

4536 keep_vars: Whether to keep variables as Variables instead of tensors 

4537 

4538 Returns: 

4539 Direct calls return TransformerLens-format keys; recursive calls 

4540 return the supplied destination with standard module-tree keys. 

4541 """ 

4542 if destination is not None or prefix: 

4543 return super().state_dict( 

4544 destination=destination, 

4545 prefix=prefix, 

4546 keep_vars=keep_vars, 

4547 ) 

4548 

4549 raw_state_dict = self.original_model.state_dict(keep_vars=keep_vars) 

4550 

4551 # Clean _original_component references and convert to TL format 

4552 # Also filter out nested HuggingFace components that are wrapped by bridge components 

4553 tl_state_dict = {} 

4554 

4555 for key, value in raw_state_dict.items(): 

4556 # Skip _original_component keys 

4557 if key == "_original_component" or key.startswith("_original_component."): 4557 ↛ 4558line 4557 didn't jump to line 4558 because the condition on line 4557 was never true

4558 continue 

4559 

4560 # Remove all _original_component from the key 

4561 clean_key = key.replace("._original_component", "") 

4562 

4563 # Check if this is a valid bridge path (not a nested HF component) 

4564 if not self._is_valid_bridge_path(clean_key): 

4565 continue 

4566 

4567 # Normalize bridge component names to HF names for conversion 

4568 # (e.g., 'ln1' -> 'ln_1', 'mlp.in' -> 'mlp.c_fc') 

4569 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

4570 

4571 # Convert to TL format - this uses the adapter's component_mapping 

4572 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

4573 

4574 # Only add if we haven't seen this TL key yet (handles duplicates) 

4575 if tl_key not in tl_state_dict: 

4576 tl_state_dict[tl_key] = value 

4577 

4578 return tl_state_dict 

4579 

4580 def _tl_key_to_actual_keys(self) -> dict[str, list[str]]: 

4581 """Inverse of the renaming state_dict() applies: map each TL-format key 

4582 back to every raw parameter/buffer path that represents it. 

4583 

4584 Mirrors the filtering and key-conversion in state_dict() exactly, except 

4585 it keeps every raw key for a given TL key instead of only the first-seen 

4586 one. Bridge components frequently expose the same underlying parameter 

4587 through more than one attribute path (e.g. GPT-2's split q/k/v weights 

4588 are views into the wrapped module's combined c_attn weight, reachable 

4589 both via a block-level shortcut and via the nested _original_component 

4590 chain) - all of those aliases must be written for the round trip to 

4591 actually change what forward() reads, not just what state_dict() shows. 

4592 """ 

4593 mapping: dict[str, list[str]] = {} 

4594 for actual_key in self.original_model.state_dict(): 

4595 if actual_key == "_original_component" or actual_key.startswith("_original_component."): 4595 ↛ 4596line 4595 didn't jump to line 4596 because the condition on line 4595 was never true

4596 continue 

4597 clean_key = actual_key.replace("._original_component", "") 

4598 if not self._is_valid_bridge_path(clean_key): 

4599 continue 

4600 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

4601 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

4602 mapping.setdefault(tl_key, []).append(actual_key) 

4603 return mapping 

4604 

4605 def load_state_dict(self, state_dict, strict=True, assign=False): 

4606 """Load state dict into the model, handling both clean keys and original keys with _original_component references. 

4607 

4608 Accepts three key formats: TL-format keys as emitted by state_dict() 

4609 (e.g. "blocks.0.attn.q.weight"), raw native parameter paths (e.g. for 

4610 ``boot_native`` / tracr-style loading), and raw paths with 

4611 "_original_component" segments stripped. 

4612 

4613 Args: 

4614 state_dict: Dictionary containing a whole state of the module 

4615 strict: Whether to strictly enforce that the keys in state_dict match the keys returned by this module's state_dict() function 

4616 assign: Whether to assign items in the state dictionary to their corresponding keys in the module instead of copying them 

4617 

4618 Returns: 

4619 NamedTuple with missing_keys and unexpected_keys fields 

4620 """ 

4621 current_state_dict = self.original_model.state_dict(keep_vars=True) 

4622 clean_to_actual = {} 

4623 for actual_key in current_state_dict.keys(): 

4624 if actual_key != "_original_component": 4624 ↛ 4623line 4624 didn't jump to line 4623 because the condition on line 4624 was always true

4625 clean_to_actual[actual_key.replace("._original_component", "")] = actual_key 

4626 

4627 tl_to_actual = self._tl_key_to_actual_keys() 

4628 

4629 mapped_state_dict = {} 

4630 unexpected_keys = [] 

4631 for input_key, value in state_dict.items(): 

4632 if input_key in current_state_dict: 

4633 mapped_state_dict[input_key] = value 

4634 elif input_key in clean_to_actual: 

4635 mapped_state_dict[clean_to_actual[input_key]] = value 

4636 elif input_key in tl_to_actual: 

4637 for actual_key in tl_to_actual[input_key]: 

4638 mapped_state_dict[actual_key] = value 

4639 else: 

4640 unexpected_keys.append(input_key) 

4641 

4642 # A TL key's actual-key aliases share the same underlying storage (see 

4643 # _tl_key_to_actual_keys), so writing any one of them already updates 

4644 # what forward() reads for all of them. Treat the group as satisfied 

4645 # if any alias was written -- e.g. a caller supplying clean/raw keys 

4646 # (the branch above maps each clean key to exactly one actual key) 

4647 # shouldn't have the *other*, unwritten aliases reported as missing. 

4648 missing_keys = sorted( 

4649 actual_key 

4650 for actual_keys in tl_to_actual.values() 

4651 if not any(k in mapped_state_dict for k in actual_keys) 

4652 for actual_key in actual_keys 

4653 ) 

4654 

4655 if strict and (missing_keys or unexpected_keys): 

4656 error_msgs = [] 

4657 if unexpected_keys: 

4658 error_msgs.append( 

4659 "Unexpected key(s) in state_dict: " 

4660 + ", ".join(f'"{k}"' for k in sorted(unexpected_keys)) 

4661 ) 

4662 if missing_keys: 

4663 error_msgs.append( 

4664 "Missing key(s) in state_dict: " + ", ".join(f'"{k}"' for k in missing_keys) 

4665 ) 

4666 raise RuntimeError( 

4667 "Error(s) in loading state_dict for {}:\n\t{}".format( 

4668 type(self.original_model).__name__, "\n\t".join(error_msgs) 

4669 ) 

4670 ) 

4671 

4672 if not assign: 

4673 result = self.original_model.load_state_dict( 

4674 mapped_state_dict, strict=False, assign=False 

4675 ) 

4676 return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys) 

4677 

4678 # assign=True normally makes nn.Module.load_state_dict *replace* each 

4679 # target parameter/buffer with the incoming tensor rather than copying 

4680 # into existing storage. Any key whose current tensor shares storage 

4681 # with another key -- a split QKV/gate-up component's view into a 

4682 # combined weight, the combined weight itself (writing it directly 

4683 # would silently orphan the views), or a tied pair like embed/unembed 

4684 # -- would desync from whatever it shares storage with: the bridge 

4685 # might keep reading a stale value, or a live view not even part of 

4686 # this load would silently keep the pre-load data while 

4687 # original_model.state_dict() (what save_pretrained() exports) shows 

4688 # the new one. Route every key in a storage-sharing group through an 

4689 # explicit in-place .data.copy_() instead, regardless of the caller's 

4690 # assign=True, so those relationships survive. 

4691 shared_keys = _storage_group_keys(current_state_dict) 

4692 

4693 copy_items: dict[str, tuple[torch.Tensor, torch.Tensor]] = {} 

4694 passthrough_items = {} 

4695 errors = [] 

4696 for key, value in mapped_state_dict.items(): 

4697 target = current_state_dict.get(key) 

4698 if target is None or key not in shared_keys: 

4699 passthrough_items[key] = value 

4700 continue 

4701 problems = [] 

4702 if target.is_meta: 4702 ↛ 4706line 4702 didn't jump to line 4706 because the condition on line 4702 was never true

4703 # copy_() onto a meta tensor silently no-ops rather than 

4704 # raising, so a meta target would otherwise report a 

4705 # successful load while never actually materializing. 

4706 problems.append( 

4707 "the current parameter is a meta tensor; an in-place copy " 

4708 "can't materialize it, and this key shares storage with " 

4709 "another parameter so ordinary assign=True replacement " 

4710 "isn't safe here either" 

4711 ) 

4712 else: 

4713 if tuple(target.shape) != tuple(value.shape): 

4714 problems.append(f"shape {tuple(value.shape)} != expected {tuple(target.shape)}") 

4715 if target.dtype != value.dtype: 

4716 problems.append(f"dtype {value.dtype} != expected {target.dtype}") 

4717 if target.device != value.device: 

4718 problems.append(f"device {value.device} != expected {target.device}") 

4719 if problems: 

4720 errors.append(f'"{key}": ' + "; ".join(problems)) 

4721 else: 

4722 copy_items[key] = (target, value) 

4723 

4724 if errors: 

4725 raise RuntimeError( 

4726 "Cannot load the following key(s) with assign=True: each shares " 

4727 "storage with another parameter/buffer (e.g. a split QKV/" 

4728 "gate-up component's view into a combined weight, or a tied " 

4729 "pair like embed/unembed), so it can only be loaded via an " 

4730 "in-place copy, which requires an exact match.\n\t" + "\n\t".join(errors) 

4731 ) 

4732 

4733 for target, value in copy_items.values(): 

4734 with torch.no_grad(): 

4735 target.data.copy_(value) 

4736 

4737 result = self.original_model.load_state_dict(passthrough_items, strict=False, assign=True) 

4738 refresh_container_state_owners(self) 

4739 return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys) 

4740 

4741 def get_params(self): 

4742 """Access to model parameters in the format expected by SVDInterpreter. 

4743 

4744 For missing weights, returns zero tensors of appropriate shape instead of raising exceptions. 

4745 This ensures compatibility across different model architectures. 

4746 

4747 Returns: 

4748 dict: Dictionary of parameter tensors with TransformerLens naming convention 

4749 

4750 Raises: 

4751 ValueError: If configuration is inconsistent (e.g., cfg.n_layers != len(blocks)) 

4752 """ 

4753 return get_bridge_params(self) 

4754 

4755 # NOTE: list_supported_models and check_model_support are attached to this class 

4756 # dynamically by transformer_lens.model_bridge.sources.transformers module. 

4757 # These are HuggingFace-specific methods that belong in the transformers source module.