--- name: saelens description: Train sparse autoencoders to interpret model features. version: 1.0.1 author: Orchestra Research license: MIT dependencies: [sae-lens>=6.0.0, transformer-lens>=2.0.0, torch>=2.0.0] platforms: [linux, macos, windows] metadata: hermes: tags: [Sparse Autoencoders, SAE, Mechanistic Interpretability, Feature Discovery, Superposition] --- # SAELens: Sparse Autoencoders for Mechanistic Interpretability SAELens is the primary library for training and analyzing Sparse Autoencoders (SAEs) - a technique for decomposing polysemantic neural network activations into sparse, interpretable features. Based on Anthropic's groundbreaking research on monosemanticity. **GitHub**: [jbloomAus/SAELens](https://github.com/jbloomAus/SAELens) (1,100+ stars) ## The Problem: Polysemanticity & Superposition Individual neurons in neural networks are **polysemantic** - they activate in multiple, semantically distinct contexts. This happens because models use **superposition** to represent more features than they have neurons, making interpretability difficult. **SAEs solve this** by decomposing dense activations into sparse, monosemantic features - typically only a small number of features activate for any given input, and each feature corresponds to an interpretable concept. ## When to Use SAELens **Use SAELens when you need to:** - Discover interpretable features in model activations - Understand what concepts a model has learned - Study superposition and feature geometry - Perform feature-based steering or ablation - Analyze safety-relevant features (deception, bias, harmful content) **Consider alternatives when:** - You need basic activation analysis → Use **TransformerLens** directly - You want causal intervention experiments → Use **pyvene** or **TransformerLens** - You need production steering → Consider direct activation engineering ## Installation ```bash pip install sae-lens ``` Requirements: Python 3.10+, transformer-lens>=2.0.0 ## Core Concepts ### What SAEs Learn SAEs are trained to reconstruct model activations through a sparse bottleneck: ``` Input Activation → Encoder → Sparse Features → Decoder → Reconstructed Activation (d_model) ↓ (d_sae >> d_model) ↓ (d_model) sparsity reconstruction penalty loss ``` **Loss Function**: `MSE(original, reconstructed) + L1_coefficient × L1(features)` ### Key Validation (Anthropic Research) In "Towards Monosemanticity", human evaluators found **70% of SAE features genuinely interpretable**. Features discovered include: - DNA sequences, legal language, HTTP requests - Hebrew text, nutrition statements, code syntax - Sentiment, named entities, grammatical structures ## Workflow 1: Loading and Analyzing Pre-trained SAEs ### Step-by-Step ```python from transformer_lens import HookedTransformer from sae_lens import SAE # 1. Load model and pre-trained SAE model = HookedTransformer.from_pretrained("gpt2-small", device="cuda") # In sae-lens v6, SAE.from_pretrained() returns JUST the SAE (not a tuple). sae = SAE.from_pretrained( release="gpt2-small-res-jb", sae_id="blocks.8.hook_resid_pre", device="cuda" ) # If you also need the cfg dict and feature sparsity, use: # sae, cfg_dict, sparsity = SAE.from_pretrained_with_cfg_and_sparsity(...) # 2. Get model activations tokens = model.to_tokens("The capital of France is Paris") _, cache = model.run_with_cache(tokens) activations = cache["resid_pre", 8] # [batch, pos, d_model] # 3. Encode to SAE features sae_features = sae.encode(activations) # [batch, pos, d_sae] print(f"Active features: {(sae_features > 0).sum()}") # 4. Find top features for each position for pos in range(tokens.shape[1]): top_features = sae_features[0, pos].topk(5) token = model.to_str_tokens(tokens[0, pos:pos+1])[0] print(f"Token '{token}': features {top_features.indices.tolist()}") # 5. Reconstruct activations reconstructed = sae.decode(sae_features) reconstruction_error = (activations - reconstructed).norm() ``` ### Available Pre-trained SAEs | Release | Model | Layers | |---------|-------|--------| | `gpt2-small-res-jb` | GPT-2 Small | Multiple residual streams | | `gemma-2b-res` | Gemma 2B | Residual streams | | Various on HuggingFace | Search tag `saelens` | Various | ### Checklist - [ ] Load model with TransformerLens - [ ] Load matching SAE for target layer - [ ] Encode activations to sparse features - [ ] Identify top-activating features per token - [ ] Validate reconstruction quality ## Workflow 2: Training a Custom SAE ### Step-by-Step ```python from sae_lens import ( LanguageModelSAETrainingRunner, LanguageModelSAERunnerConfig, StandardTrainingSAEConfig, LoggingConfig, ) # 1. Configure training (v6 uses a NESTED config: SAE-specific options live in a # `sae=` sub-config, and logging options live in a `logger=` sub-config). # Note: `architecture`, `d_sae`, `l1_coefficient` etc. are now on the SAE sub-config, # and legacy flat options like `hook_layer`, `activation_fn`, `log_to_wandb` were removed. cfg = LanguageModelSAERunnerConfig( # SAE architecture + sparsity (nested) sae=StandardTrainingSAEConfig( d_in=768, # Model dimension d_sae=768 * 8, # Expansion factor of 8 l1_coefficient=8e-5, # Sparsity penalty apply_b_dec_to_input=True, normalize_activations="expected_average_only_in", ), # Data-generating function (model + hook point) model_name="gpt2-small", hook_name="blocks.8.hook_resid_pre", # layer is inferred from hook_name (no hook_layer) # Training lr=4e-4, l1_warm_up_steps=1000, train_batch_size_tokens=4096, training_tokens=100_000_000, # Data dataset_path="monology/pile-uncopyrighted", context_size=128, # Logging (nested) logger=LoggingConfig( log_to_wandb=True, wandb_project="sae-training", ), # Checkpointing checkpoint_path="checkpoints", n_checkpoints=5, ) # 2. Train trainer = LanguageModelSAETrainingRunner(cfg) # SAETrainingRunner still works as an alias sae = trainer.run() # 3. Evaluate print(f"L0 (avg active features): {trainer.metrics['l0']}") print(f"CE Loss Recovered: {trainer.metrics['ce_loss_score']}") ``` > **v6 migration note:** For other SAE types swap the `sae=` sub-config — > `GatedTrainingSAEConfig`, `TopKTrainingSAEConfig` (set `k` directly), or > `JumpReLUTrainingSAEConfig` (uses `l0_coefficient`). Legacy flat options > (`architecture`, `expansion_factor`, `hook_layer`, `activation_fn`/`activation_fn_kwargs`, > `use_ghost_grads`, ghost grads, b_dec/decoder init options) were removed in v6. ### Key Hyperparameters | Parameter | Typical Value | Effect | |-----------|---------------|--------| | `d_sae` | 4-16× d_model | More features, higher capacity | | `l1_coefficient` | 5e-5 to 1e-4 | Higher = sparser, less accurate | | `lr` | 1e-4 to 1e-3 | Standard optimizer LR | | `l1_warm_up_steps` | 500-2000 | Prevents early feature death | ### Evaluation Metrics | Metric | Target | Meaning | |--------|--------|---------| | **L0** | 50-200 | Average active features per token | | **CE Loss Score** | 80-95% | Cross-entropy recovered vs original | | **Dead Features** | <5% | Features that never activate | | **Explained Variance** | >90% | Reconstruction quality | ### Checklist - [ ] Choose target layer and hook point - [ ] Set expansion factor (d_sae = 4-16× d_model) - [ ] Tune L1 coefficient for desired sparsity - [ ] Enable L1 warm-up to prevent dead features - [ ] Monitor metrics during training (W&B) - [ ] Validate L0 and CE loss recovery - [ ] Check dead feature ratio ## Workflow 3: Feature Analysis and Steering ### Analyzing Individual Features ```python from transformer_lens import HookedTransformer from sae_lens import SAE import torch model = HookedTransformer.from_pretrained("gpt2-small", device="cuda") sae = SAE.from_pretrained( # v6 returns just the SAE release="gpt2-small-res-jb", sae_id="blocks.8.hook_resid_pre", device="cuda" ) # Find what activates a specific feature feature_idx = 1234 test_texts = [ "The scientist conducted an experiment", "I love chocolate cake", "The code compiles successfully", "Paris is beautiful in spring", ] for text in test_texts: tokens = model.to_tokens(text) _, cache = model.run_with_cache(tokens) features = sae.encode(cache["resid_pre", 8]) activation = features[0, :, feature_idx].max().item() print(f"{activation:.3f}: {text}") ``` ### Feature Steering ```python def steer_with_feature(model, sae, prompt, feature_idx, strength=5.0): """Add SAE feature direction to residual stream.""" tokens = model.to_tokens(prompt) # Get feature direction from decoder feature_direction = sae.W_dec[feature_idx] # [d_model] def steering_hook(activation, hook): # Add scaled feature direction at all positions activation += strength * feature_direction return activation # Generate with steering output = model.generate( tokens, max_new_tokens=50, fwd_hooks=[("blocks.8.hook_resid_pre", steering_hook)] ) return model.to_string(output[0]) ``` ### Feature Attribution ```python # Which features most affect a specific output? tokens = model.to_tokens("The capital of France is") _, cache = model.run_with_cache(tokens) # Get features at final position features = sae.encode(cache["resid_pre", 8])[0, -1] # [d_sae] # Get logit attribution per feature # Feature contribution = feature_activation × decoder_weight × unembedding W_dec = sae.W_dec # [d_sae, d_model] W_U = model.W_U # [d_model, vocab] # Contribution to "Paris" logit paris_token = model.to_single_token(" Paris") feature_contributions = features * (W_dec @ W_U[:, paris_token]) top_features = feature_contributions.topk(10) print("Top features for 'Paris' prediction:") for idx, val in zip(top_features.indices, top_features.values): print(f" Feature {idx.item()}: {val.item():.3f}") ``` ## Common Issues & Solutions > All examples below use the v6 nested config: SAE-specific options go in the `sae=` > sub-config (`StandardTrainingSAEConfig` / `TopKTrainingSAEConfig` / etc.), training > knobs stay on the top-level `LanguageModelSAERunnerConfig`. ### Issue: High dead feature ratio ```python from sae_lens import LanguageModelSAERunnerConfig, StandardTrainingSAEConfig # WRONG: no warm-up, features die early cfg = LanguageModelSAERunnerConfig( sae=StandardTrainingSAEConfig(d_in=768, d_sae=768*8, l1_coefficient=1e-4), l1_warm_up_steps=0, # Bad! ) # RIGHT: warm up the L1 penalty (v6 removed ghost grads; warm-up is the lever now) cfg = LanguageModelSAERunnerConfig( sae=StandardTrainingSAEConfig(d_in=768, d_sae=768*8, l1_coefficient=8e-5), l1_warm_up_steps=1000, # Gradually increase ) ``` ### Issue: Poor reconstruction (low CE recovery) ```python # Reduce sparsity penalty and/or add capacity (both on the SAE sub-config) cfg = LanguageModelSAERunnerConfig( sae=StandardTrainingSAEConfig( d_in=768, d_sae=768 * 16, # More capacity l1_coefficient=5e-5, # Lower = better reconstruction ), ) ``` ### Issue: Features not interpretable ```python from sae_lens import LanguageModelSAERunnerConfig, StandardTrainingSAEConfig, TopKTrainingSAEConfig # Increase sparsity (higher L1) cfg = LanguageModelSAERunnerConfig( sae=StandardTrainingSAEConfig(d_in=768, d_sae=768*8, l1_coefficient=1e-4), ) # Or use a TopK SAE (k is set directly in v6, not via activation_fn_kwargs) cfg = LanguageModelSAERunnerConfig( sae=TopKTrainingSAEConfig(d_in=768, d_sae=768*8, k=50), # Exactly 50 active features ) ``` ### Issue: Memory errors during training ```python cfg = LanguageModelSAERunnerConfig( sae=StandardTrainingSAEConfig(d_in=768, d_sae=768*8, l1_coefficient=8e-5), train_batch_size_tokens=2048, # Reduce batch size store_batch_size_prompts=4, # Fewer prompts in buffer n_batches_in_buffer=8, # Smaller activation buffer ) ``` ## Integration with Neuronpedia Browse pre-trained SAE features at [neuronpedia.org](https://neuronpedia.org): ```python # Features are indexed by SAE ID # Example: gpt2-small layer 8 feature 1234 # → neuronpedia.org/gpt2-small/8-res-jb/1234 ``` ## Key Classes Reference | Class | Purpose | |-------|---------| | `SAE` | Sparse Autoencoder model | | `LanguageModelSAERunnerConfig` | Top-level training configuration (nests `sae=` and `logger=`) | | `StandardTrainingSAEConfig` / `TopKTrainingSAEConfig` / `GatedTrainingSAEConfig` / `JumpReLUTrainingSAEConfig` | SAE-type-specific sub-configs (v6) | | `LoggingConfig` | Logging/W&B sub-config (v6) | | `LanguageModelSAETrainingRunner` | Training loop manager (alias: `SAETrainingRunner`) | | `ActivationsStore` | Activation collection and batching | | `HookedSAETransformer` | TransformerLens + SAE integration | ## Reference Documentation For detailed API documentation, tutorials, and advanced usage, see the `references/` folder: | File | Contents | |------|----------| | [references/README.md](references/README.md) | Overview and quick start guide | | [references/api.md](references/api.md) | Complete API reference for SAE, TrainingSAE, configurations | | [references/tutorials.md](references/tutorials.md) | Step-by-step tutorials for training, analysis, steering | ## External Resources ### Tutorials - [Basic Loading & Analysis](https://github.com/jbloomAus/SAELens/blob/main/tutorials/basic_loading_and_analysing.ipynb) - [Training a Sparse Autoencoder](https://github.com/jbloomAus/SAELens/blob/main/tutorials/training_a_sparse_autoencoder.ipynb) - [ARENA SAE Curriculum](https://www.lesswrong.com/posts/LnHowHgmrMbWtpkxx/intro-to-superposition-and-sparse-autoencoders-colab) ### Papers - [Towards Monosemanticity](https://transformer-circuits.pub/2023/monosemantic-features) - Anthropic (2023) - [Scaling Monosemanticity](https://transformer-circuits.pub/2024/scaling-monosemanticity/) - Anthropic (2024) - [Sparse Autoencoders Find Highly Interpretable Features](https://arxiv.org/abs/2309.08600) - Cunningham et al. (ICLR 2024) ### Official Documentation - [SAELens Docs](https://jbloomaus.github.io/SAELens/) - [Neuronpedia](https://neuronpedia.org) - Feature browser ## SAE Architectures | Architecture | Description | Use Case | |--------------|-------------|----------| | **Standard** | ReLU + L1 penalty | General purpose | | **Gated** | Learned gating mechanism | Better sparsity control | | **TopK** | Exactly K active features | Consistent sparsity | ```python from sae_lens import LanguageModelSAERunnerConfig, TopKTrainingSAEConfig # TopK SAE (exactly 50 features active) — `k` is set on the SAE sub-config in v6 cfg = LanguageModelSAERunnerConfig( sae=TopKTrainingSAEConfig(d_in=768, d_sae=768*8, k=50), ) ```