
在这篇文章里,我们会学习 Continuous Batching。这是一种让 LLM 服务器在生成的每一步都让 GPU 保持忙碌,从而同时处理更多用户的技术。
我们会讲这些内容:
- 大图景
- 快速回顾:LLM 如何生成 token
- 为什么 batching 对 LLM 很重要
- 旧做法:Static Batching
- Static Batching 的问题
- 什么是 Continuous Batching?
- 拼车类比
- Continuous Batching 如何一步步工作
- 一个数字例子
- 真实数字与加速效果
- Continuous Batching 的好处
- 几点重要说明
- 快速总结
我是 Amit Shekhar,Outcome School 创始人。我教过并辅导过很多开发者,他们靠自己的努力拿到了高薪技术岗位;我也帮助过许多科技公司解决各自独特的问题,并创建过很多被顶级公司使用的开源库。我热衷于通过开源、博客和视频分享知识。
我在 Outcome School 教 AI and Machine Learning。
我们开始吧。
大图景
在进入细节之前,我们先理解大图景。
当很多用户同时向 LLM 发送请求时,服务器必须处理所有这些请求。服务器运行在 GPU 上,而 GPU 很擅长并行做很多小任务。为了更好地利用 GPU,服务器会把请求合并成一个 batch,一起处理。
旧的 batching 方式叫 Static Batching,它会让 GPU 等待。新的方式叫 Continuous Batching,它会让 GPU 在每一步都保持忙碌。
简单说:
Continuous Batching = 不等整个 batch 全部完成,而是在任意旧请求完成的瞬间,就把新请求加入 batch。
就这么一个想法,可以让 LLM 服务器用同样的硬件服务更多用户。
快速回顾:LLM 如何生成 token
要理解 Continuous Batching,我们必须先理解 LLM 如何生成回复。
token 是一小段文本。它可以是一个词、词的一部分,或者单个字符。
LLM 生成文本是一次一个 token。为了生成下一个 token,它会查看目前为止的所有 token,并预测最可能的下一个 token。然后把这个 token 加到文本里,再预测下一个。这个过程会持续到模型决定停止。
推理有两个阶段:
- Prefill 阶段: 模型一次性读取整个 prompt,并在一次前向传播(forward pass)中一起处理所有输入 token。在这次前向传播里,它会为每个 prompt token 计算 Key 和 Value,并把它们存进 KV Cache。prefill 结束时,模型会产生第一个输出 token。每个请求只会在开始时发生一次 prefill。
- Decode 阶段: 模型一次生成一个 token。每个新 token 都是一步。一个 200 token 的回复,就意味着 200 个 decode step。
对我们来说,关键点是:生成一个回复不是一个大任务,而是几百个小步骤,每一步生成一个 token。
因为回复是每一步产生一个 token,服务器可以在每个 token 准备好的一刻就把它发送给用户,而不是等整段回复全部完成。我们有一篇文章介绍 Token Streaming 如何工作,解释了这一点。
为了让 decode 更快,服务器会为每个请求保存一个叫 KV Cache 的东西。KV Cache 保存目前为止已经看过的所有 token 的 Key 和 Value,所以模型不必在每一步重新计算它们。batch 中的每个请求都有自己的 KV Cache。如果想深入了解,可以读我们关于 KV Cache 的详细文章。
如果想深入学习 LLM 内部机制、KV Cache 和 Tokenization,可以看看 Outcome School 的 AI and Machine Learning Program。
为什么 batching 对 LLM 很重要
LLM 运行在 GPU 上。GPU 很擅长同时对多份数据做同一种工作。如果我们只给 GPU 一个请求处理,它的大部分能力都会闲置。
为了让 GPU 保持忙碌,服务器会同时处理多个请求。这个请求组就叫一个 batch。
假设 8 个用户同时发送请求。服务器不会一个接一个地跑,而是把这 8 个请求作为一个 batch 一起跑。GPU 为这 8 个请求做完工作,大致只需要处理 1 个请求的时间。
这就是 batching 重要的原因:它让单个 GPU 可以同时服务多个用户,而且不会让每个用户等待太久,不会比自己独占 GPU 时慢很多。
但这里有个问题。我们怎么组成 batch?什么时候开始?什么时候结束?这就是 Static Batching 和 Continuous Batching 的区别所在。
旧做法:Static Batching
在 Static Batching 中,服务器会收集一组固定请求,把它们一起运行,并等它们全部完成之后,才开始新的 batch。
我们看看它怎么工作:
- 服务器收集比如 4 个请求组成一个 batch。
- 它一起运行这 4 个请求的 prefill 阶段。
- 然后运行 decode 阶段。每个 decode step 都会为这 4 个请求各产生一个新 token。
- 它会持续 decode,直到 batch 中每个请求都完成。
- 只有这 4 个请求全部完成以后,服务器才会开始新的 batch。
这种方式简单,也容易实现。但它有一个严重问题。
Static Batching 的问题
问题在于,不同请求完成的时间差异很大。
一个用户可能问“2 + 2 等于多少?”,回复只有几个 token。另一个用户可能问“写一篇关于气候变化的详细文章。”,回复有 1,000 个 token。
如果这两个请求在同一个 batch 里,短请求可能在 5 个 decode step 后就完成了。但 batch 还要继续运行几百步,因为长请求还没结束。
短请求占用的那个 slot 会怎么样?它就空在那里。 GPU 仍然会为这个空 slot 做工作,但这份工作不会产生任何有用结果。它被浪费了。
看到问题了吗?
在 batch 生命周期的大部分时间里,只有少数 slot 在做有用工作。剩下的 slot 都是空的,在等最慢的请求完成。GPU 看起来很忙,但大部分工作都浪费了。
而那些中途到来的新用户,必须等当前整个 batch 全部完成,才能开始处理。
于是,Continuous Batching 就派上用场了。
什么是 Continuous Batching?
Continuous Batching 是一种运行 batch 的方式:服务器不会等待整个 batch 完成。一旦 batch 中有任意请求完成,服务器就立刻用队列里等待的新请求替换它。
batch 永远不会闲置。每个 slot 都始终在为某个请求做有用工作。
不要再把 batch 想成“一组一起开始、一起结束的请求”,而要把它想成“一组永远有人坐着的 slot”。
核心想法很简单:
Static Batching 在请求级别工作。Continuous Batching 在 token 级别工作。
在 Static Batching 中,工作单位是“一个完整请求”。batch 在所有请求开始时开始,在所有请求结束时结束。
在 Continuous Batching 中,工作单位是“当前 batch 中这些请求的一次 decode step”。每一个 decode step 之后,服务器都会检查:有没有请求完成?如果有,就把它换出去。有没有新请求到达?如果有,就把它放进空 slot。
拼车类比
我们用一个现实世界的类比来理解。
想象一辆有 4 个座位的拼车。车沿着一条长路线行驶,乘客到站后一个个下车。
Static Batching 像这样: 一开始 4 个乘客上车。车会在每一站等到所有 4 个乘客都到达目的地。即使某个乘客 5 分钟后就到了,他的座位在剩下的行程里也一直空着。路边等待的新乘客不能上车,必须等整趟行程结束、车回来接下一组 4 个人。
Continuous Batching 像这样: 一开始 4 个乘客上车。只要有任何一个乘客到站下车,路边等待的新乘客就立刻坐进这个空座。车里始终有 4 个乘客。没有座位会空很久。
哪辆车一天能服务更多乘客?显然是第二辆。座位是昂贵资源,我们要让每个座位始终坐满。
座位就是 GPU slot。乘客就是请求。乘客坐车的时间,就是这个请求会生成的 token 数量。Continuous Batching 会让每个 GPU slot 在每一步都保持满载。
Continuous Batching 如何一步步工作
我们一步步看支持 Continuous Batching 的服务器如何处理请求。
第 1 步: 服务器在 batch 中有固定数量的 slot。假设是 4 个 slot。
第 2 步: 请求进入队列。只要 batch 里有空 slot,服务器就从队列里取出下一个请求,放进这个 slot。服务器会为新请求运行 prefill 阶段,准备它的状态。
第 3 步: 服务器为当前 batch 中的所有请求运行一个 decode step。每个请求都会得到一个新 token。
第 4 步: decode step 之后,服务器检查每个请求。如果模型产生 stop token,或者达到最大长度,请求就算完成。服务器会把完成的请求从 slot 中移除,并把结果发回给用户。
第 5 步: 服务器查看队列。如果有等待中的请求,它就进入刚空出来的 slot。服务器为它运行 prefill。
第 6 步: 服务器运行下一个 decode step。然后从第 4 步重复。
整个流程如下:
+-----------+
| Queue | <- new requests arrive here
+-----------+
|
| pull when a slot is free
v
+-------------------------------+
| Batch (4 slots) |
| [Slot 1] [Slot 2] |
| [Slot 3] [Slot 4] |
+-------------------------------+
|
v
+-------------------------------+
| Decode step |
| (one new token per slot) |
+-------------------------------+
|
v
+-------------------------------+
| Check each slot: |
| - finished? remove and send |
| - free? pull from queue |
+-------------------------------+
|
+---> loop back to Decode step
这个循环会一直运行。每一个 decode step,服务器都会检查完成的请求和新到达的请求。batch 始终是满的。GPU 始终在做有用工作。
给你一个小提示
无论你在哪个技术领域工作,都应该熟悉这些主题:
- LLM
- RAG
- MCP
- Agent
- Fine-tuning
- Quantization
我们把它们整理进了一个视频:
AI Engineering Explained: LLM, RAG, MCP, Agent, Fine-Tuning, and Quantization
不用现在停下来去看——先收藏,等有时间再看。未来的你会感谢现在的你。
现在,我们回到主题。
一个数字例子
我们用真实数字来把这件事放到具体场景里看。
假设服务器有 4 个 slot。来了 4 个请求:
- Request A:需要 100 个 decode step
- Request B:需要 20 个 decode step
- Request C:需要 50 个 decode step
- Request D:需要 200 个 decode step
新请求 E 在 batch 刚开始后到达。E 需要 30 个 decode step。
使用 Static Batching:
batch 从 A、B、C、D 开始。E 在队列里等待。下面是每一步 slot 的样子([.] 表示空 slot,正在做浪费的工作):
Slot 1 Slot 2 Slot 3 Slot 4 Queue
Step 0: [A] [B] [C] [D] [E]
Step 20: [A] [.] [C] [D] [E] <- B done, slot sits empty
Step 50: [A] [.] [.] [D] [E] <- C done, two slots empty
Step 100: [.] [.] [.] [D] [E] <- A done, three slots wasted
Step 200: [.] [.] [.] [.] [E] <- D done, batch ends
从第 20 步开始,GPU 大部分时间都在空 slot 上做浪费的工作。E 这段时间一直在队列里等待。直到第 200 步之后,E 才终于拿到 slot 开始处理。
使用 Continuous Batching:
batch 从 A、B、C、D 开始。E 在队列里等待。
Slot 1 Slot 2 Slot 3 Slot 4 Queue
Step 0: [A] [B] [C] [D] [E]
Step 20: [A] [E] [C] [D] [ ] <- B done, E slots in immediately
Step 50: [A] [.] [.] [D] [ ] <- E and C both finish
Step 100: [.] [.] [.] [D] [ ] <- A done
Step 200: [.] [.] [.] [.] [ ] <- D done
E 在第 50 步完成,而不是等到第 200 步才开始。这里 slot 会空下来,只是因为队列里已经没有请求了。在有稳定流量的真实服务器里,每个 slot 在每一步都会保持满载,GPU 会持续做有用工作。
真实数字与加速效果
我们再用真实数字来算清楚能省多少。
假设:
- GPU 上每个 decode step 需要 50 ms
- batch size 是 4 个 slot
- 使用上面例子里的同 5 个请求(A=100、B=20、C=50、D=200、E=30 token)
不使用 Continuous Batching(Static):
Round 1: A, B, C, D in batch. Batch runs until all done = 200 steps.
Time: 200 x 50 ms = 10,000 ms = 10.0 s
Round 2: E (alone in next batch). Runs 30 steps.
Time: 30 x 50 ms = 1,500 ms = 1.5 s
Total time to finish all 5 requests: 11.5 s
Total tokens generated: 100 + 20 + 50 + 200 + 30 = 400 tokens
使用 Continuous Batching:
All 5 requests finish in one continuous run.
E slots in at step 20 and finishes at step 50.
D, the longest, finishes at step 200.
Total time to finish all 5 requests: 200 x 50 ms = 10,000 ms = 10.0 s
Total tokens generated: 400 tokens
现在比较用户实际感受到的东西:每个请求的响应时间。
Static Batching Continuous Batching
Request A (100): 5.0 s 5.0 s
Request B (20): 1.0 s 1.0 s
Request C (50): 2.5 s 2.5 s
Request D (200): 10.0 s 10.0 s
Request E (30): 11.5 s 2.5 s <- 4.6x faster
Average: 6.0 s 4.2 s
用户 E 原本要等前一个 batch 结束 10 秒后才能开始。使用 Continuous Batching 后,E 会立刻进入空 slot,并快 4.6 倍完成。
现在,我们看看它对现实工作负载的影响。
假设有 100 个请求到达服务器:
- 50 个短请求(每个 20 token)
- 50 个长请求(每个 200 token)
- 总共要生成的 token 数:50 x 20 + 50 x 200 = 11,000 token
这里的关键指标是吞吐量(throughput),也就是服务器每秒生成的 token 数。吞吐量越高,同样硬件上能服务的用户越多。计算如下:
Without Continuous Batching:
Each batch of 4 has on average 2 short + 2 long requests.
The batch runs until the longest finishes = 200 steps.
Number of batches: 100 / 4 = 25 batches
Time per batch: 200 x 50 ms = 10 s
Total time: 25 x 10 = 250 s
Throughput: 11,000 tokens / 250 s = 44 tokens/sec
With Continuous Batching:
Slots stay full because the queue keeps feeding new requests.
Each step produces 1 token per slot = 4 tokens per step.
Steps needed: 11,000 / 4 = 2,750 steps
Total time: 2,750 x 50 ms = 137,500 ms = 137.5 s
Throughput: 11,000 tokens / 137.5 s = 80 tokens/sec
Speedup: 80 / 44 = 1.8x
Time saved: 250 - 137.5 = 112.5 s (45% less time for the same work)
下面是同一组对比放到时间线上:
Time (seconds): 0 50 100 150 200 250
Without: |==========================================| 250 s
(25 batches of 4, each running 200 steps)
With: |=======================| 137.5 s
(continuous, 4 slots always full)
<--- 112.5 s saved --->
服务器用 137.5 秒完成了同样的工作,而不是 250 秒。硬件不变,完成的工作几乎翻倍。这就是 Continuous Batching 的妙处。
在实践中,对多数真实工作负载来说,加速通常在 2 倍到 4 倍 之间。具体数字取决于:
- 请求长度差异有多大(差异越大,收益越大)
- 进入流量有多稳定(流量越稳定,收益越大)
- 模型和硬件
来自 vLLM 等系统的公开基准测试显示,Continuous Batching 结合 Paged Attention 后,相比朴素服务方式可以提供超过 20 倍的吞吐量;在同一块 GPU 上,相比 Static Batching 也能高 2 到 4 倍。
如果想端到端深入学习 vLLM、Paged Attention 和推理优化,我们有一套完整课程——可以看看 Outcome School 的 AI and Machine Learning Program。
Continuous Batching 的好处
Continuous Batching 带来三个主要好处:
- 更高吞吐量: throughput 是服务器每秒生成的 token 数。因为 GPU 始终在做有用工作,所以每秒能生成更多 token。在真实系统里,相比 Static Batching,吞吐量往往能提升数倍。
- 更低等待时间: 新请求不必等待整个 batch 完成。只要有座位空出来,它就能立刻进入。这会让用户感觉服务器更快。
- 更好的硬件利用率: GPU 是 LLM 服务器里最昂贵的部分。Continuous Batching 让它始终忙于有用工作,所以同一块 GPU 能发挥更大价值。
这就是为什么几乎所有现代 LLM 服务系统,比如 vLLM、SGLang、TensorRT-LLM 等,都会使用 Continuous Batching。
几点重要说明
- Continuous Batching 和 Paged Attention 配合得很好。Paged Attention 解决 KV Cache 的内存浪费问题,Continuous Batching 解决静态 batch 的计算浪费问题。两者结合,正是单个 GPU 能同时服务很多用户的原因。我们有一篇详细介绍 vLLM 如何工作的文章,解释了服务系统如何把这两者组合起来。
- Continuous Batching 不会改变模型本身。它不会改变注意力机制的工作方式,不会改变 token 的生成方式,也不会改变模型输出。它只改变服务器如何在 GPU 上调度请求。每个用户得到的输出,和单独运行这个请求时一样。
- batch size 不是无限的。GPU 内存有限,batch 中每个请求都要为自己的 KV Cache 使用内存。服务器会选择一个能放进内存的 batch size(批大小)。如果 batch 已满,新请求就会在队列里等待,直到有 slot 空出来。
- 例子里的数字为了说明清楚做了一些简化。当新请求加入 continuous batch 时,会有一小段 prefill 成本,我们把它当作 0;而 Static Batching 中 2 个短请求 + 2 个长请求的组合,只是一个平均情况,真实 batch 会围绕它波动。但定性规律不变:当请求长度差异很大时,Continuous Batching 通常能带来数倍收益。
快速总结
我们回顾一下学到的内容:
- LLM 一次生成一个 token。 一个回复是几百个小 decode step,不是一个大任务。
- Batching 很重要,因为 GPU 最擅长同时对多份数据做同一种工作。
- Static Batching 会把一组固定请求一起运行,并等待最慢的请求完成。提前完成的 slot 会空着,浪费 GPU 工作。
- Continuous Batching 会在任何旧请求完成的一刻换入新请求。batch 始终是满的。
- 拼车类比: Static Batching 是一辆等所有乘客都到站才结束行程的车。Continuous Batching 是一辆只要有座位空出就接新乘客的车。
- token 级调度: Continuous Batching 在 token 级别工作,而不是请求级别。每个 decode step,服务器都会检查完成和新到达的请求。
- 好处: 更高吞吐量、更低等待时间,以及更好的 GPU 利用率。
- 和 Paged Attention 配合很好: 它们一起解决了 LLM 服务中的内存浪费和计算浪费问题。
这就是 Continuous Batching 如何让 GPU 保持忙碌,并让单台服务器同时处理更多用户。
准备 AI 工程面试可以看这里:AI Engineering Interview Questions
这次就到这里。
谢谢
Amit Shekhar Outcome School 创始人