推理服务的缓存与调度:前缀、多级存储、集群路由
长上下文服务里一次前缀缓存 miss 比 hit 贵几个数量级,所以整条路径都围着缓存转:块哈希的前缀缓存怎么复用,KV 在 GPU、DRAM、SSD 之间按 write-through 还是 write-back 搬,集群层面怎么让请求跟着缓存走、又不被长请求的突发拖垮。
推理服务的两个阶段里,prefill 的成本和输入长度成正比,decode 的成本和输出长度成正比。长上下文场景下输入远长于输出,一次典型的编码请求可能带着几十万 token 的历史、只新增几千 token。这时候整个服务的效率取决于一件事:这几十万 token 的 KV cache 能不能复用。这一篇讲复用的三个层次:单实例内的前缀缓存、跨存储层次的 KV 池、集群层面的路由和准入。
前缀缓存:按块哈希
两个请求如果开头的 token 完全相同,它们的 KV cache 在这一段上也完全相同(对因果注意力,一个位置的 KV 只依赖它前面的 token)。前缀缓存就是把这一段存下来给后来者用。
主流实现按块做:KV cache 本来就是按固定大小的物理块(比如 16 到 64 个 token)分页管理的,每个块算一个哈希,哈希值覆盖这个块和它前面所有块的 token,所以命中一个块的哈希就等于证明了到它为止的整个前缀一致。新请求来时逐块查哈希,最长的连续命中就是可以跳过的前缀,从命中点之后开始 prefill。vLLM 的自动前缀缓存和 SGLang 的 RadixAttention 都是这个思路,后者把哈希表换成了 token 树。
粒度是关键参数。只有完整的块才能哈希,所以块越大,能复用的前缀越短(尾巴上不满一块的部分永远浪费),块越小,元数据越多。更麻烦的是混合架构:如果模型里有些层的 cache 不是按 token 分页的(比如线性注意力的固定大小状态),它们只能在稀疏的位置存快照,块大小被迫跟着变粗。Kimi K3 第 7 篇讲了把哈希粒度和物理块粒度解耦的做法。
命中和 miss 差多少
prefill 的计算量与前缀长度成正比。前缀 400K、增量 4K 的请求,命中时只算 4K,miss 时算 404K,差两个数量级。所以只要前缀有机会复用,服务的每一层设计都应该优先保命中率,哪怕为此付别的代价。下面两节都是这个原则的推论。
KV 池:三层存储,write-back
GPU 显存放不下所有活跃会话的前缀,尤其是多轮对话里用户在思考、agent 在等工具返回的那些时刻,前缀闲着但不能丢。办法是把 KV 往下一层存储搬:CPU DRAM 大一个数量级,SSD 再大一个数量级,代价是带宽逐层下降。
搬的策略有两种:
- write-through:每个块生成时同步写一份到 DRAM。简单,但为所有块付 DRAM 空间和传输带宽,包括那些一直活跃在 GPU 上、根本不会被驱逐的块。
- write-back:块留在 GPU 上,只有被驱逐时才写回 DRAM,下次复用前预取回来。只为真正离开 GPU 的块付账,代价是驱逐时多一次拷贝的延迟,所以要配预取。
长上下文服务下 write-back 通常更划算,因为活跃块的比例高、周转快。和它配套的是驱逐策略(谁先下去)和预取时机(什么时候回来),以及一个容易忽略的点:如果一个前缀由多种 cache 组成(比如 KV 块加线性注意力的状态),它们必须一起搬、一起回来,否则命中了一半等于没命中。
DRAM 从哪来也是个问题。训练和推理共处一批机器时(RL 的 rollout 阶段就是这样),KV 池和训练态在抢 DRAM,一种做法是按时间错开:训练结束把训练态下到 SSD,rollout 结束释放 KV 池。
集群:让请求跟着缓存走
单实例之上,缓存复用的第一个敌人是负载均衡器。轮询或者按负载分发会把同一个会话的下一次请求送到另一台机器上,那里没有它的前缀,一次 miss 抵得上几百次 hit。所以要缓存亲和:同一个会话的请求路由到持有它缓存的实例或集群。把缓存搬过去不是选项,跨集群链路比集群内的网络慢得多。
亲和的代价是绑定。一个会话绑死在一个集群上,集群一挂,上面所有会话一起断。标准解法是一致性哈希:每个会话哈希到环上,顺时针第一个集群是主,第二个是备。主集群服务流量,故障时备集群接管。备集群没有这个会话的缓存,切换后要重新 prefill,但一致性哈希把不同会话的备集群均匀散在整个环上,重 prefill 的活摊给了所有集群,不会集中在一个上。常态下缓存局部性保住,单点故障的影响有界。
集群:按类别做预算
第二个敌人是长请求的突发。生产流量里短请求几千 token,长请求上百万,单个请求的代价跨三个数量级。按「平均请求」做容量规划、排队模型、限流配额,在这种方差下全部失效:一阵长请求就能把算力吃满,后到的短请求排不上,所有流量的首 token 延迟一起恶化。
解法是把准入控制从「数请求」改成「分预算」:按请求类别(长度、推理强度)划分各自的资源份额,每类只能消耗自己那份。长上下文的突发最多把自己那份用完,短请求的延迟不受影响。这和网络里的流量整形是同一个思想。
收尾
三个层次的设计都是同一个原则的推论:miss 比 hit 贵几个数量级,所以整条路径都围着缓存转。实例内按块哈希复用,存储层次上只为离开 GPU 的块付账,集群上让请求跟着缓存走并给突发设上限。一个把三层都做全的真实例子是 Kimi K3,见第 7 篇的前缀缓存和第 10 篇的 KV 池与集群调度。