CS 8803-LLM · Session 03

Embeddings II: MLA & Architectural Choices

Two papers, one move: squeeze a wide representation through a narrow, learned channel before you use it. DeepSeek-V2 does it to the KV cache. NV-Embed does it to an entire token sequence. Same trick, two very different payoffs.

Prerequisites: attention as Q/K/V matrix multiplication + what a KV cache is during autoregressive decoding. Everything else is built here.
10
Chapters
3
Simulations
0
Assumed Knowledge

Chapter 0: The Memory Wall

You are on the inference team the week DeepSeek-V2 finishes training: 236 billion parameters, a mixture-of-experts model that only activates 21 billion of them per token. It is strong, it is efficient to train, and now it is your job to serve it to actual users without falling over.

Traffic climbs. Users open long conversations, paste in long documents, ask for long answers. You watch a dashboard, and one number is climbing faster than any other — not the model weights, which are fixed the moment you load them, but the KV cache: a block of memory that grows for every open conversation, for as long as that conversation stays open.

Why there is a cache at all

Quick refresher, because everything in this session hangs on it. During autoregressive decoding, generating token t requires attending over the keys and values of every token before it — tokens 1 through t−1. If you recomputed those keys and values from scratch at every step, decoding a 1,000-token response would redo the same matrix multiplications roughly 500,000 times more than necessary. So instead you compute each token’s key and value once, the moment it is generated, and cache them. Step t+1 reuses everything already cached and only computes the one new key/value pair.

That cache is a gift for compute — and a tax on memory. It grows with every token, it is per-request (a hundred open conversations means a hundred separate caches), and unlike the model weights it cannot be shared across requests. It sits on the same GPU memory as the weights, competing for the same finite space.

Serving actually has two distinct phases, and it’s worth naming both so “decoding” above isn’t doing more work than it should. Prefill is the one-time pass over your entire prompt — every token of your question, processed in parallel, building the cache for the first time. Decode is what happens after: one token at a time, each new token reading the whole cache built so far. Prefill is compute-heavy but happens once per request; decode is what repeats, token after token, for the entire length of the response — and it is decode that pays the memory-bandwidth cost this chapter is about. A long conversation is expensive specifically because every one of its many decode steps re-reads an ever-growing cache.

The misconception: “we just need a bigger GPU.” The KV cache does not grow with model size alone — it grows with model size times concurrent users times context length, all three multiplied together. A bigger GPU buys you a bigger denominator once; more users and longer contexts keep growing the numerator every single day. The fix has to be architectural, not just more silicon.

The number that should worry you

Here is the punchline of the first half of this session, stated before we’ve earned it — we will derive every digit by Chapter 3. DeepSeek-V2’s attention uses 128 heads, each 128 dimensions wide, across 60 layers. Under the textbook attention mechanism (Multi-Head Attention, or MHA), caching the keys and values for a single token, summed across all 60 layers, costs 3.75 MB.

Now put a realistic load on it: 100 concurrent conversations, each holding 4,096 tokens of context.

100 × 4,096 = 409,600 token-slots of cache to hold
409,600 × 3.75 MB = 1,536,000 MB  =  1.5 TB  —  just for cache

One and a half terabytes is more memory than nineteen 80 GB accelerators hold combined — and that number describes only the cache, before a single one of the model’s 236 billion weights has been loaded anywhere. Multi-head Latent Attention, the subject of the first half of this session, takes the identical 100-conversation, 4,096-token scenario down to about 27,000 MB — roughly 26 GB. Same model, same traffic, same accuracy (in fact, better accuracy, as you’ll see in Chapter 3) — a 57× smaller memory bill.

The obvious fixes, and why they cost you something

This problem is not new, and MLA is not the first attempt to solve it. Two earlier ideas shrink the cache by brute force: Multi-Query Attention (MQA) makes every head share a single key and a single value, so instead of caching 128 separate key vectors per token you cache one. Grouped-Query Attention (GQA) is the compromise between MHA and MQA — split the 128 heads into a handful of groups, and every head in a group shares one key/value pair.

Picture it concretely. Under MHA, head 1 and head 2 each keep their own private key vector — two independent 128-dimensional views of the token. Under GQA with, say, 8 groups, heads 1 through 16 are forced to share a single key vector between them; whatever head 1 wanted to look for and whatever head 16 wanted to look for now have to be satisfied by the same 128 numbers. Under MQA, all 128 heads share that one key vector — every head’s query still gets to be different, but every head is scoring against the identical key and value. You have not removed a computation; you have removed a distinction.

Both work, in the sense that the cache shrinks exactly as much as you’d expect from the arithmetic. But they are blunt instruments: they throw away information indiscriminately, and Chapter 3 will show you the real published numbers — MQA and GQA measurably lose accuracy compared to full MHA, and the loss is worst on exactly the “hard” benchmarks that require the most attention precision. You are trading quality for memory, at a fixed, disappointing exchange rate.

There is a third fix worth naming, because it is the one most engineers reach for first: just store the cache in fewer bits. Quantizing a bf16 cache down to int8 or int4 buys you a flat 2× or 4× reduction, no architecture changes required. It is real, it is cheap to implement, and production systems often do it anyway — but notice it is orthogonal to everything this chapter is building toward. Quantization shrinks however many elements you already have; MLA shrinks how many elements there are in the first place. The two stack: a quantized MLA cache is smaller still. This session is entirely about the second lever, because it is the one with room for a 57× win rather than a 2–4× one.

The claim this session exists to prove. Multi-head Latent Attention gets you out of that trade entirely. It does not just shrink the cache by throwing heads away — it changes what gets cached. Instead of caching each head’s full key and value, it caches a single small latent vector that every head’s key and value can be reconstructed from on demand. The cache shrinks because the thing being stored is smaller, not because less is being computed. That distinction is the entire lesson of Chapters 1–4.

Where this session is going

Two papers, two halves, one shared idea. The first half (Chapters 1–4) works through DeepSeek-V2’s Multi-head Latent Attention — how a per-token key/value pair gets compressed into a latent vector small enough to make the memory wall disappear, and why a naive version of that idea breaks positional encoding and needs a clever patch. The second half (Chapters 5–7) turns to NV-Embed, a paper about a completely different problem — turning a whole document into one embedding vector for search — that solves it with a mechanism built from the same underlying move: compress a wide, variable-length thing through a narrow, learned bottleneck. Chapter 8 puts the two side by side and names the pattern. Chapter 9 sends you onward.

The memory wall

Slide the traffic (concurrent conversations) and the context length. Watch the two bars — vanilla MHA cache vs. MLA cache — for the exact same workload. The dashed line marks one 80 GB accelerator’s worth of memory, for scale.

concurrent conversations100
context length (tokens)4096

A worked example: how many users fit on your GPU?

Turn the memory numbers into the question that actually keeps an inference engineer up at night: how many concurrent users can I serve? Suppose you have an 80 GB accelerator, and after loading your share of the model weights you have roughly 40 GB left over, all of which the serving framework can spend on KV cache. Cap every conversation at 2,048 tokens of context. How many conversations fit, under each mechanism?

40 GB = 40 × 1,024 MB = 40,960 MB of cache budget

Under MHA, each conversation’s cache costs 2,048 tokens × 3.75 MB/token = 7,680 MB. Divide the budget by the per-conversation cost:

40,960 ÷ 7,680 ≈ 5 concurrent conversations

Five. On an 80 GB card, running a 236-billion-parameter model, vanilla MHA lets you serve five people at once before the cache alone exhausts your spare memory. Now the same arithmetic under MLA, where each conversation costs 2,048 × 67.5 KB ≈ 138,240 KB ≈ 135 MB:

40,960 ÷ 135 ≈ 303 concurrent conversations

Same GPU, same model, same 2,048-token cap — roughly sixty times more simultaneous users. This is not an abstract memory-accounting exercise; it is the difference between a chatbot that queues users behind a “server busy” message and one that doesn’t.

Concept → realization: what “cache” means operationally

Being concrete about what actually sits in GPU memory: for one layer, one attention head, one token, MHA stores one key vector and one value vector, each of dimension dh. At DeepSeek-V2’s dh = 128, that is 256 floating-point numbers, per head, per token, per layer. Multiply by 128 heads and 60 layers and you get the 1,966,080-element number we’ll derive properly next chapter. Every one of those numbers has to physically live in high-bandwidth memory for as long as that token stays part of the conversation’s context — that is what “cache” costs, in hardware terms, not an abstraction.

And it is worth being honest about the shape of the rest of this session before you commit to it: Chapters 1–4 are almost entirely arithmetic and linear algebra — derivations, not prose. That is deliberate. The claim “MLA gives you memory and quality, not a trade” is a strong one, and the only way to actually believe it, rather than just take the paper’s word for it, is to rebuild the formulas yourself and watch the numbers fall out the way the paper says they do.

This is not a thought experiment — it shipped

Everything above is motivation for an exercise you are about to work through by hand over the next three chapters. Before you invest that effort, it is worth knowing the arithmetic is not hypothetical. DeepSeek-V2’s own paper reports what actually happened when they deployed it. Compared against DeepSeek 67B — their own prior-generation, standard-MHA dense model — DeepSeek-V2 reduces the KV cache by 93.3% and boosts maximum generation throughput to 5.76×. On a single node of 8 H800 GPUs, DeepSeek-V2 sustains a generation throughput exceeding 50,000 tokens per second.

Hold that 93.3% next to the 57× this session is about to derive, and notice they are deliberately not the same measurement. The 57× you will derive over Chapters 1–3 is a controlled comparison: the paper trains two otherwise-identical models — same layers, same data, same everything except the attention mechanism — and measures the cache directly, isolating MLA as the only variable. The 93.3% figure is a production comparison: DeepSeek-V2 (236B total / 21B activated parameters, MLA, MoE) against DeepSeek 67B, an earlier model that differs in far more ways than just its attention mechanism — different total size, dense rather than mixture-of-experts, a different training run entirely. Both numbers point the same direction, and both are real, but only the first isolates attention as the sole cause. Keep that distinction in your pocket for reading any paper’s headline numbers: ask what, precisely, was held constant when the comparison was made, because a production-vs-predecessor comparison and a controlled ablation are answering different questions even when they use the same units.

Concept → realization. A 5.76× throughput number is not a separate fact from the cache-size arithmetic above — it is a consequence of it. Chapter 1 will show that decoding is memory-bandwidth-bound: every decode step re-reads the entire accumulated cache off high-bandwidth memory. Shrink the cache and you shrink the bytes moved on every single step, and because decoding’s bottleneck is precisely that byte-shuffling, throughput rises in something close to lockstep with how much smaller the cache gets. The memory win and the speed win are the same win, counted two different ways.

The other half of the story: prefill has its own throughput number

This chapter drew a line between prefill (the one-time pass over your prompt, compute-heavy) and decode (one token at a time, memory-bandwidth-heavy, repeated for the whole response). The 5.76× and 50,000 tokens/second figures above are both decode-side numbers — the regime the KV cache actually lives in. DeepSeek-V2’s paper reports a separate figure for the other phase: prompt input throughput exceeding 100,000 tokens per second. Prefill runs roughly twice as fast, in tokens processed per second, as decode does — which is exactly what you’d expect once you know why the two phases are bottlenecked differently. Prefill processes every prompt token in parallel in one pass, limited mainly by raw arithmetic throughput; decode processes one token at a time, forced to re-read the entire accumulated cache at every single step, limited by how many bytes can move through memory. The KV cache this chapter is about to spend four chapters shrinking is a decode-side cost specifically — it is why decode is the slower of the two phases in the first place, and why a cache-shrinking mechanism moves decode’s throughput number so much more than it would move prefill’s.

A second capacity example, doubled context

Chapter 0’s “how many users fit” exercise used a 2,048-token cap. Redo it with a smaller GPU and a longer cap, to confirm the arithmetic isn’t an artifact of the specific numbers chosen the first time. Suppose a 40 GB accelerator, with 20 GB free for KV cache after weights are loaded, and a 4,096-token context cap per conversation.

20 GB = 20 × 1,024 MB = 20,480 MB of cache budget

Under MHA, each conversation costs 4,096 × 3.75 MB = 15,360 MB — more than three-quarters of the entire budget for one conversation alone:

20,480 ÷ 15,360 ≈ 1 concurrent conversation

One. A 40 GB card running this model under vanilla MHA can barely serve a single 4,096-token conversation at all. Now MLA, where each conversation costs 4,096 × 67.5 KB ≈ 276,480 KB ≈ 270 MB:

20,480 ÷ 270 ≈ 76 concurrent conversations

Same smaller card, same doubled context length — roughly 76× more simultaneous users, not the ≈60× the earlier 2,048-token example found. The multiplier isn’t fixed; it depends on exactly how tight the memory budget is relative to the per-conversation cost, but in every configuration this session has tried, the gap between “a handful of users” and “dozens to hundreds of users” is the same gap, just scaled differently by the specific hardware and context length in play.

Pushed to an extreme: 1,000 conversations at once

One more scale check, in the same “how many 80 GB accelerators’ worth” units the widget above already uses. At 1,000 concurrent conversations, each holding 4,096 tokens:

1,000 × 4,096 = 4,096,000 token-slots
MHA: 4,096,000 × 3.75 MB = 15,360,000 MB ≈ 15,000 GB of cache alone  —  ≈ 188 accelerators’ worth
MLA: 4,096,000 × 67.5 KB ≈ 270,000 MB ≈ 264 GB of cache  —  ≈ 3.3 accelerators’ worth

At this scale, MHA’s cache alone would need roughly 188 of the 80 GB accelerators the chart’s dashed line marks — before a single weight of the 236-billion-parameter model has been loaded anywhere. MLA needs a little over three. This is the same 57× ratio Chapter 3 will derive, just pushed out to a scale large enough that the absolute numbers, not only the ratio between them, start to matter for whether a deployment is possible at all.

The KV cache for a batch of open conversations grows fastest with which combination of factors?

Chapter 1: Multi-Head Attention, Refreshed

Before you can appreciate a compression trick, you have to know exactly what is being compressed. This chapter derives the standard Multi-Head Attention cache cost from scratch, using DeepSeek-V2’s real numbers, so that Chapters 2 and 3 have a fixed target to beat.

The forward pass, symbol by symbol

Let d be the model’s hidden dimension, nh the number of attention heads, and dh the dimension of each head. For a token at position t, let ht ∈ ℝd be that token’s hidden state entering the attention layer. Standard MHA produces the query, key, and value for that token with three learned matrices:

qt = WQht     kt = WKht     vt = WVht

where WQ, WK, WV ∈ ℝdhnh×d. Each of qt, kt, vt is then sliced into nh head-sized chunks — qt,i, kt,i, vt,i ∈ ℝdh — and each head runs its own attention:

ot,i = ∑j=1t softmaxj &Bigl( qt,iTkj,i√dh &Bigr) vj,i

and the heads are concatenated and mixed back down with an output projection WO:

ut = WO[ot,1; ot,2; …; ot,nh]

Nothing exotic so far — this is the Transformer attention block exactly as the Transformer lesson teaches it. The part that matters for this session is what has to be remembered between decoding steps: every kj,i and vj,i for every past position j ≤ t, every head i, and (because every layer runs its own independent attention) every layer l.

Counting the cache, one token at a time

Fix your attention on a single token. How many numbers does MHA need to remember about it, forever, for as long as it stays in context? Two vectors (key and value) per head, times the number of heads, times the number of layers:

KV cache per token = 2 × nh × dh × l

where l is the number of Transformer layers. The 2 is not a rounding factor — it is literally “one key vector, one value vector.” Now plug in DeepSeek-V2’s shipped configuration: 60 layers, 128 heads, 128 dimensions per head.

2 × 128 × 128 × 60 = 1,966,080 elements per token

Worth pausing on the arithmetic: 2×128 = 256; 256×128 = 32,768 (the per-layer cache for one token, across all heads); 32,768×60 = 1,966,080. Each of those elements is stored in bf16 — 2 bytes — so in memory that is:

1,966,080 × 2 bytes = 3,932,160 bytes = 3.75 MB per token

That is the number Chapter 0 asked you to take on faith. Now you’ve derived it: a single token, remembered across all 60 layers of a 128-head, 128-dim-per-head attention stack, costs 3.75 megabytes — forever, for as long as it stays part of someone’s conversation.

Deriving GQA and MQA’s cache the same way

Chapter 0 quoted GQA and MQA’s cache formulas without proof. Now that you have MHA’s derivation in hand, they take one line each, because both are just MHA with fewer independent key/value pairs. GQA splits the 128 heads into ng groups — DeepSeek-V2’s own appendix ablation uses 8 — and every head in a group shares one key/value pair, so instead of 128 separate pairs you store ng:

GQA cache per token = 2 × ng × dh × l

Plug in ng = 8, holding dh and l at DeepSeek-V2’s values:

2 × 8 × 128 × 60 = 122,880 elements per token

MQA is the extreme case, ng = 1 — every head shares the same single pair:

MQA cache per token = 2 × dh × l   —   = 2 × 128 × 60 = 15,360 elements per token

Line the three up and the pattern is obvious — every mechanism so far is the exact same formula, 2×(something)×dh×l, with only the head-count-like factor changing:

MechanismShared factorCache / token (elements)Reduction vs. MHA
MHA128 (every head private)1,966,080
GQA (8 groups)8122,88016× smaller
MQA1 (every head shares)15,360128× smaller

MQA’s reduction ratio is not a coincidence — going from 128 independent pairs to 1 shared pair is exactly a 128× reduction, because the formula is linear in the group count. That cleanliness is also MQA’s downfall: it is the most extreme point on this line, throwing away the most information, and Chapter 3’s ablation table will show it paying for that in accuracy more than any other mechanism tested. Keep these three numbers — 1,966,080, 122,880, 15,360 — in your head; Chapter 3 is about to show you a fourth number that beats all of them without following the same “fewer heads” logic at all.

Why this is a memory-bandwidth problem, not a compute problem

One more piece of systems context worth having before Chapter 2, because it explains why engineers care about this cache size so much in the first place, beyond just “it’s a lot of gigabytes.” Generating one token at a time is memory-bandwidth bound: for every new token, the GPU has to read the entire KV cache back off high-bandwidth memory to compute that token’s attention scores, and it does almost no arithmetic per byte read — one multiply-add per cached number, roughly. Modern accelerators can do vastly more arithmetic per second than they can move bytes per second, so decoding spends most of its time waiting on memory traffic, not on the matrix multiplies themselves. A smaller cache is not just “fits in less space” — it is fewer bytes to shuttle through memory on every single decoding step, which is why cache size and decoding speed move together almost one-to-one.

A detail worth noticing: attention is wider than the model

Look again at the head count and head width: nh = 128, dh = 128, so the concatenated width of all the query heads together is 128×128 = 16,384. But DeepSeek-V2’s hidden dimension is only d = 5,120. The attention computation is actually wider than the residual stream it reads from and writes back into — WQ projects up from 5,120 to 16,384 before the heads even split. This is a real, deliberate design choice: a model can afford to compute attention in a wide space as long as what gets cached is narrow. That asymmetry — wide computation, narrow memory — is exactly the gap MLA is about to exploit.

Concept → realization. “KV cache” is not a single number for the whole model — it is a per-token, per-layer allocation that the serving system has to hold in high-bandwidth memory continuously. Doubling your context length exactly doubles it. Doubling your batch size exactly doubles it again. There is no economy of scale here: 1,966,080 elements per token is a hard floor under MHA, no matter how efficiently you batch requests.

The same computation, as code

Here is the forward pass with the shapes annotated at every step, so the arithmetic above has somewhere concrete to land.

python
import torch

d, n_h, d_h, l = 5120, 128, 128, 60

# one layer's projections
h_t = torch.randn(d)                       # (5120,)      hidden state, this token
q_t = W_Q @ h_t                             # (16384,)     n_h * d_h
k_t = W_K @ h_t                             # (16384,)     -> cached, per layer
v_t = W_V @ h_t                             # (16384,)     -> cached, per layer

# what the cache holds for THIS token, THIS layer
layer_cache_elems = 2 * n_h * d_h          # 32,768

# across all 60 layers
total_cache_elems = layer_cache_elems * l   # 1,966,080
total_cache_bytes = total_cache_elems * 2  # bf16 -> 3,932,160 bytes = 3.75 MB
The misconception: “the model weights are the expensive part, so that’s where I should optimize memory.” At serving time, for a model with many concurrent long-context users, the weights are a fixed cost paid once. The KV cache is a variable cost that scales with usage — and past a certain traffic level, as Chapter 0’s numbers showed, it dwarfs the weights entirely. Reducing weight count by 10% barely matters if the cache is already the bottleneck.

Counting cache growth, layer by layer

The formula 2×nh×dh×l is linear in l, which is easy to state and worth actually watching happen. Fix one token and tally the running cache total as more of DeepSeek-V2’s 60 layers come online. Each layer adds exactly 32,768 elements (2×128×128, derived above) — the same fixed increment, every layer, no matter how deep you already are:

Layers processedRunning total (elements)Running total (bytes, bf16)
10327,680655,360 (640 KB)
20655,3601,310,720 (1.25 MB)
30983,0401,966,080 (1.875 MB)
401,310,7202,621,440 (2.5 MB)
501,638,4003,276,800 (3.125 MB)
60 (all layers)1,966,0803,932,160 (3.75 MB)

Notice the table is exactly a straight line — each row is 327,680 elements more than the last, because 32,768×10 = 327,680. There is no layer where the per-token cost suddenly jumps or plateaus; a token entering the cache at layer 1 costs the system exactly as much per layer as a token already resident at layer 59. That linearity is what makes the formula trustworthy for planning: double the layer count of a future model, holding everything else fixed, and you can predict the new cache cost without re-deriving anything.

A footnote from the paper’s own ablation: attention mechanism changes parameter count too

One detail easy to miss when you only track cache size: switching the attention mechanism also changes how many parameters the model has, because MQA and GQA need fewer K/V projection weights than MHA does. When DeepSeek-V2’s authors ran the Chapter 3 ablation comparing MHA, GQA, and MQA at “7B scale,” they could not simply swap the attention mechanism and leave everything else identical — doing so would have left the three models at different total parameter counts, which would confound the comparison (is GQA scoring lower because it shares heads, or just because it has fewer total parameters to work with?). So the paper adjusted the layer count of each variant to bring all three back to roughly 7B parameters: the published configuration lands at 7.1B parameters for the MQA model, 6.9B for the GQA (8-group) model, and 6.9B for the MHA model, each trained on 1.33 trillion tokens with everything else held fixed.

Concept → realization. This is why the ablation in Chapter 3 is trustworthy evidence about the attention mechanism specifically, and not just an artifact of parameter count: the authors controlled for total parameters by tuning layer depth, precisely so the accuracy gap you are about to read (MHA beating MQA by 7.3 points on MMLU) could be attributed to the attention mechanism and not to one model quietly having more capacity than another.

Where ht actually comes from

One link worth closing before moving on: ht, the hidden state this whole chapter treats as a given input, is not a free-floating abstraction. It is produced by an embedding lookup followed by every Transformer layer before the current one. DeepSeek-V2 uses a Byte-level Byte-Pair Encoding tokenizer with a vocabulary of 100,000 tokens — so the very first hidden state any token ever gets is a row pulled out of a 100,000×5,120 embedding table, before a single attention layer has touched it. By the time ht reaches layer 30 or layer 59, it has already been rewritten 30 or 59 times by prior attention and feed-forward blocks — it is not the raw token identity anymore, it is a running summary of everything the model has decided matters about that token and its context so far. Keeping this in view matters for the rest of the session: the compression Chapter 2 is about to build compresses this evolving, already-contextualized representation, not the raw input word.

The same table, run for GQA instead of MHA

The layer-by-layer table above used MHA’s per-layer cost, 32,768 elements. Run the identical exercise for GQA at 8 groups, whose per-layer cost is 2×8×128 = 2,048 elements — a sixteenth of MHA’s, matching the 16× reduction ratio from the table earlier in this chapter:

Layers processedMHA running totalGQA (8 groups) running total
10327,68020,480
30983,04061,440
60 (all layers)1,966,080122,880

Every entry in the right column is exactly one-sixteenth of the entry to its left, at every layer count, not just at the final total — because both columns are linear in l with a fixed ratio between their slopes. Linearity in the layer count is not a special property of MHA; every mechanism this session derives is some fixed per-layer cost multiplied by l, which is exactly why Chapter 4’s calculator can show all four bars moving together, proportionally, whenever you drag the layer-count slider.

Why does DeepSeek-V2’s attention compute in a 16,384-wide space (128 heads × 128 dims) when its hidden dimension is only 5,120?

Chapter 2: Compressing Keys and Values Into a Latent

Chapter 1 established the floor: 1,966,080 elements per token, non-negotiable under standard MHA. This chapter builds the mechanism that breaks that floor — the core move of Multi-head Latent Attention.

The idea, before the algebra

Look again at what MHA caches: 128 separate key vectors and 128 separate value vectors per token, one pair per head. But those 256 vectors are not independent information — they are all linear functions of the same 5,120-dimensional hidden state ht. If WK and WV are just two more linear maps off the same input, maybe you don’t need to store their outputs at all. Maybe you can store something smaller that those outputs can be reconstructed from, on demand, whenever attention actually needs them.

That “something smaller” is a latent vector: a compressed representation of the token that carries only what the keys and values need, in far fewer dimensions than the keys and values themselves.

Low-rank joint compression, derived

Instead of projecting ht straight to keys and values, MLA inserts one extra step. First, a down-projection compresses the hidden state into a small latent:

ctKV = WDKVht

where ctKV ∈ ℝdc and dc ≪ dhnh — the compression dimension is deliberately far smaller than the full key/value width. Then, whenever attention actually needs to run, two up-projections reconstruct the full-width keys and values from that latent:

ktC = WUKctKV     vtC = WUVctKV

The critical decision: which of these three vectors gets cached. Not ktC, not vtC — only the small latent, ctKV. During inference, MLA caches ctKV once per token per layer, and reconstructs both the key and the value from it fresh whenever attention needs them.

ht
hidden state, width d = 5,120
↓ down-project: WDKV (d → dc)
ctKV
the latent — width dc, THIS is what gets cached
↓ up-project on demand: WUK, WUV
ktC, vtC
full-width key & value, reconstructed, never stored

A toy example, by hand

Formulas convince the eye; arithmetic convinces the hand. Shrink everything to numbers you can multiply on paper. Let the hidden state be 4-dimensional (real: 5,120) and compress it to a 2-dimensional latent (real: 512).

ht = [1.0, 0.5, −0.5, 0.2]

Pick a down-projection WDKV (2×4) that has one row summing the “positive” half of the vector and one row summing the “negative” half — simple by design, so the arithmetic reads as a story:

WDKV = ┌ 1  1  0  0 ┐
        └ 0  0  1  1 ┘
ctKV = WDKVht = [1.0+0.5,  −0.5+0.2] = [1.5, −0.3]

That two-number latent is what gets cached — half the width of the original vector, in this toy scale exactly matching the 64× reduction ratio Chapter 2 derived for the real model. Now reconstruct a key with an up-projection WUK (back to width 4):

WUK = ┌ 1  0 ┐
       │ 1  0 │
       │ 0  1 │
       └ 0  1 ┘
ktC = WUKctKV = [1.5,  1.5,  −0.3,  −0.3]

Notice what happened: the reconstructed key is not identical to the original ht — it can’t be, two numbers cannot losslessly encode four independent numbers in general. What survived is whatever the down-projection decided to keep (here, the sum within each half); what’s gone is whatever distinguished the two numbers within each half (1.0 vs 0.5, and −0.5 vs 0.2 are no longer individually recoverable). In a real, trained MLA, WDKV is not hand-picked to sum halves — it is learned by gradient descent to keep whatever combinations of the 5,120 input dimensions the attention scores actually depend on, and discard whatever the loss function never needed. Compression is lossy in general; it is useful precisely because a trained model learns to lose the parts nothing downstream was using anyway.

The trick inside the trick: weight absorption

You might expect reconstructing keys and values every step to add compute back that you saved in memory. It doesn’t, and the reason is a small piece of linear algebra worth sitting with. During inference the query computation is:

score = qtTktC = qtT(WUKctKV) = (qtTWUK)ctKV

Matrix multiplication is associative, so you can pre-multiply WQ and WUK together once, at deployment time, before any request ever arrives — the paper calls this “absorbing” WUK into WQ. The same trick absorbs WUV into WO on the output side. The consequence is striking: at inference time, the model never actually materializes ktC or vtC as explicit tensors at all. It scores directly against the cached latent, using pre-fused weights. The “reconstruct, then attend” story in the previous section is the mathematical picture; the deployed computational picture skips the middle step entirely.

Verify the associativity claim on numbers small enough to trust by eye. Reuse the toy WUK (4×2) from the previous section along with a toy cached latent cjKV = [1, −1] (someone else’s token). Since WUK reconstructs a width-4 key, give the query matching width: qt = [2, 1, 2, 1]. Compute “reconstruct then score” first — the mathematical story:

kjC = WUK[1,−1] = [1, 1, −1, −1]
score = qtTkjC = 2(1)+1(1)+2(−1)+1(−1) = 2+1−2−1 = 0

Now “fuse first, score directly against the latent” — the deployed story. Compute qtTWUK once (a 1×2 row vector):

qtTWUK = [2(1)+1(1)+2(0)+1(0), \ 2(0)+1(0)+2(1)+1(1)] = [3, 3]
score = [3, 3] · [1, −1] = 3 − 3 = 0

Same answer, two routes. That is the associativity Chapter 2 is leaning on, made concrete instead of asserted — and notice the second route never built the 4-wide reconstructed key at all, only a 2-wide fused query that gets computed once and reused against every cached latent.

Why this is the whole point. The cache shrinks not because MLA discards information the way MQA and GQA do — averaging heads together, throwing away distinctions between them — but because it stores a compressed encoding of the same information, decodable in full whenever it’s needed. Nothing about which head attends to what is lost; it is just represented more efficiently in memory, the same way a ZIP file holds the same bytes as the original in a smaller footprint.

The cache size, with real numbers

DeepSeek-V2 sets dc = 512 (the paper states this as 4×dh, and 4×128 = 512 — we’ll come back to why a multiple of dh is a natural choice in Chapter 3). Ignore positional encoding for one more chapter — it needs its own treatment — and just compute the cache for the compressed latent alone, across 60 layers:

dc × l = 512 × 60 = 30,720 elements per token

Compare that to Chapter 1’s 1,966,080. Already, before Chapter 3’s refinement, this is a 64× reduction:

1,966,080 ÷ 30,720 = 64  —  exactly, because 512 × 64 = 32,768, the per-layer MHA width

A second compression, for a different reason

MLA also compresses the query path the same way — a down-projection to a query latent ctQ ∈ ℝdc (DeepSeek-V2 sets dc′ = 1,536), followed by an up-projection back to full query width:

ctQ = WDQht     qtC = WUQctQ

This one is easy to misread as “the same trick, so it must also shrink the cache.” It does not — queries are never cached in the first place; only keys and values persist across decoding steps, because only keys and values from past tokens are needed to compute attention for the current token. Query compression exists for a different reason entirely: it shrinks the activation memory during training, where you must hold intermediate tensors for every token in a batch simultaneously for the backward pass, not just the current one. It is a training-time optimization riding on the same low-rank idea, bundled into the same paper because the mechanism is identical — but the KV compression is what fixes the memory wall.

The mechanism in code

python
import torch, torch.nn as nn

d, d_c = 5120, 512       # hidden dim, KV compression dim
n_h, d_h = 128, 128      # heads, per-head dim

W_DKV = nn.Linear(d, d_c, bias=False)             # down-projection
W_UK  = nn.Linear(d_c, n_h * d_h, bias=False)     # up-projection, keys
W_UV  = nn.Linear(d_c, n_h * d_h, bias=False)     # up-projection, values

def step(h_t, cache):
    c_t = W_DKV(h_t)                    # (512,)   <- THIS is what gets appended to cache
    cache.append(c_t)                  # cache grows by 512 elements/layer/token, not 32,768

    # at attention time, reconstruct on demand for every cached token:
    all_c = torch.stack(cache)          # (t, 512)
    K = W_UK(all_c)                     # (t, 16384)  <- materialized fresh, or fused via absorption
    V = W_UV(all_c)                     # (t, 16384)
    return K, V
The sanity check. Print c_t.shape right after the down-projection, before anything else happens. If it isn’t (dc,) — 512 elements, not 16,384 — the compression didn’t happen and you’re about to cache the full-width tensors by accident, silently losing every byte of the memory saving this chapter just derived.

The parameter savings, not just the cache savings

Everything so far has been about what gets cached at inference time. There is a second, separate saving worth deriving by hand: low-rank factorization also shrinks the weight matrices themselves — the parameters the model has to store and load, before a single request ever arrives.

Standard MHA needs two full projection matrices, WK and WV, each of shape dhnh×d = 16,384×5,120:

params(WK) = params(WV) = 16,384 × 5,120 = 83,886,080 each
standard total = 2 × 83,886,080 = 167,772,160 parameters

MLA replaces those two matrices with three smaller ones: the down-projection WDKV (dc×d = 512×5,120) and the two up-projections WUK, WUV (each dhnh×dc = 16,384×512):

params(WDKV) = 512 × 5,120 = 2,621,440
params(WUK) = params(WUV) = 16,384 × 512 = 8,388,608 each
MLA total = 2,621,440 + 8,388,608 + 8,388,608 = 19,398,656 parameters

Divide the two totals and factorization saves parameters too, not just cache:

167,772,160 ÷ 19,398,656 ≈ 8.65× fewer parameters in the K/V projection weights, per layer

This is the general shape of any low-rank factorization: replacing one m×n matrix with a product of an m×r and an r×n matrix costs r(m+n) parameters instead of mn, and the saving is largest exactly when r ≪ min(m,n) — which is precisely the regime dc = 512 ≪ 16,384 that Chapter 2 opened with. Worth being precise about what this saving is not: it is a training-time and storage saving, on top of and separate from the inference-time cache saving this chapter has been building toward. A smaller checkpoint to load is a real, additional benefit — it just isn’t the 57× headline number, which is specifically about what gets cached per token during serving.

Why can MLA cache only the 512-dimensional latent ctKV, rather than the full 16,384-dimensional keys and values, without losing information the attention mechanism needs?

Chapter 3: The RoPE Problem and Decoupled Position

Chapter 2 left a hole: it said “ignore positional encoding for now.” Time to stop ignoring it, because it turns out low-rank compression and the position encoding DeepSeek-V2 actually uses are fundamentally incompatible — and the fix is one of the more elegant patches in the paper.

What RoPE does, in one sentence

DeepSeek-V2 uses Rotary Position Embedding (RoPE): instead of adding a position vector to the token embedding, RoPE rotates the query and key vectors by an angle that depends on their position, so that the dot product qtTkj automatically encodes the relative distance tj between them. The important property for this chapter: RoPE is applied directly to the query and key vectors themselves, as a position-dependent rotation matrix.

Why that breaks the absorption trick

Recall Chapter 2’s key move: pre-multiplying WQ and WUK together once, offline, so inference never has to reconstruct ktC explicitly. That trick relies on matrix multiplication being associative — you can regroup (qtTWUK) freely because there is nothing sitting between WQ and WUK that depends on the token currently being generated.

Now suppose you applied RoPE to ktC directly, the naive way. RoPE inserts a rotation matrix Rt that depends on position t — which is different for every token, and in particular different for the token you’re generating right now versus every token already in the cache. The score becomes:

score = (qtTWUK) Rt−j cjKV

Rt−j now sits between the fused (WQWUK) block and the cached latent, and matrix multiplication does not commute — you cannot slide Rt−j out of the way to keep the weights pre-fused. Worse, because Rt−j changes for every new token generated, you would have to recompute the key for every already-cached token, every single decoding step. That is exactly the O(n2) recomputation the cache exists to avoid in the first place. Applying RoPE naively to compressed keys doesn’t just cost a little efficiency — it destroys the entire point of compression.

The non-commutativity, by hand

“Rotation matrices don’t commute with arbitrary matrices” is easy to assert and worth actually checking. RoPE’s rotation, in its simplest 2D block, is exactly a standard rotation matrix by angle θ:

R(θ) = ┌ cosθ  −sinθ ┐
        └ sinθ    cosθ ┘

Take θ = 90°, so cosθ = 0 and sinθ = 1, and a simple fused weight A = [[1, 0],[0, 0]] (picks out the first coordinate only). Multiply A·R(θ) first:

A·R(90°) = ┌1 0┐┌0 −1┐ = ┌0 −1┐
     └0 0┘└1  0┘  └0  0┘

Now multiply the other order, R(θ)·A:

R(90°)·A = ┌0 −1┐┌1 0┐ = ┌0  0┐
     └1  0┘└0 0┘  └1  0┘

[[0,−1],[0,0]] and [[0,0],[1,0]] are not the same matrix — order matters, exactly as claimed. In the real mechanism, A stands in for the fused (WQWUK) block, computed once offline; R stands in for RoPE’s position-dependent rotation. If R had to sit between them, you would be forced to recompute that product for every distinct position, every step — the fusion could never be done ahead of time. This two-line calculation is the entire reason Chapter 3 exists.

The misconception: “just don’t compress the part of the key that carries position — problem solved.” That is almost exactly MLA’s real fix, but the details matter: you can’t just skip compressing part of the existing key, because the whole key is a single linear function of the latent. You need an entirely separate, parallel channel dedicated to carrying position — which is what “decoupled” means in the next section.

Decoupled RoPE, derived

MLA’s solution: split the query and key into two concatenated pieces. One piece — the “content” piece, qt,iC and kt,iC — is exactly what Chapter 2 built: compressed, absorbed, position-free. The other piece — the “RoPE” piece — is small, computed fresh (not through the compressed latent), and carries only position information:

qt,iR = RoPE(WQRctQ)     ktR = RoPE(WKRht)

Notice ktR has no head index — it is shared across all 128 heads, one small RoPE-carrying key per token, not 128 of them. The query and key each head actually uses are the two pieces concatenated:

qt,i = [qt,iC; qt,iR]     kt,i = [kt,iC; ktR]

DeepSeek-V2 sets the decoupled dimension dhR = 64 — exactly dh⁄2, half a normal head’s width, dedicated purely to position. The content half keeps its absorption trick intact (position never touches it); the small RoPE half is cheap enough to cache directly, uncompressed, without threatening the memory budget.

The final cache formula

Now assemble the complete picture. Every token’s cache holds two things: the compressed content latent (shared across all heads, one copy per layer) and the shared RoPE key (also one copy per layer, since it too has no head index):

MLA cache per token = (dc + dhR) × l
= (512 + 64) × 60 = 576 × 60 = 34,560 elements per token

In bf16 bytes: 34,560 × 2 = 69,120 bytes = 67.5 KB per token. Compare against Chapter 1’s MHA floor of 3.75 MB per token — a factor of:

1,966,080 ÷ 34,560 ≈ 56.9× smaller   (a 98.2% reduction)

A hidden equivalence: MLA is “GQA with 2.25 groups”

Here’s a satisfying piece of algebra straight from the paper. Recall GQA’s cache formula from Chapter 0: 2×ng×dh×l, where ng is the number of groups. Set that equal to MLA’s cache and solve for what group count would give GQA the identical memory footprint:

2 × ng × 128 × 60 = 34,560   ⇒   ng = 34,560 ÷ 15,360 = 2.25

MLA occupies exactly as much cache as GQA would with 2.25 groups — a number GQA can’t even implement, since group counts have to be whole numbers that divide 128 evenly (1, 2, 4, 8…). MLA sits in a gap the coarser mechanism structurally cannot reach.

But does it actually perform well? The real ablation numbers

Cache size alone doesn’t win an argument — MQA is even smaller. The paper’s appendix runs controlled experiments, training otherwise-identical models with different attention mechanisms, and reports accuracy on four hard benchmarks. First, MHA against the cache-shrinking alternatives, at 7B dense scale:

AttentionBBH (3-shot)MMLU (5-shot)C-Eval (5-shot)CMMLU (5-shot)
MQA33.237.930.034.6
GQA (8 groups)35.641.237.738.4
MHA37.045.242.943.5

MHA wins on every benchmark, by a wide margin — on MMLU, MHA beats MQA by 7.3 points, nearly a fifth of MQA’s score. This confirms Chapter 0’s warning: the cheap fixes really do cost accuracy, not just theoretically but measurably. Now the number that justifies this entire chapter — MLA against MHA, at production MoE scale (roughly DeepSeek-V2’s own configuration, trained on 420B tokens for this controlled comparison):

AttentionKV cache / tokenBBHMMLUC-EvalCMMLU
MHA860.2K elements46.657.557.960.7
MLA34.6K elements50.759.059.262.5

Read the cache column first: 34.6K matches our hand-derived 34,560 almost to the digit — a real, independent confirmation that the formula in this chapter is exactly what got shipped. And read the accuracy columns: MLA does not just match MHA at a fraction of the memory — it beats MHA on all four benchmarks, most strikingly BBH, up 4.1 points. Do the cache-ratio arithmetic by hand:

860.2 ÷ 34.6 ≈ 24.9× smaller cache — while scoring higher on every benchmark measured

That is the trade Chapter 0 promised and Chapter 3 has now proven: not a compromise between memory and quality, but a genuine win on both axes simultaneously.

The compression ratio gets better as the model gets bigger

The paper ran this same MLA-vs-MHA ablation at a second, smaller scale too — roughly 16-billion-parameter MoE models, trained on 1.33 trillion tokens:

ScaleAttentionActivated / total paramsKV cache / tokenBBHMMLU
Small MoE (~16B)MHA2.5B / 15.8B110.6K37.948.7
Small MoE (~16B)MLA2.4B / 15.7B15.6K39.050.0
Large MoE (~250B)MHA25.0B / 250.8B860.2K46.657.5
Large MoE (~250B)MLA21.5B / 247.4B34.6K50.759.0

Compute the reduction ratio by hand at both scales: at 16B, 15.6÷110.6 ≈ 14.1%, so MLA keeps about 14% of MHA’s cache. At 250B, 34.6÷860.2 ≈ 4.0%, so MLA keeps only about 4%. The compression ratio itself improves as the model scales up — and Chapter 1’s formulas explain exactly why. MHA’s cache grows with nh, and bigger models tend to add more attention heads as they scale. MLA’s cache formula, (dc + dhRl, has no nh term at all — every head DeepSeek-V2 adds costs MHA proportionally more cache, while costing MLA nothing. This is not a coincidence of DeepSeek-V2’s specific numbers; it is a structural property of the two formulas, and it means MLA’s advantage over MHA only widens as frontier models keep growing.

Why dhR is set to exactly half a head

One more design choice worth pausing on: DeepSeek-V2 sets the decoupled RoPE width dhR to 64, precisely half of dh = 128. This is a bandwidth trade-off, not an arbitrary round number. Every bit you spend on the decoupled RoPE channel is a bit that gets cached uncompressed — a direct tax on the memory savings Chapter 2 fought for. Spend too little and the positional signal is too coarse for the model to reliably tell nearby tokens apart; spend too much and you are back to caching most of a full head’s worth of uncompressed data, defeating the compression’s purpose. Half a head’s width is the paper’s answer to that balance — small enough to keep the (dc + dhR) sum dominated by the compressed part (512 vs. 64, an 8:1 ratio), large enough to give RoPE room to encode position reliably. Chapter 4’s calculator lets you drag this exact number and watch the trade-off directly.

MechanismKV cache / tokenCapability (paper’s framing)
MHA2 nh dh lStrong
GQA2 ng dh lModerate
MQA2 dh lWeak
MLA(dc + dhR) l ≈ 92 · dh lStronger

A detail the formula alone hides: the softmax denominator changes too

Look back at Chapter 1’s attention formula and its denominator, √dh. That scaling factor exists to keep the variance of the pre-softmax scores roughly constant regardless of how wide the vectors being dotted are — without it, wider vectors would produce systematically larger dot products, pushing softmax into a regime where it saturates and gradients vanish. The rule is always the same: divide by the square root of the width of the vectors you just dotted together.

Once query and key are each split into a content half and a RoPE half and concatenated back together (qt,i = [qt,iCqt,iR], and likewise for k), the vectors actually being dotted are no longer dh = 128 wide. They are dh + dhR = 128 + 64 = 192 wide — the content piece and the RoPE piece concatenated. DeepSeek-V2’s own attention formula reflects this directly: the denominator is √(dh + dhR), not √dh.

naive (wrong) scaling: √dh = √128 ≈ 11.31
DeepSeek-V2’s actual scaling: √(dh + dhR) = √192 ≈ 13.86

It is a small correction — about 22.6% larger a divisor — but skipping it is exactly the kind of bug that would silently degrade a model without crashing anything: scores computed with the wrong denominator would still run, still produce a valid probability distribution after softmax, and still train — just slightly mis-calibrated in a way that would be nearly invisible until you compared final benchmark numbers against a correctly-scaled baseline. The decoupling in this chapter is not just about which projections get fused; it changes the width of the vector attention actually scores against, and the denominator has to track that.

The other payoff of decoupling: context length becomes a local edit

Chapter 0 mentioned DeepSeek-V2 supports a 128K-token context window. Here is where the decoupled RoPE channel pays for itself a second time. DeepSeek-V2 is pretrained at a much shorter context — 4K tokens initially, extended in a later training stage to 32K — and only afterward stretched to 128K using a technique called YaRN, which rescales how RoPE’s rotation angles map onto position indices so a model trained on short sequences generalizes to much longer ones without retraining from scratch.

Here is the payoff of Chapter 3’s decoupling, stated precisely: because position information lives only in the small, separate ktR channel, extending context length only requires rescaling that one small piece — YaRN is applied specifically to the decoupled shared key ktR, and nothing about the compressed content latent ctKV needs to change at all. If DeepSeek-V2 had applied RoPE directly to the full compressed key the naive way (the mistake this chapter opened by ruling out), extending context length would have meant touching a mechanism braided into the same pathway carrying all of the model’s cached content — a far riskier edit than rescaling one small, isolated, position-only channel. Decoupling did not just fix an incompatibility; it left the model with a single, surgical place to intervene when the engineering requirements changed later.

Why this matters beyond DeepSeek-V2 specifically. Architectural decisions that seem to be about solving today’s problem (here: making weight absorption work at all) often pay a second, unplanned dividend later (here: making context extension a one-channel edit instead of a whole-mechanism retrofit). Isolating a concern into its own narrow channel doesn’t just make the immediate math work — it makes future changes to that concern cheaper, because there is exactly one place to make them.

The honest caveat: not every benchmark moves the same way at every scale

Chapter 3 has been building toward “MLA beats MHA on every benchmark tested,” and at the large-MoE scale (roughly DeepSeek-V2’s own configuration) that is exactly true — all four benchmarks improved. But the paper also ran this ablation at a second, smaller scale, and the full picture there is more textured than the two-column BBH/MMLU summary earlier in this chapter let on. Here is the complete four-benchmark table, both scales, matching the paper’s own Table 7 exactly:

ScaleAttentionBBHMMLUC-EvalCMMLU
Small MoE (~16B)MHA37.948.751.652.3
Small MoE (~16B)MLA39.050.050.953.4
Large MoE (~250B)MHA46.657.557.960.7
Large MoE (~250B)MLA50.759.059.262.5

Look closely at the two bolded C-Eval entries. At the small (~16B) scale, MLA scores lower than MHA on C-Eval — 50.9 versus 51.6, a 0.7-point dip. Every other cell at every other scale favors MLA, but this one does not. That is worth stating plainly rather than smoothing over: the claim “MLA wins on every axis simultaneously” holds cleanly at production scale, where DeepSeek-V2 actually shipped, but the small-scale ablation shows the win is not perfectly uniform across every benchmark at every model size. Three of four benchmarks improve at small scale; the fourth is a near-wash, slightly negative.

The misconception this table heads off: “since the paper reports MLA beating MHA, it must win on literally every measurement they ever took.” Real ablations are rarely that clean, and papers that report one is often smoothing over exactly this kind of mixed cell. The trustworthy version of the claim, precisely stated, is: MLA wins the KV-cache trade decisively at every scale tested, and wins accuracy on the large majority of benchmarks at both scales tested, with the win becoming a clean sweep specifically at the scale the model was actually shipped at.
Why can’t MLA simply apply RoPE to the compressed key ktC the same way MHA applies it to a normal key?

Chapter 4: KV Cache Calculator

Time to make Chapters 1–3’s formulas playable. This chapter is one interactive simulation: drag the same hyperparameters DeepSeek-V2’s engineers had to choose, and watch how the cache size and the GQA-equivalence number respond in real time.

What you’re controlling

Five sliders, each tied directly to a symbol from Chapters 1–3: the number of heads (nh), the per-head dimension (dh), the number of layers (l), the KV compression dimension (dc), and the decoupled RoPE dimension (dhR). The chart draws four bars — MHA, GQA (locked at 8 groups, matching the paper’s own ablation), MQA, and MLA — recomputed live from the exact formulas you just derived by hand.

Load the widget and it starts at DeepSeek-V2’s actual shipped configuration. Confirm the MLA bar reads 34,560 before you touch anything — that is your checkpoint that the formulas are wired correctly. Then break things: push dc up toward dhnh and watch MLA’s advantage evaporate (a compression dimension close to the uncompressed width barely compresses anything). Push it down toward zero and watch it undercut even MQA — and remember from Chapter 3’s ablation table that shrinking dc too far is exactly how you’d expect to trade away the accuracy gain, even though this calculator can’t show you accuracy, only memory.

Push dc all the way to its minimum, 32, with dhR also at its floor. The bar still won’t hit zero — there is always some latent, however thin, because the mechanism has to cache something to reconstruct keys and values from at all. The calculator has no slider that lets MLA's cache reach zero, and that's correct: unlike quantization (Chapter 0), which can theoretically compress toward nothing, low-rank compression has a floor set by however many independent directions of variation the keys and values actually need to represent. Push dc below that floor and you're not saving memory for free anymore — you're discarding information the attention mechanism structurally needs, which is exactly the region Chapter 3's ablation table warns you away from.

KV cache per token, by mechanism

Bars are on a log scale — the mechanisms span nearly two orders of magnitude. The dashed marker shows the GQA group count that would match MLA’s footprint exactly.

heads (nh)128
per-head dim (dh)128
layers (l)60
dc (KV compression)512
dhR (decoupled RoPE)64

The four formulas, as one function

Everything the sliders drive is exactly the code below — the same four formulas from Chapters 1–3, with nothing hidden:

python
def kv_cache_per_token(n_h, d_h, l, d_c, d_h_r, n_g=8):
    mha = 2 * n_h * d_h * l
    gqa = 2 * n_g * d_h * l      # n_g locked at 8, matching the paper's own ablation
    mqa = 2 * d_h * l
    mla = (d_c + d_h_r) * l
    return {'MHA': mha, 'GQA': gqa, 'MQA': mqa, 'MLA': mla}

# DeepSeek-V2 defaults
kv_cache_per_token(n_h=128, d_h=128, l=60, d_c=512, d_h_r=64)
# -> {'MHA': 1966080, 'GQA': 122880, 'MQA': 15360, 'MLA': 34560}

Three exercises to run before moving on

Each of these takes under a minute on the sliders and cements one specific fact from Chapters 1–3:

Try thisWhat you should seeWhy (chapter)
Set dc to its maximum, 2,048MLA's bar approaches (and can exceed) MHA's — the compression stops compressingCh. 2: dc ≪ dhnh was the whole premise
Set dhR to 0MLA's bar drops slightly, but position information vanishes (not visible here — Ch. 3's ablation is the accuracy evidence)Ch. 3: RoPE needs its own channel or the absorption trick breaks
Drag heads (nh) from 8 up to 128MHA and GQA's bars climb steadily; MLA's bar never movesCh. 1/3: nh doesn't appear in MLA's formula at all

What the calculator makes obvious

Two facts fall out once you can see the bars move. First: MHA and GQA scale with nh (or ng), while MLA is completely flat with respect to head count — drag nh up to 128 and MHA’s bar shoots up while MLA’s doesn’t move at all, because nh doesn’t appear in MLA’s formula. This is the algebraic reason the compression ratio improves as models get bigger: Chapter 3’s ablation showed 14% of MHA’s cache at 16B-parameter scale but only 4% at 250B-parameter scale, and the calculator shows you exactly why — bigger models tend to add more heads, and every added head costs MHA proportionally while costing MLA nothing.

Second: layers (l) scale every mechanism’s cache identically, because every mechanism multiplies by l the same way. Drag the layer slider and all four bars move together, proportionally — depth is not where the mechanisms differ; width is.

Concept → realization. The reason engineers can tune dc and dhR as free hyperparameters, separate from nh and dh, is precisely because of the decoupling Chapter 3 built: the compression dimension controls how much content information survives, and the decoupled RoPE dimension controls how much positional resolution survives, and neither one is mechanically tied to how many attention heads the model happens to have. That independence is a design freedom MHA, GQA, and MQA simply don’t offer — their cache size is welded to their head count.

Loop back to Chapter 0’s serving-capacity question with the calculator’s numbers in hand. The concurrent-conversation arithmetic from Chapter 0 was a direct function of “bytes per token,” and “bytes per token” is exactly what every bar in this chart reports. Every architectural choice DeepSeek-V2’s engineers made when picking dc and dhR was, underneath the math, a decision about how many simultaneous users a fixed GPU fleet could serve — which is the entire reason this session opened with a serving scenario instead of a formula.

Three regimes, side by side

It helps to think of the (dc, dhR) pair as choosing a point on a spectrum rather than tuning two independent knobs. Set the calculator to each of these three points and read off what each regime buys and costs:

Regimedc, dhRCache / tokenWhat you'd expect
Aggressive256, 32(256+32)×60 = 17,280 el.Smallest cache; Ch. 3's ablation logic predicts the largest accuracy risk, since less content survives the bottleneck
DeepSeek-V2's actual choice512, 6434,560 el.The published, measured sweet spot: 24.9× smaller than MHA and higher accuracy on every benchmark tested
Conservative1024, 12869,120 el.Still a real win over MHA's 1,966,080, but you're leaving compression on the table without published evidence it buys more quality

DeepSeek-V2’s published configuration sits closer to the aggressive end of this range than the conservative one — a reminder that the paper’s engineers didn’t just pick “small enough to matter”, they pushed until the ablation stopped improving, then stopped. The calculator can’t show you where that stopping point was found (that required actually training models at each setting, which is what Chapter 3’s ablation table represents), but it can show you exactly how much memory each candidate point would have cost, which is half of what you’d need to make that engineering decision yourself.

A sixth knob the calculator locks: GQA’s group count

The calculator fixes GQA at 8 groups, matching the paper’s own ablation — but the underlying formula, 2×ng×dh×l, works for any group count that evenly divides 128. Sweep it by hand and MHA and MQA turn out to be the two endpoints of the exact same line GQA sits on, not separate mechanisms:

ng (groups)Cache / token (elements)Which mechanism this is
115,360MQA (the extreme case)
230,720GQA
461,440GQA
8122,880GQA (the paper’s tested configuration)
16245,760GQA
32491,520GQA
64983,040GQA
1281,966,080MHA (the other extreme — every head private)

Where would MLA’s 34,560 sit if you inserted it into this same sorted list? Between the ng=2 row (30,720) and the ng=4 row (61,440) — much closer to the ng=2 row, which is exactly what Chapter 3’s “2.25 groups” result says algebraically. Sweeping the whole table by hand makes that fraction concrete instead of abstract: MLA doesn’t just beat GQA at 8 groups, it sits in a gap between two specific whole-number rows that GQA is structurally incapable of landing on, no matter which divisor of 128 you pick.

Reading the log scale correctly

The calculator’s bars are drawn on a log scale because the four mechanisms span nearly two orders of magnitude — a linear scale would make MLA’s bar an invisible sliver next to MHA’s. Reading a log bar by eye takes a moment’s recalibration: equal lengths on the bar represent equal ratios, not equal differences. Check this against the two numbers you already trust:

log10(1,966,080) ≈ 6.29      log10(34,560) ≈ 4.54
6.29 − 4.54 = 1.76 decades of difference   ⇒   101.7656.9×

That 56.9× is exactly Chapter 3’s reduction ratio, recovered from nothing but the two bar lengths and a calculator’s worth of arithmetic. It is worth doing this conversion by hand once, because it is precisely what your eye is doing automatically (and often wrongly) every time you glance at a log-scaled chart in a paper: a bar that looks “about a third as long” on a log axis is not a third the size — depending on the axis range, it could easily be one-thousandth the size.

Three more configurations, worked by hand before you touch the sliders

Predict each of these, then check the calculator against your prediction:

ConfigurationMLA cache / tokenHow it compares to DeepSeek-V2’s 34,560
dc=768, dhR=96, l=80 (a hypothetical deeper, less-compressed model)(768+96)×80 = 69,120exactly 2× DeepSeek-V2’s cache — both the compression widths and the layer count scaled up together
dc=512, dhR=64, l=30 (DeepSeek-V2’s widths, half the depth)576×30 = 17,280exactly half — confirms the formula is linear in l alone, holding dc and dhR fixed
dc=1,536, dhR=64, l=60 (triple the content compression, same everything else)1,600×60 = 96,000still 20.5× smaller than MHA’s 1,966,080 — even a much less aggressive compression choice would have beaten MHA’s cache handily

The third row is the most instructive: DeepSeek-V2’s engineers did not pick dc=512 because anything less aggressive would have failed to help. Even tripling the compressed width to 1,536 — giving up most of the compression ratio — would still have beaten MHA’s cache by more than 20×. They chose 512 because the ablation in Chapter 3 showed it was where accuracy stopped improving, not because it was the minimum needed to beat MHA at all.

What the calculator can’t show you, and why that’s honest

Every bar in this chart is a memory number. None of them is an accuracy number, and that omission is deliberate, not a missing feature. Chapters 1–3 derived cache size from closed-form formulas — 2×nh×dh×l and its variants — which is exactly the kind of thing a slider and a live-redrawing bar chart can represent faithfully, because the answer really is just arithmetic. Accuracy is not arithmetic. It is the output of actually training a model at a given configuration for trillions of tokens and evaluating it, and there is no formula this calculator (or any calculator) could plug in to predict it in advance. That is precisely why Chapter 3 spent so much space on Table 6 and Table 7’s real, measured numbers instead of asserting a rule — and why dragging dc down toward its floor here will show you a shrinking bar with total confidence, while the accuracy consequence of that same drag is something the paper’s authors could only find out by actually training the smaller model and running the benchmark suite. Knowing which parts of an architectural trade-off are computable in advance and which parts require an experiment is itself a useful skill this calculator is quietly teaching by what it leaves out.

Running the calculator backwards

Every use of this calculator so far has gone forward: pick dc and dhR, read off the cache. Engineering usually runs the other direction — you are handed a memory budget and have to find hyperparameters that fit it. Try it: suppose a budget of at most 50,000 elements per token, with dhR and l held at DeepSeek-V2’s values (64 and 60). Solve for the largest dc that still fits:

(dc + 64) × 60 ≤ 50,000
dc + 64 ≤ 50,000 ÷ 60 ≈ 833.3
dc ≤ 833.3 − 64 = 769.3

The calculator’s dc slider moves in steps of 32, so the largest achievable value under 769.3 is 768. Check it lands inside budget:

(768 + 64) × 60 = 832 × 60 = 49,920  —  just under the 50,000 ceiling

That is the calculator used as a design tool rather than a demonstration: given a hard constraint (a memory budget, set by the hardware you actually have), solve the same linear formula for the largest compression width that respects it. DeepSeek-V2’s engineers were solving a version of exactly this problem, just with accuracy as a second, harder-to-quantify constraint layered on top of the memory one — which is why their answer, 512, sits well below this budget’s ceiling of 768: the memory constraint alone would have permitted a looser compression, but the accuracy ablation argued for a tighter one anyway.

In the calculator, why does MLA’s bar stay flat when you increase the number of heads (nh), while MHA’s and GQA’s bars grow?

Chapter 5: One Vector for the Whole Sequence

New problem, same shape of solution. Forget serving a chatbot for a moment — you are now building a search engine. A user’s query and a million candidate documents each need to become one vector, so that cosine similarity between two vectors can stand in for “how relevant is this document to this query.” A decoder-only LLM like Mistral 7B, run over a document, produces one hidden vector per token — hundreds of them for a real document. Somehow those hundreds of vectors have to collapse into one. This is the pooling problem, and it is where NV-Embed enters the session.

The two obvious answers, and why both are flawed

There are two standard ways to pool a sequence of token vectors into one embedding, and NV-Embed’s paper is explicit about the failure mode of each.

Mean pooling — the traditional choice for bidirectional models like BERT — simply averages every token’s hidden vector. It is popular and stable, but averaging dilutes: a document’s two or three semantically load-bearing phrases get mixed in, uniformly, with dozens of function words and filler tokens that carry almost no distinguishing signal. The important words don’t get to matter more just because they matter more.

Last-token (<EOS>) pooling — the popular choice for decoder-only LLM embedding models — takes only the hidden vector at the final position, reasoning that a causal model has, by construction, folded everything before it into that last vector. But this creates recency bias: the final vector is disproportionately shaped by whatever token happened to be last, and information from early in a long document has to survive many layers of attention dilution to still be legible by the time generation reaches the end.

The misconception: “the last token in a causal model has already attended to everything before it, so of course it’s a complete summary.” Attending to something and weighting it appropriately are different guarantees. The last token’s representation is optimized, during pretraining, for predicting the next token — not for compressing the whole document evenly. Whatever made a good next-token prediction at that specific position is what survives, and that is a different objective than “summarize this document.”

NV-Embed’s answer: a learned dictionary, not a formula

NV-Embed’s fix is neither “average everything” (a fixed, hand-designed rule) nor “take the last position” (also a fixed rule) — it is a small, trained module that learns which parts of the sequence to weight, called the latent attention layer. The name is not a coincidence: the mechanism inside it is structurally the cross-attention cousin of the very idea Chapters 1–4 just spent four chapters on.

Here is the construction, directly from the paper. Let Q ∈ ℝl×d be the decoder’s last-layer hidden states for a sequence of length l — the whole token sequence, used as the query side of a cross-attention. Then introduce a small, trainable set of vectors, playing the role of key and value simultaneously:

K = V ∈ ℝr×d     — a “dictionary” of r trainable entries

NV-Embed sets r = 512 and d = 4,096 (Mistral 7B’s hidden width), with 8 attention heads. The cross-attention itself is the plain formula you already know:

O = softmax(QKT)V   ∈  ℝl×d

followed by a small MLP (two linear layers with a GELU in between), and finally mean pooling over the output positions to collapse to a single vector. Notice the sleight of hand: mean pooling is still here — but now it’s averaging the output of a learned attention step, not the raw token vectors. The averaging happens after the model has already decided, via attention weights, how much of each position’s content deserves to survive.

decoder hidden states
Q ∈ ℝl×4096 — one row per token
↓ cross-attend to the trainable dictionary K=V ∈ ℝ512×4096
O = softmax(QKT)V
still l rows — every token position, re-weighted by what the dictionary found relevant
↓ MLP (2 linear layers + GELU)
mean pool
collapse l rows → ONE vector, the embedding

A toy example, by hand

Same instinct as Chapter 2: shrink the dictionary until you can multiply it on paper. Let the sequence be 2 tokens wide (real: up to 512), the hidden dimension 2 (real: 4,096), and the dictionary just r = 2 entries (real: 512), with a single attention head. Say the decoder produced these two hidden states:

Q = ┌ 1.0   0.0 ┐   —   token 1 looks “topical,” token 2 looks “sentiment-y”
    └ 0.0   1.0 ┘

And say training has settled on a dictionary where entry 1 has learned to respond to “topical” content and entry 2 to “sentiment” content:

K = V = ┌ 2.0   0.0 ┐
        └ 0.0   2.0 ┘

Compute QKT (2×2 · 2×2 = 2×2 scores, one row per query token, one column per dictionary entry):

QKT = ┌ 1(2)+0(0)   1(0)+0(2) ┐ = ┌ 2   0 ┐
        └ 0(2)+1(0)   0(0)+1(2) ┘  └ 0   2 ┘

Row 1 (token 1) scores 2 against entry 1 and 0 against entry 2 — a decisive vote for the topical entry. Softmax that row (using e2 ≈ 7.39, e0 = 1):

softmax([2, 0]) = [7.39⁄(7.39+1), 1⁄(7.39+1)] ≈ [0.88, 0.12]

Token 1’s output row is 88% entry 1, 12% entry 2 — overwhelmingly the topical dictionary entry, exactly matching the query that looked topical. By symmetry, token 2’s row comes out 12% entry 1, 88% entry 2. This is the mechanism doing its job: each token position gets re-expressed as a weighted mixture of the learned dictionary entries that best match what that position was actually about — not a fixed rule, a content-dependent lookup. Multiply those softmax weights back through V (identical to K here) and you get each token’s output row; only then, after this re-weighting has happened, does the final mean pool average the two rows together.

The parallel to Chapters 1–4, made explicit

Put MLA’s KV compression and NV-Embed’s latent attention side by side and the shared shape is unmistakable. Both introduce a small set of learned vectors that a larger representation must pass through. MLA’s latent is per-token, computed fresh every step, and shrinks a per-request memory cost. NV-Embed’s latents are a fixed, globally shared dictionary trained once and reused for every document, and they shrink a sequence down to one summary vector. Different axis of compression — memory versus sequence length — but the same underlying move: replace a fixed rule (cache everything / average everything) with a narrow channel the model gets to shape during training. Chapter 8 returns to this comparison and names it properly.

A worked number: what 512 trainable entries actually costs

Concretely size the dictionary. K=V is a single r×d matrix (key and value are literally the same trainable parameters, not two separate ones — a deliberate parameter-sharing choice):

512 × 4,096 = 2,097,152 trainable parameters — one matrix, doing double duty as both K and V

And per-head, since the cross-attention runs with 8 heads splitting the 4,096-wide space:

4,096 ÷ 8 = 512 dimensions per head

Compare that 2.1M-parameter dictionary to Mistral 7B’s roughly 7 billion parameters underneath it — the latent attention layer adds well under 0.1% to the total parameter count, yet (as Chapter 6’s ablation table will show) it is the single highest-scoring pooling method the paper tests. A tiny, trained addition beating two much simpler, hand-designed rules.

Notice, too, what K=V sharing costs versus a standard cross-attention where key and value are separate learned matrices. A conventional design would need two r×d matrices — 4,194,304 parameters, twice the dictionary size — to let “what I match against” and “what I return” differ freely. NV-Embed's choice to force them equal halves that cost, and reframes the mechanism as literally a lookup table: each of the 512 rows is one concept, and matching against it and retrieving its content are the same operation, because they use the same numbers. That framing is also why the paper insists on calling it a “dictionary” rather than a generic cross-attention block — the K=V constraint isn't an implementation shortcut, it's what makes the dictionary metaphor literally true rather than just evocative.

“Not the Perceiver” — a precision worth keeping

The paper is careful to note that this design is inspired by Perceiver-style cross-attention (Jaegle et al., 2021) but explicitly differs from it in spirit: Perceiver IO uses its latents as a general-purpose bottleneck for processing arbitrary high-dimensional inputs, while NV-Embed frames its latents as a dictionary in the classical sense — a fixed vocabulary of learned “concepts” that the pooling step looks things up against. The distinction matters for intuition: think of the 512 entries less like a generic information funnel and more like 512 trained templates the model has learned are useful summaries to check every document against.

Why K and V must share Q’s width, dimensionally

One easy-to-skip detail worth making explicit: the dictionary K=V lives in ℝr×d with the same d = 4,096 as Q, not some independently chosen width. This is not a stylistic choice — it is forced by the dot product. QKT only type-checks if Q and K share their last dimension: Q is l×d, KT is d×r, and the matrix product needs those inner d’s to match. The dictionary’s width is welded to whatever hidden width the decoder happens to produce — swap Mistral 7B for a model with a different hidden size and the dictionary’s width has to change with it, even though r, the number of entries, could stay at 512 unchanged. Width is about compatibility with the decoder; entry count is about how many concepts the dictionary can represent — two independent design knobs that happen to both live inside the same matrix.

The multi-head version of the toy example

The single-head toy example above is the whole idea, but the real mechanism runs with 8 heads, and it is worth seeing how the splitting works, in miniature. Extend the toy: hidden width 4 (real: 4,096), 2 heads of width 2 each (real: 8 heads of width 512), dictionary still r = 2 entries wide 4 total (real: 512 entries wide 4,096 total). Take the same Q and K=V from before and split each row into its two head-halves:

Q = [1.0, 0.0, 0.0, 1.0]  →  head 1: [1.0, 0.0]    head 2: [0.0, 1.0]

(using token 1’s row from the earlier example, now cut into its two head-sized pieces). Each head runs its own, independent cross-attention against its own slice of the dictionary — head 1’s query only ever compares against head 1’s slice of K=V, never head 2’s. This is structurally identical to Chapter 1’s multi-head splitting of MHA’s query, key, and value — the same “slice into nh independent pieces, run attention per piece, concatenate the outputs” pattern, just applied to a cross-attention against a fixed dictionary instead of a self-attention against other tokens. The payoff of running 8 narrower heads instead of 1 wide one is the same payoff multi-head attention always offers: each head can specialize in matching a different kind of content against the dictionary — one head might learn to weight entries that capture topic, another entries that capture sentiment or syntax — rather than one attention pattern having to serve every kind of signal at once.

Exactly how small 2.1 million parameters really is

Chapter 5 already computed the dictionary’s 2,097,152 parameters and called it “well under 0.1%” of Mistral 7B. Do the division precisely instead of rounding down to a qualitative claim:

2,097,152 ÷ 7,000,000,000 ≈ 0.030% of the base model’s parameter count

Three hundredths of one percent, added on top of a 7-billion-parameter model, is responsible for the single largest jump in the entire pooling ablation — a reminder of just how disproportionate a well-placed learned bottleneck’s effect can be relative to its size. The parameters that matter most are not always the ones that take up the most room.

What the dictionary is not: a retrieval index

It is tempting, on first hearing “a trained dictionary the document attends to,” to picture something like a vector database — 512 stored documents or passages that the model looks up against. That mental model is wrong in an instructive way. The 512 rows of K=V are not embeddings of anything that exists outside the model; they are raw trainable parameters, initialized randomly and updated purely by gradient descent on the contrastive loss, with no text, document, or external content ever assigned to any specific row. Nobody chose what “row 37” means. Training discovered whatever 512 directions in a 4,096-dimensional space turned out to be useful for producing embeddings that satisfy the contrastive objective — the “dictionary” metaphor is about the mechanism’s shape (fixed-size lookup keys, shared across every document), not about its rows corresponding to anything a human would recognize as concepts, let alone to any retrievable content. The inspection code at the end of Chapter 7 — summing attention weights down the token axis to see which entries a document leaned on — is the closest you can get to naming what a row represents after the fact, and even then what you get back is “entry 137 lit up for this document,” not a human-readable label.

Sizing the cross-attention itself, not just the dictionary

Chapter 5 sized the dictionary’s parameters (2,097,152) but not the work the cross-attention does per document, which is worth separating out because it scales with document length in a way the parameter count does not. For a document of l tokens, computing QKT multiplies an l×4,096 matrix against a 4,096×512 matrix — roughly l×4,096×512 multiply-adds. At NV-Embed’s own 512-token maximum document length:

512 × 4,096 × 512 ≈ 1.07 billion multiply-adds, just for QKT, one document

Multiplying by V afterward costs a comparable second billion. That is real, non-trivial compute — but it is a fixed cost per document, independent of how many other documents exist in the corpus, run exactly once at indexing time and never again. Compare that to what an alternative like full self-attention over the document (Chapter 6’s fourth ablation row) would cost instead: self-attention’s cost grows with the square of sequence length, l², because every token attends to every other token, not just to a fixed 512-entry dictionary. Latent attention’s cost against a fixed-size dictionary is linear in l × r instead of quadratic in l × l — a second, quieter reason (beyond the ablation numbers themselves) the dictionary approach scales more gracefully to longer documents than adding another ordinary self-attention layer would.

What is the fundamental difference between mean pooling and NV-Embed's latent attention layer, given that latent attention also ends with a mean pool?

Chapter 6: Teaching the Model to Look Both Ways

Chapter 5 built the pooling mechanism. This chapter covers the other two pieces of NV-Embed’s recipe: a one-line architectural change to the LLM itself, and the two-stage training procedure that actually makes the whole thing work.

The causal mask was built for a different job

Every decoder-only LLM, including Mistral 7B, is trained with a causal attention mask: token i can only attend to tokens 1 through i, never to anything after it. This exists for a good reason — next-token prediction has to be honest, and letting a token peek at the answer it’s trying to predict would be cheating. But that constraint, built for generation, is actively harmful for a model whose job has changed to representation. An embedding model isn’t predicting the next token; it’s trying to build the best possible summary of a document that already fully exists. Under a causal mask, the vector at token 5 of a 50-token document has no way to know what tokens 6 through 50 say — even though, at embedding time, all 50 tokens are sitting right there, fully visible, with nothing left to predict.

NV-Embed’s fix is almost aggressively simple: during contrastive training, remove the causal mask entirely. Every token attends to every other token, in both directions, exactly like BERT. Other recent work solves the same problem with more machinery — LLM2Vec adds a whole extra training phase with masked token prediction to “warm up” bidirectional attention; GRIT mixes bidirectional and causal objectives together. NV-Embed just flips the mask and trains through it, and reports that the simple version works compellingly well.

It is worth seeing the landscape of competing answers side by side, because “just remove the mask” is a genuinely contrarian choice against a field that mostly reached for more machinery:

ModelIts approach to the causal-mask problemMTEB score
E5-mistral-7b-instructkeeps the causal mask; compensates with massive proprietary GPT-4 synthetic data66.63
LLM2Vecadds a whole extra pretraining phase with masked-token prediction to "warm up" bidirectional attention before contrastive training65.01
GritLMunifies embedding and generation in a single model, mixing bidirectional representation learning with causal generative training66.76
Geckodistills a smaller bidirectional model from a decoder-only LLM, using the LLM to relabel synthetic candidate passages66.31
NV-Embedsimply removes the causal mask during contrastive training, no extra phase, no distillation69.32

None of the more elaborate approaches beat the simple one on this leaderboard. That is not proof that architectural complexity never helps — but it is a useful data point against reaching for machinery before you have tried the version with less of it.

This is not a leap of faith — it is consistent with a pattern that predates all of these papers. Encoder models trained bidirectionally from the start, like BERT and T5, have historically beaten similarly-sized decoder-only GPT-style models on natural language understanding benchmarks, even though GPT-style models dominate at generation. NV-Embed’s bet is that this same gap — unidirectional attention limiting how well a model can represent meaning, independent of how well it can generate — is exactly what was holding decoder-only embedding models back, and that you don’t need BERT’s architecture to get BERT’s representational advantage; you just need BERT’s attention pattern, applied to an otherwise unmodified decoder-only LLM.

Where the negatives actually come from

One detail Chapter 5 glossed over: every training triplet needs seven hard negatives — documents that are superficially similar to the correct answer but actually wrong, which are far more useful for training than random unrelated documents. Most of NV-Embed’s retrieval datasets (MS MARCO, HotpotQA, Natural Questions, and others) don’t ship with hard negatives built in, so the paper mines them: it fine-tunes a separate, smaller encoder-based embedding model specifically to go find the passages that look most similar to the correct answer without actually being correct, and uses that model’s output as the curated hard-negative pool. For the classification datasets, a different contamination risk shows up — some training splits (Emotion-Classification, AmazonCounterfactual-Classification) contain text close enough to their own evaluation splits that training on them would be quietly cheating on the benchmark, so the paper uses BM25 similarity thresholds to detect and strip that overlap out before subsampling.

Does it actually help? Read the ablation, both ways

The paper ran a full ablation crossing four pooling types against both mask types, at two training stages. Every single comparison points the same direction. Here is the stage-2 (final) table, MTEB average across 56 tasks:

Pool typebidirectionalcausalgain from flipping the mask
<EOS>-last67.8566.50+1.35
Mean68.9768.13+0.84
Latent-attention69.3268.47+0.85
Self-attention69.1068.16+0.94

Every pooling method gains from the mask flip, on the order of 0.8–1.4 points — a consistent, mechanism-independent effect. And notice which pooling method wins overall: latent-attention with bidirectional attention, at 69.32, is the single best combination in the whole table — the number that becomes NV-Embed’s headline MTEB score.

Two stages, because one recipe doesn’t fit every task

Bidirectional attention and a good pooling layer aren’t enough on their own — the training data has to be handled carefully too, and the reason is a subtlety about negative examples. In-batch negatives are a classic trick for training retrievers efficiently: if a batch has B queries, each with its own correct passage, you can reuse all the computation and score every query against every other query’s passage as a “negative,” getting B2 query–passage comparisons out of just B forward passes.

B = 128  ⇒  B2 = 1282 = 16,384 query–passage pairs, from 128 forward passes on each side

That trick is free efficiency for retrieval, where any other query’s passage genuinely is an irrelevant document. But it quietly breaks for classification or clustering data: if the batch contains several examples from the same class, another example’s “passage” might belong to the exact same class as your query — treating it as a negative would actively teach the model that same-class examples should look different, exactly backwards from the objective.

NV-Embed’s answer is to sequence the training in two stages rather than pick one policy for everything:

Stage 1: retrieval only
in-batch negatives ON, plus curated hard negatives — lr 2e-5
↓ blend in the rest of the data
Stage 2: retrieval + classification + clustering + STS
in-batch negatives OFF — only the curated hard negatives remain — lr 1.5e-5

Every training example, regardless of stage or task, is built from the same triplet shape: one instructed query, one positive document, and seven curated hard negatives, in batches of 128. Retrieval goes first because, in the paper’s own words, it is the harder task — get that foundation solid before blending in tasks that would be actively hurt by the in-batch trick that helped build it.

The full-model recipe, for completeness: NV-Embed starts from base Mistral 7B (not an existing embedding model — the paper is explicit that it is not fine-tuning e5-mistral-7b-instruct or anything similar), and trains with LoRA (rank 16, alpha 32, dropout 0.1) rather than full fine-tuning, using the Adam optimizer with 500 warm-up steps and a linear decay schedule — learning rate 2×10−5 for stage 1, dropping to 1.5×10−5 for stage 2. Every input, query or document, is capped at 512 tokens and wrapped with special <BOS> and <EOS> markers. Every instructed query follows one fixed template — Instruct: {task description}\nQuery: {query} — and critically, the instruction tokens themselves are masked out of the final pooled output (they still shape it, through self-attention, but they don’t directly contribute their own vectors to the pooling step); documents never get an instruction prefix at all, only queries do.

One more design choice worth flagging, because it distinguishes NV-Embed from its closest competitor, SFR-Embedding-Mistral (which also blends retrieval with non-retrieval data, and which NV-Embed’s own paper discusses directly). SFR-Embedding constructs each training batch from a single task at a time — task-homogeneous batches. NV-Embed does the opposite: every batch is a well-blended mix of samples from across all the tasks in that stage. The paper’s stated reasoning is that task-homogeneous batches risk a “zigzag” gradient — a batch entirely of retrieval examples pulls the weights one way, the next batch entirely of clustering examples pulls them a different way, and the optimizer spends part of every step undoing the previous step’s specialization instead of finding a direction good for every task at once. Blended batches average that tension out within a single gradient step rather than across steps.

The two-stage payoff, measured

Compare the same pooling-and-mask combination (latent-attention, bidirectional) before and after stage 2 is added:

stage 1 average: 64.18  →  stage 2 average: 69.32     gain = 69.32 − 64.18 = +5.14

Break that gain down by task category and the two-stage logic becomes obvious — the categories that were barely touched in stage 1 improve the most:

Task categoryafter stage 1after stage 2gain
Retrieval (15 tasks)59.0059.36+0.36
STS (10 tasks)79.0782.84+3.77
Clustering (11 tasks)45.4452.80+7.36
Classification (12 tasks)73.9387.35+13.42

Classification jumps by more than 13 points once its own data enters training — unsurprising, since stage 1 never saw a single classification example. What is mildly surprising, and the paper calls this out explicitly: retrieval doesn’t degrade when in-batch negatives are turned off in stage 2 to accommodate the other tasks — it actually improves slightly, by 0.36 points. Blending in more diverse supervision helped the task the model was already good at, not just the tasks it was bad at.

The training loop, as code

python
# stage 1 -> stage 2, same triplet shape, different negative policy
batch_size, n_hard_neg = 128, 7

# stage 1: retrieval only
stage1_batch = sample_retrieval(batch_size)          # (query, pos, 7 hard negs) x 128
loss1 = contrastive_loss(stage1_batch, in_batch_negatives=True)   # B^2 = 16,384 pairs

# stage 2: retrieval + non-retrieval, blended
stage2_batch = sample_blended(batch_size)             # retrieval, classification, clustering, STS
loss2 = contrastive_loss(stage2_batch, in_batch_negatives=False)  # only the 7 curated hard negs count
Concept → realization. “In-batch negatives OFF” is not a minor knob — it changes how many negative comparisons a single training step performs. Stage 1 gets roughly B2 pairs practically for free; stage 2 gets only B×7 = 896 curated pairs. Stage 2 trades quantity of negatives for trustworthiness of negatives — every one of those 896 was deliberately selected to be a true negative, unlike the free-but-occasionally-wrong in-batch ones.

The training data, by name

“Public retrieval datasets” and “public non-retrieval datasets” have been abstractions so far. The paper is specific about what actually flowed through training, and naming the datasets makes the recipe concrete instead of generic:

Task categoryDatasets used
RetrievalMS MARCO, HotpotQA, Natural Questions, PAQ, StackExchange, Natural Language Inference, SQuAD, ArguAna, BioASQ, FiQA, FEVER (11 datasets)
ClassificationAmazonReviews, AmazonCounterfactual, Banking77, Emotion, IMDB, MTOPIntent, ToxicConversations, TweetSentimentExtraction (8 datasets)
Clusteringraw arXiv, raw bioRxiv, raw medRxiv cluster labels, plus TwentyNewsgroups
STSSTS12, STS22, STS-Benchmark

Most of those 11 retrieval datasets do not ship with hard negatives of their own, which is exactly why Chapter 6 opened by describing a separate encoder model fine-tuned specifically to mine them. For the classification data, the paper reuses each example’s own text field as the query and its label_text field as the positive document, sampling other label texts in the same batch as negatives — which is precisely the setup that makes in-batch negatives dangerous for classification data in the first place: if two examples in a batch happen to share a label, one becomes an accidental false negative for the other. For STS, a pair only becomes a positive training example when its human-annotated similarity score is 4 or higher (on whatever scale that dataset uses), and hard negatives are mined by BM25 lexical overlap, keeping only candidates ranked 2nd-or-lower in similarity that also score below 2.5 — close enough in wording to be genuinely confusable, far enough in meaning to be a legitimate negative.

The ablation’s fourth row, and why it fails where latent attention succeeds

Chapter 6’s ablation table has a row this session hasn’t discussed yet: self-attention pooling, at 69.10 bidirectional. It sits close to latent-attention’s 69.32 average — close enough to ask why the paper didn’t just add a self-attention layer instead of building an entire separate dictionary mechanism. Self-attention pooling means exactly what it sounds like: one more ordinary self-attention layer, over the sequence itself, before the final mean pool — no external trainable dictionary, just more of what the decoder already does internally.

Look at the retrieval-specific slice, not just the overall average, and the two methods split apart: mean pooling scores 58.71 on the 15 BEIR retrieval tasks, self-attention scores 58.64 — slightly worse, despite adding an entire extra layer of parameters and compute. Latent-attention, on the same slice, scores 59.36 — clearly ahead of both. The paper’s own explanation is direct: the underlying LLM already has dozens of self-attention layers dedicated to learning representations; stacking one more self-attention layer on top doesn’t give the model any new kind of computation to lean on, so it adds cost without adding value. The dictionary in latent attention is different in kind, not just in degree — it is a fixed, external reference frame the model learns once and reuses for every document, not another copy of the same self-referential computation the decoder was already doing.

Why this sharpens Chapter 8’s argument. “Add more attention” and “add a learned bottleneck” sound like the same move from a distance, but the ablation shows they are not interchangeable. More self-attention is more of the same computation the decoder already runs. A trained external dictionary is a genuinely new, fixed reference the sequence gets compared against — and it is specifically that novelty, not just added parameters or added compute, that the retrieval-slice gap between 58.64 and 59.36 is evidence for.

The full hyperparameter table, and what it costs to run

For completeness, here is every training-schedule number the paper reports, alongside the two already covered (batch size 128, 7 hard negatives per example):

ParameterValue
Training stepsstage 1: 20,000    stage 2: 18,000
Warm-up steps500
Weight decay0.03
OptimizerAdam
Padding sideright

Multiply steps by batch size and you get the total number of training triplets each stage actually processes — a number worth having, since it grounds “20,000 steps” in something more tangible than a step count:

stage 1: 20,000 × 128 = 2,560,000 query–document examples seen
stage 2: 18,000 × 128 = 2,304,000 query–document examples seen

Roughly 4.86 million labeled examples in total, across both stages — and every one of them, remember, brings 7 curated hard negatives along with it, so the actual number of query–document comparisons the model learns from is several times that figure again.

Why does NV-Embed turn OFF in-batch negatives specifically when training on classification and clustering data?

Chapter 7: Pooling Arena

One simulation to make Chapters 5 and 6 tangible at once: watch what a causal mask hides, and watch how three different pooling strategies distribute their attention over a token sequence differently.

What the simulation shows

The top panel is a 10-token attention grid. Toggle the mask type and watch which (row, column) cells go dark: under a causal mask, the grid is a strict lower triangle — token 3 can see tokens 1–3, never 4–10. Flip to bidirectional and every cell lights up — full visibility, both directions, exactly the change Chapter 6 argued for.

The bottom panel shows, for the same 10-token sequence, how much each position contributes to the final pooled embedding under three strategies: EOS-last (all the weight sits on position 10, everything else contributes zero), mean (every position gets an identical, flat share), and latent-attention (an uneven, content-sensitive weighting — illustrative here, standing in for what a trained cross-attention would learn to emphasize, since the real learned weights aren’t published token-by-token). Switch pooling methods and watch the weight bars redistribute.

Two of the three bar patterns are hard constraints, not choices — worth being clear about which. EOS-last pooling cannot produce anything other than a single spike at the final position; that is the entire definition of the method, not a limitation the simulation is imposing. Mean pooling cannot produce anything other than a flat line; averaging is uniform by construction. Only latent-attention’s bars are free to take any shape training decides is useful — which is precisely Chapter 5’s point restated visually: two of these three methods have their weighting distribution fixed before a single gradient is ever computed, and one of them has to earn its weighting distribution through training.

Causal vs. bidirectional, and three ways to pool

Top: which token positions can attend to which, under each mask. Bottom: how much each position contributes to the final pooled vector, under each pooling method.

Reading the grid correctly

The causal grid is not merely “half the information” — it’s the wrong half for embedding purposes. The most-informed positions under a causal mask are the last few tokens, exactly mirroring why EOS-last pooling seemed reasonable in Chapter 5. But under bidirectional attention, position 1 is just as informed as position 10 — every position has genuinely seen the entire document. That equalizing effect is precisely why the ablation table in Chapter 6 showed EOS-last pooling gaining the most from the mask flip (+1.35, the largest gain in that table): EOS-last was the pooling method most damaged by causal masking’s asymmetry, so removing that asymmetry helped it the most.

Count the lit cells by hand, for the 10-token grid the simulation draws. Under a causal mask, position i can see positions 1 through i — that’s i visible cells in row i. Sum across all 10 rows:

1+2+3+…+10 = 10(10+1)2 = 55 visible cells, out of 100 total — exactly 55%

Under bidirectional attention every one of the 100 cells lights up — 100%. Averaged over the whole sequence, a causally-masked model only ever sees 55% of the pairwise context a bidirectional model sees for the same tokens — and the deficit isn’t spread evenly. Position 1 sees only itself (1 out of 10, a brutal 10%), while position 10 sees everything (10 out of 10, 100%). That row-1-through-row-10 gradient is the recency bias Chapter 5 described, drawn as a shape instead of asserted as a claim.

The full leaderboard, for context

Zoom out to where all this lands. Here is where NV-Embed’s final 69.32 sat among the frontier embedding models on the MTEB leaderboard the paper reports against, as of the paper’s publication:

ModelMTEB avg (56 tasks)Notes
text-embedding-3-large (OpenAI)64.59proprietary, API-only
UAE-Large-V164.64bidirectional encoder
mxbai-embed-large-v164.68bidirectional encoder
LLM2Vec65.01public data, extra warm-up training phase
GritLM66.76unifies embedding + generation in one model
Gecko66.31distills a bidirectional model from an LLM
E5-mistral-7b-instruct66.63proprietary GPT-4 synthetic data
SFR-Embedding-Mistral67.56fine-tunes E5-mistral further
Voyage-large-2-instruct68.28proprietary
NV-Embed69.32public data only, trained from base Mistral 7B directly

The detail worth sitting with: several of the closest competitors depend on proprietary GPT-4-generated synthetic training data. NV-Embed reaches the top of the same leaderboard using only public datasets — which is precisely the paper’s stated motivation for pairing architectural changes (Chapters 5–6) with a two-stage recipe rather than reaching for more data.

Zoom in on the retrieval-only slice specifically, since it is the task the whole two-stage design was built around. NV-Embed’s final retrieval score across the 15 BEIR tasks in MTEB is 59.36 — also the highest reported in the paper’s comparison, ahead of E5-mistral-7b-instruct despite E5-mistral having access to proprietary synthetic training data NV-Embed deliberately avoided. Put Chapter 6’s per-category breakdown next to this leaderboard and the full shape of the argument closes: architecture (Chapters 5–6: latent attention pooling, bidirectional attention) bought the quality gain visible in every row of the ablation tables, and training recipe (Chapter 6: two-stage, hard-negative-curated) bought the data efficiency that let public-only data reach a score proprietary-data competitors needed GPT-4 synthetic examples to match.

Concept → realization: how you’d actually pull these weights from a real model

The simulation’s bottom panel is labelled illustrative because the paper doesn’t publish per-token attention weights — but the mechanism for extracting the real ones, from a real checkpoint, is just three lines, because Chapter 5 already gave you the exact formula:

python
# given Q: (l, 4096) real decoder hidden states for a real document,
# and the trained latent_dict: (512, 4096) loaded from the checkpoint
attn_weights = softmax(Q @ latent_dict.T, dim=-1)   # (l, 512) -- real, not illustrative
per_token_mass = attn_weights.sum(dim=-1)          # doesn't apply -- mass is PER dictionary entry, not per token
per_entry_usage = attn_weights.sum(dim=0)           # (512,) -- which dictionary entries this document leaned on

That last line is the more interesting one in practice: sum the attention weights down the token axis (rather than across the dictionary axis) and you get a 512-long vector saying which of the 512 learned dictionary entries this particular document activated most — a real, inspectable signature of what the model decided mattered, obtainable from any checkpoint with three lines of code and zero retraining. This is the kind of concrete, checkable claim the illustrative bars in this simulation are standing in for.

Does the 55% pattern hold at other sequence lengths?

The 55-out-of-100 count above is specific to a 10-token grid. Generalize the same hand-count to an N-token sequence and see whether “a little over half” is a coincidence of the number 10 or a structural fact about causal masking. Row i always sees exactly i cells out of N, so the total visible fraction is:

visible fraction = (1+2+…+N) = N(N+1)/2 = 12 + 12N

Plug in a few values and watch the fraction drift toward exactly one half as the sequence grows:

N (sequence length)Visible cellsTotal cellsFraction
105510055.0%
2021040052.5%
501,2752,50051.0%
1005,05010,00050.5%

The formula ½ + 12N makes the limit obvious without needing the table: as N grows, the second term shrinks toward zero and the visible fraction converges to exactly 50%. The 55% the simulation shows at N=10 isn’t the “real” number — it’s a small-sequence artifact, inflated above the true limiting behavior by the +5% that vanishes as documents get longer. For a real document hundreds of tokens long, a causally-masked model sees, on average across all its positions, almost exactly half of the pairwise context a bidirectional model sees for the identical tokens — not “a little over half,” the asymptotic answer the 10-token toy example only approximates.

The full per-task breakdown, not just the headline average

69.32 is an average across 56 tasks grouped into 7 categories. NV-Embed’s own results table reports every category separately, and it is worth seeing where the number actually comes from — and where the model comes closest to not winning:

CategoryNV-Embed (latent-attn)NV-Embed (mean pool, same model)
Retrieval (15 tasks)59.3658.71
Reranking (4 tasks)60.5960.75
Clustering (11 tasks)52.8052.80
Pair Classification (3 tasks)86.9185.85
Classification (12 tasks)87.3587.06
STS (10 tasks)82.8482.53
Summarization (1 task)31.2030.49
Average (56 tasks)69.3268.98

Two honest details this fuller table surfaces. First, latent-attention doesn’t win every single category — on Reranking, the mean-pool variant of the same underlying model actually scores marginally higher (60.75 versus 60.59). Second, Clustering ties exactly (52.80 both ways), meaning the pooling swap made literally zero difference on those 11 tasks. The 69.32-versus-68.98 average gap is real and latent-attention wins it, but it is the net of six categories favoring latent-attention by small-to-large margins, one category favoring mean pooling slightly, and one tying — not a uniform win recorded in every row. Reading past the average into the breakdown is exactly the habit Chapter 3’s C-Eval caveat was training you to have.

Zoom out one more level, to the competitor leaderboard from earlier in this chapter. NV-Embed also does not win every category against every rival: Voyage-large-2-instruct posts 89.24 on Pair Classification against NV-Embed’s 86.91, and several bidirectional encoders edge out small margins on Clustering specifically. NV-Embed’s claim to the top of the leaderboard rests on the average across all 56 tasks and on leading the retrieval slice specifically — the two numbers the paper foregrounds — not on topping literally every one of the seven category columns individually.

Reading the simulation and the leaderboard as one argument

The top panel and the bottom table of this chapter are more connected than they look at first glance. The top panel shows a mechanism fact: how much of a 10-token sequence each position can see, under each mask. The bottom table shows a consequence fact: what happens to a real model’s leaderboard score once that mechanism fact is fixed by removing the mask, and once the pooling method built on top of it is chosen well. Neither half of this chapter proves the other by itself — the attention-visibility count (55%, or the exact 50% limit at long sequence lengths) is pure combinatorics, true regardless of what any model actually does with that visibility; the leaderboard numbers are empirical, measured by actually training and evaluating real systems. What connects them is the causal story Chapter 6 argued and this chapter’s simulation makes visible: a model structurally forced to see less of its own input (the causal grid) measurably produces worse embeddings (the +1.35-point EOS-last gain from removing that restriction), and a model that additionally chooses how to weight what it sees, rather than averaging blindly or reading only the last position, does measurably better still (latent-attention’s lead over both alternatives, in every ablation table this session has shown). The simulation makes the mechanism intuitive; the leaderboard is the receipt.

What the simulation would need to add to stop being illustrative

Worth being precise about the gap between this chapter’s simulation and a real trained model, since Chapter 7’s own code snippet already showed you how to close it. The mask-visibility grid is not illustrative at all — causal masking really does restrict attention to exactly the lower-triangular pattern drawn, for any Transformer using it, no approximation involved. The pooling weight bars are a different story: EOS-last and mean pooling are exact, because both are hard-coded rules with no parameters to learn. Only the latent-attention bars are a stand-in, and specifically for one reason — the weights a real, trained NV-Embed checkpoint would produce for a specific 10-token sequence depend on what those 10 tokens actually say, which the simulation has no way to know without either running a real 7-billion-parameter model in your browser or shipping a lookup table of pre-computed examples. Everything else about the mechanism — the shape of the computation, the fact that weights are content-dependent rather than fixed, the fact that averaging happens after re-weighting — is exactly as derived in Chapter 5, not simplified for the simulation’s sake.

Under a causal mask, why is the EOS-last pooling method specifically the one that benefits most from switching to bidirectional attention?

Chapter 8: Latent Bottlenecks as Design

Step back from both papers and name the pattern directly. This chapter is the payoff of pairing MLA and NV-Embed in the same session: they were written by different teams (DeepSeek-AI and NVIDIA), for different problems, submitted to arXiv three weeks apart — DeepSeek-V2 on May 7, 2024, NV-Embed on May 27, 2024 — and they independently arrived at the same architectural idea.

Neither paper cites the other. There is no shared codebase, no shared author, no reason to expect convergence — and yet Section 2.1 of one and Section 3.2 of the other both open with a version of the same complaint: the standard way of handling a wide or long representation (cache every key and value; average every token) is wasteful, and the fix is a small trained set of vectors the bigger representation gets projected through. When two independent teams solving unrelated problems reach for the identical structural idea within the same month, that’s a signal worth taking seriously — not proof the idea is correct, but evidence it was, by mid-2024, sitting close enough to the surface of common practice that two different sets of engineers, staring at two different bottlenecks, found the same tool.

The pattern, stated once, generally

The shared move. Take a representation that is wide, or long, or expensive to keep around in full. Force it through a narrow channel — a small set of vectors, far smaller than the original — whose contents are learned, not hand-designed. Use that narrow channel as the thing you actually store, or attend to, or build your final answer from. The narrowness is not a limitation to apologize for; it is the entire point. A narrow, trainable channel forces the network to decide, through gradient descent, what is actually worth keeping — a decision a fixed rule (average everything, keep every head, take the last position) can never make.

The same pattern, two different bottleneck shapes

Line up the two mechanisms directly, because the differences are as instructive as the similarity:

MLA (Chapters 1–4)NV-Embed latent attention (Chapters 5–7)
What gets compressedone token's key & value, per layera whole document's token sequence
The bottleneckctKV ∈ ℝ512 — fresh per tokenK=V ∈ ℝ512×4096 — one fixed dictionary, shared by every document
Computed atevery decoding step, every requestonce, trained; reused unchanged at inference
What it buyssmaller memory footprint — less compute and storagea better fixed-size summary — a small amount of extra compute for higher quality
Axis compressedper-token width (across attention heads)sequence length (across token positions)

That last two rows are the sharpest contrast, and easy to miss: MLA’s bottleneck exists to save resources — it is purely a cost-reduction move, and Chapter 3’s ablation showed it happens to also improve quality, almost as a bonus. NV-Embed’s bottleneck adds a small amount of compute (that 2.1M-parameter cross-attention and MLP) specifically to buy quality — it is not trying to save anything, it is spending a little to get a better answer than a free alternative (mean or last-token pooling) would give. Both are “latent bottlenecks.” They sit on opposite ends of the cost-versus-quality motivation.

Concept → realization: the two mechanisms, side by side in code

python
# MLA: compress ONE TOKEN's key/value, fresh, every decoding step
def mla_compress(h_t):                     # h_t: (5120,)  one token's hidden state
    c_t = W_DKV @ h_t                    # (512,)        <- cached; recomputed every new token
    return c_t                            # nothing shared across tokens or requests

# NV-Embed: compress a WHOLE SEQUENCE against a fixed, trained dictionary
latent_dict = nn.Parameter(torch.randn(512, 4096))  # trained ONCE, shared by every document

def nvembed_pool(Q):                       # Q: (l, 4096)  the WHOLE document's hidden states
    O = softmax(Q @ latent_dict.T) @ latent_dict  # (l, 4096)     unchanged in eval mode
    O = mlp(O)                            # (l, 4096)
    return O.mean(dim=0)                    # (4096,)       ONE vector, whole document

Read the two functions’ signatures and the contrast is exact: mla_compress runs once per token, on the fly, and its output is thrown away the moment the request ends. nvembed_pool runs once per document, using a dictionary that was fixed at the end of training and never changes again. One is a runtime bottleneck; the other is a learned, static bottleneck. Same shape of idea — project down, use the projection, expand back out if needed — deployed for opposite reasons.

Why this pattern keeps reappearing

Once you can name it, you start noticing it is not confined to these two 2024 papers. Any time a system needs to represent something large with something small — and needs that small thing to be good, not just small — a learned bottleneck beats a fixed rule, because gradient descent gets to decide what “good” means for the actual downstream objective, instead of an engineer guessing in advance. MLA and NV-Embed are two instances of the same design principle solving two problems that, on the surface, have nothing to do with each other — inference memory and search relevance — because the underlying shape of the problem (“compress this, but don’t lose what matters”) is the same shape both times.

If you have taken the Audio LLMs session in this series, you have already met a third instance without a name: SALMONN’s window-level Q-Former, which cross-attends a block of audio-encoder frames against a small set of learned query vectors to shrink a long audio sequence down to a manageable number of “tokens” before an LLM ever sees them. Structurally, that is NV-Embed’s latent attention layer wearing a different costume — a token sequence (there, audio frames; here, document tokens) cross-attending into a small, trained set of vectors that decide what survives. Three papers, three modalities — language-model serving, text embedding, audio understanding — independently reaching for cross-attention into a learned dictionary as the tool of choice whenever “too many vectors” needs to become “a manageable few, well chosen.”

Note too what did not happen: neither team reached for a hand-designed heuristic dressed up as sophistication — a weighted average with hand-tuned weights, a rule based on token position or part of speech. Both went straight to “let the network learn the weighting function,” because by 2024 that had become the default instinct for representation problems, not a novel idea requiring justification. That shift in default instinct — from hand-designed pooling to learned pooling, from hand-designed cache eviction to learned compression — is arguably the more durable lesson of this session than either paper’s specific formula.

When a latent bottleneck is the wrong tool

Naming a pattern is not the same as recommending it everywhere, and it’s worth being precise about when this move earns its cost. A learned bottleneck needs something MHA, mean pooling, and last-token pooling all get for free: training. MLA’s down- and up-projection matrices have to be learned jointly with the rest of the network from the start — you cannot retrofit MLA onto an already-trained MHA checkpoint and expect the compressed latent to carry the right information, because nothing ever taught it what to keep. NV-Embed’s dictionary is the same story: those 512 entries only became useful lookup targets because contrastive training shaped them to be, over the two-stage recipe Chapter 6 walked through. If you don’t have the training budget or the labelled contrastive data to shape a bottleneck properly, a fixed rule that at least behaves predictably — GQA, mean pooling — can be the more defensible engineering choice, even knowing it leaves performance on the table.

There is a broader family this pattern belongs to, for readers who have seen compression bottlenecks in other contexts: an autoencoder's narrow middle layer, or a VQ-VAE's discrete codebook, are both, at heart, the same move — force information through a channel narrower than the input, and let training decide what the channel keeps. What MLA and NV-Embed add to that lineage is specificity about where the bottleneck sits relative to a live Transformer: not as a separate autoencoding step bolted onto the side, but woven directly into the attention mechanism itself, compressing exactly the tensor that was about to be cached or pooled anyway, with no extra forward pass required to reach it.

The test you can apply to any new architecture paper. When you see a paper introduce a small learned vector or matrix that a bigger representation gets projected through, ask two questions: what is it compressing along — width, depth, sequence length, batch, something else? And is the bottleneck being used to save a resource, or to buy quality at a small extra cost? Those two questions locate almost any latent-bottleneck design on the same map this chapter just drew for MLA and NV-Embed.

A third instance, hiding inside the first one

Chapter 5’s aside on query compression is worth revisiting now that the pattern has a name. MLA doesn’t just compress keys and values — it separately compresses queries, through WDQ and WUQ, down to a query latent ctQ ∈ ℝ1,536. Structurally, this is a third instance of the exact same “narrow, learned channel” move — a wide representation forced through a small trained bottleneck. It didn’t earn a row in Chapter 8’s comparison table because it fails the first question the “test you can apply to any architecture paper” callout asks: what is it compressing along, and is the compressed thing ever the thing being stored? Queries are never cached — only keys and values persist across decoding steps — so query compression buys neither serving memory (MLA’s reason) nor pooling quality (NV-Embed’s reason). It buys a third, distinct thing entirely: smaller activation memory during training, where every token in a batch needs its query held simultaneously for the backward pass, not just the current one.

Extend the comparison table's own logic one more row and the family becomes three members deep, not two:

MLA’s KV latentMLA’s query latentNV-Embed’s dictionary
Motivationsave serving memorysave training activation memorybuy pooling quality
Ever cached at inference?yes — this is the cacheno — queries are never cachedno — it’s a fixed parameter, not a per-request value
Recomputed how often?every decoding step, every requestevery decoding step, every requestnever — trained once, frozen at inference

Same shape of idea, three distinct reasons to reach for it. The pattern this session has been naming isn’t “MLA does X, NV-Embed does Y, and they happen to rhyme” — it is a genuinely general move that a single paper can even use twice, for two different reasons, within the same architecture.

The complete inventory, laid out side by side

It is easy to lose track of exactly how many learned matrices each mechanism actually introduces, buried across four chapters of derivation. Laid out plainly, MLA is the heavier machinery of the two — more moving parts, each doing a narrower job:

MechanismLearned matrices/parameters introduced
MLAWDKV, WUK, WUV (content path) + WDQ, WUQ (query path) + WQR, WKR (RoPE path) — seven matrices, per layer
NV-Embed latent attentionone K=V dictionary + a 2-layer MLP with GELU — a handful of parameters, once, not per layer

That contrast tracks the “save vs. spend” distinction from earlier in this chapter almost exactly. MLA’s seven matrices exist because saving memory at the scale DeepSeek-V2 operates at is worth a correspondingly intricate mechanism, engineered once and amortized across every request the model ever serves. NV-Embed’s much simpler dictionary-plus-MLP is enough because its job — buy a quality improvement over a free pooling rule — doesn’t require nearly as much machinery to pay for itself.

A question worth asking about your own next project

Turn the “test you can apply to any architecture paper” callout above into something you could actually run against a system you are designing, not just a paper you are reading. Whenever you find yourself about to cache everything, average everything, or keep only the most recent thing — the three fixed rules this session has spent nine chapters replacing — stop and ask the same two questions MLA and NV-Embed each answered differently: what is the wide or long thing here, specifically, and what narrow, trainable channel could stand in for it if you gave gradient descent the chance to decide what’s worth keeping? Sometimes the honest answer is that you don’t have the labelled data or training budget to shape that channel well, in which case Chapter 8’s earlier caveat applies and the fixed rule remains the right engineering choice for now. But the question is worth asking explicitly, every time, rather than reaching for “average everything” purely out of habit — which, as this session’s two independent case studies suggest, is exactly the habit the field itself was quietly moving away from by 2024.

What would falsify this chapter’s argument

A pattern worth naming is only useful if it is possible to imagine evidence against it, so state that plainly before moving on. This chapter’s claim would weaken if a well-resourced team tried the learned-bottleneck approach on a comparable problem and a fixed rule won anyway — if, say, a careful ablation showed a hand-designed cache-eviction policy matching MLA’s memory savings without giving up MLA’s accuracy gain, or a simple weighted-average pooling rule (weighting by inverse document frequency, say, a classic information-retrieval heuristic) matched latent-attention’s MTEB score. Neither paper in this session ran that comparison, which is worth flagging rather than glossing over: both papers compare their learned mechanism against cruder fixed rules (uniform averaging, last-token, indiscriminate head-sharing), not against the best fixed rule domain experts could design with real effort. The claim this chapter supports, precisely, is that a learned bottleneck beat the fixed rules these two teams actually tried — a real, useful, well-evidenced claim, but a narrower one than “learned bottlenecks always beat fixed rules,” which neither paper set out to test.

What is the key difference in MOTIVATION between MLA's latent bottleneck and NV-Embed's latent attention dictionary, even though both are "compress through a narrow learned channel"?

Chapter 9: Connections & Limits

One last pass over what this session did and did not cover, and where to go next.

The numbers worth carrying out of this session

NumberWhat it is
1,966,080 elements / 3.75 MBMHA's KV cache per token, DeepSeek-V2's real shape (Ch. 1)
34,560 elements / 67.5 KBMLA's KV cache per token, same model (Ch. 3) — a 56.9× reduction
2.25the (non-integer) GQA group count MLA's cache is equivalent to (Ch. 3)
24.9×MLA's cache reduction vs. MHA at large-MoE scale, WITH higher accuracy on every benchmark (Ch. 3)
512, 4,096, 8NV-Embed's latent dictionary size, hidden width, and head count (Ch. 5)
69.32NV-Embed's final MTEB score — latent-attention pooling + bidirectional attention + two-stage training, public data only (Ch. 6–7)
+5.14the MTEB point gain from stage-2 training alone, same architecture (Ch. 6)
93.3% / 5.76×DeepSeek-V2’s production KV-cache reduction and throughput gain vs. its own predecessor, DeepSeek 67B — a different comparison from the controlled 24.9× ablation above (Ch. 0)
128K / 4K → 32KDeepSeek-V2’s served context length, reached via YaRN extension applied only to the decoupled RoPE channel (Ch. 3)

If you can reconstruct where every one of those nine numbers comes from without looking back, you have the session.

What MLA does not solve

Multi-head Latent Attention is a memory win, not a free lunch. The weight-absorption trick that lets inference skip materializing full keys and values (Chapter 2) is specifically a low-rank matrix algebra identity — it works because attention scores are linear in the compressed latent. Any future change to how attention scores are computed (a nonlinearity inserted between the projections, for instance) would need to preserve that identity or lose the trick entirely. And the decoupled RoPE channel (Chapter 3), while small, is not zero — it is a permanent 64-dimensional tax per head that exists purely to route around an incompatibility, not because the model benefits from it directly.

There is also a training-time cost this session set aside. Compression is a choice made once, at architecture design time, and baked into every weight the model ever learns — you cannot decide after pretraining that dc should have been 768 instead of 512; that would mean retraining WDKV, WUK, and WUV from scratch, since their shapes are fixed by the choice. MLA is a decision you get to make once, correctly, at the start — not a runtime knob.

One more precision worth stating plainly, because it is easy to over-claim: MLA reduces how much memory attention needs, not how much arithmetic it does. Generating token t still requires scoring against every one of the t−1 cached positions — the same O(t) per-step score computations MHA needs, the same asymptotic shape. Weight absorption changes which weights get multiplied when, folding two matrix multiplications into one ahead of time, but it does not shrink the number of positions the current token has to attend over. A long conversation is still O(n) work per new token under MLA, exactly as it is under MHA — MLA never touches that count. What MLA changes is how many bytes have to move through memory to do that O(n) work, which is a real and large win precisely because Chapter 1 established decoding is memory-bandwidth-bound, not compute-bound — but it is a different claim than “attention got computationally cheaper,” and conflating the two overstates what the mechanism actually does.

What NV-Embed does not solve

The latent attention layer (Chapter 5) improves pooling quality, but it is not free at inference time the way MLA's compression is — every document still needs the extra cross-attention and MLP pass, a small but real cost mean pooling doesn't have. And the two-stage recipe (Chapter 6) is a training-time decision, baked into the final weights; you cannot toggle between "in-batch negatives on" and "off" behavior after the fact; whichever stage's data mixture the model finished training on is the behavior you get.

There is a sharper limit worth naming directly, because it caps how much the pooling mechanism can help in practice: NV-Embed trains and evaluates with a maximum sequence length of 512 tokens, matching prior work for a fair comparison. Everything Chapter 5 built — the trainable dictionary, the cross-attention re-weighting, the mean pool that only averages after re-weighting has happened — only ever operates on the first 512 tokens of whatever document it is handed. A ten-page PDF is thousands of tokens long; the latent attention layer never sees the vast majority of it. However good the pooling mechanism is at deciding what to keep among the tokens it sees, it cannot rescue information the 512-token truncation already discarded before pooling even started. The quality gains in Chapters 5–7 are gains within a fixed, fairly short context budget, not evidence the mechanism scales gracefully to book-length documents.

It is also worth being honest about what the ablation numbers in this session can and cannot tell you. Every comparison in Chapters 3, 6, and 7 held the rest of the architecture fixed and varied one thing — attention mechanism, mask type, pooling method — which is exactly what makes them trustworthy evidence of that one variable's effect. But it also means neither paper tells you what happens if you stack every trick from both papers into one system: an MLA-style compressed KV cache feeding a decoder that also uses NV-Embed-style latent attention pooling for embedding tasks. That combination is a natural next experiment this session leaves on the table.

The two papers, filed under what kind of claim each number is

Before the final comparison table, it is worth sorting every number this session leaned on into the kind of evidence it actually is, because the two papers offer three genuinely different kinds and blurring them together is the easiest way to walk away over-confident. Derived numbers — 1,966,080, 34,560, 2,097,152, the 8.65× parameter-count ratio — are pure arithmetic, true by construction the moment the formula and the inputs are fixed; you verified several of these by hand yourself. Controlled-ablation numbers — the MHA/GQA/MQA benchmark table, the MLA-vs-MHA table at two scales, the pooling-and-mask ablation — are empirical, but isolate one variable at a time by construction, which is what makes them trustworthy evidence about that one variable’s effect specifically. Production numbers — the 93.3% cache reduction, the 5.76× throughput figure, NV-Embed’s leaderboard rank — are real deployment or competition outcomes, but they reflect the sum of every design decision in the system at once, not just the one this session focused on. All three kinds are legitimate evidence. None of them substitutes for either of the other two, and a paper (or a lesson) that only ever quotes the flattering kind without naming which kind it is has quietly made itself harder to argue with than it deserves to be.

Comparison table, for your notes

Cheap, fixed ruleThe paper's answerWhat you give up / gain
KV cacheGQA / MQA (share heads)MLA (compress to a latent)gain: memory AND quality, not a trade
Positional encoding under compressionapply RoPE to the compressed key directlydecoupled RoPE (small separate channel)small fixed width tax, keeps absorption intact
Query path during trainingkeep queries at full widthcompress queries too (dc′=1,536)smaller training-time activation memory; no cache effect, queries aren't cached
Sequence poolingmean pool / last-token poollatent attention layersmall extra compute, consistently higher MTEB score
Attention direction for embeddingskeep the causal mask from pretrainingremove it during contrastive training0.8–1.4 point gain, no extra parameters
Negative sampling policyone fixed policy for all datain-batch negatives for retrieval, curated-only for stage 2+13.4 points on classification, no retrieval regression

Five exercises to check you actually absorbed this

Work these from memory, without scrolling back — each one targets a specific chapter's core derivation:

#ExerciseChecks
1Derive MHA's cache formula, then compute it for a hypothetical 96-head, 96-dim-per-head, 40-layer modelCh. 1
2Explain, in one sentence, why WUK can be absorbed into WQ but only if nothing position-dependent sits between themCh. 2 & 3
3Given dc=384 and dhR=48 at 48 layers, compute the MLA cache per token and the equivalent GQA group countCh. 3
4Explain why NV-Embed's K and V matrices in the latent attention layer are literally the same tensorCh. 5
5Explain why stage 2 turns off in-batch negatives even though it demonstrably improves stage 1's retrieval scoreCh. 6

A worked method, on different numbers, so you can check your own approach to exercise 3 without seeing its answer: take dc=640, dhR=80, at l=72 layers. The cache is (640+80)×72 = 720×72 = 51,840 elements per token. The equivalent GQA group count solves 2×ng×128×72 = 51,840, which gives ng = 51,840 ÷ 18,432 ≈ 2.81 — again not a whole number, again sitting between two GQA rows a real deployment would have to choose between. If your answer to exercise 3 follows the same two steps — sum the two widths, multiply by layers; then divide by 2×dh×l — and lands on a similarly non-integer group count, your method is sound even before you check the specific digits.

Exactly which parts of each paper this session drew from

For readers who want to go straight to the source material rather than take this session’s word for anything, here is a more granular map than the references table below gives on its own — the specific sections and tables each chapter’s claims trace back to:

PaperSection / tableWhat it grounds
DeepSeek-V2§2.1.1–2.1.2standard MHA and the low-rank KV compression derivation (Ch. 1–2)
DeepSeek-V2§2.1.3, Table 1decoupled RoPE and the four-mechanism cache-formula comparison (Ch. 3)
DeepSeek-V2Appendix C.1, Table 6the 7B-dense MHA/GQA/MQA benchmark ablation (Ch. 3)
DeepSeek-V2Appendix C.2, Table 7the MLA-vs-MHA ablation at two MoE scales (Ch. 3)
DeepSeek-V2§3.1.4 (YaRN), §3.2.3context-length extension and the production throughput figures (Ch. 0, 3)
NV-Embed§3.1–3.2bidirectional attention and the latent attention layer mechanism (Ch. 5–6)
NV-Embed§4.1–4.2the named training datasets across all four task categories (Ch. 6)
NV-Embed§3.3, §5.1 / Table 5 (Appendix A)the two-stage recipe and full hyperparameter table (Ch. 6)
NV-Embed§5.3.1–5.3.2the causal-vs-bidirectional and pooling-method ablations (Ch. 6–7)
NV-EmbedTable 1 (leaderboard)the full 10-model leaderboard and per-category breakdown (Ch. 7)

Where to go from here

This session assumed you already knew the basics of Transformer attention and what an embedding is for. From here: Similarity Metrics covers what happens on the other side of NV-Embed's output vector — how cosine similarity and its relatives actually compare two embeddings. Vector Databases covers what you do with millions of embeddings once you have them. And RAG shows the whole embedding pipeline wired into a real retrieval-augmented generation system, the application that motivated most of NV-Embed's own training data choices in the first place. If mixture-of-experts routing (the other half of DeepSeek-V2's architecture, which this session deliberately set aside to focus on attention) is next, look for the DeepSeekMoE-focused session in this series.

Ten things you should be able to do without looking back

A different kind of self-check than the numbers table or the five exercises above — not “what is the number” but “can you do the thing.” One capability per chapter, roughly in order:

#Can you…
0explain why KV cache size depends on concurrent users × context length, not model size alone
1derive the MHA cache-per-token formula, 2×nh×dh×l, from first principles — not recall it
2explain, using the associativity of matrix multiplication, why weight absorption lets MLA skip reconstructing full keys at inference
3state why RoPE breaks that same associativity, and describe the decoupled fix in one sentence
4predict, without touching a slider, which of the four calculator bars moves when you change nh versus l
5explain why NV-Embed's K and V being literally the same matrix makes "dictionary" more than a metaphor
6state, in one sentence, why a causal mask is actively harmful for an embedding task specifically
7compute what fraction of an N-token sequence a causally-masked model sees on average, for any N
8name the one-sentence difference in motivation between MLA's bottleneck and NV-Embed's, despite their identical shape
9list one real limitation of each mechanism that neither paper's headline numbers advertise

If any row stalls you, that chapter's own quiz and worked examples are the fastest way back in — each row maps directly onto the chapter with the same number.

Glossary, for quick reference

TermOne-line definition
KV cachestored keys and values for every past token, reused so decoding never recomputes them from scratch
Prefill / decodethe one-time parallel pass over a prompt, versus the repeated one-token-at-a-time generation phase after it
MHA / GQA / MQAmulti-head, grouped-query, and multi-query attention — full, partially-shared, and fully-shared key/value pairs across heads
Latent vectora compressed stand-in for a wider representation, small enough to store, rich enough to reconstruct from
Down-/up-projectionthe pair of learned linear maps that compress into a latent, then expand back out of one
Weight absorptionpre-fusing two consecutive linear maps offline, using associativity, so inference skips materializing the middle result
Decoupled RoPEMLA's small, separate, uncompressed channel that carries positional rotation so it never blocks weight absorption
Poolingcollapsing a sequence of per-token vectors into one fixed-size vector for the whole sequence
Latent attention layerNV-Embed's trained dictionary (K=V) that a document cross-attends into before a final mean pool
Bidirectional attentionremoving the causal mask so every token can attend to every other token, both directions
In-batch negativesreusing other examples already in a training batch as free negative examples for contrastive loss
Hard negativea deliberately curated wrong answer that is superficially similar to the correct one
MTEB / BEIRthe 56-task Massive Text Embedding Benchmark, of which the 15 retrieval tasks are also known as BEIR

References

PaperSection(s) this session covers
DeepSeek-AI. “DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model.” arXiv:2405.04434, May 2024.§2.1 (Multi-head Latent Attention) and Appendix C (MHA/GQA/MQA and MLA/MHA ablations) — Chapters 1–4
Lee, C., Roy, R., Xu, M., Raiman, J., Shoeybi, M., Catanzaro, B., Ping, W. (NVIDIA). “NV-Embed: Improved Techniques for Training LLMs as Generalist Embedding Models.” arXiv:2405.17428, May 2024.§3 (Bidirectional Attention, Latent Attention Layer, Two-stage Instruction-Tuning) and §5.3 (ablations) — Chapters 5–7
“The purpose of computing is insight, not numbers.” — Richard Hamming. Both papers in this session could have chased their targets with brute force — more heads shared away, more training data bought. Instead each one asked a sharper question about what a fixed-size vector actually needs to preserve, and let training find the answer inside a bottleneck small enough to be practical. That is the insight worth carrying out of this session, past the specific formulas: compression, done well, is a modeling decision, not a compromise.
Which statement best summarizes the actual relationship between MLA's KV cache reduction and its accuracy, according to the ablation numbers in Chapter 3?