自 OpenAI 发布 ChatGPT 以来,基于 Transformer 架构的大语言模型(LLM)在全球范围内引发了深度的技术关注和广泛的实践应用。其强大的理解和生成能力,正在深刻改变我们对人工智能的认知和应用。然而大语言模型的推理应用成本过高,高昂的成本大大阻碍了技术落地。
OpenPPL 一直致力于提供高性能多后端深度学习推理部署服务。面对推理部署大语言模型的新需求,我们结合原有 OpenPPL 在深度学习推理的技术和业务实践,正式推出一款专为大语言模型设计的自研高性能推理引擎 —— OpenPPL-LLM。
OpenPPL-LLM 实现了LLM 任务全流程深度优化,整个推理系统包含 4 部分:
- ppl.llm.serving 是服务框架,是推理服务系统的入口;
- ppl.pmx 包含 pmx算子标准的文档以及PythonAPI,以及官方模型的ModelZoo;
- ppl.nn.llm 是推理引擎,负责核心网络部分推理加速;
- ppl.llm.kernel.cuda 包含针对大模型开发的高性能 kernel。
目前均已开源,欢迎大家 star。
本文将从以下方面对其进行介绍:
- 性能表现
- OpenPPL-LLM 推理优化方案
- Tranformer 结构
- 模型吞吐量与显存优化
- 算子融合与延迟优化
- 推理服务优化
- 模型量化
- 模型并行
- 未来规划
一、性能表现
经过开发gg们的持续努力和精心打磨, OpenPPL-LLM 在不同场景下的推理性能都表现得无比优秀。让我们先来观摩下!
1、在固定输入、输出token的场景下:
在[OUTPUT LEN = 512,INPUT LEN = Repeat([5, 13, 27, 51])]条件下,对比 FasterTransformer,OpenPPL-LLM 在各种 batchsize下吞吐量都表现亮眼,尤其最大 batchsize 下,吞吐量提升约 40%。
2、在模拟真实对话请求的场景下:
在利用 ShareGPT_V3 数据集模拟真实对话场景的测试中,在[TASK_NUM = 1024, AVG_INPUT_LEN = 114, AVG_GEN_LEN = 322],采样算法为 ArgMax 条件下,OpenPPL.LLM 在不同模型大小下的吞吐量都明显高于主流推理框架 vLLM。尤其是在大模型(LLaMA-65B)上,OpenPPL.LLM 的吞吐量是vLLM的 3.02 倍。
而开启了概率采样(SampleTopP)之后,使用同样的数据集进行测试,设置 TopP = 0.9,temperature = 1.0 作为采样参数。由于随机采样导致部分句子提早结束生成,以及vLLM概率采样器性能较弱的原因,此时 OpenPPL.LLM 的吞吐量达到了vLLM 的 4 倍以上。
这样出色的性能表现并非偶然,而是开发团队深入研究和实施推理优化方案所带来的直接成果。下面会详细展开给大家一一介绍。
二、OpenPPL-LLM 推理优化方案
1、Transformer 结构
相比传统CNN网络,大语言模型的结构更加简单直接,并且各家模型的结构差异不大。我们目前所见的大语言模型皆是由多个相同的Transformer Block串联而成,每一个Transformer Block中则由Attention Block(多头注意力模块),FeedForward Block(前馈神经网络模块)两部分组成的,而Transformer Block之间使用带残差链接的归一化层进行链接。
下图形象的展示了Transformer Block具体结构,每一层Transformer Block都是由正规化层(Normalization),矩阵乘法(MatMul, Feedforward),自注意力层(Self Attention)所构成的。在本文当中我们也将沿用此图中的算子名称进行表示。
这其中Self Attention算子被认为是Transformer结构的灵魂所在。其中我们必须要说明的是,在GPT系列模型出现后,我们将使用KV Cache技术对Self Attention算子进行优化,这项技术的出现大大缩减了Self Attention的计算量。这使得在KV Cache技术出现后,Self Attention算子的计算特点非常显著:这是一个运算访存比接近1:1的访存密集型算子。对其访存量和计算量进行理论估计,可得以下结论:
作为对比,对MatMul和FeedForward(都是矩阵乘法算子)做类似的估计,可得结论:
在大语言模型推理中,Attention这一算子是访存密集型的,其耗时受限于硬件的访存带宽,而非运算速度。
而大语言模型中的矩阵惩罚算子则具备一些截然不同的特点,其计算量将随着Batchsize增长而快速增加。同时大语言模型有这样一个显著的特点,它们真的太”大”了——我们会发现这些FeedForward Block中的矩阵乘法的参数太多了,因此当我们输入的数据不够多,运算量不够大的时候,这些矩阵乘法算子也会因为参数访存过多而同样受限于访存带宽。以GPU为例,当Batchsize小于16时,我们都可以认为这部分算子为访存密集型的。只有当Batchsize充分大时,矩阵乘法算子才会变成计算密集型的,它们的性质会随着Batchsize变化而变化。
在大多数端侧应用中,我们都是以Batchsize=1的方式去调用大语言模型,此时大语言模型整体的运算-访存比极低,整个网络都将是访存密集的,其运行耗时完全受限于访存带宽而非硬件算力。
上表中,我们定量地分析了Batchsize=1时,7B大语言模型算子的访存需求和算力需求。可以看到,Batchsize=1时,网络中大部分的计算量和访存量都集中在Feedforward block中,
在GPT系列模型之后,我们引入了KV缓存这项技术保存用户的上下文历史记录,其大幅缩减了Attention算子的计算量,实际上Attention算子在大语言模型推理中没有消耗太多计算资源。Self Attention是大语言模型中唯一一个有记忆力的算子。它的计算过程需要读取用户的全部上下文数据,这使得上下文,运行时间则将占到20%~30%左右下图形象的展示了Self Attention算子的特点。
矩阵乘法是大语言模型推理中最为耗时的部分。下表展示了在A100 40G单卡上执行LLM-7B模型的定量分析情况,测试时使用Hidden dim=4096, Layer=32, Context Seqlen=128。可以看到矩阵乘法在整个网络执行过程中耗时占比超过80%,Self Attention算子的耗时不超过10%。
我们在此需要强调一项非常重要的特性:当我们扩大Batchsize时,所有算子的运行时间并非会随着计算量线性增长的。从Batchsize=1增长到Batchsize=512,我们的运算量增长了500倍,但执行时间仅仅延长了3倍。
产生上述现象的原因是多方面的,简单来说我们的GPU在较大规模的计算任务中会发挥更高的效率,当我们的计算任务Batchsize太小,GPU就无法充分利用它的并行核心完成计算,其计算效率会比较低。这也引出我们针对大语言模型性能优化的核心:设法提升模型能够处理的最大Batchsize,从而提升GPU的利用率与吞吐量。
上表展示了在Nvidia A100 GPU上不同形状矩阵乘法对Tensor Core的利用效率,当推理时的Batchsize较小时(<128),我们对硬件算力的利用率将不足5%。而当模型推理时batchsize大小达到512或更高,我们可以就可以更加充分地利用硬件算力。
2、模型吞吐量与显存优化
我们为何无法使用更大的Batchsize完成推理?主要的限制因素是有限的显存空间。当我们要同时处理请求数量增多,我们也就要保存更多用户的上下文数据(KV Cache),这部分的显存开销往往非常巨大,并且随着用户数量和上下文长度线性增长。
下表展示了LLM-7B模型的上下文缓存(KV Cache)显存占用情况:
以7B模型为例。当同时推理的batchsize达到32(也即32个用户同时在线),用户上下文长度达到2048时,仅仅保存用户上下文缓存就将消耗32GB的显存空间。考虑到模型自身还要大约13G的参数需要保存在显存当中,我们不难发现,即使使用拥有80G显存空间的设备进行推理,其最大支持的用户并发数也很难超过100。这并非是我们想要的结果,为此我们需要最小化每一位用户在服务器上占用的显存空间。OpenPPL-LLM将使用三种不同的优化策略,它们将成倍地降低上下文的显存占用,提供成倍的吞吐量提升。
Paged Attention:在大语言模型的推理中,每当新的用户到来,我们就会在显存上为他开辟一片充分长的KV缓存空间,用于保存用户的上下文数据。为了保证内存的连续性,所有用户的缓存空间都将被初始化成一样长的,其空间大小足以容纳长度为2048的上下文数据。而
在我们进行的测试中,用户实际使用的上下文长度平均下来只有大约500,这种粗暴的显存管理方式将导致有大约75%的显存空间被浪费掉了!
Paged Attention 实际上是一个动态分配显存的管理器,在使用了这项技术之后,我们会在模型的推理过程中不断动态地为用户追加分配他们的上下文空间。OpenPPL-LLM在此基础上使用一套基于GPU虚拟内存(MMU)的缓存管理机制,这使得我们实现的Self Attention算子能够以极低的开销访问硬件层面不连续的内存地址,并动态地完成Cache扩容。
Quantized KV Cache:这是由OpenPPL-LLM提供的缓存量化功能,我们会在写入KV缓存数据集将其量化到更低比特,从而压缩显存空间。我们会使用分组量化的方式将用户的上下文缓存数据进行高精度压缩,它的执行精度可以得到很好的保证,我们会将数据的解压缩过程融合进入Attention算子。这将节省大约50%~75%的显存空间,并且加速算子的执行。
Group Attention:在大语言模型中我们广泛应用多头注意力机制,这是对自注意力的一种扩展,通过使用多个独立的注意力头来计算不同的注意力权重。每个头都学习不同的表示和语义信息。Group Attention通过修改网络结构的方式直接削减了参与运算的注意力头的数量,可以成倍的压缩上下文缓存空间。并且许多实验结果显示大部分情况网络的性能都不会因此受到很大影响。
这三项强而有力的策略都将成倍地降低上下文的显存占用,提供成倍的吞吐量提升,并且它们是相互独立的,我们可以同时应用上述三种技术!但小心我们可能会陷入过犹不及的境地,如果我们同时应用上述技术,则模型的Batchsize将提升到一个不可思议的大小——在A100 80G上运行7B模型时,我们将可以同时服务上千个用户。我们曾经说过,随着Batchsize的提升,系统面临的计算压力会非常高,随之而来后果将是服务延迟增长到一个令用户无法接受的程度(>100ms per token),这并非是我们想要的。
3、算子融合与延迟优化
在服务端的推理中,我们重视模型的吞吐量,但也无法接受一个延迟极高的推理系统。在实际的线上服务中,我们会要求模型生成单个token的时间控制在50ms左右。在本节中,我们将从系统延迟角度对大语言模型进行分析,介绍OpenPPL-LLM中有关算子融合的方案与No pad优化。
算子融合旨在将多个相邻的算子合并为一个更大的算子,以减少计算和内存开销,从而提高网络执行的效率。算子融合能够减少中间计算结果的存储,从而降低了内存访问次数。在大语言模型推理中,核心的图融合优化全部发生在Attention Block中。如上图所示,我们将执行四个关键的算子融合:1)我们将合并残差链接与归一化层之间的操作全部融合,这将减少数次对全局内存的访问;2)我们将横向合并Q, K, V矩阵乘,更大规模的矩阵乘将更充分地利用算力;3)我们将合并Rotary Embedding的相关操作,这将减少数次全局内存访问;4)我们将使用flash attention这一较高性能的实现。
下表展示了与LLama实现的算子相比,融合操作的性能提升情况:
正如前文所述,在充分融合后的大语言模型中,所有细碎的算子均被合并了,这会导致在模型运行过程中矩阵乘的时间占比可以达到惊人的90%以上。
接下来我们将介绍No Pad优化,这项技术可以有效缩减模型推理时所需的计算量:在自然语言处理(NLP)中,padding是一种常见的预处理操作,用于将文本序列中的样本长度调整为相同的长度。这是因为在NLP任务中,输入文本通常是变长的,但是深度学习模型要求输入数据具有固定的尺寸。为了使这些模型能够处理不同长度的文本,需要使用padding来将长度不一的文本序列填充为相同的长度。
Padding操作通过在较短的文本序列末尾添加特定的符号(通常是0)来实现。这些添加的符号并不携带实际的语义信息,只是用来填充序列长度。填充后,所有的文本序列就会有相同的长度,从而可以被作为一个统一的批次输入到模型中进行处理。
而在大语言模型中推理中,我们可以通过修改Attention算子实现的方式移除网络推理时所需的padding,这项技术目前也被LightSeq, Faster Transformer, TGI等推理框架广泛使用。具体来说,我们会将参与运算的所有文本序列头尾相接,拼接成一个超长的输入序列。为了标记每一个文本序列的起止位置,我们还会需要一个序列去记录拼接文本的长度。
No Padding技术的引入,可以使得我们在大语言模型prefill阶段的运算量减少50%以上,这大大提升了系统的响应速度。
4、推理服务优化
大语言模型的推理服务优化是有挑战性的,且对于系统性能来说是至关重要的。OpenPPL-LLM引入了Dynamic Batching与Async Serving两项技术来提升服务效率。
4.1 Dynamic Batching
大语言模型是自回归迭代的,在整个推理过程中我们需要逐个生成单词。但用户的问题有长有短,所需的答案也必然长短不一,生成长度不一致的问题将使得模型推理服务的效率极低。
如上图所示,假设我们在一个Batch中,同时为{User1, User2}两位用户提供服务,User1只需生成两个Token就可以完成推理,而User2需要四个Token完成它的推理。那么对于这一个batch的推理而言我们必须等待User2的推理全部结束时才能拿到结果,并发送回客户端(Send)。并且在第三个Token之后针对User1的计算是无用的,这些出现的计算空泡使得大量算力被浪费了。
为了解决上述问题,我们将针对性地展开优化,经过优化的工作流如下图所示。首先,我们不再等待到推理结束时再一次性地返回结果,而是在每次token生成结束后都立即返回结果给用户(Send)。这样修改过后,从用户发起请求到其拿到第一批结果(虽然只有一个Token)的时间就大大缩短了。
而后,我们希望在模型推理过程中动态重组推理请求,从而充分利用算力。具体来说,我们希望从推理服务中立即移除那些已经结束推理的用户请求,并在合适的时机将新的用户请求切换到当前的模型推理过程中来。这样一来我们就可以尽可能地减少计算空泡。这种动态重组推理请求的技术也被称为Dynamic Batching。由于计算空泡的减少,在实际的推理场景中,Dynamic Batching技术的引入将提升100%~200%大模型推理服务能力(QPS)。
这是我们首次引入QPS(每秒服务用户数量)这个概念,与模型吞吐量不同,QPS衡量了大模型系统所能提供的实际业务服务能力。举例来说,假设我们搭建的大语言模型系统吞吐量为5000 tokens/sec,但计算过程中有太多无效计算(存在大量计算空泡),许多生成的token都是无效的EOS标记,这会使得生成的5000个token中可能只有2500个是有效的(实际情况接近这个比例),从而导致系统吞吐量高,但系统实际的服务能力很低。
Dynamic Batching技术的引入使得系统可以充分发挥吞吐量优势服务更多用户,首个从工业角度解决上述问题的框架是text-generation-inference(TGI),这是由著名的huggingface团队推出的专为大语言模型推理设计的完整技术方案。借助Dynamic Batching技术,TGI的模型服务能力(QPS)可达Faster Transformer的两倍以上,即使Faster Transformer的算子优化做的更好,并且可以提供更高的模型吞吐量。
OpenPPL-LLM 提出的大模型推理方案同样使用了Dynamic Batching技术,并且借助PPL Cuda的优秀框架与算子实现,Open PPL-LLM的大模型推理方案吞吐量相比Faster Transformer而言具有优势,相比TGI也具有更高的模型服务能力。
4.2 Async Severing
随着模型吞吐量的提升,模型推理过程中的前后处理与网络数据传输开销就无法被忽视了,以7B的大语言模型为例,模型的推理时间和后处理时间占比如下表所示:
OpenPPL-LLM使用高性能的后处理算子与多线程操作掩蔽前后处理的时间开销,具体来说为了解决前后处理耗时长的问题,OpenPPL-LLM启动多个线程分别处理Tokenize, Model Inference与Post Processing任务。
如下图所示,OpenPPL-LLM将启动多个线程异步地执行Tokenizer(模型前处理)与PostProcessing(模型后处理),负责前处理的线程还将承担负载均衡的职责,会将用户请求”均匀”地发送给推理线程(Model GPU1, Model GPU2),推理线程在接到请求后还将利用dynamic batching的特性拼接任务,在处理完毕后将结果发送给后处理线程。
通过线程间的延迟相互掩蔽,在大吞吐量的场景下,OpenPPL-LLM最多可以获得30%~50%的性能提升。读者需要注意的是,随着模型规模的增长,模型的吞吐量会显著下降,模型前后处理的时间可能在大模型推理过程中的时间占比会缩小。
5、模型量化
为了进一步降低大语言模型的推理成本,并且能够在内存有限的设备上运行规模更大的模型,我们在大语言模型推理中广泛应用量化压缩技术。大语言模型的结构简单且固定,为了追求更高的压缩效率,我们会针对应用场景的不同与模型结构特点设计更具性价比的压缩方案。
在OpenPPL-LLM的推理实践中,我们引入两种不同的量化技术,即Groupwise量化(通常为int4量化)与Channelwise量化(通常为INT8量化)。如下图所示,对于一个4x4矩阵而言,Groupwise量化将相邻的两个元素视作一组(图中颜色相同的元素为一组),组内元素共享量化参数。由于组内元素数量很少,这种量化方案具有较高量化精度,但将为系统带来更大的计算负担与访存开销。Channelwise量化将矩阵的每一列元素视作一组,每列元素共享量化参数,这种方案量化精度稍差,但其访存模式利于tensor core的运算,可以充分利用GPU的低精度运算器加速运算。这两种量化方式都可以起到压缩显存的作用。
针对batchsize<16的推理场景,大语言模型中的所有计算任务都是访存密集型的,系统的运行瓶颈不在于计算,因此非常适合使用groupwise量化的方式进行处理,这种量化将提供更高的量化精度。groupwise量化所带来的额外计算量也不会拖慢系统整体的运行效率。
针对batchsize>16的推理场景,groupwise的量化方式无法起到加速作用,此时系统的计算负载将逐渐转变为计算密集型的,我们必须利用GPU上具有更高算力的低精度运算器(int8 Tensorcore)加速网络执行。Groupwise量化并不利于GPU TensorCore的运行,这种量化方式将导致严重的访存不连续,因此性能很差。Channelwise量化可以更好地解决这一问题。
下图展示了针对矩阵乘法运算而言,两种量化模式在不同batchsize的场景下的性能情况:对于batchsize极小的情况,Groupwise量化将提供接近3倍的性能提升,但随着batchsize的提高,Groupwise的加速效果将大打折扣。随着batchsize提升到16~32以后,Channelwise量化将体现出更好的性能优势。
需要注意的是,Groupwise量化与Channelwise量化对应着不同的权重预处理过程,这两种方案必须在服务启动时加以确定,不能在模型运行时动态切换。因此我们需要在服务部署之初就确定量化模式的选择。
对于大语言模型而言,其性能瓶颈在于矩阵乘法算子(占系统运行时间的80%以上),以及Self Attention(占系统显存开销的50%以上),OpenPPL-LLM针对大语言模型的量化压缩方案将围绕这两个算子展开。
大语言模型中的矩阵乘法算子往往具有很大规模的参数矩阵,参数访存量远远超过激活访存。基于这个特性,在batchsize比较小时,我们推荐以Groupwise量化的方式单独压缩矩阵乘法的参数矩阵,其计算和输入输出仍然保持FP16精度。在batchsize比较大时,我们推荐以Channelwise量化的方式同时对矩阵乘法的输入和参数矩阵进行量化。
Self Attention算子在推理时将占据显存开销的50%以上,这一算子限制了我们能够使用的最大batchsize。通过量化压缩KV Cache的方式,我们可以成倍地缩小KV Cache的显存占用,从而大幅提升系统吞吐量,这是一项至关重要的优化。OpenPPL-LLM针对性地使用Groupwise量化模式对KV Cache进行压缩,在计算时将数据全部解压缩回fp16,从而实现极高精度的量化,加速网络运行,提高模型最大吞吐量。Self Attention的算子行为不涉及tensor core的运算,因此不论batchsize选取如何,都将采用精度更高的Groupwise方式完成量化。KV Cache的量化压缩能够成倍的提升系统吞吐量,并具有极高的量化精度,OpenPPL-LLM将默认开启这项优化。
6、模型并行
目前大语言模型并行推理的瓶颈在于通信量小,但通信次数很多。在运行参数量在13B以上的大语言模型时,由于单卡容量限制,模型需要被平均切分到多张显卡上去运行。我们使用Tensor Parallel技术对模型进行切分,这种并行模式的开销最小,其并行推理示意图如下图所示:
对于一个典型的176B大语言模型而言,其包含70个Transformer Block,Hidden Dim为14336;每一次前向传播过程中共计需要141次,每次通信传输的数据量约为batchsize*14336*2,通常远远小于1M。目前A100的Nvlink理论带宽为400-600GBps,但由于每次的通信量很小,实际带宽远远小于理论带宽,大约为15-30GBps左右。若以PCIE方式进行通信,理论带宽为32-64GBps左右,实际带宽为1-3GBps。
下表记录了在8卡A100 40G SXM2环境下,由NCCL官方样例测试的all reduce带宽。可以看到在通信量较小时,实际通信对带宽的利用率极低。
关于通信效率较低的问题,OpenPPL-LLM 中目前并没有进一步的解决方案,我们依然使用Tensor parallel 的模式完成模型切分,使用NCCL完成卡间互联通信。在未来我们将探索更为高效的模型并行方式,尝试提升模型的并行效率。
三、未来规划
- 推理引擎
- 更高效的融合优化以及 Paged Attention
- 自研更高效的Flash Attention
- 支持Group Query Attention
- 数据排布优化,提高内存带宽利用率
- GEMM 优化,INT4/INT8 量化支持
- 针对大模型的动态内存管理,调度管理
- 尝试NCCL性能优化,进一步降低通信开销
- 优化代码结构
- 服务优化
- 精细化KV Cache 管理,支持用户迁移
- 优化框架性能,提高硬件利用率
- 支持Pipeline 并行,提高可扩展性
- 优化框架架构
- 模型工具
- 扩展模型支持,InternLM,ChatGLM,Baichuan,Bloom…
- Python API 性能优化,高效Python 推理
- 硬件支持
- 引入更多国产芯片支持,MLU,Ascend
- 移动端GPU支持,主要为OpenCL
- ARM CPU支持以及更多硬件平台
关注我们的开源项目:
- https://github.com/openppl-public/ppl.llm.serving
- https://github.com/openppl-public/ppl.pmx
- https://github.com/openppl-public/ppl.nn.llm
- https://github.com/openppl-public/ppl.llm.kernel.cuda
References
- https://github.com/vllm-project
- https://github.com/huggingface/text-generation-inference
- https://github.com/NVIDIA/FasterTransformer
- https://github.com/google/sentencepiece
- https://github.com/FMInference/FlexGen
- Shazeer N. Fast transformer decoding: One write-head is all you need[J]. arXiv preprint arXiv:1911.02150, 2019.