news 2026/8/30 5:14:35

LLM推理三种批处理策略:静态、动态与连续批处理全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM推理三种批处理策略:静态、动态与连续批处理全解析

同一个模型,同样的显卡,两个推理服务端给出的吞吐却可能差出好几倍。这个差距很少来自模型权重本身,更多来自推理引擎怎么安排请求的执行顺序。今天这篇只聊一件事情:LLM 推理里的 Static Batching、Dynamic Batching、Continuous Batching 三种批处理策略到底差在哪,为什么 Continuous Batching 几乎成了现代 LLM 推理框架的标配,以及你自己部署推理服务时应该怎么选。

这篇不是纯概念科普。我会先讲清三种策略的执行模型和代码差异,再对比它们对吞吐、显存、延迟的实际影响,然后结合 vLLM、TensorRT-LLM 这类常用推理框架说明它们各自落在哪个位置,最后给出一套本地部署时的选型建议和压测方法。适合正在做模型服务化、API 封装、性能调优的读者,也适合那些刚接触 vLLM、TGI、TensorRT-LLM 时对 continuous batching 概念反复犯迷糊的人。

1. 核心能力速览

维度Static BatchingDynamic BatchingContinuous Batching
调度粒度整批请求整批请求请求级 / 迭代级
请求同步性同进同出同进同出各自独立推进
Padding 开销高,按批次内最长序列补齐较高,仍按批内长度对齐低,按请求实际长度推进
吞吐潜力中等
实现复杂度中等
队列调度有等待窗口凑批请求级状态机 + 优先级调度
典型场景离线批量推理、实验脚本早期 Web 推理服务现代在线 LLM 推理框架
代表实现直接循环model.generate()Triton Dynamic Batcher、早期 TF ServingvLLM、TensorRT-LLM In-flight Batching、TGI

需要先说明一点:这三种策略没有绝对的谁替代谁。静态批处理在离线全量任务里仍然简单可靠,动态批处理适合请求量不大但需要自动聚合的场景,连续批处理则是面对高并发在线推理时吞吐最优的选择。

2. 为什么 Batching 是 LLM 推理的关键

要理解三种批处理策略,先得明白 LLM 推理本身的特殊性。

LLM 生成一个回答,通常分为两个阶段:

  • Prefill(预填充):把用户输入的 prompt 一次性喂给模型,并行计算,生成第一个 token。这个过程计算密集。
  • Decode(解码):逐个生成后续 token,每个 token 都要依赖前面所有 token 的 KV Cache。这个过程是内存带宽密集,而不是计算密集。

真正让 GPU 头痛的是 decode 阶段。每个请求单步只生成一个 token,计算量很小,但要把整个模型权重和 KV Cache 从显存里搬一遍。如果 GPU 只服务一个请求,显存带宽会被大量浪费,GPU 利用率很低。

Batching 的作用,就是把多个请求的 decode 步骤合并成一次前向计算。模型权重只需要从显存读一次,多个请求共享这次读取,从而把吞吐拉起来。

问题也随之而来:请求什么时候到?请求长度差多少?先到的请求要不要等后到的请求?这就是静态批处理、动态批处理、连续批处理三种策略要回答的核心问题。

3. 静态批处理 Static Batching

静态批处理是最直观、最容易实现的方式。

它的执行模型是:把一批请求打包成一个固定 batch,模型对这批请求统一执行 prefill 和 decode,整个 batch 必须全部生成结束后,才开始下一批请求。

这段逻辑用伪代码看更清楚:

def run_static_batch(requests, batch_size): for start in range(0, len(requests), batch_size): batch = requests[start:start + batch_size] # 统计当前批次内最长的 token 序列 max_len = max(len(req["input_ids"]) + req["max_new_tokens"] for req in batch) # 长度不足的请求需要用 padding 补齐 padded_batch = [pad_to(req, max_len) for req in batch] # 整个 batch 同步生成,所有请求结束后才返回 outputs = model.generate(padded_batch) save_outputs(outputs)

静态批处理最明显的问题有两个:

第一是 padding 浪费。LLM 推理的 decode 阶段长度取决于每个请求的生成进度,但静态批处理在开始时就把批次内所有请求补齐到相同长度。短的请求早就生成完了,但必须陪着最长的请求一起等,中间这段显存和计算资源全部浪费。

第二是队头阻塞。如果批次里有一个请求生成长文本,其他请求可能几十个 token 就结束,但整个 batch 必须等它全部完成才能释放。之后释放的显存也只能等下一批凑齐,GPU 会有明显的空闲气泡。

从工程角度看,静态批处理并不是一无是处。它的实现简单,行为可预测,不会出现请求被反复抢占导致的延迟抖动。离线批量任务、不需要响应实时性的场景,静态批处理仍然够用。PyTorch 原生写推理脚本时,model.generate()走的基本就是这条思路。

4. 动态批处理 Dynamic Batching

动态批处理在静态批处理的基础上加了一个关键机制:请求队列和等待窗口。

动态批处理器维护一个请求队列,新请求到达后先进入队列,调度器等待一个可配置的时间窗口。窗口内如果凑够了 batch 大小,就启动一批;如果等待超时,也先用当前已到达的请求组成 batch 启动。这样就不需要所有请求刚好同时到达,降低了对请求到达节奏的敏感度。

核心逻辑可以简化成下面这样:

import time def dynamic_batching_scheduler(request_queue, max_batch_size, wait_timeout_ms): batch = [] deadline = None while True: req = request_queue.get() batch.append(req) if deadline is None: deadline = time.time() + wait_timeout_ms / 1000.0 if len(batch) >= max_batch_size or time.time() >= deadline: # 触发一次推理 yield batch batch = [] deadline = None

动态批处理解决的问题是“请求到达不均匀”。它允许请求在时间维度上聚合成批,而不是像静态批处理那样必须整整齐齐同时到达。

它的问题依然存在:批次内仍然按最长的序列 padding。只要 batch 里有一个长请求,其他短请求就要陪跑到最后。动态批处理并没有从根本上消除解码阶段的等待浪费,只是把“同时到达”的要求放宽成了“一个时间窗口内到达”。

动态批处理适合请求并发量不高、延迟要求不极端的场景。比如内网工具类服务,平均每秒几个请求,动态批处理就可以把零散请求聚合成批,获得不错的吞吐提升。很多早期推理服务框架默认采用这种策略,配置项里通常叫max_batch_sizewait_timeout

5. 连续批处理 Continuous Batching

连续批处理又叫迭代级调度,或者 In-flight Batching,是目前 LLM 推理框架的主流方案。

它的核心思想是:不再把整个 batch 当作一个不可拆分的整体,而是把调度粒度缩小到每一次迭代,或者说每一个 decode 步骤。

每个请求在推理引擎内部是一个独立的状态对象。调度器每个 step 都会重新决定:下一轮前向计算要包含哪些请求。有的请求刚刚完成 prefill,有的请求已经 decode 了 40 个 token,有的请求已经结束需要释放显存,有的请求还在等待队列里。这些状态可以交错执行。

class ContinuousScheduler: def __init__(self, max_running_seqs=16): self.waiting = [] self.running = [] self.max_running_seqs = max_running_seqs def step(self): # 每次迭代前,尝试让等待队列里的请求进入执行集合 while len(self.running) < self.max_running_seqs and self.waiting: seq = self.waiting.pop(0) seq.prefill() # 新请求先做 prefill self.running.append(seq) # 每个请求只前进一步 for seq in self.running: seq.decode_one_token() # 只保留未完成的请求 self.running = [seq for seq in self.running if not seq.is_finished()]

这段伪代码省略了大量工程细节,但核心逻辑已经体现出来了:

  • 没有整批同步等待。
  • 每个请求在 running 列表里都有独立进度。
  • 新请求可以在任意迭代加入,完成的请求马上释放资源。
  • 不存在批次内 padding。每个请求按自己的实际 token 长度推进,GPU 算力被不同进度的请求填满。

连续批处理要真正高效,还需要解决几个配套问题。

第一是显存管理。每个请求的 KV Cache 长度会随生成过程动态增长,不能按固定最大长度预留,否则显存浪费会很严重。vLLM 提出的 PagedAttention 就是把 KV Cache 按固定大小的块管理,类似操作系统里的分页。腾讯、英伟达等框架也有类似的 KV Cache 管理器。

第二是抢占与优先级。当并发请求非常多、显存放不下所有请求的 KV Cache 时,调度器必须决定先跑哪些请求。通常新请求的 prefill 开销大,decode 请求已经处于尾段,直接全部挤掉会让延迟剧烈抖动。工程上常见的做法是给请求划分优先级池,或者控制max_num_seqs来限制同时运行的请求数。

第三是调度开销。连续批处理每个迭代都要做一次请求集合决策,如果调度器本身写得不够高效,调度本身会成为新的瓶颈。这也是为什么现代推理框架通常用 C++/CUDA 实现调度核心,而不是简单用 Python 循环。

从效果看,连续批处理能让 GPU 在 decode 阶段始终保持高利用率,不同的请求交错填充计算间隙,整体吞吐通常明显高于静态和动态批处理。付出的代价是调度器复杂度显著上升。

6. 三种策略横向对比

对比维度Static BatchingDynamic BatchingContinuous Batching
调度触发时机批次完全凑齐等待窗口或 batch 满每次迭代
请求是否可以中途加入
请求是否可以提前离开
Padding 浪费严重较严重基本消除
GPU 利用率中等
平均响应延迟可能被长请求拖累等待窗口内波动稳定但受抢占影响
是否适合高并发在线服务勉强
实现成本
工程系统复杂度简单中等高,包含显存管理和调度器

这里要特别强调一个问题:连续批处理提升的是吞吐,不代表每个请求的延迟都会变好。在一些极端抢占场景下,先到的请求可能被新请求的 prefill 挤占资源,导致尾延迟上升。在线服务部署时,需要结合优先级队列和并发上限来控制这种波动。

7. 从原理到常用推理框架

理解了三种策略之后,再看主流 LLM 推理框架就非常清晰了。

  • vLLM:Continuous Batching 的代表实现,配合 PagedAttention 管理 KV Cache,是当前开源社区最常用的高吞吐推理框架之一。
  • TensorRT-LLM:英伟达的方案,把连续批处理叫做 In-flight Batching。它在显存规划、算子融合上做得非常细,更适合生产化部署。
  • Hugging Face TGI:原生支持 Continuous Batching,封装比较友好,适合快速把模型包装成服务。
  • SGLang:在连续批处理基础上进一步优化了前缀复用,多个请求共享相同 prompt 前缀时,KV Cache 可以复用,这正好和 continuous batching 的逐迭代调度天然契合。

这几个框架通常都已经默认开启连续批处理。你在部署时看到max_num_seqsmax_running_requestsmax_batch_size之类的配置,本质都是在控制同时运行的请求数量,也就是连续批处理的并发窗口大小。

配置思路通常是:max_num_seqs设置得越大,GPU 越容易被填满,但 KV Cache 显存占用也越高。设置太小,显存还有富余但吞吐上不去。需要根据模型大小、输入输出长度和显存容量一起调整。

这里还可以回答一个常见的本地部署疑问:ComfyUI 和 LLM 推理服务是否必须部署在同一台机器上。答案是不一定。如果 LLM 服务通过 HTTP API 对外提供能力,ComfyUI 工作流里只需要一个 API 调用节点,两者完全可以分机部署。只有当 ComfyUI 插件直接把模型加载在 ComfyUI 进程内,或者需要共享显存、共享模型文件时,才要求它们落在同一台机器上。是否必须同机,取决于工作流的设计方式,而不是模型本身。

8. 本地部署时的显存与吞吐观察方法

很多人关心“我应该用哪种批处理策略”,实际上在主流框架里,策略往往已经被框架固定了,你需要做的是调参和验证。下面给出一套通用验证流程,不依赖具体框架。

第一步,准备一组长度统一的测试请求。建议固定输入长度和输出长度,比如输入 64 token,输出 128 token。这样测试结果更好对比。第二步,用压测脚本并发发送请求,从 1 个并发逐步增加到 64 个。第三步,观察四类指标:吞吐、平均延迟、P99 延迟、GPU 利用率和显存占用。

一个通用的并发压测脚本模板如下:

import asyncio import aiohttp async def send(session, url, prompt, max_tokens=128): payload = { "prompt": prompt, "max_tokens": max_tokens } async with session.post(url, json=payload) as resp: return await resp.json() async def main(): url = "http://127.0.0.1:8000/v1/completions" prompt = "Write a paragraph about LLM inference batching." # 构造 32 个并发请求 tasks = [] async with aiohttp.ClientSession() as session: for _ in range(32): tasks.append(send(session, url, prompt, max_tokens=128)) results = await asyncio.gather(*tasks) print("completed:", len(results)) asyncio.run(main())

这个脚本只负责触发并发,实际框架的接口路径和参数名需要按你部署的服务调整。如果有 OpenAI 兼容接口,一般可以直接用/v1/completions/v1/chat/completions

观察时需要重点区分两个阶段:prefill 阶段 GPU 利用率比较高,但耗时取决于输入长度;decode 阶段每个请求生成 token 的速度接近,并发越高,总吞吐越高,但单请求的生成速度会因为显存带宽竞争而下降。

更稳妥的判断方式是横向对比。在同一个框架里,分别用小并发、中并发、大并发压测,找到吞吐增长曲线拐点。拐点出现的位置往往就是max_num_seqs设置过大、KV Cache 占用过多、或者服务端已经开始排队的位置。

显存占用建议直接用nvidia-smi观察,核心看两行:Memory-UsageVolatile GPU-Util。如果显存还有大量剩余但 GPU 利用率不高,说明并发开小了;如果显存吃满但吞吐停滞,说明并发和 KV Cache 配额已经到顶;如果显存频繁溢出,说明需要调低max_num_seqs或限制最大生成长度。

连续批处理框架通常还会提供一个/metrics端点,暴露排队的请求数、正在运行的请求数、KV Cache 使用率等关键指标。这些指标比只看显存占用准得多。部署后建议优先确认这些指标能不能正常读到。

9. 选型建议:什么时候用哪种策略

如果你是自己部署推理服务,可以参考下面这套判断逻辑。

离线批量任务可以选择静态批处理。比如离线跑一批文本摘要、标签抽取、数据增强,不需要实时响应,静态批处理实现简单、行为稳定,不存在复杂的调度问题。PyTorch 原生的model.generate()已经够用。

请求量很小、延迟不敏感的内网工具可以选择动态批处理。比如内部知识库问答系统,每分钟几十个请求,动态批处理器可以把它们聚合成批,减少显存带宽浪费。注意把等待超时控制在一个合理范围,避免请求排队时间过长。

面向外部用户、高并发、需要稳定 P99 延迟的在线服务,优先选择连续批处理。vLLM 或 TensorRT-LLM 这类框架已经帮你把调度器实现好了,你只需要调好max_num_seqs和 KV Cache 上限。

显存受限的场景要特别注意。连续批处理虽然总体上更省显存,但它的 KV Cache 采用按块分配机制,块大小需要配置。如果块设得过大,小请求也会占用大块显存,浪费反而严重;如果块设得过小,分配次数增加,调度开销上升。具体块大小必须根据实际模型和请求长度测试。

延迟敏感型应用要额外考虑优先级。连续批处理在新请求加入时,需要做 prefill,prefill 计算量远大于单步 decode,如果调度器不做控制,新请求的 prefill 会抢占正在 decode 的旧请求。这个时候可以通过请求级优先级、prefill 和 decode 分池等方式保障旧请求的尾部延迟。

10. 常见问题与排查方法

问题现象可能原因排查方式解决方案
并发一高就显存溢出max_num_seqs设置过大,或 KV Cache 预留不足查看服务日志和显存监控调低并发上限,限制最大生成长度
GPU 利用率很低但显存充足并发太小,批大小不足观察nvidia-smi的 GPU-Util增大并发请求数或调高max_num_seqs
单请求延迟忽高忽低新请求 prefill 抢占 decode,或不同长度请求混跑查看 P99 延迟曲线设置请求优先级,限制并发窗口
排队时间过长等待窗口设置不合理,或服务吞吐已达到上限查看框架 metrics 中的排队数量调整 batch 等待超时,或扩容
输出速度越来越慢decode 阶段显存带宽饱和,并发请求过多观察 N 个并发下的单个 token 生成速度适度降低并发,减少尾部阻塞
相同模型在 A 框架快、B 框架慢框架调度策略不同,padding 和显存管理差异对比两套框架的请求级日志按场景选择框架,不要盲目迁移
显存碎片化严重KV Cache 分配块策略不合理查看显存碎片率和不连续分配次数调整 KV Cache 块大小,或切换显存分配策略

如果你的服务出现“并发一高就 OOM”,优先检查是不是一次性分配的 KV Cache 太大。很多推理框架支持按请求限流的配置,调低后可以避免显存剧烈波动。出现“单请求延迟抖动”时,不要急着怀疑模型,先看看是不是调度器频繁抢占引起的。用 metrics 里的排队请求数做判断,比猜更准确。

11. 最佳实践与使用建议

第一,第一次部署先跑最小配置。用 1 个并发请求验证模型能正常生成,再逐步增加并发。不要上来就把并发拉到 128,否则显存问题、调度问题、接口问题会混在一起,很难定位。

第二,保留一套最小可运行配置。把模型路径、max_num_seqs、最大生成长度、KV Cache 上限固定下来,遇到环境变动时先回到这一套配置验证能跑通,再继续调优。

第三,测试数据要覆盖不同长度。纯短文本请求和纯长文本请求测出来的结论不同。建议按短输入短输出、短输入长输出、长输入短输出三类组合分别压测,三种策略对输入输出长度的敏感度差异会非常明显。

第四,本地部署涉及业务数据时需要确认边界。LLM 服务如果只在本机内存和显存里处理数据,隔离性相对可控;一旦通过 API 开放给其他机器或外部工具调用,数据会跨节点传输,需要根据业务合规要求确定是否允许。涉及隐私数据、未公开资料、人脸声音素材时,更要确认使用授权和存储边界。

第五,批量任务要加日志和失败重试。虽然连续批处理提升了吞吐,但请求越长、并发越高,单个请求失败的概率也会增加。建议在客户端记录请求 ID、输入长度、输出长度、耗时和错误码,超时任务做指数退避重试。

第六,监控指标比猜重要。现代推理框架一般都会暴露吞吐、排队长度、KV Cache 使用率、prefill/decode 时长等指标。把这些指标接入 Prometheus 或简单的定时脚本,比盯着nvidia-smi本地观察要靠谱得多。

第七,多框架对比时不要只看一个数字。某些框架在小并发下可能不如另一个,但高并发下吞吐优势明显。对比时要固定相同的并发档位、相同的输入输出长度,最好测一个并发梯度,而不是单点比较。

第八,商用或发布前要做效果复核。批处理策略优化的是性能和资源利用率,不会改变模型本身的生成质量,但高并发下模型可能因为显存压缩、KV Cache 限制等原因产生不同于单机测试的输出。发布前建议挑一批典型样本重新审查输出质量。

12. 总结与下一步

这篇文章的核心可以压缩成三句话。静态批处理把所有请求捆绑成同一个批次,简单但浪费严重。动态批处理引入等待窗口,让请求在时间上聚合成批,但批内长度对齐问题没有解决。连续批处理把调度粒度缩小到迭代级,每个请求独立推进,基本消除了 padding 浪费,代价是调度器和显存管理复杂度大幅上升。

如果你用的是 vLLM、TensorRT-LLM、TGI 这类现代推理框架,默认策略大概率已经是连续批处理。你真正需要做的不是重新实现一套调度器,而是理解max_num_seqs、KV Cache 上限、优先级队列这些配置背后的含义,然后针对自己的请求长度分布做压测和调参。

最容易踩的坑是并发开太高导致 KV Cache 溢出,以及只有 GPU 利用率低却不知道是并发不够还是 padding 浪费。建议上手先跑一个小规模的并发梯度,把吞吐随并发变化的曲线打出来,再谈后续优化。

后续可以继续扩展的方向包括:prefix caching 复用公共前缀的 KV Cache、speculative decoding 用小模型辅助大模型生成、prefill 和 decode 分阶段部署。这些技术和 continuous batching 并不冲突,它们是在调度器已经足够高效之后,进一步压榨推理吞吐的工程手段。先把 batching 策略的原理吃透,再看这些高级优化,思路会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 5:13:49

MATLAB水下图像融合增强实战:从颜色校正到金字塔融合

简介&#xff1a;本资源是一份面向高校课程设计与图像处理初学者的MATLAB实践项目&#xff0c;聚焦水下图像质量退化问题&#xff0c;提供从增强到融合的完整算法实现方案。针对水下图像存在的颜色失真、低对比度、光照不均与散射噪声等典型缺陷&#xff0c;资源集成了直方图均…

作者头像 李华
网站建设 2026/8/30 5:13:16

基于Java的车位租赁管理系统:从设计到答辩全解析

简介&#xff1a;本资源是一套完整的基于Java开发的车位租赁管理系统实战项目&#xff0c;面向计算机专业本科生、Java初学者及Web开发入门者&#xff0c;聚焦停车场资源数字化管理场景&#xff0c;解决车位信息登记、租约签订、费用结算与用户权限管控等核心业务问题。压缩包共…

作者头像 李华
网站建设 2026/8/30 5:12:18

MATLAB印刷品缺陷检测实战:图像差分与形态学分析

简介&#xff1a;本资源是一个基于MATLAB开发的印刷品缺陷检测系统&#xff0c;面向计算机、人工智能、自动化及通信等专业的学生、教师与工程实践者&#xff0c;解决印刷质量控制中污点、刮痕、色差等常见缺陷的自动识别与定位问题&#xff0c;适用于课程设计、大作业及毕业设…

作者头像 李华
网站建设 2026/8/30 5:11:39

欢聚时代2018校招iOS笔试题解析:核心考点与答题策略

1. 试卷整体设计与考察思路1.1 这套卷子到底在考什么拿到欢聚时代2018年校招的iOS A卷时&#xff0c;我第一反应是&#xff1a;这套题出得挺规矩的。成都场这份卷子没有太多偏题怪题&#xff0c;考察的内容集中在iOS开发最基础也最核心的几个模块&#xff1a;OC语言特性、内存管…

作者头像 李华
网站建设 2026/8/30 5:06:13

在NUCLEO-N657X0-Q上使用STM32Cube AI Studio验证AI模型并开启overdrive模式

1. 项目背景&#xff1a;为什么要在 NUCLEO-N657X0-Q 上验证 AI 模型最近在把一个图像分类模型往 STM32N657X0-Q 这颗芯片上搬&#xff0c;前前后后在 STM32Cube AI Studio 里折腾了不少时间。如果你也卡在“PC 上跑得挺好&#xff0c;板子上不知道行不行”这个阶段&#xff0c…

作者头像 李华