作者 | Timothée Lacroix
选择正确的 LLM 推理栈意味着选择适合你的任务的正确模型,并配以适当的推理代码在适当的硬件上运行。本文介绍了流行的 LLM 推理堆栈和设置,详细说明其推理的成本构成;并讨论当前的开源模型以及如何充分利用它们,同时还涉及当前开源服务栈中仍然缺失的功能,以及未来模型将解锁的新功能。
本文源自 Mistral AI 首席技术官 Timothée Lacroix 的演讲。他于 2015 年在 Facebook AI Research 担任工程师,于 2016 年至 2019 年间与École des Ponts 合作完成了关于推荐系统的张量分解的论文。2023 年他成为 Mistral AI 的联合创始人。Mistral AI 于近期发布了业内首个开源 MoE 大模型 Mixtral-8x7B。
本次演讲的很多内容都基于我在网上找到的信息或通过对第一个 LLaMA 版本模型进行实验时的发现。我认为,现在的 Mistral 更关注推理成本,而非训练成本。因此,我将分享推理成本的构成、吞吐、时延及其影响因素。
很多人想要部署语言大模型,我将分享如何使用开源工具部署自己的语言大模型。当然,你也可以使用一些出色的公共 API,但我对开源工具更感兴趣,所以接下来我将深入讨论部署一个 70 亿参数模型的重要细节。我将分享的许多内容也同样适用于更大规模的模型,但那需要更多 GPU。
影响推理的指标
我们将首先讨论有哪些重要指标,以及这些指标的影响因素,包括硬件和软件层面。接下来,我将介绍一些能够改善性能的技巧,据我所知,其中一些技巧还未获得广泛实现。我尝试在各种不同的硬件上运行了一系列模型,并尝试获得性能曲线,我认为实例非常重要,所以我将通过这些数据得出结论。
首先,我们该关注哪些指标?第一是吞吐量,以每秒查询数(Query/second)表示,我们希望在批处理作业中将这一指标最大化,或者希望允许更多用户使用我们的服务。第二是时延,以每词元每秒(seconds/token)表示,即输出下一个词元所需的时间,这决定了你的应用程序的速度和灵敏度。在 ChatGPT 中,这一速度相当快。对于较小的模型,可以更轻松地实现快速响应,因此我们希望将这个值最小化以提升用户体验。较为优秀的阈值是每分钟输出 250 个单词,我认为这是人类的平均阅读速度,只要你的时延低于这个值,用户就不会感到无聊。第三是成本,毫无疑问,这一数值越低越好。
影响推理指标的因素
现在我将深入探讨这些指标的影响因素。我只会谈论自回归解码,即基于一批批词元通过神经网络确定下一批词元,这部分不包括处理查询的第一部分。提示处理有时被称为预填充(prefill)部分,我们会一次性将大量词元输入到神经网络中,这部分处理通常已经经过充分优化,挑战性相对较低。
考虑到这一点,我们对大小为 P 的模型的推理感兴趣。可以假设 P 是 7B,为执行一步推理,大约需要 2xPxBatch_size 的 FLOPs(浮点运算数)。在进行这些浮点运算时,我们需要将整个模型加载到实际运行计算的 GPU,并且需要一次性加载整个模型,即大致上需要的内存搬运(memory movement)量等于模型的参数数量。
这两个数量有趣的地方在于,第一个数量受硬件浮点运算能力的限制,即 GPU 可以实现的浮点运算次数,并且与批大小呈线性关系,在上述图表上呈增长趋势。除非批大小特别大,内存移动量并不随批大小而变化。但正如我所说,这种情况已经得到了相当程度的优化,所以我们并不太关心内存移动量。我们还有一个常量,即模型大小除以内存带宽,这是一次性加载整个模型所需的最短时间,每次都需要重新执行这个操作。
还有一个与批次大小有关的数量,它们在一个有趣的点上相交。这个点不取决于硬件之外的任何因素。举例来说,在 A10G 和 A100 上,硬件可以实现的总浮点运算次数的两倍除以内存带宽为 400。
B*这个批大小非常有趣,因为低于这一批大小,基本上是在浪费 FLOPs,因为计算受到了内存限制,我们在等待 GPU 加载数据,而计算速度太快,图中某部分的时延是恒定的。如果超过这个 B*这个阈值,时延就会开始增加,就变成了计算受限。
因此,B* 的真正优势在于,这个批大小的时延范围是最优的,因此用户体验是最佳的,同时也没有浪费任何 FLOPs。
不管怎样,我们理想的批大小 B* 是 400,这个值似乎相当大,所以我们来计算一下 LLaMA 等模型规模的几项指标。LLaMA 模型有 4K 个维度,深度 32 层,模型大小很容易计算,在 FP16 中每个模型权重占两个字节,所以只需 2x7=14GB 内存。
然后,我们用 KV 缓存存储计算结果,这样当我们重新编码一个新词元时,就不必重新从头计算。KV 缓存的大小为 2,包括 K 缓存和 V 缓存,且使用 FP16 格式,每个都乘以 2,然后每层有一个 KV 缓存,并且必须为批次中的每个元素保存数据,每个位置在序列中表示一个词元,然后乘以维度。
把实际数值代入这个公式发现,每个批次元素需要约 2G 内存才能支持最大长度 4K,因此,在 A10(24GB 内存)上,我们的最大批大小约为 5,在更大的 A100(80GB 内存)上,最大批大小只有 33 左右,这仍远低于理想值 400。
因此,对于所有实际用例,使用 70 亿参数的模型进行推理时,解码过程将严重受限于内存带宽。这也证明了 Mistral 从一开始就非常谨慎的一点:模型和 KV 缓存所占内存的大小确实影响了可允许的最大批大小,而最大批大小直接决定了效率的高低。
实用技巧
现在我将深入讨论一些已经存在但我个人很喜欢的技巧。其中一部分已经为 Mistral 所用,其他一些尚未在 Mistral 中得到应用,还有些则更多地涉及软件部署层面。
分组查询注意力
第一个技巧是分组查询注意力。分组查询注意力是通过每个查询使用更少的键和值来减少 KV 缓存的方法。这在 LLaMA 2 中使用过,但只用于较大的模型尺寸,而非 70 亿参数模型。在标准的多头注意力中,有多少查询,就有多少键和值。而在分组查询注意力中,一对键值与一组查询相关联。在 Mistral,我们的每个键和值使用四个查询,因此要执行的浮点运算量将保持不变,但内存开销只有原来的四分之一。这是一个简单的技巧,不会对性能造成实质性损害,这一做法很不错。
量化
第二个技巧是量化,对此我们并没有进行专门研究,但尤其在 LLaMA 发布后,这项技术发展得非常迅速。很多优秀的现成解决方案为许多开源社区的人所使用,提供了模型的 int8 或 int4 版本。使用 int8 时,模型尺寸会减半,在使用 int4 时,会减少至四分之一。
这不会改变最优批大小,因为这一比率只取决于硬件,与其他因素无关。就计算速度而言,量化后的速度为原来的两倍,但我们发现,对于 Mistral 模型规模以及其他模型,很难达到这个速度,如果以纯浮点运算量衡量,1.5 倍的速度更为合理。使用 int8 还会机械地增加 KV 缓存的可用内存。
因此,如果你处于内存受限的状态,一切操作都会快两倍,这很不错。另一个好处是,int8 几乎没有或者只有极小的精度损失,而在 int4 下会有一些性能损失,但似乎可以通过 QLoRA 来恢复,或者如果你只关心特定用例,那么我认为这也可以正常运作,且 serving 成本会低得多。
分页注意力(Paged Attention)
第三个技巧是分页注意力,由来自伯克利的 vLLM 专家提出。没有分页注意力的 KV 缓存是矩形的,需要分配一个大矩形内存,其中一个维度是批大小,即模型一次可以处理的最大序列数,另一个维度是,允许用户使用的最大序列长度。当一个新序列进来时,会为这个用户分配一整行内存,但这并不理想,因为用户中很可能只有 10% 会使用整行内存,而大多数用户可能只会发起短请求。因此,这最终会浪费硬件内存中的大量宝贵空间。
分页注意力的作用是在 GPU 内存中分配块(block)。首先,加载模型以了解剩余空间大小,然后用内存块填充剩余部分。这些块可以容纳多达 16 到 32 个词元,当新序列到来时,就可以为 prompt 分配所需的内存块,然后根据需要逐渐扩展。
在上述示意图中,可以看到序列并不一定分配在连续的内存块上,例如橙色、蓝色或绿色并不在连续的块上,这并不重要。这种方式能够更精细地控制内存分配,因此在示意图中,右侧完全空闲的部分可以用于新来的序列,一旦序列解码完成,就可以释放已使用的块,非常高效。分页注意力的提出者称,与标准的实现方法相比,分页注意力可以增加约 20 倍的吞吐量,这听起来并不是那么遥不可及。
滑动窗口注意力 (Sliding Window Attention)
我们在 Mistral 中添加了一个技巧,即滑动窗口注意力。通过这个技巧,我们可以训练模型在缓存中仅使用过去的 K 个词元。这样做的好处在于,我们可以使用一个固定的缓存大小。
众所周知,一个序列一旦超过滑动窗口的词元数量,我们就可以在缓存中循环覆写,从而重新开始,而这不会影响模型性能。
进一步来说,通过这个技巧,我们可以使用比滑动窗口更大的长下文长度。我们在博客文章或 GitHub 上对此进行了简要描述。
对于这个技巧的良好实现是将 KV 缓存看作是一个循环缓冲区。在上图中的 t 时刻,我们在缓存的最后位置插入;在 t+1 时刻,由于序列超出了滑动窗口,所以只进行了覆写操作。这种实现非常简单,因为缓存中的位置并不重要,所有与位置相关的信息都通过位置嵌入进行编码。总之,这种方法兼具易可实现性和有效性。
连续批处理(Continuous Batching)
还有一个技巧是连续批处理。正如我在前面提到的,预填充阶段同时处理的词元数量要比解码阶段多得多。因此,我们可以尝试将这些词元与解码词元一起进行批处理。我在 vLLM 和 TGI 中都注意到了同一个问题,即它们没有尝试对预填充阶段进行分块处理。如果一个用户向模型发送一个包含 4K 词元的提示,这将增加所有用户的时延,因为我们需要花费大量时间一次性处理这些词元。
这其实是一种浪费,因为这时模型就不再处于既能实现低时延,又能充分利用计算资源的最佳状态。因此,我建议在这些软件中对预填充进行分块处理,这样我们一次只处理 K 个词元。这种方法能够更加精细地分配资源,并且能够更好地对解码和预填充进行批处理。
代码
最后一种技巧是代码。在处理这些规模的模型时,代码性能非常重要。通常,我们可以观察到 Python 代码的开销很大。虽然我没有详细分析过 vLLM 和 TGI 的性能,但它们运行的是 Python 代码,根据经验,在这些规模下通常会存在一定的额外开销。我们可以采取一些方法,在不影响 Python 大部分优点的前提下缓解这一问题。
xFormers 库就是一个很好的示例,它使用 CUDA 图实现了零开销。NVIDIA 的 TensorRT 可以通过追踪推理并利用模式匹配来自动提高性能。此外,我们还可以使用自定义内核(如融合)来减少内存带宽,这样可以避免在内存中来回移动数据。在数据已加载的情况下,我们可以执行激活等操作,通常可以找到激活函数等优化技巧,然后轻松地将它们插入到代码中。
总之,驱动这些性能指标的因素主要是硬件中的固定浮点运算与内存带宽之间的比率。这给出了最小批大小 B*,以充分利用硬件资源,避免浪费不必要的浮点运算。这个大小主要由硬件决定,不太受模型影响,除非你使用了 Transformer 之外的非传统架构。由于设备的内存有限,因此要达到最佳批大小并不容易。
我检查了两个用于部署模型的开源库,它们仍在运行 Python 代码,在这一规模下,模型会产生很多额外开销。我还研究了 Faster Transformer 项目,它没有额外开销,但部署起来会比较困难。上述信息主要来自博文《语言大模型的推理演算》。
不同配置下的吞吐、时延与成本
现在让我们谈谈吞吐量 - 时延平面图,这通常是我评判这些指标的方式。在这个平面中,x 轴表示时延,y 轴表示吞吐量,我们主要关注上方和左方,即更好的吞吐量和更低的时延。
如果购买更好的硬件,会改变这一吞吐量 - 时延性能曲线。对于固定硬件,左下角区域是固定时延,即内存受限区域。随着批大小增加,系统从内存受限区域转变为计算受限区域。如果购买更先进的硬件,成本会更高,但吞吐量 - 时延上的所有曲线会整体向左上方移动。
改进代码或采用更好的模型会在低时延区域产生显著影响,增加吞吐量,这对大型批大小的影响较小,因为这时候优化已经相对容易。
下面是一些性能测试结果及免责声明,这个测试是我在短时间内完成的,因为使用 Mistral 和 LLaMA 等配置工具比较容易,我运行了 vLLM 基准测试脚本。我不确定这些结果是否是我能取得的最佳结果,但至少整体方向是正确的,下面是我复制粘贴过来的 Matplotlib 图,以供参考。
上图是 Mistral 和 LLaMA 的性能比较。图中黑线表示人类的阅读速度。
上图是在同一模型中,A10 和 H100 这两种硬件之间的比较。可以看到,尽管 H100 价格更高,但由于其卓越的性能,更换硬件是一种更明智的选择,而不是继续使用老硬件。
总的来说,使用开源代码在小型实例上部署小型模型非常容易,无需任何额外操作就能取得良好的运行效果。仅需约 15 美元 / 天(并不算太高的费用),我们就可以在 A10 上使用 Mistral-7B 模型处理上百万个请求。改变模型精度可能使服务的请求数量翻倍。
开源部署解决方案在易用性方面表现出色,我认为在实际的模型代码部分还有很多工作要做。此外我认为,未来模型的速度会越来越快。
答听众问 问题 1:如何选择用于特定模型的最佳处理器?
Timothée Lacroix : 我还没有测试过专用的 AI 硬件,主要测试过一系列 GPU。我甚至还没有在 MacBook 上运行过模型,因为目前没有找到合适的用途,但后续我可能会尝试。对于用户而言,如果只是想与模型聊天,直接在 MacBook 上运行更经济。当每天需要处理的请求达到一百万次时,使用 A10 会非常划算,相当于每天 15 美元的费用,如果用户能够负担这一费用,那么我建议选择 A10 处理器,它易于部署,而且效果很好。
关于选择何种规模的硬件,由于硬件在任何地方都很容易部署,我们可以从最便宜的硬件开始,如果没有达到所需的吞吐量或速度,再考虑升级。
我曾提到,在考虑成本的情况下,相比使用一堆 A10 处理器,H100 是更明智的选择。然而,我们也经常面临可用性问题。因此,我建议按照处理器的成本和可用性顺序逐个尝试。如果你尝试使用这些处理器大约 20 分钟,这样做的成本相对较低,并且这大致是运行基准测试所需的最长时间。通过这种方式,你可以在短时间内获得特定用例的准确成本和性能数据,从而更好地选择适合自己需求的处理器。
问题 2: 是否推荐使用 Mojo 来减少 Python 开销?你是否尝试过使用 Mojo?
Timothée Lacroix:完全没有。我首次尝试减少开销是通过使用 CUDA 图,虽然在调试过程中有一些困难,但随着时间推移,情况已经好转了,XFormers 就是一个很好的例子。在未来,torch.compile 也许能有效降低 Python 开销,但我不清楚它们在处理可变序列长度等方面的进展如何。总之,我非常推荐 CUDA 图,这是我目前降低开销的首选方法。
问题 3:如果我们想要 LLM 具备多语理解能力,但目前数据集主要是英文,相比起来,使用非英文数据进行微调的效果并不理想,对于这种情况,最有效的策略是什么?
Timothée Lacroix:LLM 的一切能力都源自数据,所以我们首先需要获取目标语言数据。所有 LLM 都是在维基百科上训练的,这为模型掌握多语能力打下了良好基础,这也解释了为何模型可以在未经特别训练的情况下理解一些法语。我认为,让模型掌握多语能力存在一种权衡,例如,如果模型在法语方面取得了进步,就会略微损失其他语言能力,但这种损失并不明显,是可以接受的,因为整体而言,在其他语言上的性能提升可能更为显著。
OneDiff 是一个开箱即用的图片 / 视频生成推理引擎。开源版最新功能:1. 切换图片尺寸无需重新编译(即没有时间消耗);2. 更快地保存和加载图;3. 更小的静态内存。
审核编辑:黄飞
评论
查看更多