測驗:KV Cache 與 PagedAttention
共 5 題,點選答案後會立即顯示結果
1. 根據文章,一顆 GPU 跑 LLM 時,「會隨著請求變多、序列變長而膨脹」、吃掉大半記憶體的主角是什麼?
2. 文章用「便利貼筆記」比喻 KV Cache,主要是為了說明它的什麼作用?
3. PagedAttention 借用了作業系統的哪個老招數來管理 KV Cache?
4. 看到以下啟動日誌,這行主要在告訴你什麼?
5. 依文章的 OOM 排查順序,遇到記憶體不足時「最有效、應該最先動」的參數是哪一個?
系列第 2 篇,共 4 篇|難度:L2-進階
前置:已讀第 1 篇,能用vllm serve把模型跑起來,知道 token、context length 是什麼。
第 1 篇你已經把模型服務起來了。這一篇要回答一個你遲早會遇到的問題:
「我的 GPU 明明有 24GB,模型檔案才 14GB,為什麼一跑起來就說記憶體不夠 / OOM?剩下的記憶體到底被誰吃掉了?」
答案的主角就是這篇的兩個名詞:KV Cache 和 PagedAttention。搞懂它們,你就能看懂 vLLM 啟動時那一長串日誌,也能自己動手調參數換併發、避開 OOM。
這篇不推導任何數學,我們用工程師的直覺來理解。
一句話說明
KV Cache:把模型算過的 token「筆記」存起來,避免每次都重算——序列越長,筆記越厚,吃越多記憶體。
PagedAttention:像作業系統管記憶體那樣,把這堆筆記切成固定大小的小塊分頁管理,不再一次預留一大塊連續空間,所以塞得下更多請求。
KV Cache 是什麼:模型的「草稿筆記」
大型語言模型是一個 token 一個 token 往外吐字的。每吐一個新字,它都要「回頭看」前面所有的字,才知道接下來該講什麼。
問題來了:如果每吐一個字都要把前面全部重算一次,越到後面越慢,算力全浪費在重複勞動上。
所以模型做了一件很直覺的事:把前面每個 token 算過的中間結果存起來,下次直接拿來用。 這份存起來的中間結果,就叫 KV Cache(K 和 V 是 Key/Value 的縮寫,你不用管它的數學意義,把它想成「每個 token 的注意力筆記」就好)。
用白話類比:
沒有 KV Cache:
寫第 100 個字時,把前面 99 個字從頭讀一遍 → 慢
有 KV Cache:
前面 99 個字的重點早就記在便利貼上了 → 直接看便利貼 → 快
關鍵直覺:這份筆記會隨著序列變長而變厚。
- 你的 prompt 越長 → 筆記越厚
- 模型生成的內容越長 → 筆記越厚
- 同時服務的請求越多 → 每個請求各有一份筆記,加起來越厚
這就是為什麼 GPU 記憶體會被吃掉一大半——不是模型權重吃的,是這些筆記吃的。
記憶體大概怎麼分配
一顆跑 LLM 的 GPU,記憶體大致分三塊:
GPU 記憶體
├── 模型權重 ← 固定的,模型多大就多大(例如 14GB)
├── KV Cache ← 會膨脹的,請求越多、序列越長吃越多 ← 主角
└── 其他雜項 ← 框架本身、暫存空間等
你的 24GB GPU 跑 14GB 的模型,扣掉權重和雜項,剩下的「才是」能拿來放 KV Cache 的空間。vLLM 會把這塊空間拿去切成一格一格的 block(下一段講),能塞幾格,就決定了你能同時服務多少請求、每個請求能多長。
PagedAttention:像作業系統一樣管筆記
知道 KV Cache 會膨脹之後,下一個問題是:怎麼管理這堆會膨脹的筆記?
舊做法的浪費
在 PagedAttention 出現前,傳統做法是:一個請求進來,就先「預留」一大塊連續記憶體,預設它可能會長到最大長度。
問題是:
- 大部分請求根本用不到最大長度,預留的空間就這樣空著浪費(內部碎片)
- 剩下的零碎空間東一塊西一塊,湊不出下一個請求要的「連續」大塊(外部碎片)
結果就是:記憶體明明還有,卻塞不進新請求。
PagedAttention 的點子
vLLM 的作者借用了一個作業系統管了幾十年記憶體的老招數:分頁(paging)。
作業系統不會給程式一整塊連續的實體記憶體,而是切成固定大小的「頁」,程式要多少給多少頁,這些頁在實體記憶體裡可以是散的,靠一張表對應起來。
PagedAttention 對 KV Cache 做一模一樣的事:
傳統做法:
請求A ▓▓▓▓▓▓▓▓░░░░░░░░ ← 預留一大塊,後面一半空著浪費
請求B ▓▓▓░░░░░░░░░░░░░ ← 又預留一大塊
PagedAttention:
把記憶體切成固定大小的小格子(block)
┌──┬──┬──┬──┬──┬──┬──┬──┐
│A │A │A │B │A │B │ │ │ ← 誰要用就給一格,用完再給
└──┴──┴──┴──┴──┴──┴──┴──┘
格子可以散著放,靠一張表串起來 → 幾乎不浪費
你只要記住三個好處:
- 不預留、用多少給多少 → 記憶體浪費大幅減少
- 碎片幾乎消失 → 同樣的 GPU 能塞下更多請求(更高併發)
- 格子可以共享 → 兩個請求如果開頭一樣(例如同一段 system prompt),可以共用同一批格子(這叫 prefix 共享,是第 4 篇的主角,這裡先埋個伏筆)
這個「固定大小的小格子」,在 vLLM 裡就叫 block,大小由 --block-size 控制(預設通常是 16,表示一格裝 16 個 token 的筆記)。
看懂 vLLM 啟動日誌
理解了上面的概念,現在來看 vLLM 啟動時印出來的那幾行關鍵日誌。這是你每次上線都該掃一眼的資訊:
INFO ... Memory profiling results: ... available_kv_cache_memory=8.42GiB
INFO ... GPU KV cache size: 74,096 tokens
INFO ... Maximum concurrency for 8,192 tokens per request: 9.04x
一行一行翻譯:
available_kv_cache_memory=8.42GiB
→ 扣掉模型權重和雜項後,還有 8.42GB 拿來放 KV Cache 筆記
GPU KV cache size: 74,096 tokens
→ 這 8.42GB 換算下來,總共能存 74,096 個 token 的筆記
→ (有些版本會印成 "# GPU blocks: 4631" 之類,block 數 × block-size 就是這個 token 數)
Maximum concurrency for 8,192 tokens per request: 9.04x
→ 如果每個請求都吃滿 8,192 token(= 你設的 max-model-len),
那大約能同時塞 9 個請求
→ 74,096 ÷ 8,192 ≈ 9.04
Code language: JavaScript (javascript)這三行就是你的容量儀表板。 覺得併發不夠?就是要想辦法把 GPU KV cache size 這個數字做大,或把每個請求的 max-model-len 做小。怎麼調,看下一段。
認得就好:不同 vLLM 版本這幾行的用字會略有不同(有的印
# GPU blocks、有的印GPU KV cache size),但意思都是「總共能存多少筆記」和「能同時服務幾個請求」。抓這兩個數字就對了。
實務調參:四個你會動到的參數
以下四個是你 90% 情況會用到的參數。先看它們各自在幹嘛:
| 參數 | 白話意思 | 調大會怎樣 |
|---|---|---|
--gpu-memory-utilization |
允許 vLLM 用掉 GPU 記憶體的比例(預設 0.9) | 更多空間給 KV Cache,但太貪心會 OOM |
--max-model-len |
單一請求最長能到多少 token(prompt + 生成) | 每個請求上限更高,但吃更多筆記空間 |
--max-num-seqs |
最多同時處理幾個請求 | 併發更高,但每個請求分到的空間變少 |
--block-size |
一格 block 裝幾個 token 的筆記(預設 16) | 通常不用動,用預設就好 |
--gpu-memory-utilization:給 vLLM 多大的碗
vllm serve my-model --gpu-memory-utilization 0.9
Code language: CSS (css)翻譯:「你最多可以用掉這張 GPU 90% 的記憶體」。剩下的 10% 留給系統和安全緩衝。
- 調高(例如 0.95)→ 更多空間放 KV Cache → 併發更高,但太貪心容易 OOM,尤其這張卡上還有別的程式在跑時
- 調低(例如 0.8)→ 比較安全,但能塞的請求變少
這是你記憶體不夠時第一個該檢查的參數:是不是設太高,把系統需要的餘裕也吃掉了。
--max-model-len:每個請求的長度上限
vllm serve my-model --max-model-len 8192
翻譯:「每個請求(prompt + 生成)最長 8192 個 token,超過就拒絕」。
這個參數對記憶體的影響很直接:回頭看剛剛的日誌,最大併發 = KV cache 總量 ÷ max-model-len。所以:
max-model-len設小 → 每個請求佔的空間上限變小 → 能塞更多請求max-model-len設大 → 支援超長 prompt,但併發直接被壓下來
實務心法:不要無腦設成模型支援的最大值(有些模型宣稱 128K)。你的用戶實際用不到那麼長,設太大只是白白壓低併發、還可能一啟動就 OOM。設成你實際需要的長度就好。
--max-num-seqs:同時服務幾個請求
vllm serve my-model --max-num-seqs 256
翻譯:「最多同時處理 256 個請求,多的排隊等」。
這是你用併發換穩定的旋鈕:
- 調高 → 吞吐量更高,但每個請求可用的 KV Cache 空間被稀釋,長請求可能被中途搶佔(preemption)而變慢
- 調低 → 每個請求更寬裕、更穩,但整體吞吐下降
--block-size:先別動它
vllm serve my-model --block-size 16
翻譯:「一格 block 裝 16 個 token 的筆記」。
這是 PagedAttention 那個「固定大小格子」的大小。新手直接用預設就好,除非你在做進階效能調校,否則動它通常弊大於利。認得它是什麼、知道它跟 PagedAttention 有關,就夠了。
OOM 了怎麼辦:排查順序
torch.OutOfMemoryError: CUDA out of memory 是你最常撞到的錯誤。按這個順序處理,不要亂槍打鳥:
1. 先降 --max-model-len
→ 最有效。每個請求的筆記上限直接變小,馬上省一大塊
→ 例如從 32768 降到 8192
2. 再降 --gpu-memory-utilization
→ 如果是「一啟動就 OOM」或這張卡有別的程式,
從 0.9 降到 0.85 / 0.8 留多點餘裕
3. 然後降 --max-num-seqs
→ 如果是「跑一陣子、併發衝高才 OOM」,把同時請求數壓下來
4. 都不行 → 換更小的模型 或 開量化版本(例如 AWQ/GPTQ)
→ 權重本身就佔太多,前三步救不回來
判斷技巧:看 OOM 是什麼時候發生的——
- 一啟動就 OOM → 權重 + 保留空間就超了,動
gpu-memory-utilization或換小模型 - 跑一陣子、流量上來才 OOM → 是 KV Cache 膨脹撐爆的,動
max-model-len或max-num-seqs
為什麼 vLLM 能服務更多請求
繞了一圈,回到最開始的直覺。同一張 GPU,為什麼 vLLM 能比土法煉鋼塞下更多併發請求?
因為 PagedAttention 把 KV Cache 這堆「會膨脹的筆記」管得又省又緊:不預留、不浪費、碎片幾乎為零,用多少給多少。省下來的每一格記憶體,都能拿去服務下一個請求。
而那個「格子可以共享」的伏筆——當很多請求都用同一段開頭(例如同一個 system prompt、同一份 few-shot 範例)時,這批筆記只存一份、大家共用——就是 prefix 共享 / prefix caching,能再進一步省記憶體、加速回應。這是第 4 篇的主題,到時候再展開。
🚩 紅旗:這些設定看到要警覺
紅旗 1:--gpu-memory-utilization 設到 0.98 甚至 1.0
vllm serve my-model --gpu-memory-utilization 0.99 # 🚩 沒留餘裕,隨時會 OOM
Code language: CSS (css)為什麼危險:GPU 記憶體不是只有 vLLM 在用,框架暫存、CUDA context、峰值波動都需要餘裕。設太滿,平常沒事,一遇流量尖峰就 OOM 掛掉。
跟 AI 這樣說:「這個 --gpu-memory-utilization 0.99 太貪心了,改成保守的 0.9 並解釋為什麼要留餘裕。」
紅旗 2:--max-model-len 無腦拉到模型最大值
vllm serve my-model --max-model-len 131072 # 🚩 用不到卻把併發壓到剩 1~2 個
Code language: PHP (php)為什麼危險:把長度上限設成 128K,代表 vLLM 要為「萬一有超長請求」預留規劃空間,結果併發直接崩到剩個位數,甚至一啟動就 OOM。多數場景根本用不到這麼長。
跟 AI 這樣說:「我實際請求最長大概 X token,把 --max-model-len 設成貼近實際需求的值,不要用模型上限,並說明這樣對併發的影響。」
紅旗 3:只加參數卻不看啟動日誌
為什麼危險:調完參數如果不回頭看 Maximum concurrency 和 GPU KV cache size 這兩行,你根本不知道這次調整到底讓併發變高還變低,等於瞎調。
跟 AI 這樣說:「幫我把 vLLM 啟動日誌裡跟 KV cache、max concurrency 有關的行找出來,解釋這次參數改動讓數字往哪個方向變了。」
Vibe Coder 驗收檢查點
- [ ] 啟動 vLLM 後,在日誌裡找到
GPU KV cache size(或# GPU blocks)和Maximum concurrency這兩行,讀出「總共能存幾個 token」和「大概能同時服務幾個請求」。 - [ ] 把
--max-model-len從大值改成小值(例如 32768 → 8192)重啟一次,觀察Maximum concurrency那行的倍數是不是變大了。 - [ ] 故意把
--gpu-memory-utilization設 0.99 試跑,看是否更容易 OOM 或啟動失敗,理解「留餘裕」的意義(測完記得改回 0.9)。 - [ ] 問 AI:「以我的 GPU 記憶體大小和這個模型,
--max-model-len和--max-num-seqs建議設多少?請解釋你怎麼從 KV cache 空間推算的。」 - [ ] 撞到 OOM 時,先確認錯誤是「一啟動」還是「流量上來後」發生的,再對照排查順序決定先動哪個參數。
看不懂就這樣問 AI
「我在跑 vLLM,貼上啟動日誌。請用白話解釋
GPU KV cache size、Maximum concurrency、available_kv_cache_memory這幾行各代表什麼,我是初學者。」
「我的 GPU 是 __GB,要跑 __ 這個模型,實際請求最長大約 __ token。請幫我建議
--gpu-memory-utilization、--max-model-len、--max-num-seqs該設多少,並解釋每個值怎麼影響記憶體和併發。」
「我遇到 CUDA out of memory,錯誤發生在 __(一啟動 / 跑一陣子後)。請按排查順序告訴我先調哪個 vLLM 參數、為什麼。」
小結
- KV Cache = 模型算過 token 的筆記,序列越長、請求越多吃越多記憶體,它才是吃掉 GPU 記憶體的主角(不是權重)。
- PagedAttention = 學作業系統分頁,把 KV Cache 切成固定大小的 block 管理,不預留、少碎片、可共享 → 同一張卡塞更多請求。
- 啟動日誌看兩個數字:KV cache 總量 和 max concurrency,這是你的容量儀表板。
- 四個常用參數:
--gpu-memory-utilization(碗多大)、--max-model-len(每份多長)、--max-num-seqs(同時幾份)、--block-size(格子大小,先別動)。 - OOM 排查順序:先降
max-model-len→ 再降gpu-memory-utilization→ 再降max-num-seqs→ 最後換小模型 / 量化。 - 「格子可以共享」的 prefix caching,留待第 4 篇。
下一篇(#03)我們會把服務推得更遠,看更多實戰場景的調校。
進階測驗:KV Cache 與 PagedAttention
共 5 題,包含情境題與錯誤診斷題。