3、静态批处理与动态批处理:静态批处理的局限、动态批处理的核心思想与优势

在大模型推理服务化部署的演进过程中,批处理(Batching)策略是决定GPU利用率与请求延迟的关键机制。早期的推理框架普遍采用静态批处理(Static Batching),即预先固定一个batch大小,将多个请求凑满一个批次后统一交给GPU执行。这种策略在CV模型或传统DNN模型中表现尚可,但在以Transformer架构为主的大语言模型(LLM)推理场景中,静态批处理暴露出严重的算力浪费问题。vLLM之所以能在业界获得广泛认可,核心突破之一便是引入了基于连续批处理(Continuous Batching)思想的动态批处理机制。本章将深入剖析静态批处理的局限性,阐明动态批处理的核心思想、实现优势,并通过一个具体的工程案例帮助读者建立从理论到实践的完整认知。

静态批处理(Static Batching) Req A Req B Req C 等待中 凑满 Batch=4 才执行 必须等待最慢请求 GPU 执行批次 A、B、C 同步前向传播 所有序列长度固定为 max_len A 完成 B 完成 C 仍在生成 GPU 资源被空转占用 新请求 D 必须等待整个批次结束 动态批处理(Continuous Batching) Req A Req B Req C Req D Req E 迭代级调度器(Iteration-level Scheduler) 每个 Decode 步骤动态决定哪些序列参与计算 每步动态插入/退出 GPU 执行(Step t) A(生成中) + B(生成中) + D(新加入) C 已提前退出,E 等待下个 Step A 完成退出 B 继续生成 D 继续生成 GPU 始终被有效请求占满 新请求可在任意 Step 无缝加入 静态批处理:以“请求”为粒度,同步等待;动态批处理:以“迭代步”为粒度,流水线式调度

3.1 静态批处理的局限:从请求粒度到GPU空转

3.1.1 核心原理:请求级同步等待

静态批处理的工作模式可以概括为“凑批—执行—同步释放”。推理服务维护一个请求队列,当队列中的请求数量达到预设的batch size(例如4、8或16)时,框架将这些请求打包成一个批次,一次性送入GPU执行。在Transformer模型的推理过程中,一个批次内的所有序列必须共享相同的序列长度(通常padding到该批次的最大长度),并且前向传播的每一步(Prefill和Decode)都是同步进行的。这意味着,如果批次中有3个请求已经生成了结束符(EOS),而第4个请求还在继续生成token,那么前3个请求所占用的GPU显存和计算资源必须继续被占用,直到整个批次全部完成。

3.1.2 三大核心局限

局限维度 具体表现 对延迟/吞吐的影响
GPU利用率低下 已完成的序列无法释放显存,新请求无法插入,GPU计算单元在Decode阶段大量空闲 吞吐量(Tokens/s)显著下降,GPU算力浪费可达40%-60%
尾延迟(Tail Latency)恶化 一个长序列请求会拖慢整个批次,其他短请求必须等待其全部生成完毕 P99延迟急剧升高,用户体验不稳定
调度僵化 无法应对请求到达速率的不均匀性;请求少时batch凑不满,请求多时排队严重 服务吞吐波动大,资源规划困难

3.1.3 案例拆解:一个典型的静态批处理困境

假设某团队部署了一个基于静态批处理的LLM服务,batch size设置为4。某一时刻,GPU正在处理4个请求:A(需要生成200个token)、B(需要生成50个token)、C(需要生成30个token)、D(需要生成10个token)。由于静态批处理的同步特性,当B、C、D分别在生成完50、30、10个token后已经满足用户需求,但它们无法退出批次。GPU必须继续为这4个请求同时执行前向计算,直到A生成完200个token。在这个过程中,B、C、D的显存占用无法释放,新的请求E、F只能排队等待。最终,整个批次的完成时间由最慢的A决定,而GPU在最后100个token的生成阶段,实际上只有A在产生有效输出,其余3个请求的计算完全属于空转。

风险提示:静态批处理在长尾延迟场景下尤为致命。当批次中混有长对话和短对话时,短请求的响应时间会被拉长数倍,导致SLA(服务等级协议)违约。此外,静态批处理对显存预留要求较高,因为每个请求都需要按最大序列长度预留KV Cache空间,进一步加剧了显存浪费。

3.2 动态批处理的核心思想:迭代级调度

3.2.1 核心原理:从“请求级”到“迭代级”

动态批处理(Continuous Batching)的核心思想是将调度粒度从“整个请求”细化为“单个Decode迭代步”。在vLLM的实现中,调度器在每个Transformer解码步骤(Step)开始时,都会重新评估当前所有活跃序列的状态。如果一个序列已经生成了EOS token,它会在当前Step结束后立即从批次中移除,其占用的GPU显存(包括KV Cache)被立即释放。与此同时,调度器可以从等待队列中选取新的请求,将其Prefill计算插入到下一个Step中。这种机制使得GPU在每个Step上都能处理尽可能多的有效序列,实现了真正的“流水线式”计算。

3.2.2 操作流程:动态批处理的调度循环

  1. 状态检查:在每个Step开始时,遍历当前批次中的所有序列,标记已完成(EOS)或达到最大长度的序列。
  2. 显存回收:将已完成序列的KV Cache从物理显存中释放,更新显存分配器(vLLM使用PagedAttention管理KV Cache,支持按需分配和回收)。
  3. 新请求准入:检查等待队列,根据当前可用显存和GPU算力,决定是否将新请求的Prefill计算加入本Step。
  4. 混合执行:在一个Step内,同时执行新请求的Prefill计算和已有序列的Decode计算(vLLM支持Prefill与Decode的混合批处理)。
  5. 迭代循环:重复上述步骤,直到所有请求处理完毕。

3.2.3 动态批处理的核心优势

  • 吞吐量大幅提升:由于GPU始终在处理有效请求,整体吞吐量(Tokens/s)可提升2-3倍。vLLM官方测试显示,在相同硬件条件下,动态批处理比静态批处理吞吐量提升最高可达23倍(在长序列场景下)。
  • 尾延迟显著降低:短请求不再被长请求阻塞,P99延迟可降低一个数量级。每个请求的完成时间只取决于其自身的生成长度和GPU当前的负载。
  • 显存利用率优化:结合PagedAttention技术,KV Cache不再需要连续的大块显存,而是按需分页分配,显存碎片化问题得到有效缓解。
  • 服务稳定性增强:动态批处理天然适应请求到达速率的变化,无需人工调整batch size参数。
经验之谈:动态批处理并非简单的“来一个处理一个”,而是通过迭代级调度器在“吞吐最大化”和“延迟最小化”之间寻找平衡。在实际部署中,vLLM还引入了抢占(Preemption)机制——当显存不足时,会暂停某些序列的Decode,优先保证新请求的Prefill,从而避免显存溢出。

3.3 实战案例:从静态批处理迁移到vLLM动态批处理

3.3.1 背景与初始现象

某互联网公司内部有一个基于自研静态批处理框架的代码生成服务,使用2张NVIDIA A100(80GB)GPU。业务方反馈:在高峰期(并发请求约50个),服务的平均响应时间达到8秒,P99响应时间超过20秒,且GPU利用率仅为35%左右。通过监控发现,大量请求在排队等待凑批,而GPU在Decode阶段存在明显的计算空转。

3.3.2 关键数据与问题分析

指标 静态批处理(优化前) vLLM动态批处理(优化后) 变化
GPU利用率 35% 78% ↑ 122%
平均响应时间 8.2s 2.4s ↓ 70.7%
P99响应时间 21.5s 4.8s ↓ 77.7%
吞吐量(Tokens/s) 1,200 3,850 ↑ 220%

3.3.3 迁移操作步骤

  1. 环境准备:使用vLLM 0.4.2版本,通过pip安装,并下载对应的模型权重(该团队使用的是CodeLlama-13B)。
  2. 启动服务:使用vLLM的OpenAI兼容API启动服务,关键参数配置如下:
python -m vllm.entrypoints.openai.api_server \
    --model /data/models/codellama-13b-hf \
    --tensor-parallel-size 2 \
    --gpu-memory-utilization 0.90 \
    --max-num-seqs 32 \
    --max-model-len 4096 \
    --swap-space 16
  1. 参数调优:将max-num-seqs从默认值调至32,以允许更多序列同时参与动态批处理;gpu-memory-utilization设为0.90,确保KV Cache有足够的显存空间。
  2. 客户端改造:业务代码只需将API endpoint从原来的自研接口切换为vLLM的/v1/completions接口,请求格式兼容OpenAI规范。
  3. 灰度验证:先切10%流量,观察P99延迟和GPU利用率稳定后,逐步放量至100%。

3.3.4 结果对比与复盘

迁移后,GPU利用率从35%提升至78%,平均响应时间从8.2秒降至2.4秒。更重要的是,在高峰期并发请求达到80个时,vLLM的动态批处理依然能保持稳定的P99延迟(4.8秒),而旧框架在并发超过60个时P99延迟已飙升至30秒以上。复盘发现,旧框架的瓶颈在于静态批处理导致的“长尾阻塞”,而vLLM的迭代级调度让短请求能够快速完成并释放资源,从而大幅提升了整体吞吐。

调优建议:如果业务场景中长序列请求占比高,建议适当调大max-num-seqs并增加swap-space,以允许更多序列在显存和CPU内存之间进行换入换出。如果追求极致的低延迟,可以设置--enforce-eager模式减少CUDA graph的捕获开销,但会牺牲部分吞吐。

3.4 常见误区与总结复盘

3.4.1 误区一:动态批处理就是“来一个处理一个”

这是最常见的误解。动态批处理仍然会积累一定数量的请求后才开始执行,只是调度粒度从“整个请求”变成了“单个Decode步骤”。vLLM的调度器会综合考虑当前显存余量、等待队列长度、序列的生成进度等因素,决定每个Step执行哪些序列。盲目追求“零等待”反而会导致GPU在每个Step上处理的序列过少,降低吞吐。

3.4.2 误区二:动态批处理不需要配置batch size

虽然不需要手动设置固定的batch size,但max-num-seqs参数实际上就是动态批处理的上限。如果该值设置过小,GPU无法充分利用;设置过大,则可能导致显存溢出(OOM)。建议根据模型大小和GPU显存进行估算:max-num-seqs ≈ 可用显存 / (平均序列长度 × KV Cache每token占用)。

3.4.3 误区三:动态批处理只对长序列场景有效

实际上,动态批处理在短序列场景下同样有效。因为短序列的生成速度快,静态批处理中频繁的“凑批—执行—释放”循环会导致大量调度开销;而动态批处理通过迭代级调度,让短序列能够快速插入和退出,减少了调度等待时间。

3.4.4 总结复盘

静态批处理与动态批处理的本质区别在于:前者以“请求”为调度单元,后者以“迭代步”为调度单元。动态批处理通过精细化的调度,让GPU始终处于高利用率状态,同时显著降低了尾延迟。在实际部署中,vLLM的Continuous Batching结合PagedAttention,已经成为LLM推理服务的事实标准。对于正在使用静态批处理框架的团队,建议尽早迁移到vLLM或同类支持动态批处理的推理引擎,以获得数量级的性能提升。