选择正确的LLM推理栈意味着选择适合你的任务的正确模型,并配以适当的推理代码在适当的硬件上运行。本文介绍了流行的LLM推理堆栈和设置,详细说明其推理的成本构成;并讨论当前的开源模型以及如何充分利用它们,同时还涉及当前开源服务栈中仍然缺失的功能,以及未来模型将解锁的新功能。
本文源自Mistral AI首席技术官Timothée Lacroix的演讲。他于2015年在Facebook AI Research担任工程师,于2016年至2019年间与École des Ponts合作完成了关于推荐系统的张量分解的论文。2023年他成为Mistral AI的联合创始人。Mistral AI于近期发布了业内首个开源MoE大模型Mixtral-8x7B。
(以下内容由OneFlow编译发布,转载请联系授权。地址:https://www.youtube.com/watch?v=mYRqvB1_gRk)
作者 | Timothée Lacroix
OneFlow编译
翻译|宛子琳、杨婷
本次演讲的很多内容都基于我在网上找到的信息或通过对第一个LLaMA版本模型进行实验时的发现。我认为,现在的Mistral更关注推理成本,而非训练成本。因此,我将分享推理成本的构成、吞吐、时延及其影响因素。
很多人想要部署语言大模型,我将分享如何使用开源工具部署自己的语言大模型。当然,你也可以使用一些出色的公共API,但我对开源工具更感兴趣,所以接下来我将深入讨论部署一个70亿参数模型的重要细节。我将分享的许多内容也同样适用于更大规模的模型,但那需要更多GPU。
1
影响推理的指标
我们将首先讨论有哪些重要指标,以及这些指标的影响因素,包括硬件和软件层面。接下来,我将介绍一些能够改善性能的技巧,据我所知,其中一些技巧还未获得广泛实现。我尝试在各种不同的硬件上运行了一系列模型,并尝试获得性能曲线,我认为实例非常重要,所以我将通过这些数据得出结论。
首先,我们该关注哪些指标?第一是吞吐量,以每秒查询数(Query/second)表示,我们希望在批处理作业中将这一指标最大化,或者希望允许更多用户使用我们的服务。第二是时延,以每词元每秒(seconds/token)表示,即输出下一个词元所需的时间,这决定了你的应用程序的速度和灵敏度。在ChatGPT中,这一速度相当快。对于较小的模型,可以更轻松地实现快速响应,因此我们希望将这个值最小化以提升用户体验。较为优秀的阈值是每分钟输出250个单词,我认为这是人类的平均阅读速度,只要你的时延低于这个值,用户就不会感到无聊。第三是成本,毫无疑问,这一数值越低越好。
2
影响推理指标的因素
现在我将深入探讨这些指标的影响因素。我只会谈论自回归解码,即基于一批批词元通过神经网络确定下一批词元,这部分不包括处理查询的第一部分。提示处理有时被称为预填充(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中每个模型权重占两个字节,所以只需2×7=14GB内存。
然后,我们用KV缓存存储计算结果,这样当我们重新编码一个新词元时,就不必重新从头计算。KV缓存的大小为2,包括K缓存和V缓存,且使用FP16格式,每个都乘以2,然后每层有一个KV缓存,并且必须为批次中的每个元素保存数据,每个位置在序列中表示一个词元,然后乘以维度。
把实际数值代入这个公式发现,每个批次元素需要约2G内存才能支持最大长度4K,因此,在A10(24GB内存)上,我们的最大批大小约为5,在更大的A100(80GB内存)上,最大批大小只有33左右,这仍远低于理想值400。
因此,对于所有实际用例,使用70亿参数的模型进行推理时,解码过程将严重受限于内存带宽。这也证明了Mistral从一开始就非常谨慎的一点:模型和KV缓存所占内存的大小确实影响了可允许的最大批大小,而最大批大小直接决定了效率的高低。
3
实用技巧
现在我将深入讨论一些已经存在但我个人很喜欢的技巧。其中一部分已经为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项目,它没有额外开销,但部署起来会比较困难。上述信息主要来自博文《语言大模型的推理演算》。
3
不同配置下的吞吐、时延与成本
现在让我们谈谈吞吐量-时延平面图,这通常是我评判这些指标的方式。在这个平面中,x轴表示时延,y轴表示吞吐量,我们主要关注上方和左方,即更好的吞吐量和更低的时延。
如果购买更好的硬件,会改变这一吞吐量-时延性能曲线。对于固定硬件,左下角区域是固定时延,即内存受限区域。随着批大小增加,系统从内存受限区域转变为计算受限区域。如果购买更先进的硬件,成本会更高,但吞吐量-时延上的所有曲线会整体向左上方移动。
改进代码或采用更好的模型会在低时延区域产生显著影响,增加吞吐量,这对大型批大小的影响较小,因为这时候优化已经相对容易。
下面是一些性能测试结果及免责声明,这个测试是我在短时间内完成的,因为使用Mistral和LLaMA等配置工具比较容易,我运行了vLLM基准测试脚本。我不确定这些结果是否是我能取得的最佳结果,但至少整体方向是正确的,下面是我复制粘贴过来的Matplotlib图,以供参考。
上图是Mistral和LLaMA的性能比较。图中黑线表示人类的阅读速度。
上图是在同一模型中,A10和H100这两种硬件之间的比较。可以看到,尽管H100价格更高,但由于其卓越的性能,更换硬件是一种更明智的选择,而不是继续使用老硬件。
总的来说,使用开源代码在小型实例上部署小型模型非常容易,无需任何额外操作就能取得良好的运行效果。仅需约15美元/天(并不算太高的费用),我们就可以在A10上使用Mistral-7B模型处理上百万个请求。改变模型精度可能使服务的请求数量翻倍。
开源部署解决方案在易用性方面表现出色,我认为在实际的模型代码部分还有很多工作要做。此外我认为,未来模型的速度会越来越快。
4
答听众问
问题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.更小的静态内存。
地址:https://github.com/siliconflow/onediff
使用方法:https://github.com/siliconflow/onediff/releases/tag/0.12.0
其他人都在看
试用OneDiff: github.com/siliconflow/onediff