loading

Loading

探索与发现

首页 AI 前沿

批内前缀共享度分桶:同一批次里谁跟谁绑在一起

分类:AI 前沿
字数: (2446)
阅读: (5)
0
摘要:批内前缀共享能不能真正吃到复用收益,取决于调度器把哪些请求绑进同一个批次。从 vLLM 的 rolling block hash 时机说起,讨论前缀共享度分桶怎么切、桶太细的失败长什么样,以及高负载下被打散的两种极端请求。

连续批处理上线之后,很多人会先看前缀缓存的整体命中率,然后发现这个数字还行,但 P99 的 TTFT 并没有同步好转。原因常常不在缓存本身,而在调度器把哪些请求塞进了同一个批次。批内前缀共享度这事情,想蹭 vLLM 那个「同 batch 内先写 KV、再做注意力」的时机,前提是针对性的分组,否则同一个 batch 里前缀五花八门,复用率会低到不如不做。

同批复用的前提,得先讲清楚

vLLM 能在同一个 batch 里做前缀去重,这一点跟跨批缓存不一样。它依赖 rolling block hash:一个 block 的 hash 由前面所有 block 的内容叠加出来,前面的内容一变,后续所有 hash 都会失效,所以它锁的是「以这段内容为前缀」的整条序列。调度拿到 block 时就把 hash 写进全局哈希表,此时 KV 还没写进去,相当于提前建了索引;同一 batch 里第二个请求调度时按 hash 命中这些 block,就算命中。工程上要紧的是,同一个批次的前向分成两步:先算 QKV 把 K/V 写进 cache,再做注意力与 softmax。轮到注意力时,被复用的那块 KV 已经有值了,复用才安全。

这条链路上有一个隐含假设:命中的块在同一个批次里。如果一次调度只装进一个共享相同 system prompt 的请求,第二个同前缀请求落到了另一个批次,复用机会就随着批次边界漏掉了。跨批也有前缀缓存兜着,但跨批意味着可能碰到显存压力下的驱逐、抢占,实际收益比批内直连要打折扣。

批内共享度分桶想解决什么

标准的连续批处理是按到达顺序拼批:谁先来谁进,显存到头就停。这种策略在两种请求混在一起时会很别扭。一类是同一个 Agent 会话的多轮请求,system prompt 加上历史对话动辄几万 token,公共前缀一大截;另一边是独立的短问答,前缀各异,几乎没有复用空间。两种请求塞同一个 batch,前者能带进来的缓存命中被后者摊薄,_decode 阶段的 attention 计算量也跟着多请求长短分化。

分桶的做法很直接:在调度器入批之前,先按「shared prefix 的预估块数」给等待队列里的请求归堆,共享前缀长的进一组,短的进另一组,然后在每组内部再做显存与 token 预算约束。落到实现上,需要调度器在 grouping 环节保留从 block hash 表里查到的前缀命中数,而不是只留一个布尔值「是否命中缓存」。这个命中数就是分桶的键。

一个常见的误判:把「命中率」当作桶号,忘了命中的是缓存里哪一段。跨会话共享 system prompt 和同一会话内的多轮历史,虽然都命中,但可复用的块数完全不同。按命中数分层,比按命中与否分层更接近真实开销。

桶怎么切,几个工程取舍

按绝对块数等宽切桶,问题在长尾:生产上大部分请求的前缀分布不是均匀的,可能是少数超长会话把长前缀桶撑满,而短桶里塞了几十个小请求抢同一个 decode slot。可以按前缀长度分三级,比如小于 1K 块一桶、1K 到 8K 一桶、8K 以上一桶。小米 MiMo 在 Agentic 场景里公开过类似的做法,他们对长度偏斜做了 0–64K / 64K–256K / 256K–1M 三级长度分桶,理由是短请求会被长请求拖慢,DP-Attention 的同步和 chunked prefill 会放大这种干扰。前缀共享度分桶和长度分桶不是一回事,但动机同源:让开销接近的请求互相扩散,而不是把长短混在一起。

桶的数量不能失守。批处理的价值在于填满 GPU 计算单元,桶越多意味着单桶里能攒的可拼请求越少,空跑的计算就回来了。经验上先按静态配额跑一段,统计各桶的排队时间与每批次填充率。两个指标同时差,说明桶太细;命中数看涨而吞吐不动,说明桶内的前缀并没有真的对齐——可能是会话间共用了模板、但各自注入的变量导致首个分叉 block 就 miss。

被分桶打散的两种极端情况

高负载时

显存水位高、请求排队的时候,分桶会让「同前缀的组「优先级更高,独立短前缀在某一批次里拿不到 decode 名额。这不是 bug,是取舍:牺牲长尾请求的时延,换整体复用收益。在对外 SLA 分层的情况下,把分桶和队列优先级并在一起调整排序很容易失控,建议分桶只决定组进哪批,队列优先级另算。预填充抢占长前缀请求时也要再看一眼:把一个高共享度的组抢走了,复用链条一断,下一个批次从头重算,抢占领到的显存很快又会被重新填满。

低负载时

桶的存在感很弱,还会带来小样本被冷落的观感:一个单独的长系统提示请求,前面等着等不到同前缀的同伴先入批,反过来等同伴这个动作本身没有成本,只要实现上不要扣掉显存配额就行。建议桶不跨批次常驻,每次装配先取当前组的 top 命中块子集,凑不够也照常下发。真正应该等待的只有会话历史带有强动态短前缀、基本每秒都有新来的 Agent 场景。

还需要的一份观测账

前置查询拿到的命中数是估算值,不同 chunk 粒度会影响它的口径。vLLM 的 automatic prefix caching 用的是按 block 内容的 hash 判断;切块粒度跟序列位置相关,超长序列的首块不同即失配,因此 chunk 的量化还需要有一本账:

  • 每个桶级内实际的缓存命中 token 数,对比入桶时估算值,偏差持续大的桶考虑调整建键粒度或预取策略;
  • 每批次实际拼进了几个桶,跨界次数多意味着分组并不稳定;
  • 逐批对比「不分组,按序拼批」的 P99 TTFT 和吞吐差额,先把这一笔账记最小粒度,再谈维持分桶策略;

分组不是免费午餐。让同前缀请求集中在同一个批次会压低重复 prefill 的开销,代价是路由和排队状态变多、可观测性和 test 覆盖面变大。状态多一点少一点,不决定上线;关键要看复用确实省下来的算力是否在自己的延迟预算里能转化为实际收益的吞吐,先在灰度上看这个比值。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/ai-frontier/679.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图