A sparse autoencoder (SAE) is a tool for finding the variables a trained network computes with. It takes the network's internal activation vectors, one per token, and rewrites each one as a short sum of directions taken from a long learned list. It needs no labels. Each direction on the list is a candidate feature, a concept the network may represent, such as "this text is base64" or "this is the Golden Gate Bridge". SAEs are the main unsupervised tool of interpretability today. They have been trained with 34 million directions on Claude 3 Sonnet (Templeton et al. 2024) and 16 million on GPT-4 (Gao et al. 2024), and they are the building block of the attribution graphs of §23.8.
The need for such a tool comes from §23.5. A layer with d coordinates can store far more than d features if each feature is rarely active. It gives every feature its own direction, lets the directions overlap a little, and tolerates the small errors the overlap causes. This packing is called superposition. Its price is that single neurons mix unrelated concepts, which is called being polysemantic. One neuron in a one-layer Transformer studied by Bricken et al. (2023) responds to academic citations, English dialogue, HTTP requests, and Korean text. Reading neurons one at a time cannot untangle that. The features are directions, there are more of them than coordinates, and nobody told us which directions they are.
So the question for this section is concrete. Suppose a 4,096-dimensional activation is a sparse sum of hundreds of thousands of feature directions. Can we learn those directions from the activations alone? We answer in steps. We state the underlying problem, called dictionary learning, turn it into a network, and show by hand that its usual penalty biases every answer downward. Then we train SAEs from scratch on data whose true features we planted, remove the bias with newer rules, keep the SAE's units from going permanently silent, and measure the result honestly. The last parts cover how dictionaries fail, two relatives called transcoders and crosscoders, the results at frontier scale, and the 2025 evidence on what SAEs are and are not good for.
Keep in mind what an SAE can prove. Section 23.1 set out a ladder of evidence with four rungs, association (some quantity tracks a concept), direct effect, causal effect, and finally mechanism. An SAE on its own sits on the first rung. It proposes candidate variables, directions whose activations track concepts in the input. Only interventions on those variables, like the patching of §23.4, can show that the model uses them. As in the rest of this chapter, vectors are rows and a matrix acts on the right, xW.
Dictionary learning is the problem underneath
Start with a case small enough to check by hand. A layer has two coordinates but stores three features. Their unit directions are g1=(1,0), g2=(−21,23), and g3=(−21,−23), spaced 120 degrees apart. On one token, feature 1 is on with strength 1.0 and feature 2 with strength 0.5. The activation is x=g1+0.5g2=(0.75,0.433).
Python · cell 1
That is the whole section in miniature. With more features than coordinates, many codes rebuild an activation equally well, and a preference for sparse codes chooses among them.
Now the general case. This chapter's synthetic data uses planted features. An activation is x=∑i=1naigi+ϵ∈Rd, where the gi are n>d unit feature directions. Each coefficient ai≥0 is zero with probability S, called the sparsity, and ϵ is small noise. Section 23.5, which wrote m for the width, showed two things about such data. A model trained on it stores the features in superposition. And a sparse code can be recovered from fewer measurements than its length, which is the subject of compressed sensing. That recovery assumed the directions were known. Here we must learn them too.
In plain words, write every activation as a short nonnegative recipe over a shared set of ingredient directions. Choose the ingredients so that every recipe is both short and accurate. The L1 term is the sum of the entries, the quantity the hand example minimized.
Three facts about this problem shape everything that follows.
For a fixed dictionary, finding one token's code is a convex problem. It is a least-squares fit with an L1 penalty and nonnegative entries, called a nonnegative lasso. The iterative shrinkage-thresholding algorithm (ISTA) solves it by alternating a gradient step on the squared error with a step that pulls every entry toward zero. Exercise 23.5d implements ISTA for codes of either sign, and the nonnegative version only adds a clip at zero.
Jointly in the dictionary and the codes, the problem is not convex. Training finds a local optimum that depends on where it starts.
The answer is defined only up to the order of the dictionary entries and their scale, since fidi=(fi/c)(cdi) for any c>0. The unit-norm constraint removes the scale ambiguity, and we treat the dictionary as an unordered set.
The idea is old. Olshausen and Field (1996) learned a sparse code for small patches of natural images. The dictionary entries came out looking like the oriented, localized edge detectors that simple cells in the visual cortex implement. Nobody had told the algorithm about edges. Sparsity alone, applied to the right data, recovered the variables the data was built from. Interpretability bets that the same will happen with a network's activations. Classic sparse coding had already been run on the hidden states of BERT (Yun et al. 2021). In 2023 two groups showed, at about the same time, that sparse autoencoders find interpretable features in language models, Bricken et al. and Cunningham et al. (2023).
An interviewer may ask why sparsity is the right prior. The hand example is the answer. With more features than dimensions, reconstruction alone cannot choose a code. The assumption that each feature is rarely active is what makes the problem well posed.
An autoencoder predicts the code in one step
Solving a lasso for every token is expensive. ISTA needs tens to hundreds of iterations, each costing two products with the F×d dictionary, and an SAE at frontier scale is trained on billions of tokens. The fix is amortization. Instead of optimizing each token's code separately, learn a function that predicts the code in one matrix product.
That function is the encoder of an autoencoder. An autoencoder is a network with two halves. The encoder maps an input to a code. The decoder maps the code back to a reconstruction of the input. Training makes the reconstruction match the input, and §32.1 develops the idea in full. An ordinary autoencoder squeezes the input through a code smaller than the input. Ours is wider than the input, F≫d, and what keeps it from copying the input is sparsity, not width. A vector-quantized autoencoder, which replaces each code by the nearest entry of a learned codebook, is the extreme case in which every code has exactly one nonzero entry, and that entry is 1.
The sparse autoencoder
In plain words, center the activation, then read off how strongly each of F detectors responds. That response, before the ReLU, is the pre-activation. Keep only the positive responses, and rebuild the activation as a weighted sum of the latents' directions. Pay for errors with the squared distance, and for every unit of activation with λ.
Each piece has a reason.
Subtracting bdec before encoding makes the latents describe how an activation differs from a typical one. Bricken et al. found that this pre-encoder bias improved results in toy models. They start it at the geometric median of the dataset, the point with the smallest total distance to all the activations.
The ReLU makes activations nonnegative, so a latent means "this feature is present, this much".
The encoder bias benc, usually negative, acts as a threshold. It keeps a latent silent until its detector's response is large enough.
The fixed rescaling makes λ mean the same thing across models and layers, whose raw activation sizes differ by orders of magnitude.
Two words need care. A latent is a unit of the SAE, one entry of f. A feature is a variable the model is hypothesized to compute. The whole enterprise is the hope that latents are features, and much of this section is about when they are not. Papers often say "feature" for both.
The encoder and the decoder do different jobs, so they are kept as separate matrices. The decoder row di is where the feature points. The encoder column is a detector. A good detector for a feature in superposition must respond to di while ignoring the neighboring directions that overlap with it. Only if the dictionary were orthonormal would the best detector be di itself. Bricken et al. found that the learned encoder columns are often far from their decoder rows, and that leaving the two untied matters for capacity. In the runs below, an encoder column ends up on average at a cosine of about 0.9 with its decoder row, not 1.
In practice, the first thing anyone reports about an SAE is three numbers. They are how many latents fire per token, how much of the activation the SAE fails to explain, and how many latents never fire at all. The first exercise computes them.
Warm-up
SAE forward pass and the metrics you report every time
Implement the forward pass of the definition box and the three numbers every SAE paper reports.
sae_forward(X, W_enc, b_enc, W_dec, b_dec) takes a batch X of shape (N, d). Subtract b_dec, multiply by the encoder, add its bias, apply the ReLU, and decode. Return (f, X_tilde) with shapes (N, F) and (N, d).
sae_metrics(X, f, X_tilde) returns a tuple (l0, fvu, dead_frac).
l0 is the mean number of active (positive) latents per row.
fvu is the fraction of variance unexplained, ∑t∥xt−x~t∥2/∑t∥xt−xˉ∥2, with xˉ the mean row of X.
dead_frac is the fraction of latents that are zero on every row.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
An L1 penalty shrinks every activation
The L1 penalty charges for every unit of activation. So the cheapest answer under-reports every feature a little. This bias is called shrinkage. Start with the smallest possible case, by hand.
With an orthonormal dictionary the same calculation holds for every latent at once.
∂ Proof of the soft threshold
Write y=x−bdec. Expanding the square and using di⋅dj=1[i=j],
Adding the penalty, the objective is ∥y∥2+∑ig(fi) with g(f)=f2−2aif+λf. It separates, so each fi can be minimized alone over fi≥0. The function g is a convex parabola with its vertex at f=ai−λ/2. If ai>λ/2, the vertex is feasible and is the minimizer. If ai≤λ/2, then g′(f)=2f−2ai+λ≥0 for all f≥0. So g never decreases on the feasible set, and the minimizer is f=0. Both cases are max(0,ai−λ/2).
Finally, with Wenc=Wdec⊤ and benc=−2λ1, the pre-activation is zi=y⋅di−λ/2=ai−λ/2, and the ReLU returns max(0,ai−λ/2). ∎
In plain words, an L1 SAE with an orthonormal dictionary is exactly the lasso, solved in one step. It zeroes every projection below λ/2 and subtracts λ/2 from every projection above it. The first part is the sparsity we wanted. The second part is a systematic bias we did not want. The cell checks the formula against a numerical minimizer.
Python · cell 2
In superposition the dictionary is not orthonormal, and shrinkage gets worse for a second reason. When feature j is active, its overlap with di leaks into latent i's pre-activation. This leak is called interference, and its size is about ajdi⋅dj. To stay silent on interference, latent i needs a threshold, a negative encoder bias benc,i. But in ReLU(zi) the same bias also sets the zero from which the magnitude is measured. So a threshold high enough to reject interference is subtracted from every genuine activation too. One parameter is doing two jobs. It decides whether a feature is present, and it sets how large the feature is reported to be. Rajamanoharan et al. (2024a) put the problem in exactly these terms, and every architecture after the plain ReLU SAE is a way to separate the two jobs.
In practice, an L1 SAE reports every feature as weaker than it is. If you use a latent's activation to measure how strongly a concept is present, that measurement is biased low. "Write the SAE loss" is a common interview question, and "what is shrinkage?" is almost always the follow-up.
The penalty must carry the decoder norm
The penalty in the definition box is λ∑ifi∥di∥, not the plain λ∑ifi. The reason is a loophole, and numbers show it best. Take a latent whose activation is 0.8 and whose decoder row has norm 1, so its plain penalty is 0.8λ. Divide its encoder column and bias by 10, and multiply its decoder row by 10. A ReLU of a number divided by 10 is the ReLU divided by 10, so the activation becomes 0.08. The reconstruction 0.08×10di is the same vector as before. But the plain penalty has fallen to 0.08λ.
Repeat with any factor c>1, and the plain penalty falls to λfi/c at no cost in reconstruction. Left alone, gradient descent will grow the decoder norms to dodge the penalty. The L1 term then measures nothing but the arbitrary scale of the decoder. The cell runs the loophole on one random latent.
Python · cell 3
There are two standard fixes, and they agree whenever the norms are 1. Bricken et al. keep the plain penalty and constrain ∥di∥=1, renormalizing each decoder row after every optimizer step. Templeton et al. drop the constraint and put the norm into the penalty. That makes fi∥di∥ the natural "feature activation" and di/∥di∥ the feature direction.
The constraint needs one more detail. The part of a row's gradient that points along the row only changes the row's length, and the renormalization immediately undoes that. The Adam optimizer (§0.6), which scales each step by running averages of past gradients, would still fold that wasted part into its averages. So Bricken et al. remove it before the step,
gi←gi−(gi⋅di)di,
where gi is the gradient of the loss with respect to di. In plain words, keep only the part of the gradient that turns the row, and drop the part that stretches it. Geometrically, this projects the gradient onto the plane tangent to the unit sphere at di. Bricken et al. report a small but real reduction in total loss from it, and Gao et al. use it too.
∂ Gradients of the L1 SAE loss for one batch
Take a batch X∈RB×d and the batch-mean loss L=B1∑t(∥x~t−xt∥2+λ∑ifti∥di∥). Write Y=X−1bdec, Z=YWenc+1benc, F=ReLU(Z), R=FWdec+1bdec−X, and n∈RF for the vector of decoder norms, where 1 is the B×1 column of ones that copies a bias to every row. Work backward with the matrix rules of §0.2.
The reconstruction term gives ∂L/∂X~=G:=B2R∈RB×d.
Through X~=FWdec+1bdec, the decoder gets F⊤G from reconstruction. From the penalty it gets ∂/∂di of Bλ(∑tfti)∥di∥, which is Bλ(∑tfti)di/∥di∥. So row i of ∂L/∂Wdec is (F⊤G)i+Bλ(∑tfti)di/∥di∥. The penalty part is parallel to di, so the tangent projection removes it when the norms are held at 1.
The latents get ∂L/∂F=GWdec⊤+Bλ1n⊤. The ReLU passes it only where the pre-activation is positive, ∂L/∂Z=(GWdec⊤+Bλ1n⊤)⊙1[Z>0]. Call it GZ.
The encoder gets ∂L/∂Wenc=Y⊤GZ and ∂L/∂benc=1⊤GZ.
The decoder bias appears twice, added after decoding and subtracted before encoding, so ∂L/∂bdec=1⊤G−1⊤GZWenc⊤.
Step 5 is the one people forget. Step 3 explains how dead latents are born. A latent whose pre-activation is negative on the whole batch has a zero column in GZ, so its encoder receives no gradient at all.
Training an SAE on planted features
On synthetic data we know the right answer, so we can train SAEs and grade them. The data has n=128 planted features in d=32 dimensions, four features per dimension. Each feature is on with probability 1−S=0.015, and when on, its coefficient is uniform on [0.5,1.5]. A token has 128×0.015=1.92 active features in expectation, and 1.95 in this sample.
Python · cell 4
The trainer below implements the derivation line by line, with unit-norm decoder rows, the tangent projection, and Adam. It also supports a second rule, set by rule="topk". That rule keeps the k largest pre-activations of each token and uses no penalty. It is called TopK and is defined properly in the next section, but having it here lets us compare the two on the same data. As in the equation of Gao et al., the TopK encoder has no bias.
Python · cell 5
We grade a dictionary in six ways.
L0 and FVU are the usual sparsity and fidelity numbers. FVU, the fraction of variance unexplained, is the squared reconstruction error divided by the total variance of the data.
Recovery is the mean, over the 128 true features, of the largest cosine between the feature's direction and any decoder row. A score of 1 means every true feature has a latent pointing exactly along it.
Shrinkage is measured on the tokens that contain exactly one true feature. It is the mean ratio of the best-matching latent's activation to the true coefficient, so a ratio of 1 means unbiased.
Dead is the fraction of latents that never fire on the data.
Detector alignment is the mean cosine between each latent's encoder column and its decoder row.
Python · cell 6
Even TopK reports only 0.90 of the true value on single-feature tokens, and TopK has no penalty. The likely reason is that the encoder learns to discount every reading a little. A reading of feature i usually also contains interference from other active features. For a reading a+e with independent zero-mean noise e, the gain c that minimizes E(c(a+e)−a)2 is Ea2/(Ea2+Ee2), which is below 1. A noisy measurement is best shrunk a little before you use it.
The widget lets you watch training with every direction visible. Five features live in a plane, arranged as the pentagon that §23.5 showed a toy model building, so the superposition is extreme. You choose the rule, the penalty or k, and the number of latents F. Every change restarts training, and Train again starts over from a new random initialization.
Can a sparse autoencoder recover features that are packed in superposition?
Five features share a plane, each on 4 percent of the time. Each arrow is one latent’s decoder direction.
Loading interactive figure…
Core
Train an L1 SAE by hand
Train an L1 SAE with hand-written gradients, following the derivation box on gradients.
sae_l1_grads(X, params, lam) computes the batch-mean loss B1∑t(∥xt−x~t∥2+λ∑ifti∥di∥) and its gradients. params is a dict with W_enc (d, F), b_enc (F,), W_dec (F, d), and b_dec (d,). Return (loss, grads), where grads has the same keys and shapes. Do not assume the decoder rows have unit norm.
train_sae(X, F, lam, steps, lr, rng, B=256) trains an SAE and returns params.
Draw W_dec = rng.normal(size=(F, d)) and scale each row to unit norm. Set W_enc to a copy of W_dec.T, b_enc to zeros, and b_dec to the mean row of X.
At each step, draw the batch X[rng.integers(0, N, B)] and compute the gradients at lam.
Remove from each decoder row's gradient its component along that row.
Update with the provided adam_step(params, grads, state, lr), starting from an empty dict state, then renormalize the decoder rows to unit norm.
Use the random generator only for these two draws, in this order, so that your run can be compared step by step with a reference.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
TopK, JumpReLU, and gates separate detection from size
The fixes for shrinkage all attack the same coupling, one parameter deciding both whether a latent fires and how large it is. They do it in three ways. TopK fixes how many latents may fire. JumpReLU learns a separate firing threshold for each latent. Gated SAEs give detection its own path through the network.
TopK sidesteps shrinkage because nothing penalizes magnitude, so the kept values pass through unchanged. The gradient flows only through the k selected latents. The selection itself is treated as fixed, the same way backpropagation through a maximum sends gradient only to the entry that won. Gao et al. report a better trade-off between reconstruction and sparsity than ReLU SAEs, and clean scaling laws in F and k. They also note a practical advantage. L0 becomes a number you set, not one you discover after a sweep over λ.
The drawback is rigidity. Every token gets k latents whether it needs them or not, the beginning-of-sequence token included. And an SAE trained at one k reconstructs substantially worse when run at a much larger one. BatchTopK relaxes the first problem. Bussmann et al. found that their BatchTopK SAEs used one latent on some tokens and more than 80 on others. They reconstructed better than TopK at the same average L0 on GPT-2 Small and Gemma 2 2B.
Selecting the top k correctly, without loops and in both forms, is a common coding question.
Interview
TopK and BatchTopK encoders
Implement both TopK rules with no Python loops over rows.
topk_encode(Z, k) takes pre-activations Z of shape (B, F). In each row, keep the k largest entries (by value, not by absolute value) and set every other entry to 0. Then set to 0 any kept entry that is negative.
batch_topk_encode(Z, k) keeps the B * k largest entries of the whole matrix, with the same rule for negatives.
Both take an integer 1≤k≤F and return an array of the same shape as Z. You may assume the entries of Z are distinct.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
JumpReLU learns a threshold for each latent
The second fix gives each latent its own threshold and stops subtracting that threshold from the magnitude.
In plain words, the threshold decides whether the latent fires, and once it fires, its value is left alone. The L0 penalty counts active latents instead of charging for their size, which removes the incentive to shrink. For example, take θi=0.3. A pre-activation of 0.25 gives 0, and a pre-activation of 1.0 gives 1.0. A ReLU latent that used a bias of −0.3 as its threshold would turn the same response of 1.0 into 0.7.
The difficulty is that the loss is a step function of θi. Nudging a threshold changes nothing until some pre-activation crosses it. So the derivative with respect to θi is zero almost everywhere, and plain backpropagation never moves the thresholds. The way out is to remember what we are minimizing. It is an expected loss over the data, which is smooth in θi even though each sample's loss is not.
∂ The threshold gradient, through the expected loss
Fix one latent and drop the index. Let p be the probability density of its pre-activation z over the data. The expected L0 contribution is E1[z>θ]=Pr(z>θ)=∫θ∞p(u)du, so by the fundamental theorem of calculus
∂θ∂E1[z>θ]=−p(θ).
Raising the threshold switches off the tokens whose pre-activation sits right at it, at a rate equal to the density there. To estimate p(θ) from a batch z1,…,zB, use a kernel density estimate, which counts the samples near a point. With the rectangle kernel rect(u)=1[∣u∣<21] and bandwidth ε,
p^(θ)=Bε1b=1∑Brect(εzb−θ),
which counts the pre-activations within ε/2 of the threshold and divides by the window width. Rajamanoharan et al. implement this as a straight-through estimator. That is a rule that keeps the forward pass exact but, in the backward pass, replaces the zero derivative of the step by a pseudo-derivative,
∂θ∂1[zb>θ]⇝−ε1rect(εzb−θ),
whose batch mean is exactly −p^(θ). The same reasoning applies to f=z1[z>θ]. Its value jumps by θ when z crosses the threshold, which gives the pseudo-derivative −εθrect(εz−θ). With respect to z itself, f is treated as having derivative 1[z>θ], the ordinary gradient wherever the latent is on.
Putting the two together for a loss ℓrec(f)+λ∑i1[zi>θi], the batch threshold gradient is
For squared error, ∂ℓrec/∂fbi=−2di⋅(xb−x~b). The gradient then becomes the paper's estimator, Bε1∑b(2θidi⋅(xb−x~b)−λ)rect(⋅). Write Ibi=2θidi⋅(xb−x~b). It is, to first order, how much the squared error would grow if latent i switched off at the threshold and stopped writing θidi. A gradient step lowers θi when the borderline tokens have Ibi larger than λ on average, so that the latent is worth its cost there. Otherwise it raises θi. Only tokens within ε/2 of the threshold contribute. The paper uses ε=0.001 on activations rescaled to unit mean squared norm. Its training also subtracts bdec before the encoder, as in the SAE definition, and learns logθi so that the thresholds stay positive.
In the original paper, on Gemma 2 9B, JumpReLU SAEs reconstructed more faithfully than Gated SAEs and at least as well as TopK SAEs at the same sparsity, measured by the increase in the model's loss. They are the architecture of Gemma Scope, Google DeepMind's open suite of SAEs for its Gemma 2 models.
Two more designs complete the family. A Gated SAE (Rajamanoharan et al. 2024a) computes which latents are on in one path and how large they are in another, and puts the L1 penalty only on the first. A Matryoshka SAE (Bussmann et al. 2025) trains nested dictionaries so that the first few latents must reconstruct the input on their own, which pushes them toward general concepts. Matryoshka SAEs fight a failure called absorption that we meet below. The box has the details.
Research
JumpReLU and its straight-through threshold gradient
Implement the JumpReLU forward pass and the pseudo-gradient that trains its thresholds.
jumprelu_forward(Z, theta) returns fbi=zbi1[zbi>θi] for Z of shape (B, F) and theta of shape (F,).
jumprelu_theta_grad(Z, theta, eps, lam, G_f) returns the (F,) pseudo-gradient, with respect to theta, of the batch-mean loss B1∑b(ℓrec,b+λ∑i1[zbi>θi]). Here G_f[b, i] is ∂ℓrec,b/∂fbi, and the kernel is the rectangle of width 1, rect(u)=1[∣u∣<1/2].
The tests check two things. The L0 part must estimate the true derivative of the expected L0, −λp(θ), for Gaussian pre-activations. The reconstruction part must match the paper's form.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Compare SAEs at equal sparsity
Every SAE sits somewhere on a trade-off between sparsity (low L0) and fidelity (low FVU or low added loss), so no single number ranks two of them. Compare them at equal L0, or compare whole curves. The widget shows such curves for runs of each rule on the planted data of the cells above. The runs were trained when the book was written, with 3,000 Adam steps each, and the JumpReLU runs used the straight-through estimator just derived.
Before you look, predict two things. The data has 1.95 active features per token, so at an L0 of about 2, which rule should reconstruct best? And why should a rule that can vary its L0 from token to token have an edge over one that cannot? Then switch the dictionary from 128 to 512 latents and watch how many TopK latents are dead.
What does each sparsity rule buy on the sparsity and fidelity trade-off?
Each point is one SAE trained on the planted data above. Lower left is better. Click a point.
Loading interactive figure…
Rule
Sparsity control
Shrinkage
Training difficulty
Where it is used
ReLU with L1
λ, L0 found after training
strong
easy, but L0 needs a sweep
the early Anthropic dictionaries (Bricken et al., Templeton et al., the latter with the decoder-norm penalty)
TopK
k, exact per token
none from a penalty
easy, needs AuxK at scale
OpenAI's GPT-4 and GPT-2 SAEs (Gao et al.)
BatchTopK
k on average
none from a penalty
easy, threshold at inference
crosscoders and Matryoshka SAEs in 2025
JumpReLU
λ on L0
none from a penalty
straight-through estimator, bandwidth ε
Gemma Scope and Gemma Scope 2
Gated
λ on the gate only
small
extra decoder pass in training
the first DeepMind fix for shrinkage
Matryoshka
any of the above inside
as its inner rule
several reconstructions per step
dictionaries that resist absorption
In an interview, the expected summary is one sentence. L1 couples "is it on" with "how big is it", and every later rule separates the two, by fixing the count (TopK), by learning a threshold (JumpReLU), or by a separate gate (Gated).
Dead latents
A latent is dead if it never fires. The gradient derivation showed why that state is permanent. A ReLU latent whose pre-activation is negative on every token gets zero gradient in its encoder column. A TopK latent that is never among the top k is in the same position. Nothing ever moves it again. In practice a latent is declared dead if it has not fired on any of a long window of tokens, 107 in Templeton et al. and Gao et al.
At large scale the problem is severe. Templeton et al.'s dictionaries of about 1, 4, and 34 million features ended training with roughly 2, 35, and 65 percent dead. Gao et al. saw up to 90 percent dead in experiments without countermeasures. The opposite case also exists. Dense latents fire on a large fraction of tokens, 10 percent or more by the usual cutoff. They were long treated as training artifacts, but Sun et al. (2025) found that many track real properties, such as the position of a token in the context or the part of speech.
Four cures are in use.
Resampling (Bricken et al.). Every so often, find the latents that have not fired recently. Reset each one's decoder row to an input the SAE currently reconstructs badly, chosen with probability proportional to its squared error. Give it a small encoder norm, so the revived latent fires only weakly at first.
Tied initialization (Conerly et al. 2024, Gao et al.). Start the encoder at the transpose of the decoder, Wenc=Wdec⊤. Each latent then initially detects its own direction and fires on the inputs that point along it. The two matrices are trained separately afterward.
AuxK (Gao et al.). Add a loss that asks the dead latents to explain what the live ones missed, defined next.
Gentler optimization. A learning rate that is too high kills latents (Bricken et al.). Conerly et al. raise the L1 coefficient linearly from zero over the first 5 percent of training. Together with tied initialization and the decoder-norm penalty, that left under 1 percent of their latents dead, so they stopped resampling. An earlier idea, the "ghost grads" of Jermyn and Templeton (2024), gave dead latents a separate gradient signal, and was dropped two months later when it no longer lowered the loss (March 2024 update).
In plain words, the dead latents are given the job of modeling what the live ones missed. A dead latent whose direction would help gets gradient, and its pre-activation rises. Eventually it wins a place in the main top k and comes back to life. With tied initialization and AuxK, Gao et al.'s 16-million-latent SAE on GPT-4 ended with only 7 percent dead.
How much does initialization alone matter? The cell trains the TopK SAE from before again, with a random encoder instead of a tied one, and prints it next to the tied run.
Python · cell 7
With a random encoder, 28 percent of the latents never fire on the data. Recovery falls from 0.989 to 0.833, and FVU more than doubles, from 0.174 to 0.375. Only the initialization changed. A random encoder column is unrelated to its decoder row, so many latents never make it into the top k during the first steps, and once out they stay out. In practice, the dead fraction is the first number to check when a run looks worse than expected. "How do you prevent dead latents?" is a standard interview question.
Core
Dead latents and the AuxK loss
Implement the dead-latent mask and the AuxK loss of the definition box.
dead_mask(fire_counts) takes fire_counts of shape (W, F), the number of tokens each latent fired on in each of the last W training steps. Return a boolean (F,) array that is True for latents that fired on no token in the window.
auxk_loss(err, Z, W_dec, dead, k_aux) takes the main error err=X−X~ of shape (B, d), pre-activations Z of shape (B, F), the decoder W_dec of shape (F, d), and dead of shape (F,).
In each row, keep the k_aux largest pre-activations among dead latents only (or all dead latents, if fewer than k_aux are dead).
Apply a ReLU and decode with W_dec, with no bias, to get E~.
Return ∑t∥et−e~t∥2/∑t∥et−eˉ∥2, where eˉ is the mean row of err. You may assume the rows of err are not all equal.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Measuring whether the dictionary is any good
A real model has no ground truth, so evaluation is a set of partial views. These are the ones every SAE paper reports.
Each metric can be gamed, the lesson of §22.1 in a new setting. L0 and FVU say nothing about whether the model's computation survives the reconstruction, which is why the spliced-in loss matters. The residual stream is the running sum that every layer of a Transformer reads from and writes to (§5.1). Gemma Scope's evaluation found that SAEs on the residual stream have a consistently higher delta loss than SAEs on the outputs of the attention and MLP sublayers. (An MLP sublayer is the two-layer feed-forward network inside each Transformer block, §5.3.) Their FVUs are roughly comparable. The authors conjecture that even small errors in reconstructing the residual stream can have a large effect on the loss.
Loss recovered against zero ablation flatters an SAE. Zeroing a residual stream destroys the model, so the denominator is huge, and almost any reconstruction looks like it recovers most of the loss. Mean ablation, which replaces the activation by its average, is a much harder baseline. The torch block below shows the gap on a small model, and Exercise 23.6f asks you to compute it.
Gao et al. proposed a more honest reading. Express the spliced-in loss as the pretraining compute a model would need to reach the same loss. Their 16-million-latent SAE, spliced into GPT-4, gave a loss equal to that of a model trained with about 10 percent of GPT-4's pretraining compute. That is a sobering way to say how much of the model the dictionary misses.
Spliced-in loss on a small model
The torch block trains a small classifier that reads a 16-dimensional stream holding 64 sparse features in superposition. It then trains a TopK SAE with AuxK on the stream. On a real model, the PyTorch hooks of §0.9 would do the splicing. Here the model is two functions, so we call them directly. The block reports the SAE's own metrics, then the classifier's cross-entropy on the original stream, on the reconstruction, and with the stream zero-ablated and mean-ablated.
🔥 In PyTorchfor a machine with PyTorch
Core
Spliced-in loss, loss recovered, and compute equivalence
Compute the two headline numbers for an SAE from per-token log-probabilities.
You are given, for T held-out tokens, the natural-log probabilities of the correct next token under three models, each of shape (T,). They are lp_orig for the original model, lp_sae with the SAE spliced in, and lp_abl with the site ablated.
loss_recovered(lp_orig, lp_sae, lp_abl) returns the loss-recovered score of the definition box. Each cross-entropy is the mean negative log-probability.
compute_equivalent(ce, a, b, c) takes a fitted power law L(C)=c+aC−b (§9.1), in which C is training compute as a fraction of the original model's. It returns the C at which the law reaches the loss ce, which you may assume is above the floor c.
In a comment, say why the same SAE scores lower against mean ablation than against zero ablation.
import numpy as np
def loss_recovered(lp_orig, lp_sae, lp_abl):
raise NotImplementedError
def compute_equivalent(ce, a, b, c):
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
How dictionaries fail
The failures teach as much as the successes, and interviewers ask about them.
Feature splitting. As the dictionary grows, one latent becomes several more specific ones. Bricken et al. saw the number of latents specific to base64 text go from 1 in their 512-latent run to 3 in the 4,096-latent run, and many more at 16,384. Templeton et al. found that whether a concept gets its own latent at all depends on how often it appears in the training data. Claude 3 Sonnet could list every London borough, yet the 34-million-feature SAE had features for only about 60 percent of them. Splitting means there is no single right dictionary size, only a resolution.
Absorption is splitting with a hole in it. A researcher who trusts the "starts with S" latent as a detector is misled exactly on the tokens it absorbed. The cell builds such a hierarchy. There are four parent features, each with three children, and a child occurs only together with its parent. All 16 directions are orthogonal, so no interference is involved. The cell trains L1 SAEs with 16 latents, one per true feature.
Python · cell 8
Chanin et al. found absorption across hundreds of real SAEs on a first-letter task, and found that changing the size or the sparsity of the SAE did not remove it. Matryoshka training is one of the most direct fixes proposed so far.
Dark matter (Engels et al. 2024). The reconstruction error x−x~ is not noise. About half of the error vector, and more than 90 percent of its norm, can be predicted linearly from the input activation. Larger SAEs mostly fail on the same tokens as smaller ones. So part of what the SAE misses is linear structure it could in principle learn. The authors suggest the rest, the part that is not linearly predictable, may be a different kind of thing that more latents will not fix.
Seed dependence (Paulo and Belrose 2025). Two SAEs trained on the same activations with different random seeds learn different dictionaries. In an SAE with about 131,000 latents on an MLP of Llama 3 8B, only 30 percent of the latents had a close counterpart in the other seed's dictionary. TopK SAEs were more seed-dependent than L1 SAEs. The authors draw the right conclusion. A dictionary is a useful decomposition of the activation space, not a list of the variables the model truly uses.
Errors that compound. An SAE spliced into one layer perturbs everything downstream, and SAEs at many layers at once compound their errors. That is why circuit work keeps an explicit error term at each site, and §23.8 makes those error terms nodes of the graph.
Transcoders and crosscoders
An SAE describes one activation. Circuits are about how activations at one place cause activations at another, and there an SAE on the output of an MLP sublayer has a problem. To see how a feature before the MLP produces a feature after it, you must pass through the MLP's thousands of neurons and their nonlinearities. The answer you get by patching or gradients then holds only for the one input you tried. Dunefsky, Chlenski, and Nanda (2024) proposed replacing the MLP itself.
The payoff is linearity. Suppose latent i of a transcoder at layer ℓ is active with value fi and writes fidi into the residual stream. Let wj be the encoder column of latent j in a later transcoder that reads the same stream (we write w here because e is the reconstruction error). Freeze the normalization in front of the later MLP at its value on this input, the convention of §23.3, and fold its scale into wj. Then, through the residual connection, the contribution of latent i to latent j's pre-activation is
fi(di⋅wj).
In plain words, the effect of one feature on another is an activation that depends on the input, times a weight that does not. The weight di⋅wj comes from the two transcoders alone, so it can be read once and holds on every input. Dunefsky et al. call this the factorization of a circuit into input-dependent and input-invariant terms. Summed over all sources, the contributions add up exactly to the later pre-activation, as the cell checks. Paths that pass through attention add a factor for the attention pattern, which §23.8 handles.
Python · cell 9
Their transcoders on models from 120 million to 1.4 billion parameters were at least on par with SAEs in sparsity, faithfulness, and interpretability. They used them to find new structure in the greater-than circuit of GPT-2 small (§23.4). A transcoder replaces one MLP with one wide sparse layer. The cross-layer transcoder of §23.8 lets each latent read at one layer and write to all later ones, and attribution graphs are built from it.
Two uses follow. Across layers, a feature that persists through the residual stream gets one latent instead of one per layer. Across models, a crosscoder on a base model and its chat-tuned version supports model diffing, finding what fine-tuning changed. Chat tuning is the supervised and reinforcement-learning fine-tuning that turns a base model into an assistant.
The recipe compares, for each latent, the norms of its two decoder rows. A latent with nearly all its norm in the chat model is a candidate for something chat tuning created. One with all its norm in the base model is a candidate for something it removed, and one with equal norms is shared. On the middle layer of Claude 3 Sonnet and its base model, a crosscoder with 1 million latents showed exactly these three clusters, with four to five thousand model-specific latents for each model.
"Model-specific" is a strong claim, though, and the sparsity penalty can manufacture it. Before trusting a chat-only latent, check how much it actually explains in each model, and prefer a crosscoder trained with BatchTopK, as the box explains.
A plain SAE can diff models too, and the most striking case so far concerns emergent misalignment. Emergent misalignment (§19.6) is the finding that fine-tuning a model on a narrow bad task, such as writing insecure code or giving subtly wrong advice, can make it broadly misaligned on unrelated prompts.
SAEs at frontier scale
Everything above was checked on small models whose features we planted. The same recipes have now run on production models, at sizes that change what an SAE costs and what it can find. Four reference points follow, in the order they were published. They run from a one-layer model to Claude 3 Sonnet and GPT-4, and then to an open suite for every layer of two Gemma 2 models.
Those dictionary sizes are easy to read past. A quick count of parameters shows why covering a whole frontier model this way is expensive.
The features themselves were the headline. Claude 3 Sonnet's dictionaries held features that were multilingual and multimodal, responding to the same concept in many languages and in images. They also held features relevant to safety, such as deception, sycophancy, bias, and dangerous content. The most famous is the Golden Gate Bridge feature 34M/31164353, which fires on mentions and images of the bridge. Templeton et al. clamped it, fixing its activation at 10 times its maximum on every token, and the model began to describe itself as the Golden Gate Bridge. Clamping is a form of steering. Section 23.7 takes it up properly, along with the evidence on whether SAE features steer better than simpler directions.
The open suites mean you can run a production SAE at home. The block below needs a machine with a GPU and access to Gemma 2 on Hugging Face. It loads one Gemma Scope SAE and encodes a prompt. The released weights have the training-time rescaling and pre-encoder subtraction folded in, so the code encodes the raw activation, and it uses the JumpReLU rule.
The latent indices it prints can be looked up on Neuronpedia, which hosts explanations and top activations for the Gemma Scope dictionaries.
What SAEs have and have not delivered
By 2025 there was enough evidence to judge SAEs against simpler tools, and much of it was negative. Most of it compares SAEs with linear probes, the classifiers of §23.2 trained to read a labeled concept off the activations.
Kantamneni et al. (2025) tested probes built on SAE latents in four hard regimes, scarce data, class imbalance, label noise, and covariate shift. SAEs occasionally helped on individual datasets. But no ensemble that added SAEs consistently beat ensembles of the baselines alone. The apparent wins, finding spurious correlations, detecting bad labels, and multi-token probes, were matched by simple methods without SAEs.
The Google DeepMind interpretability team (2025) asked whether SAE probes detect harmful user intent better out of distribution, on new jailbreaks. Dense linear probes on the raw residual stream performed nearly perfectly, even out of distribution. A single SAE latent used as a probe could not fit the training set. Probes on about 20 SAE latents fit it but generalized worse, and SAEs fine-tuned on chat data closed only about half the gap. Probes trained on the SAE reconstruction were worse than probes on the stream, so the SAE was discarding information about the concept. The team deprioritized fundamental SAE research while keeping SAEs as a tool. They noted one positive result, that SAE probes quickly exposed spurious correlations in their dataset.
AxBench (Wu et al. 2025), a benchmark for concept detection and steering, found SAEs not competitive for either on Gemma 2 2B and 9B. Prompting was the best steering method, and the difference of class means the best detector. Section 23.7 has the details.
The positive evidence is about discovery, not detection.
Marks et al. (2024) built sparse feature circuits from SAE latents and introduced SHIFT. It removes a classifier's reliance on a spurious signal (gender, in a profession classifier) by ablating the latents a human judges irrelevant to the task. It needs no examples that break the correlation.
In the blind auditing game of Marks et al. (2025), a model was trained with a hidden objective, exploiting reward-model biases it had read about. Four teams investigated it. On one team allowed to use SAEs, a single researcher found the key training documents in 90 minutes. They did it by inspecting the SAE features active on a prompt about the "potentially concerning" behaviors. A second SAE team also succeeded, and so did a team barred from SAEs that used only black-box queries plus the training data. The team with API access and no data did not. The authors note that the fastest team's search could also have been done with a simpler search over text embeddings. SAEs helped, and they were not the only route.
Wang et al.'s persona latents, above, explained a surprising generalization and gave an early-warning signal.
Anthropic has reported using SAE features in pre-deployment audits of its models, which §23.8 describes.
In practice the two tools combine. Use an SAE to generate hypotheses about what a model represents. Then train a probe on labeled data, validate it on held-out distributions, and deploy the probe as the monitor.
Gao et al. (2024). Scaling and evaluating sparse autoencoders. TopK SAEs, tied initialization and AuxK against dead latents, scaling laws in latents and sparsity, and the compute-equivalent reading of spliced-in loss.
🎬 The Dark Matter of AI [Mechanistic Interpretability] · Welch LabsTraces text through Gemma's neurons to show polysemanticity, the superposition hypothesis behind it, and sparse autoencoders on Gemma 2 with Gemma Scope, the best visual intuition for why dictionaries are needed.🎬 What Happened With Sparse Autoencoders? · Neel NandaA frank account of the SAE story from dictionary learning to the 2025 negative results, including the critique of SAE metrics (from 25:24) that matches this section's evaluation discussion.🎬 Scaling interpretability · AnthropicAnthropic researchers on the engineering problems of scaling dictionary learning to production models like Claude 3 Sonnet.