你经营过餐厅吗?假设你有一家只有四个灶台的小饭馆,传统做法是:等凑齐一桌客人(攒够一个 batch)才一起开火,然后这桌客人必须全部吃完才能一起离席,空出来的灶台才能服务下一桌。这种“宴会厅模式”最大的问题是:有人吃得快,有人吃得慢,吃得快的人早早放下筷子,也只能干坐着占位置,外面排队的客人进不来,灶台白白空转。 大模型推理(LLM Inference)也是这样。早期 GPU 服务采用“静态批处理”:攒够一批请求同时计算,整批全部生成完才释放资源。但真实世界的请求到达是忽多忽少的——聊天、写作、翻译的请求从来不会整整齐齐排队进场。于是,人们借鉴了快餐店的翻台思路,提出了连续批处理(Continuous Batching):每个请求独立入座、独立上菜,谁吃完谁走,新客人随到随坐,让 GPU 这块“灶台”时刻保持最高的翻台率。 本文就用这家“推理快餐店”的故事,带你从零理解连续批处理的核心原理、请求的动态加入与退出、迭代级调度策略,以及它适用的真实场景。

岗位关联

对 AI 从业者而言,连续批处理是搭建大模型在线服务的必备技能。掌握它,你就能把 GPU 利用率提升数倍,让服务在流量高峰时依然又快又稳——这项能力正是大模型推理系统工程师与算法工程师的核心竞争力。

目录
  1. 可视化演示
  2. 静态批处理痛点
  3. 连续批处理原理
  4. 动态加入机制
  5. 动态退出机制
  6. 迭代级调度策略
  7. 效果评估指标
  8. 适用场景画像
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过对比静态与连续批处理、迭代级调度三阶段决策、以及请求生命周期Slot管理,展示连续批处理如何实现请求动态加入退出提升GPU利用率。

本节配有交互动效(进入视口后自动加载,约 3KB)。

静态 vs 连续批处理对比 静态批处理需等待所有请求完成才能接纳新请求 连续批处理每iteration动态加入退出请求 GPU利用率=活跃Slot/总Slot 动态调度显著提升GPU利用率

静态批处理痛点

小明开了一家“推理快餐店”,最初采用宴会厅模式:大厅里只有 4 张大圆桌,服务员要等凑满一桌客人才通知后厨开火。开火后,同一桌客人吃的是一模一样的固定套餐,每人吃菜速度不同——有人风卷残云,有人细嚼慢咽。最糟糕的是,哪怕桌上有人早就吃完了,也必须坐在原地等全桌人离席,服务员才能收拾翻台。

这个模式映射到 LLM 推理,就是静态批处理(Static Batching):系统攒够固定数量的请求(batch)才真正投入 GPU 计算;一个批次内的所有请求同时开始、同步生成。模型每走一步会为批内所有请求各预测一个token(可以理解为“一个词/字单位”,就是上一道菜),直到整个批次全部生成完毕,这批请求才会一起“离席”,GPU 显存中的KV Cache(每桌客人的进食记录)才统一下一释放。

听起来很合理?问题在于真实世界的客流量从来不会整整齐齐。如果最快吃完的客人在第 3 分钟就放下筷子,最慢的客人要吃到第 10 分钟,那这张桌子就是被白占了 7 分钟。批处理中所谓“批”,指桌上这几个请求共用一次前向计算(一起让 GPU 推算下一个 token),但因为所有请求必须同步结束,耗时完全由最慢的请求决定:

把宴会厅模式坚持了半年,小明发现后厨常有这样的场景:一口炒锅好不容易热起来,灶台上却有一个盘子早就空了,锅铲根本没法炒下一道菜。静态批处理最直接的后果就是 GPU 算力空转、资源利用率低下;同时,晚到的请求必须在门外排队,等待当前整批结束才能入席,即使 GPU 内部明明有空闲的“灶位”也无法插队。对小明的快餐店来说,这就是每天都在亏钱。

Tbatch=max⁡1≤i≤nTi\displaystyle T_{batch} = \max_{1 \le i \le n} T_i
这个公式的含义是:整批请求的总时长,等于批内单个请求耗时的最大值,而不是平均值。就像一桌人吃饭,最快的人吃完了也得等到最慢的人放下筷子。
  1. T_batch 的含义T_batch 表示这一整批请求从开始到全部结束的总耗时,也就是桌子被占用的总时长。
    Tbatch=批次开始时刻→批次全部结束时刻\displaystyle T_{batch} = \text{批次开始时刻} \to \text{批次全部结束时刻}
  2. max 取最大的含义max 表示在花括号里的一堆耗时中挑出最大的那个,对应“木桶效应”——最短的板决定不是桶能装多少,最长的耗时决定整批何时结束。
    max⁡1≤i≤nTi=所有请求耗时中的最大值\displaystyle \max_{1 \le i \le n} T_i = \text{所有请求耗时中的最大值}
  3. 下标 i 与 n 的含义i 是请求在批次内的编号,n 是本批请求的总数;T_i 是第 i 个请求如果单独在 GPU 上跑完所需的耗时。
    Ti=第 i 个请求的独立执行时间\displaystyle T_i = \text{第 } i \text{ 个请求的独立执行时间}

连续批处理原理

痛定思痛,小明把宴会厅改成了快餐店:撤掉大圆桌,换成四人小桌;不再要求“凑齐一桌才开火”,而是来一位客人安排一位,谁吃完谁走,空出的桌子马上安排下一个等位的客人。

这个“随到随吃”的思路,就是连续批处理(Continuous Batching)。它的核心变革在于:调度的粗细粒度从“整批一起”缩小到了“每一步一起”。大模型生成文本是一个词一个词往外蹦的,每蹦出一个 token,就完成了一次前向计算(一个 iteration,可以理解为“后厨炒一道菜”)。连续批处理不再要求批内所有请求同生共死,而是让每个请求独立维护自己的生成进度,每一次 iteration 结束后都检查一遍:有没有人吃完了?有没有新客人在等位?如果有,立刻翻台。

于是,GPU 上的“锅”始终在炒菜。某个请求可能已经生成了 50 个 token,而它旁边的请求才刚刚完成第一个 token——它们速度不同、进度不同,但共享着同一块 GPU 算力,谁也不会拖累谁。从全局看,一段墙钟时间内完成的请求数量显著增加:

小明算了一笔账:以前宴会厅一天翻 20 台,改成快餐店后能翻 80 台。大模型推理也是如此——连续批处理用“多轮小翻台”替代“一桌大团餐”,让每一份 GPU 算力都花在刀刃上。这也是为什么如今主流推理引擎(如 vLLM、TensorRT-LLM)都内置了连续批处理能力。

Throughput=NdoneTwall\displaystyle Throughput = \frac{N_{done}}{T_{wall}}
这个公式说的是:系统吞吐量等于“在墙钟时间 T_wall 内成功完成的请求数 N_done”除以总时间。你可以想象成快餐店一天能接待多少拨客人——接待的拨数越多,翻台率越高。
  1. 分子 N_done 的含义N_done 是统计时间段内真正完成全部生成、把响应完整返回给用户的请求个数,相当于快餐店这天端出去又收了回来的餐盘数。
    Ndone=∑t∈[0,Twall]1{请求在 t 时刻完成}\displaystyle N_{done} = \sum_{t \in [0, T_{wall}]} \mathbf{1}_{\{\text{请求在 } t \text{ 时刻完成}\}}
  2. 分母 T_wall 的含义T_wall 是墙钟时间,就是真实世界从计时开始到结束流逝的秒数,对应小店从开门到打烊实际营业了多少分钟。
    Twall=tend−tstart\displaystyle T_{wall} = t_{end} - t_{start}
  3. 整体解读吞吐量 = 完成数 ÷ 总时间。分母越小(等待空窗越少)、分子越大(翻台越多),吞吐量越高。连续批处理同时改善了这两者:不让灶台空等,也不让客人白占座。

动态加入机制

快餐店开业后,门口放了一个取号机,客人取号后在等候区坐着刷手机。快餐店的服务员每次路过出餐口,都会瞄一眼叫号屏:只要有一张桌子空出来,立刻叫下一个号。

这个取号机,对应连续批处理中的等待队列(Waiting Queue)。新请求到达时,并不会直接打断正在运行的请求(那不是连续批处理,是胡闹),而是先进入等待队列“取个号”。调度器会在每一个 iteration 结束时检查当前 GPU 是否有空余的槽位(看有没有空桌),如果有,就把队列中最先到达的请求领进来。

新请求入座时,还要完成一个特殊环节——预填充(Prefill)。你可以把它理解为“后厨先把这桌客人的锅底一次性熬好”:模型需要先读完这个请求的完整输入文本(prompt),把中间结果写入 KV Cache,然后才能开始逐字输出第一个 token。在静态批处理里,一个请求的 prefill 可能因为“还没凑齐一桌”而无限推迟;而连续批处理中,只要有一个空槽,这个请求的 prefill 就能立刻执行。从用户视角看,请求“从发出到开始有响应”的等待时间大幅缩短:

排队叫号机制听起来简单,但实现上有一个关键问题:槽位分配。GPU 的内存有限,每个请求的 KV Cache 都要占据显存。系统需要为每个槽位预留足够的显存空间,并在新请求入座时动态分配 — 就像快餐店要数清楚还有几把椅子能坐,不能把过道都塞满。好的调度器还会优先把槽位给 waiting 队列中等待最久的请求,避免某个请求被“饿死”。

如今你去任何一家大模型 API 服务后端,都能看到这种“取号入座”的机制。它换来了一个直接的用户体验提升——用户在高峰期发消息,不再需要盯着“转圈”等很久才看到第一个字蹦出来。

Wavg=1N∑j=1N(tstart(j)−tarrive(j))\displaystyle W_{avg} = \frac{1}{N} \sum_{j=1}^{N} \left( t_{start}^{(j)} - t_{arrive}^{(j)} \right)
这个公式代表平均排队等待时间:把所有请求“真正开始被服务时刻”减去“到达时刻”的差值加起来,再除以请求总数,就是每个客人在等候区平均坐了多久。
  1. 括号内的差t_start 是请求被调度器安排进 GPU 槽位、开始 prefill 的时刻;t_arrive 是用户发出请求、系统收到的时刻。两者相减,就是这个请求在队列里等待的时长。
    tstart(j)−tarrive(j)=第 j 个请求的排队时长\displaystyle t_{start}^{(j)} - t_{arrive}^{(j)} = \text{第 } j \text{ 个请求的排队时长}
  2. 求和符号Σ 把 N 个请求各自的排队时长全部累加,得到的总等待人时,相当于把每位客人的等位时间都记在小本本上。
    ∑j=1N(tstart(j)−tarrive(j))=所有请求排队时长总和\displaystyle \sum_{j=1}^{N} \left( t_{start}^{(j)} - t_{arrive}^{(j)} \right) = \text{所有请求排队时长总和}
  3. 除以 N 取平均除以请求总数 N,就得到平均每人等了多久。排队时间越短,越接近“随到随吃”的理想状态。
    1N∑j=1N(⋯ )=平均排队等待时间\displaystyle \frac{1}{N} \sum_{j=1}^{N} \left( \cdots \right) = \text{平均排队等待时间}

动态退出机制

快餐店生意好了,小明又观察到一个现象:大多数客人吃完饭后不会赖着不走,但如果没有服务员主动收桌,桌子还是会空置到“下一轮叫号”才被利用。于是小明规定:客人一放下筷子,服务员立刻收桌,这套动作的学名叫“翻台”。

在大模型推理中,“放下筷子”就是请求的生成终止判定。一个请求什么时候算“吃完”?通常有两个条件:一是模型生成了特殊的终止符 EOS(End of Sequence,可以理解为菜谱上“菜品已完成”的标记),二是生成了用户设定的最大长度 max_tokens。只要满足其中一个,这个请求就立即停止生成,不再占用计算资源。

真正体现“立即收桌”的是 KV Cache 的释放。前文说过,KV Cache 相当于每桌客人的进食记录——记录了这个请求已经生成的上下文,是后续生成 token 的“记忆”。静态批处理中,这些记录要等整批结束才统一清空;而连续批处理中,请求一旦终止,系统立刻释放它占用的显存槽位,让等待队列中的下一个请求马上复用这块空间。用一句话概括动态加入与退出的平衡:

还有一层防护叫越界保护。有些请求可能会生成超长文本,或者在生成过程中陷入循环、停不下来。连续批处理要求系统在每次迭代中检查“这个请求是不是已经达到了 max_tokens”,一旦越界立刻强制终止并释放资源。这就像餐厅规定每桌用餐时间上限,防止个别客人长期霸占座位。

动态退出机制让“翻台”变成了一项自动化操作:客走桌净、立即补位,GPU 上的每一块显存都在高频轮转。这就是连续批处理能大幅提升吞吐量的第二个秘诀——不仅让新客不等桌,更让空桌不等客。

Nactive(t+1)=Nactive(t)−Nfinish(t)+Nenter(t)\displaystyle N_{active}(t+1) = N_{active}(t) - N_{finish}(t) + N_{enter}(t)
这个公式描述的是:下一个时刻正在运行的请求数 = 当前运行数 - 这一轮完成的请求数 + 这一轮新进入的请求数。就像餐厅里此刻在吃的客人 = 刚才在吃的 - 刚走的 + 刚入座的。
  1. N_active 的含义N_active(t) 表示 t 时刻正在 GPU 上运行、正在逐个生成 token 的请求数量,也就是大厅里正在用餐的客人数。
    Nactive(t)=时刻 t 正在运行的请求数\displaystyle N_{active}(t) = \text{时刻 } t \text{ 正在运行的请求数}
  2. 减去 N_finishN_finish(t) 是本轮迭代中生成完成(遇到 EOS 或达到 max_tokens)而退出运行的请求数,对应这一波刚放下筷子起身离开的客人。
    Nfinish(t)=本轮迭代中完成的请求数\displaystyle N_{finish}(t) = \text{本轮迭代中完成的请求数}
  3. 加上 N_enterN_enter(t) 是新一轮调度中新进入运行的请求数,也就是刚被叫到号、坐到空位上的新客人。三者相加得到的 N_active(t+1) 就是下一时刻的“上座率”。
    Nenter(t)=本轮迭代中新加入的请求数\displaystyle N_{enter}(t) = \text{本轮迭代中新加入的请求数}

迭代级调度策略

餐厅越来越忙后,小明请了一位“店长”负责调度。这位店长有一个奇怪的习惯:每出一道菜就绕着大厅走一圈,看看有没有人吃完、有没有人需要加水、门口有没有新客人在等。他的调度频率不是“每半小时一次”,而是“每一道菜上桌时一次”。

在连续批处理系统中,这位店长就是调度器(Scheduler),它的工作节奏叫作迭代级调度(Iteration-level Scheduling):每完成一次前向计算(生成一个 token),调度器就获得一次介入机会。它的待办清单包括:检查运行中请求是否完成、检查 waiting 队列是否有请求、决定是否把新请求插入空槽。可以认为调度的“时间颗粒度”等于单个 token 的生成时间:

有了调度时机,还要有调度策略。最常见的两类策略,就好比餐厅叫号的不同规则:先到先服务(FCFS) 严格按取号顺序叫号,公平但可能让“想简单吃口快餐”的客人也排很久;最长等待优先(Longest Wait First) 优先叫那些等得最久的号,避免个别客人等到天荒地老,这是对大模型服务而言更友好的策略。

调度中还有一个精妙的难点——抢占(Preemption)。当一个新请求需要做 prefill(煮锅底)而 GPU 算力已被占满时,系统可以选择暂停某个正在解码的请求(把它的状态移出显存),把资源让给新请求做 prefill。这就像后厨突然来了一个加急大单,只好先把某个小菜暂时熄火。为了减少这种“抢锅”的代价,业界提出了分块预填充(Chunked Prefill):把 prefill 这个大任务切成小份,一次只做一小块,和 decode 交替进行,就像把“煮一锅汤”拆成“每次加一勺水”,让灶台始终有活干、又不会被长期独占。

多亏了这位勤快的店长,餐厅的每一张桌子都在高效轮转。调度器的存在,让连续批处理不再是“能跑就行”,而是“跑得又快又公平”——这正是它和朴素动态批处理的本质区别。

Δtsched=titer=1rtoken\displaystyle \Delta t_{sched} = t_{iter} = \frac{1}{r_{token}}
这个公式说明:两次调度之间的时间间隔,恰好等于模型解码一个 token 所需的时长。token 生成得越快,调度检查得越频繁,系统的“翻台响应”就越灵敏。
  1. Δt_sched 的含义Δ t_sched 是相邻两次调度决策之间的时间间隔,也就是店长两次“巡场”之间的间隔。
    Δtsched=两次调度之间的时间间隔\displaystyle \Delta t_{sched} = \text{两次调度之间的时间间隔}
  2. t_iter 的含义t_iter 是模型生成一个 token 所需的时间——大模型每走一步(一个 iteration)就生成一个 token,这既是生成节奏,也是调度节奏。
    titer=单次迭代/单个 token 的解码耗时\displaystyle t_{iter} = \text{单次迭代/单个 token 的解码耗时}
  3. 倒数关系r_token 是每秒生成的 token 数;它的倒数就是生成速度的周期。迭代越快,调度机会越密集,资源空窗越短。
    1rtoken=token 生成速度的倒数,即每个 token 的平均耗时\displaystyle \frac{1}{r_{token}} = \text{token 生成速度的倒数,即每个 token 的平均耗时}

效果评估指标

店长上任一个月,小明想知道改革成效如何。他调出了三组数据:翻台率(每天接待了多少拨客人)、首口菜时间(客人坐下后多久吃上第一口)、等位时长(客人在门口平均等了多久)。这三组数据,对应连续批处理系统的三个核心指标:

第一个是吞吐量(Throughput),即单位时间内完成的请求数,或者单位时间内生成的 token 总数。这是评估 GPU 利用效率最直观的指标。连续批处理把“等最慢请求”的空转时间抢了回来,在相同的硬件上通常能把吞吐量提升 2~10 倍。另一个重要指标是首令牌延迟(TTFT,Time To First Token)——从用户发出请求到收到第一个 token 的时间:

第三个指标是排队等待时间,与 TTFT 相关但有区别:排队时间只统计在 waiting 队列中等待的时间,不包含 prefill 和 decode 的时间。第四个维度是服务质量权衡——连续批处理并非“免费的午餐”:请求越多、GPU 越满,单个请求的生成速度(inter-token latency,即相邻 token 的间隔)可能变慢。就像快餐店顾客太多时,后厨再快,每桌的上菜间隔也会拉长。

小明把三个指标一对比,心里有了数:TTFT 和排队时间的改善,让用户体验显著提升;吞吐量的提升,让硬件成本大幅摊薄;至于 token 间隔略微变长,在高并发场景下完全值得。这也提醒你:评估连续批处理效果时,不要只看一个指标,而是像看体检报告一样,把吞吐量、TTFT、排队时间放在一起综合判断。

TTFT=tfirst_token−tarrival\displaystyle TTFT = t_{first\_token} - t_{arrival}
这个公式的含义是:首令牌延迟 = 模型吐出第一个 token 的时刻减去请求到达系统的时刻。它衡量的是“用户拿筷子的那一瞬间”要等多久,是聊天体验中最敏感的数字。
  1. t_first_token 的含义t_first_token 是系统完成请求的预填充后、输出第一个 token 并返回给用户的时刻,对应快餐店端上第一口菜的时刻。
    tfirst_token=第一个 token 被生成并返回的时间戳\displaystyle t_{first\_token} = \text{第一个 token 被生成并返回的时间戳}
  2. t_arrival 的含义t_arrival 是请求到达系统、进入等待队列的时刻,也就是客人推门而入按下取号机的那一刻。
    tarrival=请求到达系统的时间戳\displaystyle t_{arrival} = \text{请求到达系统的时间戳}
  3. 整体解读两者相减,就是用户从“发出消息”到“看见第一个字蹦出来”的等待时间。连续批处理让新请求不必等整批结束,TTFT 往往从“秒级”下降到“毫秒级”。
    TTFT=首 token 生成时间−请求到达时间\displaystyle TTFT = \text{首 token 生成时间} - \text{请求到达时间}

适用场景画像

尝到甜头后,小明想把“快餐模式”复制到其他餐饮形态中,但他很快发现,并不是所有餐厅都适合翻台策略。楼下那家法式慢料理餐厅讲究“一餐吃三小时”,客人不赶时间,翻台策略意义不大;但机场里的面馆、写字楼下的便利店,翻台策略就是生死线。

同理,连续批处理也有明确的适用场景画像。最典型的特征是在线服务负载不均:请求到达率忽高忽低,会有“早高峰”和“下午茶”,而不是匀速匀速地一条条到来。在这种流量下,静态批处理要么在低峰期攒不齐批次、白白浪费 GPU,要么在高峰期把队列拖到超时;而连续批处理能灵活适配,低峰期不攒批,高峰期高效翻台。

第二个特征是长短请求混合。实际业务中,有的请求很短(“用一句话介绍你自己”),有的请求很长(“请分析这篇 8000 字的财报并给出 10 条建议”)。静态批处理会让短请求被长请求拖到天荒地老;而连续批处理让长短请求并行共存、各走各的路,互不拖累。第三个特征是多轮对话场景——对话上下文不断累积,每轮请求都需要处理前面所有历史,生成长度和计算量天然不均匀,这正是连续批处理最擅长的“每桌口味不同”。

反过来说,如果你的系统请求量极小、流量恒定、请求长度也都差不多,那静态批处理也能正常工作,连续批处理带来的收益有限。但今天绝大多数大模型在线服务——从智能客服到 AI 编程助手——都具备“负载不均 + 长短混合 + 多轮对话”三个特征,这也是连续批处理成为行业标配的根本原因。快餐模式不是万能灵药,但对大多数“现代餐厅”而言,它就是最佳的经营方式。

ρ=λμ\displaystyle \rho = \frac{\lambda}{\mu}
这个公式是排队论中的利用率公式:系统繁忙程度 ρ = 请求到达率 λ ÷ 服务率 μ。它告诉我们:当到达率接近服务率时,系统会接近饱和,排队时间会急剧恶化。连续批处理通过提升 μ(服务率)来压低 ρ,相当于给餐厅新增了更多“有效灶台”。
  1. 分子 λ 的含义λ(lambda)是请求到达率,即平均每秒有多少新请求到达系统,对应快餐店门口每分钟进来的客人数。
    λ=ΔNarriveΔt=单位时间到达的请求数\displaystyle \lambda = \frac{\Delta N_{arrive}}{\Delta t} = \text{单位时间到达的请求数}
  2. 分母 μ 的含义μ(mu)是服务率,即系统平均每秒能处理多少个请求,对应后厨每小时能出多少份餐。
    μ=ΔNfinishΔt=单位时间完成的请求数\displaystyle \mu = \frac{\Delta N_{finish}}{\Delta t} = \text{单位时间完成的请求数}
  3. 整体解读ρ 越接近 1,系统越接近满负荷,等待队列会急剧膨胀;ρ 越小,系统越从容。连续批处理让 μ 变大,意味着同样的硬件能承接更高的请求洪峰。
    ρ=λμ≈系统繁忙程度,越大越拥堵\displaystyle \rho = \frac{\lambda}{\mu} \approx \text{系统繁忙程度,越大越拥堵}

AI 应用场景

大模型在线聊天服务

阶段:推理加速与部署优化

解决的问题:聊天类大模型服务的请求到达完全不均匀——白天上班族集中提问、深夜几乎无人、节假日流量暴涨。传统静态批处理在高峰时需要用户排长队,低谷时 GPU 又大量空转。连续批处理让每个请求独立翻台,GPU 时刻保持高利用率,用户请求随到随处理。

具体案例:某互联网公司部署 70 亿参数对话模型,高峰期每秒到达上百个请求,单个回复最长可达数千字。接入连续批处理后,GPU 利用率从 40% 提升到 90% 以上,同样的 GPU 资源可以承载三倍的用户量,且首 token 延迟从 2 秒下降到 300 毫秒。

智能文档问答系统

阶段:长上下文推理优化

解决的问题:企业知识库问答常常面临“长短请求混合”的困境:有人只问“报销流程是什么”(短请求),有人上传整份合同要求逐条分析(长请求)。静态批处理下,短请求会被长请求拖累;连续批处理让长短请求并行推进、各走各的生成进度,避免长任务霸占 GPU。

具体案例:某律所的合同审查助手,用户提交 20 页合同后需要模型逐段分析。系统将多个用户请求混批调度:短请求几秒内返回,长请求安静地逐 token 生成,互不阻塞。整体日处理量从 3000 份提升到 15000 份。

多租户大模型 API 服务

阶段:资源管理与调度策略

解决的问题:云厂商为大客户提供模型 API,多个租户共享同一批 GPU。租户 A 的流量可能突增,租户 B 的请求却很短。连续批处理的迭代级调度能力,让不同租户的请求在同一个 GPU 上公平混跑,并通过最长等待优先策略防止某租户被“饿死”。

具体案例:某 AI 云平台在共享推理集群中引入连续批处理与分块预填充后,SLA(服务等级协议)达标率从 92% 提升到 99.5%,同时整体 GPU 采购成本下降了约 40%。

Python 演示

python74 行
import random  class Request:    def __init__(self, rid, arrival, work):        self.rid = rid        self.arrival = arrival        self.work = work        self.start = None        self.finish = None  def gen_requests(n, seed=0):    rng = random.Random(seed)    reqs = []    t = 0.0    for i in range(n):        t += rng.expovariate(1 / 1.8)        work = rng.randint(3, 12)        reqs.append(Request(i, round(t, 2), work))    return reqs  def run_static(reqs, batch_size):    reqs = sorted(reqs, key=lambda r: r.arrival)    t = 0.0    for i in range(0, len(reqs), batch_size):        batch = reqs[i:i + batch_size]        start = max(t, batch[-1].arrival)        finish = start + max(r.work for r in batch)        for r in batch:            r.start = start            r.finish = finish        t = finish    return reqs  def run_continuous(reqs, slots):    reqs = sorted(reqs, key=lambda r: r.arrival)    free_at = [0.0] * slots    for r in reqs:        idx = min(range(slots), key=lambda k: free_at[k])        start = max(r.arrival, free_at[idx])        r.start = start        r.finish = start + r.work        free_at[idx] = r.finish    return reqs  def evaluate(reqs):    last = max(r.finish for r in reqs)    throughput = len(reqs) / last    avg_wait = sum(r.start - r.arrival for r in reqs) / len(reqs)    return last, throughput, avg_wait  def main():    reqs = gen_requests(60, seed=42)    static = run_static(list(reqs), batch_size=3)    cont = run_continuous(list(reqs), slots=3)    s_last, s_tp, s_wait = evaluate(static)    c_last, c_tp, c_wait = evaluate(cont)    line = "-" * 48    print(line)    print("静态批处理  batch_size=3")    print("  总时长: %.2f  吞吐量: %.2f 个/秒 平均排队: %.2f" % (s_last, s_tp, s_wait))    print(line)    print("连续批处理  slots=3")    print("  总时长: %.2f  吞吐量: %.2f 个/秒 平均排队: %.2f" % (c_last, c_tp, c_wait))    print(line)  if __name__ == "__main__":    main()
目的:用 60 个随机到达、耗时不均的模拟请求,分别在“静态批处理”和“连续批处理”两种调度模式下运行,直观对比两者的总耗时、吞吐量和平均排队时间。
  1. t += rng.expovariate(1 / 1.8)用指数分布模拟“请求到达不均匀”:客流有时密集、有时稀疏,贴近真实在线服务。
  2. start = max(t, batch[-1].arrival)静态批处理必须等到本批最后一个请求也到达才开跑,这就是“凑齐一桌才开火”。
  3. finish = start + max(r.work for r in batch)整批的结束时间由最慢请求决定,复现木桶效应。
  4. free_at = [0.0] * slots连续批处理用 3 个槽位管理“空桌”,每个槽位记录下次空闲时刻。
  5. idx = min(range(slots), key=lambda k: free_at[k])新请求总是找最早空闲的槽位,相当于店长把新客人安排到最快翻台的桌子上。
  6. start = max(r.arrival, free_at[idx])请求到达即可入座,只要槽位空着就不必等别人,这就是连续批处理的精髓。

常见误区

误区我以为是连续批处理就把每个请求独立计算,让 GPU 一次只处理一个请求,跟批处理没关系了。

正解恰恰相反,连续批处理仍然让多个请求共享一次 GPU 前向计算,只是把“整批同步开始、同步结束”变成了“批内请求各自独立推进”。就像快餐店依然同时服务很多桌客人,只是每桌吃菜速度互不影响。多请求共享算力的本质没变,变的是调度粒度从“批”细化为“每一步”。

为何常见:因为“连续”这个词容易让人联想到“逐个处理”,而忽略了底层的矩阵并行计算仍然在批量进行。

误区那既然批处理能提升吞吐,我把 batch size 设到最大、把 GPU 显存塞满,是不是收益最大?

正解batch 过大会带来两个问题:一是每个请求的 KV Cache 都需要显存,批太大直接显存溢出;二是单次迭代耗时变长,每个请求的 token 生成间隔(ITL)被拉长,用户会明显感觉“打字变慢”。连续批处理强调的是“动态适配的批”,而不是“盲目追求大的批”。

为何常见:人们容易把吞吐量和批大小当成简单的正比关系,忽略了显存约束和单个请求延迟的权衡。

误区我的理解是,连续批处理就是让 GPU 更忙,它只影响服务器整体吞吐,单个用户的体验不会变好。

正解实际上,连续批处理对延迟的帮助非常明显——尤其是首令牌延迟(TTFT)和排队时间。静态批处理中,晚到的请求必须等整批结束后才能开始 prefill;连续批处理中,只要有空槽,请求立刻入座。对用户而言,从“发消息到看到第一个字”的等待时间大幅缩短。

为何常见:人们把“吞吐”和“延迟”天然对立,以为优化吞吐必然牺牲延迟,忽略了“消除等待空转”对两者是双赢。

误区请求一结束,GPU 显存马上就空出来了,新请求立刻就能用,不需要任何额外处理。

正解KV Cache 的分配与释放并非零成本。实际系统中,显存管理通常采用分页机制(如 PagedAttention),一个请求的 KV Cache 分散在多个“页”中,释放时需要回收这些页、更新空闲表。频繁的加入退出还可能产生显存碎片,因此业界会配合 chunked prefill、显存池等机制来优化。这就像快餐店翻台还要擦桌子、摆餐具,不能想当然地以为客人起身那一刻桌子就能用。

为何常见:从逻辑上看“释放=空闲”想当然,没有意识到底层显存管理的物理细节和工程成本。

面试题

Q1. 静态批处理最核心的低效在于什么?

单选题 · easy

  • A.GPU 的算力本身不够用
  • B.整批请求必须等最慢的完成← 正确答案
  • C.请求无法进入等待队列
  • D.模型无法复用 KV 缓存

答案:B。静态批处理是“宴会厅模式”,整批请求同步开始、同步结束,先完成请求的槽位被白白占住,直到最慢请求完成才释放,这就是木桶效应。A 是硬件问题,C、D 与静态批处理的低效核心无关。

Q2. 连续批处理在什么粒度上进行动态调度?

单选题 · easy

  • A.请求级别
  • B.批次级别
  • C.迭代级别← 正确答案
  • D.会话级别

答案:C。连续批处理的核心是 iteration-level scheduling,即每生成一个 token(每次迭代)后就检查一次调度机会,让新请求随时入座、完成的请求随时离席,而不是等整个 batch 结束。

Q3. 大模型生成文本时遇到的 EOS 标记表示什么?

单选题 · easy

  • A.请求刚到达系统
  • B.预填充阶段开始
  • C.序列生成到此结束← 正确答案
  • D.显存缓存已写满

答案:C。EOS(End of Sequence)是模型训练时学会的终止符,一旦生成 EOS 或达到 max_tokens,请求就应立刻停止生成并释放资源。这是动态退出机制的重要判定条件。

Q4. 新请求进入系统后,首先被放入哪个位置?

单选题 · mid

  • A.运行中的 GPU 槽位
  • B.等待队列 waiting 队列← 正确答案
  • C.KV 缓存池
  • D.直接返回错误给用户

答案:B。新请求到达后先进入 waiting 队列“取号”,调度器在每个迭代结束时检查空槽,再按策略从 waiting 队列中取出请求进入 GPU 槽位执行 prefill。这与快餐店客人先取号等位、有空桌再入座完全一致。

Q5. TTFT 指标衡量的是什么时间?

单选题 · mid

  • A.请求从开始到完成的全部耗时
  • B.用户发出请求到收到首个 token 的延迟← 正确答案
  • C.相邻两个 token 之间的生成间隔
  • D.请求在等待队列中的排队时长

答案:B。TTFT(Time To First Token)是从请求到达时刻到第一个生成 token 返回给用户的耗时,对应“客人从进门到吃上第一口菜”的等待,是聊天体验最敏感的指标之一。

Q6. 连续批处理提升系统性能的根本原因是什么?

单选题 · mid

  • A.增大了模型的参数量
  • B.减少了 GPU 等待空转的时间← 正确答案
  • C.降低了每个请求的计算量
  • D.将模型压缩到更小的体积

答案:B。连续批处理通过动态加入与退出机制,消除了静态批处理中“等客齐”和“等最慢”两种典型空转,让 GPU 算力持续运转,同时优化了吞吐和排队延迟。

Q7. 请求动态退出后,系统第一时间要做的事是什么?

单选题 · mid

  • A.把请求重新放回等待队列
  • B.释放其占用的 KV 缓存槽位← 正确答案
  • C.将整个 GPU 显存清空
  • D.等待下一批请求统一回收

答案:B。请求生成终止后,系统立即释放它占用的 KV 缓存槽位,让等待队列中的新请求马上复用这块显存。这对应快餐店“客人起身、服务员立刻收桌”,而不是等所有桌统一翻台。

Q8. 以下哪些属于连续批处理中的常见调度策略?

多选题 · mid

  • A.先到先服务(FCFS)← 正确答案
  • B.攒满固定批次再运行
  • C.优先服务等待最久的请求← 正确答案
  • D.用抢占方式插入新请求← 正确答案

答案:ACD。连续批处理的调度策略包括 FCFS、最长等待优先(防止饿死)、以及通过抢占机制为新请求的 prefill 腾出算力。B 是静态批处理的攒批逻辑,与连续批处理背道而驰。

Q9. 以下哪些场景特征适合采用连续批处理?

多选题 · mid

  • A.请求到达时间高度不均匀← 正确答案
  • B.长短请求混合在一起服务← 正确答案
  • C.所有请求耗时均匀且恒定
  • D.高并发场景下的流式输出← 正确答案

答案:ABD。负载不均、长短混合、高并发流式输出都是连续批处理的优势场景。C 恰恰是静态批处理也能很好工作的场景,连续批处理在均匀负载下收益有限。

Q10. 关于迭代级调度的时机,以下说法正确的是?

单选题 · hard

  • A.每生成一个词元后都可检查并调度← 正确答案
  • B.仅在请求全部结束后统一调度
  • C.仅由用户在 API 请求中显式触发
  • D.只在服务器部署重启时调度一次

答案:A。迭代级调度的时间颗粒度等于一个 token 的生成时间,调度器在每次迭代结束后都可以检查完成与等待队列,从而实现动态加入和退出。B、C、D 描述的都是静态或一次性调度,不符合连续批处理的设计。