對應原題(Common ‧ LLM Internals / Inference)

  • What is the KV cache, and what are its memory implications at scale? Derive the formula.
    — Asked at: OpenAI, xAI, Mistral AI, Amazon, Apple, NVIDIA, Together AI, Character.AI
  • Estimate the GPU memory needed to serve a 70B model: weights, KV cache, activations, fragmentation. — Asked at: NVIDIA
  • Implement a KV cache and single-step decode. — Asked at: Moonshot AI

🎯 什麼情境該想到我

當你「要算一張 GPU 到底能同時撐幾個對話、或想知道長上下文為什麼突然 OOM」的時候。這是 LLM 服務成本的核心變數——權重是固定成本,KV cache 是隨流量成長的變動成本。

⚙️ 怎麼用(推導與算帳)

它解決什麼

自迴歸生成每次只多產一個 token。若什麼都不存,生成第 個 token 時要重算前面 個 token 的 K/V——整段生成的總成本是 的重複勞動。

KV cache 就是把已算過的 K、V 存起來,每步只算新 token 那一列:

不快取快取後
每步計算重算 個 token 的 K/V只算 1 個 token 的 K/V
每步 attention(1×t 向量)
代價算力顯存 + 記憶體頻寬

這是典型的拿空間換時間,而換來的空間問題大到催生了 PagedAttention 與 vLLM。

★ 記憶體公式(現場推導版)

一個 token、一層,要存 K 和 V 各一份,每份 個數:

符號意義
層數
序列長度(prompt + 已生成)
KV 頭數(GQA 下 ≠ query 頭數,見 注意力機制)
每頭維度
每個數的位元組數(fp16/bf16 = 2、fp8/int8 = 1)
batch size(並行的序列數)

記法:2 × 層 × 長度 × KV維度 × 精度 × 批次。

實例:Llama-2-70B

規格:、、query 頭 64、KV 頭 8(GQA)、。

每 token 每層  = 2 × 8 × 128 × 2 bytes = 4,096 bytes = 4 KiB
每 token 全模型 = 4 KiB × 80 層         = 320 KiB
4K 上下文一條序列 = 320 KiB × 4,096      ≈ 1.25 GiB
batch 32                                ≈ 40 GiB

對照組:如果它用 MHA(64 個 KV 頭)
每 token 變 8 倍 = 2.5 MiB,一條 4K 序列就要 10 GiB。
→ 這就是 GQA 的價值:同樣顯存能多服務 8 倍的併發。

整張卡的記憶體帳(NVIDIA 那題的標準答法)

服務 70B、bf16、2×H100 80GB = 160 GB ≈ 149 GiB(假設與 GPU 記憶體估算 一致:框架開銷每卡 ~3 GB):

項目大小說明
權重140 GB ≈ 130 GiB70B × 2 bytes = 140×10⁹ bytes ÷ 2³⁰。固定,不隨流量變
框架 / CUDA context / 啟動開銷~3 GB/卡 × 2 = 6 GB ≈ 5.6 GiB每張卡都有(常見 2–4 GB,取中間值)
啟動值 activations小(decode 階段)decode 每步 batch×1 token,幾乎可忽略;prefill 才大
剩給 KV cache~14 GB ≈ 13 GiB160 − 140 − 6 = 14 GB;149 − 130.4 − 5.6 ≈ 13.0 GiB
→ 能撐多少~10 條 4K 序列13.0 GiB ÷ 1.25 GiB ≈ 10.4

結論很刺眼:權重佔掉 87%,真正能服務的併發少得可憐。這直接推出三條路——量化權重(見 量化 Quantization)、壓 KV、別浪費 KV(見 PagedAttention 與 vLLM)。

加上碎片化:vLLM 論文指出,傳統做法(依 max_seq_len 預留連續空間)的實際有效利用率常低於 40%,其餘都浪費在內部/外部碎片與預留。這數字是論文情境下的量測,套到你的 workload 前要自己量。

單步 decode 的最小實作(coding 題)

class KVCache:
    def __init__(self, B, n_layers, h_kv, d_head, max_len, dtype, device):
        shape = (n_layers, B, h_kv, max_len, d_head)
        self.k = torch.zeros(shape, dtype=dtype, device=device)
        self.v = torch.zeros(shape, dtype=dtype, device=device)
        self.len = 0                      # 目前已填長度
 
    def append(self, layer, k_new, v_new):   # k_new: (B, h_kv, 1, d_head)
        t = self.len
        self.k[layer, :, :, t:t+1] = k_new
        self.v[layer, :, :, t:t+1] = v_new
        return self.k[layer, :, :, :t+1], self.v[layer, :, :, :t+1]
 
# 單步 decode:注意 q 只有 1 個 token,但 K/V 是整段
def decode_step(x_t, layer, cache):          # x_t: (B, 1, d_model)
    q = proj_q(x_t)                          # (B, h_q, 1, d_head)
    k, v = cache.append(layer, proj_k(x_t), proj_v(x_t))
    if h_q != h_kv:                          # GQA:把 KV 頭重複到 Q 頭數
        rep = h_q // h_kv
        k, v = k.repeat_interleave(rep, dim=1), v.repeat_interleave(rep, dim=1)
    # 這一步不需要 causal mask——q 本來就只看得到 0..t
    return sdpa(q, k, v, causal=False)

兩個面試官會挑的點:

  1. decode 階段不需要 causal mask——當前 token 的 query 天生只對到 cache 裡已有的位置。加 mask 是多餘甚至錯的。
  2. GQA 的 repeat_interleave 是邏輯上的展開;高效實作會直接在 kernel 裡廣播,不真的複製記憶體。

壓縮 KV cache 的四條路

路線做法損失
架構層MQA / GQA / MLA —— 少存頭或壓低維度要重訓或續訓
精度層KV 量化成 int8 / fp8長序列上誤差會累積,要實測
長度層滑動窗口、注意力池(StreamingLLM)、重要度驅逐(H2O)會真的丟掉資訊
配置層PagedAttention 分頁、前綴共享無損,純粹省浪費 —— 優先做這個

順序建議:先做無損的(分頁、前綴共享)→ 再量化 → 最後才考慮丟資訊。

🧪 我實際套用的紀錄

  • 2026-09-20:從面試題庫展開,尚未實際套用。

⚠️ 注意 / 什麼時候不適用

  • 別把 query 頭數當成 。GQA 模型算錯會高估 8 倍。查 config 的 num_key_value_heads。
  • KV cache 隨 線性成長,且不可預測——使用者可以一直聊下去。容量規劃要算「最壞情況的併發 × 上下文」,不是平均值。
  • 它讓 decode 變成 memory-bound。每生成一個 token 都要把整個 cache 讀過一遍;序列越長,每個 token 越慢。這是 TPOT 隨對話變長而劣化的主因。
  • 前綴快取 ≠ KV cache。KV cache 是單次請求內的;前綴快取 Prompt Caching 是跨請求重用。兩者疊加才有完整效益。
  • MLA 存的是 latent,不是 K/V,上面的公式不能直接套。

🔗 相關