4、动态批处理核心概念:请求队列、批大小、调度窗口、最大延迟约束、批处理粒度、连续批处理与级联批处理

动态批处理(Dynamic Batching)是大模型推理服务中平衡吞吐量与延迟的关键机制。与静态批处理(固定batch size、固定调度周期)不同,动态批处理需要根据实时到达的请求流、GPU显存余量、各请求的优先级与deadline,持续调整批的构成与调度时机。要真正掌握动态批处理,必须首先厘清六个核心概念:请求队列、批大小、调度窗口、最大延迟约束、批处理粒度、连续批处理与级联批处理。这六个概念并非孤立存在,而是构成了一个从“请求到达”到“结果返回”的完整决策链路。本章将逐一拆解其定义、作用、权衡关系及工程落地方法,并给出可操作的判断流程与实战案例。

动态批处理核心概念关系与调度决策链路 请求队列 FIFO / 优先级 / 超时标记 队列深度监控 排队延迟预估 批大小 (Batch Size) GPU显存上限约束 吞吐 vs 延迟权衡 动态伸缩策略 调度窗口 窗口长度 (ms级) 最大等待时间 窗口内请求聚合 最大延迟约束 SLO / P99延迟目标 deadline 优先级 抢占与丢弃策略 批处理粒度 请求级 / Token级 序列级 / 微批 粒度对效率的影响 连续批处理 iteration-level调度 请求动态加入/退出 无等待批填充 级联批处理 Prefill / Decode 分离 多阶段流水线 跨阶段批传递 调度器在每个窗口内综合队列状态、显存余量、延迟约束,决定批大小与批处理粒度

上图展示了六个核心概念之间的逻辑关系:请求队列是输入源,批大小与调度窗口是调度器的两个主要调节旋钮,最大延迟约束是硬性边界条件,批处理粒度决定了优化的精细程度,而连续批处理与级联批处理则是两种高级调度范式。下面逐一深入展开。

4.1 请求队列:动态批处理的输入缓冲与排序中枢

4.1.1 核心原理

请求队列是动态批处理系统的第一道关卡。所有到达的推理请求首先进入队列,等待调度器决定何时将其加入批次。队列不仅起到缓冲作用,更是调度器获取全局信息的窗口——通过队列深度、请求等待时长、请求优先级分布,调度器才能做出合理的批构建决策。在实际系统中,请求队列通常采用多级队列结构:一级队列接收原始请求,二级队列按优先级或业务类型分流,三级队列用于存放已超时或即将超时的紧急请求。

4.1.2 判断方法与操作流程

构建请求队列时,需要明确以下决策点:

  • 队列容量上限:设置最大排队请求数,超过后触发拒绝策略(如直接返回503或丢弃最老请求)。
  • 优先级策略:支持按用户等级、业务重要性或deadline紧迫度进行排序,而非单纯FIFO。
  • 超时标记:为每个请求记录入队时间,当等待时间超过阈值时,将其标记为“紧急”或直接移出队列单独处理。
  • 队列深度监控:实时暴露队列长度、平均等待时间、超时率等指标,供调度器参考。
// 请求队列核心数据结构示例
struct Request {
    int id;
    int priority;          // 0-10,越高越紧急
    int max_latency_ms;    // 最大可接受延迟
    int arrival_time_ms;   // 入队时间戳
    int tokens_estimate;   // 预估输出token数
    bool is_urgent;        // 是否已接近deadline
};
// 队列按优先级+等待时间综合排序
bool compare(Request a, Request b) {
    if (a.is_urgent != b.is_urgent) return a.is_urgent;
    if (a.priority != b.priority) return a.priority > b.priority;
    return a.arrival_time_ms < b.arrival_time_ms;
}

4.1.3 案例拆解:电商大促场景下的队列压力

某电商平台在大促期间,AI推荐服务的请求量从平时2k QPS飙升至15k QPS。初始系统采用简单FIFO队列,导致两个问题:一是部分高价值用户的请求被大量普通请求阻塞,平均延迟从80ms恶化到450ms;二是部分请求在队列中等待超过2秒,直接超时。分析后发现,队列深度达到5万,但其中约30%是低优先级的日志分析请求。优化方案:将队列拆分为高、中、低三个优先级子队列,高优先级队列容量限制为5000,低优先级队列容量限制为20000;调度器每50ms检查一次高优先级队列,确保其请求等待时间不超过200ms。实施后,高优先级请求P99延迟从450ms降至120ms,整体超时率从8%降至0.5%。代价是低优先级请求的延迟从平均300ms升至800ms,但业务方确认可接受。

风险提示:队列优先级设计不当会导致“饿死”现象——低优先级请求永远得不到调度。必须设置低优先级队列的最大等待时间上限,超过后强制提升优先级。

4.2 批大小与调度窗口:吞吐与延迟的平衡杠杆

4.2.1 核心原理

批大小(Batch Size)决定了一次前向推理同时处理的请求数量。批大小越大,GPU利用率越高,单位token的算力成本越低,但单次推理的延迟也会增加(因为需要等待所有请求的输入就绪,且计算量增大)。调度窗口(Scheduling Window)是调度器收集请求的时间范围——窗口越长,能聚合到的请求越多,批大小可以更大,但请求的排队延迟也越长。两者本质上是同一枚硬币的两面:窗口长度决定了“能凑到多少请求”,批大小决定了“一次处理多少请求”。

4.2.2 判断方法与操作流程

确定最优批大小与调度窗口,需要遵循以下步骤:

  1. 测量GPU显存上限:通过实验确定当前模型在给定显存下能支持的最大batch size(例如A100 80GB上,LLaMA-7B FP16最大batch约128)。
  2. 设定延迟SLO:明确业务要求的P99延迟上限(例如500ms)。
  3. 建立延迟-批大小模型:通过压测获得不同batch size下的平均推理延迟,拟合曲线。
  4. 动态调整窗口:根据当前请求到达速率,动态调整窗口长度。请求速率高时缩短窗口(如20ms),请求速率低时延长窗口(如100ms),但窗口上限受SLO约束。
请求速率 (QPS)推荐窗口长度预期批大小P99延迟GPU利用率
100100ms10150ms45%
50050ms25220ms72%
200020ms40350ms88%
500010ms50480ms95%

4.2.3 实战场景:视频理解服务的窗口调优

某视频理解服务需要处理多路视频流,每路视频每秒产生5帧分析请求。初始配置固定窗口50ms,批大小固定32。实际运行中发现:白天高峰期请求速率达到3000 QPS,窗口内能凑满32个请求,但P99延迟达到600ms,超过500ms的SLO;夜间低峰期请求速率仅200 QPS,窗口内平均只有10个请求,批大小远未饱和,GPU利用率仅30%。优化方案:将窗口改为动态调整——当队列深度超过50时,窗口缩短至15ms;当队列深度低于10时,窗口延长至120ms。同时引入批大小上限动态调整:当P99延迟接近SLO时,自动将最大批大小从32降至24。实施后,高峰期P99延迟稳定在450ms,低峰期GPU利用率提升至55%。

经验提醒:调度窗口不宜设置过短(<5ms),否则调度器自身开销会占据较大比例;也不宜过长(>200ms),否则交互式请求的体验会明显劣化。

4.3 最大延迟约束与批处理粒度

4.3.1 核心原理

最大延迟约束(Max Latency Constraint)是动态批处理的“硬边界”。每个请求都有其允许的最大端到端延迟(从入队到返回结果),调度器必须在约束内完成调度与推理。批处理粒度(Batching Granularity)则决定了调度的最小单元:请求级粒度(整个请求作为一个整体参与批处理)、Token级粒度(将请求拆分为多个token,不同请求的token可以混合在一个批次中)、序列级粒度(按序列长度分组)。粒度越细,调度灵活性越高,但调度开销也越大。

4.3.2 判断方法与操作流程

设置最大延迟约束时,需要区分硬约束与软约束:

  • 硬约束:超过即失败,必须保证。例如金融风控请求要求P99=200ms,超过则直接返回失败。
  • 软约束:超过会影响体验但不致命。例如聊天机器人要求P99=2s,超过则降低回复质量。

选择批处理粒度时,遵循以下原则:

  1. 如果请求的输入长度差异不大(如短文本分类),采用请求级粒度即可。
  2. 如果请求的输入长度差异巨大(如文档摘要vs短句翻译),采用Token级粒度能显著减少padding浪费。
  3. 如果模型支持prefix caching(如KV cache复用),序列级粒度更有利于缓存命中。

4.3.3 案例拆解:混合负载下的粒度选择

某企业级LLM网关同时承载三类请求:代码生成(平均输入2000 token,输出500 token)、文本摘要(平均输入5000 token,输出300 token)、对话问答(平均输入200 token,输出200 token)。初始采用请求级批处理,batch size=16时,由于输入长度差异大,实际有效计算量仅占理论值的55%(大量padding)。优化方案:改用Token级批处理,将每个请求拆分为固定长度的token块(如512 token),不同请求的token块混合组成批次。调度器优先填充当前批次中未满的token块,当某个请求的所有token块处理完毕,立即将其从批次中移除并返回结果。实施后,有效计算量提升至82%,整体吞吐量提升1.6倍,P99延迟从800ms降至520ms。代价是调度器复杂度显著增加,需要维护每个请求的token块状态。

关键结论:批处理粒度越细,GPU利用率越高,但调度器需要处理的状态量也越多。当请求长度方差较大时,Token级粒度的收益通常能覆盖调度开销;当请求长度均匀时,请求级粒度更简单高效。

4.4 连续批处理与级联批处理

4.4.1 核心原理

连续批处理(Continuous Batching)是指调度器在每个iteration(一次前向计算)结束后,立即将已完成的请求移出批次,并将新到达的请求加入批次,无需等待整个批次全部完成。级联批处理(Cascaded Batching)则是将推理过程拆分为多个阶段(如prefill阶段和decode阶段),每个阶段独立进行批处理,阶段之间通过中间结果传递。连续批处理解决的是“批内请求完成时间不一致”的问题,级联批处理解决的是“不同阶段计算特性差异大”的问题。

4.4.2 判断方法与操作流程

实施连续批处理的步骤:

  1. 将推理循环改为iteration级调度:每个iteration结束后,检查批次中是否有请求已完成生成。
  2. 将已完成请求的结果返回,并释放其占用的显存(KV cache)。
  3. 从请求队列中取出新请求,加入批次(需重新计算显存余量)。
  4. 重复上述过程直到所有请求处理完毕。

实施级联批处理的步骤:

  1. 将推理过程拆分为prefill(处理输入token)和decode(逐token生成)两个阶段。
  2. 为prefill和decode分别维护独立的批次,prefill批次可以较大(因为计算密集),decode批次可以较小(因为访存密集)。
  3. prefill阶段完成后,将KV cache传递给decode阶段,decode阶段按连续批处理方式运行。

4.4.3 实战场景:LLM服务从静态批处理到连续批处理

某LLM API服务最初采用静态批处理:每200ms收集一批请求,batch size=32,整批推理完成后统一返回。问题:当32个请求中有一个生成长度特别长(如1000 token),其他请求只有50 token时,所有请求都要等待最慢的请求完成,导致P99延迟高达3秒。优化方案:改为连续批处理。每个iteration(约10ms)结束后,检查批次中每个请求是否已生成结束token(EOS)。已完成的请求立即返回,新请求从队列中补入。实施后,短请求的平均延迟从2.5秒降至0.8秒,P99延迟从3秒降至1.2秒。进一步优化:将prefill和decode分离,prefill阶段使用batch size=64(因为prefill计算密集,可以充分利用GPU),decode阶段使用batch size=16(因为decode访存密集,过大batch会导致显存带宽瓶颈)。实施后,整体吞吐量再提升40%。

方案P99延迟吞吐量 (req/s)GPU利用率实现复杂度
静态批处理3000ms16065%低
连续批处理1200ms28082%中
连续+级联批处理950ms39091%高
风险提示:连续批处理会引入“批内请求状态不一致”的问题——不同请求可能处于不同的生成阶段(有的刚加入,有的即将结束)。这要求推理引擎支持动态shape和动态KV cache管理,否则可能因内存碎片化导致显存利用率下降。

4.5 常见误区与总结复盘

在动态批处理的实际落地中,以下误区需要特别警惕:

  • 误区一:批大小越大越好。当batch size超过某个阈值后,GPU利用率增长趋缓,但延迟线性增加。必须找到“拐点”。
  • 误区二:调度窗口固定不变。请求速率是波动的,固定窗口要么在高峰期造成队列积压,要么在低峰期浪费GPU资源。
  • 误区三:忽略请求长度分布。如果只关注batch size而不关注批内请求的长度分布,padding浪费可能高达50%。
  • 误区四:将连续批处理与级联批处理对立。两者是互补关系,连续批处理解决“批内动态性”,级联批处理解决“阶段间差异性”,可以同时使用。

总结本章核心要点:动态批处理的核心是“在约束下做最优决策”——请求队列提供输入视图,批大小与调度窗口是调节手段,最大延迟约束是边界条件,批处理粒度决定优化精度,连续批处理与级联批处理是两种高级范式。实际部署时,建议先以请求级粒度+固定窗口起步,测量延迟与吞吐基线,再逐步引入Token级粒度和连续批处理,最后根据业务场景决定是否需要级联批处理。每一步优化都应以上线前的压测数据为依据,避免盲目追求复杂方案。