LLM 中的 Continuous Batching

Continuous Batching in LLMs

在这篇文章里,我们会学习 Continuous Batching。这是一种让 LLM 服务器在生成的每一步都让 GPU 保持忙碌,从而同时处理更多用户的技术。

我们会讲这些内容:

  • 大图景
  • 快速回顾:LLM 如何生成 token
  • 为什么 batching 对 LLM 很重要
  • 旧做法:Static Batching
  • Static Batching 的问题
  • 什么是 Continuous Batching?
  • 拼车类比
  • Continuous Batching 如何一步步工作
  • 一个数字例子
  • 真实数字与加速效果
  • Continuous Batching 的好处
  • 几点重要说明
  • 快速总结

我是 Amit ShekharOutcome 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 服务系统,比如 vLLMSGLangTensorRT-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 创始人