Continuous batching:每一轮重新组队
短答案先离场,新请求马上补上。
按 → 推进 · 动画可播放、拖动和倒回
2026-10-20 · 图中的容量、时长与 token 均为教学示意
短请求结束了,为什么还在占位置?
把 A、B、C 一起送进一个静态 batch。它们分别需要生成 2、4、6 个 token。
两轮后 A 就结束了。固定的批次还要陪 B、C 走完,A 对应的位置开始空转。
直到 C 也完成,下一批才开始。长短不一的答案,把同一张 GPU 切出了许多等待。
方格表示生成一轮的工作,不表示真实耗时;静态 batch 这里指批次成员在完成前不变。
一轮 forward,就是一次换人的机会
自回归生成是一个循环:读历史,算下一个 token,再把它接回历史。
调度器把当前可运行的请求组成 batch,GPU 为每条普通 decode 请求处理一个 token。
拿到新 token 后检查结束条件,再决定下一轮谁留下、谁离开、谁加入。
普通 decode 每请求每轮推进一个 token;投机解码等机制可一次验证多个 token。
调度器每轮读的,是一张不断变化的状态表
| 字段 | 含义 | 为何需要 |
|---|---|---|
| arrival | 请求到达时刻 | 衡量排队与公平性 |
| prompt_remaining | 还未计算的输入 token | 决定 prefill chunk |
| emitted / max_output | 已经生成 / 允许生成的数量 | 决定继续 decode 或完成 |
| block_table / KV length | 已驻留的历史及位置 | 检查显存与构造 attention 输入 |
状态通常经历 waiting → prefill → decode → finished;取消也应释放资源。被抢占的请求要保留 token ID,准备恢复计算。
一条请求有三把时钟
TTFT 包含排队、tokenization、prefill 和传输;下面的教学模拟只建模队列与 forward,真实测量用客户端时钟。
吞吐也要说清:输入 token/s、输出 token/s、完成请求/s 不可混写。平均 ITL 之外还要看 P95/P99,用户会感觉到偶发停顿。
一个 token 预算,不是一个毫秒预算
普通 decode 每条请求本轮计算 1 个输入位置;投机验证、并行草稿等情况需要单独计数。
Prefill
同一请求有多个 query,算子更容易形成大矩阵;chunk 越靠后,还会读取之前的 KV。
Decode
每条只有一个 query,读取长度不同的历史;同样 32 个 token 预算,可能有完全不同的时长。
调度性能模型至少需要 prefill token 数、decode 条数和历史长度分布,随后用实际 profiling 校准。
A 刚离场,D 就能补进来
这一轮跑 A、B、C,D 在等待。batch 的成员不需要从头到尾固定。
A 输出结束 token,调度器移除 A,并归还它不再使用的资源。
下一轮把已准备好的 D 加进来。B、C 继续生成,不必停下来重开整批。
D 这里已完成 prefill;新提示词的 prefill 也需要被调度,不能凭空跳过。
长提示词和逐字生成,节奏不一样
新请求先处理提示词,建立 KV cache,这叫 prefill。之后进入 decode,逐轮生成。
prefill 一次处理许多 token,矩阵运算更容易摊薄权重读取;长提示词可能占住 GPU 较久。
decode 每轮的计算较少,却要读取权重和历史 KV。调度器需要照顾正在等下一字的人。
计算与带宽的占比随模型、batch 和上下文长度变化;这里展示常见工作形态。
从三条请求开始,手工当一次调度器
| 请求 | 到达时间 | 输入长度 | 总输出 token |
|---|---|---|---|
| A | 0 ms | 6 | 3 |
| B | 2 ms | 2 | 2 |
| C | 6 ms | 3 | 2 |
P 是本轮 prefill token 数,D 是 decode 请求数。先安排 decode,再按到达顺序填入 prefill;轮内到达只在下个边界加入。
这是便于手算的教学成本模型。最后一个 prefill chunk 直接采样首 token,后续每轮 decode 再产出一个。
看着时钟走,就知道谁在等待
0–4 ms 只计算 A。B 在 2 ms 到达,要等到 4 ms 才能参加;C 在 6 ms 到达,要等到 8 ms。
现在能把延迟和吞吐都算出来
| 请求 | 输出时刻 / ms | TTFT | ITL / ms |
|---|---|---|---|
| A | 8, 13, 16.5 | 8 − 0 = 8 ms | 5, 3.5 |
| B | 8, 13 | 8 − 2 = 6 ms | 5 |
| C | 16.5, 19.5 | 16.5 − 6 = 10.5 ms | 3 |
C 只有 3 个输入 token,却比 A 的首字更晚;它到达时已有两条 decode 占用预算。只按 prompt 长度估 TTFT 会漏掉排队。
首 token 从哪里来,决定计数会不会差一轮
prefill(prompt_tokens) -> logits_at_last_prompt_position first_output = sample(logits) # 此时 first_output 的 KV 还没写入 decode(first_output, cached_prompt) -> next_logits second_output = sample(next_logits)
输出长度为 3,只需要首轮 prefill 加 2 次 decode。停止后,最后一个输出 token 的 KV 未必需要再计算。
调度表里的“已采样 token 数”和“已计算 KV 长度”是不同字段。错把两者相等,会引入无效位置、重复计算或错位的位置编码。
一轮调度,分成计划、预留、执行、提交
ready = collect_arrived_requests(now) plan = pick_decode(ready, token_budget) plan += fill_prefill_chunks(ready, remaining_budget(plan)) plan = fit_kv_capacity(plan) reserve_blocks(plan) # 先让本轮所需位置有地址 outputs = execute(plan) commit_kv_lengths_and_samples(outputs) release_finished_or_cancelled_requests()
token 上限、序列数量上限、KV 块容量是三个约束。实现可以把选择与分配交错,但失败路径不能留下半完成的状态。
先留出下一字,再把空余预算给提示词
把每轮处理的 token 总数限制为 8。现在 A、B、C 各需要一个 decode token。
先把 3 个 decode token 放进去,剩余预算是 5。
新请求有 12 个提示词 token,这一轮先算 5 个,其余留到后续轮次。这叫 chunked prefill。
这是 vLLM 的 decode 优先策略示意;总 token 数上限对应 max_num_batched_tokens。
长提示词分几轮完成,老请求持续前进
第 1 轮:A、B、C 生成下一字,P 处理最前面的 5 个提示词 token。
第 2 轮:沿用 P 已算好的前缀 KV,继续处理后面 5 个。A、B、C 又各前进一步。
第 3 轮:P 处理最后 2 个 token,prefill 完成。后续就能和大家一起 decode。
表中每轮只标记调度内容,不按真实耗时等比例绘制;后续 chunk 仍会读前面 chunk 的 KV。
把长 prompt 切成三段,究竟换来了什么?
已有 2 条持续 decode。新 prompt 长 12,仍用 t = 2 + 0.5P + D ms;忽略网络,下一轮从 0 ms 开始。
| 策略 | 每轮 P / D | prompt 完成时刻 | 老请求下一 token |
|---|---|---|---|
| 大段:C=14 | 12 / 2 | 10 ms | 10 ms |
| 小段:C=6 | 4 / 2,连续 3 轮 | 18 ms | 6、12、18 ms |
小段让首个 decode 响应更早,也产生更多次固定调度开销。新请求的 prefill 反而更晚完成。
两种策略在 prompt 完成前做的 decode 工作量不同;这是用户等待时间的比较,不是同等总工作量的 GPU 加速比。
屏障切短,老请求就能更早收到字
条形表示轮次经过的时间;数字只统计已完成的 prefill。在 6 ms 时小段已交付下一字,到 18 ms 才完成全部 prompt。
本轮能算 100 个 token,不代表还能再接 100 条
教学例:KV 池 1,000 块,已有活跃请求占 900 块。新请求输入长度 1,600,块大小 16,光 prompt 就需要 100 块。
如果全部接入,它处理完 prompt 时空闲块归零;其他请求一旦跨块边界,就可能触发抢占。chunked prefill 可以分段分配,但不会消除最终驻留的 KV。
入场规则应结合当前块需求、预计增长、并发上限与抢占成本;设预留余量时,要用业务长度分布验证。
吞吐到了平台期,排队就成了主要延迟
若完成能力约 20 请求/s,输入却持续 25 请求/s,队列平均每秒多 5 条。增加等待时间不能解决算力缺口。
在稳定运行的另一个观测窗口,λ = 16 请求/s,平均 W = 0.75 s,则平均在途 L = 12 条。这里包含等待和正在运行的请求。
先设置有界队列、超时和 admission control,再选择扩容或降级。Little 定律用于稳定窗口的平均值,不能拿它预测某一条请求的 P99。
decode 优先也要给长 prompt 留下一条路
饥饿怎样发生
token 预算 C=8,恰好总有 8 条 decode 等待;prefill 每轮拿到 0,输入队列一直增长。
可执行的修复
限制活跃 decode 数;为超时 prefill 保留少量预算;采用年龄权重或每租户配额,控制单个大请求占用。
有些服务愿意为现有会话保持低 ITL,有些更看重新请求 TTFT。把目标写成明确的 SLO,再选择调度权重。
“先到先服务”也需说明粒度:请求级、chunk 级或 token 级会给出不同结果。
吞吐提高了,用户却开始抱怨卡顿
| 教学实验:同一请求 trace | 方案 A | 方案 B |
|---|---|---|
| prefill token 预算 | 512 | 4,096 |
| 输出吞吐 | 900 token/s | 1,050 token/s |
| P99 ITL | 35 ms | 130 ms |
| P95 TTFT | 700 ms | 430 ms |
若 SLO 要求 P99 ITL ≤ 80 ms,B 不合格。先减小 chunk,或按观测耗时限制本轮 prefill;再检查吞吐仍能否覆盖到达率。
不要看到 GPU 利用率提高就宣布成功。把每轮 prefill 数量与 ITL 尖峰对齐,才能验证卡顿确实来自长轮次。
让压测保留真实的“什么时候到达”
固定并发压测
上一条结束后再发下一条。适合测饱和吞吐,但服务变慢时,发出的请求也会变少。
按时间重放
按预定到达时间发送,不等前一条结束。能看到突发、队列增长和 SLO 失守。
保存每条请求的到达时刻、首字时刻、所有流式 token 时刻、结束原因;同时保存服务器轮次、预算与 KV 占用。
按长短 prompt、租户、缓存命中状态分别分组,避免平均值掩盖某一群请求被饿住。
token 预算够,显存也得装得下
这一轮算多少?
token 预算限制本轮计算规模;序列数限制同时服务多少条请求。
历史要存多少?
KV cache 累积的是所有在途请求的历史。长上下文会持续占用显存块。
资源不足时,调度器会让请求等待;某些策略也会暂停请求,之后重算或恢复。
吞吐、首字时间、字间间隔,一起看
Prefill chunk 小一些
已有请求更快拿到下一字;长提示词需要更多轮才能处理完。
Prefill chunk 大一些
长提示词更快完成、计算更集中;同一轮的 decode 可能要等更久。
TTFT:到第一个输出 token 的时间。ITL:相邻输出 token 的间隔。吞吐:单位时间完成的工作量。用真实到达率与长度分布一起测。
同一条请求,四个延迟数该怎么填?
展开参考解答
排队 = 30−10 = 20 ms。TTFT = 70−10 = 60 ms。
ITL = [20,35,25] ms,均值 = 80/3 ≈ 26.67 ms。
E2E = 150−10 = 140 ms。最大间隔是 35 ms,均值不能替代尾延迟;多条请求还需统计分位数。
12 个位置,先 decode 以后怎么切 prompt?
展开参考解答
先给 decode 5 个位置,剩 12−5=7。A 处理 7,A 剩 3;B 本轮处理 0。
P=7、D=5,时长 2+0.5×7+5=10.5 ms。
若计划还要跨块分配,必须再通过 KV 容量检查;token 预算满足只是第一个条件。
平均负载不高,为何新请求一直等?
展开参考解答
这是策略造成的 prefill 饥饿:每轮 8 个位置都被 decode 消耗。
改法一:降低活跃 decode 上限或给老化的 prefill 留 1–2 个位置;观察 TTFT 降幅与 ITL 代价。
改法二:限制新长会话入场,按租户或等待年龄轮换;观察队列年龄和最慢租户,而不只看总体吞吐。
修改预算,重新计算每一条时间线
下载代码后,在保存代码的目录运行:
python3 lab.py python3 lab.py --budget 6
默认预算 4:A 的 TTFT=8 ms,B=6 ms,C=10.5 ms;总用时 19.5 ms,输出吞吐 358.97 token/s。
先在纸上改成预算 6,再比较输出。尝试加入一次 1,000-token 突发,观察短请求的首字等待;再实现每请求最大 chunk,让状态转换与 token 守恒断言继续通过。
下载 lab.py · Python 3 标准库把一个 batch,变成持续流动的队伍
- Yu et al. — Orca: A Distributed Serving System for Transformer-Based Generative Models(OSDI 2022):iteration-level scheduling。
- vLLM:Optimization and Tuning:chunked prefill、decode 优先与 token 预算。
- PagedAttention 论文:连续调度依赖的 KV 显存管理。