状态:草案(2026-10-27),已实际测量卸载,未实际测量分离式服务。 使用单张 8GB 级笔记本 GPU(Ada 架构)、vLLM 0.31.0、Qwen3-Reranker-0.6B 于 2026-10-27 进行测量。关于使用 LMCache 和 NVIDIA Dynamo 的卸载及分离式服务的原理与实测,详见《LLM KV 缓存剖析》第 3、4 篇,本文专注于 vLLM 内置的 CPU 卸载。 编写规则:以实测数据为基础,无法测量的标注为“未实测”,未能确认的标注为“未确认”。
目录
1. GPU 内的前缀缓存很快就会被挤出
我们在第 2 篇中测量过,如果重复使用 3,200 个 token 的前缀,首个 token 的生成时间会从 174.7ms 缩短至 18.9ms。然而,这些块只有在留在 GPU KV 的“空闲位置”时才能被重复使用。当新请求索要空间时,vLLM 会从所有未使用的块中最久未使用的开始清除。对于交替查询多篇长文档的服务来说,几个 GB 的 GPU 空间很快就会耗尽。
KV 卸载是指在清除块之前(准确地说是当块被填满时),将这些块复制到更大、更慢的位置(即 CPU 内存)中的功能。当相同的前缀再次到来时,如果在 GPU 中找不到对应的块,就会从 CPU 中查找并恢复。LMCache 篇中讨论了盈亏平衡点:只有在传输比重新计算更快时才划算。
2. vLLM 的默认卸载
vLLM 将 KV 与引擎外部进行交互的工作统一打包在名为 KV Connector的接口中。同一个接口可以挂载多种实现。
| 连接器 | 功能 |
|---|---|
OffloadingConnector |
vLLM 默认的 CPU 卸载。在 0.11.0 中随 LRU 管理一同引入,并在 0.19.0 中整理为通用的 CPU 卸载 |
LMCacheConnectorV1, LMCacheMPConnector |
通过 LMCache 支持 CPU、磁盘和远程存储 |
NixlConnector, MooncakeConnector |
将 KV 发送到其他引擎(其他 GPU、其他服务器)。用于分离式服务 |
MultiConnector |
按顺序串联多个连接器 |
只需两行配置即可开启默认卸载。 --kv-offloading-size 是以 GiB 为单位的 CPU 缓冲区大小,提供该值后就会启用 native 后端(即 OffloadingConnector)。
vllm serve <모델> --kv-offloading-size 0.5 --kv-offloading-backend native
详细配置通过 --kv-transfer-config 的 kv_connector_extra_config 来指定。根据 0.32.0 版本的文档,有仅使用单个 CPU 的 CPUOffloadingSpec(默认),以及在 CPU 下方放置类似文件系统的第二层缓存的 TieringOffloadingSpec ,清理顺序则选择 lru 或 arc 。默认情况下,只下放提示词(Prefill)块,不下放解码块(offload_prompt_only)。文档指出,GPU 与 CPU 之间的复制是通过 cudaMemcpyAsync 与计算重叠进行的。
3. 亲自测量
方法
我们人为制造了被挤出的场景。
- 将 GPU KV 限制在
--kv-cache-memory-bytes 268435456(256MiB),使得只能放入 2,336 个 token。--max-model-len 2048. - 一组包含五个请求。首次发送前缀 A(约 1,600 个 token,冷状态),紧接着再发一次(GPU 命中),然后发送其他前缀 B 和 C 将 A 挤出,最后再次发送 A。
- 我们在无卸载服务器和
--kv-offloading-size 0.5(约 4,600 个 token 的容量)服务器上分别运行了五组测试。第一组的第一个请求由于包含服务器预热,故单独观察。 - 其余环境与第 2 篇相同(8GB 级笔记本 GPU、vLLM 0.31.0、Qwen3-Reranker-0.6B、容器内存限制 4GiB)。
结果
| 阶段 | 无卸载(TTFT 中位数,第 2~5 组) | 卸载 0.5GiB(相同标准) |
|---|---|---|
| 首次发送 A(冷状态) | 76.8ms | 77.7ms |
| 立即再次发送 A(GPU 命中) | 14.0ms | 14.3ms |
| 被 B 和 C 挤出 | 77.2ms | 78.1ms |
| 再次发送 A(被挤出后) | 77.3ms(重新计算) | 35.1ms(从 CPU 加载) |
观察全部五组数据,开启卸载服务器上的“再次发送 A”分别为 35.3、35.4、34.8、33.3、35.6ms,均在 34ms 左右;而无卸载服务器为 76.3~78.0ms,与冷请求相同。两台服务器的首个请求均为 104ms,其中包含了近 30ms 的预热开销。
我们还通过服务器指标确认了具体迁移了什么内容。
指标(/metrics) |
值 | 含义 |
|---|---|---|
vllm:kv_offload_load_bytes_total |
849.6MB(5次) | 从 CPU→GPU 恢复的数据量。单次约 170MB = 约 1,480 个 token × 114,688 字节 |
vllm:kv_offload_load_time_total |
0.0673秒 | 五次恢复所花费的时间。约每秒 12.6GB,单次 13.5ms |
vllm:kv_offload_store_bytes_total |
2.82GB(25次) | 从 GPU→CPU 下放的数据量。所有提示词块均会被下放 |
vllm:kv_offload_store_time_total |
0.221秒 | 约每秒 12.8GB |
vllm:prompt_tokens_by_source_total |
计算 24,771 / GPU 命中 7,936 / 外部 7,408 | “外部”指从 CPU 加载的 token |
解读如下:
- 被挤出的前缀的首个 token 生成速度快了 2.2 倍(77.3ms → 35.1ms)。 但它仍然比留在 GPU 中(14.0ms)慢 21ms。这其中的大部分差距来自于传输 170MB 所需的 13.5ms。
- 在该 GPU 上传输 KV 的速度约为每秒 12.6GB。 重新计算相同的 1,480 个 token 大约需要 63ms(从 77.3ms 中减去 GPU 命中的 14.0ms)。计算速度约为每秒 23,000 个 token,而传输速度相当于每秒 110,000 个 token,因此在这种组合下,传输方式快了近五倍。
- 下放操作并非免费的。 甚至连未被重复使用的块在内,总共下放了 2.82GB。由于复制是异步的,因此在本次测量的冷请求 TTFT 中几乎看不出差异(76.8 对 77.7ms),但它持续占用了 PCIe 带宽和 CPU 内存。
- CPU 缓冲区是固定内存(pinned memory)。 分配 0.5GiB 就会导致相应数量的页面被固定,并直接计入容器内存限制中。本次测试由于必须在 4GiB 的限制内进行,因此把缓冲区设得比较小。
4. 什么时候没有收益
- 模型越小、GPU 计算越快,收益就会越少。 这是因为重新计算的时间接近于传输时间。相反,模型越大,每个 token 的计算量就越多,而由于 GQA·MLA 的存在,每个 token 的 KV 字节数增长相对较少。在《LLM KV 缓存剖析》第 3 篇中,使用相同 GPU 测得的 LMCache 实测数据(576ms → 48.6ms)收益比这次更大,也是因为输入更长。
- 如果没有复用,就只有成本。 由于所有提示词块都会被下放,在没有前缀重复使用的负载下,只会白白消耗 PCIe 带宽和固定内存。
store_threshold(可以通过设置仅下放被查询过几次的块)来减少这种消耗(未实测)。 - 如果 GPU 空间充足,则无需使用。 第 2 篇中的默认服务器拥有 40,208 个 KV token,因此在相同的测试中 A 不会被挤出。
5. 分离式服务:通过相同的连接器将 KV 发送到其他引擎
将 Prefill 和 Decode 放在不同 GPU 上的分离式服务(disaggregated prefill)运行在上述连接器结构之上。Prefill 引擎生成 KV 并通过 NixlConnector 发送给 Decode 引擎,Decode 引擎则利用收到的 KV 继续生成。vLLM 文档列出了两个原因:
- 为了分别调优 TTFT 和 ITL(可以为 Prefill 和 Decode 分配不同的并行度)
- 为了控制长尾 ITL。我们在第 2 篇中看到的“长输入暂停解码的时间”虽然可以通过块大小(chunk size)来缓解,但文档指出很难选择合适的块大小。
同一篇文档明确指出“分离式服务并不能提高吞吐量”,并将该功能整体标记为实验性质。最初的论文 DistServe (OSDI 2024) 以同时满足 TTFT 和 TPOT 目标的每个 GPU 最大请求率为指标,报告称其请求处理能力达到了传统系统的 7.4 倍或 12.6 倍严苛目标(基于摘要)。在 0.32.0 版本中,还加入了在分离式服务中让 Decode 端复用 Prefill 端生成的 token ID 的功能(kv_transfer_params 的 prompt_token_ids)。
由于只有一张 GPU,本次 vLLM 独立分离式服务同样未实际测量。使用单张同系列 GPU 和通过 1GbE 连接的两台机器运行 NVIDIA Dynamo 进行分离式服务的结果(单张卡慢 5 倍以上,两台机器之间慢 20 多倍)可在 Dynamo 篇和《Toss ML/AI Infra 准备工作》第 24 篇中查阅。KV 传输速度只有像 NVLink/RDMA 那样快才有意义的结论,与本次卸载实测(通过 PCIe 达到每秒 12.6GB)的方向是一致的。
6. 运维建议
- 对于交替查询多篇长服务的应用(RAG、代码库问答、长对话),CPU 卸载只需一行选项就能将挤出前缀的 TTFT 缩短一半以上。当 GPU KV 使用率(
vllm:kv_cache_usage_perc)持续很高且前缀命中率低于预期时,非常值得首先尝试开启。 - 开启后,请通过
vllm:prompt_tokens_by_source_total{source="external_kv_transfer"}和vllm:kv_offload_load_time_total确认是否真的在加载以及花费了多长时间。 - CPU 缓冲区大小需要叠加计入容器内存限制中。固定内存不会被换出(swapped out),一旦超出限制,引擎就会因 OOM 而崩溃。
7. 未能确认的内容
TieringOffloadingSpec的文件系统层、arc策略以及store_threshold均未实际测量。- 在高并发下,卸载复制对 Decode 吞吐量的影响未实际测量。本次测试是逐个发送请求的。
- vLLM 独立分离式服务(
NixlConnector)因只有一张 GPU 而未实际测量。
参考
- vLLM 文档,KV Offloading Usage Guide, Disaggregated Prefilling(0.32.0): https://docs.vllm.ai/en/latest/features/kv_offloading_usage.html, https://docs.vllm.ai/en/latest/features/disagg_prefill.html
- vLLM 0.11.0, 0.19.0 发布说明(CPU 卸载):https://github.com/vllm-project/vllm/releases
- Zhong 等,“DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving”,OSDI 2024. https://arxiv.org/abs/2401.09670
- 上一篇:LMCache: 将 KV 缓存移出 GPU,NVIDIA Dynamo