如何通过连续批处理(continuous batching)将LLM推理吞吐量提升23倍,同时减少延迟

在这篇博客中,我们将介绍大型语言模型(LLM)推理的基础知识,同时强调传统批处理策略中的低效性。我们将介绍连续批处理,并讨论现有批处理系统(如HuggingFace的text-generation-inference和vLLM)的基准测试结果。通过利用vLLM,用户可以在减少p50延迟的同时,实现23倍的推理吞吐量提升。

由于LLMs会有大量的GPU内存占用和计算成本,LLM推理占据实际应用大部分的计算成本。机器学习工程师通常将LLMs视为只能通过内部改进(如量化和自定义CUDA内核)来优化的"黑匣子"。然而,事实并非如此。因为LLMs是以迭代方式生成输出的,而且LLM推理通常是受内存限制的而不是计算限制的,所以有一些令人惊讶的系统级批处理优化可以在实际工作负载中产生10倍甚至更高的效果。

最近提出的一种这样的优化方法是连续批处理,也被称为动态批处理,或具有迭代级调度的批处理。我们想要了解这种优化的性能表现。我们将在下文详细讨论,包括模拟生产负载的方法,但简要概括一下我们的发现:

  • 使用连续批处理和针对连续批处理的内存优化(使用vLLM),吞吐量提高了多达23倍。

  • 利用连续批处理(在Ray Serve和Hugging Face的text-generation-inference中),吞吐量比简单批处理提高了8倍。

  • 利用优化的模型实现(NVIDIA的FasterTransformer),吞吐量比简单批处理提高了4倍。

你今天就可以尝试使用连续批处理:查看此示例,在Ray Serve上运行vLLM

本博客的剩余部分结构如下:

  • 我们将介绍LLM推理的基本原理,并强调传统的基于请求的动态批处理策略中的低效性。

  • 我们将介绍连续批处理以及它如何回应基于请求的动态批处理的许多低效路径。

  • 接着我们将讨论我们的基准测试和这对如何成本有效地提供LLM模型的影响。


LLM 推理基础知识

关于 LLM 推理有很多内容了解,可以到在单个 GPU 上进行高效推理优化故事:Bloom 推理以了解更多细节。然而,在高层次上,LLM 推理相当直接明了。

对于每个请求:

  1. 你从一个 Token 序列(称为"前缀"或"提示")开始。

  2. LLM 生成一系列完成 Token,直到产生 Stop Token 或达到最大序列长度才停止。

这是一个迭代过程。每进行一次新的模型向前传递,你都会得到一个额外的完成 Token。例如,假设你提示一句"What is the capital of California: ",这需要十个前向传递迭代才能得到完整的响应["S", "a", "c", "r", "a", "m", "e", "n", "t", "o"]。这个例子将事情简化了一点,因为实际上 Token 并没有1:1地映射到 ASCII 字符(一种流行的 Token 编码技术是字节对编码(Byte-Pair Encoding),这超出了本文的范围),但无论你如何对序列进行分词,生成的迭代性质都是一样的。

上图为简化的 LLM 推理。这个玩具示例展示了一个支持最大序列长度为8个 Token(T1、T2...T8)的假设模型。从提示 Token(黄色)开始,迭代过程一次生成一个 Token(蓝色)。一旦模型生成序列结束 Token(红色),生成循环停止。这个示例展示了一个仅包含一个输入序列的批次,因此批次大小为1。

现在我们了解了迭代过程的简单性,让我们深入了解一些你可能不知道的关于 LLM 推理的事情:

  1. 初始摄取("预填充")提示"What is the capital of California: "与生成每个后续 Token 大约需要相同的时间。这是因为预填充阶段预先计算了输入部分的注意力矩阵,而这部分生成过程中保持不变。预填充阶段有效利用了 GPU 的并行计算,因为这些输入可以彼此独立计算。

  2. LLM 推理内存 IO 是瓶颈,而不是计算。换句话说,目前将1MB 数据加载到 GPU 计算内核所需的时间要比 GPU 计算内核执行1MB 数据的 LLM 计算多。这意味着 LLM 推理吞吐量在很大程度上取决于你可以将多大的批次放入高带宽 GPU 内存。查看 NVIDIA 文档中的这个页面以获得更多详细信息。

  3. GPU 内存消耗与基本模型大小 + Token 序列长度成正比。在每个 LLM 开发者都应该知道的数据中,估计一个130 亿参数的模型对每个序列中的 Token 消耗近1MB 的状态。在 RAM 较高端的 A100 GPU(40GB 内存)上,可以推算出,在存储完 26GB 模型参数后还剩下 14GB,这意味着一次可以在内存中容纳约 14k 个 Token。这看起来可能很高,但实际上相当有限;如果我们将序列长度限制为 512,我们最多只能同时处理约 28 个序列。对于更长的序列长度,问题更加严重;序列长度为 2048 时,我们的批处理大小仅限于 7 个序列。注意,这是一个上限,因为它没有为存储中间计算腾出空间。

这意味着,如果你可以优化内存使用,那么所谓的"桌子上还有很大的空间"。这就是诸如 AutoGPTQ 之类的模型量化策略可能如此强大的原因;如果你通过从 16 位转换为 8 位表示来减少一半的内存使用,你可以将可用于更大批量大小的空间加倍。然而,并非所有策略都需要修改模型权重。例如,FlashAttention 通过重新组织注意力计算以减少内存 IO 的需求,从而显著改进了吞吐量。

连续批处理是另一种不需要修改模型的内存优化技术。接下来,我们将解释朴素批处理的运作方式(及其低效率),以及连续批处理如何提高 LLM 生成的内存效率。

LLM 批处理解释

GPU 是大规模并行计算架构,计算速率(以每秒浮点运算为单位,或者 flops)在 A100 的 teraflop 或甚至 H100 的 petaflop 范围内。尽管具有如此惊人的计算速度,LLM 仍然难以实现饱和和流式,因为芯片的大部分内存带宽都被用来加载模型参数。

批处理是一种改进方法;而不是每次输入序列时加载新的模型参数,你可以一次加载模型参数,然后使用它们来处理多个输入序列。这可以更有效地使用芯片的内存带宽,从而导致更高的计算利用率、更高的吞吐量和更低的 LLM 推理成本。

朴素批处理 / 静态批处理

我们称这种传统的批处理方法为静态批处理,因为批处理的大小在推理完成之前保持不变。以下是在 LLM 推理环境中静态批处理的一个示例:

使用静态批处理完成四个序列。在第一次迭代(左边)时,每个序列从提示 Token(黄色)生成一个 Token(蓝色)。经过几次迭代后(右边),已完成序列的大小各不相同,因为每个序列在不同的迭代中发出其序列结束 Token(红色)。尽管序列 3 在两个迭代后完成,但静态批处理意味着 GPU 在最后一个序列(本例中为序列 2)完成生成之前会处于低利用率状态。

与传统的深度学习模型不同,批处理对于LLM(长序列生成模型)来说可能会有些棘手,因为它们的推理过程具有迭代性。从直观上讲,这是因为有些请求会在批次完成之前"结束",但在这种情况下要释放它们所占用的资源并将其他处于不同完成状态的请求加入到这一批次中是非常棘手的。这意味着,当一个批次中不同序列的生成长度与该批次中最大生成长度不同时,GPU会被低效利用。在上面右边的图中,这可以通过序列1、3和4的序列结束标记后的白色方块来直观表示。

那么,静态批处理低效利用GPU的频率有多高呢?这取决于一批次中序列的生成长度。例如,我们可以利用LLM推理来输出一个单一的标记,作为一个分类任务(尽管有更好的方法来实现这一点,但让我们用这个例子作为说明)。在这种情况下,每个输出序列的大小都相同(1个标记)。如果输入序列的大小也相同(比如说,512个标记),那么每个静态批次将实现最佳的GPU利用率。

然而,一个由LLM驱动的聊天机器人服务不能假设固定长度的输入序列,也不能假设固定长度的输出序列。截至撰写本文时,专有模型提供了超过8K个标记的最大上下文长度。在静态批处理中,生成输出的方差可能会导致GPU的大量低效利用。难怪OpenAI首席执行官Sam Altman将计算成本形容为令人瞠目结舌

在没有对用户输入和模型输出进行严格限制的情况下,未优化的生产级LLM系统根本无法高效利用GPU并在低成本的情况下为网络请求提供服务。我们需要优化服务LLM的方式,使它们的优势得到广泛应用。

连续批处理

业界认识到了这种低效率,并提出了更好的方法。《Orca: A Distributed Serving System for Transformer-Based Generative Models》是OSDI '22中呈现的一篇论文,它是我们所知道的第一篇研究解决这个问题的论文。Orca并不等到一个批次中的所有序列完成生成,而是实现了迭代调度,批次大小在每次迭代中确定。这样,一旦一个批次中的序列完成生成,就可以将一个新序列插入到它的位置,从而实现比静态批处理更高的GPU利用率。

使用连续批处理完成七个序列。左图显示了一个迭代后的批次,右图显示了经过几次迭代后的批次。一旦一个序列生成了一个序列结束标记,我们就在它的位置插入一个新的序列(如序列S5, S6和S7)。这样可以实现更高的GPU利用率,因为GPU不必等待所有序列完成才开始新的序列。

实际情况比这个简化模型更复杂些:由于预填充阶段需要计算,并且具有与生成标记截然不同的计算模式,所以无法与生成标记轻松地进行批处理。当前的连续批处理框架通过超参数来解决这个问题:waiting_served_ratio,它表示等待预填充的请求与等待序列结束标记请求的比率。

谈到框架,Hugging Face已经将连续批处理产品化,应用于他们基于Rust和Python的文本生成推理LLM推理服务器。我们使用他们的实现来理解我们下面的基准测试中连续批处理的性能特征。

注意:连续批处理、动态批处理和迭代调度在含义上都非常接近,它们中的任何一个都可以用来描述批处理算法。在本文中,我们选择使用连续批处理。动态批处理确实很适合,但容易与请求级批处理混淆,后者指的是LLM推理服务器在当前批次完全完成生成后选择静态批次的大小。我们认为迭代调度能描述调度机制,但不能描述整个过程。

PagedAttention 与 vLLM

在这篇博客文章中,我们想要展示静态批处理和连续批处理之间的差异。事实证明,通过改进 Orca 的设计,连续批处理可以实现无法通过静态批处理实现的内存优化。

PagedAttention 是一种新的注意力机制,已在 vLLM中实现。它从传统的操作系统概念(如分页和虚拟内存)中汲取灵感。它们允许 KV 缓存(上面讨论的"预填充"阶段中计算的内容)通过将内存分配到固定大小的"页面"(或块)中而变得不连续。然后可以重写注意力机制以在块对齐输入的情况下进行操作,从而允许在不连续的内存范围上执行注意力。

这意味着缓冲区分配可以实时进行,而不是提前进行:当启动新的生成时,框架无需分配大小为 maximum_context_length 的连续缓冲区。在每次迭代时,调度程序可以决定是否需要为某个生成提供更多空间,并在不降低 PagedAttention 性能的情况下随时分配。这并不能保证内存的完美利用(他们的博客说浪费不足 4% ),但它大大改善了目前广泛使用的预先分配方案所带来的浪费。

总之,PagedAttention + vLLM 实现了巨大的内存节省,因为大多数序列都不会消耗整个上下文窗口。这些内存节省直接转化为更高的批量大小,这意味着更高的吞吐量和更便宜的服务。我们在下面的基准测试中包括了 vLLM。

基准测试设置

我们将讨论我们的实验设置,然后深入研究我们的基准测试结果。

实验

我们的目标是了解连续批处理在模拟真实世界在线推理工作负载下与静态批处理相比的表现。从根本上说,我们关注成本。我们将成本分解为吞吐量和延迟,因为成本直接受到您能在给定延迟下提供服务的效率的影响。

基准测试目标

评估指标 评估方法
测量吞吐量 处理包含512个输入token的1000个请求队列的耗时,生成长度从指数分布中抽样。
测量延迟 在固定的平均速率下,不同输入长度、输出长度和到达时间的100个请求的延迟。

我们将在各自的结果部分讨论实验数据集和其他细节。

硬件/模型

我们在Anyscale提供的单个NVIDIA A100 GPU上测试吞吐量和延迟。我们的A100具有40GB的GPU RAM。我们选择Meta的OPT-13B模型,因为每个测试框架都有一个与该模型的集成。我们选择了13B版本,因为它适合我们的GPU,而无需使用张量并行性,同时又足够大以导致内存效率方面的挑战。我们选择不使用将每个Transformer分块分布在多个GPU的张量并行性,以保持实验的简单性,尽管静态批处理和连续批处理都支持张量并行性。

框架

我们测试了两个静态批处理框架和三个连续批处理框架。我们的静态批处理框架包括:

  • Hugging Face的Pipelines。这是最简单的推理解决方案。它提供具有易于使用的API的静态批处理,可与任何模型一起使用,并支持比简单文本生成更多的任务。我们将此作为基准。
  • NVIDIA的FasterTransformer。这是一个库,提供了各种Transformer模型的优化实现。它目前只提供静态批处理(Triton推理服务器提供请求级动态批处理,但尚未提供连续批处理)。这使我们了解在极限优化情况下,某一模型静态批处理的发展潜力------相较于Hugging Face Hub上提供的相对未优化的OPT-13B实现,它提供了一个更具竞争力的基准。

我们的连续批处理框架有:

  • Hugging Face 的 text-generation-inference:这是 Hugging Face 用来支持他们的 LLM 实时推理 API 的推理服务器。它实现了连续批处理。

  • Ray Serve 上的连续批处理:Ray Serve 利用 Ray 的无服务器功能提供无缝自动扩展、高可用性和复杂 DAG 支持。我们想了解连续批处理的工作原理,于是在 Ray Serve 上用纯 Python 重新实现了 text-generation-inference 的核心连续批处理逻辑。如我们的结果所示,我们的实现达到了与 text-generation-inference 相同的性能,证实了我们的理解。

  • vLLM:这是一个由 UC Berkeley 的小伙伴们最近发布的开源项目(GitHub)。它通过完全控制动态内存分配,在 GPU 内存碎片化方面实现了显著降低,从而优化了基于 Orca 的连续批处理设计。我们测试这个框架,是因为它展示了通过迭代级调度和连续批处理所实现的进一步优化的影响。

基准测试结果:吞吐量

基于我们对静态批处理的理解,当每批次序列长度的方差较大时,我们预计连续批处理的性能要明显优于静态批处理。为了证明这一点,我们为每个框架运行四次吞吐量基准测试,每次在具有较高序列长度方差的数据集上进行。

为了做到这一点,我们创建了一个包含1000个序列的数据集,每个序列具有512个输入token。我们将模型配置为始终发出每个序列的生成长度,通过忽略序列结束token并配置 max_tokens。然后,我们生成1000个生成长度,每个请求一个,采样自平均值为 128 个token的指数分布。我们使用指数分布,因为它是在为类似 ChatGPT 的应用提供服务时可能遇到的生成长度的一个很好的近似。为了改变每次运行的方差,我们只选择指数分布中小于或等于 32、128、512 和 1536 的样本。然后,总输出序列长度最多为 512+32=544、512+128=640、512+512=1024 和 512+1536=2048(我们模型的最大序列长度)。

接着,我们使用一个简单的 asyncio Python 基准测试脚本向我们的模型服务器提交 HTTP 请求。基准测试脚本以突发式提交所有请求,以使计算资源饱和。

以下是实验结果:

随着序列长度方差的增加,每个框架每秒吞吐的 token 数。

正如预期的那样,对于较低方差生成长度,静态批处理器和朴素连续批处理器的性能几乎相同。然而,随着方差的增加,朴素静态批处理的性能急剧下降至 81 token/s。相比之下,FasterTransformers 在朴素静态批处理上有了很大的提高,几乎与朴素连续批处理保持同步,直到生成长度限制为 1536。Ray Serve 和 text-generation-inference 上的连续批处理性能大致相同,这符合我们的预期,因为它们使用了相同的批处理算法。

这里最令人印象深刻的是 vLLM。在每个数据集上,vLLM 的性能相对于朴素连续批处理提高了一倍以上。我们还没有分析哪种优化对 vLLM 性能的影响最大,但我们怀疑 vLLM 通过动态预留空间而不是提前预留空间的能力,使其批量大小得以显著提高。

我们将这些性能结果与朴素静态批处理进行对比绘制:

吞吐量基准测试结果以朴素静态批处理的改进倍数表示,对数刻度。 值得注意的是,即使 FasterTransformer 的 4 倍改进也十分令人印象深刻;我们对于在 NVIDIA 实现后测试 FasterTransformers 与连续批处理的效果感兴趣。然而,与优化过的模型相比,连续批处理显然是静态批处理的一项重要改进。当您考虑到连续批处理和迭代级调度允许的进一步内存优化时(如 vLLM),性能差距变得非常大。

基准测试结果:延迟

实时推理通常面临延迟与吞吐量的权衡,需要根据用户需求进行优化。我们对真实工作负载进行延迟基准测试,并测量每个框架下延迟累积分布函数如何变化。

与吞吐量基准测试类似,我们将模型配置为每次请求时始终发出指定的 token 数量。我们通过对长度在 1 个 token 到 512 个 token 之间的均匀分布进行采样,准备了 100 个随机生成的提示。我们从具有平均值=128 和最大尺寸 1536 的限制指数分布中抽取 100 个输出长度。这些数字之所以被选中,是因为它们相当现实,且允许生成充分利用我们模型的完整上下文长度(512+1536=2048)。

与在吞吐量基准测试中一并提交所有请求不同的是,我们将每个请求的延迟设定为预先确定的秒数。我们对泊松分布进行采样,以确定每个请求在先前提交的请求后等待多长时间。泊松分布的参数是 λ(期望值),在我们的案例中,指的是每秒查询(QPS)击中我们的模型端点。我们在 QPS=1 和 QPS=4 上测量延迟,看看随着负载变化,延迟分布如何改变。

在平均负载下,每个框架生成请求的中位数延迟为 1 QPS 和 4 QPS。连续批处理系统可以改善中位数延迟。 我们发现,在改善吞吐量的同时,连续批处理系统还可以降低中位延迟。这是因为连续批处理系统允许在每次迭代时,如果有空间,将新请求添加到现有批次中。那么其他百分位的情况如何呢?事实上,我们发现它们在所有百分位上都改善了延迟:

每个框架在 QPS=1 时生成请求延迟的累积分布函数。由于连续批处理器中存在迭代级批处理调度,静态批处理器和连续批处理器具有不同的曲线形状。在此负载下,所有连续批处理器的表现大致相同;FasterTransformers 的表现明显优于朴素模型实现的静态批处理。 连续批处理在所有百分位上改善延迟的原因与它在 p50 上改善延迟的原因相同:无论批处理中其他序列的生成过程进行到哪一步,都可以添加新请求。然而,和静态批处理一样,连续批处理仍受 GPU 上可用空间的限制。随着服务系统被请求饱和,平均批大小越大,则在接收到请求时立即注入新请求的机会就越少。当我们将平均 QPS 提高到 4 时,我们可以看到这一点:

QPS=4 时,每个框架生成请求延迟的累积分布函数。与 QPS=1 相比,FasterTransformer 的延迟分布变得更接近朴素模型的静态批处理。Ray Serve 和 text-generation-inference 的连续批处理实现性能相似,但明显不如 vLLM。 我们观察到 FasterTransformer 变得更加接近朴素静态批处理,并且 text-generation-inference 和 Ray Serve 的连续批处理实现也在向 QPS=1 时 FasterTransformer 的曲线靠拢。也就是说,随着系统的饱和度增加,请求延迟上升,即时注入新请求的机会减少。这与 vLLM 曲线一致------在 QPS=1 和 QPS=4 之间,它基本保持不变。这是因为由于其先进的内存优化,它具有更高的最大批量大小。

有趣的是,我们观察到 vLLM 在 QPS=8 附近饱和,吞吐量接近 1900 token/s。要将这些数字与其他服务系统进行公平比较需要更多的实验;不过我们已经证明了,连续批处理相对于静态批处理有很大的改进,原因如下:1)通过在可能的情况下立即注入新请求,降低了延迟;2)通过启用先进的内存优化(在 vLLM 的情况下),提高服务系统在饱和之前能够处理的 QPS。

结论

LLM具有一些惊人的能力,我们认为它们的影响仍然大部分未被发掘。我们分享了一种新的推理技术------连续批处理,以及它是如何胜过静态批处理的。连续批处理通过减少安排新请求的机会浪费,提高了吞吐量;同时,通过能够立即将新请求注入计算流,连续批处理也降低了延迟。我们很高兴看到人们能用连续批处理做出什么,以及未来行业的发展方向。

自己尝试连续批处理

我们有一个vLLM + Ray Serve示例,让您可以尝试连续批处理。我们正在将连续批处理系统集成到Aviary中,这是一个webapp,可以让你并行比较不同LLM的输出,并将在一周内发布。

致谢。我们要感谢以下人员在基准测试和/或审查我们的成果方面给予的帮助。Anyscale:Stephanie Wang,Antoni Baum,Edward Oakes 和 Amog Kamsetty;UC Berkeley:Zhuohan Li 和 Woosuk Kwon。

参与Ray

博客文章中使用的实验代码在这里。要与Ray社区建立联系,请加入Ray Slack或在Discuss论坛上提问。如果您有兴趣托管LLM,请查看我们的托管Ray产品。如果您对了解更多关于Ray的信息,请查看ray.iodocs.ray.io

查看我们之前关于解决生成式AI基础设施和使用LangChain与Ray的博客系列。

Ray Summit 2023 :如果您有兴趣了解更多关于如何使用Ray构建高性能且可扩展的LLM应用程序,以及如何在Ray上进行LLM的微调/训练/服务,请加入9月18-20日的Ray Summit!我们邀请了一系列杰出的主题演讲者,包括来自OpenAI的John Schulman和来自Cohere的Aidan Gomez,大会还将包括关于Ray的社区和技术演讲以及专注于LLM的实际培训。

相关推荐
算法歌者5 分钟前
[算法]入门1.矩阵转置
算法
林开落L19 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色20 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download22 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna42 分钟前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿1 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
九圣残炎1 小时前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
YSRM1 小时前
Experimental Analysis of Dedicated GPU in Virtual Framework using vGPU 论文分析
算法·gpu算力·vgpu·pci直通
韭菜盖饭2 小时前
LeetCode每日一题3261---统计满足 K 约束的子字符串数量 II
数据结构·算法·leetcode