Home › Machine learning › Modern layers
algorithm low

Attention variants

Problem Teams serving large sequence models find that memory for cached past context limits how many requests they can handle and how long a context they can support. They must choose an attention design that balances quality against that memory.

Multi-head, multi-query, grouped-query, sliding-window and latent attention; same softmax(QKᵀ/√d)V core, different trade-offs between quality and KV-cache size.

prefill O(T²·d) per layer (O(T·w·d) with window w); decode O(T·d) per token, memory-bound on reading the KV cache
attentiongqamqasliding-windowmlakv-cachetransformer
Overview

Every variant computes softmax(q·Kᵀ/√d_h)·V per head; they differ in how many K/V heads exist and which keys a query sees. Decoding re-reads the whole KV cache per token, so serving speed and batch size are set by KV bytes, not FLOPs.

Key facts

Use when

  • GQA (4-8 KV heads): default for any served decoder LLM.
  • Sliding window: long, mostly local context, interleaved with global layers; windowed attention also bounds cost on high-resolution images (Swin Transformer).

Avoid when

  • MQA (1 KV head) on large models: reported quality loss and instability.
  • Sliding window with no global layers: long-range retrieval degrades.

Deep dive

How it works

In: Q [T,H,d_h], K and V [T,H_kv,d_h], a mask · Out: attended values [T,H,d_h] · State: KV cache of 2·layers·H_kv·d_h elements per token · Cost: prefill O(T²·d), decode O(T·d) per token

Shapes for one sequence of length T: Q: [T, H, d_h], K, V: [T, H_kv, d_h], with H % H_kv == 0.

  1. Query head h reads KV head h / (H / H_kv). MHA is H_kv = H, MQA is H_kv = 1, GQA is in between.
  2. Score s_ij = q_i · k_j / √d_h for allowed j. Causal: j ≤ i. Sliding window w: i − w < j ≤ i.
  3. p_ij = exp(s_ij − max_j s_ij) / Σ_j exp(…), output o_i = Σ_j p_ij v_j.
  4. Concatenate heads and project with W_o.
MHA   q0 q1 q2 q3 q4 q5 q6 q7      GQA (H_kv=2)  q0..q3 → kv0   q4..q7 → kv1
      k0 k1 k2 k3 k4 k5 k6 k7      MQA (H_kv=1)  q0..q7 → kv0
sliding window w=3, causal mask for query 5:  keys 3 4 5 visible, 0-2 masked

MLA (DeepSeek-V2). Cache one latent c_t = W_dkv·x_t (d_c = 512) per token and layer, plus a shared 64-dim RoPE key. K and V for all heads are up-projections of c_t. The up-projection for K can be absorbed into W_q, and the one for V into W_o, so decode attends directly over the latents. Cache: 576 elements per token per layer, vs 2 · 128 heads · 128 = 32768 for MHA on the same model.

Code

/// Causal grouped-query attention for one sequence, row-major:
/// q: [t][h][d], k, v: [t][h_kv][d]. window = None means full causal attention.
pub fn gqa_attention(q: &[f32], k: &[f32], v: &[f32], t: usize, h: usize, h_kv: usize, d: usize,
                     window: Option<usize>) -> Vec<f32> {
    assert!(h % h_kv == 0);
    let group = h / h_kv;
    let scale = 1.0 / (d as f32).sqrt();
    let mut out = vec![0.0f32; t * h * d];
    let mut s = vec![0.0f32; t];
    for i in 0..t {
        let lo = window.map_or(0, |w| (i + 1).saturating_sub(w)); // first visible key
        for hh in 0..h {
            let kvh = hh / group;                                    // shared KV head
            let qi = &q[(i * h + hh) * d..][..d];
            let mut m = f32::NEG_INFINITY;
            for j in lo..=i {
                let kj = &k[(j * h_kv + kvh) * d..][..d];
                s[j] = qi.iter().zip(kj).map(|(a, b)| a * b).sum::<f32>() * scale;
                m = m.max(s[j]);
            }
            let mut denom = 0.0f32;
            for j in lo..=i { s[j] = (s[j] - m).exp(); denom += s[j]; }
            let o = &mut out[(i * h + hh) * d..][..d];
            for j in lo..=i {
                let p = s[j] / denom;
                let vj = &v[(j * h_kv + kvh) * d..][..d];
                for c in 0..d { o[c] += p * vj[c]; }
            }
        }
    }
    out
}

/// Bytes of KV cache per token (K and V, all layers).
pub fn kv_bytes_per_token(layers: usize, h_kv: usize, d: usize, bytes: usize) -> usize {
    2 * layers * h_kv * d * bytes
}

/// MHA → GQA conversion: mean-pool each group of K (or V) projection heads. w: [h][d][d_model].
pub fn mean_pool_heads(w: &[f32], h: usize, h_kv: usize, per_head: usize) -> Vec<f32> {
    let g = h / h_kv;
    let mut out = vec![0.0f32; h_kv * per_head];
    for hh in 0..h {
        for i in 0..per_head { out[(hh / g) * per_head + i] += w[hh * per_head + i] / g as f32; }
    }
    out
}

fn main() {
    let (t, h, d) = (6, 4, 8);
    let f = |n: usize, s: usize| -> Vec<f32> { (0..n).map(|i| (((i * s) % 17) as f32 - 8.0) / 8.0).collect() };
    let q = f(t * h * d, 7);
    let (k2, v2) = (f(t * 2 * d, 5), f(t * 2 * d, 3));
    // GQA with 2 KV heads == MHA where each KV head is duplicated for its 2 query heads.
    let dup = |x: &[f32]| -> Vec<f32> {
        (0..t * h * d).map(|i| { let (tok, hh, c) = (i / (h * d), (i / d) % h, i % d);
                                 x[(tok * 2 + hh / 2) * d + c] }).collect() };
    let a = gqa_attention(&q, &k2, &v2, t, h, 2, d, None);
    let b = gqa_attention(&q, &dup(&k2), &dup(&v2), t, h, h, d, None);
    assert!(a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-6));
    // A window at least as long as the sequence is plain causal attention.
    let c = gqa_attention(&q, &k2, &v2, t, h, 2, d, Some(t));
    assert!(a.iter().zip(&c).all(|(x, y)| (x - y).abs() < 1e-6));
    let mha = kv_bytes_per_token(80, 64, 128, 2);
    let gqa = kv_bytes_per_token(80, 8, 128, 2);
    println!("Llama-2-70B KV/token: MHA {} KiB, GQA {} KiB; 4k ctx GQA {:.2} GiB",
             mha / 1024, gqa / 1024, (gqa * 4096) as f64 / (1u64 << 30) as f64);
    let pooled = mean_pool_heads(&[1.0, 3.0, 5.0, 7.0], 4, 2, 1);
    assert_eq!(pooled, vec![2.0, 6.0]);
}

Complexity and constants

  • Prefill: 4·T²·d_h·H FLOPs per layer for QKᵀ and PV (half with a causal-aware kernel). Window w: 4·T·w·d_h·H.
  • Decode, per token and layer: read 2·T·H_kv·d_h cached elements, do 4·T·H·d_h FLOPs. Arithmetic intensity ≈ H/H_kv FLOPs per element read: 1 for MHA, 8 for GQA with group 8. Still far below the hundreds of FLOPs/byte a GPU needs to be compute-bound, so decode attention is bandwidth-bound.
  • Llama 3 8B (32 layers, 8 KV heads, d_h 128): 128 KiB/token in bf16, so the 64 GB left on an 80 GB GPU after 16 GB of weights holds about 490k cached tokens across the batch.
  • Sliding window w caps the cache at w tokens per layer (a rolling buffer, index pos % w).

Efficiency

  • Cost: prefill is compute-bound at 4·T²·d_h·H FLOPs per layer. Decode reads the whole KV cache per token (2·T·H_kv·d_h elements), so it is bandwidth-bound. Llama 3 8B holds 128 KiB of cache per token (this page): at T = 8192 that is 1 GiB per sequence, about 0.3 ms per token to stream at the 3.35 TB/s HBM3 bandwidth of an H100 SXM (arithmetic from the datasheet figure).
  • Levers:
    • GQA/MQA — cache and decode traffic shrink by H/H_kv, 8x at group size 8 (Ainslie 2023) — needs training or uptraining.
    • MLA — 93.3% smaller KV cache, 5.76x peak generation throughput vs DeepSeek 67B (DeepSeek-V2) — model trained with it.
    • FlashAttention — no T×T matrix in HBM; up to 3x on GPT-2 at length 1K (Dao 2022) — prefill and training.
    • Paged KV cache — existing systems wasted 60-80% of cache memory, PagedAttention under 4%, 2-4x throughput (Kwon 2023) — many concurrent requests.
  • Naive → standard → state of the art:
    • MHA, materialized T×T scores, contiguous cache · O(T²) HBM traffic, cache H times larger
    • GQA (8 KV heads) with a fused flash kernel · cache /8, no T² traffic
    • MLA or GQA plus paged cache and window layers · 93% smaller cache (DeepSeek-V2)

Implementation notes

  • Never materialize the T×T score matrix in production. Use a fused kernel (FlashAttention) that streams K/V tiles with an online softmax. The code above is the reference semantics.
  • GQA in kernels: load each K/V tile once and reuse it for all H/H_kv query heads in the group. That reuse is the speedup.
  • Store the cache in pages (paged KV cache) so memory grows per token instead of per max length.
  • Apply RoPE to q and k before caching k. MLA needs a separate small RoPE key, because rotation does not commute with the absorbed up-projection.
  • Logit growth: QK-norm (normalization) or soft-capping (c·tanh(s/c), Gemma 2 uses c = 50 for attention logits) keeps scores bounded in bf16.
  • Sliding-window serving with sink tokens: keep the first 4 tokens plus the window (StreamingLLM). The softmax dumps excess mass on early tokens and breaks if they are evicted.

Pitfalls

  • Wrong head mapping: h % h_kv instead of h / group silently pairs query heads with the wrong KV head. The model runs and is just worse.
  • Masking with -inf in fp16 then subtracting the row max on a fully masked row: NaN. Use a large finite negative or skip empty rows.
  • Scaling by 1/√d_model instead of 1/√d_h.
  • Sliding-window cache indexed by absolute position without the modulo, or RoPE recomputed from the buffer slot instead of the real position.
  • Assuming GQA saves FLOPs: it saves memory and bandwidth. Prefill FLOPs are unchanged.

Variants and alternatives

MHAbest quality per parameter. Fine for encoders and training-only models.
Cross-attentionqueries from one sequence, keys/values from another (encoder-decoder, vision-language).
Sparse patterns (Longformer, BigBird)window plus a few global tokens.
Linear attention / state-space models (Mamba, RWKV)O(T) with a fixed-size state. Weaker exact recall.
Local-global interleavingGemma 2 alternates 4096-token sliding-window layers with global layers.

In production

Llama 2 70B and Llama 3 (GQA, 8 KV heads), Mistral 7B (GQA plus sliding window), PaLM (MQA), Gemma 2 (local/global interleave), DeepSeek-V2 and V3 (MLA); Swin Transformer (window attention for image classification and detection). Kernels: FlashAttention 2/3, FlashInfer, vLLM PagedAttention, torch.nn.functional.scaled_dot_product_attention (has enable_gqa in recent versions).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
candleRusthuggingface/candlecargo add candle-core candle-nn————Llama, Mistral, Gemma (GQA) in candle-transformers; candle-flash-attn for CUDA
BurnRusttracel-ai/burncargo add burn————tensor framework with multi-head attention module; build variants from tensor ops
FlashAttentionCUDA/PythonDao-AILab/flash-attentionpip install flash-attn --no-build-isolation————fused kernels; MQA/GQA, sliding window
vLLMPython/CUDAvllm-project/vllmpip install vllm————PagedAttention serving
PyTorch SDPAPython/C++pytorch/pytorchpip install torch————scaled_dot_product_attention dispatches to flash/efficient kernels

Sources

  • Liu et al., Swin Transformer: Hierarchical Vision Transformer using Shifted Windows, ICCV 2021. paper · code
  • Vaswani et al., Attention Is All You Need, NeurIPS 2017. paper
  • Shazeer, Fast Transformer Decoding: One Write-Head is All You Need, 2019. paper
  • Ainslie et al., GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints, EMNLP 2023. paper
  • Jiang et al., Mistral 7B, 2023. paper · code
  • Beltagy, Peters, Cohan, Longformer: The Long-Document Transformer, 2020. paper
  • DeepSeek-AI, DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model, 2024. paper
  • Xiao et al., Efficient Streaming Language Models with Attention Sinks, ICLR 2024. paper
  • Gemma Team, Gemma 2: Improving Open Language Models at a Practical Size, 2024. paper
  • Touvron et al., Llama 2: Open Foundation and Fine-Tuned Chat Models, 2023. paper
  • Dao et al., FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, NeurIPS 2022. paper · code
  • Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention, SOSP 2023. paper · code

Used by

bev perception · plannedLifting multi-camera features into a bird's-eye-view grid (lift-splat, BEV transformers) for detection and mapping
chunked streaming attention · plannedChunk-limited attention masks with cached left context so encoders (Conformer, Whisper-style) run streaming at bounded latency
graph neural networks · plannedMessage-passing neural networks over graphs (GCN, GAT, equivariant) for molecules and relations
transformer policy value network · plannedTransformer that reads a board position as tokens and predicts move probabilities (policy) and the game outcome (value) for a search to use
vision transformer · plannedViT: patch embeddings, position encodings, encoder blocks; vision-language connectors

Systems (10):

Alternatives and related

Rotary position embedding (RoPE)A transformer treats its input as a bag of tokens unless told where each one sits. Language-model designers need a way to give attention awareness of token positions, especially relative distance, that works with cached decoding.FlashAttentionTraining or running a transformer on long sequences is slow and runs out of GPU memory, because the attention step builds and moves a huge intermediate table. Engineers want exact attention that is faster and leaner.Paged KV cacheA language-model server wastes much of its GPU memory on cached attention data reserved for sequences of unknown final length, limiting how many requests it can serve at once.Transformer blockAnyone building or sizing a language model needs the standard structure of its layers, and the exact sizes, compute and memory cost that follow from that structure.Mixture of experts (MoE)Training and serving a very large neural network is limited by how much computation each input item needs. Designers want far more model capacity without a proportional rise in per-token compute.Normalization layersDeep neural networks train slowly or blow up because activation scales drift from layer to layer. Designers need to choose a layer that keeps training stable for transformers or convolutional networks.Speculative decodingGenerating text from a large language model one token at a time is slow for a single user, because each step costs a full pass of the model. Operators want lower latency without changing the output.