Skip to content

MLA, dim by dim ​

Opening ​

This note uses one lens throughout: write attention as tensor contractions, while keeping every dimension explicit. From this view, several seemingly separate MLA phenomena can be understood more deeply and more uniformly. Specifically, we get a few interesting conclusions:

  • In prefill, MLA uses the KV latent bottleneck to reduce projection FLOPs, but it cannot absorb too early: the quadratic attention term should still live in dk,dv, not dc.
  • In decode, the best order changes because the cache already exists. After absorbing key/value projections, the length-t cache computation can go through the latent state instead of reconstructed per-head K,V.
  • RoPE gets in the way exactly because it makes the latent-to-key map position-dependent, which breaks full key absorption.
  • Q compression delays head expansion on the query side, reducing activation scale and usually projection FLOPs as well.
  • MTP-style speculative decoding behaves differently on MLA because MLA decode no longer repeatedly reads the full KV cache; it reads a smaller latent cache and does extra computation on top.

MLA sketch

Tensor View ​

Before the derivations, we first fix the tensor view used throughout the note. Every matmul below is treated as a PyTorch-style batched tensor contraction, and every attention activation follows the (h, T, D) order whenever it has a head axis: head first, sequence second, feature last. Per-head weights start with h; shared activations such as X, CKV, and CQ do not have an h axis. When a shared activation multiplies a per-head weight, the missing head axis is broadcast.

Einsum is all you need. In practice every one of the multiplications below can be written as a single einsum expression, and we find it much easier to keep track of what is being contracted vs. broadcast that way. For example:

Q=XWQ⟺Qh,T,dk=einsum("Td, hdk -> hTk",X,WQ).

The two indices that appear on both sides (like the d in this example) are contracted; the ones that appear only on one side (like h on WQ, or T on X) are preserved. Every "broadcast" I use later is just this: an activation without an h axis multiplies against a weight with an h axis, and the result inherits h.

Broadcast is legal only when dims line up. Broadcasting between batch dims follows the standard PyTorch rule: when we align the batch dimensions of A and B from the right (the last two axes are reserved for the matmul), each pair must either match or have a 1 on one side — the side with the 1 is the one being replicated. A leading axis missing entirely (as when X is only 2D above) counts as an implicit 1. If a pair is neither equal nor has a 1, the multiplication is undefined and the framework raises an error. Every matmul in this note satisfies the rule; whenever an activation without an h axis meets a weight with h, it is the activation's implicit 1 on the head axis being expanded to h.

Transpose convention. Throughout, A⊤ denotes swapping only the last two axes — the math-style matrix transpose that leaves batch dims untouched. In PyTorch this is A.transpose(-1, -2) (or the equivalent A.mT), not A.T, which reverses all axes. So K with shape (h,T,dk) has K⊤ of shape (h,dk,T), and QK⊤ contracts on the last two axes cleanly.

Output projection is an einsum contraction, not a batched matmul. The final output projection Y=OWo with O:(h,T,dv) and Wo:(h,dv,d) is the one place where we write the result as (T,d) rather than the "batched" (h,T,d). Under the einsum convention this reads Y=einsum("hTv, hvd -> Td",O,Wo): the h axis is a contracted index because it appears on both operands but not on the output, so the sum over heads is implicit. Equivalently, O can be reshaped to (T,hdv) and Wo to (hdv,d), giving the standard "Concat + WO" form of multi-head attention; the two views are byte-identical and both use FLOPs=2Thdvd.

Tensor FLOPs formula. For a 3D batched matmul the FLOPs count is simple. If we write the two operands as A with shape (a1,…,ap,m,k) and B with shape (b1,…,bq,k,n), and let B1,…,Br be the broadcast batch dims of the output, then

FLOPs=2⋅B1⋯Br⋅m⋅n⋅k.

The mental shortcut: multiply every output dimension by the contracted dimension k, then by the 2 that comes from counting one multiply-add as two FLOPs. Every FLOPs count in this note is one application of this formula.

Two examples for concreteness:

  • Q=XWQ where X:(T,d) and WQ:(h,d,dk) produces Q:(h,T,dk). The contracted dim is d; the output dims are h,T,dk; so FLOPs=2hTddk.
  • S=QK⊤ where Q:(h,T,dk) and K⊤:(h,dk,T) produces S:(h,T,T). The contracted dim is dk; the output dims are h,T,T; so FLOPs=2hT2dk.

With this tensor view in hand, the rest of the note is mostly shape accounting: write the candidate contraction order, identify the output dimensions and contracted dimension, and read off the FLOPs.

Takeaway. Once every operation is written as a tensor contraction, FLOPs become explicit shape accounting.

Notation and Preliminaries ​

For broader background on the cache tradeoffs from MHA/MQA/GQA to MLA, see Su Jianlin's note. Here I do not try to survey those variants. I use MHA as the clean algebraic reference, and MLA as the object of the derivation.

For the derivations below, I count one multiply-add as 2 FLOPs, and I only count matrix multiplication FLOPs. I temporarily ignore softmax, masks, normalization, RoPE elementwise rotation, and bias terms. Unless stated otherwise, prefill formulas use the full T2 attention matrix; causal attention replaces the full square by the lower-triangular visible pairs, but does not change the comparisons below.

The main dimensions are:

SymbolMeaningShape / role
Tprefill sequence lengthnumber of tokens in prefill or training
tdecode KV-cache lengthnumber of cached historical tokens
dhidden sizexi∈Rd
hnumber of attention headshead index s=1,…,h
dkper-head query/key dimensionqis,kis∈Rdk
dvper-head value dimensionvis,ois∈Rdv
dcMLA KV latent dimensionciKV∈Rdc
dqcMLA query latent dimensionciQ∈Rdqc

The MHA reference form is:

Q=XWQ,K=XWK,V=XWV.

The shapes are:

X:(T,d),WQ,WK:(h,d,dk),WV:(h,d,dv),

so

Q,K:(h,T,dk),V:(h,T,dv).

Then attention is:

S=QK⊤,P=softmax(S),O=PV,Y=OWo.

Here S,P:(h,T,T), O:(h,T,dv), and the output projection Y=OWo uses the head-contraction convention from the previous section.

The MLA KV path replaces per-head K,V projections with a shared latent:

CKV=XWDKV,K=CKVWUK,V=CKVWUV.

The shapes are:

WDKV:(d,dc),CKV:(T,dc),WUK:(h,dc,dk),WUV:(h,dc,dv).

Without query compression, the query path is still Q=XWQ. With query compression, it is:

CQ=XWDQ,Q=CQWUQ,

with

WDQ:(d,dqc),CQ:(T,dqc),WUQ:(h,dqc,dk).

These are the only structural definitions needed later. The remaining sections ask what FLOPs we get when the same objects are multiplied in different orders.

Baseline FLOPs ​

I start from baseline multi-head attention (MHA), derive each matrix multiplication and its FLOPs, and then compare it with MLA to see where the optimization actually comes from.

Prefill Comparison ​

Prefill FLOPs consist of three parts: the input projections, the quadratic attention computation, and the final output projection. The table below gives the result before the detailed derivation.

MethodInput-projection FLOPsAttention FLOPsOutput-projection FLOPs
MHA2Tdh(2dk+dv)2hT2(dk+dv)2Thdvd
MLA without query compression2Tddc+2Thddk+2Thdc(dk+dv)2hT2(dk+dv)2Thdvd
MLA with query compression2Tddc+2Tddqc+2Thdqcdk+2Thdc(dk+dv)2hT2(dk+dv)2Thdvd

Plugging in the DeepSeek-V2 attention configuration d=5120, h=128, dk=dv=128, dc=512, dqc=1536 gives:

MethodInput-projection FLOPsRelative to MHA
MHA503.3MT100%
MLA without query compression206.6MT41.0%
MLA with query compression104.9MT20.8%

MLA prefill does not mainly reduce the quadratic attention FLOPs. It significantly reduces the linear projection FLOPs, and this reduction is especially clear with query compression.

MHA Prefill ​

For MHA prefill, the complete score expression is

S=QK⊤=X(T,d)WQ(h,d,dk)WK⊤(h,dk,d)X⊤(d,T).

From a FLOPs perspective, the important point is to minimize the coefficient of the dominating T2 term. Since dk<d, the computation order is to first construct Q=XWQ and K=XWK, and then compute S=QK⊤:

X(T,d)WQ(h,d,dk)→Q(h,T,dk),FLOPs=2Thddk,X(T,d)WK(h,d,dk)→K(h,T,dk),FLOPs=2Thddk,Q(h,T,dk)K⊤(h,dk,T)→S(h,T,T),FLOPs=2hT2dk.

The complete value and output chain is

P(h,T,T)X(T,d)WV(h,d,dv)Wo(h,dv,d)→Y(T,d).

Value/output order: use (P(XWV))Wo, not ((PX)WV)Wo or P(X(WVWo)).

The standard order is XWV→V,PV→O,OWo→Y, with total cost FPVWo=2Thddv+2hT2dv+2Thdvd.

Computing PX first costs 2hT2d+2Thddv+2Thdvd. Precomposing WVWo also gives the quadratic term coefficient d. Since dv≪d, the standard order is cheaper: 2hT2dv instead of 2hT2d.

Combining these terms, the dense MHA prefill FLOPs are

FMHA,prefill=2Tdh(2dk+dv)+2hT2(dk+dv)+2Thdvd.

For causal prefill or training, the number of visible query-key pairs is T(T+1)/2. An implementation that exploits the lower triangle replaces 2hT2(dk+dv) with hT(T+1)(dk+dv).

MLA Prefill ​

For MLA prefill, first compute the shared KV latent CKV=XWDKV. Without query compression, the complete score expression is

S=X(T,d)WQ(h,d,dk)WUK⊤(h,dk,dc)WDKV⊤(dc,d)X⊤(d,T).

With query compression, it becomes

S=X(T,d)WDQ(d,dqc)WUQ(h,dqc,dk)WUK⊤(h,dk,dc)WDKV⊤(dc,d)X⊤(d,T).

Key-projection order: use (XWDKV)WUK, not X(WDKVWUK).

The relevant key-projection chain is:

X(T,d)WDKV(d,dc)WUK(h,dc,dk)→K(h,T,dk).

Computing the shared latent first gives

XWDKV→CKV,CKVWUK→K,F(XWDKV)WUK=2Tddc+2Thdcdk.

If we first form the effective per-head key projection WDKVWUK and then multiply it by X, the activation-side cost is

FX(WDKVWUK)=2Thddk,

not counting the additional one-time weight-precomposition cost 2hddcdk.

With d=5120, dc=512, dk=128, h=128, the shared-latent order costs 22.0MT, versus 167.8MT for the effective-WK order. So compute XWDKV first and expand to heads afterward.

Prefill score order: use Q(CKVWUK)⊤, not (QWUK⊤)(CKV)⊤.

Because dk<dc, applying WUK⊤ to Q first moves the dominant score contraction from dk to dc.

The preferred order is therefore

CKV(T,dc)WUK(h,dc,dk)→K(h,T,dk),FLOPs=2Thdcdk,Q(h,T,dk)K⊤(h,dk,T)→S(h,T,T),FLOPs=2hT2dk.

The shared latent projection CKV=XWDKV costs 2Tddc. The query projection costs 2Thddk without query compression, or 2Tddqc+2Thdqcdk with query compression.

Once V=CKVWUV has been reconstructed, the V projection costs 2Thdcdv, and the common PV contraction costs 2hT2dv. The final output projection, which is the same as in MHA, costs 2Thdvd.

This computation order is implementable at the kernel level. The most direct implementation materializes K,V first and then calls a standard FlashAttention kernel. A more memory-efficient implementation computes K,V from CKV inside each attention tile.

Without query compression, the dense MLA prefill FLOPs are

FMLA,prefill,noQ=2Tddc+2Thddk+2Thdc(dk+dv)+2hT2(dk+dv)+2Thdvd.

With query compression, they are

FMLA,prefill,Qcomp=2Tddc+2Tddqc+2Thdqcdk+2Thdc(dk+dv)+2hT2(dk+dv)+2Thdvd.

For causal prefill or training, replace the common attention term 2hT2(dk+dv) with hT(T+1)(dk+dv).

Decode Comparison ​

Cached decode FLOPs consist of the current-token projections, the attention computation that scales with cache length t, the MLA-specific local transformations, and the final output projection.

MethodCurrent-token projectionLength-t attention termRemaining local terms
MHA2dh(2dk+dv)2ht(dk+dv)2hdvd
MLA without query compression2ddc+2hddk4htdc2hdkdc+2hdcdv+2hdvd
MLA with query compression2ddc+2ddqc+2hdqcdk4htdc2hdkdc+2hdcdv+2hdvd

If we only look at FLOPs, MLA decode is not necessarily smaller than MHA. The MHA decode-length term is 2ht(dk+dv), while the MLA latent path uses 4htdc. Under the DeepSeek-V2 dimensions dk=dv=128 and dc=512, the arithmetic work of the latter is not small.

However, decode is usually memory-bound. MHA stores and reads the full per-head cache

K≤t:(h,t,dk),V≤t:(h,t,dv),

while MLA stores the shared latent C≤tKV:(t,dc),

plus a smaller decoupled RoPE cache.

With h=128, dk=dv=128, dc=512, the MHA KV cache contains 32768 dimensions per token, while the MLA latent cache contains 512 dimensions per token.

Therefore, MLA decode acceleration mainly comes from the cache change: it replaces the full per-head KV reads and writes in long-context decode with shared-latent cache reads and writes. The additional FLOPs are moved into smaller computations that can be executed inside fused kernels.

MHA Decode ​

With KV cache, historical K≤t,V≤t have already been computed in previous decode steps. The current step only computes and appends the new q,k,v. Across all heads, the current-token projection cost is 2dh(2dk+dv).

The computation order is determined by the cache: first generate q,k,v from the current token x, append k,v to the cache, and then use only the current q to read historical K≤t.

The score and value contractions are

q(h,1,dk)K≤t⊤(h,dk,t)→score(h,1,t),FLOPs=2htdk,p(h,1,t)V≤t(h,t,dv)→o(h,1,dv),FLOPs=2htdv.

The common output projection costs 2hdvd. Therefore, the cached MHA decode FLOPs are

FMHA,decode,cached=2dh(2dk+dv)+2ht(dk+dv)+2hdvd.

MLA Decode ​

For MLA decode, the cache stores the shared latent instead of full per-head K,V. At the current step,

x(1,d)WDKV(d,dc)→cKV(1,dc),FLOPs=2ddc.

The new cKV is appended to C≤tKV:(t,dc). Without query compression, the current-token projection cost is 2ddc+2hddk. With query compression, it is 2ddc+2ddqc+2hdqcdk.

The complete score expressions are

scorenoQ=x(1,d)WQ(h,d,dk)WUK⊤(h,dk,dc)(C≤tKV)⊤(dc,t),scoreQcomp=x(1,d)WDQ(d,dqc)WUQ(h,dqc,dk)WUK⊤(h,dk,dc)(C≤tKV)⊤(dc,t).

Decode score order: use (qWUK⊤)(C≤tKV)⊤, not q(C≤tKVWUK)⊤.

Both variants first absorb WUK⊤ into the current query, then multiply by the latent cache:

q(h,1,dk)WUK⊤(h,dk,dc)→qabs(h,1,dc),FLOPs=2hdkdc,qabs(h,1,dc)(C≤tKV)⊤(dc,t)→score(h,1,t),FLOPs=2htdc.

This applies WUK only to the current query, not all t cached latents.

Decode value order: use (pC≤tKV)WUV, not p(C≤tKVWUV).

First form the latent weighted sum, then apply WUV:

p(h,1,t)C≤tKV(t,dc)WUV(h,dc,dv)→o(h,1,dv),

This costs 2htdc+2hdcdv and avoids reconstructing V for all cached tokens.

The common output projection costs 2hdvd. Therefore, the cached all-head MLA attention FLOPs are

FMLA,decode,attn=2hdkdc+4htdc+2hdcdv.

For the whole MLA decode layer,

FMLA,decode,cached,noQ=2ddc+2hddk+2hdkdc+4htdc+2hdcdv+2hdvd,FMLA,decode,cached,Qcomp=2ddc+2ddqc+2hdqcdk+2hdkdc+4htdc+2hdcdv+2hdvd.

Here 4htdc is the decode-length part: one 2htdc term computes the scores from the latent cache, and the other computes the weighted latent value.

This order is implementable at the kernel level. A fused MLA decode kernel can compute qabs, stream the cache blocks of C≤tKV, run online softmax, accumulate pC≤tKV, and finally multiply by WUV.

Why RoPE Gets in the Way ​

MLA's absorption trick has an implicit prerequisite: the map from latent cache to key is fixed across positions. Without RoPE, for cached token i:

kis=ciKVWUKs,scoreis=qs(kis)⊤=(qs(WUKs)⊤)(ciKV)⊤.

Then we can first compute a current-token vector qabss=qs(WUKs)⊤, and reuse it for all cached positions. This is the clean algebraic structure behind latent-cache decode.

The problem is that RoPE inserts a position-dependent rotation into the key path. Let Rt denote the query rotation at the current position, and Ri denote the key rotation at cached position i:

scoreis=(Rtqs)(Rikis)⊤=qsRt⊤Ri(WUKs)⊤(ciKV)⊤.

Now the matrix Rt⊤Ri(WUKs)⊤ depends on i. This means we cannot form one qabss and reuse it for all cached tokens. Reconstructing and caching all rotated per-head keys is of course possible, but that basically moves us back toward a normal KV cache.

The practical solution is to decouple the content channel and the positional channel:

scoreis=qcontent,abss(ciKV)⊤+(Rtqropes)(Riki,ropes)⊤.

The content channel still uses the MLA latent-cache path; the RoPE channel is stored separately and kept small. Therefore RoPE does not make MLA impossible. It only blocks the cleanest version where the whole key is hidden behind one shared latent cache.

Why Compress Q Too ​

KV compression and Q compression solve different system problems. KV compression changes what we store and read during decode; Q compression factorizes query projection:

CQ=XWDQ,Qs=CQWUQs.
  • Activation memory. Without query compression, the query activation scale is Thdk; with query compression, the compressed query state scale is Tdqc. The key condition is simple:

    dqc<hdk.

    For DeepSeek-V2, h=128, dk=128, and dqc=1536, so hdk=16384. This significantly reduces the query activations that need to be saved, which is especially important in long-context prefill/training.

  • Prefill/training FLOPs. Q compression can also bring a compute win. The query projection changes from:

    FQ=2Tdhdk⟹FQ,comp=2Tddqc+2Thdqcdk.

    When dqc(d+hdk)<hddk, Q compression reduces query-projection FLOPs. Under DeepSeek-V2 dimensions, FQ=167.8MT, while FQ,comp=66.1MT. This also explains why the previous prefill comparison shows a clear gap between MLA no-Q and MLA Q-compression.

    • Decode. Q compression only affects the current-token query path; it does not reduce the latent cache-length term by itself.

      Query-side order: use (cQWUQ)WUK⊤, not cQ(WUQWUK⊤).

      The score path is:

    x(1,d)WDQ(d,dqc)WUQ(h,dqc,dk)WUK⊤(h,dk,dc)(C≤tKV)⊤(dc,t).

    Implementations usually keep the smaller per-head bottleneck dk. Excluding the shared x→cQ step, the all-head score cost is:

    2h(dqcdk+dkdc+tdc)instead of2h(dqcdc+tdc).

    Since dk is much smaller than dc and dqc, the two-step path is usually cheaper.

Q compression saves activation memory and may also save prefill projection FLOPs, but it changes decode compute only modestly; KV compression is still the main decode-cache bandwidth win.

Why MLA Weakens MTP's Speculative-Decoding Gain: A Roofline View ​

MTP

The DeepSeek-V3 Technical Report uses MTP as an additional training objective. For prediction depth k, the MTP module concatenates the previous-depth representation and a shifted token embedding:

hi′k=Mk[RMSNorm(hik−1);RMSNorm(Emb(ti+k))],h1:T−kk=TRMk(h1:T−k′k).

Then the shared output head predicts the k-th additional future token:

Pi+k+1k=OutHead(hik).

During normal inference, DeepSeek-V3 can directly discard these MTP modules, so the MLA decode path is not affected by MTP. The interesting question appears when we repurpose MTP for speculative decoding: MTP drafts D future tokens, and the main model verifies them in one pass. The naive expectation is that MLA (memory savings) and speculative decoding (compute batching) simply stack. They do not. The correct comparison is not "MTP on MLA vs plain MLA", but "MTP on MHA vs MTP on MLA" — and once we look at it through a roofline lens, we see why MLA changes the game.

Why Speculative Decoding Works on MHA ​

MHA decode is deeply memory-bound. Per generated token, the model reads a full h(dk+dv)-dimensional KV cache from HBM once, then does a relatively small amount of arithmetic on it. With DeepSeek-V2/V3-style dimensions, h(dk+dv)=128(128+128)=32768 per token. On any modern GPU this sits well below the roofline ridge (typically ∼281 FLOP/byte in bf16), so the compute units spend most of each decode step waiting for the cache to arrive.

Speculative decoding exploits this imbalance. When you verify D drafted tokens in one forward pass, you do NOT pay for D× the KV bandwidth: the KV cache has already been read for the current-token verification, and each additional token just does another matmul against the same weights that are already on-chip. The marginal cost per speculation token is small — you are using compute that would otherwise sit idle. This is the real reason speculative decoding accelerates MHA decode: not by reducing the amount of work, but by moving previously idle compute into useful work.

Why This No Longer Works on MLA ​

MLA compresses the KV cache per token from h(dk+dv)=32768 down to dc=512 — a ∼64× reduction in bytes moved per decode step. But queries stay per-head: each of the h heads must load its own copy of the shared latent to compute its own dot product. The ratio mq/mc climbs to h=128 (see Fergus Finn's "Economics of Speculative Decoding"). Arithmetic intensity — FLOPs per byte moved — rises together.

The consequence is sharp: even a modest number of speculation tokens (e.g., D=2) is enough to push MLA decode past the roofline ridge into the compute-bound regime on typical GPUs. Once decode is compute-bound, verifying an extra speculation token is no longer close to free — the compute cost grows nearly proportionally with D. The same cache compression that makes MLA efficient is what removes the memory-bound headroom speculative decoding needs. On MLA, MTP and the main decode are contending for the same compute resource.

Concretely, the average per-accepted-token cost of MTP on top of MLA is:

F¯MTP≈Fdraft(1:D)+Fverify(D)A.

For MTP to further accelerate decode on top of MLA, we would need:

Fdraft(1:D)+Fverify(D)<A⋅FMLA,decode.

MLA has shrunk the right-hand side (main decode is cheap now), but it has NOT shrunk the left. The MTP draft path costs the same:

Fdraft(k)≈4d2+FTRMk,decodeMLA+2dV.

Here 4d2 comes from projecting [hk−1;Emb] from 2d back to d, FTRMk,decodeMLA is the extra decode cost of the MTP Transformer block, and 2dV is the shared output-head cost for full logits. And the verify pass now sits in the compute-bound regime, so its cost grows almost linearly in D rather than being amortized by cache reuse.

There is a second, subtler cost. TRMk runs on a shifted hidden stream hk, whose token alignment differs from the main model — so it cannot directly reuse the main CKV cache. If the MTP block also uses MLA internally, it maintains its own latent cache for this stream. MLA's savings on the main path do not automatically extend to the draft path either.

Summary ​

Speculative decoding accelerates MHA decode because MHA decode is memory-bound: verification cycles that would otherwise sit idle can be filled by drafted tokens at almost no additional cost. MLA changes the operating point. By compressing the KV cache by ∼64× and raising arithmetic intensity by a factor of h, MLA pushes decode close to (or past) the roofline ridge, into the compute-bound regime. There, every speculation token has to pay its full compute cost. MLA and MTP are not independent accelerations — they draw from the same pool of compute, and MLA has already consumed most of it.

This is not an argument against combining them; DeepSeek-V3 clearly still benefits from MTP as a training objective. It is an argument for calibrating expectations: on top of MLA, MTP-based speculative decoding delivers a much smaller speedup than it does on top of MHA.