Preprint

Sparse Readout Prism

Explaining Logit-Lens Scores in Features Instead of Tokens

Matteo He William F. Shen Xinchi Qiu Nicholas D. Lane

Department of Computer Science and Technology, University of Cambridge

A lens reading reflects both the hidden state and the readout. SRP explains token scores as sums of sparse readout features, using only the readout's weights.

Abstract

A language model's prediction of its next token develops across layers, and lens methods track this process by decoding intermediate hidden states into tokens. But a lens reading reflects both the hidden state and the readout (the unembedding matrix) used to decode it. Many lenses are fit on a corpus, and we show that two lenses differing only in their fitting corpus can report different tokens for the same hidden states. We call this dependence corpus conditionality. To examine readout structure independently of the fitting corpus, we introduce Sparse Readout Prism (SRP), which decomposes the readout using only its weights and expresses any token logit or logit difference as a sum of contributions from sparse readout features. This reveals readout features as a new unit of analysis for lens readings, exposing structure that token identities can obscure and enabling comparisons across tokens, contexts, layers, and lenses. Replacing the original readout with SRP's sparse approximation reconstructs 8.9–17.3 percentage points more of the tested logit differences than the strongest of six baselines built on geometric relations among readout rows. Ablating features shifts logit differences in proportion to their SRP contributions. Although token readings vary with the fitting corpus, the dominant readout feature remains stable. Because SRP uses no corpus in its construction, it provides a control independent of the fitting corpus for lens analyses.

Panel A: the logit lens scores bug at 18.9 and insect at minus 1.5 on a prompt about software. Panel B: SRP splits the same margin into readout features for defect, crash, debug, bug, issue and fix, with no feature for the insect sense.
Where a lens reading comes from. (A) The logit lens scores bug against insect for Qwen3.5-2B on the prompt "The programmer reproduced the crash and filed a". (B) SRP decomposes that margin into signed readout feature contributions, in logit units. The leading features all concern software defects, with no feature for the insect sense among them.

Method

Left: a selected readout direction, a weighted combination of unembedding rows, is split into signed feature directions plus a residual. Right: for a decoded state, the score splits into one contribution per feature plus the residual, in logit units.
Sparse Readout Prism, schematically. Left: for coefficients α over unembedding rows, SRP decomposes the selected readout direction qα into signed feature directions βi(α) di and a residual rα. Right: for a decoded state, the score decomposes into one contribution per feature plus the residual term, all in logit units. Blue bars support A, orange bars support B, and hatching marks the residual. Figure 2 in the paper.

Key results

SRP outperforms every baseline built on row geometry

ReadoutSRP coverageBest alternativeLead (pp)
Qwen3.5-0.8B0.7540.665+8.9
Qwen3.5-2B0.7800.625+15.5
Qwen3.5-9B0.8120.639+17.3
Ministral-3-8B0.7770.648+12.9
R1-Distill-Qwen-7B0.7160.579+13.7
R1-Distill-Llama-8B0.7510.624+12.7

Coverage is the fraction of about 1,350 logit differences per model that the replaced readout reconstructs with the correct sign and error below half the score's magnitude. The best alternative is the strongest of six baselines built directly on the geometry of the readout rows, such as nearest-row ridge, k-means centroids and PCA.

Context decides which features carry a token

Signed readout feature terms for bug on Qwen3.5-2B. In an insect context the margin is carried by features whose top rows are mosquito, bee and beetle; in a software context it is carried by defect, crash and debug features.
One token, carried by different features in different contexts: signed feature terms for bug on Qwen3.5-2B in (A) an insect context, scored against error, and (B) a software context, scored against insect. Green terms support bug and orange terms oppose it.

A fitted lens reports the language of its fitting corpus

For the same frozen Qwen3.5-9B state on a Chinese factual recall prompt, the lens fitted on English reports London and the lens fitted on Chinese reports the Chinese word for London; one shared readout feature dominates both scores.
Two Jacobian lenses on Qwen3.5-9B, fitted on English and on Chinese C4 prompts, report tokens in different scripts for the same frozen states on 39 of 80 prompts. The same SRP feature dominates under both lenses on 77 of 80 prompts. Here, at layer 26 on a Chinese factual recall prompt, one lens reports London and the other the Chinese word for London, and feature f23180 dominates both scores.

Use the dictionaries

List the LM-head tokens that score highest on one readout feature, as in the paper's figures. This example downloads the 134 MB Qwen3.5-0.8B dictionary and the model's weights (about 1.9 GB in total); all dictionaries are on Hugging Face, and the code reproduces the paper.

# pip install huggingface_hub safetensors torch transformers
import torch
from huggingface_hub import hf_hub_download
from safetensors import safe_open
from transformers import AutoTokenizer

# SRP dictionary for Qwen3.5-0.8B: 16,384 readout features, k = 128.
repo, model = "hematteo/sparse-readout-prism", "Qwen/Qwen3.5-0.8B"
path = hf_hub_download(repo, "qwen3.5-0.8b/k128_16x/checkpoint.pt")
sae = torch.load(path, map_location="cpu", weights_only=True)["model_state_dict"]
W_enc = sae["encoder.weight"]  # (16384, 1024)

# The model's readout. Qwen3.5-0.8B ties its LM head to the token embedding.
weights = hf_hub_download(model, "model.safetensors-00001-of-00001.safetensors")
with safe_open(weights, "pt") as f:
    W_U = f.get_tensor("model.language_model.embed_tokens.weight").float()
tok = AutoTokenizer.from_pretrained(model)

# The dictionary reads LM-head rows centered on their mean and scaled to unit norm.
X = W_U - W_U.mean(0)  # (248320, 1024)
X /= X.norm(dim=1, keepdim=True)

feature = 14186
top = (X[: len(tok)] @ W_enc[feature]).topk(8).indices  # rows scoring highest on it
print([tok.decode(i) for i in top])
# ['的水果', '水果', ' fruit', ' Fruit', ' frutas', ' фруктов', ' fruits', '蔬果']

Related

Learning to Read Out: Unembedding Dynamics in Language Model Pretraining (NeurIPS 2026, arXiv).

BibTeX

@misc{he2026sparsereadoutprismexplaining,
  title         = {Sparse Readout Prism: Explaining Logit-Lens Scores in Features Instead of Tokens},
  author        = {Matteo He and William F. Shen and Xinchi Qiu and Nicholas D. Lane},
  year          = {2026},
  eprint        = {2609.01936},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2609.01936}
}