LabHub
开始
学习 学习路径 课程

LLM 服务

KV 缓存装不进 GPU 时:循环访问、LRU 与下放到 CPU

在 LabHub 中继续学习

一句话总结

如果交替读取的文档多于 GPU 上 KV 缓存的容量,LRU 会先淘汰最久未使用的条目,结果按同样顺序重读时命中数为 0。把被挤出的 KV 下放到 CPU 内存(LMCache 的思路),就能不重新计算而直接加载回来;在这次测量中,到首个令牌(token)的时间从 576ms 降到了 48.6ms(约 11.9 倍)。代价是首次读取时慢了约 4%。

为什么需要它

vLLM 会把开头部分(前缀)相同的请求的 KV 缓存保留在 GPU 上并复用(前缀缓存,prefix caching)。如果请求是在前面附上一篇长文档、只更换问题,就可以跳过文档部分的计算。问题在于 GPU 内存很小。这次测量加载 1.5B 模型之后,剩下的 KV 缓存容量只有 21,520 个令牌(服务器日志中的 GPU KV cache size),而一篇文档就有 5,011 个令牌,所以只能放下四篇多一点。如果有十篇文档,总计 50,110 个令牌,约为容量的 2.3 倍。

容量不足时就必须淘汰某些内容,这个缓存先淘汰最久未使用的条目(LRU)。这条规则假设刚用过的内容很快还会再用,而循环访问——从 0 号文档读到 9 号文档,再按同样顺序从 0 号重新读起——恰好让这个假设完全相反。第一遍结束时,留下的是最近读过的 6–9 号文档。第二遍读到 0 号文档时它已经不在了,于是重新计算并放入,这会把最旧的 6 号文档挤出去。放入 1 号文档,又会挤出 7 号文档。重读的顺序与被淘汰的顺序相同,所以最旧的条目恰恰是最先需要再次使用的条目。这就是容量只差一点点,命中数也会变成 0 的原因。实测中,vLLM 的 prefix_cache_hits 在第二遍也是 0。

工作原理

LMCache 会把被挤出的 KV 下放到 CPU 内存(也可以是磁盘或 Redis),当相同的前缀再次出现时,不再重新计算,而是把它加载回来。本实验的模拟器把这一流程简化为三条规则。

  1. 在 GPU 上命中就到此为止,不碰 CPU。
  2. 在 GPU 上未命中但 CPU 里有,就不重新计算,而是从 CPU 加载到 GPU。
  3. 两边都没有,就重新计算,并同时存入 GPU 和 CPU。

第 1 条规则有测量日志支持。在倒序读取的第三遍,vLLM 在 GPU 上解决的前缀是 21,488 个令牌,向 LMCache 查询的令牌是 28,755 个,正是总数 50,243 减去 GPU 命中部分的结果。第 3 条规则是对实际行为的简化。报告把首次读取慢了约 4% 解读为把 KV 下放到 CPU 的开销,并写道十篇文档(5 万个令牌)的 KV 约为 1.3GB,因此设定的 1.5GB 上限刚好装得下。CPU 一侧的容量同样有上限,读取的文档超过上限时,CPU 里也会从最旧的条目开始丢弃。

实测结果

把 10 篇文档分三遍读取:首次读取、按同样顺序重读、倒序读取。请求逐个发送,测量到首个令牌的时间(TTFT,单位 ms)。数值为 p50 和平均值。

配置 遍次 p50 平均值
仅 vLLM 首次读取 575.5 581.2
仅 vLLM 按同样顺序重读 576.2 575.9
仅 vLLM 倒序读取 505.4 343.6
LMCache 首次读取 597.8 605.4
LMCache 按同样顺序重读 48.6 49.6
LMCache 倒序读取 47.2 40.9

按同样顺序重读时,p50 从 576ms 降到 48.6ms,约快 11.9 倍,平均值约快 11.6 倍。读完一遍所需的时间从 6.2 秒降到 0.9 秒。首次读取时,p50 从 575.5ms 增加到 597.8ms,增加了约 4%。仅用 vLLM 的那一组,第二遍与第一遍几乎相同(576 对 575),这意味着缓存完全没有起到作用。

LMCache 日志显示,第二遍中的一个请求在约 5,000 个令牌里,从 CPU 加载了 4,864 个令牌(256 的倍数,共 19 个),只重新计算了剩下的约 150 个令牌。加载耗时 8–9ms,而重新计算同样的量大约需要 560ms。加载比计算便宜 60 倍以上,这就是这一差距的全部原因。

倒序读取的那一遍,模拟器也能预测准确。只用 vLLM 时,刚读过的四篇文档(9、8、7、6 号,编号是测量文件中的 doc 值)在 GPU 上命中,耗时约 30ms,其余的约为 576ms。开启 LMCache 后,同样的四篇文档约为 30ms,其余六篇从 CPU 加载,约为 46–49ms。不过,仅用 vLLM 那一组中的 5 号文档为 438ms,介于命中与未命中之间。把文档整篇放入、整篇移出的模拟器画不出这种部分命中。

这些数字能信到什么程度

在现场相遇的样子

这次测量没有改动 LabHub 生产环境的 llm 部署。要应用的话,需要修改镜像、服务器参数和环境变量,那是另一项工作。评估时,报告指出的条件是:文档的令牌总数必须大于服务器日志中的 GPU KV cache size,差距才会显现。同样,如果同一篇文档不会被再次读取,就没有可以加载的内容。CPU 一级的容量要先从每个令牌的 KV 大小算起来确定。这次测量中的 1.5GB 之所以管用,是因为它装下了十篇文档的量(约 1.3GB)。

下一项实验要做什么

不用 GPU,用纯 Python 亲自验证这一流程。实现一个 LRU 缓存,容量设为 21,520 个令牌,把 10 篇文档按同样顺序读两遍,观察命中数为 0。倒序读取时,看哪些文档会命中,并找出容量要增加到多少才会出现命中。然后实现 GPU 和 CPU 两级缓存,确认第二遍全部命中,并预测 CPU 容量不足时会怎样。最后读取上表的原始数据——逐个请求的测量文件,亲自计算 p50、平均值和倍率。