LLM推理三种批处理策略:静态、动态与连续批处理全解析
发布时间:2026/8/30 5:14:50 作者:尧图编辑部 阅读量:1,286

同一个模型同样的显卡两个推理服务端给出的吞吐却可能差出好几倍。这个差距很少来自模型权重本身更多来自推理引擎怎么安排请求的执行顺序。今天这篇只聊一件事情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_size和wait_timeout。5. 连续批处理 Continuous Batching连续批处理又叫迭代级调度或者 In-flight Batching是目前 LLM 推理框架的主流方案。它的核心思想是不再把整个 batch 当作一个不可拆分的整体而是把调度粒度缩小到每一次迭代或者说每一个 decode 步骤。每个请求在推理引擎内部是一个独立的状态对象。调度器每个 step 都会重新决定下一轮前向计算要包含哪些请求。有的请求刚刚完成 prefill有的请求已经 decode 了 40 个 token有的请求已经结束需要释放显存有的请求还在等待队列里。这些状态可以交错执行。class ContinuousScheduler: def __init__(self, max_running_seqs16): 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 推理框架就非常清晰了。vLLMContinuous Batching 的代表实现配合 PagedAttention 管理 KV Cache是当前开源社区最常用的高吞吐推理框架之一。TensorRT-LLM英伟达的方案把连续批处理叫做 In-flight Batching。它在显存规划、算子融合上做得非常细更适合生产化部署。Hugging Face TGI原生支持 Continuous Batching封装比较友好适合快速把模型包装成服务。SGLang在连续批处理基础上进一步优化了前缀复用多个请求共享相同 prompt 前缀时KV Cache 可以复用这正好和 continuous batching 的逐迭代调度天然契合。这几个框架通常都已经默认开启连续批处理。你在部署时看到max_num_seqs、max_running_requests、max_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_tokens128): payload { prompt: prompt, max_tokens: max_tokens } async with session.post(url, jsonpayload) 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_tokens128)) 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-Usage和Volatile 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 采用按块分配机制块大小需要配置。如果块设得过大小请求也会占用大块显存浪费反而严重如果块设得过小分配次数增加调度开销上升。具体块大小必须根据实际模型和请求长度测试。延迟敏感型应用要额外考虑优先级。连续批处理在新请求加入时需要做 prefillprefill 计算量远大于单步 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 策略的原理吃透再看这些高级优化思路会顺畅很多。