昨天那張速查表有一格很怪。
Gemma 4 只有 12B,一條 8K 對話卻要吃 448 MiB;gpt-oss-120b 有 117B,反而只要 293 MiB。
小十倍的模型,KV 為什麼比較貴?

圖 1:黃色是不再隨 context 繼續長大的那部分——Qwen 是固定 state,Gemma 與 gpt-oss 則是封頂的 sliding-window KV。
因為我們一直把 KV cache 當成一本帳。它至少有三本:
① 每序列總量 = 隨 context 長大的 cache
+ 已封頂的 cache
+ 每序列固定 state
② 每步碰多少 = 這一步真正讀進 attention/state 計算的歷史資料
③ HBM 常駐 = ① 裡面此刻非得留在顯存的那一部分
而每一招通常只動其中一本。看到「省 30 倍」的時候,另外兩本可能一毛都沒少。

圖 2:第三本帳不是新問題,8 月的 HiSparse 只是把它做成了一個很乾淨的例子。
先說清楚一件事:①裡有些東西嚴格說不是 KV cache,例如線性層的遞迴 state。我把它們算進同一本,是因為做容量估算時它們是同一種成本——每多一條 sequence 就多付一次。
Qwen3.6-35B-A3B 很省,不是因為它只有 35B,而是因為 40 層裡只有 10 層真的需要一路存 KV,其餘 30 層換成了 Gated DeltaNet 線性注意力。
每 token KV = 2 × 10 層 × 2 head × 256 dim × 2 B = 20 KiB @FP16
Llama-3.3-70B 同一條算式 = 320 KiB ← 十六分之一
8K 對話就是 160 MiB。但省掉的東西沒有消失:那 30 層改成每條 sequence 帶一筆 61.4 MiB 的固定 state,context 拉到 128K 它不會變大,KV 壓成 FP8 它也不跟著減半。
@FP16 KV 160 MiB + state 61.4 MiB = 221.4 MiB
@FP8 KV 80 MiB + state 61.4 MiB = 141.4 MiB KV 砍半,總量只掉三成六
第一個規律:有些架構省掉的是「隨 context 長大的那部分」,代價搬到固定成本。
想自己查,config 欄位是 full_attention_interval: 4。這套配方沒有換代——8 月上架的 Qwen3.8-27B,影響這筆帳的 text_config 欄位原封不動沿用 Qwen3.6-27B,連參數量都同一個數。
固定 state 的 dtype 要看 runtime,不是看權重精度:Qwen 這幾顆的 config 寫 mamba_ssm_dtype: "float32",vLLM 的 --mamba-ssm-cache-dtype 預設 auto 會直接讀它。
Kimi K3 把同一招推到極致:93 層裡 69 層是 KDA 線性注意力,24 層是 Gated MLA,最後一層必為全域注意力。留下來那 24 層還壓成 512+64 維的 latent,每 token 27 KiB——2.8T 的模型,KV 單價還不到 27B 那顆 Qwen 的一半。代價在另一邊:權重檔實測 1.56 TB(=1.42 TiB),T1 到 T4 全數出局。
Gemma 4 12B 的 48 層裡有 40 層是 sliding window,只看最近 1,024 個 token;每 6 層才有 1 層 global。sliding 層的 cache 因此有天花板,@FP16 封在 320 MiB,之後再長也不會變大。
代價就是圖 1 那格:@8K 時這 320 MiB 佔掉每序列成本的七成。它省的不是 KV 的單價,是 KV 的成長速度,所以兩條線一定會交叉:
Gemma 4 12B 320 MiB + 16 KiB × context
gpt-oss-120b 4.5 MiB + 36 KiB × context
────────────────────────────
交叉點 context ≈ 16,150 token
@8K 448 vs 293 MiB 12B 輸
@128K 2.31 vs 4.50 GiB 12B 省快一半
你把 context 設多長,決定了這格是優點還是缺點。
global 那 8 層還有一刀,值得自己算一次。官方報告寫 global 層「values = keys」,配上 p-RoPE 的 p = 0.25,「effectively reducing the global KV cache by 37.5%」。global head_dim 是 512,只有 0.25 × 512 = 128 維過旋轉,其餘 384 維 K 與 V 同源、可以只存一份:
不共用 K 512 + V 512 = 1,024
共用後 V 512 + K 的旋轉切片 128 = 640
1 − 640 ÷ 1,024 = 37.5%
Day 09 我寫過「K 過了旋轉、V 沒有,兩份仍要各存」——前半對,後半太保守:真正不能共用的只有那 128 維。不過本系列的表照舊按 16 KiB/token 算,因為 HF transformers 的參考實作把 K、V 兩份完整張量都塞進 cache。37.5% 是架構允許的,不是你在本機量得到的。
順帶記兩件事:這一招在 E2B 與 E4B 上是關的,而 256K context 也只有 12B、26B-A4B、31B 才有。
第二本帳砍的是「每個 token 要看多少 KV」。典型做法不動 cache 的大小,只挑著讀。
GLM-5.2(753B)用 DSA:一個 indexer 對每個 query 只挑 top-2048 條進 attention,5.2 再讓每 4 層共用一個 indexer(model card 叫 IndexShare),1M context 下 per-token FLOPs 降 2.9 倍。
它的 KV 本體仍然是 MLA latent,78 層 ×(512+64)維,每 token 87.75 KiB @FP16,還不到 Llama-3.3-70B 的三分之一。但那是 MLA 的功勞,不是 DSA 的——DSA 省的是「讀多少」,一條都沒少存。
DeepSeek-V4-Flash-0731(官方標稱 284B/13B 啟用)則是兩本一起砍:CSA 先把 KV 按 4 與 128 兩種比例壓成塊,再對壓縮塊選讀 top-512。官方報告是 1M context 下相對 V3.2 只要 10% FLOPs、7% KV cache——它能同時給出這兩個數字,是因為壓完之後原始的 KV 是真的丟掉的。
補一個容易混的邊界:在 bs=1 的 decode,第二本帳多半先表現成 memory traffic,query 只有一個、attention 受頻寬限制。真正會隨序列長度冒出 O(L²) 的是 full-attention prefill。
前面兩本今天舉的例子大多是架構先決定的(Quest 那種 training-free selector 是例外);第三本則更像 serving system 的問題。8 月初的 HiSparse(arXiv:2608.07009)把這一刀做得很乾淨,它開頭第一句就戳破了第二本帳的尷尬:attention 便宜了幾個數量級,記憶體帳單卻一個 byte 都沒少。
因為這一步沒被選中的 token,下一步可能突然被選中,所以整條 context 的 KV 都得留在 HBM 裡待命。HiSparse 一筆都不刪,只叫它搬家:

圖 3:右邊那塊 hot cache 的大小 B 是你設的,跟對話多長無關。
論文按 footprint 上界推算:GLM-5.1 一條 128K 的請求,attention KV 的 GPU 常駐從 13.09 GB 掉到約 0.4 GB,約 30 倍。那 13 GB 沒有蒸發,它搬去 host RAM,miss 的時候走 PCIe/NVLink-C2C。
「搬走的只有 KV 本體」這句要記牢:selection state、page table 與 LRU metadata 都還留在 GPU。前兩者仍會隨 context 長,LRU 則是跟 hot cache 的 B 走。但這些東西小得多——論文估 indexer key 加 page-table entry 至多每 token 幾百 bytes,對面是約 100 KB 的 KV records。
它最漂亮的地方是沒有再近似一次:selected positions、attention score、output 全都不變,改的只有 KV 住哪。但這個 exact 是相對於模型自己的 sparse 規則——拿 Quest 那種把 dense 模型硬改成選讀的做法,近似在 HiSparse 之前就發生了。
論文那個 4.7×,收益來源它自己講死了:HiSparse 不會讓單一 decode step 變快,只是讓同一塊 HBM 裝得下更大的 decode batch。它換來的是 aggregate throughput,不是你一個人的 tok/s。
那你開得起來嗎?已經 merge 進 SGLang upstream(--enable-hisparse),但官方文件目前只涵蓋 DSA 架構與 DeepSeek V4,而且只在 PD 分離部署的 decode instance 上啟用——單卡跑 llama.cpp 或 Ollama 的今天用不到。
架構決定體質,serving 還有一層:KV 換 FP8 讓每一筆變小,直接砍①,通常也連帶縮③;PagedAttention 不改單價,減的是碎片與預留浪費;prefix cache 讓相同 prefix 的 KV block 直接重用,省的是重複 prefill,不會讓新 token 的 decode TPOT 變快(vLLM 官方文件自己寫的)。
地端這邊先對一次帳:llama.cpp 的 KV 量化是 -ctk/-ctv 那組 ggml 型別(預設 f16)、不是 FP8,量化 V 一定要開 -fa;prompt caching 有而且預設開;至於 PagedAttention,它走的是自己的 unified KV/slot 路線,帳法不能拿 vLLM 那套直接套。
| 招式 | ① 每序列總量 | ② 每步碰多少 | ③ HBM 常駐 | 代價搬到哪 |
|---|---|---|---|---|
| GQA/MQA | ↓ | ↓ | ↓ | 較少獨立 K/V head,靠訓練補回來 |
| MLA latent 壓縮 | ↓↓ | ↓ | ↓↓ | 多一組低秩投影與更複雜的 kernel |
| 線性層(GDN/KDA) | ↓↓↓ | ↓↓↓ | ↓↓↓ | 每序列 61–147 MiB 固定 state |
| sliding window | 封頂 | ↓↓ | 封頂 | 那些層看不到遠處 |
| DSA/NSA 選讀 | KV 主體不變 | ↓↓↓ | 不變 | indexer 自己要算力,也要 cache |
| CSA(壓塊+選讀) | ↓↓ | ↓↓↓ | ↓↓ | 壓縮塊本身是近似 |
| HiSparse 分層存放 | 邏輯 KV 不變 | 邏輯不變 | KV ↓↓↓ | host RAM + miss 的 IO |
只有最後一列的 ③ 會跟 ① 分家——同一批 KV 一筆沒少,只是不再全部住顯存。那正是第三本帳存在的理由。
查證日 2026-08-26,KV 一律按 FP16,逐項條件見各圖的來源行。三個最容易被拿去亂引的數字:30× 是 HBM footprint、4.7× 是特定 workload 的 aggregate throughput、2.9× 是 FLOPs,三者不是同一件事。HiSparse 的快取命中率(§4.3)與 TPOT 代價(§4.6)論文都有給,這裡不轉述。V4-Flash 的 284B 是官方對主模型架構的標稱;HF safetensors 逐 tensor 加總則是 preview 290.9B、
-0731304.2B,兩份實測相差的 13.3B 才是隨附的 DSpark module(Day 06 那格用的是 304B)。兩種口徑不要直接相減——標稱對不上逐 tensor 加總是 Day 04 講過的老現象。
還有兩刀完全不碰 KV:MoE 動的是權重帳,MTP 動的是 forward 次數。今天先不開這兩本。
不用 GPU。下次遇到陌生模型,先照這張圖看五格:

圖 4:第 5 列最容易漏——它不隨 context 縮,所以短對話反而最痛。
然後兩行 curl,不下載權重也能親眼看到今天講的欄位:
# Qwen3.8-27B:每 4 層 1 層全注意力、FP32 state,全寫在 config 裡
curl -s https://huggingface.co/Qwen/Qwen3.8-27B/raw/main/config.json \
| grep -E "full_attention_interval|mamba_ssm_dtype|linear_num_value_heads"
# DeepSeek-V4-Flash-0731:top-512 選讀、window 128、出廠內建 YaRN ×16
# (裸名 DeepSeek-V4-Flash 是 preview,官方 card 自己說 0731 才是正式版)
curl -s https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731/raw/main/config.json \
| grep -E 'index_topk|sliding_window|dspark_block_size|"factor"|yarn'
架構是天生體質,但同一副體質,行為還是後天練出來的:為什麼有的模型 thinking 恆開、有的預設關?
明天 Day 16〈元凶第一名:thinking mode 沒關〉把這件事變現成手感。Day 01 那晚是兩刀救回 40 秒,明天先把第一刀親手補量給你看,同一題,thinking 開與關,token 數與 decode 時間各差幾倍。
咱們明天見。