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

1860 statements  

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

23 Tuple, 

24 Union, 

25 cast, 

26) 

27 

28import einops 

29import numpy as np 

30import torch 

31import tqdm 

32from torch import nn 

33from torch.nn import functional as F 

34from transformers.tokenization_utils_base import PreTrainedTokenizerBase 

35 

36from transformer_lens import utilities as utils 

37from transformer_lens.ActivationCache import ActivationCache 

38from transformer_lens.config import TransformerBridgeConfig 

39from transformer_lens.FactoredMatrix import FactoredMatrix 

40from transformer_lens.hook_points import HookIntrospectionMixin, HookPoint 

41from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

42from transformer_lens.model_bridge.bridge_core import ( 

43 _BLOCK_LIST_ATTRS, 

44 _SELF_ATTENTION_NAMES, 

45 BridgeCore, 

46) 

47from transformer_lens.model_bridge.component_setup import ( 

48 refresh_container_state_owners, 

49 set_original_components, 

50) 

51from transformer_lens.model_bridge.composition_scores import CompositionScores 

52from transformer_lens.model_bridge.driver_protocol import ( 

53 TensorLike, 

54 to_torch, 

55 validate_driver, 

56) 

57from transformer_lens.model_bridge.exceptions import StopAtLayerException 

58from transformer_lens.model_bridge.generalized_components.base import ( 

59 GeneralizedComponent, 

60) 

61from transformer_lens.model_bridge.generalized_components.block import ( 

62 _BLOCK_INTERNAL_MODULES, 

63 _NORM_PREFIXES, 

64 _VARIANT_SUBMODULE_SET, 

65 VARIANT_SUBMODULE_NAMES, 

66) 

67from transformer_lens.model_bridge.generalized_components.moe import ( 

68 fold_scale_into_moe_block, 

69 has_batched_experts, 

70) 

71from transformer_lens.model_bridge.get_params_util import get_bridge_params 

72from transformer_lens.utilities.activation_functions import softcap_enabled 

73from transformer_lens.utilities.devices import move_to_and_update_config 

74from transformer_lens.utilities.position_ids import accepts_mask_derived_position_ids 

75from transformer_lens.utilities.quantization import require_readable_weight 

76 

77if TYPE_CHECKING: 

78 pass 

79 

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

81 

82 

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

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

85 result = obj 

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

87 result = getattr(result, attr) 

88 return cast(torch.Tensor, result) 

89 

90 

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

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

93 

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

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

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

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

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

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

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

101 

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

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

104 comparing meta tensors by data pointer would spuriously group every 

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

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

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

108 """ 

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

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

111 if tensor.is_meta: 

112 continue 

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

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

115 

116 

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

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

119 

120 Provides a standardized interface to access components of a transformer 

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

122 to map between the TransformerLens and HuggingFace model structures. 

123 

124 Stateless reparametrization is unsupported 

125 ------------------------------------------ 

126 

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

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

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

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

131 installed. For temporary weight edits use 

132 :func:`transformer_lens.utilities.temporarily_swap_parameter`. 

133 

134 Tokenization notes 

135 ------------------ 

136 

137 :meth:`to_tokens`, :meth:`to_str_tokens`, :meth:`get_token_position`, 

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

139 to control BOS prepending. Resolution: explicit arg → 

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

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

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

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

144 

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

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

147 the bridge adds or strips manually as needed. When 

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

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

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

151 bridge-specific. 

152 

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

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

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

156 """ 

157 

158 # hook_aliases inherited from BridgeCore 

159 

160 def __init__( 

161 self, 

162 model: nn.Module, 

163 adapter: ArchitectureAdapter, 

164 tokenizer: Any, 

165 *, 

166 driver: Any = None, 

167 ): 

168 """Initialize the bridge. 

169 

170 Args: 

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

172 adapter: The architecture adapter to use 

173 tokenizer: The tokenizer to use (required) 

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

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

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

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

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

179 """ 

180 nn.Module.__init__(self) 

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

182 self.__dict__["original_model"] = model 

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

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

185 if driver is None: 

186 from transformer_lens.model_bridge.sources.transformers_driver import ( 

187 TransformersDriver, 

188 ) 

189 

190 driver = TransformersDriver(model, adapter, tokenizer) 

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

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

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

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

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

196 try: 

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

198 except StopIteration: 

199 self.cfg.device = "cpu" 

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

201 self._initialize_hook_registry() 

202 self._register_aliases() 

203 self._register_all_aliases_recursive() 

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

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

206 # instances, so no double-firing. 

207 self._scan_existing_hooks(self, "") 

208 self._setup_hook_compatibility() 

209 # Backfill supported_hook_points = registry − non_fireable. Whitelist- 

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

211 if not self._driver.supported_hook_points: 

212 self._driver.supported_hook_points = ( 

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

214 ) 

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

216 validate_driver(self._driver, after_bridge_construction=True) 

217 self.processor = None 

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

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

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

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

222 model.train(model.training) 

223 self.train(model.training) 

224 self.cfg._bind_bridge(self) 

225 

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

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

228 super().__setstate__(state) 

229 self.cfg._bind_bridge(self) 

230 

231 # boot_transformers / list_supported_models / check_model_support are 

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

233 # source package owns its own boot entry point. 

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

235 

236 @property 

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

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

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

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

241 if driver is None: 

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

243 # module in the __dict__ mirror the setter maintains. 

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

245 if model is None: 

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

247 return model 

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

249 if underlying is None: 

250 raise AttributeError( 

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

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

253 ) 

254 return underlying 

255 

256 @original_model.setter 

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

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

259 self.__dict__["original_model"] = value 

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

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

262 if callable(setter): 

263 setter(value) 

264 

265 def init_weights(self) -> None: 

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

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

268 initialize_native_model, 

269 ) 

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

271 

272 model = self.original_model 

273 if not isinstance(model, NativeModel): 

274 raise RuntimeError( 

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

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

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

278 ) 

279 initialize_native_model(model, self.cfg) 

280 

281 def _set_processed_weight_attributes(self) -> None: 

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

283 

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

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

286 - _processed_W_Q 

287 - _processed_W_K 

288 - _processed_W_V 

289 - _processed_b_Q 

290 - _processed_b_K 

291 - _processed_b_V 

292 

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

294 HookedTransformer compatibility while keeping 2D format for calculations. 

295 """ 

296 

297 n_heads = self.cfg.n_heads 

298 d_head = self.cfg.d_head 

299 d_model = self.cfg.d_model 

300 blocks_iter = [] 

301 for bl_name in _BLOCK_LIST_ATTRS: 

302 if hasattr(self, bl_name): 

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

304 if not blocks_iter: 

305 return 

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

307 if "attn" not in block._modules: 

308 continue 

309 attn = block.attn 

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

311 continue 

312 try: 

313 w_q_2d = attn.q.weight.data 

314 w_k_2d = attn.k.weight.data 

315 w_v_2d = attn.v.weight.data 

316 attn._processed_W_Q = einops.rearrange( 

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

318 ) 

319 attn._processed_W_K = einops.rearrange( 

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

321 ) 

322 attn._processed_W_V = einops.rearrange( 

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

324 ) 

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

326 b_q_2d = attn.q.bias.data 

327 b_k_2d = attn.k.bias.data 

328 b_v_2d = attn.v.bias.data 

329 attn._processed_b_Q = einops.rearrange( 

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

331 ) 

332 attn._processed_b_K = einops.rearrange( 

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

334 ) 

335 attn._processed_b_V = einops.rearrange( 

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

337 ) 

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

339 w_o_2d = attn.o.weight.data 

340 w_o_transposed = w_o_2d.T 

341 attn._processed_W_O = einops.rearrange( 

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

343 ) 

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

345 attn._processed_b_O = attn.o.bias.data 

346 except Exception: 

347 pass 

348 

349 def _register_all_aliases_recursive(self) -> None: 

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

351 

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

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

354 """ 

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

356 self._register_aliases() 

357 for module in self.modules(): 

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

359 getattr(module, "_register_aliases")() 

360 

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

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

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

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

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

366 if name == "original_model": 

367 object.__setattr__(self, name, value) 

368 return 

369 super().__setattr__(name, value) 

370 if isinstance(value, HookPoint): 

371 value.name = name 

372 self._hook_registry[name] = value 

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

374 component_hooks = value.get_hooks() 

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

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

377 hook.name = full_name 

378 self._hook_registry[full_name] = hook 

379 

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

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

382 visited = set() 

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

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

385 # alias registry entries without renaming shared HookPoint instances 

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

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

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

389 

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

391 obj_id = id(mod) 

392 if obj_id in visited: 

393 return 

394 visited.add(obj_id) 

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

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

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

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

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

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

401 hook_id = id(hook) 

402 if hook_id not in named_hook_ids: 

403 hook.name = full_name 

404 named_hook_ids.add(hook_id) 

405 self._hook_registry[full_name] = hook 

406 for attr_name in dir(mod): 

407 if attr_name.startswith("_"): 

408 continue 

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

410 continue 

411 if attr_name in [ 

412 "OV", 

413 "QK", 

414 "W_V", 

415 "W_O", 

416 "W_Q", 

417 "W_K", 

418 "W_in", 

419 "W_gate", 

420 "W_out", 

421 "b_V", 

422 "b_O", 

423 "b_Q", 

424 "b_K", 

425 "b_in", 

426 "b_out", 

427 ]: 

428 continue 

429 try: 

430 attr = getattr(mod, attr_name) 

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

432 continue 

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

434 if isinstance(attr, HookPoint): 

435 hook_id = id(attr) 

436 if hook_id not in named_hook_ids: 

437 attr.name = name 

438 named_hook_ids.add(hook_id) 

439 self._hook_registry[name] = attr 

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

441 if ( 

442 child_name == "original_component" 

443 or child_name == "_original_component" 

444 or child_name == "original_model" 

445 ): 

446 continue 

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

448 scan_module(child_module, child_path) 

449 

450 scan_module(module, prefix) 

451 

452 @property 

453 def n_params_total(self) -> int: 

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

455 

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

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

458 are excluded, so the result can differ from 

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

460 

461 Returns: 

462 int: Parameter count of the uninstrumented wrapped model. 

463 """ 

464 return self._n_params_total 

465 

466 def _has_registered_blocks(self) -> bool: 

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

468 

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

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

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

472 """ 

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

474 return "blocks" in modules 

475 

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

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

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

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

480 if name == "original_model": 

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

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

483 return prop.fget(self) 

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

485 return self.__dict__[name] 

486 # Use __dict__ directly to avoid recursion 

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

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

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

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

491 if component_mapping is not None and name in component_mapping: 

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

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

494 try: 

495 name_split = name.split(".") 

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

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

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

499 current = getattr(current, part) 

500 return current 

501 else: 

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

503 except AttributeError: 

504 pass # type: ignore[operator,assignment] 

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

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

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

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

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

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

511 descriptor.fget(self) 

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

513 

514 def __str__(self) -> str: 

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

516 

517 Returns: 

518 A string describing the bridge's components. 

519 """ 

520 lines = ["TransformerBridge:"] 

521 mapping = self.adapter.get_component_mapping() 

522 

523 def _describe(component_mapping, indent): 

524 pad = " " * indent 

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

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

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

528 if submodules: 

529 _describe(submodules, indent + 1) 

530 

531 _describe(mapping, 1) 

532 return "\n".join(lines) 

533 

534 def enable_compatibility_mode( 

535 self, 

536 disable_warnings: bool = False, 

537 no_processing: bool = False, 

538 fold_ln: bool = True, 

539 center_writing_weights: bool = True, 

540 center_unembed: bool = True, 

541 fold_value_biases: bool = True, 

542 refactor_factored_attn_matrices: bool = False, 

543 ) -> None: 

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

545 

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

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

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

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

550 

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

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

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

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

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

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

557 :py:meth:`set_use_hook_mlp_in`. 

558 

559 Args: 

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

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

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

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

564 Default: True. Ignored if no_processing=True. 

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

566 Default: True. Ignored if no_processing=True. 

567 center_unembed: Whether to center the unembedding matrix. 

568 Default: True. Ignored if no_processing=True. 

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

570 Default: True. Ignored if no_processing=True. 

571 refactor_factored_attn_matrices: Whether to refactor factored attention matrices. 

572 Default: False. Ignored if no_processing=True. 

573 """ 

574 from transformer_lens.utilities.bridge_components import ( 

575 apply_fn_to_all_components, 

576 ) 

577 

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

579 raise RuntimeError( 

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

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

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

583 ) 

584 

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

586 if hf_device_map and not no_processing: 

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

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

589 raise RuntimeError( 

590 "enable_compatibility_mode() with weight processing " 

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

592 "supported on a bridge with an offloaded device_map " 

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

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

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

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

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

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

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

600 ) 

601 

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

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

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

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

606 raise NotImplementedError( 

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

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

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

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

611 ) 

612 

613 self.compatibility_mode = True 

614 

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

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

617 component.compatibility_mode = True 

618 component.disable_warnings = disable_warnings 

619 

620 apply_fn_to_all_components(self, set_compatibility_mode) 

621 self.clear_hook_registry() 

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

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

624 for block in self.blocks: 

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

626 block._teardown_capture_hooks() 

627 try: 

628 if not no_processing: 

629 self.process_weights( 

630 fold_ln=fold_ln, 

631 center_writing_weights=center_writing_weights, 

632 center_unembed=center_unembed, 

633 fold_value_biases=fold_value_biases, 

634 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

635 ) 

636 finally: 

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

638 self._initialize_hook_registry() 

639 self._setup_hook_compatibility() 

640 self._register_all_aliases_recursive() 

641 

642 def _setup_hook_compatibility(self) -> None: 

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

644 

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

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

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

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

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

650 

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

652 compatibility mode or weight processing is enabled. 

653 

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

655 """ 

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

657 self.adapter.setup_hook_compatibility(self) 

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

659 self.adapter.setup_no_processing_hooks(self) 

660 blocks_to_process = [] 

661 for block_list_name in ( 

662 "blocks", 

663 "encoder_blocks", 

664 "decoder_blocks", 

665 "L_blocks", 

666 "H_blocks", 

667 ): 

668 if hasattr(self, block_list_name): 

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

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

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

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

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

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

675 if vision_layers is not None: 

676 blocks_to_process.extend(vision_layers) 

677 for block in blocks_to_process: 

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

679 if hasattr(block, attn_name): 

680 attn = getattr(block, attn_name) 

681 if hasattr(attn, "setup_hook_compatibility"): 

682 attn.setup_hook_compatibility() 

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

684 attn.setup_no_processing_hooks() 

685 

686 def process_weights( 

687 self, 

688 verbose: bool = False, 

689 fold_ln: bool = True, 

690 center_writing_weights: bool = True, 

691 center_unembed: bool = True, 

692 fold_value_biases: bool = True, 

693 refactor_factored_attn_matrices: bool = False, 

694 ) -> None: 

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

696 

697 This method applies weight processing transformations to improve model interpretability 

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

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

700 

701 Args: 

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

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

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

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

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

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

708 """ 

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

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

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

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

713 if self._weights_processed: 

714 logging.warning( 

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

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

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

718 ) 

719 return 

720 

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

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

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

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

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

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

727 # very tensors the converters had to guard. unreadable_weight_reason 

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

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

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

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

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

733 require_readable_weight( 

734 param, 

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

736 owner=self.original_model, 

737 remedy=( 

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

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

740 ), 

741 ) 

742 

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

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

745 self._weights_processed = True 

746 from transformer_lens.weight_processing import ProcessWeights 

747 

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

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

750 

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

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

753 logging.warning( 

754 "center_unembed=True is incompatible with logit softcapping " 

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

756 self.cfg.output_logits_soft_cap, 

757 ) 

758 center_unembed = False 

759 

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

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

762 state_dict = self.state_dict() 

763 adapter = self.adapter 

764 

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

766 embed_key = "embed.weight" 

767 unembed_key = "unembed.weight" 

768 

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

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

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

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

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

774 # Clone the unembed weight to break the tie 

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

776 

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

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

779 state_dict = adapter.preprocess_weights(state_dict) 

780 

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

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

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

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

785 state_dict = ProcessWeights.process_weights( 

786 state_dict, 

787 self.cfg, 

788 fold_ln=fold_ln, 

789 center_writing_weights=center_writing_weights, 

790 center_unembed=center_unembed, 

791 fold_value_biases=fold_value_biases, 

792 refactor_factored_attn_matrices=refactor_factored_attn_matrices, 

793 adapter=adapter, 

794 ) 

795 

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

797 import re 

798 

799 hf_to_tl_prefix = {} 

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

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

802 hf_to_tl_prefix[remote_path] = tl_name 

803 

804 normalized_state_dict = {} 

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

806 new_key = key 

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

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

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

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

811 break 

812 normalized_state_dict[new_key] = value 

813 state_dict = normalized_state_dict 

814 

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

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

817 ProcessWeights.distribute_weights_to_components( 

818 state_dict=state_dict, 

819 component_mapping=self.real_components, 

820 ) 

821 if fold_ln: 

822 self._fold_layer_norms_into_batched_experts() 

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

824 adapter.postprocess_weights(self) 

825 

826 def _fold_layer_norms_into_batched_experts(self) -> None: 

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

828 

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

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

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

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

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

834 

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

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

837 GLM4-MoE hang off the same norm. 

838 """ 

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

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

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

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

843 return 

844 

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

846 skipped: list[str] = [] 

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

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

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

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

851 continue 

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

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

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

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

856 skipped.append(label) 

857 continue 

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

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

860 skipped.append(label) 

861 continue 

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

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

864 scale += 1.0 

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

866 skipped.append(label) 

867 continue 

868 with torch.no_grad(): 

869 weight.fill_(0.0 if uses_offset else 1.0) 

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

871 logging.warning( 

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

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

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

875 ", ".join(skipped), 

876 ) 

877 

878 def to_tokens( 

879 self, 

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

881 prepend_bos: Optional[bool] = None, 

882 padding_side: Optional[str] = None, 

883 move_to_device: bool = True, 

884 truncate: bool = True, 

885 ) -> torch.Tensor: 

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

887 

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

889 semantics, the ``default_prepend_bos`` / 

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

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

892 tokenizing only part of a prompt.** 

893 

894 Args: 

895 input: The input to tokenize. 

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

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

898 to override locally. 

899 padding_side: Which side to pad on when tokenizing multiple 

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

901 ``padding_side``. 

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

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

904 

905 Returns: 

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

907 """ 

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

909 if prepend_bos is None: 

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

911 if padding_side is None: 

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

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

914 if prepend_bos and (not tokenizer_prepends_bos): 

915 bos = self.tokenizer.bos_token 

916 encodes_atomically = ( 

917 bos is not None 

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

919 ) 

920 if encodes_atomically: 

921 input = utils.get_input_with_manually_prepended_bos(bos, input) 

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

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

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

925 if isinstance(input, str): 

926 input = [input] 

927 tokens = self.tokenizer( 

928 input, 

929 return_tensors="pt", 

930 padding=True, 

931 padding_side=padding_side, 

932 truncation=truncate, 

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

934 )["input_ids"] 

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

936 if ( 

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

938 and self.tokenizer.eos_token_id is not None 

939 ): 

940 # Remove trailing EOS, keep at least 1 token 

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

942 tokens = tokens[:, :-1] 

943 if not prepend_bos and tokenizer_prepends_bos: 

944 tokens = utils.get_tokens_with_bos_removed( 

945 self.tokenizer, tokens, padding_side=padding_side 

946 ) 

947 if move_to_device: 

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

949 return tokens 

950 

951 def encoder_output( 

952 self, 

953 frames: torch.Tensor, 

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

955 ) -> torch.Tensor: 

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

957 

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

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

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

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

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

963 full path does, then the blocks run. Mirrors 

964 ``HookedAudioEncoder.encoder_output``. 

965 

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

967 ``add_hook`` / ``get_caching_hooks``. 

968 

969 Args: 

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

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

972 real frames and 0 for padding. 

973 

974 Returns: 

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

976 """ 

977 self._require_frame_entry_support() 

978 if frames.ndim != 3: 

979 raise ValueError( 

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

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

982 ) 

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

984 

985 if one_zero_attention_mask is not None: 

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

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

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

989 frames = frames.masked_fill( 

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

991 ) 

992 

993 resid = frames + self.conv_pos_embed(frames) 

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

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

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

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

998 # every stable-LN activation is silently wrong. 

999 stable_ln = self._audio_encoder_is_stable_layer_norm() 

1000 if not stable_ln: 

1001 resid = self.embed_ln(resid) 

1002 

1003 additive_attention_mask = None 

1004 if one_zero_attention_mask is not None: 

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

1006 additive_attention_mask = torch.where( 

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

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

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

1010 ) 

1011 

1012 for block in self.blocks: 

1013 output = block(resid, attention_mask=additive_attention_mask) 

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

1015 if stable_ln: 

1016 resid = self.embed_ln(resid) 

1017 return resid 

1018 

1019 def _audio_encoder_is_stable_layer_norm(self) -> bool: 

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

1021 

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

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

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

1025 """ 

1026 model = self.original_model 

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

1028 candidate = getattr(model, attr, None) 

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

1030 continue 

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

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

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

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

1035 

1036 def _require_frame_entry_support(self) -> None: 

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

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

1039 raise NotImplementedError( 

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

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

1042 ) 

1043 missing = [ 

1044 name 

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

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

1047 ] 

1048 if missing: 

1049 raise NotImplementedError( 

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

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

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

1053 ) 

1054 

1055 def to_sentence_pair_tokens( 

1056 self, 

1057 sentence_a: str, 

1058 sentence_b: str, 

1059 move_to_device: bool = True, 

1060 truncate: bool = True, 

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

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

1063 

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

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

1066 sentences as one segment and its logits collapse. Mirrors 

1067 ``BertNextSentencePrediction.to_tokens``. 

1068 

1069 Args: 

1070 sentence_a: First sentence of the pair. 

1071 sentence_b: Second sentence of the pair. 

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

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

1074 """ 

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

1076 encodings = self.tokenizer( 

1077 sentence_a, 

1078 sentence_b, 

1079 return_tensors="pt", 

1080 padding=True, 

1081 truncation=truncate, 

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

1083 ) 

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

1085 raise ValueError( 

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

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

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

1089 ) 

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

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

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

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

1094 return tokens 

1095 

1096 def predict_next_sentence( 

1097 self, 

1098 sentence_a: str, 

1099 sentence_b: str, 

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

1101 truncate: bool = True, 

1102 ) -> Any: 

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

1104 

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

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

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

1108 ``BertNextSentencePrediction.forward``. 

1109 

1110 Args: 

1111 sentence_a: First sentence of the pair. 

1112 sentence_b: Second sentence of the pair. 

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

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

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

1116 """ 

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

1118 forward_kwargs: Dict[str, Any] = { 

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

1120 } 

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

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

1123 raise ValueError( 

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

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

1126 "model_class=BertForNextSentencePrediction." 

1127 ) 

1128 if return_type == "logits": 

1129 return logits 

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

1131 

1132 def to_string( 

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

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

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

1136 

1137 Args: 

1138 tokens: Tokens to convert 

1139 

1140 Returns: 

1141 Decoded string(s) 

1142 """ 

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

1144 tokens = torch.tensor(tokens) 

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

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

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

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

1149 else: 

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

1151 

1152 def to_str_tokens( 

1153 self, 

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

1155 prepend_bos: Optional[bool] = None, 

1156 padding_side: Optional[str] = None, 

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

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

1159 

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

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

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

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

1164 

1165 Args: 

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

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

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

1169 (use the cfg setting). 

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

1171 is a string. 

1172 

1173 Returns: 

1174 List of token strings. 

1175 """ 

1176 if isinstance(input, list): 

1177 return cast( 

1178 List[List[str]], 

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

1180 ) 

1181 elif isinstance(input, str): 

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

1183 elif isinstance(input, torch.Tensor): 1183 ↛ 1190line 1183 didn't jump to line 1190 because the condition on line 1183 was always true

1184 tokens = input.squeeze() 

1185 if tokens.dim() == 0: 1185 ↛ 1186line 1185 didn't jump to line 1186 because the condition on line 1185 was never true

1186 tokens = tokens.unsqueeze(0) 

1187 assert ( 

1188 tokens.dim() == 1 

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

1190 elif isinstance(input, np.ndarray): 

1191 tokens_np = input.squeeze() 

1192 if tokens_np.ndim == 0: 

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

1194 assert ( 

1195 tokens_np.ndim == 1 

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

1197 tokens = torch.tensor(tokens_np) 

1198 else: 

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

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

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

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

1203 return str_tokens 

1204 

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

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

1207 

1208 Args: 

1209 string: The string to convert 

1210 

1211 Returns: 

1212 Token ID 

1213 

1214 Raises: 

1215 AssertionError: If string is not a single token 

1216 """ 

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

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

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

1220 return int(token.item()) 

1221 

1222 def get_token_position( 

1223 self, 

1224 single_token: Union[str, int], 

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

1226 mode="first", 

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

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

1229 ): 

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

1231 

1232 Raises an error if the token is not present. 

1233 

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

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

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

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

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

1239 

1240 Args: 

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

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

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

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

1245 with a dummy batch dimension. 

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

1247 "first" or "last". Defaults to "first". 

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

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

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

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

1252 """ 

1253 if isinstance(input, str): 

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

1255 else: 

1256 tokens = input 

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

1258 assert ( 

1259 tokens.shape[0] == 1 

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

1261 tokens = tokens[0] 

1262 if isinstance(single_token, str): 

1263 single_token = self.to_single_token(single_token) 

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

1265 single_token = single_token.item() 

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

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

1268 if mode == "first": 

1269 return indices[0].item() 

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

1271 return indices[-1].item() 

1272 else: 

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

1274 

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

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

1277 

1278 Args: 

1279 int_token: The token ID 

1280 

1281 Returns: 

1282 The token string 

1283 """ 

1284 assert isinstance(int_token, int) 

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

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

1287 return str(token[0]) 

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

1289 

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

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

1292 

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

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

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

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

1297 """ 

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

1299 for list_name in _BLOCK_LIST_ATTRS: 

1300 block_list = self._modules.get(list_name) 

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

1302 continue 

1303 for block in block_list: 

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

1305 return pairs 

1306 

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

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

1309 

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

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

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

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

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

1315 """ 

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

1317 if candidate in block._modules: 

1318 return candidate 

1319 return None 

1320 

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

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

1323 if actual is None or actual == submodule: 

1324 return attr_path 

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

1326 return attr_path 

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

1328 

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

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

1331 

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

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

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

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

1336 """ 

1337 return [ 

1338 (index, block) 

1339 for index, block in self._enumerate_blocks() 

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

1341 ] 

1342 

1343 def stack_params_for( 

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

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

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

1347 

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

1349 """ 

1350 matching = self.blocks_with(submodule) 

1351 if not matching: 

1352 raise ValueError( 

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

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

1355 ) 

1356 indices: List[int] = [] 

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

1358 for idx, block in matching: 

1359 resolved = self._resolve_submodule_name(block, submodule) 

1360 w = _resolve_attr_path( 

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

1362 ) 

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

1364 raise AttributeError( 

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

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

1367 ) 

1368 if reshape_fn is not None: 

1369 w = reshape_fn(w) 

1370 weights.append(w) 

1371 indices.append(idx) 

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

1373 

1374 def _stack_block_params( 

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

1376 ) -> torch.Tensor: 

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

1378 

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

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

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

1382 AttributeError killed the accessor for the whole model. 

1383 """ 

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

1385 all_blocks = self._enumerate_blocks() 

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

1387 for i, block in all_blocks: 

1388 resolved = self._resolve_submodule_name(block, first_attr) 

1389 if resolved is None: 

1390 continue 

1391 try: 

1392 weight = _resolve_attr_path( 

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

1394 ) 

1395 except AttributeError: 

1396 continue 

1397 if weight is None: 

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

1399 # stacking would raise an opaque TypeError. 

1400 raise AttributeError( 

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

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

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

1404 f"if your analysis needs one." 

1405 ) 

1406 matching_blocks.append((i, weight)) 

1407 

1408 if len(matching_blocks) == 0: 

1409 raise AttributeError( 

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

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

1412 ) 

1413 

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

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

1416 logging.warning( 

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

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

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

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

1421 len(matching_blocks), 

1422 len(all_blocks), 

1423 attr_path, 

1424 indices, 

1425 first_attr, 

1426 attr_path, 

1427 ) 

1428 

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

1430 for _, weight in matching_blocks: 

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

1432 weights.append(w) 

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

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

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

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

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

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

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

1440 

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

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

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

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

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

1446 return w 

1447 

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

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

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

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

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

1453 return w 

1454 

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

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

1457 

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

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

1460 n_heads before factoring: query head h reads kv head 

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

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

1463 where the head axes already match. 

1464 """ 

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

1466 return w 

1467 n_kv_heads = w.shape[1] 

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

1469 raise ValueError( 

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

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

1472 ) 

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

1474 

1475 @property 

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

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

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

1479 

1480 @property 

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

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

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

1484 

1485 @property 

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

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

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

1489 

1490 @property 

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

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

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

1494 

1495 @property 

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

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

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

1499 

1500 @property 

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

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

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

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

1505 return None 

1506 

1507 @property 

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

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

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

1511 

1512 @property 

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

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

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

1516 

1517 @property 

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

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

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

1521 

1522 @property 

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

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

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

1526 

1527 @property 

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

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

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

1531 

1532 @property 

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

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

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

1536 

1537 @property 

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

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

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

1541 

1542 @property 

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

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

1545 return self.unembed.W_U 

1546 

1547 @property 

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

1549 """Unembedding bias (d_vocab).""" 

1550 return self.unembed.b_U 

1551 

1552 @property 

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

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

1555 return self.embed.W_E 

1556 

1557 @property 

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

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

1560 

1561 Only defined for models with learned absolute positional embeddings; 

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

1563 currently processed, like every other accessor — equal to 

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

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

1566 compatibility mode vs HT defaults). 

1567 """ 

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

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

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

1571 raise AttributeError( 

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

1573 f"embedding component; this model exposes none " 

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

1575 ) 

1576 w = pos_embed.W_pos 

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

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

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

1580 # wrong-shaped tensor. 

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

1582 raise AttributeError( 

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

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

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

1586 ) 

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

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

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

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

1591 return w[2:] 

1592 return w 

1593 

1594 @property 

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

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

1597 

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

1599 circuits. Mirrors ``HookedTransformer.W_E_pos``. 

1600 """ 

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

1602 

1603 @property 

1604 def QK(self): 

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

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

1607 

1608 @property 

1609 def OV(self): 

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

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

1612 

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

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

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

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

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

1618 

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

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

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

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

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

1624 

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

1626 # Mechanistic interpretability analysis methods 

1627 # ------------------------------------------------------------------ 

1628 

1629 def tokens_to_residual_directions( 

1630 self, 

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

1632 ) -> torch.Tensor: 

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

1634 

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

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

1637 logit for each token. 

1638 

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

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

1641 

1642 Args: 

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

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

1645 

1646 Returns: 

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

1648 plus a trailing d_model dimension. 

1649 """ 

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

1651 residual_directions = self.W_U[:, tokens] 

1652 residual_directions = einops.rearrange( 

1653 residual_directions, "d_model ... -> ... d_model" 

1654 ) 

1655 return residual_directions 

1656 else: 

1657 if isinstance(tokens, str): 

1658 token = self.to_single_token(tokens) 

1659 elif isinstance(tokens, int): 

1660 token = tokens 

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

1662 token = int(tokens.item()) 

1663 else: 

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

1665 residual_direction = self.W_U[:, token] 

1666 return residual_direction 

1667 

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

1669 _VARIANT_OUTPUT_BIAS_ATTRS: Dict[str, tuple] = { 

1670 "attn": ("b_O",), 

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

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

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

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

1675 } 

1676 

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

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

1679 for name in VARIANT_SUBMODULE_NAMES: 

1680 if name not in block._modules: 

1681 continue 

1682 variant = block._modules[name] 

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

1684 obj = variant 

1685 try: 

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

1687 obj = getattr(obj, attr) 

1688 except AttributeError: 

1689 continue 

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

1691 return obj 

1692 return None 

1693 

1694 def accumulated_bias( 

1695 self, 

1696 layer: int, 

1697 mlp_input: bool = False, 

1698 include_mlp_biases: bool = True, 

1699 ) -> torch.Tensor: 

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

1701 

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

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

1704 """ 

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

1706 for i in range(layer): 

1707 block = self.blocks[i] 

1708 b_O = self._get_block_variant_bias(block) 

1709 if b_O is not None: 

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

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

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

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

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

1715 if mlp_input: 

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

1717 block = self.blocks[layer] 

1718 b_O = self._get_block_variant_bias(block) 

1719 if b_O is not None: 

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

1721 return accumulated 

1722 

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

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

1725 

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

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

1728 maps tensor position i to original layer number. 

1729 """ 

1730 self._reject_encoder_decoder_composition() 

1731 attn_blocks = self.blocks_with("attn") 

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

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

1734 

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

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

1737 

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

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

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

1741 w = _resolve_attr_path(block, attr_path) 

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

1743 raise AttributeError( 

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

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

1746 ) 

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

1748 w = reshape_fn(w) 

1749 weights.append(w) 

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

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

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

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

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

1755 

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

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

1758 left = FactoredMatrix(W_V, W_O) 

1759 

1760 if mode == "Q": 

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

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

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

1764 elif mode == "K": 

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

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

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

1768 elif mode == "V": 

1769 right = left 

1770 else: 

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

1772 

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

1774 n_attn = len(indices) 

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

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

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

1778 

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

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

1781 

1782 def _reject_encoder_decoder_composition(self) -> None: 

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

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

1785 raise NotImplementedError( 

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

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

1788 "supported them there either)." 

1789 ) 

1790 

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

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

1793 self._reject_encoder_decoder_composition() 

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

1795 

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

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

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

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

1800 

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

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

1803 block = self.blocks[layer_idx] 

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

1805 

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

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

1808 types = [] 

1809 for block in self.blocks: 

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

1811 universals = sorted( 

1812 n 

1813 for n in block._modules 

1814 if n not in _VARIANT_SUBMODULE_SET 

1815 and n not in _BLOCK_INTERNAL_MODULES 

1816 and not n.startswith(_NORM_PREFIXES) 

1817 ) 

1818 parts = variants + universals 

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

1820 return types 

1821 

1822 @property 

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

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

1825 

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

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

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

1829 """ 

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

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

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

1833 heads = range(self.cfg.n_heads) 

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

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

1836 ] 

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

1838 

1839 @property 

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

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

1842 self._reject_encoder_decoder_composition() 

1843 return [ 

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

1845 ] 

1846 

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

1848 """Returns parameters following standard PyTorch semantics. 

1849 

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

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

1852 

1853 Args: 

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

1855 

1856 Returns: 

1857 Iterator of nn.Parameter objects 

1858 """ 

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

1860 

1861 def named_parameters( 

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

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

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

1865 

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

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

1868 

1869 Args: 

1870 prefix: Prefix to prepend to all parameter names 

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

1872 remove_duplicate: If True, removes duplicate parameters 

1873 

1874 Returns: 

1875 Iterator of (name, parameter) tuples 

1876 """ 

1877 return self.original_model.named_parameters(prefix, recurse, remove_duplicate) 

1878 

1879 def tl_parameters(self) -> dict[str, torch.Tensor]: 

1880 """Returns TransformerLens-style parameter dictionary. 

1881 

1882 Parameter names follow TransformerLens conventions (e.g., 'blocks.0.attn.W_Q') and may 

1883 include processed weights (non-leaf tensors). This format is expected by SVDInterpreter 

1884 among other analysis tools. 

1885 

1886 Returns: 

1887 Dictionary mapping TransformerLens parameter names to tensors 

1888 

1889 Example: 

1890 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

1891 >>> tl_params = bridge.tl_parameters() 

1892 >>> W_Q = tl_params["blocks.0.attn.W_Q"] # Shape: [n_heads, d_model, d_head] 

1893 """ 

1894 return self.get_params() 

1895 

1896 def tl_named_parameters(self) -> Iterator[tuple[str, torch.Tensor]]: 

1897 """Returns iterator of TransformerLens-style named parameters. 

1898 

1899 This provides the same parameters as tl_parameters() but as an iterator 

1900 for consistency with PyTorch's named_parameters() API pattern. 

1901 

1902 Returns: 

1903 Iterator of (name, tensor) tuples with TransformerLens naming conventions 

1904 

1905 Example: 

1906 >>> bridge = TransformerBridge.boot_transformers("gpt2") 

1907 >>> for name, param in bridge.tl_named_parameters(): 

1908 ... if "attn.W_Q" in name: 

1909 ... print(f"{name}: {param.shape}") # doctest: +ELLIPSIS 

1910 blocks.0.attn.W_Q: torch.Size([12, 768, 64]) 

1911 ... 

1912 """ 

1913 return iter(self.get_params().items()) 

1914 

1915 def input_to_embed( 

1916 self, 

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

1918 prepend_bos: Optional[bool] = None, 

1919 padding_side: Optional[str] = None, 

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

1921 ) -> Tuple[torch.Tensor, torch.Tensor, None, Optional[torch.Tensor]]: 

1922 """Convert input to the residual stream entering block 0 (``resid_pre[0]``). 

1923 

1924 Bridge analog of :meth:`HookedTransformer.input_to_embed`. Returns 

1925 ``(residual, tokens, shortformer_pos_embed, attention_mask)``; feed the 

1926 residual to ``forward(..., start_at_layer=0)`` to resume the pass. 

1927 

1928 ``shortformer_pos_embed`` is always ``None``: for the models the bridge 

1929 supports the residual already carries positional information, so there is 

1930 no separate positional stream to return. 

1931 """ 

1932 if isinstance(input, (str, list)): 

1933 assert self.tokenizer is not None, "Must provide a tokenizer for string input." 

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

1935 else: 

1936 tokens = input 

1937 if isinstance(tokens, torch.Tensor) and tokens.ndim == 1: 1937 ↛ 1938line 1937 didn't jump to line 1938 because the condition on line 1937 was never true

1938 tokens = tokens.unsqueeze(0) 

1939 if ( 1939 ↛ 1944line 1939 didn't jump to line 1944 because the condition on line 1939 was never true

1940 attention_mask is None 

1941 and self.tokenizer is not None 

1942 and self.tokenizer.padding_side == "left" 

1943 ): 

1944 _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos 

1945 attention_mask = utils.get_attention_mask(self.tokenizer, tokens, _prepend) 

1946 residual = self.forward(tokens, stop_at_layer=0, attention_mask=attention_mask) 

1947 return residual, tokens, None, attention_mask 

1948 

1949 def _accepts_derived_position_ids(self) -> bool: 

1950 """Whether it is safe to hand the wrapped model a mask-derived ``position_ids``. 

1951 

1952 Two families of model must be left alone, so the injection below is 

1953 gated on the target the same way ``output_attentions`` is in 

1954 :meth:`BridgeCore.run_with_cache`: 

1955 

1956 * **Fixed-signature models.** Remote-code forwards such as 

1957 ``LLaDAModelLM.forward`` take neither ``position_ids`` nor 

1958 ``**kwargs``, so passing it raises ``TypeError`` where the model 

1959 previously returned logits. 

1960 * **Models that own their position derivation.** mRoPE architectures 

1961 (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, GLM-4V) build a 3-D temporal / 

1962 height / width index in ``get_rope_index``, and only while 

1963 ``position_ids is None``; a supplied 2-D tensor is silently expanded 

1964 across all three streams instead. Their derivation already scatters 

1965 positions onto attended slots only, so it handles left padding 

1966 correctly on its own and needs no help from us. 

1967 * **Mask-consuming positional embeddings.** OPT's 

1968 ``OPTLearnedPositionalEmbedding.forward`` takes the mask and derives 

1969 the same positions we would, so injection buys nothing — but it does 

1970 replace the model's own padding-slot convention with ours, which 

1971 shows up as a whole-tensor diff. 

1972 

1973 Non-torch drivers (vLLM, Inspect) expose no module to introspect and 

1974 manage positions internally, so they are excluded as well. 

1975 """ 

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

1977 if underlying is None: 

1978 return False 

1979 

1980 cached = self.__dict__.get("_derived_position_ids_ok") 

1981 if cached is not None and cached[0] is underlying: 

1982 return bool(cached[1]) 

1983 

1984 accepts = accepts_mask_derived_position_ids(underlying) 

1985 self.__dict__["_derived_position_ids_ok"] = (underlying, accepts) 

1986 return accepts 

1987 

1988 @staticmethod 

1989 def _seq2seq_loss( 

1990 logits: torch.Tensor, 

1991 labels: torch.Tensor, 

1992 native_loss: Any, 

1993 *, 

1994 per_token: bool, 

1995 ) -> torch.Tensor: 

1996 """Return encoder-decoder loss without the causal LM token shift.""" 

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

1998 labels = labels.to(logits.device) 

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

2000 raise ValueError( 

2001 "seq2seq labels must match the decoder logits batch and position " 

2002 f"dimensions, got labels {tuple(labels.shape)} and logits " 

2003 f"{tuple(logits.shape)}" 

2004 ) 

2005 if not per_token and isinstance(native_loss, torch.Tensor): 

2006 return native_loss 

2007 

2008 losses = F.cross_entropy( 

2009 logits.flatten(0, 1), 

2010 labels.flatten(), 

2011 reduction="none" if per_token else "mean", 

2012 ignore_index=-100, 

2013 ) 

2014 return losses.view_as(labels) if per_token else losses 

2015 

2016 def _finalize_seq2seq_return( 

2017 self, 

2018 return_type: str, 

2019 logits: torch.Tensor, 

2020 labels: torch.Tensor, 

2021 native_output: Any, 

2022 *, 

2023 loss_per_token: bool, 

2024 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: 

2025 loss = self._seq2seq_loss( 

2026 logits, 

2027 labels, 

2028 getattr(native_output, "loss", None), 

2029 per_token=loss_per_token, 

2030 ) 

2031 return (logits, loss) if return_type == "both" else loss 

2032 

2033 def forward( 

2034 self, 

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

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

2037 loss_per_token: bool = False, 

2038 prepend_bos: Optional[bool] = None, 

2039 padding_side: Optional[str] = None, 

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

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

2042 start_at_layer: Optional[int] = None, 

2043 stop_at_layer: Optional[int] = None, 

2044 pixel_values: Optional[torch.Tensor] = None, 

2045 input_values: Optional[torch.Tensor] = None, 

2046 past_key_values: Optional[Any] = None, 

2047 **kwargs, 

2048 ) -> Any: 

2049 """Forward pass through the model. 

2050 

2051 Args: 

2052 input: Input to the model 

2053 return_type: Type of output to return ('logits', 'loss', 'both', 'predictions', 

2054 'logits_and_cache', None). 'logits_and_cache' returns 

2055 ``(logits, past_key_values)`` — the HuggingFace cache after this step, to 

2056 feed back on the next call for incremental decoding. 

2057 loss_per_token: Whether to return loss per token 

2058 prepend_bos: Whether to prepend BOS token 

2059 padding_side: Which side to pad on 

2060 labels: Explicit language-model targets. Encoder-decoder models require 

2061 labels for loss; decoder-only models fall back to input IDs when omitted. 

2062 past_key_values: HuggingFace KV cache from a prior ``use_cache=True`` step 

2063 (e.g. the second element of a ``return_type='logits_and_cache'`` return). 

2064 When provided, KV caching is enabled automatically and only the new 

2065 tokens' keys/values are computed; HF derives the position offset from the 

2066 cache length. This is the bridge's manual KV-cache entry point — it uses 

2067 HF's native cache object rather than a TransformerLens cache. 

2068 start_at_layer: Resume the forward from block ``k``, treating ``input`` as 

2069 the residual-stream tensor ``[batch, pos, d_model]`` entering that block 

2070 (mirrors HookedTransformer). Blocks 0..k-1 still execute internally (their 

2071 output is discarded when block k swaps in the residual) but are excluded 

2072 from ``run_with_cache`` output. Requires an HF model that accepts 

2073 ``inputs_embeds``; only supported on the standard ``blocks`` stack. 

2074 stop_at_layer: Layer to stop forward pass at. Only supported on the 

2075 standard ``blocks`` stack; architectures that register no ``blocks`` 

2076 (e.g. Raven's ``prelude``/``core_block``/``coda``) raise 

2077 ``NotImplementedError`` rather than running to completion. 

2078 pixel_values: Optional image tensor for multimodal models (e.g., LLaVA, Gemma3) 

2079 and vision models (eg. ViT, DeiT). 

2080 The tensor is passed directly to the underlying HuggingFace model. 

2081 Only valid when cfg.is_multimodal is True or cfg.is_visual_model is True. 

2082 input_values: Optional audio waveform tensor for audio models (e.g., HuBERT). 

2083 The tensor is passed directly to the underlying HuggingFace model. 

2084 Only valid when cfg.is_audio_model is True. 

2085 **kwargs: Additional arguments passed to model 

2086 

2087 Returns: 

2088 Model output based on return_type 

2089 """ 

2090 

2091 underlying_model = getattr(getattr(self, "_driver", None), "underlying_model", None) 

2092 model_config = getattr(underlying_model, "config", None) 

2093 is_encoder_decoder = bool(getattr(model_config, "is_encoder_decoder", False)) 

2094 if return_type in ("loss", "both"): 

2095 if is_encoder_decoder and labels is None: 

2096 raise ValueError( 

2097 "labels are required for seq2seq return_type='loss' or 'both'; " 

2098 "encoder input_ids are not decoder targets" 

2099 ) 

2100 if ( 

2101 return_type in ("loss", "both") 

2102 and not is_encoder_decoder 

2103 and not self.adapter.supports_causal_loss 

2104 ): 

2105 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2106 raise NotImplementedError( 

2107 f"{architecture} does not support TransformerBridge's shifted causal " 

2108 "loss. Request return_type='logits' and compute the architecture-specific " 

2109 "masked-token objective explicitly." 

2110 ) 

2111 

2112 if labels is not None: 

2113 kwargs["labels"] = labels 

2114 

2115 if start_at_layer is not None: 

2116 input = self._setup_start_at_layer(input, start_at_layer) 

2117 # Set stop_at_layer flag on all blocks if requested 

2118 if stop_at_layer is not None: 

2119 if not self._has_registered_blocks(): 

2120 raise NotImplementedError( 

2121 "stop_at_layer requires a 'blocks' stack; this architecture " 

2122 "does not register one." 

2123 ) 

2124 effective_stop_at_layer = ( 

2125 len(self.blocks) + stop_at_layer if stop_at_layer < 0 else stop_at_layer 

2126 ) 

2127 for block in self.blocks: 

2128 block._stop_at_layer_idx = effective_stop_at_layer 

2129 

2130 # Map HookedEncoderDecoder-style kwargs to HF-compatible names 

2131 if "decoder_input" in kwargs: 

2132 kwargs["decoder_input_ids"] = kwargs.pop("decoder_input") 

2133 if "one_zero_attention_mask" in kwargs: 2133 ↛ 2134line 2133 didn't jump to line 2134 because the condition on line 2133 was never true

2134 if attention_mask is None: 

2135 attention_mask = kwargs.pop("one_zero_attention_mask") 

2136 else: 

2137 kwargs.pop("one_zero_attention_mask") 

2138 

2139 # Detect batched list input that may need padding. Forward follows the 

2140 # requested/tokenizer side; generation separately forces left-padding. 

2141 _is_batched_list = ( 

2142 isinstance(input, list) 

2143 and len(input) > 1 

2144 and not getattr(self.cfg, "is_audio_model", False) 

2145 and not getattr(self.cfg, "is_visual_model", False) 

2146 ) 

2147 _resolved_padding_side = padding_side 

2148 if _resolved_padding_side is None and self.tokenizer is not None: 

2149 _resolved_padding_side = getattr(self.tokenizer, "padding_side", "right") 

2150 

2151 try: 

2152 if isinstance(input, (str, list)): 

2153 if getattr(self.cfg, "is_audio_model", False): 2153 ↛ 2154line 2153 didn't jump to line 2154 because the condition on line 2153 was never true

2154 raise ValueError( 

2155 "Audio models require tensor input (raw waveform), not text. " 

2156 "Pass a torch.Tensor or use the input_values parameter." 

2157 ) 

2158 if getattr(self.cfg, "is_visual_model", False): 2158 ↛ 2159line 2158 didn't jump to line 2159 because the condition on line 2158 was never true

2159 raise ValueError( 

2160 "Visual models require tensor input (pixel values), not text. " 

2161 "Pass a torch.Tensor or use the pixel_values parameter." 

2162 ) 

2163 input_ids = self.to_tokens( 

2164 input, prepend_bos=prepend_bos, padding_side=padding_side 

2165 ) 

2166 else: 

2167 input_ids = input 

2168 # Promote 1D integer token tensors to 2D [batch=1, seq] to match 

2169 # HookedTransformer's contract. Float tensors (inputs_embeds, 

2170 # audio waveforms) are passed through unchanged. 

2171 if ( 

2172 isinstance(input_ids, torch.Tensor) 

2173 and input_ids.ndim == 1 

2174 and not input_ids.is_floating_point() 

2175 ): 

2176 input_ids = input_ids.unsqueeze(0) 

2177 

2178 # Detect inputs_embeds: if the tensor is floating point, it's pre-computed 

2179 # embeddings (e.g., from multimodal models) rather than token IDs. 

2180 _is_inputs_embeds = ( 

2181 isinstance(input_ids, torch.Tensor) and input_ids.is_floating_point() 

2182 ) 

2183 

2184 # Left padding needs a mask and corrected positions. Right padding is 

2185 # harmless for causal real-token positions and remains unmasked to 

2186 # match HookedTransformer; bidirectional/encoder inputs still need it. 

2187 if ( 

2188 _is_batched_list 

2189 and attention_mask is None 

2190 and self.tokenizer is not None 

2191 and self.tokenizer.pad_token_id is not None 

2192 and not _is_inputs_embeds 

2193 and ( 

2194 _resolved_padding_side == "left" 

2195 or is_encoder_decoder 

2196 or not self.adapter.supports_causal_loss 

2197 ) 

2198 ): 

2199 attention_mask = utils.get_attention_mask( 

2200 self.tokenizer, 

2201 input_ids, 

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

2203 padding_side=_resolved_padding_side, 

2204 ).to(self.cfg.device) 

2205 # Gated on the target for the same reason the derivation below is: 

2206 # a fixed-signature forward raises TypeError on the kwarg, and a 

2207 # model that owns its own position derivation is overridden by it 

2208 # (#1626). 

2209 if "position_ids" not in kwargs and self._accepts_derived_position_ids(): 

2210 position_ids = attention_mask.long().cumsum(-1) - 1 

2211 position_ids.masked_fill_(attention_mask == 0, 1) 

2212 kwargs["position_ids"] = position_ids 

2213 

2214 # Any masked-out token shifts the absolute position of every real token 

2215 # after it, so positions must be derived from the mask rather than left 

2216 # to HF's default arange. This is the same derivation HookedTransformer 

2217 # applies in pos_embed; without it the bridge silently returns wrong 

2218 # logits. An all-ones mask reduces to arange, so this is a no-op there. 

2219 # 

2220 # The mask spans any cached prefix as well as the new tokens, so it is 

2221 # offset back to just the tokens actually being passed — matching how 

2222 # AbstractAttention/PosEmbed use past_kv_pos_offset. 

2223 if ( 

2224 attention_mask is not None 

2225 and "position_ids" not in kwargs 

2226 and not _is_inputs_embeds 

2227 and attention_mask.ndim == 2 

2228 and isinstance(input_ids, torch.Tensor) 

2229 and input_ids.ndim == 2 

2230 and attention_mask.shape[1] >= input_ids.shape[1] 

2231 and self._accepts_derived_position_ids() 

2232 ): 

2233 # .long() because callers may hand in a float 0/1 mask, and 

2234 # positions index an embedding table. 

2235 _derived = utils.get_offset_position_ids(0, attention_mask.long()) 

2236 _arange = torch.arange(attention_mask.shape[1], device=_derived.device) 

2237 # Decide per row, not per batch. A row only needs the derived 

2238 # positions when its mask actually moves one of its attended 

2239 # tokens off the default position — i.e. a masked token precedes 

2240 # a real one (left padding, or an interior gap). Rows that are 

2241 # unpadded or purely right-padded keep arange verbatim, so one 

2242 # left-padded row in a batch cannot perturb its neighbours. 

2243 _needs = ((_derived != _arange) & (attention_mask != 0)).any(dim=1, keepdim=True) 

2244 if bool(_needs.any()): 

2245 _positions = torch.where(_needs, _derived, _arange.expand_as(_derived)) 

2246 kwargs["position_ids"] = _positions[ 

2247 :, attention_mask.shape[1] - input_ids.shape[1] : 

2248 ] 

2249 

2250 if attention_mask is not None: 

2251 kwargs["attention_mask"] = attention_mask 

2252 if past_key_values is not None: 

2253 # Manual KV-cache injection: hand HF its own cache back and let it 

2254 # extend it, computing only the new tokens' keys/values. 

2255 kwargs["past_key_values"] = past_key_values 

2256 kwargs["use_cache"] = True 

2257 if kwargs.pop("use_past_kv_cache", False) or kwargs.get("use_cache", False): 

2258 kwargs["use_cache"] = True 

2259 # Auto-generate decoder_input_ids for encoder-decoder models 

2260 if "decoder_input_ids" not in kwargs and labels is None and is_encoder_decoder: 

2261 decoder_start_token_id = getattr( 

2262 self.original_model.config, "decoder_start_token_id", None 

2263 ) 

2264 if decoder_start_token_id is not None: 

2265 shifted = input_ids[:, :-1] 

2266 start_tokens = torch.full( 

2267 (input_ids.shape[0], 1), 

2268 decoder_start_token_id, 

2269 dtype=input_ids.dtype, 

2270 device=input_ids.device, 

2271 ) 

2272 kwargs["decoder_input_ids"] = torch.cat([start_tokens, shifted], dim=1) 

2273 else: 

2274 kwargs["decoder_input_ids"] = input_ids 

2275 

2276 # Tell PosEmbedBridge to expand batch=1 position_ids to full batch. 

2277 if hasattr(self, "pos_embed"): 

2278 self.pos_embed._current_batch_size = input_ids.shape[0] 

2279 

2280 # Handle pixel_values for multimodal and vision models 

2281 if pixel_values is not None: 

2282 if not ( 

2283 getattr(self.cfg, "is_multimodal", False) 

2284 or getattr(self.cfg, "is_visual_model", False) 

2285 ): 

2286 raise ValueError( 

2287 "pixel_values can only be passed to multimodal or vision models " 

2288 "(cfg.is_multimodal or cfg.is_visual_model must be True)" 

2289 ) 

2290 kwargs["pixel_values"] = pixel_values 

2291 

2292 # Handle input_values for audio models 

2293 if input_values is not None: 2293 ↛ 2294line 2293 didn't jump to line 2294 because the condition on line 2293 was never true

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

2295 raise ValueError( 

2296 "input_values can only be passed to audio models " 

2297 "(cfg.is_audio_model must be True)" 

2298 ) 

2299 kwargs["input_values"] = input_values 

2300 

2301 # Audio models use input_values (waveform), not input_ids 

2302 if getattr(self.cfg, "is_audio_model", False): 

2303 if input_values is not None: 2303 ↛ 2304line 2303 didn't jump to line 2304 because the condition on line 2303 was never true

2304 result = self._driver.forward(**kwargs) 

2305 elif isinstance(input, torch.Tensor): 2305 ↛ 2309line 2305 didn't jump to line 2309 because the condition on line 2305 was always true

2306 kwargs["input_values"] = input 

2307 result = self._driver.forward(**kwargs) 

2308 else: 

2309 raise ValueError( 

2310 "Audio models require tensor input (raw waveform). " 

2311 "Pass a torch.Tensor or use input_values parameter." 

2312 ) 

2313 elif getattr(self.cfg, "is_visual_model", False): 

2314 # "pixel_values" may already be in kwargs from the "if pixel_values is not None:" 

2315 # gate above (explicit pixel_values=... call); otherwise treat `input` itself as 

2316 # the image tensor, matching how the audio branch treats `input` as the waveform. 

2317 if "pixel_values" not in kwargs: 2317 ↛ 2325line 2317 didn't jump to line 2325 because the condition on line 2317 was always true

2318 if isinstance(input, torch.Tensor): 2318 ↛ 2321line 2318 didn't jump to line 2321 because the condition on line 2318 was always true

2319 kwargs["pixel_values"] = input 

2320 else: 

2321 raise ValueError( 

2322 "Visual models require tensor input (pixel values). " 

2323 "Pass a torch.Tensor as `input` or use the pixel_values parameter." 

2324 ) 

2325 result = self._driver.forward(**kwargs) 

2326 elif _is_inputs_embeds: 

2327 result = self._driver.forward(inputs_embeds=input_ids, **kwargs) 

2328 else: 

2329 # By keyword so kw-only ``input_ids`` drivers don't TypeError. 

2330 result = self._driver.forward(input_ids=input_ids, **kwargs) 

2331 output = result.raw_output 

2332 # No-op for HF (its hooks already fired); load-bearing for vLLM/Inspect. 

2333 if result.captured: 2333 ↛ 2334line 2333 didn't jump to line 2334 because the condition on line 2333 was never true

2334 self._replay_captures(result.captured) 

2335 # Convert TensorLike to torch at the boundary; let weird shapes 

2336 # (audio/CTC dataclasses, tuple-of-tuples) pass through unchanged — 

2337 # downstream return_type branches catch them with specific errors. 

2338 logits = result.logits 

2339 if isinstance(logits, torch.Tensor): 2339 ↛ 2341line 2339 didn't jump to line 2341 because the condition on line 2339 was always true

2340 pass 

2341 elif logits is not None and isinstance(logits, TensorLike): 

2342 logits = to_torch(logits) 

2343 # Stash only the cache object (not the full output) for generate(). 

2344 if getattr(self, "_capture_hf_cache", False): 

2345 self._last_hf_cache = getattr(output, "past_key_values", None) 

2346 if return_type == "logits_and_cache": 

2347 past_key_values = getattr(output, "past_key_values", None) 

2348 return (logits, past_key_values) 

2349 if is_encoder_decoder and return_type in ("loss", "both"): 

2350 assert isinstance( 

2351 logits, torch.Tensor 

2352 ), f"Expected seq2seq logits tensor, got {type(logits)}" 

2353 assert isinstance(labels, torch.Tensor) 

2354 return self._finalize_seq2seq_return( 

2355 return_type, 

2356 logits, 

2357 labels, 

2358 output, 

2359 loss_per_token=loss_per_token, 

2360 ) 

2361 return self._finalize_return( 

2362 return_type, 

2363 logits, 

2364 input_ids, 

2365 attention_mask=attention_mask, 

2366 labels=labels, 

2367 is_audio_model=getattr(self.cfg, "is_audio_model", False), 

2368 is_visual_model=getattr(self.cfg, "is_visual_model", False), 

2369 inputs_embeds_was_used=_is_inputs_embeds, 

2370 loss_per_token=loss_per_token, 

2371 ) 

2372 except StopAtLayerException as e: 

2373 # Execution stopped at the requested layer 

2374 return e.layer_output 

2375 finally: 

2376 # Clean up state that may be inconsistent after StopAtLayerException 

2377 if stop_at_layer is not None: 

2378 for bl_name in ( 

2379 "blocks", 

2380 "encoder_blocks", 

2381 "decoder_blocks", 

2382 "L_blocks", 

2383 "H_blocks", 

2384 ): 

2385 if hasattr(self, bl_name): 

2386 for block in getattr(self, bl_name): 

2387 block._stop_at_layer_idx = None 

2388 

2389 # Clear any stale KV cache — layers after the stop point didn't 

2390 # execute, so the cache is incomplete and would corrupt subsequent 

2391 # generate() calls that expect a full cache. 

2392 if hasattr(self, "_last_hf_cache"): 2392 ↛ 2393line 2392 didn't jump to line 2393 because the condition on line 2392 was never true

2393 del self._last_hf_cache 

2394 

2395 if start_at_layer is not None: 

2396 self._teardown_start_at_layer() 

2397 

2398 def _setup_start_at_layer(self, input: Any, start_at_layer: int) -> torch.Tensor: 

2399 """Arm residual-stream injection at block ``start_at_layer``. 

2400 

2401 ``input`` is the residual entering that block. It is returned (batch-promoted) 

2402 to drive the HF forward as ``inputs_embeds`` so position ids / attention mask 

2403 are computed for the right sequence length; block ``start_at_layer`` then swaps 

2404 it back in for its own input, discarding whatever blocks 0..k-1 produced. 

2405 """ 

2406 if getattr(self.cfg, "is_visual_model", False): 

2407 raise NotImplementedError( 

2408 "start_at_layer is not supported for vision models: the residual " 

2409 "re-entry path assumes token inputs_embeds." 

2410 ) 

2411 if getattr(self.cfg, "is_audio_model", False): 

2412 raise NotImplementedError( 

2413 "start_at_layer is not supported for audio models: audio encoders " 

2414 "process waveforms through convolutional feature extraction before " 

2415 "the transformer blocks, making residual-stream injection infeasible." 

2416 ) 

2417 for alt in ("encoder_blocks", "decoder_blocks", "L_blocks", "H_blocks"): 

2418 if hasattr(self, alt): 2418 ↛ 2419line 2418 didn't jump to line 2419 because the condition on line 2418 was never true

2419 raise NotImplementedError( 

2420 "start_at_layer is only supported on the standard 'blocks' stack, " 

2421 f"not {alt!r}." 

2422 ) 

2423 if not self._has_registered_blocks(): 

2424 raise NotImplementedError("start_at_layer requires a 'blocks' stack.") 

2425 

2426 if not (isinstance(input, torch.Tensor) and input.is_floating_point()): 

2427 raise ValueError( 

2428 "start_at_layer requires a residual-stream tensor [batch, pos, d_model]; " 

2429 f"got {type(input).__name__}. Capture it from a prior run_with_cache " 

2430 "(e.g. cache['blocks.k.hook_in'])." 

2431 ) 

2432 residual = input if input.ndim == 3 else input.unsqueeze(0) 

2433 n_blocks = len(self.blocks) 

2434 if start_at_layer < 0: 

2435 start_at_layer += n_blocks 

2436 if not 0 <= start_at_layer < n_blocks: 

2437 raise ValueError(f"start_at_layer={start_at_layer} out of range [0, {n_blocks}).") 

2438 # The returned tensor is fed to HF as inputs_embeds; stash a clone so an 

2439 # architecture that mutates inputs_embeds in place can't corrupt the value 

2440 # block ``start_at_layer`` swaps back in. 

2441 injected = residual.clone() 

2442 for block in self.blocks: 

2443 block._start_at_layer_idx = start_at_layer 

2444 block._start_residual = injected 

2445 return residual 

2446 

2447 def _teardown_start_at_layer(self) -> None: 

2448 """Clear the residual-injection state set by ``_setup_start_at_layer``.""" 

2449 if hasattr(self, "blocks"): 2449 ↛ exitline 2449 didn't return from function '_teardown_start_at_layer' because the condition on line 2449 was always true

2450 for block in self.blocks: 

2451 block._start_at_layer_idx = None 

2452 block._start_residual = None 

2453 

2454 # loss_fn inherited from BridgeCore 

2455 

2456 def _resolve_stopping_criteria( 

2457 self, 

2458 stop_strings: Optional[Union[str, List[str]]], 

2459 stopping_criteria: Optional[Any], 

2460 ) -> Optional[Any]: 

2461 """Combine ``stop_strings`` and ``stopping_criteria`` into one StoppingCriteriaList. 

2462 

2463 Returns ``None`` when neither is supplied (or both reduce to no-ops), 

2464 so callers can cheaply check whether any extra stop signal is active. 

2465 ``stop_strings`` is turned into a HuggingFace ``StopStringCriteria`` (which reproduces 

2466 HF's exact partial-token-aware, end-anchored matching: it fires when the stop string 

2467 ends the generated text, even if the string straddles token boundaries) and therefore 

2468 requires a tokenizer. 

2469 A user-supplied ``stopping_criteria`` may be a single ``StoppingCriteria``, 

2470 a list of them, or a ``StoppingCriteriaList``. 

2471 

2472 Raises: 

2473 ValueError: if ``stop_strings`` is supplied without a tokenizer. 

2474 TypeError: if ``stopping_criteria`` is not a ``StoppingCriteria``, a 

2475 list/tuple of them, or a ``StoppingCriteriaList``. 

2476 """ 

2477 if stop_strings is None and stopping_criteria is None: 

2478 return None 

2479 

2480 from transformers import ( # local import: matches the file's transformers usage 

2481 StoppingCriteria, 

2482 StoppingCriteriaList, 

2483 StopStringCriteria, 

2484 ) 

2485 

2486 criteria = StoppingCriteriaList() 

2487 

2488 if stop_strings is not None: 

2489 strings = [stop_strings] if isinstance(stop_strings, str) else list(stop_strings) 

2490 strings = [s for s in strings if s] # drop empty strings (HF errors on them) 

2491 if strings: 

2492 if self.tokenizer is None: 

2493 raise ValueError( 

2494 "stop_strings requires a tokenizer (stop strings are detected by " 

2495 "matching against the tokenizer vocabulary), but this TransformerBridge " 

2496 "has no tokenizer. Pass a stopping_criteria callable that operates on " 

2497 "token ids instead, or use hf_generate()." 

2498 ) 

2499 criteria.append(StopStringCriteria(tokenizer=self.tokenizer, stop_strings=strings)) 

2500 

2501 if stopping_criteria is not None: 

2502 if isinstance(stopping_criteria, StoppingCriteriaList): 

2503 criteria.extend(stopping_criteria) 

2504 elif isinstance(stopping_criteria, (list, tuple)): 

2505 criteria.extend(stopping_criteria) 

2506 elif isinstance(stopping_criteria, StoppingCriteria): 

2507 criteria.append(stopping_criteria) 

2508 else: 

2509 raise TypeError( 

2510 "stopping_criteria must be a transformers.StoppingCriteria, a list of " 

2511 f"them, or a StoppingCriteriaList, but got {type(stopping_criteria).__name__}." 

2512 ) 

2513 

2514 return criteria if len(criteria) > 0 else None 

2515 

2516 def _encdec_ngram_processor(self) -> Optional[Any]: 

2517 """generation_config.no_repeat_ngram_size as transformers' own 

2518 processor, or None. HF applies it by default; parity for models whose 

2519 greedy decode needs it to escape token attractors.""" 

2520 size = getattr( 

2521 getattr(self.original_model, "generation_config", None), 

2522 "no_repeat_ngram_size", 

2523 None, 

2524 ) 

2525 if not size: 

2526 return None 

2527 from transformers.generation.logits_process import NoRepeatNGramLogitsProcessor 

2528 

2529 return NoRepeatNGramLogitsProcessor(size) 

2530 

2531 def _encdec_forced_eos_token_id(self) -> Optional[int]: 

2532 """generation_config.forced_eos_token_id, or None. HF forces EOS as the 

2533 final token when max length is reached; BART-family configs set it by 

2534 default and their summaries otherwise end mid-sentence.""" 

2535 value = getattr( 

2536 getattr(self.original_model, "generation_config", None), 

2537 "forced_eos_token_id", 

2538 None, 

2539 ) 

2540 return value if isinstance(value, int) else None 

2541 

2542 def _encoder_decoder_setup( 

2543 self, input_tokens: torch.Tensor, forced_bos_token_id: Optional[int] 

2544 ) -> tuple[torch.Tensor, Optional[int]]: 

2545 """Share decoder initialization and length defaults across generation paths.""" 

2546 config = self.original_model.config 

2547 generation_config = getattr(self.original_model, "generation_config", None) 

2548 if forced_bos_token_id is None: 

2549 # HF defaults such as bart-large-cnn's forced BOS apply to both loops. 

2550 forced_bos_token_id = getattr(generation_config, "forced_bos_token_id", None) 

2551 min_decoder_length = getattr(generation_config, "min_length", None) 

2552 start = getattr(config, "decoder_start_token_id", None) 

2553 if start is None: 

2554 # HF falls back to BOS, then EOS; IndicBART starts decoding from EOS. 

2555 start = getattr(config, "bos_token_id", None) 

2556 if start is None: 

2557 start = getattr(config, "eos_token_id", None) 

2558 if isinstance(start, (list, tuple)): 

2559 start = start[0] 

2560 if start is None: 

2561 start = 0 

2562 seed = torch.full( 

2563 (input_tokens.shape[0], 1), 

2564 start, 

2565 dtype=input_tokens.dtype, 

2566 device=input_tokens.device, 

2567 ) 

2568 if forced_bos_token_id is not None: 

2569 # Multilingual seq2seq selects the target language after decoder_start. 

2570 forced = torch.full_like(seed, forced_bos_token_id) 

2571 seed = torch.cat([seed, forced], dim=1) 

2572 return seed, min_decoder_length 

2573 

2574 def _generate_tokens( 

2575 self, 

2576 current_tokens: torch.Tensor, 

2577 input_tokens: torch.Tensor, 

2578 batch_size: int, 

2579 *, 

2580 max_new_tokens: int, 

2581 do_sample: bool, 

2582 top_k: Optional[int], 

2583 top_p: Optional[float], 

2584 temperature: float, 

2585 freq_penalty: float, 

2586 repetition_penalty: float, 

2587 stop_at_eos: bool, 

2588 stop_tokens: List[int], 

2589 eos_token_for_padding: int, 

2590 finished_sequences: torch.Tensor, 

2591 use_past_kv_cache: bool, 

2592 use_stateful_cache: bool, 

2593 mamba_cache: Any, 

2594 mamba_conv_kernel: int, 

2595 is_encoder_decoder: bool, 

2596 _is_batched_list: bool, 

2597 _generate_from_embeds: bool, 

2598 encoder_input: Optional[torch.Tensor], 

2599 decoder_tokens: Optional[torch.Tensor], 

2600 generated_token_ids: Optional[List[torch.Tensor]], 

2601 pixel_values: Optional[torch.Tensor], 

2602 multimodal_kwargs: Dict[str, Any], 

2603 verbose: bool, 

2604 stopping_criteria_list: Optional[Any] = None, 

2605 initial_attention_mask: Optional[torch.Tensor] = None, 

2606 min_decoder_length: Optional[int] = None, 

2607 ngram_processor: Optional[Any] = None, 

2608 encoder_attention_mask: Optional[torch.Tensor] = None, 

2609 forced_eos_token_id: Optional[int] = None, 

2610 ) -> Generator[Tuple[torch.Tensor, torch.Tensor, bool], None, None]: 

2611 """Core generation loop. Yields (sampled_tokens, final_logits, all_finished) per step. 

2612 

2613 Owns the forward pass, sampling, stop handling (EOS and any 

2614 ``stopping_criteria_list``), token accumulation, and KV cache management. Callers 

2615 are responsible for try/finally cleanup of ``_capture_hf_cache``. 

2616 

2617 ``stopping_criteria_list`` (from ``_resolve_stopping_criteria``) is evaluated on 

2618 the running sequence each step and folded into the finished-sequence mask alongside 

2619 EOS, so when it is ``None`` the loop runs the EOS-only path unchanged. 

2620 """ 

2621 _hf_kv_cache = None 

2622 # A prior multimodal forward caches mrope deltas on the wrapped model, and a 

2623 # text-only generation would inherit them through the KV-cache position path 

2624 # (HF's own generate recomputes them at prefill; driving forward() directly 

2625 # makes that reset this loop's job). 

2626 base = getattr(self.original_model, "base_model", self.original_model) 

2627 if getattr(base, "rope_deltas", None) is not None: 

2628 base.rope_deltas = None 

2629 # A row may finish via EOS and/or any of the configured stopping criteria. 

2630 any_stop_active = stop_at_eos or stopping_criteria_list is not None 

2631 

2632 # Models that own their position derivation (the gate refuses them) cache 

2633 # mRoPE deltas on the module between calls; a text-only prefill never 

2634 # refreshes them, so a stale delta from an earlier multimodal forward gets 

2635 # added to every cached-step position. HF's generate recomputes them at 

2636 # prefill via prepare_inputs_for_generation, which this loop bypasses — 

2637 # so match it by clearing before the prompt pass. A multimodal prefill 

2638 # recomputes its own fresh deltas regardless. 

2639 if not self._accepts_derived_position_ids(): 

2640 underlying = getattr(self, "original_model", None) 

2641 for module in ( 

2642 underlying, 

2643 getattr(underlying, "model", None), 

2644 getattr(underlying, "language_model", None), 

2645 ): 

2646 if module is not None and hasattr(module, "rope_deltas"): 

2647 module.rope_deltas = None 

2648 

2649 # Pure-SSM models (Mamba-1/2) take the stateful cache as `cache_params`; 

2650 # modern hybrids (Bamba, NemotronH, FalconH1) take `past_key_values` and 

2651 # would receive a duplicate cache_params via **kwargs cascade otherwise. 

2652 stateful_cache_kwarg = "cache_params" 

2653 if use_stateful_cache: 

2654 forward_params = inspect.signature(self.original_model.forward).parameters 

2655 if "cache_params" not in forward_params: 

2656 stateful_cache_kwarg = "past_key_values" 

2657 

2658 for gen_step_idx in tqdm.tqdm(range(max_new_tokens), disable=not verbose): 

2659 with torch.no_grad(): 

2660 if is_encoder_decoder: 

2661 assert encoder_input is not None 

2662 encdec_kwargs: Dict[str, Any] = {} 

2663 if encoder_attention_mask is not None: 

2664 encdec_kwargs["attention_mask"] = encoder_attention_mask.to( 

2665 encoder_input.device 

2666 ) 

2667 logits = self( 

2668 encoder_input, 

2669 return_type="logits", 

2670 decoder_input=decoder_tokens, 

2671 **encdec_kwargs, 

2672 ) 

2673 else: 

2674 forward_kwargs: Dict[str, Any] = {} 

2675 # A prompt mask covers only the prompt, so extend it by one 

2676 # attended column per token generated so far. position_ids are 

2677 # left to forward(), which derives them from the mask for the 

2678 # models that can take them. 

2679 running_attention_mask: Optional[torch.Tensor] = None 

2680 if initial_attention_mask is not None: 

2681 n_generated = current_tokens.shape[1] - initial_attention_mask.shape[1] 

2682 running_attention_mask = torch.cat( 

2683 [ 

2684 initial_attention_mask.to(current_tokens.device), 

2685 torch.ones( 

2686 (current_tokens.shape[0], n_generated), 

2687 dtype=initial_attention_mask.dtype, 

2688 device=current_tokens.device, 

2689 ), 

2690 ], 

2691 dim=1, 

2692 ) 

2693 forward_kwargs["attention_mask"] = running_attention_mask 

2694 # Compute attention mask and position_ids for batched 

2695 # inputs with padding. 

2696 if ( 

2697 initial_attention_mask is None 

2698 and _is_batched_list 

2699 and self.tokenizer is not None 

2700 and self.tokenizer.pad_token_id is not None 

2701 ): 

2702 attn_mask = utils.get_attention_mask( 

2703 self.tokenizer, 

2704 current_tokens, 

2705 prepend_bos=getattr(self.cfg, "default_prepend_bos", True), 

2706 padding_side="left", 

2707 ).to(self.cfg.device) 

2708 forward_kwargs["attention_mask"] = attn_mask 

2709 # Same target gate as the forward() path: the mask is safe 

2710 # for every model, the derived positions are not (#1626). 

2711 if self._accepts_derived_position_ids(): 

2712 position_ids = attn_mask.long().cumsum(-1) - 1 

2713 position_ids.masked_fill_(attn_mask == 0, 1) 

2714 forward_kwargs["position_ids"] = position_ids 

2715 if gen_step_idx == 0: 

2716 if pixel_values is not None: 

2717 forward_kwargs["pixel_values"] = pixel_values 

2718 if multimodal_kwargs: 

2719 forward_kwargs.update(multimodal_kwargs) 

2720 if use_stateful_cache: 

2721 forward_kwargs[stateful_cache_kwarg] = mamba_cache 

2722 forward_kwargs["use_cache"] = True 

2723 if gen_step_idx == 0: 

2724 # Mamba's conv-window warmup positions vs standard 

2725 # full-prompt positions for past_key_values hybrids. 

2726 prefill_len = ( 

2727 mamba_conv_kernel 

2728 if stateful_cache_kwarg == "cache_params" 

2729 else current_tokens.shape[1] 

2730 ) 

2731 cache_position = torch.arange(0, prefill_len, device=self.cfg.device) 

2732 forward_kwargs["cache_position"] = cache_position 

2733 logits = self( 

2734 current_tokens, 

2735 return_type="logits", 

2736 **forward_kwargs, 

2737 ) 

2738 else: 

2739 input_seq_pos = input_tokens.shape[1] + gen_step_idx - 1 

2740 cache_position = torch.tensor([input_seq_pos], device=self.cfg.device) 

2741 forward_kwargs["cache_position"] = cache_position 

2742 if "position_ids" in forward_kwargs: 2742 ↛ 2743line 2742 didn't jump to line 2743 because the condition on line 2742 was never true

2743 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

2744 :, -1: 

2745 ] 

2746 logits = self( 

2747 current_tokens[:, -1:], 

2748 return_type="logits", 

2749 **forward_kwargs, 

2750 ) 

2751 elif use_past_kv_cache: 

2752 forward_kwargs["use_cache"] = True 

2753 if _hf_kv_cache is not None: 

2754 forward_kwargs["past_key_values"] = _hf_kv_cache 

2755 # HF v5 + macOS-arm64 NaNs when these are inferred 

2756 # from cache state alone. Mirror HF generate(): pass 

2757 # both an (batch, total_len) attention_mask and a 

2758 # (batch, 1) position_ids for the new token. 

2759 batch_size = current_tokens.shape[0] 

2760 total_len = current_tokens.shape[1] 

2761 device = current_tokens.device 

2762 if "attention_mask" not in forward_kwargs: 

2763 forward_kwargs["attention_mask"] = torch.ones( 

2764 (batch_size, total_len), 

2765 dtype=torch.long, 

2766 device=device, 

2767 ) 

2768 # Gated as a whole (#1626): every branch below supplies 

2769 # position_ids, so gating only the prompt derivation 

2770 # above would divert a refused model into the 

2771 # total_len - 1 fallback, which counts pad slots and is 

2772 # wrong per row for a left-padded batch. A model that 

2773 # owns its position derivation gets the mask alone, 

2774 # matching the uncached path. 

2775 if self._accepts_derived_position_ids(): 

2776 if "position_ids" in forward_kwargs: 

2777 forward_kwargs["position_ids"] = forward_kwargs["position_ids"][ 

2778 :, -1: 

2779 ] 

2780 elif running_attention_mask is not None: 

2781 # total_len - 1 counts pad slots, so it is wrong 

2782 # for a left-padded prompt. Derive the new token's 

2783 # position from the mask instead. 

2784 forward_kwargs["position_ids"] = utils.get_offset_position_ids( 

2785 0, running_attention_mask.long() 

2786 )[:, -1:] 

2787 else: 

2788 forward_kwargs["position_ids"] = torch.full( 

2789 (batch_size, 1), 

2790 total_len - 1, 

2791 dtype=torch.long, 

2792 device=device, 

2793 ) 

2794 logits = self( 

2795 current_tokens[:, -1:], 

2796 return_type="logits", 

2797 **forward_kwargs, 

2798 ) 

2799 else: 

2800 logits = self( 

2801 current_tokens, 

2802 return_type="logits", 

2803 **forward_kwargs, 

2804 ) 

2805 else: 

2806 logits = self(current_tokens, return_type="logits", **forward_kwargs) 

2807 if use_past_kv_cache and hasattr(self, "_last_hf_cache"): 

2808 _hf_kv_cache = self._last_hf_cache or _hf_kv_cache 

2809 del self._last_hf_cache 

2810 final_logits = logits[:, -1, :] 

2811 

2812 # Sample next token 

2813 penalty_tokens = ( 

2814 torch.stack(generated_token_ids, dim=1) 

2815 if _generate_from_embeds and generated_token_ids 

2816 else None 

2817 ) 

2818 # transformers' own NoRepeatNGramLogitsProcessor, honoring 

2819 # generation_config (bart-large-cnn pins 3; without it greedy 

2820 # decoding falls into a BOS attractor and emits nothing). 

2821 if ngram_processor is not None and decoder_tokens is not None: 

2822 final_logits = ngram_processor(decoder_tokens, final_logits) 

2823 # HF's generate() suppresses EOS below generation_config.min_length 

2824 # (bart-large-cnn pins 56); without this the loop can EOS on step 

2825 # one and emit an empty summary. 

2826 if ( 

2827 min_decoder_length is not None 

2828 and is_encoder_decoder 

2829 and decoder_tokens is not None 

2830 and decoder_tokens.shape[1] < min_decoder_length 

2831 and stop_tokens 

2832 ): 

2833 final_logits = final_logits.clone() 

2834 final_logits[:, stop_tokens] = float("-inf") 

2835 # Force EOS as the last allowed token, applied after the other 

2836 # processors so it wins — the same ordering HF uses for 

2837 # ForcedEOSTokenLogitsProcessor. 

2838 if forced_eos_token_id is not None and gen_step_idx == max_new_tokens - 1: 

2839 final_logits = torch.full_like(final_logits, float("-inf")) 

2840 final_logits[:, forced_eos_token_id] = 0.0 

2841 if do_sample: 

2842 sampled_tokens = utils.sample_logits( 

2843 final_logits, 

2844 top_k=top_k, 

2845 top_p=top_p, 

2846 temperature=temperature, 

2847 freq_penalty=freq_penalty, 

2848 repetition_penalty=repetition_penalty, 

2849 tokens=( 

2850 penalty_tokens 

2851 if _generate_from_embeds 

2852 else (decoder_tokens if is_encoder_decoder else current_tokens) 

2853 ), 

2854 ).to(self.cfg.device) 

2855 else: 

2856 sampled_tokens = utils.sample_logits( 

2857 final_logits, 

2858 temperature=0.0, 

2859 repetition_penalty=repetition_penalty, 

2860 tokens=( 

2861 penalty_tokens 

2862 if _generate_from_embeds 

2863 else (decoder_tokens if is_encoder_decoder else current_tokens) 

2864 ), 

2865 ).to(self.cfg.device) 

2866 

2867 # Freeze rows that finished on an earlier step so they stop emitting 

2868 # real tokens. Applies to every active stop mechanism, not just EOS. 

2869 if any_stop_active: 

2870 sampled_tokens[finished_sequences] = eos_token_for_padding 

2871 

2872 # Fold this step's EOS matches into the finished mask. 

2873 if stop_at_eos: 

2874 finished_sequences.logical_or_( 

2875 torch.isin( 

2876 sampled_tokens.to(self.cfg.device), 

2877 torch.tensor(stop_tokens).to(self.cfg.device), 

2878 ) 

2879 ) 

2880 

2881 # Update token sequences 

2882 if is_encoder_decoder: 

2883 assert decoder_tokens is not None 

2884 decoder_tokens = torch.cat([decoder_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

2885 elif _generate_from_embeds: 

2886 assert generated_token_ids is not None 

2887 generated_token_ids.append(sampled_tokens) 

2888 embed_fn = self.original_model.get_input_embeddings() # type: ignore[operator] 

2889 assert embed_fn is not None 

2890 new_embed = embed_fn(sampled_tokens.unsqueeze(1)).to(current_tokens.dtype) 

2891 current_tokens = torch.cat([current_tokens, new_embed], dim=1) 

2892 else: 

2893 current_tokens = torch.cat([current_tokens, sampled_tokens.unsqueeze(1)], dim=1) 

2894 

2895 # Fold stop_strings / stopping_criteria into the finished mask. They are 

2896 # evaluated on the full running sequence (prompt + everything generated so 

2897 # far, including the token just appended) with this step's logits as the 

2898 # scores argument, matching transformers' StoppingCriteria contract. The 

2899 # combined list returns a per-row bool [batch] OR-ing every criterion. 

2900 # generate()/generate_stream() guarantee this is plain decoder-only token 

2901 # generation, so current_tokens is the running token sequence. 

2902 if stopping_criteria_list is not None: 

2903 criteria_finished = stopping_criteria_list(current_tokens, final_logits).to( 

2904 device=self.cfg.device, dtype=torch.bool 

2905 ) 

2906 if criteria_finished.shape != finished_sequences.shape: 

2907 raise ValueError( 

2908 "A stopping criterion returned shape " 

2909 f"{tuple(criteria_finished.shape)}, expected a per-row bool of " 

2910 f"shape {tuple(finished_sequences.shape)} (one entry per sequence)." 

2911 ) 

2912 finished_sequences.logical_or_(criteria_finished) 

2913 

2914 all_finished = bool(any_stop_active and finished_sequences.all().item()) 

2915 

2916 yield sampled_tokens, final_logits, all_finished 

2917 

2918 if all_finished: 2918 ↛ 2919line 2918 didn't jump to line 2919 because the condition on line 2918 was never true

2919 return 

2920 

2921 def _resolve_generation_caching(self, use_past_kv_cache: bool, batched: bool) -> bool: 

2922 """Honor adapter caching/batching limits (recurrent/conv decoders have no KV 

2923 cache; batching is rejected where padding can't be masked, not mis-generated).""" 

2924 if batched and not getattr(self.adapter, "supports_batched_generation", True): 

2925 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2926 raise NotImplementedError( 

2927 f"Batched generation is not supported by {architecture}: its forward does not " 

2928 "apply an attention mask, so padded rows would corrupt the output. Generate one " 

2929 "sequence at a time, or pass equal-length inputs as a tensor." 

2930 ) 

2931 if not getattr(self.adapter, "supports_kv_cache", True): 

2932 return False 

2933 return use_past_kv_cache 

2934 

2935 def _ensure_generation_supported(self, api_name: str) -> None: 

2936 """Reject autoregressive generation for forward-only architectures.""" 

2937 if not self.adapter.supports_generation: 

2938 architecture = self.cfg.architecture or type(self.adapter).__name__ 

2939 hint = ( 

2940 " Use diffusion_generate() — this architecture samples by iterative denoising, " 

2941 "not left-to-right." 

2942 if getattr(self.adapter, "native_sampler", None) 

2943 else "" 

2944 ) 

2945 raise NotImplementedError( 

2946 f"TransformerBridge.{api_name}() generation is not supported by " 

2947 f"the {architecture} architecture.{hint}" 

2948 ) 

2949 

2950 def generate( 

2951 self, 

2952 input: Union[str, List[str], torch.Tensor] = "", 

2953 max_new_tokens: int = 10, 

2954 stop_at_eos: bool = True, 

2955 eos_token_id: Optional[int] = None, 

2956 do_sample: bool = True, 

2957 top_k: Optional[int] = None, 

2958 top_p: Optional[float] = None, 

2959 temperature: float = 1.0, 

2960 freq_penalty: float = 0.0, 

2961 repetition_penalty: float = 1.0, 

2962 use_past_kv_cache: bool = True, 

2963 prepend_bos: Optional[bool] = None, 

2964 padding_side: Optional[str] = None, 

2965 return_type: Optional[str] = "input", 

2966 verbose: bool = True, 

2967 output_logits: bool = False, 

2968 return_cache: bool = False, 

2969 return_input_tokens: bool = False, 

2970 names_filter: Optional[Union[str, List[str], Callable[[str], bool]]] = None, 

2971 device: Optional[Union[str, torch.device]] = None, 

2972 pixel_values: Optional[torch.Tensor] = None, 

2973 stop_strings: Optional[Union[str, List[str]]] = None, 

2974 stopping_criteria: Optional[Any] = None, 

2975 attention_mask: Optional[torch.Tensor] = None, 

2976 forced_bos_token_id: Optional[int] = None, 

2977 **multimodal_kwargs, 

2978 ) -> ( 

2979 str 

2980 | list[str] 

2981 | torch.Tensor 

2982 | Any 

2983 | tuple[Any, ActivationCache] 

2984 | tuple[Any, torch.Tensor] 

2985 ): # Any for transformers.utils.ModelOutput 

2986 # Any: beartype forward ref limitation (beartype#546) 

2987 """Sample tokens from the model. 

2988 

2989 Sample tokens from the model until the model outputs eos_token or max_new_tokens is reached. 

2990 This implementation is based on HookedTransformer.generate() to ensure consistent behavior. 

2991 

2992 Args: 

2993 input: Text string, list of strings, or tensor of tokens 

2994 max_new_tokens: Maximum number of tokens to generate 

2995 stop_at_eos: If True, stop generating tokens when the model outputs eos_token 

2996 eos_token_id: The token ID to use for end of sentence 

2997 do_sample: If True, sample from the model's output distribution. Otherwise, use greedy search 

2998 top_k: Number of tokens to sample from. If None, sample from all tokens 

2999 top_p: Probability mass to sample from. If 1.0, sample from all tokens 

3000 temperature: Temperature for sampling. Higher values will make the model more random 

3001 freq_penalty: Frequency penalty for sampling - how much to penalise previous tokens 

3002 repetition_penalty: HuggingFace-style repetition penalty. Values > 1.0 discourage 

3003 repetition by dividing positive logits and multiplying negative logits for 

3004 previously seen tokens. Default 1.0 (no penalty). 

3005 use_past_kv_cache: If True, use KV caching for faster generation 

3006 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

3007 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

3008 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

3009 text that already contains the BOS token to avoid double-BOS. 

3010 Ignored when input is already a token tensor. 

3011 padding_side: Which side to pad when tokenizing multiple strings of different 

3012 lengths. For batched list inputs, left-padding is forced internally for 

3013 correct generation behavior. Defaults to None (tokenizer default). 

3014 return_type: The type of output to return - 'input', 'str', or 'tokens' 

3015 verbose: Not used in Bridge (kept for API compatibility) 

3016 output_logits: If True, return a ModelOutput with sequences and logits tuple 

3017 return_cache: If True, also return an ActivationCache for the full prompt + 

3018 generated sequence, identical to ``run_with_cache(output)``, and the call 

3019 returns an ``(output, cache)`` tuple. Implemented as one extra clean forward 

3020 over the output, so the cache includes every hook point (attention patterns 

3021 included). Supported only for single-sequence, decoder-only text generation; 

3022 encoder-decoder, SSM, multimodal, batched, and inputs_embeds inputs raise 

3023 NotImplementedError. The cache spans prompt + max_new_tokens and can be large, 

3024 use ``names_filter`` to scope it and/or ``device`` to offload it. 

3025 return_input_tokens: If True, return an ``(output, input_tokens)`` tuple where 

3026 ``input_tokens`` is the token tensor that was actually fed to the model 

3027 (after BOS handling). Useful for debugging tokenization, especially when 

3028 using chat templates where BOS handling can be subtle. Can be combined 

3029 with ``return_cache`` to get ``(output, cache, input_tokens)``. 

3030 names_filter: Passed to ``run_with_cache`` when ``return_cache=True``; restricts 

3031 which activations are cached (str, list of str, or callable). 

3032 device: Passed through when ``return_cache=True`` to offload the cached tensors 

3033 to this device (e.g. "cpu") to save accelerator memory. 

3034 pixel_values: Optional image tensor for multimodal models. Only passed on the 

3035 first generation step (the vision encoder processes the image once, then 

3036 embeddings are part of the token sequence for subsequent steps). 

3037 stop_strings: Optional string or list of strings. A sequence stops once its 

3038 generated text ends with one of these strings, using HuggingFace's 

3039 StopStringCriteria (partial-token-aware, end-anchored) matching. 

3040 Requires a tokenizer (raises ValueError otherwise). 

3041 Independent of stop_at_eos: either can stop a sequence. 

3042 stopping_criteria: Optional HuggingFace stopping criteria, a single 

3043 transformers.StoppingCriteria, a list of them, or a StoppingCriteriaList. 

3044 Each is called as criterion(input_ids, scores) after every step and ORed 

3045 with the other stop signals, where input_ids is the running sequence and 

3046 scores is this step's logits ([batch, d_vocab]). Each criterion must return 

3047 a per-row bool [batch] (or a scalar bool). stop_strings and stopping_criteria 

3048 are supported only for standard decoder-only text generation. Encoder-decoder, 

3049 inputs_embeds, and multimodal generation always raise NotImplementedError. 

3050 Stateful/SSM models raise only when run with use_past_kv_cache=False (the 

3051 default keeps them on the hooked loop). Each error names the supported 

3052 alternative. 

3053 attention_mask: Optional ``[batch, pos]`` 0/1 mask over the prompt, marking 

3054 which prompt tokens are real. Required to generate correctly from an 

3055 already-padded token tensor: without it the pad tokens are treated as 

3056 real context and every real token's position is shifted, so the 

3057 continuation differs from the same prompt unpadded. The mask is extended 

3058 by one attended column per generated token. Takes precedence over the 

3059 ``padding_side`` heuristic, and unlike it can express an interior gap or 

3060 a pad id that also occurs as a real token. Passing ``padding_side`` 

3061 instead reads the padding off the pad token, which is enough for the 

3062 common single-edge case, and raises if this bridge has no tokenizer 

3063 or pad id to read it from. On the encoder-decoder and inputs_embeds 

3064 paths the mask is forwarded to the model as-is rather than grown per 

3065 step, which is what processors emitting one alongside 

3066 ``pixel_values`` expect. 

3067 forced_bos_token_id: Optional token id seeded as the first decoder token 

3068 after ``decoder_start`` on encoder-decoder models. Multilingual 

3069 translators (M2M100/MBart/NLLB) select their target language this way. 

3070 Raises ValueError on decoder-only models. 

3071 

3072 Returns: 

3073 Generated sequence as string, list of strings, or tensor depending on input type and return_type. 

3074 If output_logits=True, returns a ModelOutput-like object with 'sequences' and 'logits' attributes. 

3075 If return_cache=True, returns an ``(output, ActivationCache)`` tuple where ``output`` is the 

3076 value that would otherwise be returned and the cache equals ``run_with_cache(output)``. 

3077 If return_input_tokens=True, returns an ``(output, input_tokens)`` tuple. 

3078 If both return_cache and return_input_tokens are True, returns ``(output, cache, input_tokens)``. 

3079 

3080 Example: 

3081 ``out, cache = model.generate(prompt, max_new_tokens=20, return_cache=True)`` returns a 

3082 normal ActivationCache over the full prompt + generated sequence (equivalent to 

3083 ``run_with_cache(out)``). 

3084 

3085 ``out, input_tokens = model.generate(prompt, return_input_tokens=True)`` returns 

3086 the tokens that were fed to the model, useful for verifying BOS handling with 

3087 chat templates. 

3088 """ 

3089 self._ensure_generation_supported("generate") 

3090 # padding_side is handled internally: for batched list inputs, left-padding 

3091 # is forced to ensure correct generation. See _is_batched_list logic below. 

3092 

3093 # Stateful dispatch is decided after input parsing so we can fall back 

3094 # to hf_generate() for input types the stateful loop doesn't handle. 

3095 is_stateful_model = getattr(self.cfg, "is_stateful", False) 

3096 

3097 _is_batched_list = isinstance(input, list) and len(input) > 1 

3098 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

3099 

3100 _generate_from_embeds = False 

3101 _encdec_early = hasattr(self.original_model, "config") and getattr( 

3102 self.original_model.config, "is_encoder_decoder", False 

3103 ) 

3104 if isinstance(input, str): 

3105 if _encdec_early: 

3106 # Deliberate divergence: prepend_bos is IGNORED for enc-dec 

3107 # string/list input. Encoder input follows the tokenizer's own 

3108 # recipe (lang token + trailing </s>); to_tokens' decoder-style 

3109 # BOS policy corrupts it — m2m100 degenerates to loops. 

3110 input_tokens = self.tokenizer(input, return_tensors="pt")["input_ids"].to( 

3111 self.cfg.device 

3112 ) 

3113 else: 

3114 input_tokens = self.to_tokens( 

3115 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3116 ) 

3117 input_type = "str" 

3118 elif isinstance(input, list): 

3119 if _encdec_early: 

3120 # Same native-recipe rule as the str branch: to_tokens' BOS 

3121 # policy corrupts encoder inputs (stray <s>, dropped </s>). 

3122 # Keep the tokenizer's mask too — unequal rows otherwise 

3123 # attend over pads in the encoder. 

3124 _enc_batch = self.tokenizer(input, return_tensors="pt", padding=True) 

3125 input_tokens = _enc_batch["input_ids"].to(self.cfg.device) 

3126 if attention_mask is None and "attention_mask" in _enc_batch: 3126 ↛ 3140line 3126 didn't jump to line 3140 because the condition on line 3126 was always true

3127 attention_mask = _enc_batch["attention_mask"].to(self.cfg.device) 

3128 else: 

3129 # Force left-padding for batched generation so real tokens are 

3130 # flush-right and logits[:, -1, :] is always the last real token. 

3131 # Passed as a kwarg rather than assigned: a raise between assignment 

3132 # and restore would pin the shared tokenizer left for the session. 

3133 input_tokens = self.to_tokens( 

3134 input, 

3135 prepend_bos=prepend_bos, 

3136 padding_side="left" if _is_batched_list else None, 

3137 move_to_device=True, 

3138 truncate=False, 

3139 ) 

3140 input_type = "list" 

3141 elif isinstance(input, torch.Tensor) and input.is_floating_point(): 

3142 # inputs_embeds: pre-computed embeddings (e.g., from multimodal models) 

3143 input_tokens = input.to(self.cfg.device) 

3144 input_type = "embeds" 

3145 _generate_from_embeds = True 

3146 else: 

3147 input_tokens = input.to(self.cfg.device) 

3148 input_type = "tokens" 

3149 

3150 # Without one of these a pre-padded tensor generates as though its pads were 

3151 # real context, shifting every real token's position (#1612). An explicit 

3152 # mask wins; otherwise the padding is read off the tokens, but only when the 

3153 # caller asked for that by passing padding_side. Deriving a mask on the 

3154 # default path would silently change behaviour for every existing caller, 

3155 # and would demand a real tokenizer where today none is required. 

3156 initial_attention_mask: Optional[torch.Tensor] = attention_mask 

3157 if initial_attention_mask is not None and ( 

3158 _generate_from_embeds 

3159 or getattr(getattr(self.original_model, "config", None), "is_encoder_decoder", False) 

3160 ): 

3161 # Growing the mask per step only means something for decoder-only token 

3162 # generation. On these paths the mask used to arrive via 

3163 # **multimodal_kwargs and be forwarded to the model untouched — as 

3164 # processors emit it alongside pixel_values — so keep doing that rather 

3165 # than reject a call that worked before this parameter existed. 

3166 multimodal_kwargs = {**multimodal_kwargs, "attention_mask": initial_attention_mask} 

3167 initial_attention_mask = None 

3168 if initial_attention_mask is not None: 

3169 if initial_attention_mask.shape != input_tokens.shape: 

3170 raise ValueError( 

3171 f"attention_mask shape {tuple(initial_attention_mask.shape)} does not " 

3172 f"match the prompt shape {tuple(input_tokens.shape)}. Pass a 0/1 mask " 

3173 "covering exactly the prompt tokens; generate() extends it itself." 

3174 ) 

3175 initial_attention_mask = initial_attention_mask.to(self.cfg.device) 

3176 elif padding_side is not None and input_type == "tokens": 

3177 # Reading the padding off the tokens needs a tokenizer with a pad id. 

3178 # Without one the argument would be inert, leaving exactly the bug this 

3179 # fixes — silently, on a bridge booted without a tokenizer. Say so 

3180 # rather than generate something quietly wrong. 

3181 if not isinstance(self.tokenizer, PreTrainedTokenizerBase): 

3182 raise ValueError( 

3183 "generate(padding_side=...) reads the padding off the pad token, " 

3184 "which needs a tokenizer; this bridge has none. Pass " 

3185 "attention_mask=... to state the padding directly instead." 

3186 ) 

3187 if self.tokenizer.pad_token_id is None: 

3188 raise ValueError( 

3189 "generate(padding_side=...) reads the padding off the pad token, " 

3190 "but this tokenizer has no pad_token_id. Set one, or pass " 

3191 "attention_mask=... to state the padding directly instead." 

3192 ) 

3193 _prepend = self.cfg.default_prepend_bos if prepend_bos is None else prepend_bos 

3194 _orig_side = self.tokenizer.padding_side 

3195 self.tokenizer.padding_side = padding_side 

3196 try: 

3197 initial_attention_mask = utils.get_attention_mask( 

3198 self.tokenizer, input_tokens, _prepend 

3199 ).to(self.cfg.device) 

3200 finally: 

3201 self.tokenizer.padding_side = _orig_side 

3202 # An all-ones mask is what the model assumes anyway; skipping it keeps 

3203 # the unpadded path byte-identical to before. 

3204 if initial_attention_mask is not None and bool(initial_attention_mask.all()): 3204 ↛ 3205line 3204 didn't jump to line 3205 because the condition on line 3204 was never true

3205 initial_attention_mask = None 

3206 

3207 # Determine return type 

3208 if return_type == "input": 

3209 if input_type in ["str", "list"]: 

3210 return_type = "str" 

3211 elif input_type == "embeds": 

3212 return_type = "tokens" 

3213 else: 

3214 return_type = "tokens" 

3215 

3216 batch_size = input_tokens.shape[0] 

3217 

3218 # Setup EOS token handling 

3219 stop_tokens = [] 

3220 eos_token_for_padding = 0 

3221 if stop_at_eos: 

3222 tokenizer_has_eos_token = ( 

3223 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

3224 ) 

3225 if eos_token_id is None: 

3226 # Some chat models use a turn-end token that differs from the 

3227 # tokenizer's primary EOS. Let adapters provide the full stop 

3228 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

3229 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

3230 if eos_token_id is None: 

3231 assert ( 

3232 tokenizer_has_eos_token 

3233 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

3234 assert self.tokenizer is not None 

3235 eos_token_id = self.tokenizer.eos_token_id 

3236 

3237 if isinstance(eos_token_id, int): 

3238 stop_tokens = [eos_token_id] 

3239 eos_token_for_padding = eos_token_id 

3240 else: 

3241 stop_tokens = list(eos_token_id) 

3242 if tokenizer_has_eos_token: 

3243 assert self.tokenizer is not None 

3244 eos_token_for_padding = self.tokenizer.eos_token_id 

3245 else: 

3246 eos_token_for_padding = eos_token_id[0] 

3247 

3248 # Track which sequences have finished 

3249 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

3250 

3251 # Optionally collect logits at each generation step for downstream tooling/tests 

3252 logits_seq_list: list[torch.Tensor] | None = [] if output_logits else None 

3253 

3254 # Detect encoder-decoder models (T5, BART, etc.) 

3255 is_encoder_decoder = hasattr(self.original_model, "config") and getattr( 

3256 self.original_model.config, "is_encoder_decoder", False 

3257 ) 

3258 if forced_bos_token_id is not None and not is_encoder_decoder: 3258 ↛ 3261line 3258 didn't jump to line 3261 because the condition on line 3258 was never true

3259 # Raise before any state mutation (_capture_hf_cache) and before 

3260 # the stateful hf_generate early-return would drop the kwarg. 

3261 raise ValueError("forced_bos_token_id is only meaningful for encoder-decoder models") 

3262 

3263 # return_cache recomputes run_with_cache on the generated output (see issue #697). 

3264 # That is well-defined only for single-sequence, decoder-only text generation, so 

3265 # reject the paths whose cache would be wrong/undefined, with a clear pointer to the 

3266 # run_with_cache workaround. Fail fast here, before any generation work. 

3267 if return_cache: 

3268 if is_encoder_decoder: 3268 ↛ 3269line 3268 didn't jump to line 3269 because the condition on line 3268 was never true

3269 raise NotImplementedError( 

3270 "generate(return_cache=True) is not supported for encoder-decoder " 

3271 "models yet. Run run_with_cache on the generated output instead." 

3272 ) 

3273 if is_stateful_model: 3273 ↛ 3274line 3273 didn't jump to line 3274 because the condition on line 3273 was never true

3274 raise NotImplementedError( 

3275 "generate(return_cache=True) is not supported for stateful/SSM models " 

3276 "(e.g. Mamba); they do not expose standard transformer hook points." 

3277 ) 

3278 if pixel_values is not None or multimodal_kwargs: 3278 ↛ 3279line 3278 didn't jump to line 3279 because the condition on line 3278 was never true

3279 raise NotImplementedError( 

3280 "generate(return_cache=True) is not supported for multimodal generation " 

3281 "yet. Run run_with_cache on the generated output instead." 

3282 ) 

3283 if _generate_from_embeds: 

3284 raise NotImplementedError( 

3285 "generate(return_cache=True) requires token input, not inputs_embeds." 

3286 ) 

3287 if batch_size > 1: 

3288 raise NotImplementedError( 

3289 "generate(return_cache=True) is not supported for batched/multi-prompt " 

3290 "generation yet. Pass a single prompt, or run run_with_cache on each " 

3291 "output sequence." 

3292 ) 

3293 

3294 # HF cache flows opaquely through the component chain via 

3295 # _reconstruct_attention() → _update_kv_cache() on each layer. 

3296 _hf_kv_cache = None 

3297 if use_past_kv_cache and is_encoder_decoder: 

3298 # Encoder-decoder models (T5, BART) don't support the opaque 

3299 # cache path — silently disable rather than crash, since 

3300 # use_past_kv_cache=True is the default. 

3301 use_past_kv_cache = False 

3302 

3303 # SSMs (Mamba/Mamba-2) run through a dedicated cache path so hooks 

3304 # fire on every step. Unsupported input types fall back to hf_generate(). 

3305 use_stateful_cache = ( 

3306 is_stateful_model 

3307 and use_past_kv_cache 

3308 and not is_encoder_decoder 

3309 and not _generate_from_embeds 

3310 and pixel_values is None 

3311 and not multimodal_kwargs 

3312 ) 

3313 

3314 # stop_strings / stopping_criteria are applied inside the hooked _generate_tokens 

3315 # loop, so they are supported only on the standard decoder-only text path. Reject 

3316 # the paths that route around that loop with a clear error rather than silently 

3317 # dropping the kwargs. This must run before the stateful delegation below. 

3318 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

3319 if stopping_criteria_list is not None: 

3320 if is_encoder_decoder: 

3321 _unsupported = "encoder-decoder models" 

3322 elif _generate_from_embeds: 

3323 _unsupported = "inputs_embeds generation" 

3324 elif pixel_values is not None or multimodal_kwargs: 

3325 _unsupported = "multimodal (pixel_values) generation" 

3326 else: 

3327 _unsupported = None 

3328 if _unsupported is not None: 

3329 raise NotImplementedError( 

3330 f"stop_strings/stopping_criteria are not supported for {_unsupported} in " 

3331 "TransformerBridge.generate(). Call hf_generate(...), which runs " 

3332 "HuggingFace's own generation loop and supports HF-native stopping on " 

3333 "those inputs." 

3334 ) 

3335 if is_stateful_model and not use_stateful_cache: 

3336 # Reached only for a stateful/SSM model with use_past_kv_cache=False: the 

3337 # hooked loop needs the stateful cache, so generate() would otherwise fall 

3338 # back to hf_generate() and drop these kwargs. The default cache setting 

3339 # keeps generation on the hooked loop, where stopping is applied. 

3340 raise NotImplementedError( 

3341 "stop_strings/stopping_criteria on a stateful/SSM model require the " 

3342 "stateful cache path, which runs only with use_past_kv_cache=True (the " 

3343 "default). With use_past_kv_cache=False generate() falls back to " 

3344 "hf_generate(). Set use_past_kv_cache=True to keep stopping on the hooked " 

3345 "loop, or call hf_generate(...) directly for HF-native stopping." 

3346 ) 

3347 # Finished rows are overwritten with this id so they stop emitting real tokens 

3348 # while the rest of a batch keeps going. stop_at_eos already set a sensible 

3349 # value, otherwise fall back to the tokenizer pad/eos id. (For a single 

3350 # sequence this id is never read: the loop exits when the row finishes.) 

3351 if not stop_at_eos: 

3352 _pad_id = None 

3353 if self.tokenizer is not None: 

3354 _pad_id = ( 

3355 self.tokenizer.pad_token_id 

3356 if self.tokenizer.pad_token_id is not None 

3357 else self.tokenizer.eos_token_id 

3358 ) 

3359 if _pad_id is not None: 

3360 eos_token_for_padding = _pad_id 

3361 elif batch_size > 1: 

3362 raise ValueError( 

3363 "Batched generation with stopping_criteria and stop_at_eos=False " 

3364 "needs a padding token to freeze finished rows, but no tokenizer " 

3365 "pad/eos id is available. Set stop_at_eos=True, use a tokenizer with " 

3366 "a pad or eos token, or generate one sequence at a time." 

3367 ) 

3368 

3369 if is_stateful_model and not use_stateful_cache: 

3370 hf_kwargs: dict[str, Any] = { 

3371 "max_new_tokens": max_new_tokens, 

3372 "do_sample": do_sample, 

3373 "temperature": temperature, 

3374 # hf_generate defaults this to True, so omitting it would make 

3375 # generate(stop_at_eos=False) silently stop at EOS on this path. 

3376 "stop_at_eos": stop_at_eos, 

3377 } 

3378 if top_k is not None: 3378 ↛ 3379line 3378 didn't jump to line 3379 because the condition on line 3378 was never true

3379 hf_kwargs["top_k"] = top_k 

3380 if top_p is not None: 3380 ↛ 3381line 3380 didn't jump to line 3381 because the condition on line 3380 was never true

3381 hf_kwargs["top_p"] = top_p 

3382 if eos_token_id is not None: 

3383 hf_kwargs["eos_token_id"] = eos_token_id 

3384 # generate() already derived and shape-validated this mask; dropping it here 

3385 # would let the model attend to pad positions on the stateful fallback. 

3386 if initial_attention_mask is not None: 

3387 hf_kwargs["attention_mask"] = initial_attention_mask 

3388 return self.hf_generate(input, **hf_kwargs) 

3389 

3390 # SSM cache is built once and mutated in place across forward calls. 

3391 # Adapter owns the cache-type choice; new SSMs just override 

3392 # create_stateful_cache(). 

3393 mamba_cache: Any = None 

3394 mamba_conv_kernel: int = 0 

3395 if use_stateful_cache: 

3396 hf_model: Any = self.original_model 

3397 mamba_conv_kernel = int(getattr(hf_model.config, "conv_kernel", 4)) 

3398 cache_dtype = self.cfg.dtype or torch.float32 

3399 mamba_cache = self.adapter.create_stateful_cache( 

3400 hf_model=hf_model, 

3401 batch_size=batch_size, 

3402 device=self.cfg.device, 

3403 dtype=cache_dtype, 

3404 ) 

3405 

3406 if use_past_kv_cache and not use_stateful_cache: 

3407 self._capture_hf_cache = True # Signal forward() to stash cache 

3408 

3409 # Generate tokens 

3410 current_tokens = input_tokens.clone() 

3411 # For inputs_embeds generation, also track generated token IDs for decoding 

3412 if _generate_from_embeds: 

3413 generated_token_ids: list[torch.Tensor] = [] 

3414 sampled_tokens_list = [] 

3415 

3416 # For encoder-decoder models, keep encoder input fixed and grow decoder input 

3417 min_decoder_length = None 

3418 if is_encoder_decoder: 

3419 encoder_input = input_tokens.clone() 

3420 decoder_tokens, min_decoder_length = self._encoder_decoder_setup( 

3421 input_tokens, forced_bos_token_id 

3422 ) 

3423 

3424 try: 

3425 for sampled_tokens, final_logits, all_finished in self._generate_tokens( 

3426 current_tokens, 

3427 input_tokens, 

3428 batch_size, 

3429 max_new_tokens=max_new_tokens, 

3430 do_sample=do_sample, 

3431 top_k=top_k, 

3432 top_p=top_p, 

3433 temperature=temperature, 

3434 freq_penalty=freq_penalty, 

3435 repetition_penalty=repetition_penalty, 

3436 stop_at_eos=stop_at_eos, 

3437 stop_tokens=stop_tokens, 

3438 eos_token_for_padding=eos_token_for_padding, 

3439 finished_sequences=finished_sequences, 

3440 use_past_kv_cache=use_past_kv_cache, 

3441 use_stateful_cache=use_stateful_cache, 

3442 mamba_cache=mamba_cache, 

3443 mamba_conv_kernel=mamba_conv_kernel, 

3444 is_encoder_decoder=is_encoder_decoder, 

3445 _is_batched_list=_is_batched_list, 

3446 _generate_from_embeds=_generate_from_embeds, 

3447 encoder_input=encoder_input if is_encoder_decoder else None, 

3448 decoder_tokens=decoder_tokens if is_encoder_decoder else None, 

3449 generated_token_ids=generated_token_ids if _generate_from_embeds else None, 

3450 pixel_values=pixel_values, 

3451 multimodal_kwargs=multimodal_kwargs if multimodal_kwargs else {}, 

3452 verbose=verbose, 

3453 stopping_criteria_list=stopping_criteria_list, 

3454 initial_attention_mask=initial_attention_mask, 

3455 min_decoder_length=min_decoder_length, 

3456 ngram_processor=(self._encdec_ngram_processor() if is_encoder_decoder else None), 

3457 encoder_attention_mask=(attention_mask if is_encoder_decoder else None), 

3458 forced_eos_token_id=( 

3459 self._encdec_forced_eos_token_id() if is_encoder_decoder else None 

3460 ), 

3461 ): 

3462 sampled_tokens_list.append(sampled_tokens.unsqueeze(1)) 

3463 if logits_seq_list is not None: 

3464 logits_seq_list.append(final_logits.clone()) 

3465 if all_finished: 

3466 break 

3467 finally: 

3468 self._capture_hf_cache = False 

3469 if hasattr(self, "_last_hf_cache"): 3469 ↛ 3470line 3469 didn't jump to line 3470 because the condition on line 3469 was never true

3470 del self._last_hf_cache 

3471 

3472 sampled_tokens = torch.cat(sampled_tokens_list, dim=1) 

3473 if is_encoder_decoder: 

3474 # Reconstruct full decoder sequence: start token + generated tokens 

3475 output_tokens = torch.cat([decoder_tokens, sampled_tokens], dim=1) 

3476 elif _generate_from_embeds: 

3477 # For inputs_embeds, we only have the generated token IDs (no input token IDs) 

3478 output_tokens = sampled_tokens 

3479 else: 

3480 output_tokens = torch.cat([input_tokens, sampled_tokens], dim=1) 

3481 

3482 # Build the formatted output (shape unchanged: ModelOutput / str / list[str] / tokens). 

3483 result: Any 

3484 if output_logits and logits_seq_list is not None: 

3485 from transformers.utils import ModelOutput # type: ignore 

3486 

3487 def _logits_to_tuple(logits_list: list[torch.Tensor]) -> tuple[torch.Tensor, ...]: 

3488 assert logits_list is not None 

3489 # Convert list of [batch, vocab] tensors to tuple 

3490 return tuple(logits_list) 

3491 

3492 try: 

3493 from transformers.generation.utils import GenerateDecoderOnlyOutput 

3494 

3495 # HF-compatible ModelOutput structure. 

3496 # GenerateDecoderOnlyOutput expects: sequences, scores (optional), logits (optional) 

3497 result = GenerateDecoderOnlyOutput( 

3498 sequences=cast(torch.LongTensor, output_tokens), 

3499 # HF's type hint says tuple[FloatTensor] but should be tuple[FloatTensor, ...] 

3500 # (variable-length tuple with one element per generated token) 

3501 logits=_logits_to_tuple(logits_seq_list), # type: ignore[arg-type] 

3502 ) 

3503 except (ImportError, AttributeError): 

3504 # Fallback if GenerateDecoderOnlyOutput not available in this transformers version 

3505 result = ModelOutput( 

3506 sequences=output_tokens, 

3507 logits=_logits_to_tuple(logits_seq_list), 

3508 ) 

3509 elif return_type == "str": 

3510 assert self.tokenizer is not None 

3511 if input_type == "str": 

3512 result = self.tokenizer.decode(output_tokens[0], skip_special_tokens=True) 

3513 else: 

3514 decoded_texts = [ 

3515 self.tokenizer.decode(tokens, skip_special_tokens=True) 

3516 for tokens in output_tokens 

3517 ] 

3518 result = decoded_texts[0] if len(decoded_texts) == 1 else decoded_texts 

3519 else: # return_type == "tokens" 

3520 result = output_tokens 

3521 

3522 if not return_cache and not return_input_tokens: 

3523 return result 

3524 

3525 if return_cache: 

3526 # return_cache: recompute one clean forward over the full generated sequence so the 

3527 # cache is identical to run_with_cache(output_tokens) - all hook points, including 

3528 # attention patterns. The guards above restrict this to single-sequence, decoder-only 

3529 # text generation (see issue #697). 

3530 _, cache = self.run_with_cache(output_tokens, names_filter=names_filter, device=device) 

3531 if return_input_tokens: 

3532 return result, cache, input_tokens 

3533 return result, cache 

3534 

3535 # return_input_tokens only (no cache) 

3536 return result, input_tokens 

3537 

3538 @torch.no_grad() 

3539 def diffusion_generate( 

3540 self, 

3541 input: Union[str, List[str], torch.Tensor], 

3542 max_new_tokens: int = 32, 

3543 prepend_bos: Optional[bool] = None, 

3544 **kwargs: Any, 

3545 ) -> Union[str, torch.Tensor]: 

3546 """Sample from a non-autoregressive (diffusion) architecture. 

3547 

3548 Delegates to the model's own sampler, which calls the model through 

3549 ``__call__`` so bridge hooks fire on every denoising step. 

3550 """ 

3551 sampler_name = getattr(self.adapter, "native_sampler", None) 

3552 architecture = self.cfg.architecture or type(self.adapter).__name__ 

3553 if sampler_name is None: 3553 ↛ 3554line 3553 didn't jump to line 3554 because the condition on line 3553 was never true

3554 raise NotImplementedError( 

3555 f"{architecture} has no native sampler; use generate() for autoregressive " 

3556 "architectures." 

3557 ) 

3558 sampler = getattr(self.original_model, sampler_name, None) 

3559 if sampler is None: 3559 ↛ 3560line 3559 didn't jump to line 3560 because the condition on line 3559 was never true

3560 raise NotImplementedError( 

3561 f"{architecture} declares native_sampler={sampler_name!r} but the loaded model " 

3562 "has no such method." 

3563 ) 

3564 

3565 was_string = isinstance(input, str) 

3566 if isinstance(input, list) and len(input) > 1: 3566 ↛ 3570line 3566 didn't jump to line 3570 because the condition on line 3566 was never true

3567 # Unequal prompts would be right-padded into the sampler's canvas, 

3568 # where pad tokens read as real context. generate() gates batching 

3569 # for the same reason; do not silently corrupt rows here. 

3570 raise NotImplementedError( 

3571 f"diffusion_generate() does not support batched prompts for {architecture}: " 

3572 "the native samplers condition on a padded canvas. Sample one prompt at a time." 

3573 ) 

3574 if isinstance(input, torch.Tensor): 3574 ↛ 3579line 3574 didn't jump to line 3579 because the condition on line 3574 was always true

3575 tokens = input.to(self.cfg.device) 

3576 else: 

3577 # Tokenization is the bridge's concern, not the sampler's — absorb 

3578 # prepend_bos here rather than forwarding it into native kwargs. 

3579 tokens = self.to_tokens( 

3580 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3581 ) 

3582 

3583 sampler_kwargs = self.adapter.native_sampler_kwargs(max_new_tokens, tokens.shape[-1]) 

3584 sampler_kwargs.update(kwargs) 

3585 output = sampler(tokens, **sampler_kwargs) 

3586 # Samplers return either bare ids or a generation output object. 

3587 sequences = getattr(output, "sequences", output) 

3588 # They also disagree on whether the prompt is included: Dream and Gidd 

3589 # return the whole canvas, LLaDA2 slices it off. Normalize to 

3590 # generate()'s contract (prompt + continuation). 

3591 if isinstance(sequences, torch.Tensor) and sequences.ndim == tokens.ndim: 3591 ↛ 3602line 3591 didn't jump to line 3602 because the condition on line 3591 was always true

3592 prompt_len = tokens.shape[-1] 

3593 # Compare on one device: torch.equal raises on a device mismatch, 

3594 # which some samplers produce by assembling output on CPU. 

3595 sequences = sequences.to(tokens.device) 

3596 includes_prompt = sequences.shape[-1] >= prompt_len and torch.equal( 

3597 sequences[..., :prompt_len], tokens 

3598 ) 

3599 if not includes_prompt: 

3600 sequences = torch.cat([tokens, sequences], dim=-1) 

3601 

3602 if was_string and self.tokenizer is not None: 3602 ↛ 3603line 3602 didn't jump to line 3603 because the condition on line 3602 was never true

3603 return self.tokenizer.decode(sequences[0], skip_special_tokens=True) 

3604 return sequences 

3605 

3606 @torch.no_grad() 

3607 def generate_stream( 

3608 self, 

3609 input: Union[str, List[str], torch.Tensor] = "", 

3610 max_new_tokens: int = 10, 

3611 max_tokens_per_yield: int = 25, 

3612 stop_at_eos: bool = True, 

3613 eos_token_id: Optional[int] = None, 

3614 do_sample: bool = True, 

3615 top_k: Optional[int] = None, 

3616 top_p: Optional[float] = None, 

3617 temperature: float = 1.0, 

3618 freq_penalty: float = 0.0, 

3619 repetition_penalty: float = 1.0, 

3620 use_past_kv_cache: bool = True, 

3621 prepend_bos: Optional[bool] = None, 

3622 padding_side: Optional[str] = None, 

3623 return_type: Optional[str] = "input", 

3624 verbose: bool = True, 

3625 stop_strings: Optional[Union[str, List[str]]] = None, 

3626 stopping_criteria: Optional[Any] = None, 

3627 attention_mask: Optional[torch.Tensor] = None, 

3628 forced_bos_token_id: Optional[int] = None, 

3629 ) -> Generator[Union[torch.Tensor, str, List[str]], None, None]: 

3630 """Stream tokens from the model as they are generated. 

3631 

3632 Yields batches of tokens progressively during generation rather than 

3633 waiting for the entire sequence. Uses the same core loop as generate(). 

3634 

3635 Args: 

3636 input: Text string, list of strings, or tensor of tokens. 

3637 max_new_tokens: Maximum number of tokens to generate. 

3638 max_tokens_per_yield: Yield accumulated tokens every this many steps. 

3639 stop_at_eos: If True, stop when eos_token is produced. 

3640 eos_token_id: Token ID(s) for end of sentence. Defaults to tokenizer's. 

3641 do_sample: If True, sample; otherwise greedy. 

3642 top_k: Top-k sampling. None means no filtering. 

3643 top_p: Nucleus sampling threshold. 

3644 temperature: Sampling temperature. 

3645 freq_penalty: Frequency penalty for previous tokens. 

3646 repetition_penalty: HF-style repetition penalty (>1.0 discourages repeats). 

3647 use_past_kv_cache: Use KV caching for faster generation. 

3648 prepend_bos: Whether to prepend a BOS token when tokenizing string inputs. 

3649 Defaults to None (uses ``cfg.default_prepend_bos``, typically True). 

3650 Pass ``prepend_bos=False`` when the input is pre-formatted chat-template 

3651 text that already contains the BOS token to avoid double-BOS. 

3652 Ignored when input is already a token tensor. 

3653 padding_side: Which side to pad for batched list inputs. Left-padding 

3654 is forced internally for batched generation. 

3655 return_type: 'input' (match input type), 'str', or 'tokens'. 

3656 verbose: Show progress bar. 

3657 stop_strings: Optional string or list of strings. A sequence stops once its 

3658 generated text ends with one of them (HF StopStringCriteria). Requires a 

3659 tokenizer. See generate() for details. 

3660 stopping_criteria: Optional transformers StoppingCriteria, list, or 

3661 StoppingCriteriaList, called as criterion(input_ids, scores) each step 

3662 (scores is the step's logits). See generate() for the full contract. 

3663 attention_mask: Optional ``[batch, pos]`` 0/1 mask over the prompt, 

3664 required to generate correctly from an already-padded token tensor. 

3665 Extended by one attended column per generated token on decoder-only 

3666 models; forwarded to the encoder as-is on encoder-decoder models. 

3667 See generate() for the full contract. 

3668 forced_bos_token_id: Optional token id seeded as the first decoder token 

3669 after ``decoder_start`` on encoder-decoder models, overriding any 

3670 generation_config default. Raises ValueError on decoder-only models. 

3671 

3672 Yields: 

3673 Token tensors [batch, seq_len], or decoded text when return_type='str' - 

3674 a bare string for a single sequence and one string per batch row for a 

3675 larger batch, matching generate(). Chunks accumulate up to 

3676 max_tokens_per_yield tokens between yields; the first yield includes the 

3677 input tokens and subsequent yields contain only new tokens. For encoder-decoder 

3678 models, the first yield includes the decoder start token (and configured 

3679 forced BOS token), not the encoder input. Encoder-decoder streaming uses 

3680 the uncached loop, as generate() does; custom stopping criteria are unsupported. 

3681 """ 

3682 self._ensure_generation_supported("generate_stream") 

3683 # --- Input parsing (mirrors generate()) --- 

3684 _is_batched_list = isinstance(input, list) and len(input) > 1 

3685 use_past_kv_cache = self._resolve_generation_caching(use_past_kv_cache, _is_batched_list) 

3686 

3687 _encdec_early = hasattr(self.original_model, "config") and getattr( 

3688 self.original_model.config, "is_encoder_decoder", False 

3689 ) 

3690 encoder_attention_mask = None 

3691 if _encdec_early: 

3692 use_past_kv_cache = False 

3693 if isinstance(input, str): 

3694 if _encdec_early: 

3695 # Native recipe: to_tokens' BOS policy corrupts encoder inputs. 

3696 encoded = self.tokenizer(input, return_tensors="pt") 

3697 input_tokens = encoded["input_ids"].to(self.cfg.device) 

3698 encoder_attention_mask = encoded.get("attention_mask") 

3699 else: 

3700 input_tokens = self.to_tokens( 

3701 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3702 ) 

3703 input_type = "str" 

3704 elif isinstance(input, list): 

3705 if _encdec_early: 

3706 encoded = self.tokenizer(input, return_tensors="pt", padding=True) 

3707 input_tokens = encoded["input_ids"].to(self.cfg.device) 

3708 encoder_attention_mask = encoded.get("attention_mask") 

3709 elif _is_batched_list: 

3710 _orig_ps = self.tokenizer.padding_side 

3711 self.tokenizer.padding_side = "left" 

3712 try: 

3713 input_tokens = self.to_tokens( 

3714 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3715 ) 

3716 finally: 

3717 self.tokenizer.padding_side = _orig_ps 

3718 else: 

3719 input_tokens = self.to_tokens( 

3720 input, prepend_bos=prepend_bos, move_to_device=True, truncate=False 

3721 ) 

3722 input_type = "list" 

3723 else: 

3724 input_tokens = input.to(self.cfg.device) 

3725 input_type = "tokens" 

3726 

3727 if return_type == "input": 

3728 return_type = "str" if input_type in ["str", "list"] else "tokens" 

3729 

3730 if forced_bos_token_id is not None and not _encdec_early: 

3731 raise ValueError("forced_bos_token_id is only meaningful for encoder-decoder models") 

3732 

3733 # An explicit mask wins over any tokenizer-derived one (mirrors generate()): 

3734 # the encoder consumes it as-is, a decoder-only prompt mask is grown per step. 

3735 initial_attention_mask: Optional[torch.Tensor] = None 

3736 if attention_mask is not None: 

3737 if _encdec_early: 

3738 encoder_attention_mask = attention_mask 

3739 else: 

3740 if attention_mask.shape != input_tokens.shape: 

3741 raise ValueError( 

3742 f"attention_mask shape {tuple(attention_mask.shape)} does not " 

3743 f"match the prompt shape {tuple(input_tokens.shape)}. Pass a 0/1 " 

3744 "mask covering exactly the prompt tokens; generate_stream() " 

3745 "extends it itself." 

3746 ) 

3747 initial_attention_mask = attention_mask.to(self.cfg.device) 

3748 

3749 batch_size = input_tokens.shape[0] 

3750 

3751 # --- EOS setup --- 

3752 stop_tokens: List[int] = [] 

3753 eos_token_for_padding = 0 

3754 if stop_at_eos: 

3755 tokenizer_has_eos_token = ( 

3756 self.tokenizer is not None and self.tokenizer.eos_token_id is not None 

3757 ) 

3758 if eos_token_id is None: 

3759 # Some chat models use a turn-end token that differs from the 

3760 # tokenizer's primary EOS. Let adapters provide the full stop 

3761 # set via cfg.eos_token_id; otherwise fall back to the tokenizer. 

3762 eos_token_id = getattr(self.cfg, "eos_token_id", None) 

3763 if eos_token_id is None: 

3764 assert ( 

3765 tokenizer_has_eos_token 

3766 ), "Must pass eos_token_id if stop_at_eos is True and tokenizer is None or has no eos_token_id" 

3767 assert self.tokenizer is not None 

3768 eos_token_id = self.tokenizer.eos_token_id 

3769 if isinstance(eos_token_id, int): 

3770 stop_tokens = [eos_token_id] 

3771 eos_token_for_padding = eos_token_id 

3772 else: 

3773 stop_tokens = list(eos_token_id) 

3774 if tokenizer_has_eos_token: 3774 ↛ 3775line 3774 didn't jump to line 3775 because the condition on line 3774 was never true

3775 assert self.tokenizer is not None 

3776 eos_token_for_padding = self.tokenizer.eos_token_id 

3777 else: 

3778 eos_token_for_padding = eos_token_id[0] 

3779 

3780 finished_sequences = torch.zeros(batch_size, dtype=torch.bool, device=self.cfg.device) 

3781 

3782 stopping_criteria_list = self._resolve_stopping_criteria(stop_strings, stopping_criteria) 

3783 if _encdec_early and stopping_criteria_list is not None: 

3784 raise NotImplementedError( 

3785 "stop_strings/stopping_criteria are not supported for encoder-decoder " 

3786 "generation in TransformerBridge.generate_stream(). Call hf_generate(...) " 

3787 "for HF-native stopping on those inputs." 

3788 ) 

3789 if stopping_criteria_list is not None and not stop_at_eos: 

3790 _pad_id = None 

3791 if self.tokenizer is not None: 

3792 _pad_id = ( 

3793 self.tokenizer.pad_token_id 

3794 if self.tokenizer.pad_token_id is not None 

3795 else self.tokenizer.eos_token_id 

3796 ) 

3797 if _pad_id is not None: 

3798 eos_token_for_padding = _pad_id 

3799 elif batch_size > 1: 3799 ↛ 3808line 3799 didn't jump to line 3808 because the condition on line 3799 was always true

3800 raise ValueError( 

3801 "Batched generate_stream with stopping_criteria and stop_at_eos=False " 

3802 "needs a padding token to freeze finished rows, but no tokenizer pad/eos " 

3803 "id is available. Set stop_at_eos=True or use a tokenizer with a pad/eos " 

3804 "token." 

3805 ) 

3806 

3807 # --- Cache setup --- 

3808 if use_past_kv_cache: 

3809 self._capture_hf_cache = True 

3810 

3811 current_tokens = input_tokens.clone() 

3812 encoder_input = input_tokens if _encdec_early else None 

3813 decoder_tokens = None 

3814 stream_prefix = input_tokens 

3815 min_decoder_length = None 

3816 if _encdec_early: 

3817 decoder_tokens, min_decoder_length = self._encoder_decoder_setup( 

3818 input_tokens, forced_bos_token_id 

3819 ) 

3820 stream_prefix = decoder_tokens 

3821 

3822 # --- Streaming loop --- 

3823 # All yields are token tensors [batch, seq_len]. Each yield contains 

3824 # only the newly generated tokens since the previous yield (the first 

3825 # yield additionally prepends the input tokens for context). 

3826 accumulated_tokens: Optional[torch.Tensor] = None 

3827 tokens_since_last_yield = 0 

3828 

3829 # Decoding each chunk alone splits any character whose bytes straddle a yield 

3830 # boundary into U+FFFD, and concatenating chunks cannot rebuild it. Decode the 

3831 # whole stream each time, emit only the new text, and hold back a trailing 

3832 # incomplete character until the tokens that finish it arrive. 

3833 decoded_history: Optional[torch.Tensor] = None 

3834 emitted_chars = [0] * batch_size 

3835 

3836 def _decode_delta(hold_incomplete: bool) -> List[str]: 

3837 assert self.tokenizer is not None and decoded_history is not None 

3838 deltas = [] 

3839 for row_idx, row in enumerate(decoded_history): 

3840 text = self.tokenizer.decode(row, skip_special_tokens=True) 

3841 if hold_incomplete: 

3842 text = text.rstrip("�") 

3843 deltas.append(text[emitted_chars[row_idx] :]) 

3844 emitted_chars[row_idx] = len(text) 

3845 return deltas 

3846 

3847 def _maybe_decode(tokens: torch.Tensor) -> Union[torch.Tensor, str, List[str]]: 

3848 nonlocal decoded_history 

3849 if return_type != "str": 

3850 return tokens 

3851 decoded_history = ( 

3852 tokens if decoded_history is None else torch.cat([decoded_history, tokens], dim=-1) 

3853 ) 

3854 deltas = _decode_delta(hold_incomplete=True) 

3855 return deltas[0] if len(deltas) == 1 else deltas 

3856 

3857 def _flush_held() -> Optional[Union[str, List[str]]]: 

3858 """Emit a withheld partial character at stream end rather than dropping it.""" 

3859 if return_type != "str" or decoded_history is None: 

3860 return None 

3861 deltas = _decode_delta(hold_incomplete=False) 

3862 if not any(deltas): 

3863 return None 

3864 return deltas[0] if len(deltas) == 1 else deltas 

3865 

3866 try: 

3867 for step_idx, (sampled_tokens, _, all_finished) in enumerate( 

3868 self._generate_tokens( 

3869 current_tokens, 

3870 input_tokens, 

3871 batch_size, 

3872 max_new_tokens=max_new_tokens, 

3873 do_sample=do_sample, 

3874 top_k=top_k, 

3875 top_p=top_p, 

3876 temperature=temperature, 

3877 freq_penalty=freq_penalty, 

3878 repetition_penalty=repetition_penalty, 

3879 stop_at_eos=stop_at_eos, 

3880 stop_tokens=stop_tokens, 

3881 eos_token_for_padding=eos_token_for_padding, 

3882 finished_sequences=finished_sequences, 

3883 use_past_kv_cache=use_past_kv_cache, 

3884 use_stateful_cache=False, 

3885 mamba_cache=None, 

3886 mamba_conv_kernel=0, 

3887 is_encoder_decoder=_encdec_early, 

3888 _is_batched_list=_is_batched_list, 

3889 _generate_from_embeds=False, 

3890 encoder_input=encoder_input, 

3891 decoder_tokens=decoder_tokens, 

3892 generated_token_ids=None, 

3893 pixel_values=None, 

3894 multimodal_kwargs={}, 

3895 verbose=verbose, 

3896 stopping_criteria_list=stopping_criteria_list, 

3897 initial_attention_mask=initial_attention_mask, 

3898 min_decoder_length=min_decoder_length, 

3899 ngram_processor=(self._encdec_ngram_processor() if _encdec_early else None), 

3900 encoder_attention_mask=encoder_attention_mask, 

3901 forced_eos_token_id=( 

3902 self._encdec_forced_eos_token_id() if _encdec_early else None 

3903 ), 

3904 ) 

3905 ): 

3906 new_tokens = sampled_tokens.unsqueeze(-1) 

3907 

3908 if step_idx == 0: 

3909 accumulated_tokens = torch.cat([stream_prefix, new_tokens], dim=-1) 

3910 tokens_since_last_yield = accumulated_tokens.shape[1] 

3911 else: 

3912 if accumulated_tokens is None: 

3913 accumulated_tokens = new_tokens 

3914 else: 

3915 accumulated_tokens = torch.cat([accumulated_tokens, new_tokens], dim=-1) 

3916 tokens_since_last_yield += 1 

3917 

3918 if tokens_since_last_yield >= max_tokens_per_yield: 

3919 yield _maybe_decode(accumulated_tokens) 

3920 tokens_since_last_yield = 0 

3921 accumulated_tokens = None 

3922 

3923 if all_finished: 

3924 if accumulated_tokens is not None: 

3925 yield _maybe_decode(accumulated_tokens) 

3926 accumulated_tokens = None 

3927 break 

3928 

3929 # Yield remainder after loop completes without break 

3930 if accumulated_tokens is not None: 

3931 yield _maybe_decode(accumulated_tokens) 

3932 held_back = _flush_held() 

3933 if held_back is not None: 

3934 yield held_back 

3935 finally: 

3936 self._capture_hf_cache = False 

3937 if hasattr(self, "_last_hf_cache"): 3937 ↛ 3938line 3937 didn't jump to line 3938 because the condition on line 3937 was never true

3938 del self._last_hf_cache 

3939 

3940 def hf_generate( 

3941 self, 

3942 input: str | list[str] | torch.Tensor = "", 

3943 max_new_tokens: int = 10, 

3944 stop_at_eos: bool = True, 

3945 eos_token_id: int | Sequence[int] | None = None, 

3946 do_sample: bool = True, 

3947 top_k: int | None = None, 

3948 top_p: float | None = None, 

3949 temperature: float = 1.0, 

3950 use_past_kv_cache: bool = True, 

3951 return_type: str | None = "input", 

3952 pixel_values: torch.Tensor | None = None, 

3953 **generation_kwargs, 

3954 ) -> str | list[str] | torch.Tensor | Any: # Any for HF ModelOutput types 

3955 # Any: beartype forward ref limitation (beartype#546) 

3956 """Generate text using the underlying HuggingFace model with full HF API support. 

3957 

3958 This method provides direct access to HuggingFace's generation API, forwarding all 

3959 generation parameters (including output_scores, output_logits, output_attentions, 

3960 output_hidden_states) directly to the underlying HF model. Use this when you need 

3961 full HuggingFace generation features not supported by the standard generate() method. 

3962 

3963 For the standard TransformerLens generation interface, use generate() instead. 

3964 

3965 Args: 

3966 input: Text string, list of strings, or tensor of tokens 

3967 max_new_tokens: Maximum number of tokens to generate 

3968 stop_at_eos: If True, stop generating tokens when the model outputs eos_token. 

3969 If False, disable EOS stopping, including the model's configured EOS. 

3970 Custom stopping criteria still apply. 

3971 eos_token_id: The token ID (or sequence of IDs) to stop on. Ignored 

3972 when stop_at_eos=False, matching generate(). 

3973 do_sample: If True, sample from the model's output distribution 

3974 top_k: Number of tokens to sample from 

3975 top_p: Probability mass to sample from 

3976 temperature: Temperature for sampling 

3977 use_past_kv_cache: If True, use KV caching for faster generation 

3978 return_type: The type of output to return - 'input', 'str', or 'tokens' 

3979 **generation_kwargs: Additional HuggingFace generation parameters including: 

3980 - output_scores: Return generation scores 

3981 - output_logits: Return generation logits 

3982 - output_attentions: Return attention weights 

3983 - output_hidden_states: Return hidden states 

3984 - return_dict_in_generate: Return ModelOutput object 

3985 - And any other HF generation parameters 

3986 

3987 Returns: 

3988 Generated sequence as string, list of strings, tensor, or HF ModelOutput 

3989 depending on input type, return_type, and generation_kwargs. 

3990 

3991 Example:: 

3992 

3993 # Get full HF ModelOutput with logits and attentions 

3994 from transformer_lens import TransformerBridge 

3995 model = TransformerBridge.boot_transformers("tiny-stories-1M") 

3996 result = model.hf_generate( 

3997 "Hello world", 

3998 max_new_tokens=5, 

3999 output_logits=True, 

4000 output_attentions=True, 

4001 return_dict_in_generate=True 

4002 ) 

4003 print(result.sequences) # Generated tokens 

4004 print(result.logits) # Logits for each generation step 

4005 print(result.attentions) # Attention weights 

4006 """ 

4007 self._ensure_generation_supported("hf_generate") 

4008 input_attention_mask: torch.Tensor | None = None 

4009 # Handle string input by tokenizing it 

4010 if isinstance(input, str): 

4011 inputs = self.tokenizer(input, return_tensors="pt", padding=False, truncation=False).to( 

4012 self.cfg.device 

4013 ) 

4014 input_ids = inputs["input_ids"] 

4015 input_type = "str" 

4016 elif isinstance(input, list): 

4017 is_encoder_decoder = getattr( 

4018 getattr(self.original_model, "config", None), "is_encoder_decoder", False 

4019 ) 

4020 tokenizer_kwargs = {} if is_encoder_decoder else {"padding_side": "left"} 

4021 inputs = self.tokenizer( 

4022 input, 

4023 return_tensors="pt", 

4024 padding=True, 

4025 truncation=False, 

4026 **tokenizer_kwargs, 

4027 ).to(self.cfg.device) 

4028 input_ids = inputs["input_ids"] 

4029 input_attention_mask = inputs["attention_mask"] 

4030 input_type = "list" 

4031 else: 

4032 input_ids = input 

4033 if input_ids.device != self.cfg.device: 

4034 input_ids = input_ids.to(self.cfg.device) 

4035 input_type = "tokens" 

4036 

4037 # Build generation_kwargs from explicit args and kwargs 

4038 generation_kwargs = dict(generation_kwargs) if generation_kwargs is not None else {} 

4039 if input_attention_mask is not None: 

4040 generation_kwargs["attention_mask"] = input_attention_mask 

4041 generation_kwargs.update( 

4042 { 

4043 "max_new_tokens": max_new_tokens, 

4044 "do_sample": do_sample, 

4045 "temperature": temperature, 

4046 "pad_token_id": self.tokenizer.eos_token_id, 

4047 } 

4048 ) 

4049 

4050 if top_k is not None: 4050 ↛ 4051line 4050 didn't jump to line 4051 because the condition on line 4050 was never true

4051 generation_kwargs["top_k"] = top_k 

4052 if top_p is not None: 4052 ↛ 4053line 4052 didn't jump to line 4053 because the condition on line 4052 was never true

4053 generation_kwargs["top_p"] = top_p 

4054 if not stop_at_eos: 

4055 # Omitting the kwarg would restore generation_config's EOS stopping. 

4056 generation_kwargs["eos_token_id"] = None 

4057 elif eos_token_id is not None: 

4058 # cfg.eos_token_id can carry a chat model's full stop set; HF takes 

4059 # int or list, so normalize any other sequence to a list. 

4060 generation_kwargs["eos_token_id"] = ( 

4061 eos_token_id if isinstance(eos_token_id, int) else list(eos_token_id) 

4062 ) 

4063 elif self.tokenizer.eos_token_id is not None: 4063 ↛ 4066line 4063 didn't jump to line 4066 because the condition on line 4063 was always true

4064 generation_kwargs["eos_token_id"] = self.tokenizer.eos_token_id 

4065 

4066 if pixel_values is not None: 4066 ↛ 4067line 4066 didn't jump to line 4067 because the condition on line 4066 was never true

4067 generation_kwargs["pixel_values"] = pixel_values 

4068 

4069 if use_past_kv_cache: 4069 ↛ 4073line 4069 didn't jump to line 4073 because the condition on line 4069 was always true

4070 generation_kwargs["use_cache"] = True 

4071 

4072 # HF dict flags that trigger ModelOutput returns 

4073 hf_dict_flags = ( 

4074 "output_scores", 

4075 "output_logits", 

4076 "output_attentions", 

4077 "output_hidden_states", 

4078 ) 

4079 

4080 # If any HF-style output flags are provided, ensure return_dict_in_generate is set 

4081 any_flag_set = False 

4082 for f in hf_dict_flags: 

4083 if generation_kwargs.get(f) is not None: 

4084 generation_kwargs[f] = bool(generation_kwargs[f]) 

4085 any_flag_set = True 

4086 

4087 if any_flag_set: 

4088 generation_kwargs.setdefault("return_dict_in_generate", True) 

4089 

4090 with torch.no_grad(): 

4091 outputs = self.original_model.generate(input_ids, **generation_kwargs) # type: ignore[operator] 

4092 

4093 # Check if output is a ModelOutput 

4094 try: 

4095 from transformers.utils import ModelOutput # type: ignore 

4096 

4097 is_model_output = isinstance(outputs, ModelOutput) 

4098 except Exception: 

4099 is_model_output = False 

4100 

4101 # Return based on return_type and input format 

4102 if return_type == "input" or return_type is None: 

4103 if input_type == "str": 

4104 # Decode the full output back to string 

4105 if is_model_output and hasattr(outputs, "sequences"): 4105 ↛ 4107line 4105 didn't jump to line 4107 because the condition on line 4105 was always true

4106 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4107 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4108 elif input_type == "list": 

4109 # Decode each sequence in the batch 

4110 if is_model_output and hasattr(outputs, "sequences"): 4110 ↛ 4115line 4110 didn't jump to line 4115 because the condition on line 4110 was always true

4111 return [ 

4112 self.tokenizer.decode(seq, skip_special_tokens=True) 

4113 for seq in outputs.sequences 

4114 ] 

4115 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4116 else: 

4117 # Return the full token sequence including input 

4118 return outputs 

4119 elif return_type == "tokens": 4119 ↛ 4123line 4119 didn't jump to line 4123 because the condition on line 4119 was always true

4120 return outputs 

4121 else: 

4122 # For other return types, default to the decoded text 

4123 if input_type == "str": 

4124 if is_model_output and hasattr(outputs, "sequences"): 

4125 return self.tokenizer.decode(outputs.sequences[0], skip_special_tokens=True) 

4126 return self.tokenizer.decode(outputs[0], skip_special_tokens=True) 

4127 elif input_type == "list": 

4128 if is_model_output and hasattr(outputs, "sequences"): 

4129 return [ 

4130 self.tokenizer.decode(seq, skip_special_tokens=True) 

4131 for seq in outputs.sequences 

4132 ] 

4133 return [self.tokenizer.decode(seq, skip_special_tokens=True) for seq in outputs] 

4134 else: 

4135 return outputs 

4136 

4137 def prepare_multimodal_inputs( 

4138 self, 

4139 text: Union[str, List[str]], 

4140 images: Optional[Any] = None, 

4141 ) -> Dict[str, torch.Tensor]: 

4142 """Prepare multimodal inputs using the model's processor. 

4143 

4144 Converts text and images into model-ready tensors (input_ids, pixel_values, 

4145 attention_mask, etc.) using the HuggingFace processor loaded during boot(). 

4146 

4147 Args: 

4148 text: Text prompt(s), typically containing image placeholder tokens 

4149 (e.g., "<image>" for LLaVA). 

4150 images: PIL Image or list of PIL Images to process. Pass None for 

4151 text-only inputs on a multimodal model. 

4152 

4153 Returns: 

4154 Dictionary with 'input_ids', 'pixel_values', 'attention_mask', etc. 

4155 All tensors are moved to the model's device. 

4156 

4157 Raises: 

4158 ValueError: If model is not multimodal or processor is not available. 

4159 """ 

4160 if not getattr(self.cfg, "is_multimodal", False): 

4161 raise ValueError( 

4162 "prepare_multimodal_inputs() requires a multimodal model " 

4163 "(cfg.is_multimodal must be True)" 

4164 ) 

4165 if self.processor is None: 

4166 raise ValueError( 

4167 "No processor available. Load model with boot_transformers() or " 

4168 "set bridge.processor = AutoProcessor.from_pretrained(...) manually." 

4169 ) 

4170 inputs = self.processor(text=text, images=images, return_tensors="pt") 

4171 return {k: v.to(self.cfg.device) if hasattr(v, "to") else v for k, v in inputs.items()} 

4172 

4173 def to(self, *args, **kwargs) -> "TransformerBridge": 

4174 """Move model to device and/or change dtype. 

4175 

4176 Args: 

4177 args: Positional arguments for nn.Module.to 

4178 kwargs: Keyword arguments for nn.Module.to 

4179 print_details: Whether to print details about device/dtype changes (default: True) 

4180 

4181 Returns: 

4182 Self for chaining 

4183 """ 

4184 # Extract print_details if provided 

4185 print_details = kwargs.pop("print_details", True) 

4186 

4187 # Handle both device and dtype changes 

4188 # torch.nn.Module.to() supports: to(device), to(dtype), to(device, dtype), 

4189 # to(device=...), to(dtype=...), to(device=..., dtype=...) 

4190 target_device, target_dtype = None, None 

4191 

4192 if len(args) >= 1: 

4193 first_arg = args[0] 

4194 if isinstance(first_arg, (torch.device, str)): 

4195 target_device = first_arg 

4196 elif isinstance(first_arg, torch.dtype): 4196 ↛ 4198line 4196 didn't jump to line 4198 because the condition on line 4196 was always true

4197 target_dtype = first_arg 

4198 if len(args) >= 2: 

4199 second_arg = args[1] 

4200 if isinstance(second_arg, torch.dtype): 4200 ↛ 4204line 4200 didn't jump to line 4204 because the condition on line 4200 was always true

4201 target_dtype = second_arg 

4202 

4203 # these override positional args 

4204 if "device" in kwargs: 4204 ↛ 4205line 4204 didn't jump to line 4205 because the condition on line 4204 was never true

4205 target_device = kwargs["device"] 

4206 if "dtype" in kwargs: 

4207 target_dtype = kwargs["dtype"] 

4208 

4209 # Moving a multi-device (device_map-dispatched) model to a single device would 

4210 # collapse the split and break accelerate's hook routing. Warn and drop the 

4211 # device move; still honor dtype changes. 

4212 if target_device is not None and getattr(self.cfg, "n_devices", 1) > 1: 

4213 warnings.warn( 

4214 f"TransformerBridge.to({target_device!r}) ignored: model is dispatched " 

4215 f"across {self.cfg.n_devices} devices via device_map. Reload with " 

4216 "device=... (and no device_map/n_devices) to move to a single device.", 

4217 stacklevel=2, 

4218 ) 

4219 target_device = None 

4220 

4221 if target_device is not None: 

4222 move_to_and_update_config(self, target_device, print_details) 

4223 if target_dtype is not None: 

4224 move_to_and_update_config(self, target_dtype, print_details) 

4225 

4226 # Move the original model with all original args/kwargs (with print_details removed). 

4227 # When we've nulled target_device for multi-GPU safety, strip device args so the 

4228 # underlying module isn't moved either. 

4229 if target_device is None and (len(args) > 0 or "device" in kwargs): 

4230 kwargs.pop("device", None) 

4231 # Filter positional args: drop devices/strings, keep dtypes. 

4232 args = tuple(a for a in args if not isinstance(a, (torch.device, str))) 

4233 self.original_model = self.original_model.to(*args, **kwargs) 

4234 return self 

4235 

4236 def cuda(self, device: Optional[Union[int, torch.device]] = None) -> "TransformerBridge": 

4237 """Move model to CUDA. 

4238 

4239 Args: 

4240 device: CUDA device 

4241 

4242 Returns: 

4243 Self for chaining 

4244 """ 

4245 if isinstance(device, int): 

4246 return self.to(f"cuda:{device}") 

4247 elif device is None: 

4248 return self.to("cuda") 

4249 else: 

4250 return self.to(device) 

4251 

4252 def cpu(self) -> "TransformerBridge": 

4253 """Move model to CPU. 

4254 

4255 Returns: 

4256 Self for chaining 

4257 """ 

4258 return self.to(torch.device("cpu")) 

4259 

4260 def mps(self) -> "TransformerBridge": 

4261 """Move model to MPS. 

4262 

4263 Returns: 

4264 Self for chaining 

4265 """ 

4266 return self.to(torch.device("mps")) 

4267 

4268 def train(self, mode: bool = True) -> "TransformerBridge": 

4269 """Set training mode, propagating to the wrapped source model. 

4270 

4271 ``original_model`` lives in ``__dict__`` rather than the registered 

4272 module tree, so the inherited ``nn.Module.train()`` recursion does not 

4273 reach it. 

4274 """ 

4275 super().train(mode) 

4276 original = getattr(self, "original_model", None) 

4277 if isinstance(original, torch.nn.Module): 

4278 original.train(mode) 

4279 return self 

4280 

4281 def set_use_attn_result(self, use_attn_result: bool): 

4282 """Toggle whether to explicitly calculate and expose the result for each attention head. 

4283 

4284 Useful for interpretability but can easily burn through GPU memory. 

4285 """ 

4286 if use_attn_result: 

4287 self._validate_attention_fork_supported("use_attn_result") 

4288 self.cfg._set_bridge_managed_hook_flag("use_attn_result", use_attn_result) 

4289 self._propagate_attention_flag("use_attn_result", use_attn_result) 

4290 

4291 def set_use_split_qkv_input(self, use_split_qkv_input: bool): 

4292 """Toggle independent residual copies for Q/K/V so each path can be patched alone. 

4293 

4294 Mutually exclusive with `use_attn_in` — set that flag off first if it's on. 

4295 """ 

4296 if use_split_qkv_input: 

4297 if bool(getattr(self.cfg, "use_attn_in", False)): 

4298 raise ValueError( 

4299 "use_split_qkv_input and use_attn_in are mutually exclusive. " 

4300 "Call set_use_attn_in(False) before enabling use_split_qkv_input." 

4301 ) 

4302 self._validate_attention_fork_supported("use_split_qkv_input") 

4303 self.cfg._set_bridge_managed_hook_flag("use_split_qkv_input", use_split_qkv_input) 

4304 self._propagate_attention_flag("use_split_qkv_input", use_split_qkv_input) 

4305 

4306 def set_use_attn_in(self, use_attn_in: bool): 

4307 """Toggle a single 4D residual copy feeding all three Q/K/V projections. 

4308 

4309 Mutually exclusive with `use_split_qkv_input` — set that flag off first 

4310 if it's on. When on, `hook_attn_in` fires at 

4311 `[batch, pos, n_heads, d_model]`, enabling coarse-grained interventions 

4312 on the residual-stream copy shared across Q/K/V. 

4313 """ 

4314 if use_attn_in: 

4315 if bool(getattr(self.cfg, "use_split_qkv_input", False)): 

4316 raise ValueError( 

4317 "use_attn_in and use_split_qkv_input are mutually exclusive. " 

4318 "Call set_use_split_qkv_input(False) before enabling use_attn_in." 

4319 ) 

4320 self._validate_attention_fork_supported("use_attn_in") 

4321 self.cfg._set_bridge_managed_hook_flag("use_attn_in", use_attn_in) 

4322 self._propagate_attention_flag("use_attn_in", use_attn_in) 

4323 

4324 def set_use_hook_mlp_in(self, use_hook_mlp_in: bool) -> None: 

4325 """Toggle the ``hook_mlp_in`` HookPoint (the MLP-branch entry: pre-ln2, or 

4326 the MLP input on post-norm blocks), matching legacy semantics. 

4327 """ 

4328 self.cfg._set_bridge_managed_hook_flag("use_hook_mlp_in", use_hook_mlp_in) 

4329 if not hasattr(self, "blocks"): 4329 ↛ 4330line 4329 didn't jump to line 4330 because the condition on line 4329 was never true

4330 return 

4331 for block in self.blocks: 

4332 block_cfg = getattr(block, "config", None) 

4333 if block_cfg is not None and block_cfg is not self.cfg: 

4334 try: 

4335 self._write_propagated_hook_flag(block_cfg, "use_hook_mlp_in", use_hook_mlp_in) 

4336 except (AttributeError, TypeError): 

4337 pass 

4338 block._use_hook_mlp_in = use_hook_mlp_in 

4339 

4340 @staticmethod 

4341 def _write_propagated_hook_flag(config: Any, flag_name: str, value: bool) -> None: 

4342 """Write a cloned config flag without dispatching through its live Bridge.""" 

4343 if isinstance(config, TransformerBridgeConfig): 

4344 config._set_bridge_managed_hook_flag(flag_name, value) 

4345 else: 

4346 object.__setattr__(config, flag_name, value) 

4347 

4348 def _propagate_attention_flag(self, flag_name: str, value: bool) -> None: 

4349 """Mirror `bridge.cfg.<flag>` onto every block's attention config. 

4350 

4351 Some adapters (Llama family) deep-copy the block template during 

4352 `setup_blocks_bridge`, cloning the attention bridge's config along 

4353 with it. Others (Pythia, GPT-2) override `__deepcopy__` to share the 

4354 config. Setting the flag only on `self.cfg` silently misses the 

4355 cloned-config case. Propagating explicitly keeps both patterns 

4356 honest — a no-op when configs are shared, a correctness fix when 

4357 they aren't. 

4358 """ 

4359 if not hasattr(self, "blocks"): 4359 ↛ 4360line 4359 didn't jump to line 4360 because the condition on line 4359 was never true

4360 return 

4361 for block in self.blocks: 

4362 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

4363 if attn is None: 4363 ↛ 4364line 4363 didn't jump to line 4364 because the condition on line 4363 was never true

4364 continue 

4365 attn_cfg = getattr(attn, "config", None) 

4366 if attn_cfg is not None and attn_cfg is not self.cfg: 

4367 try: 

4368 self._write_propagated_hook_flag(attn_cfg, flag_name, value) 

4369 except (AttributeError, TypeError): 

4370 # Some config-like objects reject attributes even when 

4371 # bypassing their custom __setattr__ implementation. 

4372 pass 

4373 

4374 def _validate_attention_fork_supported(self, flag_name: str) -> None: 

4375 """Raise / warn if the model can't honor a fine-grained attention flag. 

4376 

4377 The post-ln1 fork path lives on JointQKVAttentionBridge and 

4378 PositionEmbeddingsAttentionBridge. Plain AttentionBridge delegates to 

4379 HF and exposes no fork point; we raise rather than setting the flag 

4380 silently. For hybrid models (some attention layers, some not), we warn 

4381 and list which layers will honor the flag. 

4382 """ 

4383 # Deferred imports: tight circular dependency with bridge setup. 

4384 from transformer_lens.model_bridge.generalized_components.joint_qkv_attention import ( 

4385 JointQKVAttentionBridge, 

4386 ) 

4387 from transformer_lens.model_bridge.generalized_components.position_embeddings_attention import ( 

4388 PositionEmbeddingsAttentionBridge, 

4389 ) 

4390 

4391 if not hasattr(self, "blocks"): 4391 ↛ 4392line 4391 didn't jump to line 4392 because the condition on line 4391 was never true

4392 raise NotImplementedError( 

4393 f"{flag_name}: this bridge has no `blocks` attribute, so no " 

4394 "attention bridges to apply the flag to." 

4395 ) 

4396 supported_classes = (JointQKVAttentionBridge, PositionEmbeddingsAttentionBridge) 

4397 supporting_layers: list[int] = [] 

4398 attn_classes: set[str] = set() 

4399 total_with_attn = 0 

4400 for idx, block in enumerate(self.blocks): 

4401 attn = block._modules.get("attn") if hasattr(block, "_modules") else None 

4402 if attn is None: 4402 ↛ 4403line 4402 didn't jump to line 4403 because the condition on line 4402 was never true

4403 continue 

4404 total_with_attn += 1 

4405 attn_classes.add(type(attn).__name__) 

4406 supports_flag = ( 

4407 bool(getattr(attn, "supports_attn_result", False)) 

4408 if flag_name == "use_attn_result" 

4409 else isinstance(attn, supported_classes) 

4410 ) 

4411 if supports_flag: 

4412 supporting_layers.append(idx) 

4413 if total_with_attn == 0: 4413 ↛ 4414line 4413 didn't jump to line 4414 because the condition on line 4413 was never true

4414 raise NotImplementedError(f"{flag_name}: no attention bridges found on self.blocks.") 

4415 if not supporting_layers: 

4416 if flag_name == "use_attn_result": 

4417 capability_detail = "Per-head result computation is unavailable." 

4418 else: 

4419 capability_detail = "No hook point is available before the Q/K/V projections." 

4420 raise NotImplementedError( 

4421 f"{flag_name}: none of this model's attention bridges support " 

4422 "the requested fine-grained attention hook. Found attention classes: " 

4423 f"{sorted(attn_classes)}. Supported classes: " 

4424 f"{[c.__name__ for c in supported_classes]}. {capability_detail}" 

4425 ) 

4426 if len(supporting_layers) < total_with_attn: 4426 ↛ 4427line 4426 didn't jump to line 4427 because the condition on line 4426 was never true

4427 skipped = total_with_attn - len(supporting_layers) 

4428 warnings.warn( 

4429 f"{flag_name}: {skipped} of {total_with_attn} attention layers " 

4430 "use an attention-bridge class that cannot honor this flag " 

4431 f"(attention classes present: {sorted(attn_classes)}). " 

4432 f"The flag will affect layers: {supporting_layers}.", 

4433 stacklevel=3, 

4434 ) 

4435 

4436 def _is_valid_bridge_path(self, hf_path: str) -> bool: 

4437 """Check if a HuggingFace path corresponds to a valid bridge component. 

4438 

4439 This validates that the path follows the bridge component structure and doesn't 

4440 contain nested HuggingFace components that should have been wrapped. 

4441 

4442 Args: 

4443 hf_path: HuggingFace path after removing _original_component 

4444 

4445 Returns: 

4446 True if the path is valid, False if it contains nested HF components 

4447 """ 

4448 parts = hf_path.split(".") 

4449 

4450 # Get the component mapping for validation 

4451 component_mapping = self.adapter.component_mapping 

4452 if not component_mapping: 4452 ↛ 4453line 4452 didn't jump to line 4453 because the condition on line 4452 was never true

4453 return True # If no mapping, accept all keys 

4454 

4455 # Walk through the path and check if each level is a registered bridge component 

4456 # For example, transformer.h.0.mlp.in.weight should be valid 

4457 # but transformer.h.0.mlp.c_fc.weight should be invalid (c_fc is nested HF component) 

4458 

4459 # Start from the root 

4460 current_component = None 

4461 idx = 0 

4462 

4463 # Find which top-level component this belongs to 

4464 for tl_name, component in component_mapping.items(): 

4465 if component.name and hf_path.startswith(component.name + "."): 

4466 current_component = component 

4467 # Skip past the HF prefix 

4468 remaining_path = hf_path[len(component.name) + 1 :] 

4469 parts = remaining_path.split(".") 

4470 idx = 0 

4471 break 

4472 

4473 if current_component is None: 

4474 return True # Path doesn't match any component, let it through 

4475 

4476 # Special handling for blocks 

4477 if hasattr(current_component, "is_list_item") and current_component.is_list_item: 

4478 # Skip the layer index 

4479 if idx < len(parts) and parts[idx].isdigit(): 4479 ↛ 4483line 4479 didn't jump to line 4483 because the condition on line 4479 was always true

4480 idx += 1 

4481 

4482 # Now validate the rest of the path against submodules 

4483 while idx < len(parts): 4483 ↛ 4510line 4483 didn't jump to line 4510 because the condition on line 4483 was always true

4484 part = parts[idx] 

4485 

4486 # If we hit 'weight' or 'bias', we're at a parameter - this is valid 

4487 if part in ("weight", "bias"): 

4488 return True 

4489 

4490 # Check if this part is a registered submodule 

4491 if hasattr(current_component, "submodules") and current_component.submodules: 

4492 if part in current_component.submodules: 

4493 current_component = current_component.submodules[part] 

4494 idx += 1 

4495 continue 

4496 else: 

4497 # This part is not a registered bridge component 

4498 # It's likely a nested HF component (like c_fc, c_proj, c_attn) 

4499 return False 

4500 else: 

4501 # No submodules to check, but not at a parameter yet 

4502 # Check if next is weight/bias 

4503 if idx + 1 < len(parts) and parts[idx + 1] in ("weight", "bias"): 

4504 return True 

4505 # Otherwise this is likely a nested HF component 

4506 return False 

4507 

4508 idx += 1 

4509 

4510 return True 

4511 

4512 def _normalize_bridge_key_to_hf(self, key: str) -> str: 

4513 """Normalize a key that uses bridge attribute names to use HF module names. 

4514 

4515 PyTorch's state_dict uses the Python attribute names (e.g., 'ln1') 

4516 but the conversion logic expects HF module names (e.g., 'ln_1'). This 

4517 function only replaces non-nested component names, leaving bridge 

4518 subcomponents (like 'in', 'out', 'q', 'k', 'v') unchanged since they're 

4519 handled by the component structure. 

4520 

4521 Args: 

4522 key: Key that may use bridge attribute names 

4523 

4524 Returns: 

4525 Key with attribute names replaced by module names where needed 

4526 """ 

4527 component_mapping = self.adapter.component_mapping 

4528 if not component_mapping: 4528 ↛ 4529line 4528 didn't jump to line 4529 because the condition on line 4528 was never true

4529 return key 

4530 

4531 # Build a mapping of only the direct module attribute names to HF names 

4532 # We only care about top-level and block-level component names, NOT subcomponents 

4533 attr_to_hf = {} 

4534 

4535 # Map top-level components 

4536 block_list_names = {"blocks", "L_blocks", "H_blocks", "encoder_blocks", "decoder_blocks"} 

4537 for tl_name, component in component_mapping.items(): 

4538 if component.name and tl_name not in block_list_names: 

4539 # Skip if TL name is already a segment of its HF path (avoids doubling). 

4540 if tl_name != component.name and tl_name not in component.name.split("."): 

4541 attr_to_hf[tl_name] = component.name 

4542 

4543 # Map block-level components (ln1, ln2, attn, mlp) for all block lists 

4544 for bl_name in block_list_names: 

4545 blocks_component = component_mapping.get(bl_name) 

4546 if blocks_component and hasattr(blocks_component, "submodules"): 

4547 for tl_subname, subcomponent in blocks_component.submodules.items(): 

4548 if subcomponent.name: 

4549 # Only map if the names differ (e.g., ln1 -> ln_1, but attn -> attn) 

4550 if tl_subname != subcomponent.name: 

4551 attr_to_hf[tl_subname] = subcomponent.name 

4552 

4553 # Replace only these specific attribute names in the key 

4554 # We need to be careful to only replace whole path components, not substrings 

4555 parts = key.split(".") 

4556 result_parts = [] 

4557 

4558 for part in parts: 

4559 if part in attr_to_hf: 

4560 result_parts.append(attr_to_hf[part]) 

4561 else: 

4562 result_parts.append(part) 

4563 

4564 return ".".join(result_parts) 

4565 

4566 def state_dict(self, destination=None, prefix="", keep_vars=False): 

4567 """Get state dict with TransformerLens format keys. 

4568 

4569 Converts HuggingFace format keys to TransformerLens format and filters out 

4570 _original_component references and nested HuggingFace components. 

4571 

4572 A direct no-argument call returns a clean state dict with bridge component 

4573 paths converted to TL format. Calls that supply ``destination`` or 

4574 ``prefix`` use standard ``nn.Module`` recursive semantics so a Bridge can 

4575 compose inside a parent module. 

4576 

4577 Args: 

4578 destination: Optional dict to store state dict in 

4579 prefix: Optional prefix to add to all keys 

4580 keep_vars: Whether to keep variables as Variables instead of tensors 

4581 

4582 Returns: 

4583 Direct calls return TransformerLens-format keys; recursive calls 

4584 return the supplied destination with standard module-tree keys. 

4585 """ 

4586 if destination is not None or prefix: 

4587 return super().state_dict( 

4588 destination=destination, 

4589 prefix=prefix, 

4590 keep_vars=keep_vars, 

4591 ) 

4592 

4593 raw_state_dict = self.original_model.state_dict(keep_vars=keep_vars) 

4594 

4595 # Clean _original_component references and convert to TL format 

4596 # Also filter out nested HuggingFace components that are wrapped by bridge components 

4597 tl_state_dict = {} 

4598 

4599 for key, value in raw_state_dict.items(): 

4600 # Skip _original_component keys 

4601 if key == "_original_component" or key.startswith("_original_component."): 4601 ↛ 4602line 4601 didn't jump to line 4602 because the condition on line 4601 was never true

4602 continue 

4603 

4604 # Remove all _original_component from the key 

4605 clean_key = key.replace("._original_component", "") 

4606 

4607 # Check if this is a valid bridge path (not a nested HF component) 

4608 if not self._is_valid_bridge_path(clean_key): 

4609 continue 

4610 

4611 # Normalize bridge component names to HF names for conversion 

4612 # (e.g., 'ln1' -> 'ln_1', 'mlp.in' -> 'mlp.c_fc') 

4613 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

4614 

4615 # Convert to TL format - this uses the adapter's component_mapping 

4616 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

4617 

4618 # Only add if we haven't seen this TL key yet (handles duplicates) 

4619 if tl_key not in tl_state_dict: 

4620 tl_state_dict[tl_key] = value 

4621 

4622 return tl_state_dict 

4623 

4624 def _tl_key_to_actual_keys(self) -> dict[str, list[str]]: 

4625 """Inverse of the renaming state_dict() applies: map each TL-format key 

4626 back to every raw parameter/buffer path that represents it. 

4627 

4628 Mirrors the filtering and key-conversion in state_dict() exactly, except 

4629 it keeps every raw key for a given TL key instead of only the first-seen 

4630 one. Bridge components frequently expose the same underlying parameter 

4631 through more than one attribute path (e.g. GPT-2's split q/k/v weights 

4632 are views into the wrapped module's combined c_attn weight, reachable 

4633 both via a block-level shortcut and via the nested _original_component 

4634 chain) - all of those aliases must be written for the round trip to 

4635 actually change what forward() reads, not just what state_dict() shows. 

4636 """ 

4637 mapping: dict[str, list[str]] = {} 

4638 for actual_key in self.original_model.state_dict(): 

4639 if actual_key == "_original_component" or actual_key.startswith("_original_component."): 4639 ↛ 4640line 4639 didn't jump to line 4640 because the condition on line 4639 was never true

4640 continue 

4641 clean_key = actual_key.replace("._original_component", "") 

4642 if not self._is_valid_bridge_path(clean_key): 

4643 continue 

4644 hf_key = self._normalize_bridge_key_to_hf(clean_key) 

4645 tl_key = self.adapter.convert_hf_key_to_tl_key(hf_key) 

4646 mapping.setdefault(tl_key, []).append(actual_key) 

4647 return mapping 

4648 

4649 def load_state_dict(self, state_dict, strict=True, assign=False): 

4650 """Load state dict into the model, handling both clean keys and original keys with _original_component references. 

4651 

4652 Accepts three key formats: TL-format keys as emitted by state_dict() 

4653 (e.g. "blocks.0.attn.q.weight"), raw native parameter paths (e.g. for 

4654 ``boot_native`` / tracr-style loading), and raw paths with 

4655 "_original_component" segments stripped. 

4656 

4657 Args: 

4658 state_dict: Dictionary containing a whole state of the module 

4659 strict: Whether to strictly enforce that the keys in state_dict match the keys returned by this module's state_dict() function 

4660 assign: Whether to assign items in the state dictionary to their corresponding keys in the module instead of copying them 

4661 

4662 Returns: 

4663 NamedTuple with missing_keys and unexpected_keys fields 

4664 """ 

4665 current_state_dict = self.original_model.state_dict(keep_vars=True) 

4666 clean_to_actual = {} 

4667 for actual_key in current_state_dict.keys(): 

4668 if actual_key != "_original_component": 4668 ↛ 4667line 4668 didn't jump to line 4667 because the condition on line 4668 was always true

4669 clean_to_actual[actual_key.replace("._original_component", "")] = actual_key 

4670 

4671 tl_to_actual = self._tl_key_to_actual_keys() 

4672 

4673 mapped_state_dict = {} 

4674 unexpected_keys = [] 

4675 for input_key, value in state_dict.items(): 

4676 if input_key in current_state_dict: 

4677 mapped_state_dict[input_key] = value 

4678 elif input_key in clean_to_actual: 

4679 mapped_state_dict[clean_to_actual[input_key]] = value 

4680 elif input_key in tl_to_actual: 

4681 for actual_key in tl_to_actual[input_key]: 

4682 mapped_state_dict[actual_key] = value 

4683 else: 

4684 unexpected_keys.append(input_key) 

4685 

4686 # A TL key's actual-key aliases share the same underlying storage (see 

4687 # _tl_key_to_actual_keys), so writing any one of them already updates 

4688 # what forward() reads for all of them. Treat the group as satisfied 

4689 # if any alias was written -- e.g. a caller supplying clean/raw keys 

4690 # (the branch above maps each clean key to exactly one actual key) 

4691 # shouldn't have the *other*, unwritten aliases reported as missing. 

4692 missing_keys = sorted( 

4693 actual_key 

4694 for actual_keys in tl_to_actual.values() 

4695 if not any(k in mapped_state_dict for k in actual_keys) 

4696 for actual_key in actual_keys 

4697 ) 

4698 

4699 if strict and (missing_keys or unexpected_keys): 

4700 error_msgs = [] 

4701 if unexpected_keys: 

4702 error_msgs.append( 

4703 "Unexpected key(s) in state_dict: " 

4704 + ", ".join(f'"{k}"' for k in sorted(unexpected_keys)) 

4705 ) 

4706 if missing_keys: 

4707 error_msgs.append( 

4708 "Missing key(s) in state_dict: " + ", ".join(f'"{k}"' for k in missing_keys) 

4709 ) 

4710 raise RuntimeError( 

4711 "Error(s) in loading state_dict for {}:\n\t{}".format( 

4712 type(self.original_model).__name__, "\n\t".join(error_msgs) 

4713 ) 

4714 ) 

4715 

4716 if not assign: 

4717 result = self.original_model.load_state_dict( 

4718 mapped_state_dict, strict=False, assign=False 

4719 ) 

4720 return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys) 

4721 

4722 # assign=True normally makes nn.Module.load_state_dict *replace* each 

4723 # target parameter/buffer with the incoming tensor rather than copying 

4724 # into existing storage. Any key whose current tensor shares storage 

4725 # with another key -- a split QKV/gate-up component's view into a 

4726 # combined weight, the combined weight itself (writing it directly 

4727 # would silently orphan the views), or a tied pair like embed/unembed 

4728 # -- would desync from whatever it shares storage with: the bridge 

4729 # might keep reading a stale value, or a live view not even part of 

4730 # this load would silently keep the pre-load data while 

4731 # original_model.state_dict() (what save_pretrained() exports) shows 

4732 # the new one. Route every key in a storage-sharing group through an 

4733 # explicit in-place .data.copy_() instead, regardless of the caller's 

4734 # assign=True, so those relationships survive. 

4735 shared_keys = _storage_group_keys(current_state_dict) 

4736 

4737 copy_items: dict[str, tuple[torch.Tensor, torch.Tensor]] = {} 

4738 passthrough_items = {} 

4739 errors = [] 

4740 for key, value in mapped_state_dict.items(): 

4741 target = current_state_dict.get(key) 

4742 if target is None or key not in shared_keys: 

4743 passthrough_items[key] = value 

4744 continue 

4745 problems = [] 

4746 if target.is_meta: 4746 ↛ 4750line 4746 didn't jump to line 4750 because the condition on line 4746 was never true

4747 # copy_() onto a meta tensor silently no-ops rather than 

4748 # raising, so a meta target would otherwise report a 

4749 # successful load while never actually materializing. 

4750 problems.append( 

4751 "the current parameter is a meta tensor; an in-place copy " 

4752 "can't materialize it, and this key shares storage with " 

4753 "another parameter so ordinary assign=True replacement " 

4754 "isn't safe here either" 

4755 ) 

4756 else: 

4757 if tuple(target.shape) != tuple(value.shape): 

4758 problems.append(f"shape {tuple(value.shape)} != expected {tuple(target.shape)}") 

4759 if target.dtype != value.dtype: 

4760 problems.append(f"dtype {value.dtype} != expected {target.dtype}") 

4761 if target.device != value.device: 

4762 problems.append(f"device {value.device} != expected {target.device}") 

4763 if problems: 

4764 errors.append(f'"{key}": ' + "; ".join(problems)) 

4765 else: 

4766 copy_items[key] = (target, value) 

4767 

4768 if errors: 

4769 raise RuntimeError( 

4770 "Cannot load the following key(s) with assign=True: each shares " 

4771 "storage with another parameter/buffer (e.g. a split QKV/" 

4772 "gate-up component's view into a combined weight, or a tied " 

4773 "pair like embed/unembed), so it can only be loaded via an " 

4774 "in-place copy, which requires an exact match.\n\t" + "\n\t".join(errors) 

4775 ) 

4776 

4777 for target, value in copy_items.values(): 

4778 with torch.no_grad(): 

4779 target.data.copy_(value) 

4780 

4781 result = self.original_model.load_state_dict(passthrough_items, strict=False, assign=True) 

4782 refresh_container_state_owners(self) 

4783 return type(result)(missing_keys=missing_keys, unexpected_keys=unexpected_keys) 

4784 

4785 def get_params(self): 

4786 """Access to model parameters in the format expected by SVDInterpreter. 

4787 

4788 For missing weights, returns zero tensors of appropriate shape instead of raising exceptions. 

4789 This ensures compatibility across different model architectures. 

4790 

4791 Returns: 

4792 dict: Dictionary of parameter tensors with TransformerLens naming convention 

4793 

4794 Raises: 

4795 ValueError: If configuration is inconsistent (e.g., cfg.n_layers != len(blocks)) 

4796 """ 

4797 return get_bridge_params(self) 

4798 

4799 # NOTE: list_supported_models and check_model_support are attached to this class 

4800 # dynamically by transformer_lens.model_bridge.sources.transformers module. 

4801 # These are HuggingFace-specific methods that belong in the transformers source module.