AI 互動教室 ‹ 個人地端大語言模型實作
下載 .py 單獨開啟實驗場 ↗ 留言回報
OFFLOAD · 05 · 分層儲存與 SSD 卸載

KV Cache 分層:
LMCache 與 SSD 卸載

上一課你算的是「同一段前綴重複送,命中快取就便宜」。這一課把同一筆帳搬回自己的機器: 快取放不下的時候,能不能往 CPU RAM、甚至往 SSD 一層層丟,下次再撈回來就不用重算? 先點點看這四層各是什麼角色——右邊那欄是把一段 10k token 的前綴從那一層拿回 GPU要多久。

右邊的實驗場是真的 Python(在你的瀏覽器裡跑,不用安裝任何東西)。 首次載入約需 30–60 秒,正好夠你讀完第 1 節。這一課只有兩筆帳要算: KV 有多大載回來比重算快多少——兩筆都在右邊真的算, 改壞了重新整理就復原。

01 · 問題

首 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 只跟架構有關, 跟你問什麼無關。

02 · 分層儲存

LMCache:GPU 放不下的 KV,往下收

LMCacheKV 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✅ 核心功能
快取的持久化層
03 · 怎麼開

兩件事:掛 connector、選層

第一件事在啟動參數,把 LMCache 掛成 KV connector:

vllm serve <model> \ --kv-transfer-config '{"kv_connector":"LMCacheConnectorV1","kv_role":"kv_both"}'

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_ALGORITHMchunk 檔名的雜湊,務必固定sha256

上表那組值是一份公開實測的 SSD-only 設定:刻意把 LMCACHE_LOCAL_CPU 關掉, 好讓「從磁碟撈回來」這件事單獨被觀察到。正式服務通常兩層都開。

三個上線前要先想過的地雷:

地雷怎麼避
雜湊沒固定chunk 是用內容雜湊當檔名找回來的。演算法一飄,舊 chunk 全部對不上,等於白存——明確釘 sha256
請求被中途 abortstop 參數提早收尾、串流中途斷線,都會走到中止路徑。上線前先壓測這條路徑,別等使用者幫你測
磁碟無限長大MAX_LOCAL_DISK_SIZE 給上限,並想好清理策略;快取是高頻寫入,SSD 的 TBW 會被吃掉
04 · 實測

三種情境,一行日誌,三個數字

要證明「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 端的日誌會講得很白:

Inference Engine computed tokens: 0, LMCache hit tokens: 1280, need to load: 1280

computed tokens: 0 = 引擎一個 token 都沒重算need to load: 1280 = 這 1280 個 token 的 KV 全部是從下層載回來的。 調參數之後先看這一行,再看碼表。

三個模型跑同一套流程,冷啟動 vs SSD 複用的 TTFT 加速比:

模型加速比為什麼是這個幅度
Qwen3-0.6B(bf16)1.1xprefill 太便宜了,重算跟載入一樣快
Qwen3-1.7B(FP8)1.2xkernel 很快,計算仍然太便宜
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️⃣ 用純頻寬算出「上限加速比」,你會發現它比這三個實測數字大得多—— 那個差額就是這一課最該帶走的東西:模型告訴你誰有機會贏,實測告訴你實際贏多少。

05 · 判斷

先確認你真的需要——多數人不需要

這一節可能會省下你一筆錢。判斷準則只有一句: 先看 CPU RAM 是不是已經不夠用;沒到那個門檻,就不需要 SSD 層。

把數字擺出來就很清楚。8B 級 GQA 模型每個 token 的 KV 是 128 KB(右邊 1️⃣ 算給你看),於是:

場景KV 需求該放哪一層
單人 32k 上下文(讀論文)4 GBVRAM 就夠
10 個人各 8k10 GBCPU RAM
單人 128k 上下文(整本書)16 GBCPU 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,大模型一次驗完。

06 · 延伸

訓練也能卸載到 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,請以官方文件為準並自己跑過一輪。

07 · 實戰

換你動手

LEVEL 1

把 1️⃣ 的 KV 型別改成 fp8,看 128k 上下文的需求掉到多少——4️⃣ 的判斷器會不會從「開 CPU 層」翻回「VRAM 就裝得下」?

LEVEL 2

算出損益平衡的 prefill 吞吐(頻寬 ÷ 每 token 的 KV)。為什麼這個純頻寬上限幾乎永遠大於 1,實測卻只有 1.1–2.3x?差額跑到哪裡去了?

LEVEL 3

換成你自己服務的真實數字(層數與 KV head 數看模型 config.json、prefill 吞吐壓一次長 prompt、SSD 頻寬用 fio 量),重跑判斷器;再用你量到的加速比回推「你真正吃到了標稱頻寬的幾成」。

卡住了?三題在 notebook 最後一格都有折疊解答——先自己做,再打開對照。

08 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

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 有數字=快取沒問題,一定是別的地方慢」。他哪裡看錯了?

Inference Engine computed tokens: 0, LMCache hit tokens: 1280, need to load: 1280

這行日誌是完美命中的樣子: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」。他的診斷錯在哪?

每 token KV = 128 KB 131072 tokens x 1 人 → 16.00 GB → 放在 CPU RAM 損益平衡 prefill 吞吐 = 49,152 tokens/s

這是本課最容易犯的錯:把「超過 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),問題不在載得慢。

Python 環境載入中(首次約 30–60 秒)…讀完第 1 節它就好了