AI Grimoire
Sheet
statuscommon
difficultyadvanced
time
described2020
revisedtoday

Feed-Forward as Key-Value Memory

Write the feed-forward block as a weighted sum of W₂’s columns and it stops looking like a nonlinear map. It looks like a memory with d_ff entries, addressed by pattern match.

Standing

Commonly usedEstablished and frequently the right choice, but competing with live alternatives rather than having settled the question.

The standard reading of what the block does. Well supported, load-bearing for model editing, and known to be incomplete — polysemantic neurons break the clean version of it.

judged as of 2026-09 · what the labels mean

Theory

The feed-forward block is usually written as two matmuls with something in between. Expand the second one by column and a different object appears.

FFN(x)=i=1dffϕ(kix)how well x matchesviwhat to write\mathrm{FFN}(x) = \sum_{i=1}^{d_{ff}} \underbrace{\phi\bigl(k_i^\top x\bigr)}_{\text{how well } x \text{ matches}} \cdot \underbrace{v_i}_{\text{what to write}}
eq. 1 — a weighted sum over d_ff stored vectors

where kik_i is the ii-th row of W1W_1 and viv_i the ii-th column of W2W_2. The block holds dffd_{ff} key-value pairs. A token’s activation is a soft address into them, and the output is whatever the matching entries say to write to the residual stream.

This is not an analogy imposed from outside — it is the same arithmetic, regrouped. What makes it a claim rather than a rewrite is the empirical part: the keys correspond to human-recognisable input patterns, and the values correspond to recognisable output distributions.

What the keys match

Geva et al. took each kik_i, found the training examples that activate it most strongly, and had annotators describe them. Almost every neuron examined had a describable trigger, and the triggers were stratified by depth: shallow layers keyed on surface patterns — a specific suffix, a bigram, a formatting convention — and deep layers on semantic ones, such as “a sentence about a military rank” or “the object of a purchase”.

What the values write

The second paper is the sharper result. Project viv_i through the unembedding matrix and read the top tokens — the same operation as the logit lens, applied to a weight rather than an activation. The result is interpretable: a value vector’s top tokens form a coherent category, and it is usually the category that plausibly follows the key’s trigger.

logits    WU(x+iϕ(kix)vi)=WUx+iϕ(kix)(WUvi)\mathrm{logits} \;\approx\; W_U\Bigl(x + \sum_i \phi(k_i^\top x)\, v_i\Bigr) = W_U x + \sum_i \phi(k_i^\top x)\, \bigl(W_U v_i\bigr)
eq. 2 — each entry is a vote in vocabulary space

The block does not compute a distribution. It casts weighted votes over the vocabulary, and the residual stream accumulates them across layers — which is why the logit lens shows the prediction sharpening layer by layer rather than appearing at the end.

Editing

If a fact is stored as a small set of entries, changing it should not require retraining. ROME formalises this: treat the block as a linear associative memory W2W_2 satisfying W2KVW_2 K \approx V, and solve for the minimal update that maps a chosen key to a chosen value while disturbing the others as little as possible.

W2^=W2+Λ(C1k),C=E[kk]\hat{W_2} = W_2 + \Lambda\bigl(C^{-1}k_*\bigr)^\top, \qquad C = \mathbb{E}\bigl[k k^\top\bigr]
eq. 3 — a rank-one edit with a least-squares constraint

It works, and the caveats are instructive. Causal tracing localises a fact to a specific layer range, and edits applied elsewhere do not take. Edits generalise to paraphrases but frequently not to the reversed relation. And at scale the edits interfere — MEMIT extends the method to thousands, and past that the model degrades.

Implementation

python · torch · read a block as a memory
import torch
from torch import Tensor


@torch.no_grad()
def describe_entry(w1: Tensor, w2: Tensor, unembed: Tensor, i: int, k: int = 10):
    """What entry i matches on, and what it writes to the vocabulary."""
    key, value = w1[i], w2[:, i]                     # [D], [D]

    # The value's vote, read directly in token space.
    promoted = (unembed @ value).topk(k).indices     # [k] token ids
    return key, promoted


@torch.no_grad()
def entry_contributions(w1: Tensor, w2: Tensor, x: Tensor, k: int = 10):
    """Which entries actually fired for this token, ranked by contribution."""
    h = torch.relu(w1 @ x)                           # [d_ff]
    # Not just activation: an entry with a large activation and a small value
    # vector contributes little.
    strength = h * w2.norm(dim=0)
    return strength.topk(k)

That second function is the one worth keeping. Ranking by activation alone is the common mistake and it is misleading — what reaches the residual stream is hivih_i \|v_i\|, and the two orderings differ substantially.

Related

References

[1]Geva et al. — Transformer Feed-Forward Layers Are Key-Value Memories (2020)arXiv:2012.14913
[2]Meng et al. — Locating and Editing Factual Associations in GPT (2022)arXiv:2202.05262
[3]Geva et al. — Transformer Feed-Forward Layers Build Predictions by Promoting Concepts in the Vocabulary Space (2022)arXiv:2203.14680