☰
【Runtime推理引擎⑤】动态Batch Scheduler:吞吐、延迟与公平的三角平衡
2026/11/5 6:40:58 网站建设 项目流程

【Runtime推理引擎⑤】动态Batch Scheduler:吞吐、延迟与公平的三角平衡

本文是「Runtime推理引擎」系列的第五篇。很多人以为动态Batch就是"把请求凑成一批一起跑",这只说了皮毛。真正的动态Batch Scheduler是整个推理引擎的"大脑"——它决定哪些请求同批、何时发射、谁先谁后、显存不够时谁让谁。今天我们就来拆解这个三角平衡的艺术:吞吐要高、延迟要低、还要公平。


一、开篇:动态Batch不只是"凑请求",而是资源调度的核心

先问一个灵魂拷问:为什么GPU推理需要Batch?

答案很朴素——GPU太大了,单个请求喂不饱。一张A100有几千个CUDA核心,你只丢一个请求进去,就像让一个万人体育场只坐一个观众——大部分座位都是空的,算力白白浪费。

所以Batch的本质是:把多个请求打包起来,让GPU一次处理更多数据,摊薄固定开销,提升利用率。

但问题来了,线上请求是"乱七八糟"来的:

  • 有的用户问"你好",只有2个token
  • 有的用户贴了一整篇文章,有2000个token
  • 有的要生成100字回答,有的要写500字长文
  • 有的是付费VIP要秒回,有的是免费用户可以稍等

如果用最简单的"固定Batch"策略——凑够8个请求就一起跑,会发生什么?


二、为什么需要动态Batch?固定Batch的三大痛点

2.1 痛点一:Padding浪费算力

固定Batch里,所有请求要pad到同一长度才能一起算。假设一个batch里有7个短请求(各100 token)和1个长请求(1000 token),那短请求的900个pad token全是白算的——计算量浪费了90%。

固定Batch(按最长padding): 请求1: [token1, token2, ..., token100, PAD, PAD, ..., PAD] ← 900个PAD 请求2: [token1, token2, ..., token100, PAD, PAD, ..., PAD] ← 900个PAD ... 请求8: [token1, token2, ..., token100, ..., token1000] ← 正常

2.2 痛点二:拖尾效应(Straggler)

Batch内所有请求必须"齐步走"——最短的请求算完了也不能走,得等最长的那个一起结束。短请求白白占着位置,新请求也进不来。

就像一群人去吃饭,有人吃完了也不能走,得等最慢的那个人吃完才能一起买单——既浪费桌子,也饿了门外等着的人。

2.3 痛点三:响应慢、体验差

新请求到达时,如果当前batch还没跑完,只能干等。假设batch_size=8,一个iteration要50ms,那新请求最坏情况下要等将近50ms才能开始算——TTFT(首token延迟)直接飙高。

💡动态Batch的核心思想:以 iteration/token 为粒度调度,而不是以 request 为粒度调度。

每个iteration结束后,完成的请求立刻"下车",新请求立刻"上车"。GPU几乎不闲着,短请求不被长请求拖死。这就是 Continuous Batching / Dynamic Batch 的本质。


三、动态批处理策略:四种主流调度算法

Scheduler的核心决策是:下一个iteration,哪些请求进batch?

不同策略有不同的取舍,我们一个一个说。

3.1 FCFS:先到先服务(最简单也最公平)

按到达时间排队,能进batch就进,不能就等着。

这是最朴素的策略——就像银行叫号,谁先来谁先办。

  • 优点:实现简单、绝对公平、不会有请求饿死
  • 缺点:长请求占位置,短请求跟着等;对VIP用户不友好

适合场景:请求长度差异不大、没有QoS等级区分的简单服务。

3.2 SJF:短请求优先(吞吐最优但有风险)

优先挑预计成本最低的请求进batch。

SJF(Shortest Job First)的思路很直观——先把快的都处理完,平均等待时间最低。

  • 优点:平均等待时间最短,整体吞吐最高
  • 缺点:长请求可能永远轮不上,出现"饥饿"现象

想象一下,如果不断有短请求进来,那个长请求就一直被插队……用户可能等了半小时还没收到回复。

💡SJF必须配Aging机制,否则线上一定会出事故。Aging我们后面单独讲。

3.3 Priority/QoS:优先级调度(业务导向)

高优先级请求优先进batch,必要时抢占低优先级。

真实业务一定有优先级:

  • P0:线上主链路,SLA要求p95 < 500ms
  • P1:内部工具,秒级响应即可
  • P2:离线批量,能跑就行

Priority调度就是按等级来——高优先级的先服务,实在不行还能把低优先级的"挤下去"(抢占)。

  • 优点:保障核心业务SLA
  • 缺点:实现复杂,要处理抢占成本、低优先级饥饿、优先级反转

3.4 Cost-aware:成本感知(最接近真实)

比SJF更通用的策略——不按到达时间排,也不只看token数,而是综合估算请求的"成本":

cost ≈ prefill_tokens × prefill_cost + expected_decode_tokens × decode_cost + kv_blocks × kv_pressure

为什么要这么复杂?因为不同阶段的成本不一样:

  • Prefill阶段:计算密集,时间和prompt长度正相关
  • Decode阶段:访存密集,每个token一个step
  • KV Cache:占用显存,影响并发数

Cost-aware更接近真实资源消耗,但估算本身有误差——你怎么知道用户要生成多少token?生产上常用"估计成本 + aging + deadline"的折中方案。


四、请求队列管理:四条队列与入队条件

一个生产级的Scheduler,至少要维护四条队列:

classScheduler:def__init__(self):self.waiting_queue=[]# 已到达但还没进batchself.running_queue=[]# 正在batch中执行self.blocked_queue=[]# 因显存/KV/优先级暂时不能执行self.preempted_queue=[]# 被抢占,等待恢复或重算

4.1 入Batch条件:不止看request count

小白最容易犯的错误:判断能不能入batch只看len(batch) < max_batch_size。

大错特错!8个2000token的长请求,和64个32token的短请求,资源消耗完全不在一个量级。

正确的入batch条件应该是这样的:

defcan_add_to_batch(batch,request,token_budget,kv_blocks_free):# 1. 请求数不能超过上限iflen(batch)>=MAX_NUM_SEQS:returnFalse# 2. token总量不能超过预算(比request count更重要!)current_tokens=sum(req.total_tokensforreqinbatch)ifcurrent_tokens+request.total_tokens>token_budget:returnFalse# 3. KV blocks 够不够ifrequest.kv_blocks_needed>kv_blocks_free:returnFalse# 4. workspace 够不够(prefill时的临时显存)ifrequest.workspace_needed>workspace_free:returnFalse# 5. QoS / deadline 允许ifnotcheck_qos_allowed(request):returnFalsereturnTrue

💡记住:token budget 和 KV blocks 比 request count 更能反映batch的真实成本。

4.2 Aging:防饥饿的关键机制

只要用了SJF、Priority或Cost-aware,就必须配Aging,否则长请求和低优先级请求会饿死。

Aging的原理很简单——等得越久,优先级越高:

defeffective_priority(request,current_time):wait_time=current_time-request.arrival_timereturnrequest.base_priority+ALPHA*wait_time

这里的ALPHA是个关键参数:

  • 太小:没效果,长请求照样饿死
  • 太大:新的高优先级请求会被等了很久的低优先级请求挤占,失去QoS意义

生产上这个参数不能拍脑袋,要根据线上请求分布(长请求比例、到达速率)来调。


五、负载均衡与资源利用率:Batch不是越大越好

很多人直觉:batch越大,GPU利用率越高,吞吐越高。

对,但不全对。batch太大反而会有副作用:

5.1 什么时候batch太大是坏事?

  1. 短请求延迟变差:batch越大,单iteration时间越长,短请求的TPOT(每token延迟)会被拖慢
  2. KV Cache占满:太多请求占着KV blocks,新请求进不来,反而排队
  3. Prefill拖垮Decode:长prompt占prefill时间长,decode请求在旁边等着
  4. GPU已饱和:GPU利用率已经100%了,再加batch只会增加队列长度,不会增加吞吐

5.2 抢占与重算:显存不足时的四套策略

当KV blocks不够、workspace不够、显存快OOM时,Scheduler怎么办?

策略做法代价
Swap out把低优先级请求的KV Cache换到CPU内存CPU内存占用高,换回时有延迟
Recompute丢弃低优先级的中间状态,恢复时重算额外GPU计算,恢复慢
Defer不让新请求进batch,先让现有请求跑完新请求等待变长
Reject直接拒绝低优先级/非SLA请求业务可见失败,影响体验

生产上不要只问"能不能抢占",还要问:抢占之后,p95延迟会不会更差?比如你把一个快算完的请求抢占了,等它恢复的时候又得重算半天,反而更慢。


六、生产调度器必看的核心指标

没有指标的Scheduler就是瞎子摸象。一个可运维的Dynamic Batch Scheduler,至少要暴露这些指标:

# 基础指标waiting_queue_len# 等待队列长度running_batch_size# 当前batch大小preempted_count# 被抢占的请求数# 利用率指标batch_occupancy=running_seqs/max_num_seqs# batch填充率token_budget_usage# token预算使用率kv_block_usage=kv_used/kv_total# KV块使用率# 延迟指标avg_queue_wait/p95_queue_wait# 排队等待时间avg_ttft/p95_ttft# 首token延迟avg_tpot/p95_tpot# 每token延迟# 异常指标preemption_count# 抢占次数recompute_count# 重算次数swap_bytes# 交换的数据量starvation_count# 等待超时的请求数(防饥饿告警用)

举个例子,如果batch_occupancy很高但token_budget_usage很低,说明什么?

——说明batch里全是短请求,虽然请求数多,但总token量少,GPU没吃饱。这时候可以考虑放宽max_num_seqs,或者把token budget设得更合理。

再比如,如果starvation_count持续上升,说明什么?

——Aging参数太小了,或者高优先级请求太多,低优先级的一直轮不上。赶紧调参!


七、小结与思考

小结

今天我们把动态Batch Scheduler的核心内容梳理了一遍:

  1. 动态Batch的本质:以token/iteration为粒度调度,不是以request为粒度,让GPU持续高负荷运转
  2. 四种调度策略:FCFS(公平简单)、SJF(吞吐最优但易饥饿)、Priority(业务QoS导向)、Cost-aware(最接近真实成本)
  3. 队列管理是核心:waiting/running/blocked/preempted四条队列,入batch要看token budget和KV blocks,而不只是请求数
  4. Aging是必选项:只要不是纯FCFS,就必须配Aging防饥饿
  5. Batch不是越大越好:太大反而拖慢短请求、占满KV Cache、降低体验
  6. 指标驱动调参:batch_occupancy、token_budget_usage、kv_block_usage、queue_wait这些指标缺一不可

下一篇预告:我们来讲Runtime特性开发的实战——从IO契约到多租户隔离,正确的开发顺序是什么?为什么说"先做性能优化是找死"?


思考问题

  1. 为什么长prompt请求多的时候,单纯提高max_batch_size反而可能让TTFT(首token延迟)更差?
  2. 设计一个Cost-aware的成本公式时,你会怎么估计expected_decode_tokens?有哪些信号可以利用?
  3. Aging的α参数太大或太小,分别会导致什么线上现象?如何排查?
  4. KV blocks不足时,swap、recompute、defer、reject四种策略,应该如何按业务优先级选择?
  5. 如果高优先级请求到达时batch已满,你会选择插队、抢占,还是另开一个高优先级小batch?为什么?

📚本系列导航:

  • 第1篇:Runtime架构设计:从Session到Executor的全链路
  • 第2篇:内存池设计:碎片化、分配策略与多池隔离
  • 第3篇:CUDA Graph:capture/replay原理与接入实战
  • 第4篇:多流并发执行:计算与通信的流水重叠
  • 第5篇:动态Batch Scheduler:吞吐、延迟与公平的三角平衡(本文)
  • 第6篇:Runtime特性开发实战:从IO契约到多租户隔离
  • 更多内容持续更新中……

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询