@pallavishekhar_: LLM中的连续批处理 阅读:https://outcomeschool.com/blog/continuous-batching-in-llms…
摘要
一篇介绍连续批处理的博客文章,该技术通过动态地将新请求添加到已完成请求的批次中,持续保持GPU忙碌并减少空闲时间,从而提高LLM服务吞吐量。
查看缓存全文
缓存时间: 2026/06/30 15:45
大语言模型中的连续批处理
来源:https://outcomeschool.com/blog/continuous-batching-in-llms 大语言模型中的连续批处理
在本博客中,我们将学习连续批处理(Continuous Batching)——一种让 LLM 服务器在生成过程的每一步都保持 GPU 忙碌,从而能够同时处理更多用户的技术。
我们将涵盖以下内容:
- 全局概览
- 快速回顾:LLM 如何生成 Token
- 为什么批处理对 LLM 很重要
- 旧方法:静态批处理
- 静态批处理的问题
- 什么是连续批处理?
- 拼车类比
- 连续批处理分步工作原理
- 数值示例
- 实际数据与加速效果
- 连续批处理的好处
- 几点重要说明
- 快速总结
我是 Amit Shekhar,Outcome School (https://outcomeschool.com/) 的创始人。我曾教导和指导过许多开发者,帮助他们获得了高薪技术岗位,协助多家科技公司解决了独特问题,并创建了许多被顶级公司使用的开源库。我热衷于通过开源、博客和视频分享知识。
我在 Outcome School 教授 人工智能与机器学习 (https://outcomeschool.com/program/ai-and-machine-learning)。
让我们开始吧。
全局概览
在深入细节之前,我们先理解整体图景。
当许多用户同时向 LLM 发送请求时,服务器必须处理所有请求。服务器运行在 GPU 上,而 GPU 非常擅长并行执行许多小任务。为了充分利用 GPU,服务器将请求分组为批 (batch) 并同时处理它们。
老的批处理方法称为静态批处理 (Static Batching),它会让 GPU 等待。新方法称为连续批处理 (Continuous Batching),它能在每一步都保持 GPU 忙碌。
简单来说:
连续批处理 = 一旦某个旧请求完成,立即将新请求加入批次,而不是等待整个批次完成。
这个简单的想法使得 LLM 服务器能用相同的硬件服务更多用户。
快速回顾:LLM 如何生成 Token
要理解连续批处理,我们必须先了解 LLM 如何生成响应。
一个 token 是一小段文本。它可以是一个单词、单词的一部分或一个字符。
LLM 一次生成一个 token。为了生成下一个 token,它会查看到目前为止的所有 token,并预测最可能的下一个 token。然后将该 token 添加到文本中,再预测下一个,直到模型决定停止。
推理分为两个阶段:
- 预填充阶段 (Prefill phase): 模型一次读取整个提示,并在单个前向传播中处理所有输入 token。在此过程中,它会计算每个提示 token 的键和值,并将其存储在 KV 缓存 (https://outcomeschool.com/blog/kv-cache-in-llms) 中。预填充结束时,模型产生第一个输出 token。每个请求仅发生一次,在开始时。
- 解码阶段 (Decode phase): 模型一次生成一个 token。每个新 token 是一步。200 个 token 的响应意味着 200 个解码步骤。
对我们来说关键点是:生成响应不是一个大任务,而是数百个小步骤,每一步产生一个 token。
由于响应是逐步生成的,服务器可以在每个 token 准备好时立即将其发送给用户,而无需等待整个回复完成。我们有一篇关于 Token 流式传输如何工作 (https://outcomeschool.com/blog/how-does-token-streaming-work) 的博客,解释了其原理。
为了加快解码速度,服务器为每个请求存储一个 KV 缓存 (https://outcomeschool.com/blog/kv-cache-in-llms)。KV 缓存保存了到目前为止所有已见 token 的键和值,这样模型就不必在每一步都重新计算它们。批次中的每个请求都有自己的 KV 缓存。如果想深入了解,可以阅读我们关于 KV 缓存的详细博客 (https://outcomeschool.com/blog/kv-cache-in-llms)。
要深入学习 LLM 内部原理、KV 缓存和分词,请查看我们在 Outcome School 的人工智能与机器学习课程 (https://outcomeschool.com/program/ai-and-machine-learning)。
为什么批处理对 LLM 很重要
LLM 运行在 GPU 上。GPU 非常擅长在大量数据上同时执行相同类型的计算。如果只给 GPU 一个请求处理,大部分算力会闲置。
为了让 GPU 保持忙碌,服务器会一起处理多个请求。这个组称为批 (batch)。
假设 8 个用户同时发送请求。服务器不会逐一处理,而是将 8 个请求作为一个批次一起运行。GPU 处理所有 8 个请求所需的时间大致与处理 1 个请求相同。
这就是批处理的重要性:它让单个 GPU 能同时服务多个用户,而不会让每个用户的等待时间比单独处理长太多。
但这里有个问题。我们如何组成批次?何时开始?何时结束?这就是静态批处理和连续批处理的区别所在。
旧方法:静态批处理
在静态批处理中,服务器收集一组固定的请求,一起运行,并等待所有请求完成后再开始新的批次。
让我们看看它的工作方式:
- 服务器将(例如)4 个请求收集到一个批次中。
- 同时为所有 4 个请求运行预填充阶段。
- 然后运行解码阶段。每个解码步骤为 4 个请求各产生一个新 token。
- 一直解码直到批次中的每个请求都完成。
- 仅在所有 4 个请求完成后,服务器才启动新批次。
这种方法简单且易于实现,但它有一个严重问题。
静态批处理的问题
问题是不同请求完成时间差异很大。
一位用户可能问“2+2 等于几?”只有几个 token 的响应。另一位用户可能要求“写一篇关于气候变化的详细论文。”响应有 1000 个 token。
如果两者都在同一个批次中,短请求在 5 个解码步骤后就完成。但批次会继续运行数百步,因为长请求仍在进行。
短请求使用的槽位会怎样?它就空在那里。 GPU 仍然会为那个空槽位干活,但产生的都是无用功。这是一种浪费。
看到问题了吗?
在批次的大部分生命周期中,只有少数槽位在做有用功。其余都是空的,在等待最慢的请求完成。GPU 很忙,但大部分工作被浪费了。
而中途到达的新用户必须等待整个当前批次结束后才能开始。
所以,连续批处理来拯救了。
什么是连续批处理?
连续批处理是一种运行批次的方式,服务器不会等待整个批次结束。一旦批次中的某个请求完成,服务器立即用队列中等待的新请求替换它。
批次永远不会空闲。每个槽位总是在为某个请求做有用功。
我们不再将批次视为“一起开始和结束的一组请求”,而是将其视为“一组始终有人占据的槽位”。
关键思想很简单:
静态批处理在请求级别工作。连续批处理在 token 级别工作。
在静态批处理中,工作单元是“一个完整的请求”。批次在所有请求开始时开始,在所有请求结束时结束。
在连续批处理中,工作单元是“当前批次中某个人的一个解码步骤”。每完成一个解码步骤,服务器就会检查:有人完成了吗?如果有,就换出它们。有新请求到达吗?如果有,就塞入它们。
让我们用一个现实世界的类比来理解。
想象一辆有 4 个座位的共享出租车。出租车沿着一条长路线行驶,并在乘客到达各自目的地时逐个放下他们。
静态批处理是这样的: 一开始 4 位乘客上车。出租车在每个站点等待,直到所有 4 位乘客都到达目的地。即使其中一位乘客在 5 分钟后到达目的地,他的座位在剩下的路程中也会空着。在路上等待的新乘客无法上车,直到整个行程结束,出租车返回去接下一组 4 位乘客。
连续批处理是这样的: 一开始 4 位乘客上车。只要任何乘客到达目的地并下车,在路上等待的新乘客就会立即占据那个空座位。出租车始终有 4 位乘客。没有一个座位会长时间空着。
哪辆出租车一天内服务的乘客更多?显然是第二辆。座位是昂贵的资源,我们要始终保持每个座位满员。
座位是 GPU 槽位。乘客是请求。乘客在出租车上的时间是该请求生成的 token 数。连续批处理在每一步都保持每个 GPU 槽位满负荷。
连续批处理分步工作原理
让我们一步步看服务器如何使用连续批处理处理请求。
第 1 步: 服务器在批次中有固定数量的槽位。假设 4 个槽位。
第 2 步: 请求到达队列。只要批次中有空闲槽位,服务器就从队列中取出下一个请求并将其放入该槽位。服务器为新请求运行预填充阶段以准备其状态。
第 3 步: 服务器为当前批次中的所有请求运行一个解码步骤。每个请求获得一个新 token。
第 4 步: 解码步骤后,服务器检查每个请求。当模型产生停止 token 或达到最大长度时,请求完成。如果请求完成,服务器将其从槽位中移除,并将结果发送回用户。
第 5 步: 服务器查看队列。如果有等待的请求,它就会进入现在空闲的槽位。服务器为其运行预填充。
第 6 步: 服务器运行下一个解码步骤。从第 4 步重复。
整个流程如下所示:
+-----------+
| 队列 | <- 新请求到达这里
+-----------+
|
| 当有空闲槽位时拉取
v
+-------------------------------+
| 批次 (4 个槽位) |
| [槽位1] [槽位2] |
| [槽位3] [槽位4] |
+-------------------------------+
|
v
+-------------------------------+
| 解码步骤 |
| (每个槽位一个新 token) |
+-------------------------------+
|
v
+-------------------------------+
| 检查每个槽位: |
| - 完成?移除并发送 |
| - 空闲?从队列拉取 |
+-------------------------------+
|
+---> 循环回到解码步骤
这个循环持续运行。每单个解码步骤,服务器都会检查完成情况和新的到达。批次始终是满的。GPU 总是在做有用功。
一点提醒
无论你在哪个技术领域工作,都要熟悉这些主题:
- LLM
- RAG
- MCP
- Agent
- 微调 (Fine-tuning)
- 量化 (Quantization)
我们在一个视频中把它们放在一起:
AI Engineering Explained: LLM, RAG, MCP, Agent, Fine-Tuning, and Quantization (https://www.youtube.com/watch?v=lnfWvX66FUk)
不必停止阅读——先收藏,等你有时间再看。未来的你会感谢你。
现在,让我们回到正题。
数值示例
让我们用实际数字来比较。
假设服务器有 4 个槽位。四个请求到达:
- 请求 A:需要 100 个解码步骤
- 请求 B:需要 20 个解码步骤
- 请求 C:需要 50 个解码步骤
- 请求 D:需要 200 个解码步骤
一个新请求 E 在批次开始后不久到达。E 需要 30 个解码步骤。
使用静态批处理:
批次以 A、B、C、D 开始。E 在队列中等待。以下是每一步的槽位情况([.] 表示空槽位,做无用功):
槽位1 槽位2 槽位3 槽位4 队列
第 0 步: [A] [B] [C] [D] [E]
第 20 步: [A] [.] [C] [D] [E] <- B 完成,槽位空着
第 50 步: [A] [.] [.] [D] [E] <- C 完成,两个槽位空着
第 100 步:[.] [.] [.] [D] [E] <- A 完成,三个槽位浪费
第 200 步:[.] [.] [.] [.] [E] <- D 完成,批次结束
从第 20 步开始,GPU 大部分时间在空槽位上做无用功。E 在这段时间一直排队。直到第 200 步之后,E 才终于得到一个槽位开始。
使用连续批处理:
批次以 A、B、C、D 开始。E 在队列中等待。
槽位1 槽位2 槽位3 槽位4 队列
第 0 步: [A] [B] [C] [D] [E]
第 20 步: [A] [E] [C] [D] [ ] <- B 完成,E 立即入槽
第 50 步: [A] [.] [.] [D] [ ] <- E 和 C 都完成
第 100 步:[.] [.] [.] [D] [ ] <- A 完成
第 200 步:[.] [.] [.] [.] [ ] <- D 完成
E 在第 50 步完成,而不是等到第 200 步才开始。这里槽位空着只是因为队列中没有请求了。在实际流量稳定的服务器中,每个槽位在每一步都保持满负荷,GPU 持续做有用功。
实际数据与加速效果
让我们用实际数字来直观感受一下节省了多少。
假设:
- 每个解码步骤在 GPU 上耗时 50 ms
- 批次大小为 4 个槽位
- 使用上面示例中的相同 5 个请求(A=100, B=20, C=50, D=200, E=30 个 token)
没有连续批处理(静态):
第 1 轮:A、B、C、D 在一个批次中。批次运行直到全部完成 = 200 步。
时间:200 x 50 ms = 10,000 ms = 10.0 秒
第 2 轮:E(单独在下一个批次)。运行 30 步。
时间:30 x 50 ms = 1,500 ms = 1.5 秒
完成所有 5 个请求的总时间:11.5 秒
生成的 token 总数:100 + 20 + 50 + 200 + 30 = 400 token
使用连续批处理:
所有 5 个请求在一次连续运行中完成。
E 在第 20 步入槽,在第 50 步完成。
最长的 D 在第 200 步完成。
完成所有 5 个请求的总时间:200 x 50 ms = 10,000 ms = 10.0 秒
生成的 token 总数:400 token
现在让我们比较每个用户实际感受到的响应时间。
静态批处理 连续批处理
请求 A (100): 5.0 秒 5.0 秒
请求 B (20): 1.0 秒 1.0 秒
请求 C (50): 2.5 秒 2.5 秒
请求 D (200): 10.0 秒 10.0 秒
请求 E (30): 11.5 秒 2.5 秒 <- 快 4.6 倍
平均: 6.0 秒 4.2 秒
用户 E 等待了 10 秒钟,直到之前的批次结束才得以开始。而使用连续批处理,E 立即入槽,完成速度提高了 4.6 倍。
现在,看看在真实工作负载下的影响。
假设服务器收到 100 个请求:
- 50 个短请求(每个 20 个 token)
- 50 个长请求(每个 200 个 token)
- 需要生成的 token 总数:50 x 20 + 50 x 200 = 11,000 token
关键指标是吞吐量 (throughput)——服务器每秒产生的 token 数。更高的吞吐量意味着在相同硬件上服务更多用户。计算结果如下:
没有连续批处理:
每个 4 个请求的批次平均有 2 个短 + 2 个长请求。
批次运行直到最长的完成 = 200 步。
批次数量:100 / 4 = 25 个批次
每批时间:200 x 50 ms = 10 秒
总时间:25 x 10 = 250 秒
吞吐量:11,000 token / 250 秒 = 44 token/秒
有连续批处理:
槽位保持满负荷,因为队列不断提供新请求。
每一步每个槽位产生 1 个 token =
相似文章
在连续批处理中实现异步性
本文解释了如何为LLM推理实现异步连续批处理,将CPU批处理准备与GPU计算重叠,以最大化利用率并减少空闲时间。
基于阈值的LLM推理独占批处理
本文分析了混合批处理与独占批处理在LLM推理中的权衡,表明最优选择取决于GPU内存带宽。提出了一种基于阈值的混合调度器,可在两种方法间动态切换,在带宽受限的GPU上实现高达41.9%的吞吐量提升。
@amitiitbhu:新文章:vLLM 是如何工作的?请在此阅读:https://outcomeschool.com/blog/how-does-vllm-work…
一篇详细的博客文章,解释了 vLLM 的工作原理,包括 PagedAttention、KV 缓存管理和连续批处理,以实现高效的 LLM 服务。
@KL_Div:随着生成长度增加,LLM 占用的 GPU 内存持续攀升。能否在几乎不牺牲精度的前提下,让 GPU 内存占用保持恒定?
IceCache 通过“动态连续索引”(DCI)技术,在超长生成任务中将 GPU 内存占用压到恒定,且精度损失极小。
@_avichawla: LLM推理中的预填充与解码。你是否注意到,LLM的第一个令牌总是需要片刻才出现…
解释LLM推理的两个阶段——预填充和解码,详细说明GPU瓶颈如何从预填充时的计算受限转变为解码时的内存受限,以及KV缓存的重要性。