赞
踩
©作者 | 时空猫的问答盒
单位 | 北京大学
本文介绍一篇 LLM 推理加速技术相关的文章,值得读一读。
LLMs 在现实应用中的计算成本主要由服务成本所主导,但是传统的批处理策略存在低效性。在这篇文章中,我们将告诉你,为什么 Continuous Batching 连续批处理成为了解决这一问题的新方法,而不再把 LLMs 视为“黑匣子”。这个技术如何利用内存,而不是计算能力,来实现 10 倍以上的性能提升,将改变AI领域的游戏规则。
文章标题:
How continuous batching enables 23x throughput in LLM inference while reducing p50 latency
文章链接:
https://www.anyscale.com/blog/continuous-batching-llm-inference
为了更好地理解这篇文章,让我们先了解一下大型语言模型(LLM)的推断过程以及传统批处理策略中存在的低效性。
当我们谈论大型语言模型(LLM)的推断过程时,我们指的是使用已经训练好的模型来对输入文本进行处理,从而生成相应的输出。推断过程涉及将一个或多个文本片段传递给模型,并从模型中获取相应的预测或生成的文本。
在传统的批处理策略中,文本通常会被分成小批次(batch)进行处理,以便在 GPU 或其他硬件上进行并行计算。然而,由于 LLMs 通常需要大量的内存和计算资源,传统的批处理策略可能会导致一些低效性:
内存消耗高:传统批处理策略可能会导致大量的 GPU 内存被占用,限制了可以同时处理的文本量。
计算资源未被充分利用:由于内存限制,传统批处理策略可能导致 GPU 计算资源未被充分利用,从而降低了推断效率。
高延迟:由于大型模型的计算需求,传统批处理策略可能会导致高延迟,使得生成结果的响应速度变慢。
难以处理长文本:传统批处理策略可能会限制模型处理长文本的能力,因为它们可能无法一次性将整个文本载入内存。
因此,传统的批处理策略可能会限制了 LLMs 在实际应用中的效率和性能。在接下来的部分,文章将介绍连续批处理(continuous batching)作为一种优化策略,以解决传统批处理策略中存在的这些低效性问题。
传统的处理方法将 LLMs 视为“黑匣子”,主要通过内部更改(如量化和自定义 CUDA 内核)来进行优化。这种方法忽视了 LLMs 在推断过程中生成输出的迭代性质,以及 LLM 推断通常受限于内存而不是计算资源。由于 LLMs 通常需要大量的 GPU 内存和计算成本,这导致在实际应用中,服务成为计算成本的主导因素。
因为 LLMs 在推断过程中需要迭代生成输出,而且通常情况下是内存受限的,所以存在着可以在系统级别进行批处理优化的机会。这意味着可以通过合理的批处理策略来最大程度地利用 GPU 资源,从而显著提高推断吞吐量,降低计算成本,使得服务成本不再是主导因素。
作者之所以持这样的看法,是因为他们认为 LLMs 在生成输出时是迭代进行的,并且 LLM 推断通常受到内存而不是计算资源的限制。这意味着存在一些出人意料的系统级批处理优化方法,可以在实际工作负载中产生显著的性能提升。
相较于将 LLMs 视为不可调优的“黑匣子”,作者认为可以通过采用更灵活的系统级批处理策略来实现性能的大幅度提升,而不仅仅局限于内部更改如量化和自定义 CUDA 内核。这样的优化方法可以使得在实际应用中,LLMs 的性能提升达到 10 倍甚至更多。
当作者提到 LLMs 通常是内存受限而不是计算受限时,他指的是在 LLM 推断过程中,通常更多地受到可用内存的限制,而不是计算能力的限制。
内存受限意味着在处理大型语言模型时,系统的内存资源是一个相对稀缺的资源。这意味着模型在推断时需要将许多数据存储在内存中,例如输入文本、中间计算结果等。如果内存不足以容纳所需的数据,可能会导致内存溢出或性能下降。
相比之下,计算受限指的是在进行模型推断时,计算资源(例如 CPU 或 GPU 的处理能力)是主要的瓶颈。这种情况下,系统的处理能力会成为推断性能的主要限制因素,而内存资源可能并不是主要的瓶颈。
因此,在 LLM 推断中,作者指出通常更关键的是如何有效地利用有限的内存资源,而不是解决计算资源瓶颈。通过优化内存的使用方式,可以使得在实际工作负载中推断性能提升 10 倍甚至更多。这意味着通过合理地调度和利用内存,可以显著地提高 LLM 模型在实际应用中的性能表现。
连续批处理是一种最近提出的优化方法,也称为动态批处理或迭代级别调度批处理。它旨在解决传统批处理策略中的一些低效性问题。
传统批处理策略通常是基于请求的动态批处理,即一次性处理一批请求。这可能会导致一些请求在推断过程中花费的时间较长,因为某些请求可能会比其他请求更加复杂或耗时。
相比之下,连续批处理采用了一种更为灵活的方法。它允许在推断过程中动态地调整批次的大小,以适应不同请求的复杂程度。具体来说,连续批处理会在模型推断的过程中不断地将新的请求添加到当前的批次中,同时保持一定的效率。
这意味着,如果某些请求需要更多时间来完成推断,它们可以在当前批次中等待,而不会等待整个批次处理完毕。这样可以显著降低高复杂度请求的等待时间,提高了推断的效率。
当进行 LLM 推断时,对于每一个请求,我们会首先提供一个称为“前缀”或“提示”的 token 序列作为输入。这个前缀通常包含了一个或多个起始 token,用于引导模型生成接下来的文本。例如,在文章中的例子中,前缀是句子:“What is the capital of California:”。
这个前缀的目的是为了提供模型一个起点,使其能够理解用户的请求并生成相应的响应。在这个例子中,前缀引导模型去回答加利福尼亚的首府是什么。
一旦提供了前缀,LLM 会开始生成一个完整的响应序列,它会在产生一个终止 token 或达到最大序列长度时停止。这是一个迭代的过程,每一次前向传递模型都会产生一个额外的完成 token,逐步构建出完整的响应序列。
LLM 在产生完整的响应之前会产生一个包含多个 token 的序列,这个序列通常被称为 “completion tokens”。生成过程会一直进行,直到满足以下两种情况之一:
生成了一个特定的“停止”标记,表明生成过程应该终止。
达到了设定的最大序列长度,这时也会停止生成。
当以句子“加利福尼亚的首府是什么:”作为提示时,LLM 会逐步生成完整的响应。这是一个迭代的过程,每次迭代都会产生一个新的完成 token。
示例迭代过程:
第一次迭代:LLM 生成第一个 token "S",此时我们有 ["S"]。
第二次迭代:LLM 生成第二个 token "a",此时我们有 ["S", "a"]。
第三次迭代:LLM 生成第三个 token "c",此时我们有 ["S", "a", "c"]。
...
第十次迭代:LLM 生成第十个 token "o",此时我们有完整的响应:["S", "a", "c", "r", “a”, "m", "e", "n", "t", "o"]。
在这个例子中,使用了十次前向传递迭代才能得到完整的响应。
请注意,在实际情况下,token 并不是一一对应于 ASCII 字符,作者提到了一种称为 Byte-Pair Encoding 的流行的 token 编码技术,但不论如何编码,生成的迭代过程都是相似的。
当作者提到了 Byte-Pair Encoding(字节对编码)时,实际上指的是一种流行的文本压缩和编码技术。它的主要作用是将文本中的字符或字节序列进行编码,以便更有效地表示和传输文本数据。
具体来说,Byte-Pair Encoding 通过识别和合并在文本中频繁出现的字符对(字节对),来构建一个更紧凑的编码表。这使得一些常用的字符或词组可以用更短的编码表示,从而减小了文本的总体大小。
在大型语言模型(LLM)的上下文中,使用 Byte-Pair Encoding 可以帮助将原始文本转化为模型可以更有效地处理的编码形式。这也是为什么在实际情况中,token 与 ASCII 字符并不是一一映射的原因之一。
在这个玩具示例中,图中的元素代表了 LLM 推断的一些关键组成部分:
黄色方框中的 T1, T2, ..., T8:这些代表了一个假设模型,它支持最大长度为 8 个 token 的序列。这里的 T1, T2, ..., T8 是不同的 token。
蓝色的箭头:表示推断过程的迭代。从开始的“前缀”或“提示”(黄色方框中的内容)开始,模型逐步生成一个 token。
红色的方框中的 “end-of-sequence” 标志:这表示了当模型生成了一个特殊的 token,通知推断过程结束。
批处理大小(Batch Size):这里展示的示例只包含一个输入序列,因此批处理大小为1。
简而言之,这个图示说明了 LLM 推断的简化过程,从一个起始的提示开始,模型逐步生成一个 token,直到生成了一个特殊的 “end-of-sequence” 标志为止。请注意,这是一个简化的例子,实际的 LLM 推断过程可能涉及到更复杂的计算和模型结构。
当处理一个请求时,预填充阶段扮演着关键的角色。这个阶段起初可能会花费一些时间,但它在整个生成过程中扮演着非常重要的作用。
在预填充阶段,模型会提前计算一些关于注意力机制的输入信息。这些输入信息是在生成过程中保持不变的,因为它们与前缀(或提示)无关。这样做的好处是,在每次进行后续的生成时,不需要重新计算这些输入信息,从而节省了计算资源和时间。
具体来说,这些提前计算的输入信息可以帮助模型在生成后续 token 时更高效地利用 GPU 的并行计算能力。这是因为这些输入信息可以独立计算,而不受后续生成过程的影响。
总的来说,预填充阶段的作用是优化模型的生成过程,通过提前计算一些与前缀无关的输入信息,从而在后续的生成过程中节省计算资源和时间。
这个阶段的存在是为了使整个生成过程更加高效和快速,尤其是对于需要生成大量 token 的情况下,可以明显地提升性能。
当作者提到 LLM 推断是内存 - IO 受限而不是计算受限时,意味着在 LLM 推断过程中,主要的瓶颈并不在于计算速度,而在于数据的传输速度,特别是从主内存加载数据到 GPU 内存的过程。
这对 LLM 推断的吞吐量有着重要的影响。具体来说,由于数据传输速度相对较慢,如果我们可以减少需要从主内存加载到 GPU 内存的次数,就能提高推断的效率,从而提高吞吐量。
GPU 内存在这里起到了关键的作用。它是临时存储模型参数、输入数据和计算结果的地方。在 LLM 推断过程中,模型参数需要在 GPU 内存中保留,同时输入数据也需要被加载到 GPU 内存中才能进行计算。因此,GPU 内存的大小限制了我们可以处理的数据量以及批次的大小。
总的来说,GPU 内存的充足与否直接影响了 LLM 推断的性能和吞吐量。如果我们能够优化内存的使用,比如通过模型量化策略或其他方法减少内存占用,就能提升推断效率,从而实现更高的吞吐量。
当基本模型大小和 token 序列长度增加时,GPU 内存的消耗量也会相应增加。这是因为更大的模型和更长的序列需要更多的内存来存储它们的参数和生成的中间结果。
具体地说,一般可以使用以下方法来估算 GPU 内存消耗:
基本模型大小(模型参数):随着模型大小的增加,需要更多的内存来存储模型的权重、偏差等参数。一般来说,模型越大,所需内存就越多。
Token 序列长度:每个 token 都需要一定的内存来存储其编码和相关信息。因此,当序列长度增加时,内存消耗也会随之增加。
模型架构:不同的模型架构可能会对内存消耗产生不同的影响。一些模型可能会有特定的内存优化策略或特性,可以影响其在 GPU 上的内存占用。
GPU 类型和内存容量:不同类型和容量的 GPU 具有不同的内存限制。较大内存的 GPU 可以容纳更大的模型和序列。
其他辅助数据和计算:除了模型参数和 token 序列之外,还可能存在其他计算所需的内存,比如中间结果的存储等。
当涉及到优化内存使用时,文章中提到了以下一些策略和方法:
模型量化策略:例如 AutoGPTQ,它可以通过将模型权重从 16 位减少到 8 位表示,从而减少内存使用,为更大批处理提供了更多空间。
FlashAttention 技术:该技术通过重新组织注意力计算,以减少内存 - IO,从而实现了显著的吞吐量提升。
优化模型实现:例如 NVIDIA 的 FasterTransformer,通过优化模型实现可以提高吞吐量。
连续批处理:这是一种内存优化技术,不需要对模型进行修改。它可以提高 LLM 生成的内存效率。
这些策略和方法旨在充分利用GPU内存,减少内存开销,从而提高LLM推断的吞吐量和效率。
当使用连续批处理时,它允许将多个请求的前缀(prompt)合并成一个批次一起发送到模型进行推断。相比之下,朴素批处理会单独处理每个请求,即使它们之间可能存在共享的计算资源。
具体来说,连续批处理的工作方式如下:
合并前缀:对于多个请求,将它们的前缀合并成一个批次。这样做的好处是可以利用 GPU 的并行计算能力,因为可以一次性地计算多个请求的前缀。
共享计算资源:通过将多个请求的前缀合并成一个批次,模型的计算可以在这些前缀之间共享,从而减少了冗余的计算工作。这使得整体推断的效率得到了提升。
相对于朴素批处理,连续批处理的优势在于:
减少前缀处理时间:朴素批处理会为每个请求单独处理前缀,而连续批处理可以一次性地处理多个请求的前缀,从而减少了前缀处理的总时间。
提高内存利用率:连续批处理可以在同样的内存限制下处理更多的请求,因为它将多个请求的前缀合并成一个批次,从而减少了内存的浪费。
提升模型推断效率:通过共享计算资源,连续批处理可以更高效地利用 GPU 的计算能力,从而提升了模型推断的速度。
总的来说,连续批处理是一种有效的内存优化技术,它通过合并多个请求的前缀,共享计算资源,从而提高了 LLM 推断的效率,而无需对模型进行任何修改。
模型参数的加载对 LLMs 的计算饱和度有很大影响是因为在 GPU 架构中,内存和计算是两个相对独立但又相互关联的方面。
GPU 的计算能力:GPUs 是高度并行化的计算架构,可以以每秒数万亿次(teraflop)甚至百万亿次(petaflop)的浮点运算速度执行计算任务。这意味着它们可以在短时间内完成大量的计算工作。
内存带宽的限制:然而,GPU 的内存带宽相对有限。内存带宽是指 GPU 用于在内存和处理器之间传输数据的速度。加载模型参数意味着将模型的权重和其他相关数据从存储介质(如硬盘或内存)传输到 GPU 的内存中。
由于 LLMs 通常拥有大量的参数(特别是像 GPT-3 这样的大型模型拥有数十亿甚至数百亿的参数),加载模型参数可能会占用大量的内存带宽。这会导致 GPU 在加载模型参数时花费了大量的时间和资源,使得在实际计算上的利用率变得相对较低。
因此,即使 GPU 具有强大的计算能力,但如果大部分内存带宽用于加载模型参数,就会导致 LLMs 难以充分利用其计算资源,从而影响了计算饱和度。为了解决这个问题,文章提出了批处理的方法,通过一次加载模型参数,然后使用它们来处理多个输入序列,从而更有效地利用了 GPU 的内存带宽,提高了计算利用率和吞吐量。
批处理是一种将多个数据样本一起传递给模型进行处理的技术。相比于逐个处理单个样本,批处理允许在一次计算中同时处理多个样本。这样可以更有效地利用计算资源,提高计算速度。
在 LLM 推断中,批处理的优势主要体现在以下几个方面:
减少模型参数加载次数:在不使用批处理的情况下,每次处理一个输入序列都需要加载一次模型参数。而批处理可以在一次加载后多次使用这些参数,从而减少了加载的次数。
提高内存带宽的利用率:GPU 的内存带宽是有限的资源,而加载模型参数通常会消耗大量的内存带宽。通过批处理,可以更有效地利用这些内存带宽,使其在计算过程中得到更充分的利用。
提高计算资源的利用率:LLM 推断通常是内存 - IO 受限的,而不是计算受限的,意味着加载数据到 GPU 的计算核心比在计算核心上执行 LLM 计算花费的时间更多。通过批处理,可以更有效地利用计算资源,提高计算速度。
传统的批处理方法被称为静态批处理,是因为在这种方法中,批处理的大小在推断完成之前保持不变。与 LLM 推断的迭代性质相关的是,在 LLM 推断过程中,每个请求是逐步生成的。具体来说:
在静态批处理中,一次性加载了模型参数,并在整个推断过程中重复使用这些参数来处理多个输入序列。这样做更有效地利用了芯片的内存带宽,提高了计算利用率、吞吐量,并降低了 LLM 推断的成本。
然而,LLM 推断是一个迭代的过程。对于每个请求,模型会逐步生成输出序列的各个部分,直到生成停止标记或达到最大序列长度为止。这意味着每次模型前向传递时,都会获得一个额外的输出 token。例如,如果我们以句子“加利福尼亚的首府是什么:”作为提示,它将需要进行十次前向传递才能得到完整的响应,即 ["S", "a", "c", "r", “a”, "m", "e", "n", "t", "o"]。
由于 LLM 推断是一个迭代生成的过程,静态批处理可能导致 GPU 在批处理中的不同序列的生成长度不同时被低效利用。因为不同的序列可能会在批处理中的不同迭代步骤中完成生成,而静态批处理会等待所有序列完成生成后才开始处理新的序列。这导致了在等待最后一个序列完成生成之前,GPU 可能会被低效利用的情况。
静态批处理之所以会导致 GPU 低效利用,主要是因为它难以有效地处理不同生成长度的序列,这些序列可能在同一批次中同时存在。这导致了以下问题:
等待最长序列完成:在静态批处理中,所有序列都必须等待批处理中生成时间最长的序列完成,然后才能进行下一批次的处理。这意味着一旦有一个生成时间较长的序列存在,其他生成时间较短的序列将被迫等待,导致 GPU 的计算资源无法充分利用。这会浪费 GPU 的计算能力,尤其是当一些序列的生成非常快时。
难以释放资源和添加新请求:由于 LLM 推断是一个迭代过程,一些序列可能会在批次中的不同时间点完成生成。这使得难以及时释放已完成生成的序列所占用的资源,并添加新的请求到批次中。如果没有有效的机制来管理这些生成中和已完成的序列,将导致 GPU 资源的浪费和低效利用。
Variance in Generation Output:在静态批处理中,如果不同序列的生成长度差异较大,那么某些序列可能会迅速完成,而其他序列则需要更长的时间。这种差异性会导致 GPU 的部分计算资源一直处于闲置状态,因为它们无法立即用于生成更多的序列。
静态批处理在输入和输出序列长度不相等的情况下会低效利用 GPU。举例来说,假设我们有一个 LLM 模型,可以接受最多 512 个 token 的输入序列,但其生成的输出序列可能长度不一。如果我们采用静态批处理,即将一批输入序列一次性加载到 GPU 中进行推断,那么如果批中的不同序列生成长度不同,就会导致以下情况:
假设我们有一个批次,其中包含了以下两个输入序列:
输入序列 1(长度为 512)生成的输出序列长度为 20。
输入序列 2(长度为 512)生成的输出序列长度为 30。
在静态批处理的情况下,GPU 将等到批中的所有序列都完成生成后才会开始下一个批次的处理。这会导致以下问题:
输入序列 1 生成的输出序列长度只有 20,但 GPU 在等待序列 2 完成生成之前无法开始下一个批次的处理。这意味着 GPU 会在这段时间内处于空闲状态,无法充分利用其计算资源。
输入序列 2 生成的输出序列长度为 30,但在整个批次处理期间,GPU 只能等待最长的生成过程完成。这导致 GPU 的计算资源被浪费了 10 个 token 的长度。
连续批处理是一种相对于静态批处理更高效的方法,特别适用于 LLM 推断。它的工作原理如下:
动态确定批次大小:与静态批处理不同,连续批处理采用了迭代级别的调度。它并不等待每个序列在批次中完成生成后再进行下一个序列的处理。相反,它在每个迭代中根据需要确定批次的大小。这意味着一旦某个序列在批次中完成生成,就可以立即将一个新的序列插入到相同位置。
提高 GPU 利用率:连续批处理通过更灵活地利用 GPU 的计算资源来提高 GPU 的利用率。在静态批处理中,如果批次中的不同序列的生成长度不同,GPU 会被低效利用,因为它必须等待批次中的所有序列完成生成才能开始下一个批次。而在连续批处理中,一旦一个序列完成生成,就可以立即开始处理下一个序列,从而最大程度地减少了 GPU 的闲置时间。
当使用迭代级别调度时,相较于静态批处理,批次的大小是在每个迭代中动态确定的,而不是在推断过程的开始时就固定下来。这意味着一旦批次中的某个序列完成生成,就可以立即插入一个新的序列以继续利用 GPU 进行计算。
相对于静态批处理,迭代级别调度具有以下优势:
更高的 GPU 利用率:在迭代级别调度中,一旦一个序列完成生成,就可以立即开始处理下一个序列。这意味着 GPU 的计算资源得到更高效的利用,不会因为等待批次中的所有序列完成生成而被浪费。
更高的推断吞吐量:由于 GPU 的计算资源得到更充分的利用,相对于静态批处理,迭代级别调度可以实现更高的推断吞吐量,从而加快了整个推断过程的速度。
在 Hugging Face 的文本生成推断 LLM 推断服务器中,连续批处理的实现是通过一个名为 “waiting_served_ratio” 的超参数来管理预填充阶段和生成阶段的。
“waiting_served_ratio” 指的是等待预填充和等待生成结束的请求数之间的比率。这个超参数的设置影响着连续批处理的表现,它可以用来调整在生成阶段与预填充阶段之间的权衡。文章没有详细说明如何设置这个超参数,但可以推测它可能是根据具体情况和需求进行调整的关键参数之一。
这个超参数的存在表明,Hugging Face 在他们的实现中考虑了如何在预填充阶段和生成阶段之间平衡处理请求,以最大化 GPU 的利用率。
选择使用连续批处理这个术语是因为它最准确地描述了优化方法的本质。下面是连续批处理与动态批处理以及迭代级别调度之间的区别:
连续批处理:连续批处理是一种优化技术,它允许在生成过程中动态地调整批处理的大小。具体来说,一旦一个序列在批处理中完成生成,就可以立即用新的序列替代它,从而提高了 GPU 的利用率。这种方法的关键在于实时地适应当前的生成状态,而不是等待整个批次的序列都完成。
动态批处理:动态批处理也是一种批处理策略,但它通常指的是在训练过程中动态调整批处理的大小,以便更好地利用硬件资源。在推断过程中,动态批处理的意义可能会略有不同,可能是根据当前生成状态来动态调整批处理的大小。
迭代级别调度:迭代级别调度与连续批处理类似,它也是在生成过程中动态调整批处理的大小。但它强调的是调度是以迭代为单位的,也就是说,在每个迭代中决定批处理的大小,而不是随时随地都可以进行调整
参考文献
https://mp.weixin.qq.com/s/bs3puOXFZYg5K-zfyDfpOw
更多阅读
#投 稿 通 道#
让你的文字被更多人看到
如何才能让更多的优质内容以更短路径到达读者群体,缩短读者寻找优质内容的成本呢?答案就是:你不认识的人。
总有一些你不认识的人,知道你想知道的东西。PaperWeekly 或许可以成为一座桥梁,促使不同背景、不同方向的学者和学术灵感相互碰撞,迸发出更多的可能性。
PaperWeekly 鼓励高校实验室或个人,在我们的平台上分享各类优质内容,可以是最新论文解读,也可以是学术热点剖析、科研心得或竞赛经验讲解等。我们的目的只有一个,让知识真正流动起来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。