KV Cache 分層:
LMCache 與 SSD 卸載
上一課你算的是「同一段前綴重複送,命中快取就便宜」。這一課把同一筆帳搬回自己的機器: 快取放不下的時候,能不能往 CPU RAM、甚至往 SSD 一層層丟,下次再撈回來就不用重算? 先點點看這四層各是什麼角色——右邊那欄是把一段 10k token 的前綴從那一層拿回 GPU要多久。
右邊的實驗場是真的 Python(在你的瀏覽器裡跑,不用安裝任何東西)。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。這一課只有兩筆帳要算: KV 有多大、載回來比重算快多少——兩筆都在右邊真的算, 改壞了重新整理就復原。
首 token 的延遲,幾乎全是 prefill
使用者感受到的「它怎麼還不開始講話」= TTFT(time to first token),而 TTFT 幾乎等於 prefill:把整段輸入的 K/V 算出來。長文件、長 system prompt、RAG 的固定知識前綴—— 每來一次就從頭算一次。前綴越長,等越久。
vLLM 內建的 prefix cache 會幫你省掉重複的部分,但它有三個天生的限制:
| 限制 | 後果 |
|---|---|
| 只活在 GPU 記憶體 | 容量就是扣掉模型權重之後剩下的那點 VRAM,通常是 GB 級 |
| 大家互相擠壓 | 多使用者、長上下文同時在線,LRU 隨時把你的前綴逐出去 |
| 沒有任何持久層 | server 一重啟全部歸零,熱門前綴又得從頭 prefill 一輪 |
所以第一件事不是急著裝東西,是先算你的 KV 到底有多大—— 這個數字決定它應該待在哪一層。 公式就是第 3 課那條:每個 token 的 KV 只跟架構有關, 跟你問什麼無關。
LMCache:GPU 放不下的 KV,往下收
LMCache 以 KV connector 的形式掛進 vLLM engine,把單層的 GPU KV pool 變成一疊: GPU 逐出的 KV 先收進 CPU RAM,再往下收進本機 SSD,還可以再往外接遠端後端。 同一段前綴下次出現時,從下層把算好的 KV 撈回 GPU,直接跳過 prefill。
本質很單純:拿 IO 換計算。搬運要花時間,但只要搬運比重算便宜,你就賺。 所以模型計算越重、前綴越長,這筆交換就越划算——這是全課的主軸,右邊 2️⃣ 會把兩條線畫給你看。
vLLM 自己也做了一部分,分工大致是這樣:
| 能力 | vLLM 原生 | LMCache |
|---|---|---|
| KV 從 prefill 端傳給 decode 端 | ✅(靠 connector) | ✅ |
| CPU RAM KV offload | ✅ | ✅ |
| 本機 SSD KV offload | ❌ 原生沒有 | ✅ |
| Redis / S3 等遠端 KV | ❌ | ✅ |
| KV sharing(跨請求、跨副本共用同一份) | 部分靠 connector | ✅ 核心功能 |
| 快取的持久化層 | ❌ | ✅ |
兩件事:掛 connector、選層
第一件事在啟動參數,把 LMCache 掛成 KV connector:
kv_role=kv_both 是同時「存」與「取」——只存不取或只取不存都是有意義的角色,做 prefill/decode 分離時會用到。
第二件事在環境變數,決定 KV 往下收到哪一層:
| 環境變數 | 作用 | 示範值 |
|---|---|---|
| LMCACHE_CHUNK_SIZE | 幾個 token 切成一塊(快取的對齊單位) | 256 |
| LMCACHE_LOCAL_CPU | 開/關 CPU RAM 層 | False |
| LMCACHE_LOCAL_DISK | 磁碟層路徑(file:// URI) | file:///lmcache-disk/ |
| LMCACHE_MAX_LOCAL_DISK_SIZE | 磁碟層上限(GB,滿了 LRU 逐出) | 20 |
| LMCACHE_PRE_CACHING_HASH_ALGORITHM | chunk 檔名的雜湊,務必固定 | sha256 |
上表那組值是一份公開實測的 SSD-only 設定:刻意把 LMCACHE_LOCAL_CPU 關掉, 好讓「從磁碟撈回來」這件事單獨被觀察到。正式服務通常兩層都開。
三個上線前要先想過的地雷:
| 地雷 | 怎麼避 |
|---|---|
| 雜湊沒固定 | chunk 是用內容雜湊當檔名找回來的。演算法一飄,舊 chunk 全部對不上,等於白存——明確釘 sha256 |
| 請求被中途 abort | stop 參數提早收尾、串流中途斷線,都會走到中止路徑。上線前先壓測這條路徑,別等使用者幫你測 |
| 磁碟無限長大 | 用 MAX_LOCAL_DISK_SIZE 給上限,並想好清理策略;快取是高頻寫入,SSD 的 TBW 會被吃掉 |
三種情境,一行日誌,三個數字
要證明「SSD 真的有救到你」,得設計得讓快取非命中不可。一份公開實測是這樣設計的: 把 GPU 的 KV pool 壓到只剩約 2.7k tokens(容易觀察逐出),每個情境跑三次取中位數。
| 情境 | 做什麼 | 意義 |
|---|---|---|
| a 冷啟動 | 文件第一次出現 → 全量 prefill(同時 LMCache 把 KV 寫進 SSD) | 基準 1.0x |
| b GPU 命中 | 同一份文件馬上再問一次 → vLLM 自己的 prefix cache 命中 | 速度上限 |
| c SSD 複用 | 先灌兩份別的文件把它擠出 GPU pool,再回頭問 → 從 SSD 撈回 | 本課重點 |
沒有 LMCache 的話,情境 c 會直接退化成 a——全量重算。那就是磁碟層的全部價值。 而「真的命中了」不是用感覺判斷的,server 端的日誌會講得很白:
computed tokens: 0 = 引擎一個 token 都沒重算; need to load: 1280 = 這 1280 個 token 的 KV 全部是從下層載回來的。 調參數之後先看這一行,再看碼表。
三個模型跑同一套流程,冷啟動 vs SSD 複用的 TTFT 加速比:
| 模型 | 加速比 | 為什麼是這個幅度 |
|---|---|---|
| Qwen3-0.6B(bf16) | 1.1x | prefill 太便宜了,重算跟載入一樣快 |
| Qwen3-1.7B(FP8) | 1.2x | kernel 很快,計算仍然太便宜 |
| Qwen3-4B-AWQ(int4) | 2.3x | 反量化讓 prefill 變重、KV 又相對小,分子分母同時往好的方向走 |
實測(RTX 4090、vLLM + LMCache 0.5.0、2026-07)。
這三個數字最值得記的不是大小,是排序:計算越重越賺。同一個道理往上推—— 4B 級模型配 10k tokens 的前綴,冷 prefill 是秒級的事, 倍率沒變、但省下的絕對時間整個放大一個數量級。加速比也吃 SSD 的實際頻寬與 OS page cache(第二次跑常常特別快,因為根本沒碰到碟)。
右邊 3️⃣ 用純頻寬算出「上限加速比」,你會發現它比這三個實測數字大得多—— 那個差額就是這一課最該帶走的東西:模型告訴你誰有機會贏,實測告訴你實際贏多少。
先確認你真的需要——多數人不需要
這一節可能會省下你一筆錢。判斷準則只有一句: 先看 CPU RAM 是不是已經不夠用;沒到那個門檻,就不需要 SSD 層。
把數字擺出來就很清楚。8B 級 GQA 模型每個 token 的 KV 是 128 KB(右邊 1️⃣ 算給你看),於是:
| 場景 | KV 需求 | 該放哪一層 |
|---|---|---|
| 單人 32k 上下文(讀論文) | 4 GB | VRAM 就夠 |
| 10 個人各 8k | 10 GB | CPU RAM |
| 單人 128k 上下文(整本書) | 16 GB | CPU RAM |
| 單人 1M 上下文 | 122 GB | 這才輪到 SSD |
看出來了嗎——常見的那張「超過可用 VRAM 就需要 SSD」的表,跳過了中間那一層。 VRAM 和 SSD 之間還有 CPU RAM,而一台 64 GB 記憶體的個人機, 128k 上下文只用掉它的四分之一。另一份公開實測也是同一個結論: 14B 級模型即使塞進一整本長篇小說份量的上下文,KV 也不到 20 GB,CPU RAM 根本用不完。
SSD 層真正的主場是多人共用的伺服器:大量並發 × 長上下文同時存在, 把 GPU 和 CPU RAM 一起塞爆,這時候再往下卸載才有意義。 個人/小模型場景很難觸發到那個門檻。
那什麼時候值得開?把兩個條件同時滿足才算:KV 大到 CPU RAM 裝不下, 而且同一段長前綴會被重複使用。第二個條件常被忽略——每次都是全新的 prompt 時, 快取層完全無效,每一發都是冷啟動。賺最多的是這三種流量:
| 🔁 RAG 的固定知識前綴 | 同一份文件被反覆檢索、反覆送進來 |
| 🔁 多人共用的長 system prompt | 一段幾千 token 的規則,每個人每一發都帶著 |
| 🔁 多輪 agent 對話 | 歷史越滾越長,而前面那一大段每輪都一樣 |
成本直覺:同樣容量,NVMe SSD 的每 GB 價格比 VRAM 低了兩個數量級, 功耗差距也是同一個量級(多一顆碟是個位數瓦特,換一張大 VRAM 的卡是幾百瓦)。 所以「真的需要容量」的時候,SSD 是壓倒性划算的選項—— 但「真的需要」這四個字要先用上面的表確認過,不然買了只會閒置。
也別把它當成「反正裝得下就全塞」的許可證。把幾百篇文章整段塞進前綴快取起來, 雜訊變多反而更容易答錯;精準檢索 top-k 的答案品質通常更好。 快取層解決的是「同一段前綴重複用的成本」,不是「上下文塞越多越聰明」。
最後是定位:這一版的本機 SSD 層是「服務存活期間的擴容」,不是持久化—— 它把 prefix cache 的容量從 GB 級擴到數十 GB,但 server 重啟後別預期它自己認得舊 chunk。 真的要跨重啟、跨機共享,走遠端後端(Redis / S3 / P2P)。 還有一個很多人踩的認知落差:這一層只加速 prefill,decode 一點都不會變快—— 長回答慢是記憶體頻寬的問題,快取救不了。
decode 那一半怎麼加速?那是下一課的事:讓一個小模型先猜一串 token,大模型一次驗完。
訓練也能卸載到 SSD:ZeRO-Infinity
同一個「往下卸載」的想法,在訓練側也有一套:DeepSpeed 的 ZeRO-Infinity。 訓練時 GPU 上要放的東西比推論多得多——模型參數 + 梯度 + 優化器狀態, 7B 級模型全參數訓練動輒 100 GB 起跳,單卡 VRAM 完全不夠。 ZeRO-Infinity 把「這一刻暫時用不到」的狀態卸載到 NVMe,要算的時候才搬回來:
| 層 | 放什麼 | 頻寬量級 |
|---|---|---|
| GPU VRAM(數十 GB) | 正在計算的參數分片 | ~1000 GB/s |
| CPU RAM(數百 GB) | 優化器狀態、梯度 | ~50 GB/s |
| NVMe SSD(數 TB) | 放不下的全部 | ~5–7 GB/s |
關鍵技巧和 KV 卸載一樣:預先讀取 + 非同步 I/O,把搬運藏在計算後面,讓 GPU 盡量不停等。 但代價很誠實——會慢 5~20 倍。它是「跑得動 vs 跑不動」的方案,不是加速方案。 硬體也有條件:必須是 PCIe NVMe(SATA 太慢不可行),而且高頻寫入很吃 SSD 壽命。
所以優先順序是固定的:純 GPU → CPU offload → NVMe offload,能停在前面就別往後走。 而在往後走之前還有一步更划算的:微調先試 LoRA—— 用可調的參數量直接把記憶體需求壓下來,常常整個卸載需求就消失了, 順便避開全參數微調的災難性遺忘。那是第 8 課的主題。
這一節是延伸整理,不是本課的實測——真的要用 ZeRO-Infinity,請以官方文件為準並自己跑過一輪。
換你動手
把 1️⃣ 的 KV 型別改成 fp8,看 128k 上下文的需求掉到多少——4️⃣ 的判斷器會不會從「開 CPU 層」翻回「VRAM 就裝得下」?
算出損益平衡的 prefill 吞吐(頻寬 ÷ 每 token 的 KV)。為什麼這個純頻寬上限幾乎永遠大於 1,實測卻只有 1.1–2.3x?差額跑到哪裡去了?
換成你自己服務的真實數字(層數與 KV head 數看模型 config.json、prefill 吞吐壓一次長 prompt、SSD 頻寬用 fio 量),重跑判斷器;再用你量到的加速比回推「你真正吃到了標稱頻寬的幾成」。
卡住了?三題在 notebook 最後一格都有折疊解答——先自己做,再打開對照。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
你的服務每週重啟一次做模型更新,重啟後前十分鐘特別慢——熱門的長 system prompt 全部要重新 prefill。你已經開了 LMCACHE_LOCAL_DISK,磁碟上也還留著上一輪的 chunk 檔。最合適的做法是?
這一版本機磁碟層的定位是「把 prefix cache 的容量從 GB 級擴到數十 GB」,在服務存活期間有效;真的要跨重啟、跨副本共享同一份 KV,是遠端後端(Redis / S3 / P2P)的工作。搞錯定位就會一直去調錯的旋鈕:A 只是讓檔案留在碟上,服務重啟後不會因此就認得它們;B 剛好反了——雜湊要固定成 sha256,chunk 檔名才可重現,換演算法會讓舊 chunk 全部對不上;D 的檔案本來就在原位,問題從來不是檔案在不在。務實的短解是重啟後主動暖機,長解是升級到遠端後端。
Q2 錯誤診斷
同事說「LMCache 我開好了,但完全沒感覺變快」,並貼出 server 日誌。他的結論是「hit tokens 有數字=快取沒問題,一定是別的地方慢」。他哪裡看錯了?
這行日誌是完美命中的樣子:computed tokens: 0 代表引擎一個 token 都沒重算,1280 個 token 的 KV 全部從下層載回。他讀對了日誌,錯在期待——加速比 = 該層頻寬 ÷(prefill 吞吐 × 每 token 的 KV),模型小、計算便宜的時候這個比值本來就接近 1(實測 0.6B bf16 只有 1.1x,4B-AWQ 才有 2.3x)。要有感,得是計算更重的模型或更長的前綴。A 誤讀了欄位語意,need to load 是「需要從下層載回的量」不是待辦;B 若成立,hit tokens 會是 0 而不是 1280;D 是憑空猜參數,日誌裡沒有任何支持它的訊號。
Q3 錯誤診斷
同事用判斷器算完自己的機器,得到這段輸出,結論是「16 GB 超過我的 10 GB 可用 VRAM,所以要買 SSD 開 LMCACHE_LOCAL_DISK」。他的診斷錯在哪?
這是本課最容易犯的錯:把「超過 VRAM」直接等於「需要 SSD」,跳過了中間的 CPU RAM 層。判斷準則是先確認 CPU RAM 是不是真的不夠用——128k × 1 人 = 16 GB,在一台 64 GB 的機器上連四分之一都不到,開 LMCACHE_LOCAL_CPU 就結束了,還比 SSD 快一個數量級。A 就是那個跳過中間層的錯誤結論;C 的算術沒問題(2 × 32 層 × 8 個 KV head × 128 維 × 2 bytes = 128 KB/token,乘上 131072 個 token 正好是 16 GB);D 搞混了對象——chunk size 只是快取的對齊單位,改它不會讓 KV 本身變小,真要壓小 KV 得靠 KV 量化或縮短上下文。
Q4 情境題
你的 RAG 客服目前把檢索到的 top-5 段落貼進提示詞,效果不錯。你想:「反正 SSD 層裝得下,乾脆把整個知識庫幾百篇文章都做成固定前綴快取起來,一次命中永遠命中。」最好的判斷是?
快取層優化的是成本,不是品質;這兩件事被混在一起就會做出「反正裝得下就全塞」的決定。實務上一直塞文章不是好做法,雜訊變多反而更容易答錯——檢索 top-k 的品質通常更好。A 講的省時是真的,但它只回答了成本、沒回答品質,而品質才是這個決定會壞掉的地方;B 同樣只處理容量;D 的方向反了——載回時間 = KV ÷ 頻寬,通常仍遠快於重算(本課的模型算出來的上限就遠大於 1x),問題不在載得慢。