Source code for interpretune.config.sae_lens

# N.B. we need to avoid annotations import here due to jsonargparse validation issues that emerge when it is used
# (PEP 563 deferred annotations cause all annotations to become strings, and jsonargparse's
# evaluate_postponed_annotations fails globally when get_type_hints encounters AnalysisCfgProtocol
# in the ITConfig hierarchy, leaving required fields like sae_cfgs unresolved)
# from __future__ import annotations
from typing import Any, TypeAlias, TYPE_CHECKING
from collections.abc import Sequence
from dataclasses import dataclass

from sae_lens.saes.sae import SAEConfig
from sae_lens.saes.standard_sae import StandardSAEConfig
from transformer_lens.utilities.devices import get_device as tl_get_device
from transformer_lens.config import HookedTransformerConfig

from interpretune.config import (
    ITConfig,
    ITSerializableCfg,
    ITLensCfgTypes,
    ITLensCustomConfig,
    ITLensFromPretrainedConfig,
)
from interpretune.config.transformer_lens import ITLensBridgeConfig, TLConfigInitMixin
from interpretune.utils import ITInstantiationFeedbackWarning, rank_zero_warn, MisconfigurationException, _resolve_dtype

if TYPE_CHECKING:
    from interpretune.config.nnsight import NNsightConfig

# Valid backend identifiers
_VALID_SAE_BACKENDS = ("transformerlens", "nnsight")

################################################################################
# SAE Lens Configuration Encapsulation
################################################################################


[docs] @dataclass(kw_only=True) class SAELensFromPretrainedConfig(ITSerializableCfg): release: str sae_id: str device: str | None = None dtype: str | None = None def __post_init__(self) -> None: if self.device is None: # align with TL default device resolution self.device = str(tl_get_device())
[docs] @dataclass(kw_only=True) class SAELensCustomConfig(ITSerializableCfg): cfg: SAEConfig | dict[str, Any] # TODO: may add additional custom behavior handling attributes here def __post_init__(self) -> None: if not isinstance(self.cfg, SAEConfig): # TODO: add a PR to SAELens to allow SAEConfig to ref torch dtype and device objects instead of str repr # ensure the user provided a valid dtype (should be handled by SAEConfig ideally) # if self.cfg.get('dtype', None) and not isinstance(self.cfg['dtype'], torch.dtype): # self.cfg['dtype'] = _resolve_dtype(self.cfg['dtype']) # TODO: enable configuration/introspection of custom SAE subclasses here self.cfg = StandardSAEConfig.from_dict(self.cfg)
SAECfgType: TypeAlias = SAELensFromPretrainedConfig | SAELensCustomConfig
[docs] @dataclass(kw_only=True) class SAELensConfig(ITConfig, TLConfigInitMixin): """Configuration for SAE Lens adapter. Supports both TransformerLens and NNsight backends via the ``backend`` field. When ``backend="transformerlens"`` (default), ``tl_cfg`` must be provided. When ``backend="nnsight"``, ``nnsight_cfg`` must be provided instead. Inherits from :class:`ITConfig` (not ``ITLensConfig``) so that it is backend-agnostic at the type level. TL-specific initialization logic is provided by :class:`TLConfigInitMixin`, which is shared with ``ITLensConfig``. The ``use_bridge`` field is only meaningful when ``backend="transformerlens"`` and controls whether a SAETransformerBridge (True) or HookedSAETransformer (False) is used. """ # Backend selection backend: str = "transformerlens" use_bridge: bool = True # TL backend configuration (required when backend="transformerlens") tl_cfg: ITLensFromPretrainedConfig | ITLensCustomConfig | ITLensBridgeConfig | None = None # NNsight backend configuration (required when backend="nnsight") nnsight_cfg: "NNsightConfig | None" = None # SAE-specific fields # NOTE: TypeAlias inlined here so jsonargparse can resolve the type in CLI contexts # (PEP 563 deferred annotations + TypeAlias causes fail_untyped failures). sae_cfgs: ( SAELensFromPretrainedConfig | SAELensCustomConfig | Sequence[SAELensFromPretrainedConfig | SAELensCustomConfig] ) add_saes_on_init: bool = False # TODO: may push this down to SAE config level instead of setting for all saes # use_error_term: bool = False # TODO: add support for use_error_term with on_init stateful SAEs @property def normalized_sae_cfg_refs(self) -> list[str]: normalized_names = [] # Handle both single config and list of configs if isinstance(self.sae_cfgs, (SAELensFromPretrainedConfig, SAELensCustomConfig)): sae_cfgs = [self.sae_cfgs] else: sae_cfgs = self.sae_cfgs for sae_cfg in sae_cfgs: if isinstance(sae_cfg, SAELensFromPretrainedConfig): normalized_names.append(sae_cfg.sae_id) elif isinstance(sae_cfg, SAELensCustomConfig): assert isinstance(sae_cfg.cfg, SAEConfig) normalized_names.append(sae_cfg.cfg.metadata.hook_name) return normalized_names def __post_init__(self) -> None: # Validate backend if self.backend not in _VALID_SAE_BACKENDS: raise ValueError(f"Invalid backend '{self.backend}'. Must be one of {_VALID_SAE_BACKENDS}") if self.backend != "transformerlens" and self.use_bridge: rank_zero_warn( "use_bridge=True is only meaningful when backend='transformerlens'. This setting will be ignored.", category=ITInstantiationFeedbackWarning, ) # Validate and normalize sae_cfgs (backend-agnostic) if not self.sae_cfgs: raise MisconfigurationException( "At least one `SAELensFromPretrainedConfig` or `SAELensCustomConfig` must be provided to " "initialize SAE Lens." ) if isinstance(self.sae_cfgs, (SAELensFromPretrainedConfig, SAELensCustomConfig)): self.sae_cfgs = [self.sae_cfgs] # Backend-specific initialization if self.backend == "transformerlens": self._init_tl_backend() else: self._init_nnsight_backend() def _init_tl_backend(self) -> None: """Initialize TransformerLens backend configuration.""" if not self.tl_cfg: raise MisconfigurationException( "A valid tl_cfg (ITLensFromPretrainedConfig, ITLensCustomConfig, or ITLensBridgeConfig) must be " "provided when backend='transformerlens'." ) # Warn if use_bridge=True but tl_cfg is not ITLensBridgeConfig (likely misconfiguration) if ( self.use_bridge and isinstance(self.tl_cfg, ITLensFromPretrainedConfig) and not isinstance(self.tl_cfg, ITLensBridgeConfig) ): rank_zero_warn( "use_bridge=True but tl_cfg is an ITLensFromPretrainedConfig (HookedTransformer config), " "not an ITLensBridgeConfig. This will initialize a HookedTransformer, not a TransformerBridge. " "To use TransformerBridge, set tl_cfg to ITLensBridgeConfig. " "To silence this warning, set use_bridge=False explicitly.", category=ITInstantiationFeedbackWarning, ) # Warn if nnsight_cfg is set but not used if self.nnsight_cfg is not None: rank_zero_warn( "nnsight_cfg is set but backend is 'transformerlens'. This setting will be ignored.", category=ITInstantiationFeedbackWarning, ) # Initialize TL config state (validation, pretrained sync, config translation) # via TLConfigInitMixin — shared with ITLensConfig self._init_tl_cfg_state() # Call ITConfig.__post_init__ for base config setup (dtype resolution etc.) super().__post_init__() # Sync SAE device config with TL device self._sync_sl_tl_device_cfg() def _init_nnsight_backend(self) -> None: """Initialize NNsight backend configuration.""" from interpretune.config.nnsight import NNsightConfig if not self.nnsight_cfg: raise MisconfigurationException( "A valid nnsight_cfg (NNsightConfig) must be provided when backend='nnsight'." ) if not isinstance(self.nnsight_cfg, NNsightConfig): try: self.nnsight_cfg = NNsightConfig(**self.nnsight_cfg) except Exception as e: raise MisconfigurationException( f"Failed to initialize NNsightConfig from provided nnsight_cfg. " f"nnsight_cfg should be either a NNsightConfig instance or a dict " f"convertible to one. Error: {e}" ) # Warn if tl_cfg is set but not used if self.tl_cfg is not None: rank_zero_warn( "tl_cfg is set but backend is 'nnsight'. This setting will be ignored.", category=ITInstantiationFeedbackWarning, ) # Sync model_name_or_path with nnsight_cfg.model_name self._sync_nnsight_model_name() # Set dtype from nnsight_cfg if available assert self.nnsight_cfg is not None # narrowing for type checker; validated above if self.nnsight_cfg.resolved_dtype is not None: self._dtype = _resolve_dtype(self.nnsight_cfg.resolved_dtype) # Call ITConfig.__post_init__ for base config setup (dtype resolution etc.) super().__post_init__() # Sync SAE device config with NNsight model device self._sync_sl_nnsight_device_cfg() def _sync_sl_nnsight_device_cfg(self) -> None: """Sync SAE config devices with the NNsight model's target device. Mirrors :meth:`_sync_sl_tl_device_cfg` for the NNsight backend. ``SAELensFromPretrainedConfig.__post_init__`` calls ``tl_get_device()`` which may resolve to CUDA even when the NNsight model is on CPU. This method corrects the SAE configs to match the NNsight device. """ assert self.nnsight_cfg is not None device_map = self.nnsight_cfg.device_map # Resolve a simple target device string from the NNsight device_map if isinstance(device_map, str) and device_map not in ("auto", "balanced", "sequential"): target_device = device_map # e.g. "cpu", "cuda", "cuda:0" else: # For auto / dict / None, fall back to auto-detection target_device = str(tl_get_device()) if isinstance(self.sae_cfgs, (SAELensFromPretrainedConfig, SAELensCustomConfig)): sae_cfgs: Sequence[SAECfgType] = [self.sae_cfgs] else: sae_cfgs = self.sae_cfgs for sae_cfg in sae_cfgs: if hasattr(sae_cfg, "cfg"): assert isinstance(sae_cfg, SAELensCustomConfig) assert isinstance(sae_cfg.cfg, SAEConfig) setattr(sae_cfg.cfg, "device", target_device) else: assert isinstance(sae_cfg, SAELensFromPretrainedConfig) setattr(sae_cfg, "device", target_device) def _sync_nnsight_model_name(self) -> None: """Synchronize model_name_or_path with nnsight_cfg.model_name.""" assert self.nnsight_cfg is not None # validated in _init_nnsight_backend it_model = self.model_name_or_path ns_model = self.nnsight_cfg.model_name if not it_model and not ns_model: raise MisconfigurationException("Either model_name_or_path or nnsight_cfg.model_name must be provided.") if not it_model and ns_model: self.model_name_or_path = ns_model elif it_model and not ns_model: self.nnsight_cfg.model_name = it_model elif it_model != ns_model: rank_zero_warn( f"model_name_or_path ('{it_model}') differs from nnsight_cfg.model_name ('{ns_model}'). " f"Using model_name_or_path. Set nnsight_cfg.model_name=None to silence this warning.", category=ITInstantiationFeedbackWarning, ) self.nnsight_cfg.model_name = it_model def _sync_sl_tl_device_cfg(self): assert isinstance(self.tl_cfg, ITLensCfgTypes) if hasattr(self.tl_cfg, "cfg"): # TODO: consider reverting this to ternary assignment w/ type check directives assert isinstance(self.tl_cfg, ITLensCustomConfig) assert isinstance(self.tl_cfg.cfg, HookedTransformerConfig) tl_device = self.tl_cfg.cfg.device elif isinstance(self.tl_cfg, ITLensBridgeConfig): tl_device = self.tl_cfg.device else: assert isinstance(self.tl_cfg, ITLensFromPretrainedConfig) tl_device = self.tl_cfg.device # Handle both single config and list of configs if isinstance(self.sae_cfgs, (SAELensFromPretrainedConfig, SAELensCustomConfig)): sae_cfgs = [self.sae_cfgs] else: sae_cfgs = self.sae_cfgs for sae_cfg in sae_cfgs: if hasattr(sae_cfg, "cfg"): assert isinstance(sae_cfg, SAELensCustomConfig) assert isinstance(sae_cfg.cfg, SAEConfig) self._sync_sl_tl_default_device(sae_cfg_obj=sae_cfg.cfg, tl_device=str(tl_device)) else: assert isinstance(sae_cfg, SAELensFromPretrainedConfig) self._sync_sl_tl_default_device(sae_cfg_obj=sae_cfg, tl_device=str(tl_device)) def _sync_sl_tl_default_device(self, sae_cfg_obj: SAELensFromPretrainedConfig | SAEConfig, tl_device): if sae_cfg_obj.device and tl_device: if sae_cfg_obj.device != tl_device: rank_zero_warn( f"This SAEConfig's device type ('{sae_cfg_obj.device}') does not match the configured TL device " f"('{tl_device}'). Setting the device type for this SAE to match the specified TL device " f"('{tl_device}').", category=ITInstantiationFeedbackWarning, ) setattr(sae_cfg_obj, "device", tl_device) else: rank_zero_warn( "An SAEConfig device type was not provided. Setting the device type to match the currently specified " f"TL device type: '{tl_device}'.", category=ITInstantiationFeedbackWarning, ) setattr(sae_cfg_obj, "device", tl_device)