from typing import Literal, Any, TypeAlias
from dataclasses import dataclass
from functools import reduce
import torch
from transformers import AutoModelForCausalLM, PreTrainedTokenizerBase
from transformer_lens.config import HookedTransformerConfig
from transformer_lens.utilities.devices import get_device as tl_get_device
from interpretune.config import ITConfig, HFFromPretrainedConfig, CoreGenerationConfig, ITSerializableCfg
from interpretune.utils import (
ITInstantiationFeedbackWarning,
MisconfigurationException,
_resolve_dtype,
rank_zero_warn,
tl_invalid_dmap,
)
################################################################################
# TransformerLens Configuration Encapsulation
################################################################################
@dataclass(kw_only=True)
class ITLensSharedConfig(ITSerializableCfg):
"""TransformerLens configuration shared across both `from_pretrained` and config based instantiation modes."""
move_to_device: bool | None = True
default_padding_side: Literal["left", "right"] | None = "right"
use_bridge: bool | None = True # Use TransformerBridge (v3) by default, set False for legacy HookedTransformer
[docs]
@dataclass(kw_only=True)
class ITLensBridgeConfig(ITLensSharedConfig):
"""TransformerBridge-specific configuration for TransformerLens v3 integration.
This config provides explicit control over TransformerBridge initialization and
compatibility mode settings. Use this config when you need fine-grained control
over how the bridge is configured (e.g., enabling/disabling weight processing).
Args:
model_name: The model name for TransformerBridge (passed to TransformerBridgeConfig).
transformer_bridge_config_overrides: Optional dict of kwargs to pass to/override
in the TransformerBridgeConfig constructor. Use this to set device, dtype,
or any other TransformerBridgeConfig fields.
enable_compatibility_mode: Whether to call enable_compatibility_mode() on the
TransformerBridge after instantiation. Default: False.
enable_compatibility_mode_kwargs: Optional dict of kwargs for enable_compatibility_mode().
Supported kwargs:
- disable_warnings: bool (default False)
- no_processing: bool (default False) - disables ALL weight processing
- fold_ln: bool (default True) - fold LayerNorm weights
- center_writing_weights: bool (default True)
- center_unembed: bool (default True)
- fold_value_biases: bool (default True)
- refactor_factored_attn_matrices: bool (default False)
Example::
# Basic bridge config (no processing, current default behavior)
config = ITLensBridgeConfig(model_name="gpt2-small")
# Enable weight processing via compatibility mode
config = ITLensBridgeConfig(
model_name="gpt2-small",
enable_compatibility_mode=True,
enable_compatibility_mode_kwargs={"fold_ln": True, "fold_value_biases": True}
)
# Use no_processing (analogous to ITLensFromPretrainedNoProcessingConfig behavior)
config = ITLensBridgeConfig(
model_name="gpt2-small",
enable_compatibility_mode=True,
enable_compatibility_mode_kwargs={"no_processing": True}
)
"""
# The model name/path for TransformerBridge - IT handles HF model instantiation via model_name_or_path
model_name: str = "gpt2-small"
# Optional kwargs to pass to TransformerBridgeConfig constructor
transformer_bridge_config_overrides: dict[str, Any] | None = None
# Whether to call enable_compatibility_mode on the bridge after instantiation
# N.B.: See transformer_lens/model_bridge/bridge.py for details, among other things, this mode:
# 1. Breaks weight tying between embed and unembed to allow separate unembed centering
# 2. Extracts q/k/v from joint qkv matrices for compatibility with HookedTransformer parameterizations
enable_compatibility_mode: bool = False
# Optional kwargs for enable_compatibility_mode()
enable_compatibility_mode_kwargs: dict[str, Any] | None = None
# Bridge config defaults to using bridge
use_bridge: bool | None = True
# Device is commonly set, so we provide a top-level field for convenience
device: str | None = None
# Dtype is commonly set, so we provide a top-level field for convenience
dtype: str = "float32"
def __post_init__(self) -> None:
if self.device is None: # align with TL default device resolution
self.device = tl_get_device() # type: ignore
# Validate that enable_compatibility_mode_kwargs are only set when enable_compatibility_mode is True
if self.enable_compatibility_mode_kwargs and not self.enable_compatibility_mode:
rank_zero_warn(
"enable_compatibility_mode_kwargs was provided but enable_compatibility_mode is False. "
"The kwargs will be ignored. Set enable_compatibility_mode=True to use them.",
category=ITInstantiationFeedbackWarning,
)
# TODO: open a PR to have TL `from_pretrained` config encapsulated in a dataclass for improved external compatibility
[docs]
@dataclass(kw_only=True)
class ITLensFromPretrainedConfig(ITLensSharedConfig):
model_name: str = "gpt2-small"
fold_ln: bool | None = True
center_writing_weights: bool | None = True
center_unembed: bool | None = True
refactor_factored_attn_matrices: bool | None = False
checkpoint_index: int | None = None
checkpoint_value: int | None = None
# for pretrained cfg, IT handles the HF model instantiation via model_name or_path
hf_model: AutoModelForCausalLM | str | None = None
# currently only annotating with str due to omegaconf container dumping limitations wrt torch.device
device: str | None = None
n_devices: int | None = 1
# IT handles the tokenizer instantiation via either tokenizer, tokenizer_name or model_name_or_path
tokenizer: PreTrainedTokenizerBase | None = None # for pretrained cfg, IT instantiates the tokenizer
fold_value_biases: bool | None = True
default_prepend_bos: bool | None = True
dtype: str = "float32"
def __post_init__(self) -> None:
if self.device is None: # align with TL default device resolution
self.device = tl_get_device() # type: ignore
# rather than use from_pretrained_no_processing wrapper, we can specify the simplified config defaults we want directly
[docs]
@dataclass(kw_only=True)
class ITLensFromPretrainedNoProcessingConfig(ITLensFromPretrainedConfig):
fold_ln: bool | None = False
center_writing_weights: bool | None = False
center_unembed: bool | None = False
refactor_factored_attn_matrices: bool | None = False
fold_value_biases: bool | None = False
dtype: str = "float32"
default_prepend_bos: bool | None = True
[docs]
@dataclass(kw_only=True)
class ITLensCustomConfig(ITLensSharedConfig):
"""Custom TL config for creating a HookedTransformer from a TL config.
NOTE: TransformerBridge is not supported with config-only initialization.
Set `use_bridge=False` (default) or interpretune will force the value to False and warn.
"""
cfg: HookedTransformerConfig | dict[str, Any]
# When using a custom config, default to legacy HookedTransformer behavior to prevent
# misconfiguration. If the user explicitly sets `use_bridge=True`, Interpretune will
# warn and force it to False in `ITLensConfig.__post_init__`.
use_bridge: bool | None = False
# IT handles the tokenizer instantiation via either tokenizer, tokenizer_name or model_name_or_path
# tokenizer: PreTrainedTokenizerBase | None = None
def __post_init__(self) -> None:
if not isinstance(self.cfg, HookedTransformerConfig):
# ensure the user provided a valid dtype (should be handled by HookedTransformerConfig ideally)
if self.cfg.get("dtype", None) and not isinstance(self.cfg["dtype"], torch.dtype):
self.cfg["dtype"] = _resolve_dtype(self.cfg["dtype"])
self.cfg = HookedTransformerConfig.from_dict(self.cfg)
ITLensCfg: TypeAlias = ITLensFromPretrainedConfig | ITLensCustomConfig | ITLensBridgeConfig # for static typing
ITLensCfgTypes: tuple[type, type, type] = (
ITLensFromPretrainedConfig,
ITLensCustomConfig,
ITLensBridgeConfig,
) # for runtime checks
[docs]
class TLConfigInitMixin:
"""Mixin providing TransformerLens config initialization logic.
Shared by :class:`ITLensConfig` (pure TL config) and :class:`SAELensConfig` (multi-backend config
that delegates to TL when ``backend="transformerlens"``). Separating these helpers avoids code
duplication without requiring SAELensConfig to inherit from ITLensConfig.
Note: This mixin always co-inherits with :class:`ITConfig`, which provides
``hf_from_pretrained_cfg``, ``model_name_or_path``, ``tokenizer_kwargs``, etc.
Only attributes unique to TL initialization are declared here.
"""
# Attributes unique to TL initialization (not provided by ITConfig).
# ``tl_cfg`` is typed as ``Any`` to avoid invariance conflicts — consuming
# dataclasses narrow the type (non-optional on ITLensConfig, optional on
# SAELensConfig).
tl_cfg: Any
_load_from_pretrained: bool
_dtype: torch.dtype | None
# ------------------------------------------------------------------
# Core TL config state initialization (called by __post_init__ or
# backend-specific init methods on consuming classes).
# ------------------------------------------------------------------
def _init_tl_cfg_state(self) -> None:
"""Validate ``tl_cfg`` and initialize TL-specific state.
This method encapsulates the logic that was previously in ``ITLensConfig.__post_init__``
(minus the final ``super().__post_init__()`` call, which remains the caller's responsibility
so each consuming class can control its own MRO chain).
"""
if not self.tl_cfg:
raise MisconfigurationException(
"A valid tl_cfg (ITLensFromPretrainedConfig, ITLensCustomConfig, or ITLensBridgeConfig) must be"
" provided to initialize a HookedTransformer/TransformerBridge and use TransformerLens."
)
# internal variable used to bootstrap model initialization mode (we may need to override hf_from_pretrained_cfg)
# ITLensBridgeConfig is a pretrained mode config (like ITLensFromPretrainedConfig)
self._load_from_pretrained = not isinstance(self.tl_cfg, ITLensCustomConfig)
if not self._load_from_pretrained:
# If a custom config was provided, TransformerBridge (v3) cannot be used because it requires an HF model.
# Default to legacy HookedTransformer (use_bridge=False) for custom configs. If the user explicitly
# set `use_bridge=True`, warn and force it to False so the session doesn't fail unexpectedly.
if getattr(self.tl_cfg, "use_bridge", False):
rank_zero_warn(
"ITLensCustomConfig does not support TransformerBridge (use_bridge=True); "
"forcing `use_bridge=False` and falling back to HookedTransformer.",
category=ITInstantiationFeedbackWarning,
)
# Make sure downstream logic sees the intended value
self.tl_cfg.use_bridge = False
self._disable_pretrained_model_mode() # after this, hf_from_pretrained_cfg exists only if used
assert isinstance(self.tl_cfg, ITLensCustomConfig)
assert isinstance(self.tl_cfg.cfg, HookedTransformerConfig)
self._dtype = _resolve_dtype(self.tl_cfg.cfg.dtype)
else:
# TL from pretrained currently requires a hf_from_pretrained_cfg, create one if it's not already configured
if not self.hf_from_pretrained_cfg:
self.hf_from_pretrained_cfg = HFFromPretrainedConfig()
elif not isinstance(self.hf_from_pretrained_cfg, HFFromPretrainedConfig):
try:
self.hf_from_pretrained_cfg = HFFromPretrainedConfig(**self.hf_from_pretrained_cfg)
except Exception as e:
raise MisconfigurationException(
f"Failed to initialize `HFFromPretrainedConfig` from provided"
f" `hf_from_pretrained_cfg`. `hf_from_pretrained_cfg` should be "
" either a `HFFromPretrainedConfig` or a dict convertible to one."
f" Error: {e}"
)
self._sync_pretrained_cfg()
self._translate_tl_config()
# ------------------------------------------------------------------
# TL config helper methods
# ------------------------------------------------------------------
def _map_tl_fallback(self, target_key: str, tl_cfg_key: str):
qual_sub_key = tl_cfg_key.split(".")
fallback_val = reduce(getattr, qual_sub_key, self)
if fallback_val not in [None, "custom"]:
existing_val = getattr(self, target_key, None)
# Treat both None and empty string as "not provided"
if existing_val is not None and existing_val != "":
hf_override_msg = f"Since `{target_key}` was provided, `{tl_cfg_key}` will be ignored."
else:
hf_override_msg = (
f"Since `{target_key} was not provided, the value provided for `{tl_cfg_key}` will"
f" be used for `{target_key}`."
)
setattr(self, target_key, fallback_val)
setattr(reduce(getattr, qual_sub_key[:-1], self), qual_sub_key[-1], None)
rank_zero_warn(
f"Interpretune manages the HF model instantiation via `model_name_or_path`. {hf_override_msg}",
category=ITInstantiationFeedbackWarning,
)
def _translate_tl_config(self):
# TODO: driving this fallback mapping from a dict
if self._load_from_pretrained:
# Only ITLensFromPretrainedConfig and variants have an `hf_model` field that maps to model_name_or_path,
# ITLensBridgeConfig natively uses `model_name` for HF model instantiation (TL registry aliases for models
# have been deprecated in favor of HF model names/paths for bridge mode).
if hasattr(self.tl_cfg, "hf_model"):
self._map_tl_fallback(target_key="model_name_or_path", tl_cfg_key="tl_cfg.hf_model")
# Only ITLensFromPretrainedConfig has tokenizer field; ITLensBridgeConfig doesn't have it
if hasattr(self.tl_cfg, "tokenizer"):
self._map_tl_fallback(target_key="tokenizer", tl_cfg_key="tl_cfg.tokenizer")
else:
self._map_tl_fallback(target_key="model_name_or_path", tl_cfg_key="tl_cfg.cfg.model_name")
self._map_tl_fallback(target_key="tokenizer_name", tl_cfg_key="tl_cfg.cfg.tokenizer_name")
def _disable_pretrained_model_mode(self):
ignored_attrs = []
for attr in ["hf_from_pretrained_cfg", "defer_model_init"]:
if getattr(self, attr):
ignored_attrs.append(attr)
setattr(self, attr, None)
if len(ignored_attrs) > 0:
rank_zero_warn(
"Since an `ITLensCustomConfig` has been provided, the following list of set `ITConfig`"
f" attributes will be ignored: {ignored_attrs}.",
category=ITInstantiationFeedbackWarning,
)
def _sync_pretrained_cfg(self):
if self.hf_from_pretrained_cfg:
self._check_supported_device_map()
if hf_dtype := self.hf_from_pretrained_cfg.pretrained_kwargs.get("dtype", None):
hf_dtype = _resolve_dtype(hf_dtype)
# Both ITLensFromPretrainedConfig and ITLensBridgeConfig have dtype attribute
assert isinstance(self.tl_cfg, (ITLensFromPretrainedConfig, ITLensBridgeConfig))
tl_dtype = _resolve_dtype(self.tl_cfg.dtype)
self._sync_hf_tl_dtypes(hf_dtype, tl_dtype)
def _check_supported_device_map(self):
if self.hf_from_pretrained_cfg is None or self.hf_from_pretrained_cfg.pretrained_kwargs is None:
return
device_map = self.hf_from_pretrained_cfg.pretrained_kwargs.get("device_map", None)
if isinstance(device_map, dict) and len(device_map.keys()) > 1:
rank_zero_warn(tl_invalid_dmap, category=ITInstantiationFeedbackWarning)
self.hf_from_pretrained_cfg.pretrained_kwargs["device_map"] = "cpu"
def _sync_hf_tl_dtypes(self, hf_dtype, tl_dtype):
if self.hf_from_pretrained_cfg is None:
return
if self.hf_from_pretrained_cfg.pretrained_kwargs is None:
self.hf_from_pretrained_cfg.pretrained_kwargs = {}
if hf_dtype and tl_dtype:
if hf_dtype != tl_dtype: # if both are provided, TL dtype takes precedence
rank_zero_warn(
f"HF `from_pretrained` dtype {hf_dtype} does not match TL dtype {tl_dtype}."
f" Setting both to the specified TL dtype {tl_dtype}."
)
self.hf_from_pretrained_cfg.pretrained_kwargs["dtype"] = tl_dtype
else:
rank_zero_warn(
"HF `from_pretrained` dtype was not provided. Setting `from_pretrained` dtype to match"
f" specified TL dtype: {tl_dtype}."
)
self.hf_from_pretrained_cfg.pretrained_kwargs["dtype"] = tl_dtype
[docs]
@dataclass(kw_only=True)
class ITLensConfig(ITConfig, TLConfigInitMixin):
"""Dataclass to encapsulate the ITModule internal state."""
tl_cfg: ITLensFromPretrainedConfig | ITLensCustomConfig | ITLensBridgeConfig
def __post_init__(self) -> None:
self._init_tl_cfg_state()
super().__post_init__()
# TODO: we should be able to standardize on the HF GenerationConfig interface and remove TLensGenerationConfig once
# once (if) TL migrates away from HookedTransformer.generate method to using the HF generate interface
[docs]
@dataclass(kw_only=True)
class TLensGenerationConfig(CoreGenerationConfig):
stop_at_eos: bool = True
eos_token_id: int | None = None
freq_penalty: float = 0.0
use_past_kv_cache: bool = True
prepend_bos: bool | None = None
padding_side: Literal["left", "right"] | None = None
return_type: str | None = "input"
output_logits: bool | None = None
verbose: bool = True