| paperweekly
本文介绍一篇 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 通常需要大量的内存和计算资源,传统的批处理策略可能会导致一些低效性:
因此,传统的批处理策略可能会限制了 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。
示例迭代过程:
在这个例子中,使用了十次前向传递迭代才能得到完整的响应。
请注意,在实际情况下,token 并不是一一对应于 ASCII 字符,作者提到了一种称为 Byte-Pair Encoding 的流行的 token 编码技术,但不论如何编码,生成的迭代过程都是相似的。
当作者提到了 Byte-Pair Encoding(字节对编码)时,实际上指的是一种流行的文本压缩和编码技术。它的主要作用是将文本中的字符或字节序列进行编码,以便更有效地表示和传输文本数据。
具体来说,Byte-Pair Encoding 通过识别和合并在文本中频繁出现的字符对(字节对),来构建一个更紧凑的编码表。这使得一些常用的字符或词组可以用更短的编码表示,从而减小了文本的总体大小。
在大型语言模型(LLM)的上下文中,使用 Byte-Pair Encoding 可以帮助将原始文本转化为模型可以更有效地处理的编码形式。这也是为什么在实际情况中,token 与 ASCII 字符并不是一一映射的原因之一。
在这个玩具示例中,图中的元素代表了 LLM 推断的一些关键组成部分:
简而言之,这个图示说明了 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 内存消耗:
当涉及到优化内存使用时,文章中提到了以下一些策略和方法:
这些策略和方法旨在充分利用GPU内存,减少内存开销,从而提高LLM推断的吞吐量和效率。
当使用连续批处理时,它允许将多个请求的前缀(prompt)合并成一个批次一起发送到模型进行推断。相比之下,朴素批处理会单独处理每个请求,即使它们之间可能存在共享的计算资源。
具体来说,连续批处理的工作方式如下:
相对于朴素批处理,连续批处理的优势在于:
总的来说,连续批处理是一种有效的内存优化技术,它通过合并多个请求的前缀,共享计算资源,从而提高了 LLM 推断的效率,而无需对模型进行任何修改。
模型参数的加载对 LLMs 的计算饱和度有很大影响是因为在 GPU 架构中,内存和计算是两个相对独立但又相互关联的方面。
由于 LLMs 通常拥有大量的参数(特别是像 GPT-3 这样的大型模型拥有数十亿甚至数百亿的参数),加载模型参数可能会占用大量的内存带宽。这会导致 GPU 在加载模型参数时花费了大量的时间和资源,使得在实际计算上的利用率变得相对较低。
因此,即使 GPU 具有强大的计算能力,但如果大部分内存带宽用于加载模型参数,就会导致 LLMs 难以充分利用其计算资源,从而影响了计算饱和度。为了解决这个问题,文章提出了批处理的方法,通过一次加载模型参数,然后使用它们来处理多个输入序列,从而更有效地利用了 GPU 的内存带宽,提高了计算利用率和吞吐量。
批处理是一种将多个数据样本一起传递给模型进行处理的技术。相比于逐个处理单个样本,批处理允许在一次计算中同时处理多个样本。这样可以更有效地利用计算资源,提高计算速度。
在 LLM 推断中,批处理的优势主要体现在以下几个方面:
传统的批处理方法被称为静态批处理,是因为在这种方法中,批处理的大小在推断完成之前保持不变。与 LLM 推断的迭代性质相关的是,在 LLM 推断过程中,每个请求是逐步生成的。具体来说:
在静态批处理中,一次性加载了模型参数,并在整个推断过程中重复使用这些参数来处理多个输入序列。这样做更有效地利用了芯片的内存带宽,提高了计算利用率、吞吐量,并降低了 LLM 推断的成本。
然而,LLM 推断是一个迭代的过程。对于每个请求,模型会逐步生成输出序列的各个部分,直到生成停止标记或达到最大序列长度为止。这意味着每次模型前向传递时,都会获得一个额外的输出 token。例如,如果我们以句子“加利福尼亚的首府是什么:”作为提示,它将需要进行十次前向传递才能得到完整的响应,即 ["S", "a", "c", "r", “a”, "m", "e", "n", "t", "o"]。
由于 LLM 推断是一个迭代生成的过程,静态批处理可能导致 GPU 在批处理中的不同序列的生成长度不同时被低效利用。因为不同的序列可能会在批处理中的不同迭代步骤中完成生成,而静态批处理会等待所有序列完成生成后才开始处理新的序列。这导致了在等待最后一个序列完成生成之前,GPU 可能会被低效利用的情况。
静态批处理之所以会导致 GPU 低效利用,主要是因为它难以有效地处理不同生成长度的序列,这些序列可能在同一批次中同时存在。这导致了以下问题:
静态批处理在输入和输出序列长度不相等的情况下会低效利用 GPU。举例来说,假设我们有一个 LLM 模型,可以接受最多 512 个 token 的输入序列,但其生成的输出序列可能长度不一。如果我们采用静态批处理,即将一批输入序列一次性加载到 GPU 中进行推断,那么如果批中的不同序列生成长度不同,就会导致以下情况:
假设我们有一个批次,其中包含了以下两个输入序列:
在静态批处理的情况下,GPU 将等到批中的所有序列都完成生成后才会开始下一个批次的处理。这会导致以下问题:
连续批处理是一种相对于静态批处理更高效的方法,特别适用于 LLM 推断。它的工作原理如下:
当使用迭代级别调度时,相较于静态批处理,批次的大小是在每个迭代中动态确定的,而不是在推断过程的开始时就固定下来。这意味着一旦批次中的某个序列完成生成,就可以立即插入一个新的序列以继续利用 GPU 进行计算。
相对于静态批处理,迭代级别调度具有以下优势:
在 Hugging Face 的文本生成推断 LLM 推断服务器中,连续批处理的实现是通过一个名为 “waiting_served_ratio” 的超参数来管理预填充阶段和生成阶段的。
“waiting_served_ratio” 指的是等待预填充和等待生成结束的请求数之间的比率。这个超参数的设置影响着连续批处理的表现,它可以用来调整在生成阶段与预填充阶段之间的权衡。文章没有详细说明如何设置这个超参数,但可以推测它可能是根据具体情况和需求进行调整的关键参数之一。
这个超参数的存在表明,Hugging Face 在他们的实现中考虑了如何在预填充阶段和生成阶段之间平衡处理请求,以最大化 GPU 的利用率。
选择使用连续批处理这个术语是因为它最准确地描述了优化方法的本质。下面是连续批处理与动态批处理以及迭代级别调度之间的区别: