| |
|
|
| from abc import ABC, abstractmethod |
|
|
| import torch |
| from torch.nn import Linear, Module, Parameter |
| from torch import Tensor |
|
|
| from .decoder import decode |
| from .types import Stats, TopK, SAEOut |
| from .SAE_Wrapper import _HookedSAE, _disable_hooks |
|
|
| from .Utils import standardize, unit_norm_decoder |
| from typing import List, Dict, Tuple, Any |
| from transformer_lens.hook_points import HookPoint |
| from dataclasses import dataclass |
|
|
| @dataclass |
| class SAEConfig: |
| d_in: int |
| |
| d_sae: int |
| |
| hook_name: str |
| |
| hook_names: List[str] |
| |
| dead_steps_threshold: int |
| |
| dead_threshold: float |
| |
| auxk: int | None |
| |
| standardize: bool |
|
|
| class SAE_Template( |
| ABC, |
| _HookedSAE, |
| ): |
| last_nonzero: torch.Tensor |
| """The number of steps since the latents have activated.""" |
| def __init__( |
| self, |
| d_in: int, |
| d_sae: int, |
| hook_names: List[str], |
| dead_steps_threshold: int, |
| dead_threshold: float = 1e-3, |
| |
| auxk: int | None = 256, |
| standardize: bool = True, |
| ) -> None: |
| """ |
| Args: |
| d_in (int): The number of inputs. |
| |
| d_sae (int): The number of latents. |
| |
| dead_steps_threshold (int): The number of steps after which a latent is |
| flagged as dead during training. |
| |
| dead_threshold (float): The threshold for a latent to be considered |
| activated. Defaults to 1e-3. |
| |
| auxk (int | None): The number of dead latents with which to model the |
| reconstruction error. Defaults to 256. |
| |
| standardize (bool): Whether to standardize the inputs. Defaults to True. |
| """ |
|
|
| super().__init__() |
| self.cfg = SAEConfig( |
| d_in=d_in, |
| d_sae=d_sae, |
| hook_name=hook_names[0], |
| hook_names=hook_names, |
| auxk=auxk, |
| dead_steps_threshold=dead_steps_threshold, |
| dead_threshold=dead_threshold, |
| standardize=standardize, |
| ) |
| self.use_error_term = False |
| self.encoder = Linear(d_in, d_sae, bias=False) |
| self.decoder = Linear(d_sae, d_in, bias=False) |
| self.pre_encoder_bias = Parameter(torch.zeros(d_in)) |
|
|
| self.register_buffer("last_nonzero", torch.zeros(d_sae, dtype=torch.long)) |
|
|
| self.decoder.weight.data = self.encoder.weight.data.T.clone() |
| |
| self.decoder.weight.data = self.decoder.weight.data.T.contiguous().T |
| unit_norm_decoder(self.decoder) |
| |
| |
| self.hook_sae_input = HookPoint() |
| self.hook_sae_acts_pre = HookPoint() |
| self.hook_sae_acts_post = HookPoint() |
| self.hook_sae_output = HookPoint() |
| self.hook_sae_recons = HookPoint() |
| self.hook_sae_error = HookPoint() |
| super().setup() |
| |
| @abstractmethod |
| def encode(self, *args, **kwargs) -> Tuple[Any, ...]: |
| pass |
| |
| @abstractmethod |
| def decode(self, *args, **kwargs) -> torch.Tensor: |
| pass |
|
|
| @abstractmethod |
| def forward_training(self, inputs: torch.Tensor) -> SAEOut: |
| pass |
| |
| def forward( |
| self, |
| x: torch.Tensor, |
| ) -> torch.Tensor: |
| ''' |
| Modify the forward pass to allow gradient flows through the error term. |
| ''' |
| latents, _, _, stats, _ = self.encode(x) |
| sae_out = self.decode(latents, stats) |
|
|
| if self.use_error_term: |
| with torch.no_grad() if self.detach_error_term else torch.enable_grad(): |
| with _disable_hooks(self): |
| clead_sae, _, _, clean_stats, _ = self.encode(x) |
| x_reconstruct_clean = self.decode(clead_sae, clean_stats) |
| |
| if self.disable_error_grad: |
| |
| |
| with torch.no_grad(): |
| sae_error = self.hook_sae_error(x - x_reconstruct_clean) |
| else: |
| |
| |
| with torch.no_grad() if self.detach_error_term else torch.enable_grad(): |
| temp_error = (x - x_reconstruct_clean) |
| temp_error.requires_grad_() |
| |
| sae_error = self.hook_sae_error(temp_error) |
| sae_out = sae_out + sae_error |
| |
| return self.hook_sae_output(sae_out) |
| |
| def update_last_nonzero(self, topk: TopK, device: torch.device) -> None: |
| |
| last_nonzero = torch.zeros_like(self.last_nonzero, device=device) |
| last_nonzero.scatter_add_( |
| dim=0, |
| index=topk.indices.reshape(-1), |
| src=( |
| topk.values > self.cfg.dead_threshold |
| ).to(last_nonzero.dtype).reshape(-1), |
| ) |
| self.last_nonzero *= 1 - last_nonzero.clamp(max=1) |
| self.last_nonzero += 1 |
| |
| def compute_dead_latents_and_auxk(self, latents: Tensor) -> tuple[Tensor, TopK | None]: |
| |
| dead_mask = self.last_nonzero >= self.cfg.dead_steps_threshold |
| latents = latents * dead_mask |
|
|
| |
| dead = torch.sum(dead_mask, dtype=torch.float32).detach() / self.cfg.d_sae |
|
|
| |
| auxk = None |
| if self.cfg.auxk is not None: |
| values_auxk , indices_auxk = torch.topk( |
| latents, |
| k=self.cfg.auxk, |
| sorted=False |
| ) |
| auxk = TopK(values_auxk , indices_auxk) |
| return dead, auxk |
| |
| def on_after_backward(self) -> list[Linear]: |
| return [] |
| |
| def on_train_end(self) -> list[Tensor]: |
| return [] |
| |
| @property |
| def W_dec(self) -> Tensor: |
| """The decoder weight matrix. Transpose to match sae_lens""" |
| return self.decoder.weight.T |
| |
| @property |
| def W_enc(self) -> Tensor: |
| """The encoder weight matrix. Transpose to match sae_lens""" |
| return self.encoder.weight.T |
| |
| @property |
| def b_dec(self) -> Tensor: |
| """The decoder bias vector.""" |
| return self.pre_encoder_bias |
| |
| @property |
| def b_enc(self) -> Tensor: |
| """The encoder bias vector.""" |
| return -self.pre_encoder_bias |
| |
| class SAE(SAE_Template): |
| last_nonzero: torch.Tensor |
| """The number of steps since the latents have activated.""" |
| def __init__( |
| self, |
| d_in: int, |
| d_sae: int, |
| hook_names: List[str], |
| dead_steps_threshold: int, |
| dead_threshold: float = 1e-3, |
| |
| auxk: int | None = 256, |
| standardize: bool = True, |
| sparsity_coef: float = 1 / 16, |
| **kwargs, |
| ) -> None: |
| """ |
| Args: |
| d_in (int): The number of inputs. |
| |
| d_sae (int): The number of latents. |
| |
| dead_steps_threshold (int): The number of steps after which a latent is |
| flagged as dead during training. |
| |
| dead_threshold (float): The threshold for a latent to be considered |
| activated. Defaults to 1e-3. |
| |
| auxk (int | None): The number of dead latents with which to model the |
| reconstruction error. Defaults to 256. |
| |
| standardize (bool): Whether to standardize the inputs. Defaults to True. |
| """ |
|
|
| super().__init__( |
| d_in=d_in, |
| d_sae=d_sae, |
| hook_names=hook_names, |
| auxk=auxk, |
| dead_steps_threshold=dead_steps_threshold, |
| dead_threshold=dead_threshold, |
| standardize=standardize, |
| ) |
| |
| self.sparsity_coef = sparsity_coef |
|
|
| def encode( |
| self, inputs: torch.Tensor |
| ) -> tuple[torch.Tensor, TopK, TopK | None, Stats | None, torch.Tensor]: |
| |
| inputs = self.hook_sae_input(inputs) |
| |
| stats = None |
| if self.cfg.standardize: |
| inputs, stats = standardize(inputs) |
|
|
| hidden_pre = self.hook_sae_acts_pre(self.encoder.forward(inputs - self.pre_encoder_bias)) |
| latents = self.hook_sae_acts_post(torch.relu(hidden_pre)) |
|
|
| |
| mask = latents > self.cfg.dead_threshold |
| max_k = torch.max(torch.sum(mask, dim=-1)).item() |
| values, indices = torch.topk( |
| latents, |
| k=max_k, |
| sorted=False |
| ) |
| topk = TopK(values, indices) |
|
|
| self.update_last_nonzero(topk, inputs.device) |
| dead, auxk = self.compute_dead_latents_and_auxk(hidden_pre) |
|
|
| return latents, topk, auxk, stats, dead |
|
|
| def decode(self, latents: torch.Tensor, stats: Stats | None = None) -> torch.Tensor: |
| recons = (latents @ self.decoder.weight.T) + self.pre_encoder_bias |
| if stats is not None: |
| recons = recons * stats.std + stats.mean |
| return self.hook_sae_recons(recons) |
|
|
| def forward_training(self, inputs: torch.Tensor) -> SAEOut: |
| latents, topk, auxk, stats, dead = self.encode(inputs) |
|
|
| recons = self.decode(latents, stats) |
| |
| sparsity_loss = torch.abs(topk.values).sum(dim=-1).mean() * self.sparsity_coef |
| |
| auxk_recons = None |
| if auxk is not None: |
| auxk_latents = torch.zeros_like(latents) |
| auxk_latents.scatter_( |
| dim=-1, |
| index=auxk.indices, |
| src=torch.relu(auxk.values), |
| ) |
| auxk_recons = self.decode(auxk_latents) |
|
|
| recons = self.hook_sae_output(recons) |
| return SAEOut(topk, recons, auxk, auxk_recons, dead, sparsity_loss) |