管理 KV 快取
計算每 token 記憶體、配置完整區塊,分辨當下容量與完成生成所需空間。
- 作者
- Huang Tzu LinFounder
- 發布日期
- 閱讀時間
- 約 20 分鐘
- 更新於
目錄
- 01本篇在學習路線中的位置
- 02開始之前:先備知識與學習成果
- 03你將建置的成品
- 04鎖定架構與記憶體預算
- 05三種截然不同的記憶體浪費
- 預留浪費(Reservation Waste)
- 內部碎裂(Internal Fragmentation)
- 外部碎裂(External Fragmentation)
- 06如何正確解讀 2023 年 PagedAttention 的實驗結果
- 07重算單一 Token 的位元組數
- 08完整範例:825 個 Token 的請求
- 連續空間預留
- 16-Token 區塊的分頁配置
- 0912 GiB KV 預算下的容量計算
- 10區塊表與跨序列共用
- 11量化 KV、記憶體卸載與分層儲存
- 量化 KV(Quantized KV)
- 記憶體卸載(Offload)
- 共享與分層 KV 快取
- 12建立容量計算器
- 13刻意破壞的案例
- 一體適用的 4–5× 宣稱
- 把 16-Token 區塊當成萬用常數
- 代錯 Head 數量
- 誤以為記憶體瓶頸「已經徹底解決」
- 14動手實作
- 15可量測的完成標準
- 16提取練習
- 17解答
- 18本文不涵蓋的內容
- 19來源與延伸閱讀
KV 快取是隨請求動態變化的狀態:每處理一個 token 就跟著增加,各序列的長度不一,還要與模型權重、執行期工作空間爭搶加速器記憶體。分頁(paging)把這些狀態存進固定大小、不必連續的區塊,任何請求都不必預留一整塊連續空間。不過,分頁不會讓記憶體變得無限,也不保證通用的容量倍數;KV 成長、前綴留存、量化與多層儲存放置,仍是各自獨立的工程課題。
本篇教學會針對明確的 GQA 架構計算 KV 記憶體占用,逐步拆解一筆示意用的 825 個 token 區塊配置範例,在明訂的 12 GiB 預算下對照連續預留與分頁配置的容量差異,最後延伸到前綴重用、量化 KV、主機記憶體卸載(offload)與分散式分層快取。
以區塊配置 KV 快取
哪些是有效資料,哪些是已配置空間?
已用 token 不等於已配置空間
每個區塊預留 16 個 token 位置。藍色位置存放資料;虛線位置已配置但尚未使用。
下一個 token 可放進區塊 52,不必配置新區塊;之後仍有 6 個已配置位置未使用。
ceil(825 / 16) × 16 = 832 token 位置檢視配置概覽
顯示前 31 個與最後區塊;省略 20 個完整區塊。
此測例中,12 GiB 可容納 2,457 個完整區塊;未計執行期額外成本。
80 層、8 個 KV heads、head dimension 128;12 GiB 資源池的完整區塊容量。
本篇如何銜接前後內容
本篇在學習路線中的位置
帶入前作:I-00:請求時間軸與指標計算器
本篇新增:KV 區塊容量計算表。請將假設、來源 ID 與失敗案例連同成果一併留存。
後續用途:I-06、I-04、I-03 與 I-07 將沿用這些成果。跨主題系列的先備順序請見完整閱讀路線。文章 ID 是固定標籤,編號不代表閱讀順序。
學習目標與實作準備
開始之前:先備知識與學習成果
請先完成 I-00,確認你已熟悉標準的單 token KV 位元組公式,也清楚在 GQA 下,公式為何要代入 KV head 數而非 query head 數。
讀完本篇後,你將能夠:
針對明確的 MHA 或 GQA 架構算出每個 token 的 KV 位元組數;
區分預留浪費(reservation waste)、內部碎裂(internal fragmentation)與外部碎裂(external fragmentation);
計算分頁區塊配置量與尾端區塊(tail-block)浪費;
將 2023 年 PagedAttention 論文的使用率與吞吐量數字,限定在產生這些數字的比較基線與工作負載範圍內;
分清底層分頁機制與上層自動前綴比對/留存策略的職責邊界;以及
判斷導入 KV 量化、主機記憶體卸載或共享快取層時,何時需要重新做延遲與品質基準測試。
你將建置的成品
你會產出 kv-capacity.json,內容包含:
模型架構與 KV 資料型別;
每個 token 的 KV 位元組數;
記憶體池可切出的總 token 槽位數與完整區塊數;
同一筆 825-token 請求在連續預留與分頁配置下的占用量;
在 825 個 token 的快照下,兩種配置策略各自可容納的請求數;以及
64-head MHA 與理想化 FP8 KV 的敏感度對照。
此外,你也會為一組混合長度的請求序列建立區塊配置對照表。
鎖定架構與記憶體預算
本篇範例沿用 I-00 的假設性 80 層 GQA 模型規格:
輸入項目 | 假設條件 |
|---|---|
Transformer 層數 | 80 |
Query head 數 | 64 |
KV head 數 | 8(GQA) |
Head 維度 | 128 |
KV 資料型別 | BF16,2 bytes |
專供 KV 使用的記憶體預算 | 12 GiB |
歷史教學用區塊大小 | 16 個 token |
最大序列預留上限 | 每筆請求 2,048 個 token 槽位 |
典型實際使用長度 | 每筆請求 825 個 token |
16 個 token 的區塊大小沿用原始 PagedAttention 的預設值,並非通用規則:區塊大小取決於推論引擎、注意力 kernel 與硬體後端的選擇。
三種截然不同的記憶體浪費
預留浪費(Reservation Waste)
本篇的比較策略會在請求進場時,直接預留該請求的最大序列長度。連續配置本身並不強制採用這種策略;能擴充或壓縮區段的配置器可以避開它,但要付出資料複製等其他成本。當這個策略替一筆目前只處理到 825 個 token 的序列預留 2,048 個槽位,其餘 1,223 個槽位就算下一步生成即告結束,也無法讓給其他請求。
內部碎裂(Internal Fragmentation)
只要以固定大小的區塊為單位配置記憶體,序列長度沒剛好落在區塊邊界上就會留下未填滿的尾端空間。在單一快取池、無前瞻槽位預留(lookahead allocation)也無額外推測式保留的單純分頁模型中,每條序列只有最後一個區塊可能未填滿,因此內部尾端浪費最多就是 block_size - 1 個槽位。若引擎混合多種注意力快取池、有推測式解碼的預留槽位,或有留存的共用區塊,則須另外列帳。
外部碎裂(External Fragmentation)
不同長度的連續空間反覆配置與釋放後,記憶體池裡會散落許多大小不一的空隙;即使空隙加總的位元組數夠用,新請求仍可能因為找不到夠大的單一連續區段而無法准入。把記憶體池切成等大的實體區塊,就不再需要連續區段;但區塊表(block table)中繼資料、注意力 kernel 的間接定址與放置開銷依然存在。
如何正確解讀 2023 年 PagedAttention 的實驗結果
Kwon 等人論文的圖 2 剖析了第 6.2 節實驗期間的記憶體使用:在作者自行重新實作的 Orca (Max)、Orca (Pow2) 與 Orca (Oracle) 中,真正存放 token 狀態的空間只占 KV 快取所用記憶體的 20.4%–38.2%,其餘約 60%–80% 被預留、碎裂與其他開銷占去。這些數字對應的是特定的受測配置策略。論文另外報告,在相同延遲下,vLLM 的整體系統吞吐量是受評基線(FasterTransformer 與上述 Orca 變體)的 2–4×;這項結果綜合了 PagedAttention kernel、區塊表配置、迭代級排程與寫入時複製(copy-on-write)共用(Kwon et al., SOSP 2023)。
這些數字在歷史上很重要,但不代表今天每個引擎都還浪費 60%–80% 的 KV 記憶體,也不代表單靠分頁就能讓任何工作負載獲得 2–4× 吞吐量。現行推論服務堆疊本來就以分頁或相關的配置方式為起點,再往上疊加 GQA/MLA、前綴快取、量化 KV 與分層儲存。
重算單一 Token 的位元組數
對於標準 MHA 或 GQA 的 KV 張量:
KV_bytes_per_token = 2 × layers × kv_heads × head_dim × bytes_per_elementGQA 範例:
2 × 80 × 8 × 128 × 2 = 327,680 bytes/token = 0.3125 MiB/token若換成其餘規格相同、但擁有 64 個 KV head 的 MHA 模型,則會消耗:
2 × 80 × 64 × 128 × 2 = 2,621,440 bytes/token = 2.5 MiB/token這 8 倍的容量落差來自 KV head 的共用設計,而不是總參數量。MLA、滑動視窗注意力、循環層與混合狀態空間層儲存的是不同的逐 token 或固定大小狀態,必須各自重新推導。
完整範例:825 個 Token 的請求
連續空間預留
allocated slots = 2,048
used slots = 825
unused slots = 2,048 - 825 = 1,223
unused fraction = 1,223 / 2,048 = 59.72%16-Token 區塊的分頁配置
blocks required = ceil(825 / 16) = 52
allocated slots = 52 × 16 = 832
used slots = 825
unused slots = 832 - 825 = 7
internal waste = 7 / 832 = 0.8413%針對這筆 825-token 的單點快照,單筆請求的配置量比值為:
contiguous / paged = 2,048 / 832 = 2.4615×也就是 2.46×,而不是 4–5×。這個比值取決於請求實際填滿 2,048 個預留槽位的比例:實際序列越短,比值越大;序列越接近預留上限,比值越小。
12 GiB KV 預算下的容量計算
這裡的 12 GiB 是假設值:扣除模型權重、活化值與執行期工作空間之後,留給這座未分片邏輯 KV 池的 HBM 預算,並不是服務整個模型所需的總 VRAM。在多 GPU 部署中,應依每個 rank 實際分到的 KV head 與層切分,以及該卡剩餘的 HBM 估算容量,不能直接把整機各卡的記憶體加總。在我們的 GQA 假設下:
12 GiB = 12 × 2^30 = 12,884,901,888 bytes
token slots = floor(12,884,901,888 / 327,680)
= 39,321 slots區塊大小為 16 個 token 時,整座池可切出 floor(39,321 / 16) = 2,457 個完整區塊,對應 39,312 個由區塊承載的 token 槽位;剩下的 3 MiB(約 9.6 個 token 的 KV)不足以再組成一個區塊。若當下每筆進行中的請求都停在 825 個 token:
contiguous requests = floor(39,321 / 2,048) = 19
paged requests = floor(39,312 / 832) = 47
capacity ratio = 47 / 19 = 2.4737×這 47 筆只是所有請求都停在 825 個 token 那一刻的瞬間占用上限,不保證 47 筆請求能一路並行 decode 到完成:輸出一旦長進第 53 個區塊以後,排程器仍需要成長餘裕、保守准入或搶占。就算只看這個快照,第 48 筆請求也需要 48 × 832 = 39,936 個槽位,因此在這些假設下,48 筆以上的同類請求都放不下。
區塊表與跨序列共用
區塊表(block table)把每條序列的邏輯區塊映射到 HBM 中不必連續的實體 KV 區塊。模型看到的仍是一段連續的 token 序列,注意力 kernel 則透過區塊表讀取各實體區塊中的 K/V 向量;當某個實體區塊不再被任何活躍或留存的序列參照時,配置器就會把它歸還給空閒清單(free list)。
搭配參照計數(reference count)與寫入時複製(copy-on-write),多條序列可以指向同一批不可變的前綴區塊;只有當兩條序列在共用的尾端區塊內分歧時,才配置新的實體區塊。原始 vLLM 論文把這套機制用在平行取樣(parallel sampling)、束搜尋(beam search),以及由服務供應商預先定義並快取的共用前綴。分頁允許共用,但跨請求的自動重用,還得在分頁之上加入比對、留存與驅逐(eviction)策略;I-04 會介紹 vLLM 自動前綴快取與 SGLang RadixAttention。
量化 KV、記憶體卸載與分層儲存
量化 KV(Quantized KV)
若將 KV 元素從 2-byte 的 BF16 改存成理想化的 1-byte FP8,邏輯位元組占用會直接減半:
FP8 GQA bytes/token = 2 × 80 × 8 × 128 × 1
= 163,840 bytes
ideal token slots = floor(12 GiB / 163,840)
= 78,643真實的 FP8 KV 快取還要儲存量化縮放因子(scales)、遵守 kernel 的對齊規則,也可能改變準確度。vLLM 文件指出,部分注意力層類型(例如滑動視窗注意力)對 KV 快取量化較敏感,並建議以資料集校準縮放因子,以取得最佳品質(vLLM 量化 KV 快取文件)。HBM 放得下更多序列,並不代表單步延遲會跟著降低。
記憶體卸載(Offload)
把冷區塊或被搶占請求的 KV 區塊移到主機 DRAM 或 NVMe,可以擴充有效快取容量,但每次搬回都要付出 PCIe 或網路傳輸時間,也會占用互連頻寬。因此,命中卸載快取時的 TTFT 表現,與命中 HBM 時並不相同。
共享與分層 KV 快取
Mooncake 這類分離式架構,會把 GPU 叢集中未充分利用的 CPU、DRAM 與 SSD 資源變成分散式 KV 快取,供彼此分開的 prefill 與 decode 叢集使用,讓區塊放置、准入控制與 RDMA 傳輸成為同一個排程問題(Qin et al., 2024)。評估分層快取時,必須逐層追蹤命中率、搬移位元組數、傳輸尾端延遲、驅逐頻率,以及實際省下的 prefill 運算量。
建立容量計算器
將以下程式存為 kv_capacity.py,並執行 python kv_capacity.py > kv-capacity.json:
import json
LAYERS = 80
KV_HEADS = 8
HEAD_DIM = 128
BYTES_PER_ELEMENT = 2
KV_BUDGET_BYTES = 12 * 2**30
USED_TOKENS = 825
MAX_RESERVED_TOKENS = 2048
BLOCK_SIZE = 16
bytes_per_token = 2 * LAYERS * KV_HEADS * HEAD_DIM * BYTES_PER_ELEMENT
token_slots = KV_BUDGET_BYTES // bytes_per_token
if not (0 < USED_TOKENS <= MAX_RESERVED_TOKENS and BLOCK_SIZE > 0):
raise ValueError("invalid sequence or block size")
paged_blocks = (USED_TOKENS + BLOCK_SIZE - 1) // BLOCK_SIZE
pool_blocks = KV_BUDGET_BYTES // (bytes_per_token * BLOCK_SIZE)
paged_slots = paged_blocks * BLOCK_SIZE
contiguous_requests = token_slots // MAX_RESERVED_TOKENS
paged_requests = pool_blocks // paged_blocks
artifact = {
"architecture": {
"layers": LAYERS,
"kv_heads": KV_HEADS,
"head_dim": HEAD_DIM,
"bytes_per_element": BYTES_PER_ELEMENT,
},
"bytes_per_token": bytes_per_token,
"kv_token_slots": token_slots,
"whole_pool_blocks": pool_blocks,
"block_backed_token_slots": pool_blocks * BLOCK_SIZE,
"request": {
"used_tokens": USED_TOKENS,
"contiguous_slots": MAX_RESERVED_TOKENS,
"paged_blocks": paged_blocks,
"paged_slots": paged_slots,
"paged_unused_slots": paged_slots - USED_TOKENS,
"paged_internal_waste_percent": 100 * (paged_slots - USED_TOKENS) / paged_slots,
},
"capacity": {
"contiguous_requests": contiguous_requests,
"paged_requests": paged_requests,
"request_capacity_ratio": paged_requests / contiguous_requests,
},
"sensitivity": {
"mha_64_kv_heads_bytes_per_token": 2 * LAYERS * 64 * HEAD_DIM * 2,
"fp8_gqa_bytes_per_token": 2 * LAYERS * KV_HEADS * HEAD_DIM * 1,
},
}
print(json.dumps(artifact, indent=2))預期核心數值:
bytes_per_token = 327680
kv_token_slots = 39321
paged_blocks = 52
paged_slots = 832
paged_unused_slots = 7
paged_internal_waste_percent ≈ 0.8413
contiguous_requests = 19
paged_requests = 47
request_capacity_ratio ≈ 2.4737刻意破壞的案例
一體適用的 4–5× 宣稱
這筆 825 個 token 的請求對照 2,048 個槽位的預留上限,單筆配置縮減比為 2,048 / 832 = 2.4615×;在明訂預算下,可容納的請求數則是 47 筆對 19 筆(2.4737×)。直接引用固定的 4–5× 倍數,等於拿另一組歷史工作負載的使用率,偷換這次的計算結果。
把 16-Token 區塊當成萬用常數
把區塊大小固定當成 16 會忽略底層 kernel 與硬體的限制。剛好在 825 個 token 時,32-token 區塊同樣會配置 832 個槽位(26 × 32 = 832);但換成 200 個 token 的序列,16-token 區塊只占 208 個槽位,32-token 區塊則會占到 224 個槽位。撰寫容量報告時務必明列區塊大小。
代錯 Head 數量
若誤把 64 個 query head 代入公式,12 GiB 記憶體池算出的容量會從 39,321 個 token 槽位降到 4,915 個。那是 KV 狀態大上 8 倍的 MHA 變體,而不是本篇受測的 8-KV-head GQA 模型。
誤以為記憶體瓶頸「已經徹底解決」
分頁消除了連續配置的浪費與外部碎裂。容量規劃仍須考量 decode 期間的輸出成長、前綴快取驅逐、量化準確度、卸載延遲、分層儲存放置、分散式傳輸,以及持續過載時的准入控制。
動手實作
請擴充計算器,對照以下四種設定:
BF16 GQA,區塊大小 16;
BF16 GQA,區塊大小 32;
理想化 FP8 GQA,區塊大小 16;以及
具備 64 個 KV head 的 BF16 MHA,區塊大小 16。
以長度 [200, 825, 1,525] 的三筆活躍請求為一組,反覆塞進 12 GiB 的 KV 預算,直到剩餘空間放不下完整的一組為止。請輸出:
每組三筆請求合計配置的槽位數;
每組實際有效槽位數(
200 + 825 + 1,525 = 2,550);每組的尾端區塊浪費槽位數;
12 GiB 預算總共能放進幾組完整的三請求組合;以及
將最長那筆 1,525-token 請求卸載時,「有效狀態位元組數」與「完整已配置區塊位元組數」各是多少(實際傳輸量還取決於傳輸布局、中繼資料、縮放因子與填補)。
請把 FP8 的品質影響與卸載傳輸延遲列為獨立的實測驗收條件,不要從位元組數推算。
在不跨請求共用前綴、各序列獨立配置尾端區塊,且只接納完整三請求組(每組有效槽位 200 + 825 + 1,525 = 2,550)的前提下,參考答案如下:
案例 | 每組配置槽位 | 每組尾端浪費 | 完整組數 | 最長請求完整區塊位元組 | 最長請求有效位元組 |
|---|---|---|---|---|---|
BF16 GQA,區塊 16 | 2576 | 26 | 15 | 503316480 | 499712000 |
BF16 GQA,區塊 32 | 2592 | 42 | 15 | 503316480 | 499712000 |
理想 FP8 GQA,區塊 16 | 2576 | 26 | 30 | 251658240 | 249856000 |
BF16 MHA,區塊 16 | 2576 | 26 | 1 | 4026531840 | 3997696000 |
把「每組配置槽位」乘上「完整組數」,即可得到完整組合占用的總槽位數。剩餘區塊或許還放得下下一組的一部分,但本練習刻意不接納不完整的組合。這些只是靜態配置計算,不是吞吐量結果;吞吐量須以執行期壓測驗證。
可量測的完成標準
本篇 KV 快取實作須滿足以下所有條件才算通過:
模型架構、KV head 數、資料型別、HBM 預算、區塊大小與序列長度皆明確列出;
BF16 GQA 的單 token KV 大小等於 327,680 bytes;
在簡化公式下,12 GiB 可提供 39,321 個完整 token 槽位(區塊大小 16 時對應 39,312 個區塊槽位);
825-token 請求配置 52 個區塊(832 個槽位),僅剩 7 個未用槽位,內部浪費比例為 0.8413%;
在指定基準條件下,連續預留與分頁配置的靜態容量分別為 19 筆與 47 筆請求;
歷史上的 60%–80% 浪費率與 2–4× 吞吐量數字,都限定在 2023 年的比較基線內,沒有當成現今引擎的通用結果;
清楚區分底層區塊分頁與上層前綴比對/留存機制;
在實測縮放因子開銷、kernel 速度與任務準確度之前,將 FP8 容量明確標示為理想化估算;以及
評估卸載與分層儲存時,有納入傳輸延遲、頻寬與各儲存層命中率指標。
提取練習
預留浪費(reservation waste)與內部碎裂(internal fragmentation)差在哪裡?
區塊表(block table)如何省去「每筆請求都得占據一塊連續記憶體」的限制?
為什麼本篇範例算出的請求容量提升約為 2.47×,而不是 4–5×?
光靠 PagedAttention 本身,會自動決定要把哪些已完成的提示留下來給未來的請求重用嗎?
為什麼將 KV 量化成 FP8 能讓槽位容量翻倍,卻未必能壓低單步延遲?
一旦把 KV 區塊從 GPU HBM 卸載到主機 DRAM 或 SSD,會多出什麼新的效能瓶頸?
解答
預留浪費是在序列還沒長到最大長度之前,就先占住直到上限的未來位置;內部碎裂則是配置單位內部沒填滿的零頭空間,例如 825 個 token 分了 52 個 16-token 區塊後,最後一塊剩下的 7 個空槽位。
區塊表將邏輯上連續的區塊編號映射到記憶體池中任意位置的等大實體區塊,序列變長時每次只需領取 1 個空閒實體區塊,完全不必搬移既有資料。
本篇基準明確設定實際長度 825 個 token(分頁占 832 個槽位)、連續預留 2,048 個槽位,且預算為 39,321 個 token 槽位(可切出 2,457 個完整區塊),算出來自然是 47 筆對 19 筆(
47 / 19 ≈ 2.4737×)。不會。分頁只提供非連續的區塊儲存與參照計數共用;跨請求的自動重用,還需要前綴雜湊或基數樹(radix tree)比對、留存與驅逐規則,以及租戶隔離。
每個元素的位元組數減半,大約能放進兩倍的 token 槽位;但量化/反量化運算、縮放因子處理,以及多出的容量所接納的更大批次,都可能抵銷關鍵路徑上的頻寬節省。準確度校準與硬體、kernel 支援也有影響。
GPU HBM 與下層儲存之間經 PCIe 或 RDMA 的傳輸延遲與頻寬上限,加上與其他 GPU 傳輸的爭用,以及「抓回冷區塊」與「重算 prefill」之間的取捨。
本文不涵蓋的內容
I-04 會探討單機前綴留存與分散式快取感知路由;I-03 分析如何在分離的 prefill 與 decode 資源池之間搬移 KV 區塊;I-07 則在貼近真實的提示與輸出長度分布下,壓測端對端容量與延遲 SLO。
來源與延伸閱讀
Kwon, W., Li, Z., Zhuang, S., et al. "Efficient Memory Management for Large Language Model Serving with PagedAttention." SOSP, 2023。KV 記憶體使用率剖析、PagedAttention 區塊表、寫入時複製共用與 vLLM 系統評估的第一手論文。arxiv.org/abs/2309.06180
vLLM Project. "Quantized KV Cache." 現行文件,查閱日期 2026-11-02。FP8 KV 格式、縮放因子校準與執行期限制的官方說明。docs.vllm.ai
Qin, R., Li, Z., He, W., et al. "Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving." arXiv:2407.00079, 2024。會議版本以 "Mooncake: Trading More Storage for Less Computation" 為題發表於 USENIX FAST 2025。以跨節點 DRAM/SSD 構成分散式 KV 分層、服務分離式 prefill 與 decode 叢集的第一手論文。arxiv.org/abs/2407.00079
Hugging Face. "Cache Strategies." Transformers 現行文件,查閱日期 2026-11-02。動態、靜態、卸載與量化 KV 快取架構的官方概覽。huggingface.co
閱讀順序
完整閱讀路線
章節 20 / 25
Huang Tzu Lin
富宇資訊科技創辦人。撰寫 AI 系統設計、證據與工程取捨的文章。
