LLM 推理的 KV Cache 分层卸载工程 2026:带宽预算与准入控制
在 LLM 推理的连续批处理框架下,GPU 显存容量逐渐成为并发上限。本文聚焦 KV Cache 的三层卸载策略:HBM 作为 L0 热点缓存、CPU DRAM 作为 L1 温缓存、NVMe SSD 作为 L2 冷缓存,通过热度感知准入控制、异步 DMA 传输与 WAL 持久化,将并发上限提升 3-5 倍,同时将 P99 延迟控制在 SLO 允许范围内。
约 14 分钟阅读3,927 字28 次阅读博主

在 LLM 推理的连续批处理框架下,GPU 显存容量逐渐成为并发上限。本文聚焦 KV Cache 的三层卸载策略:HBM 作为 L0 热点缓存、CPU DRAM 作为 L1 温缓存、NVMe SSD 作为 L2 冷缓存,通过热度感知准入控制、异步 DMA 传输与 WAL 持久化,将并发上限提升 3-5 倍,同时将 P99 延迟控制在 SLO 允许范围内。

2024 年下半年起,主流 LLM 推理引擎在连续批处理(Continuous Batching)框架下,GPU 利用率普遍突破了 70%。然而真正制约在线服务吞吐量的,不再是计算密度,而是 KV Cache 的显存容量上限。
以 LLaMA-3-70B 为例,每个 token 的 KV 状态约为 80KB(bfloat16 下 Key 矩阵 加 Value 矩阵 ,8192 上下文长度对应约 640MB 的 KV tensor)。在 80GB HBM 的 A100/H100 上,即便 KV Cache 完全静态,70B 模型仅能容纳约 100-130 个并发请求的完整上下文——这一数字在生产峰值流量下远低于所需的并发上限。
当并发请求数超出 HBM 容量时,经典解法是重计算(re-computation):直接丢弃显存中的 KV Cache,下次需要时从第零个 token 重新前向传播。然而,重计算的代价与上下文长度成正比——在 32K 超长上下文场景下,重算一次可能带来 5-10 倍的延迟放大,用户体验急剧下降。
分层卸载(Multi-tier Offloading)提供了第三条路:将热度低的 KV Cache 页异步写出到更慢但更廉价的存储层(CPU DRAM → NVMe SSD),在需要时再迁回显存。其核心工程挑战是:卸载和回迁的延迟必须在 SLO 允许范围内,且不能因频繁换页导致 GPU 空转。
本文聚焦这一问题的完整工程链路:如何建模缓存价值、如何设计准入控制策略、如何实现异步传输、以及如何在生产环境落地并保障 SLO。
分层缓存调度的本质是有限资源下的在线决策。建模如下:
设请求 在第 个时间窗口的上下文长度为 ,其 KV Cache 当前位于层级 ,其中 表示 GPU HBM、 表示 CPU DRAM、 表示 NVMe SSD。记将 从层级 迁移至 的时间为 ,其与带宽 、页粒度 和传输协议开销 相关:
T_{h \to h'}(l) = \frac{l \cdot \text{KV\_size\_per\_token}}{B_{h\to h'}} + \omega_{h\to h'} \cdot \left\lceil \frac{l}{P} \right\rceil \tag{1}
其中 是每页的 DMA 提交开销,NVMe 的 通常为 10-30μs(取决于驱动和队列深度),而 SATA SSD 可达 500μs 以上。
缓存价值函数:对请求 ,设其下一次被访问的概率为 (基于访问模式的历史统计),重算长度为 ,则缓存保留价值为:
V_i = p_i(t) \cdot T_{\text{recompute}}(l_i^{rc}) - T_{h_i \to 0}(l_i) \tag{2}
当 时,应触发降级(offload)决策。决策约束是:任意请求的回迁延迟 不得超过该请求的 P99 延迟 SLO,即:
\forall i: \quad T_{h_i \to 0}(l_i) + T_{\text{forward}}(l_i) \leq \text{SLO}_{p99,i} \tag{3}
这一约束将卸载策略的空间大幅缩小:任何候选降级方案必须满足式 (3),否则宁可重算。
分层卸载的数据面设计需解决三个核心问题:以什么粒度管理缓存页、元数据放在哪里、如何保证跨层一致性。
PagedAttention(vLLM)已在 GPU HBM 层证明了 4KB 或 64KB 页粒度的有效性。跨层卸载时,粒度过小(4KB)会产生过高的 NVMe I/O 次数;粒度过大(1MB)则降低灵活性、增大单次迁移的数据量。经验推荐值为 256KB–1MB,对应 32-128 个 token 的 KV Cache(以 LLaMA-3-70B 为例)。
# KV Cache 页描述符(伪代码)
class KVPage:
def __init__(self, request_id: str, start_token: int, num_tokens: int,
kv_tensor: Optional[bytes], storage_ref: Optional[str]):
self.request_id = request_id
self.start_token = start_token # 页起始 token 偏移
self.num_tokens = num_tokens # 页内 token 数
self.kv_tensor = kv_tensor # GPU HBM 指针(h=0 时非空)
self.storage_ref = storage_ref # NVMe 文件路径(h≥1 时非空)
self.pin_count = 0 # 引用计数
self.access_count = 0 # 近期访问次数(LRU 排序用)
全局页表(Page Table)存放于 CPU DRAM 的共享内存中,大小为 ( 为总页数),以 LLaMA-3-70B 1M 并发 token 上限为例,1MB 页粒度对应约 10^6 个页条目,每个条目约 128 字节,总计约 128MB——完全可放入 CPU DRAM。元数据必须持久化到 NVMe(或 SSD)以实现节点重启后的状态恢复,恢复时间通常在 5-30 秒。
图表加载中…
GPU HBM 与 CPU DRAM 之间通过 CUDA Host Pinned Memory 实现零拷贝 DMA;CPU DRAM 与 NVMe 之间通过 Linux io_uring 异步提交队列实现低延迟写入。
分层卸载的调度分为准入控制(which pages to accept for caching)和页面替换(which pages to evict when space runs out)两层。
简单 LRU 无法区分"大上下文但低复用概率"和"小上下文但高复用概率"的请求。引入价值密度(Value Density)指标:
\text{VD}_i = \frac{V_i}{\text{KV\_size}(l_i)} \tag{4}
仅当 时才将新请求的 KV Cache 纳入 GPU HBM 缓存层。阈值 通过历史流量数据离线校准,并在运行时根据显存压力动态调整(压力高时收紧 )。
页面替换采用分层 LRU 队列(Tiered LRU):
┌─────────────────────────────────────────────────────┐
│ Layer 0 (GPU HBM) │ Layer 1 (CPU DRAM) │
│ LRU 队列: [P1→P2→P3→...→Pn] │
│ 当 L0 满: 将 Pn 降级到 L1 (而非直接丢弃) │
│ L1 队列: [P1→P2→...→Pm] │
│ 当 L1 满: 将 Pm 降级到 L2 (NVMe) │
│ L2 仅保留元数据指针,实际数据在 NVMe blob 中 │
└─────────────────────────────────────────────────────┘
迟滞机制(Hysteresis)防止页面在 L0/L1 边界频繁震荡:当某页刚被降级到 L1 后,设置冷却期 ,期间不允许立即升回 L0。
卸载传输的实现质量决定了页面降级/升回时对在线请求的干扰程度。核心原则是:所有跨层传输必须异步化,GPU 不得在传输路径上阻塞等待。
当 L0 需要驱逐某页时,将该页的 KV tensor 通过 pinned memory buffer 复制到 CPU 后,立即释放 GPU 显存。CPU 侧的后台线程负责将数据写入 NVMe,写入期间 GPU 完全可用。
# 伪代码:异步写回流程
async def offload_page(page: KVPage, nvme_writer: NVMewriter):
# Step 1: 复制到 pinned memory(异步,GPU 不阻塞)
pinned_buf = cuda_to_pinned_async(page.kv_tensor, page.num_tokens)
# Step 2: 立即更新页表并释放 GPU 显存
page_table.update(page.id, location="cpu_dram", pinned_ref=pinned_buf)
cuda_free(page.kv_tensor)
# Step 3: 后台线程将 pinned_buf 写入 NVMe
await nvme_writer.write(pinned_buf, page.storage_ref)
page_table.update(page.id, location="nvme")
基于请求的 prompt pattern(system prompt 共享、few-shot 示例等),可在 KV Cache 创建时预测性预取到 L1 或 L2:
当 NVMe 写入队列深度(Queue Depth)接近饱和时,系统应主动收紧 并触发紧急降级(Emergency Eviction):将 L1 中最近未访问的页面批量写出到 NVMe,以释放 CPU DRAM 缓冲空间,防止内存溢出(OOM)。
KV Cache 页表需要与 NVMe 数据保持一致性。采用 Write-Ahead Logging(WAL) 协议:
page_table.wal 文件(append-only),page_table.ckpt 文件,崩溃恢复时间(Recovery Time Objective,RTO)约为 10-60 秒,具体取决于 WAL 大小和 checkpoint 频率。
一个 KV Cache 页可能被多个并发请求共享(如共享 system prompt)。引用计数(Reference Count)必须精确管理,避免提前回收导致数据损坏。
class KVPageTable:
def pin(self, page_id: str) -> bool:
"""pin 增加引用计数,返回是否成功(已被标记删除则失败)"""
page = self.pages[page_id]
if page.marked_delete:
return False
page.pin_count += 1
return True
def unpin(self, page_id: str):
"""unpin 减少引用计数,计数为 0 且已标记删除时触发物理回收"""
page = self.pages[page_id]
page.pin_count -= 1
if page.pin_count == 0 and page.marked_delete:
self._async_gc(page)
NVMe blob 文件不支持原地覆写。采用打标记 + 延迟回收策略:删除操作仅设置 deleted=true 标志,下次 GC 线程扫描时批量清理。GC 期间如遇节点重启,未清理的 blob 不会影响正确性(页表已不引用它们)。
GPU HBM 容量分配模型:
| 分区 | 分配比例 | 用途 |
|---|---|---|
| 模型权重 | ~60% | 等静态参数 |
| 激活值 | ~15% | 单批次前向传播的临时 tensor |
| KV Cache HBM | ~20% | 热点请求的 L0 缓存 |
| 元数据/临时 | ~5% | 页表、调度队列等 |
当 L0 压力超过 90% 利用率时,触发预警并自动收紧 ;超过 95% 时强制降级全部未 pin 页面。
# 生产可观测性指标(OpenTelemetry + Prometheus)
KV_CACHE_HIT_RATE_L0 = Counter(
"kv_cache_hit_total", "KV Cache hits by layer",
["layer"] # layer in {0,1,2,miss}
)
OFFLOAD_LATENCY_P99 = Histogram(
"kv_offload_latency_seconds", "Page offload latency",
["src_layer", "dst_layer"],
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.5]
)
EVICTION_STORM = Alert(
"kv_eviction_rate > 1000 pages/min",
severity="warning",
description="Eviction rate indicates memory pressure"
)
分层卸载策略的改动采用 feature flag + 流量镜像 验证:先以 1% 流量运行新策略,与旧策略对比 P99 延迟和 Cache Hit Rate,确认无回退后再逐步放大。每 5% 增量后检查一次告警,出现指标劣化立即回滚至上一版本。
分层卸载不是孤立的技术,与当前主流的 LLM 推理优化手段存在复杂的互补与竞争关系。
与重计算的关系:分层卸载可视为"更精细的重计算"——重计算是对整段 KV Cache 全部丢弃,分层卸载则按页粒度选择性保留。极端内存压力下(NVMe 也满),分层卸载退化为重计算,两者在策略空间上是包含关系。
与 Prefix Cache 的关系:共享前缀(LLaMA-3 system prompt、chat template)的 KV Cache 具有极高的复用价值,应强制保留在 L0,不受 约束。分层卸载策略应内置"frozen prefix"白名单。
与 PD 分离的关系:Prefill-Decode 分离架构下,Prefill 节点生成 KV Cache 后通过 RoCE/InfiniBand 发送给 Decode 节点。此时 KV Cache 的跨节点传输替代了 HBM→NVMe 的同节点卸载,调度域从单机变为集群。
当前局限:分层卸载策略依赖访问模式的统计规律,对高度随机、无重复 prompt 的场景(如一次性长推理任务)效果有限。此外,NVMe 的写入寿命(DWPD,Drive Writes Per Day)需要在容量规划中纳入考量——高吞吐推理服务每天可能产生数十 TB 的 KV Cache 写入。
SRE 可落地项(生产检查清单):
kv_cache_hit_total(按 layer 分维)+ kv_offload_latency_seconds(P99)+ kv_page_table_size_bytes 三张图。SLO 告警阈值建议:L0 Hit Rate < 60% 触发 warning,L0 Hit Rate < 40% 触发 critical。smartctl -a /dev/nvme0n1 监控 Data Units Written,超出厂商 DWPD 规格 80% 时计划更换。研究者待验证假设:
一句话摘要:在 LLM 推理的连续批处理框架下,KV Cache 分层卸载通过热度感知准入、异步 DMA 传输与 WAL 持久化,将 NVMe SSD 纳入缓存层级,可在显存容量不变的条件下将并发上限提升 3-5 倍,同时将 P99 延迟控制在 SLO 允许范围内。
Conversation
0 条