连续批处理与分页 KV:请求怎样进入下一轮计算
从长短请求混跑出发,区分迭代调度与 KV 空间管理,用一个小批次和块分配例子解释吞吐、等待与容量的取舍。
把几个请求凑成一批送进模型,似乎就能提高吞吐。但生成接口有个麻烦:有的请求只回答两个 token,有的要生成几百个,新的请求还会不断到达。若一整批必须等最长请求结束才换人,短请求空出来的位置就无法及时使用。
要及时接进新请求,需要同时满足两个条件:调度器愿意在下一轮安排它,显存也有空间保存它的历史 KV。连续批处理解决前一个问题,分页 KV 帮助管理后一个问题。下面先看短请求退出后如何补位,再看补进来的请求需要分配多少缓存。
批次的边界从整条请求变成一次迭代
连续批处理允许调度器在迭代边界移出已完成请求、加入符合条件的新请求。批次成员不必从开始到结束保持不变。
用一个只观察 Decode 的小例子:A、B 已就绪,分别还需 2、5 步;C 在第 2 轮结束时就绪,还需 2 步。每轮最多容纳两个请求,假设每步耗时相同,省略 Prefill 和准入开销。
| 轮次 | 固定成员,整批结束再接 C | 每轮可以更换成员 |
|---|---|---|
| 1–2 | A、B | A、B |
| 3–4 | B;A 的位置空闲 | B、C |
| 5 | B | B |
| 6–7 | C | 已结束 |
变化发生在第 3 轮:A 结束后,C 直接进入它空出来的位置,不再等 B 完成。表里因此少了两轮,但实际系统不能据此换算成固定加速比。每轮耗时会随成员、上下文长度和硬件变化;C 在进入 Decode 前,还要完成自己的 Prefill,这部分也需要调度。
这也是“并发数越多越好”的问题所在:多一个活跃请求,可能增加本轮工作与 KV 读取量。吞吐提高时,单个请求的输出间隔仍可能变长。
长输入也会占用轮次
如果直接把一个很长的 Prefill 放进正在生成的批次,已经开始输出的请求可能等待更久。Chunked Prefill 把长输入拆成可调度的块,让调度器在输入处理和 Decode 之间分配每轮预算。
此时,“还能不能加一个请求”就不能只看并发数了。调度器至少要分别检查三个上限:
| 上限 | 限制什么 | 不能代替什么 |
|---|---|---|
| 活跃序列数 | 同时参与服务的请求数量 | 不能说明每个请求有多长 |
| 每轮 token 预算 | 本轮处理的 Prefill 与 Decode 工作量 | 不是全部历史 KV 的 token 数 |
| 可用 KV 块 | 已保留及即将增长的缓存空间 | 不是本轮计算量 |
以 vLLM v0.10.2 的调优文档为例,V1 的 chunked prefill 调度优先安排 Decode,再用剩余 token 预算安排 Prefill;这是具体版本的策略,不是所有引擎的统一规则。按其配置术语,前两项分别涉及 max_num_seqs 和 max_num_batched_tokens;具体调度规则要与引擎版本一起固定。即使本轮 token 预算还有余额,也可能因 KV 不足而无法接收新请求。反过来,显存有空闲,也不意味着扩大单轮计算不会影响输出间隔。
优先保障 Decode 可以减少正在流式输出的请求被长输入打断,但新请求的首 token 可能更晚。比较方案时需要同时看 TTFT 与 ITL,不能只选其中更好看的一个。
分页 KV 让逻辑连续与物理连续分开
一个请求的历史 token 有顺序,并不要求它们的 KV 在显存里占据一整段连续区域。分页管理把逻辑 token 区间映射到固定大小的物理块,注意力计算通过相应映射访问 KV。请求增长时追加块,结束后释放或按缓存策略保留块。PagedAttention 论文(2023)解释了逻辑块到物理块的映射及共享;下面的 16-token 块是教学假设,不是所有版本和后端的固定配置。
假设块大小为 16 token,两个互不共享 KV 的请求当前长度为 17 和 31:
实际使用 48 个槽位,块内剩余 16 个。分页缓解了预留大段连续空间和外部碎片的问题,但最后一个块仍可能有内部浪费。更小的块可以减少尾部空位,同时增加块表、分配和访问管理的成本;这里不能只按空位比例选块大小。
若每个 token 的全部层 KV 占用为 字节,例子的块数据容量就是 ,另计元数据。 的推导见 KV Cache 显存估算。
共享前缀改变的是物理占用
两个请求逻辑上各有一段历史,不代表物理上必须各存一份相同前缀。满足兼容条件时,缓存管理器可以让它们引用已有块;分支开始写入可能共享的可变区域时,需要复制或其他隔离机制,避免互相覆盖。
这时用所有请求的逻辑长度直接相加,会高估共享部分的物理容量。另一个方向也容易漏算:请求结束后,前缀块可能作为可复用缓存留下,因此活跃请求变少,缓存池的占用未必立即下降。引用计数、保留策略与驱逐共同决定哪些块真正可回收。
内存紧张时,引擎可能抢占请求、释放其缓存并在后续重算,或采用其他恢复机制。排队、驱逐与重算次数应该和延迟一起记录,否则很容易把容量压力误看成模型计算突然变慢。
用两组实验区分调度与容量
第一组固定输入输出分布和 KV 容量,扫描每轮 token 预算,观察 TTFT、ITL、输出吞吐和排队。这组主要回答计算如何分配。
第二组固定调度配置,逐渐增加上下文长度或准入上限,观察已用块、可回收块、抢占与重算。不要同时增加长度和并发,否则难以判断容量压力来自哪一项。
判断配置是否值得保留,可以采用明确的约束:先要求首 token 与输出间隔满足目标,再比较约束内能承载的完成速率。推理压测接着把这个判断写成可重复的测量过程。