| import torch |
| from torch import Tensor |
|
|
| from .decoder import decode |
| from .types import Stats, TopK, SAEOut |
| from .Standard import SAE_Template, SAEConfig |
| from .SAE_Wrapper import _disable_hooks |
| from typing import Any, Tuple, Dict, List, Optional |
|
|
| from .Utils import standardize, unit_norm_decoder |
|
|
| class BatchTopKSAE(SAE_Template): |
| threshold: torch.Tensor |
| """The inference threshold for JumpReLU activation function.""" |
|
|
| def __init__( |
| self, |
| d_in: int, |
| d_sae: int, |
| hook_names: list[str], |
| k: int, |
| dead_steps_threshold: int, |
| dead_threshold: float = 1e-3, |
| auxk: int | None = 256, |
| standardize: bool = True, |
| threshold_beta: float = 0.99, |
| **kwargs, |
| ) -> None: |
| """ |
| Args: |
| d_in (int): The number of inputs. |
| |
| d_sae (int): The number of latents. |
| |
| k (int): The number of largest latents to keep. |
| |
| 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.register_buffer("threshold", torch.tensor(dead_threshold, dtype=torch.float32)) |
| self.threshold_beta = threshold_beta |
|
|
| self.k = k |
|
|
| def encode( |
| self, inputs: torch.Tensor, use_threshold: bool = True, |
| ) -> tuple[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: Tensor = self.hook_sae_acts_pre(self.encoder.forward(inputs - self.pre_encoder_bias)) |
|
|
| if use_threshold: |
| latents = self.hook_sae_acts_post(hidden_pre * (torch.relu(hidden_pre) > self.threshold)) |
| |
| else: |
| |
| num_toks = hidden_pre.numel() // self.cfg.d_sae |
| flatten_values, flatten_indices = torch.topk( |
| torch.relu(hidden_pre.flatten()), |
| k=self.k * num_toks, |
| sorted=False |
| ) |
| |
| latents = torch.zeros_like(hidden_pre.flatten()) |
| latents = self.hook_sae_acts_post( |
| latents.scatter_(-1, flatten_indices, flatten_values).reshape(hidden_pre.shape) |
| ) |
| self.update_threshold(latents) |
| |
| max_k = torch.max(torch.sum(latents > self.cfg.dead_threshold, 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: 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, use_threshold=False) |
|
|
| recons = self.decode(latents, stats) |
|
|
| auxk_recons = None |
| if auxk is not None: |
| auxk_latents = torch.zeros_like(latents) |
| auxk_latents.scatter_( |
| -1, |
| auxk.indices, |
| torch.relu(auxk.values), |
| ) |
| auxk_recons = self.decode(auxk_latents) |
|
|
| return SAEOut(topk, recons, auxk, auxk_recons, dead, 0) |
| |
| def update_threshold(self, latents: Tensor): |
| device_type = "cuda" if latents.is_cuda else "cpu" |
| with torch.autocast(device_type=device_type, enabled=False), torch.no_grad(): |
| active = latents[latents > self.cfg.dead_threshold] |
|
|
| if active.size(0) == 0: |
| min_activation = self.cfg.dead_threshold |
| else: |
| min_activation = active.min().detach().to(dtype=torch.float32) |
|
|
| self.threshold = (self.threshold_beta * self.threshold) + ( |
| (1 - self.threshold_beta) * min_activation |
| ) |
| |
| 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 |