iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
AI Engineering

不是模型太慢,是你沒算過這筆帳:地端 LLM 工程實戰 30 天系列 第 9

Day 09 - 權重塞得下 ≠ 跑得動:KV cache 公式與 GQA、MQA、MLA

  • 分享至 

  • xImage
  •  

昨天把 T1 對 T2 的第一場對照搭好,六根釘子釘完、兩排數字都開了,賽前那注也當場打臉,差額怎麼算留給 Day 10。昨天量的是「一個人問一句誰快」;今天照說好的,把 context 拉長、人數加上去。

在拉長之前,先接一條 Day 01 的線。那 45 秒的逐刀帳裡,有 12.6 秒是模型根本沒全跑在 GPU 上,將近三成。而「塞不塞得進去」是紙筆就能算的線——在 llama.cpp 這類支援 partial offload 的 runtime,算錯甚至不報錯,只會安靜地變慢。
這條線因此有兩種用法:查你現在為什麼慢,和算這台機器還能接幾個人。

Day 04 已經教過怎麼估權重,Day 07 又拿到 Llama-3.3-70B Q4_K_M 的實際大小 42.52 GB,換算 39.60 GiB。這台 128GB Mac 實測能給 GPU 107.52 GiB。39.6 < 107.5,塞得下,收工?

**塞得下只是入場券。權重是一次付清的房租;真正決定能坐幾個人的,是每個使用者、每個 token 都要另外墊的變動成本——KV cache。**它隨 context 線性長、隨併發整數倍放大,長 context 高併發時吃掉的顯存比權重本體還大。今天把這筆帳變成紙筆作業,順便開張本系列第一個互動工具。

KV cache 是什麼

自回歸生成每產一個新 token,都還得看前面所有 token。那些 token 的 Key/Value 如果不存,每一步都得重算一遍;把它們留下來,就是 KV cache。

問題也很直接:**聊得越長 KV 越大,同時聊的人越多再乘上去。**權重在載入那一刻就固定了,KV cache 卻是活的。

每 token 的 KV:一條可以自己算的公式

每 token KV(bytes)= 2 × 全注意力層數 × kv_heads × head_dim × 每元素 bytes

逐項拆開:

  • 2:Key 一份、Value 一份。
  • 全注意力層數:每一層都有自己的 KV。特別寫「全注意力」,是因為 2026 年的模型一堆是混合注意力(sliding window 層、線性層交錯)。全注意力層會一路隨 context 長大;sliding 層只長到 window 大小就封頂,之後轉成常數。
  • kv_heads:KV 的頭數。注意不是 Q 的頭數——現代模型兩者差好幾倍,這正是 GQA 的魔法,下一節講。
  • head_dim:每個頭的維度。
  • 每元素 bytes:KV 的存放精度,FP16 = 2、FP8 = 1,容量直接減半。但「八位元」有兩種帳:FP8 是 vLLM 的口徑,llama.cpp 那邊叫 -ctk q8_0,每 32 個元素多帶一個 scale,實際是 1.0625 bytes。品質損失兩邊都很小,上線前拿自己的評測集驗一輪就好。

這是標準 MHA/GQA 的乾淨版;碰到 MLA 或混合 attention,就得逐層拆。模型參數都在 config.json,不用猜。這裡拿 Llama-3.3-70B 當例子:80 層全注意力、8 kv_heads、head_dim 128。

每 token KV = 2 × 80 × 8 × 128 × 2 B = 320 KiB @FP16 KV
                              × 1 B = 160 KiB @FP8 KV

這只是單價,還要乘上工作負載:× context × 併發。@FP8 KV 一條 32K 的對話 = 160 KiB × 32,768 = 5 GiB;8 個人同時掛著 32K 就是 40 GiB,比 39.6 GiB 的權重本體還大。把它畫出來是這樣:

https://ithelp.ithome.com.tw/upload/images/20260820/20183550rM1SF5GObb.png
圖 1:KV cache 隨 context 線性長(x 軸每格翻倍,所以直線畫出來像起飛),再隨併發整數倍放大。8 條併發(藍線)在 32K 已吃掉 40 GiB,距 45.9 GiB 的預算只剩 5.9 GiB,理論撞線約在 36.7K;下一格 64K 直接翻倍爆掉。單條序列(紅線)到原生上限 128K 都還安全。

容量線:一條減法加一條除法

KV 預算 ≈ VRAM × 0.92 − 權重 − 約 2.8 GiB / GPU(non-KV reserve) ← 權重也先換成 GiB
併發槽 = KV 預算 ÷(每 token KV × 平均 context + 每序列固定 state)

這條是 NVIDIA 卡加 vLLM 的手算式;T1 跑 llama.cpp,沒有這個 utilization 保留,直接拿 Day 07 的實測可用空間減下去。

0.92 是 vLLM gpu_memory_utilization 的現行預設,寫死在 CacheConfig 裡。Day 03 提醒過這個值會隨版本漂移、當時顯式指定 0.9——現在兌現:預設動過了,抄舊文章的 0.9 會少算一格。2.8 GiB / GPU 則是本文拿來手算的 reserve,包含 activation 峰值、CUDA context 與框架雜項,不是 vLLM 常數。多卡要乘卡數:等一下 T4 的 TP2 扣的就是 5.6 GiB。

最容易犯的錯反而是單位。顯卡標的 96GB、80GB 按二進位 GiB 算,Hugging Face 的 checkpoint 檔案卻是十進位 GB;42.52 GB 要先換成 39.60 GiB 再相減,不然一顆 70B 憑空胖 2.9 GiB。分母那筆「每序列固定 state」是留給混合注意力模型的,等一下會看到它有多大。

還有一個限制:本文的「併發槽」只代表 KV 容量裝得下幾條 request,vLLM 自己叫它 Maximum concurrency。它不保證這些人同時進來還能守住 TTFT,那是明天的吞吐題。

這條線算錯,會用三種方式讓你等

容量看起來是 sizing 的事,其實也是前段最常見的延遲病因。踩雷時外觀常常只是「怎麼突然變慢了」,但不同 runtime 的處理方式不一樣,留下的證據也不同:

**一、權重塞不下:partial offload。**llama.cpp 可以把放不進 GPU 的層留給 CPU 算,所以程式照樣跑,只是突然很慢。雙通道 DDR5-5600 約 89.6 GB/s,500 GB/s 級顯卡的理論頻寬差五倍以上,Day 01 那 12.6 秒就是這一類。啟動 log 其實會告訴你有幾層沒進 GPU,只是很多人沒看。(vLLM 不這樣做,塞不下直接不給你啟動。)

**二、KV 預算太緊:排隊與 preemption。**workload 逼近 KV 容量後會開始 waiting,嚴重時發生 preemption——被 preempt 的 request 要重做 prefill,尾延遲突然變醜,同一個問題這次兩秒下次八秒。這個很好抓:log 有 not enough KV cache space 的 warning,Prometheus 也有 counter,Day 19 接上儀表板。

**三、context 開太長:搬運量膨脹。**decode 每產一個 token 要讀的不只是權重,還有這條序列自己的 KV。70B 權重 39.6 GiB、@FP8 一條 128K 對話 20 GiB,每 token 多搬五成;用「單序列 decode 卡在記憶體頻寬」做 roofline 粗估,理論上限只剩三分之二。RAG 特別容易踩:塞十份 chunk,context 拉到兩萬以上是常態。

三條的取證方式與分診流程,Day 11 有一張表。

省 KV 的三條路:砍頭數、壓維度、縮範圍

Day 06 那張表列過六組的每 token KV,差距很大。省 KV 的招數不多,每一種都對應公式裡的某一項:

**砍 kv_heads——GQA/MQA。**MHA 時代每個 Q 頭配一組 K/V;GQA 讓一組 Q heads 共用一組 K/V,比例各家自己定——Llama-3.3-70B 是 64 個 Q 對 8 個 KV,8:1,所以 KV 縮到 MHA 的 1/8。MQA 是極端版,一層只留 1 組。

**壓維度——MLA。**GLM-4.7-Flash 每層都是全域注意力,照理最耗,卻不是:它只存一份壓縮 latent,用時再展開,47 層 × (512 + 64) 維 × 2B ≈ 53 KiB/token。53 KiB 是 BF16 latent 的大小,看到「壓縮」不要又自己除二。

**縮範圍——sliding window。**gpt-oss 一半的層只看最近 128 個 token,那些層不再隨 context 長大,公式裡的「全注意力層數」直接砍半。

https://ithelp.ithome.com.tw/upload/images/20260820/20183550mebtmrURwc.png
圖 2:省 KV 的三條路各改公式的一項——GQA/MQA 砍 kv_heads、MLA 壓維度、sliding window 縮層數與範圍。同代模型的每 token KV 因此差到 9 倍:Llama-3.3-70B 的 160 KiB 對 gpt-oss-120b 的 18 KiB。

四顆模型排排站,兩種精度都標出來:

模型 架構 隨 context 成長(FP16 / FP8) 每序列固定 state(FP16 / FP8)
Llama-3.3-70B GQA,80 層全注意力 320 / 160 KiB
GLM-4.7-Flash MLA,47 層全域但只存壓縮 latent 53 KiB latent
gpt-oss-120b GQA,36 層中 18 層 sliding(w=128) 36 / 18 KiB 4.5 / 2.25 MiB
Gemma 4 12B 48 層 = sliding ×40(w=1024) + global ×8 16 / 8 KiB 320 / 160 MiB

架構參數取自各家 config.json,右欄正是 Day 06 註明「不計」的那筆。

Gemma 4 是最容易算錯的一顆。它隨 context 成長的部分 @FP8 只有 8 KiB/token,但 40 個 sliding layer 還有每序列 160 MiB 的封頂 cache;@8K 時反而是這筆固定成本佔約七成。attention_k_eq_v=true 也不能拿來再除二——它省的是 projection 參數,而 global 層帶 partial RoPE,K 過了旋轉、V 沒有,兩份仍要各存。只盯 KiB/token,會算出漂亮但錯誤的併發數。

gpt-oss-120b 很能說明「參數量不等於 KV 壓力」。它比 Llama-3.3-70B 大七成,但 @FP8 隨 context 成長的 KV 只有 18 KiB/token,是 Llama 的 1/9。同樣放進 96 GiB 卡、同樣算 @8K,手算結果是 173 槽對 36 槽,約 4.8 倍。**參數量預測不了 KV 壓力。**標準 GQA 的成長斜率看 全注意力層數 × kv_heads × head_dim;碰到 sliding 或其他混合架構,再把 fixed state 補進去。選型時打開 config.json 算 30 秒,比只盯參數量準得多。

順便更正 Day 06:gpt-oss-120b 的 checkpoint 應該是約 60.8 GiB,當時表上那個 63.4 GB 少算了。

還有兩條路今天先掛號。Qwen3.6 的 Gated DeltaNet 走得更絕:線性注意力層不再保存隨 context 增長的 KV,改成一塊固定大小的 state。但別誤讀成「整顆 1K 和 1M 一樣大」——27B 的 64 層裡仍有 16 層全注意力(35B-A3B 是 40 層裡 10 層),那些層照樣長。它砍掉的是成長的斜率,不是成長本身。

DeepSeek-V4-Flash 我在 Day 06 說「Day 09 再拆」,今天要改口:它的 CSA + HCA 是壓縮與稀疏混著上,跟這條公式換算不對等,硬塞進同一張表只會誤導人以為兩者可比。這顆跟 Qwen3.6 一起挪到 Day 15 專章解剖。

70B 上四階:塞得下之後,差距才開始

主角回到 Llama-3.3-70B Q4(42.52 GB = 39.60 GiB),同一條公式四階逐格算:

機器 可用空間 KV 預算 @8K 併發槽 判定
T2 RTX 4070 Ti 12 GiB 11.0 GiB 負值 0 權重就塞不下
T1 M4 Max 128GB 107.5 GiB(Day 07 實測) 65.1 GiB 26 @FP16、49 @q8_0 裝得下,長 context 見底
T3 RTX PRO 6000 96 GiB 88.3 GiB(× 0.92) 45.9 GiB 36 @FP8 舒服
T4 H100 80 GiB ×4,權重改 FP8 147.2 GiB(TP2 一組) 73.9 GiB 59 × 2 組 = 118 @FP8 高併發主場

顯卡標示的 96GB/80GB 取名目 GiB。nvidia-smi 實報會少 1–2 GiB(ECC 與顯示佔用),要精算就以自己印出來的為準——這條規矩 Day 07 立過。

https://ithelp.ithome.com.tw/upload/images/20260820/20183550ZoEs91qXcX.png
圖 3:同一顆模型、五種配置的容量結果。條長是可用空間,藍色是一次付清的權重,綠色才是 KV 預算。T4 單卡那條最值得看:權重塞進去了,KV 卻只剩 2 個 @8K 槽。

**T2:連入場券都沒有。**39.6 GiB 權重對 11 GiB 可用空間,不必再算 KV。硬跑就是上一節第一條路,換模型比調參數有用。

**T1:裝得下,別急著開派對。**llama.cpp 的 KV 預設存 FP16,@8K 是 26 槽,但一條 128K 就要 40 GiB,65.1 GiB 的預算只夠一條半。-ctk q8_0 -ctv q8_0 能把 @8K 拉到 49 槽,長 context 併發還是會見底。

T3:容量上開始像伺服器。@8K 36 槽、@32K 還有 9 槽。96 GiB 卡的價值不是跑得快,是權重放下之後還剩得多;快不快明天再算。

**T4:最有意思的一格。**Day 05 講過正解是 AWQ/FP8,但別用「70B × 1 byte」估——NVIDIA 那顆 FP8 checkpoint 本身就 67.67 GiB(只有線性層轉了 FP8,還有 21 億參數留在 BF16)。單張 H100 80GB 雖然塞得下,KV 只剩 3.1 GiB,也就是 2 個 @8K 槽。TP2 之後才跳到 59 槽,四卡兩組共 118。

容量計算機,今天開張

今天整篇的算術我做成了互動計算機:選卡、選模型、選量化與 context,直接告訴你塞不塞得下、剩多少 KV 預算、能開幾個併發槽。

地端LLM計算機

T1–T4 與 Day 06 六組裡換算得動的五組都內建,架構參數取自各家 config.json,公式與今天這篇同一支,今天算過的每一格都做成了一鍵預設。

今天的實驗需要什麼

紙筆就夠,本系列最便宜的一天。想對答案的,拿一張 N 卡起一次 vLLM,啟動 log 會印出它實際切出的 KV 空間:

vllm serve <你的模型> --kv-cache-dtype fp8 --max-model-len 8192
# 啟動 log 會直接印兩行,不用自己除:
#   GPU KV cache size: ... tokens
#   Maximum concurrency for 8,192 tokens per request: ...x   ← 就是今天算的併發槽
# 我的 T2 單卡(12 GiB)跑一顆塞得下的模型,log 值與公式對照:【實測待填|發文前必填】

log 跟手算差一點不奇怪;差得多,就回頭查實際載入的權重、non-KV reserve、KV block rounding 與 backend 的 cache layout。公式負責 sizing,runtime log 才是最後答案。

小結

  • 權重塞得下只是第一關;長 context 與併發真正吃的是 KV cache。
  • 標準 MHA/GQA 可以用 2 × 全注意力層數 × kv_heads × head_dim × bytes 快速估;混合 attention 還可能有每序列的 fixed state。
  • 併發槽只是記憶體容量上限,不是實際能守住 TTFT/TPOT 的服務人數。
  • 同一顆 70B:4070 Ti 連權重都塞不下,96 GiB 卡有 36 個 @8K 槽;80 GiB 的 H100 即使裝得下 FP8 checkpoint,單卡也只剩 2 槽。容量跟「模型塞得下」真的不是同一題。

明天,Day 10:紙面頻寬 vs 有效頻寬——546 對 504 的真相。椅子擺得下不代表上菜快;Day 08 掛著的懸念——帳面同量級、數字卻反著來——用一條國中乘法,倒推出你的 GPU 實際吃到幾成頻寬。

兩條線都拿到之後,第 11 天用它們做一件更實用的事:把「我的 RAG 為什麼很慢」拆成可以逐項排除的分診表。

咱們明天見。


上一篇
Day 08 - 一大票顯卡對比文都算錯了:M4 Max 對 4070 Ti、5090 與 RTX PRO 6000
系列文
不是模型太慢,是你沒算過這筆帳:地端 LLM 工程實戰 30 天9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言