连续批处理 Continuous Batching

Continuous Batching / Dynamic Batching / Orca Batching

推理引擎在等待中的请求尚未全部完成时,动态将新请求插入 GPU 计算空隙并把已完成的请求即时弹出,持续填满 GPU 运算单元,将单卡吞吐量提升 2 至 4 倍,是 vLLM、SGLang 等现代推理框架的核心调度机制

详细解释

连续批处理(Continuous Batching,又称动态批处理、Orca Batching) 是大模型推理服务中把 GPU 利用率从 20% 至 30% 拉到 80% 至 95% 的核心调度算法。与早期静态批处理(Static Batching,必须等一整批的所有请求都生成完最后一个 Token 才能弹出、再塞下一批)完全不同,连续批处理会在每个解码步(Decode Step)结束后检查:哪些请求已经生成了 EOS 结束符可以立刻出队?队列里又有哪些新请求(或还在排队的 Prefill 请求)可以立刻塞进当前这批的空位?于是 GPU 在任何一个解码步都不会空转,整个推理过程如同一条永不间歇的流水线。

连续批处理最早由 UC Berkeley 在 2022 年的 Orca 论文 中系统化提出,随后成为 vLLM、SGLang、TensorRT-LLM、Text Generation Inference 等现代推理框架的默认调度模式。对于 7B 至 70B 级大模型,连续批处理单独一项优化即可在相同 P99 延迟下,将单 A100 的 TPS(每秒输出 Token 数)提升 2 至 4 倍,对应的 每百万 Token 成本 直接下降 50% 至 75%,是 LLM 在线推理的「成本一哥」优化手段。

唯元智创 的推理网关在接入 vLLM / SGLang 后端时默认开启 「连续批处理 + 分页注意力 + 前缀缓存」三位一体:连续批处理负责动态出队入队填 GPU 空隙,分页注意力负责把 KV Cache 切割成可按需换入换出的页避免显存碎片,前缀缓存 负责在 System Prompt、RAG 上下文重复出现时直接复用 Prefill 计算。三者叠加后,相同 GPU 集群下相比原生 TGI 静态批处理可支持的并发对话数再提升 1.5 至 2 倍,与 流式响应 结合后用户体感几乎不会出现「等很久才跳出第一字后又快速吐字」的怪现象。

四种推理批处理模式对比表

批处理模式调度时机GPU 空闲时间占比典型 70B 单 A100 输出 TPS适合场景与流式响应兼容性实现复杂度
静态批处理 Static Batching一批全部 EOS 后才切换高(30% 至 60%)1,200 至 1,800离线评测、纯离线批处理差(用户等待整批结束)极低
客户端批处理 Client Batching由调用方按时间窗攒批中高(20% 至 40%)1,800 至 2,500简单 Embedding、轻量 Rerank中(调用方控制)低
连续批处理 Continuous Batching每个解码步动态进出极低(5% 至 15%)3,500 至 5,000在线实时对话、RAG 问答极佳(逐 Token 出队)中高
连续批处理 + 投机解码(Speculative Decode)连续批 + 每步草稿验证极低(3% 至 10%)6,000 至 9,000低延迟高吞吐旗舰场景极佳高
唯元智创 默认组合(Continuous Batching + PagedAttention + Prefix Cache)连续批 + 页式 KV 管理 + 提示缓存极低(3% 至 8%)4,500 至 6,500通用在线推理 + RAG + 企业 SLA极佳中(开箱即用)

实战经验与避坑

  1. 连续批处理要生效,推理框架必须原生支持;在「外层自写请求队列攒批」不算数:很多团队想偷懒,在外层用 Python asyncio 队列按 500 毫秒或 20 条一组攒一批再扔给 TGI / vLLM,以为这就是「批处理」,但本质仍是静态批(TGI 内部如果不是连续批,还是必须等这批所有请求 EOS 后才清),吞吐提升通常不超过 50%,和真正的连续批(每解码步进出)相去甚远。正确做法:直接用 vLLM 0.2+、SGLang 0.1+、TensorRT-LLM 0.5+ 这些原生 Continuous Batching 的推理引擎,外层只做简单的并发上限控制即可。
  2. 连续批处理的「最大并发批大小 max_num_seqs」有黄金区间,不要设太高:很多人以为「连续批就是并发设越大越好」,于是把 vLLM 的 max_num_seqs 从默认 256 直接拉到 2048,结果 P99 延迟从 3 秒飙升到 15 秒以上。原因是连续批本质是一种「公平共享调度」:当你塞 2000 条请求同时运行时,每条请求每个解码步只能分到 GPU 总算力的 1/2000,于是所有请求都变慢。黄金经验:7B 模型单 A100 推荐 max_num_seqs 256 至 512,70B 模型单 A100 推荐 64 至 128,并根据你的 P99 目标反推——如果延迟超标就下调并发,而不是盲目加大。
  3. 连续批处理必须配合分页注意力使用,否则显存碎片会吃掉 30% 至 50% 的显存收益:连续批的请求长短不一,入队出队频繁,如果 KV Cache 还是「一条请求一段连续显存」的老分配方式,出队请求留下的空洞无法被新请求的 KV 填满,显存利用率骤降。必须搭配 分页注意力 把 KV Cache 切成固定大小的 16KB 或 64KB 页,出队的页立即回收供新请求复用,整体 KV 显存利用率从 40% 左右提升到 85% 至 95%,相当于白嫖了 2 倍并发。
  4. 流式响应下要关注「首字延迟 TTFT」与「每两字间隔 IRR」两个长尾指标,不要只看 TPS:连续批在高并发下虽然 TPS 非常高,但如果调度器是纯贪心的(FIFO 按先来后到塞),会出现「新来的短请求 Prefill 长时间排不到,TTFT 5 秒以上」的现象。正确做法:给 Prefill 阶段设更高的调度优先级(vLLM --preempt-mode swap + SGLang 里的 policy=fcfs 配合 prefill priority),保证每个新请求到达后 100 毫秒内至少能执行一次 Prefill,这样 P99 TTFT 才能稳定控制在 500 毫秒以内。
  5. 长输出请求要做「抢占 + 换出 Preempt/Swap」,否则会被短请求反复挤兑永远跑不完:有些请求是写 3000 字报告、代码整文件生成,连续批里如果完全公平,长请求每 30 步才轮到一次解码,实际生成时间从 30 秒拖到 5 分钟。解决方案:用 vLLM/SGLang 自带的 Preempt 机制——当显存吃紧时,把「连续运行步数最多」的长请求的 KV 页临时写到 CPU 内存(Swap)或直接丢弃(Recompute),先腾显存给新来的 Prefill 跑,短请求送走后再把长请求的 KV 页换回显存继续。这样兼顾了短请求低延迟和长请求能最终跑完,长请求完成率从 60% 左右拉到 99% 以上。
  6. 上线前做「长短线混合压测」,不要只测 1:1 的短请求:连续批的优势只有在「长短混合、输入输出长度方差大」的真实场景才能体现。纯 1000 输入 + 100 输出的短请求,连续批和静态批差距可能不到 1.5 倍;但如果混合 30% 的长请求(输出 2000 Token 以上),连续批的吞吐优势会拉到 3 至 5 倍、P99 延迟反而更低。压测时务必按真实业务的输入输出分布做(比如 60% 对话 100 至 500 Token、25% RAG 检索 1K 至 4K、15% 代码生成或写作 2K 至 8K),才能测出真实的吞吐量和 SLA 安全水位。

常见问题

连续批处理和分页注意力是一回事吗?为什么很多人总一起提?
不是一回事,但必须配合使用才能发挥出连续批的完整威力;两者解决的是不同维度的瓶颈——调度 vs 显存。先把概念拆清楚:连续批处理 Continuous Batching 解决的是「GPU 算力浪费」,核心是让 GPU 在每个解码步都塞满工作,不等待一整批请求全部结束。但请求频繁进出队会产生大量「长度不一的 KV 显存空洞」,如果还按老办法(每条请求一段连续显存块存 KV),出队留下的空洞大小未必匹配新请求的 KV 长度,很多碎片就没法复用,显存白白浪费 30% 至 50%,导致你本来能开 512 并发,实际只能开 256,连续批吃满算力的优势就被显存限制腰斩。分页注意力 PagedAttention 解决的是「KV 显存碎片」,它把 KV 切成固定大小的页(比如每页 16 个 Token),出队请求的页直接回收进空闲池,新请求按需分配空闲页,不管请求长短都能填满碎片,KV 显存利用率从 40% 左右拉到 90% 以上,这样连续批想开的并发数才真正有显存支撑。两者合在一起是「算力填满 + 显存填满」的双杀,所以现代推理框架(vLLM、SGLang)都是默认一起开启的。单独开只有一半收益:只开连续批不开分页 = 算力高但并发卡显存上限;只开分页不开连续批 = 显存利用率高但 GPU 经常空转。实践中必须两个都开,再叠加 前缀缓存 对重复 System Prompt 和 RAG 段落的 Prefill 复用,三项联动才是完整的推理性能最优解。唯元智创 的默认推理配置就是这种「三件套 + 流式优先」的组合,大多数业务无需再调参即可拿到接近 Pareto 最优的吞吐和延迟。
为什么我开了 vLLM 连续批处理,单卡 TPS 还是上不去?常见排查顺序是什么?
80% 的「连续批 TPS 上不去」都不是引擎的问题,而是并发上限、显存、调度优先级三项配置不合理,按下面 6 步查基本都能定位到根因:第一步,先看 GPU 利用率到底满没满。用 nvidia-smi dmon -s u 或 vLLM 的 Prometheus vllm:gpu_utilization 指标,如果 GPU 利用率长期低于 70%,说明「并发不够塞不满 GPU」,不是连续批的锅。第二步,检查 max_num_seqs 是否设得太小。很多人默认 128 甚至 64 去跑 7B 模型,GPU 永远填不满;7B 单 A100 先拉到 256,70B 拉到 64 做基线。第三步,看 KV 显存使用率。如果 KV 显存长期接近 100%,但 GPU 利用率 60%,说明「并发被显存卡死了」。这时优先开启分页注意力的 Swap/Preempt 到 CPU 内存,KV 可换入换出后并发能再涨 50% 至 100%;再不行就开 KV 缓存淘汰 的 Sliding Window 策略,主动把超长对话里最远的 KV 页丢掉腾显存。第四步,检查 Prefill 优先级是否太低。如果你的业务是大量短请求 + 少量长请求,但 vLLM 里 Decode 阶段优先级过高,新来的 Prefill 迟迟排不上,会出现「GPU 利用率不低但新用户 TTFT 极高、有效 TPS 低」的假象。把 vLLM 的 --prefill-chunk-size 适当调小(如 8192),保证短 Prefill 能更频繁地插入到 Decode 间隙。第五步,看请求是否被外层网关限流了。很多团队外层有个自作主张的令牌桶,按 100 QPS 封顶,后面的推理引擎再强也接不到请求。第六步,最后才怀疑是计算本身的瓶颈。如果 GPU 利用率稳定在 90% 以上、KV 显存还有富余、max_num_seqs 还没到上限但 TPS 就是上不去,说明确实到了计算瓶颈,这时才能上 FlashAttention、分页注意力的 Page Size 调优(2 Token 至 32 Token 扫一遍找最优)、量化(FP8/AWQ/GPTQ)、投机解码等优化手段。
流式响应和连续批会互相影响吗?长请求和短请求的公平性怎么平衡?
不会互相影响,反而互相成就;但真实公平性需要「多级优先级队列 + 最长运行步数抢占」组合拳来保障。首先澄清一个常见误区:流式响应和连续批是正交的两个概念——流式只决定「已生成的 Token 何时推给客户端(每生成一个就推)」,连续批决定「GPU 上同一时刻同时为哪些请求算下一个 Token」。vLLM/SGLang 里只要开了 streaming,每条请求在连续批里每往前解码一步,框架就会通过 SSE / gRPC Stream 把那个 Token 推出去,两者配合得天衣无缝,用户的体感就是「字一个一个往外冒,从来不会卡住几十秒然后一次性蹦一大段」。真正需要工程化解决的是长短请求的公平性:纯贪心 FIFO 连续批会出现两个病态场景——(a) 新来的短请求一直被长请求占着槽位,TTFT 超过 5 秒;(b) 长请求被源源不断的短请求挤兑,每 30 步才轮到 1 步,写完一篇 3000 字的报告要花 10 分钟以上。业界标准的平衡方案是三级优先级 + 步数抢占:第一级 Prefill 高优先级:新来的请求 Prefill 阶段(一次性算输入)赋予最高权重,保证 100 毫秒内至少执行一次,TTFT 稳定在 500 毫秒以内;第二级普通 Decode:已进入生成阶段的请求按 FCFS 公平轮询;第三级「长请求标记 + 步数抢占」:任何请求连续解码超过 N 步(如 128 步)就暂时降级或进入「可抢占池」,一旦显存不足就优先把它的 KV 页 Swap 出去给新请求用,腾完再换回来继续跑。三级调度的最终效果:短请求的 P99 TTFT 小于 500 毫秒、P99 总耗时小于 3 秒;长请求(2000 至 8000 Token 输出)完成率 99% 以上、完成时间相比独占 GPU 最多慢 1.5 至 2 倍(而非 10 倍以上)。唯元智创 的多租户网关就是按这套三级优先级 + 配额策略调度,确保付费大客户、免费用户、异步批处理任务等不同优先级的流量在同一个 GPU 池里平稳共存,不会出现长请求饿死或新请求永远排不到的情况。