【vLLM 工程師實戰】#02 KV Cache 與 PagedAttention:記憶體去哪了、參數怎麼調

測驗:KV Cache 與 PagedAttention

共 5 題,點選答案後會立即顯示結果

1. 根據文章,一顆 GPU 跑 LLM 時,「會隨著請求變多、序列變長而膨脹」、吃掉大半記憶體的主角是什麼?

  • A. 模型權重
  • B. CUDA context 與框架暫存
  • C. KV Cache
  • D. block-size 設定值

2. 文章用「便利貼筆記」比喻 KV Cache,主要是為了說明它的什麼作用?

  • A. 存起算過的中間結果,避免每吐一個字都把前面重算一遍
  • B. 把模型權重壓縮以節省硬碟空間
  • C. 記錄使用者的歷史對話以便日後查詢
  • D. 加密請求內容以保護隱私

3. PagedAttention 借用了作業系統的哪個老招數來管理 KV Cache?

  • A. 多執行緒排程
  • B. 分頁(把記憶體切成固定大小的頁/block 管理)
  • C. 檔案系統快取
  • D. 垃圾回收(GC)

4. 看到以下啟動日誌,這行主要在告訴你什麼?

Maximum concurrency for 8,192 tokens per request: 9.04x
  • A. 模型權重佔了 9.04GB
  • B. 每個請求最多能生成 9 個 token
  • C. block-size 被設成 9
  • D. 若每個請求都吃滿 8,192 token,大約能同時服務 9 個請求

5. 依文章的 OOM 排查順序,遇到記憶體不足時「最有效、應該最先動」的參數是哪一個?

  • A. –block-size
  • B. –max-model-len
  • C. –max-num-seqs
  • D. 直接換更小的模型

系列第 2 篇,共 4 篇|難度:L2-進階
前置:已讀第 1 篇,能用 vllm serve 把模型跑起來,知道 token、context length 是什麼。

第 1 篇你已經把模型服務起來了。這一篇要回答一個你遲早會遇到的問題:

「我的 GPU 明明有 24GB,模型檔案才 14GB,為什麼一跑起來就說記憶體不夠 / OOM?剩下的記憶體到底被誰吃掉了?」

答案的主角就是這篇的兩個名詞:KV CachePagedAttention。搞懂它們,你就能看懂 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 │  │  │  ← 誰要用就給一格,用完再給
  └──┴──┴──┴──┴──┴──┴──┴──┘
  格子可以散著放,靠一張表串起來 → 幾乎不浪費

你只要記住三個好處:

  1. 不預留、用多少給多少 → 記憶體浪費大幅減少
  2. 碎片幾乎消失 → 同樣的 GPU 能塞下更多請求(更高併發)
  3. 格子可以共享 → 兩個請求如果開頭一樣(例如同一段 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,1929.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-lenmax-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 concurrencyGPU 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 sizeMaximum concurrencyavailable_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 題,包含情境題與錯誤診斷題。

1. 情境判斷 情境題

你的 GPU 有 24GB,跑一個 14GB 的模型。啟動日誌顯示: Maximum concurrency for 32,768 tokens per request: 1.5x 你發現併發太低,服務常常排隊。你「知道用戶實際的 prompt 很少超過 8K token」。 最該優先做的調整是?
  • A. 把 –gpu-memory-utilization 從 0.9 拉到 0.99
  • B. 把 –max-model-len 從 32768 降到 8192
  • C. 把 –block-size 從 16 改成 32
  • D. 立刻換一個更小的模型

2. 情境判斷 情境題

你的服務有一段固定的長 system prompt,每個請求開頭都一模一樣。 同事說:「這種情況 vLLM 有辦法讓這些相同開頭的 KV Cache 不用各存一份。」 這指的是文章預告的哪個機制(第 4 篇主題)?
  • A. 提高 gpu-memory-utilization
  • B. 把 block-size 調大
  • C. prefix 共享 / prefix caching(相同開頭的 block 可共用)
  • D. 降低 max-num-seqs

3. 情境判斷 情境題

你調完參數重啟 vLLM,想確認這次改動「到底讓能同時服務的請求數變多還變少」。 你應該去日誌裡看哪些行來判斷,而不是憑感覺?
  • A. GPU KV cache size(或 # GPU blocks)與 Maximum concurrency 這兩行
  • B. 只看模型權重的載入時間
  • C. 看 CUDA 版本號那一行
  • D. 看 API 監聽的 port 那一行

4. 錯誤診斷 錯誤診斷

# AI 幫你產生的啟動指令 vllm serve my-model \ –gpu-memory-utilization 0.99 \ –max-model-len 131072 # 現象:平常沒事,一遇到流量尖峰就整個服務 OOM 崩潰 這段設定最主要的問題是什麼?
  • A. 沒設 –block-size 導致分頁失效
  • B. 模型名稱寫錯
  • C. –max-model-len 設太小,長請求被拒絕
  • D. 記憶體用到 0.99 沒留餘裕,加上 max-model-len 拉到 128K 壓垮空間,尖峰時 KV Cache 撐爆而 OOM

5. 錯誤診斷 錯誤診斷

兩位工程師都遇到 CUDA out of memory,症狀不同: 甲:vllm serve 一啟動、還沒有任何請求進來就直接 OOM 乙:服務跑得好好的,但同時湧入大量長請求時才 OOM 按文章的判斷技巧,他們該優先動的參數分別是?
  • A. 甲乙都先降 max-num-seqs
  • B. 甲降 max-num-seqs;乙降 gpu-memory-utilization
  • C. 甲動 gpu-memory-utilization 或換小模型;乙動 max-model-len 或 max-num-seqs
  • D. 甲乙都只能換更小的模型,別無他法

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *