MLA (Multi-Head Latent Attention) Explained
Table of contents
- Overview
- Algorithm
- References
Overview#
Let's talk about MLA.
We'll first get an overview of MLA and its benefits for long contexts, then work through KV compression, weight absorption, and decoupled RoPE. A reference implementation is available here.
MLA was first introduced in DeepSeek-V2's technical report. More recent models use MLA or its variants, including DeepSeek-V3.2 with DeepSeek Sparse Attention (DSA), Kimi K2.7 Code with MLA, and Kimi K3 with a hybrid of Kimi Delta Attention (KDA) and Gated MLA.
So what exactly is MLA?
It is a variation of Attention module in Transformer model (originally Multi-Head Attention, introduced in Attention is All You Need), where it saves the past KV values (KV Cache) in a low-rank latent space, by using a down projection weight (which refers to Down projection Weight for KV) and conceptually reconstructs them during inference using up-projection matrices and (which refer to Up projection Weights for K and V).
MLA can require more computation than standard MHA, but reduces the size of the KV cache stored and loaded during decoding. This is because MLA caches one shared KV latent rather than separate keys and values for every head. We will compare this in more detail in the following section. So fundamentally speaking, MLA makes a tradeoff between memory bandwidth (KV Cache size) and compute (FLOPs to calculate), without losing performance.
Since MLA reduces the size of KV Cache per layer per token, we have two advantages where
- Loading KV Cache from memory is faster
- We can store more context in the given memory capacity
Now that we've gone through the "interview" answer, let's dive deeper.
Algorithm#
Notations#
dimensions#
| Symbol | Meaning | Value |
|---|---|---|
| model hidden dimension | 5120 | |
| number of attention heads | 128 | |
| head index | 0 … 127 | |
| per-head dimension | 128 | |
| KV latent (compression) dimension | 512 | |
| query latent dimension | 1536 | |
| per-head dimension of the decoupled RoPE part | 64 | |
| query/key dimension used for attention-score scaling | without RoPE; with decoupled RoPE | |
| query token position | — | |
| key token position | — | |
| number of cached tokens attended to, including the current token | — |
matrices#
| Symbol | Shape | Meaning |
|---|---|---|
| input hidden state of one token (during decode) | ||
| query down-projection | ||
| query latent, | ||
| head 's content-query up-projection | ||
| head 's query projection for the decoupled RoPE part | ||
| head-shared key projection for the decoupled RoPE part | ||
| output projection of concatenated head outputs | ||
| KV down-projection | ||
| KV latent of one token, (the thing we cache; in the paper) | ||
| all cached latents stacked, one row per token | ||
| key / value up-projections | ||
| head 's column block of / | ||
| keys / values rebuilt from the latent, and | ||
| head 's keys / values for all cached tokens, and | ||
| head 's query for the current token | ||
| absorbed query, | ||
| head 's attention weights, | ||
| attention-weighted latent, | ||
| head 's attention output | ||
| decoupled RoPE key, shared across heads and also cached | ||
| RoPE rotation matrix at position , built from frequencies |
1. Low-Rank KV Joint Compression#
Figure 1. MLA uses a shared KV latent instead of separate per-head keys and values. [1]
As explained, MLA uses a shared low-rank latent to represent keys and values, with separate up-projections producing each head's keys and values.
KV Down Projection#
During down projection, we project the entire hidden dimension into latent dimension without separating heads (notice there is no in the equation below).
The hidden state of one token is projected by (we will talk about later): In DeepSeek-V2, this is .
From now on we will call as KV latent interchangeably. KV Latent is stored as KV cache for each token, for each layer.
Also keep in mind this is a KV "joint" compression, where we don't save latents for K and V separately but in one KV latent instead.
KV Up Projection#
For attention operation, we need to load the cached latent , which is every past token's stacked as rows () and expand each one back into keys and values.
Unlike the down projection, which used a single matrix , the up projection has a separate pair of matrices for each head. Every head uses the same latent but each has its own pair of up-projection matrices, and . You can think of it as each head reading the same compressed vector but in different ways.
In DeepSeek-V2, each head turns the latent into a key and a value. Across all 128 heads, the up projections produce () dimensional keys and dimensional values per token, while caching only the shared dimensional KV latent.
Q Projection#
MLA also compresses the query, projecting the hidden state into a query latent before up-projecting it into each head's query. These projections run during both training and inference. While query compression does not reduce the KV cache, it is used to reduce activation memory during training.
Figure 2. The DeepSeek-V2 report introduces query compression to reduce training activation memory. [2]
As described in Section 2.1.2 of the paper,[2] query compression reduces activation memory during training and uses a separate compression dimension , independent of . DeepSeek-V2 uses and .
Comparing Projection Shapes in MHA vs MLA#
Now that we've gone through how MLA compresses and projects its Q, K, and V, let's compare the Q, K, V projections in MHA and MLA.
In MHA, each head projects directly from the hidden states. In MLA, all heads share a query down-projection and a joint KV down-projection, then use their own up-projection matrices.
| Projection | MHA | MLA |
|---|---|---|
| Query | ||
| Key | ||
| Value |
The weight shapes are:
| Weight | MHA | MLA |
|---|---|---|
| Query | , | |
| Key | , | |
| Value | The same as the key, then |
This gives the following shape changes for each head:
| Projection | MHA | MLA |
|---|---|---|
| Query | ||
| Key | ||
| Value | ||
| As seen here, the final shapes are the same for both cases: for the query and for the cached keys and values. |
Here is a visual explanation on Q compression / projection and KV compression / projection from a YouTube video I watched which I found helpful:
Figure 3. A shared KV latent is up-projected into keys and values. [5]
Figure 4. Naive MLA reconstructs keys and values before computing attention. [5]
2. Weight Absorption, or Attention Without K/V Reconstruction#
Naive decoding#
As mentioned above, cache refers to every past token's KV Latent stacked as rows (). When a new token comes in, its query is split into 128 heads where head 's query is .
So how do we compute attention score?
The most intuitive way is to rebuild the keys from the cache, then calculate the attention score against them:
We can break this down into two steps:
- Projected : : , costing
- Attention score: : , costing
We can see where the cost comes from. Attention score computation is relatively cheap, but to get there we push every row of the cache through , and we do this again for every decoding step. The longer the context () gets, the worse it becomes.
It turns out there is a much cheaper way to get the exact same result.
Weight Absorption#
What if we don't up-project K right away?
Let's come back to the attention score equation.
What we did above was to do first (in the parentheses) and then do matrix multiplication with . However, since matrix multiplication is associative, we are free to choose which product to compute first.
Using the transpose rule , we can reverse the order of the matrices inside the transpose as:
Then, by the associative property of matrix multiplication, we can compute () first and multiply the result by (). Intuitively, you can think of as each query head projected into the latent space.
So instead of applying to all rows of the cache , we can apply it once to the query:
- Absorbed query : . This costs , independent of .
- Attention score : . This costs .
Interestingly, while the result is mathematically identical, the cost of compute is very different:
| FLOPs | |
|---|---|
| Naive (Non-absorbed) | |
| Absorbed |
Absorption reduces the -dependent cost from to , while adding FLOPs to transform the query. For integer , this uses fewer FLOPs than naive reconstruction and avoids materializing during decode.
Figure 5. Weight absorption computes attention from the cached latent without reconstructing full keys and values. [5]
The same kind of optimization can be applied during value aggregation, where we compute the weighted sum of the values with attention weights.
Each head's output before the final projection can be expressed as:
Again, by associativity, there are two ways to get the same output.
Naive (Non-absorbed): Up project the cached latents first, then compute their weighted sum.
- Value Projection: : , costing FLOPs.
- Weighted Value Aggregation: : , costing FLOPs.
Absorbed: Compute the weighted sum of the cached latents first, then up-project the result.
- Weighted Latent Aggregation: : , costing FLOPs.
- Value Projection: : , costing FLOPs.
The value projection now operates on one aggregated latent instead of all cached latents. We never materialize the full value matrix .
The cost per head, excluding the common final output projection, is:
| FLOPs | |
|---|---|
| Naive (Non-absorbed) | |
| Absorbed |
In both paths, we concatenate the head outputs and apply the same output projection:
3. Decoupled RoPE (Rotary Positional Embedding)#
Decoupled RoPE is the final part to understand to draw the full picture of MLA.
It turns out RoPE is incompatible with weight absorption, which we've just been talking about! Or to be more precise, we can use weight absorption but that won't lead to the optimization we've seen when RoPE is applied.
Why is so? Let's first recall how RoPE is calculated. RoPE rotates Q and K matrices after their projection. This can be expressed as a rotation matrix, where for every pair of dimensions at token position :
where
- denotes the rotated vector
- indexes the dimension pair
- is the rotation frequency for that pair
Let be the query position and the key position. The score becomes:
Now let's put this into MLA. Without RoPE, the score against token is what we saw in weight absorption:
Applying RoPE, the rotated key of token is , so lands right in the middle:
As seen in the equation, if we apply RoPE, the absorbed query contains in the middle which differs for each past token. So, while keeping only the latent cache, we need a separate per token: the same product times.
Figure 6. Applying RoPE to the full key makes the absorbed query depend on each key position.
RoPE Projection#
In order to avoid this scenario and use weight absorption, MLA decouples RoPE from the KV latent. Intuitively, you can think of this as splitting up content information and position information explicitly.
Each query and key is split into:
- a content part that is never rotated (what we've seen in Sections 1–2)
- a small RoPE part () in charge of position information
Looking at just the RoPE part: the query side projects the compressed query with , one per head, so in total. The key side projects the raw hidden state with . Unlike , is shared across all heads, so we only cache one 64-dim RoPE key per token.
The score is now the sum of the two parts:
Since the first term involves no rotation, weight absorption works exactly as before. In code, the content components of the query and key are often labeled nope, short for “no positional embedding,” because RoPE is not applied to them.
The second term has the rotation, but never goes through or , so we can cache it already rotated and use it directly. In total, the cache holds elements per token.
For the same query-head count, per-head dimension, and cache dtype, the KV cache sizes per token per layer are:
| Method | Cache elements per token per layer |
|---|---|
| MHA | |
| GQA | |
| MQA | |
| MLA |
Here, is the number of KV heads in GQA.
With BF16, MLA stores bytes per token per layer. For 60 layers, batch size 1, and , the total KV cache payload is bytes, or about 2.11 GiB.
Putting the FLOPs together#
We can now put the pieces together. For one decode token, both paths create the query, cache the new token's KV latent and RoPE key, and apply the final output projection. Absorption only changes the computation between these projections.
We count an by matrix multiplication as FLOPs, with one fused multiply-add (FMA) counting as 2 FLOPs. All counts below are for batch size 1, one decode token, and one layer, across all 128 heads.
Using the dimensions above, the matrix-multiplication costs are:
| Stage | Naive | Absorbed |
|---|---|---|
| Query down- and up-projections, including the RoPE query | 91.23M | 91.23M |
| New token's KV latent and RoPE key projections | 5.90M | 5.90M |
| Content attention, as derived in Section 2 | ||
| RoPE attention scores | ||
| Final output projection | 167.77M | 167.77M |
The shared projections cost about 264.90M FLOPs per decode step, independent of context length. Adding attention gives:
| Context length | Naive | Absorbed |
|---|---|---|
| 1 | 298.53M | 298.73M |
| 1,024 | 34.71G | 583.66M |
| 32,768 | 1.10T | 9.43G |
We can see a clear win as context length grows, since absorption removes the need to reconstruct every cached key and value during decode step.
Is Weight Absorption Always the Best Choice?#
On precomputing merged query weights#
DeepSeek describes both under weight absorption. Here, we use "merged weights" specifically for weight products that are precomputed and stored.
I didn't mention this when explaining weight absorption but we can also consider precomputing , combining the query up projection with the key up projection. In this case, instead of computing and transforming it into the latent space, we project directly using the merged weights.
Figure 7. The DeepSeek-V2 report describes absorbing key and value up-projections into query and output projections. [2]
Since the merged weights can be reused while the model parameters remain fixed, we need only one matrix multiplication instead of two separate projections.
However, after doing some research myself, I found that most inference implementations do not precompute merged weights, contrary to the description of the paper. For example, vLLM removed the materialized query/key and value/output weight products in favor of sequential matrix multiplications, citing lower memory usage and FLOPs.
Figure 8. vLLM replaces materialized weight products with sequential matrix multiplications. [3]
DeepSeek-V3's public inference implementation also evaluates the absorbed computation through sequential operations. It directly projects wq_b (query up projection weight) right after wq_a (query down projection weight) and normalization, instead of merged weights.
Figure 9. DeepSeek-V3 applies query down-projection, normalization, and up-projection sequentially. [4]
The relevant codes can be found in https://github.com/junuxyz/transformer-lab/blob/mla_absorbed/src/components/attention/mla.py.
Thanks for reading and feel free to reach out if there's anything misleading or unclear.
References#
- Sebastian Raschka. Multi-Head Latent Attention (MLA).
- DeepSeek-AI. DeepSeek-V2 technical report, Section 2.1.2.
- vLLM. MLA get rid of materialization, PR #14770.
- DeepSeek-AI. DeepSeek-V3 inference implementation.
- Welch Labs. How DeepSeek Rewrote the Transformer [MLA].