大语言模型推理加速技术:计算加速篇

原文:大语言模型推理加速技术:计算加速篇 - 知乎

目录

简介

Transformer和Attention

瓶颈

优化目标

计算加速

计算侧优化

KVCache

Kernel优化和算子融合

分布式推理

内存IO优化

Flash Attention

Flash Decoding

Continuous Batching

Paged Attention

SplitFuse

总结


本文是《大语言模型推理加速技术》系列的第一篇

《大语言模型推理加速技术:计算加速篇》

《大语言模型推理加速技术:模型压缩篇》

《大语言模型推理加速技术:推理框架篇》

自从去年ChatGPT横空出世之后,业界对于大语言模型的热情也愈发高涨,随着模型规模越来越大,它们的计算需求也水涨船高,大模型部署和所需的资源量也让很多团队望而却步:毕竟可以拿社区开源的预训练模型跳过训练的过程,但是部署大模型推理是无法避开的流程。本系列旨在简单讨论几个业界生产环境可用的大模型推理技术,并分析对比几个主流的推理框架。

由于各大公司和学术团队都在“卷”大模型,大模型新技术层出不穷,本系列只能保证当前的信息有效性(2023年11月初)。另外由于本文是从工程角度出发,只会介绍工业界可落地的技术,一些前沿的学术成果可能并不包含在内,敬请谅解。

简介

Transformer和Attention

当前主流的大模型都是基于2017年谷歌团队提出的Transformer架构,其核心是注意力(Attention)机制,简单来说就是计算softmax(qk^T)*v:

def attention(q_input, k_input, v_input):q = self.Q(q_input)k = self.K(k_input)v = self.V(v_input)return softmax(q * k.transpose()) * v

其中Q,K,V是模型的三个矩阵。对当前主流的Decoder-only模型来说,推理过程分为两个阶段:

  1. context phase也叫prefill phase:需要计算整个prompt的自注意力,q_input, k_input, v_input大小都为[seq_len, emb_dim],即整个prompt的embedding,context phase只需要进行一次,生成第一个token。
  2. generation phase或decoding phase:每生成一个token就要计算一次,其中q_input为[1, emb_dim],代表当前token的embedding,k_input, v_input为[n, emb_dim]代表所有前文的embedding,这个阶段计算的是当前token和所有前文的注意力。

Attention计算占据了模型推理阶段的绝大部分资源,要了解本文介绍的优化技巧,只需要了解Attention的计算过程就足够了。由于本文更多关注工程细节,对模型的设计只是一笔带过,有兴趣的读者可自行了解完整的Transformer相关信息。

Attention计算,来自Raimi Karim的Towards Data Science文章

瓶颈

想知道如何优化,我们首先要知道模型的计算慢在哪里。

  1. 大模型通常需要处理很长的输入和输出,由于当前token需要和前面所有的token进行attention计算,随着seq_len和n的增加,模型需要的计算的矩阵尺寸也越来越大。
  2. 在生成阶段,每个token的生成都依赖前面所有的计算,只能一个一个token生成,无法并发计算。

简单来说,推理瓶颈就在于Attention的计算是一个O(N^2)的操作且无法并发。本文所介绍的技术都是针对以上两个瓶颈进行优化。

优化目标

加速优化一般有两个目标,一个是更低的延迟(Latency),一个是更高的吞吐量(Throughput),其中延迟是指单个请求返回的时间,而吞吐量是一定时间内处理请求的总量。这两者有时是不可兼得的。举个例子,如果我们把一个模型切分成多个小模型进行分布式计算(张量并行),我们可以把单个请求的速度提升数倍(延迟下降),但是由于有通信和聚合的成本,系统处理单个请求的资源消耗量变多了,导致系统的吞吐量也会下降。有些加速技术只会针对其中一种目标,我们后面会详细介绍。

在本系列中,推理优化技术分为两大类:计算加速模型压缩。计算加速,通过改进算法和硬件利用率来提高效率,而不影响模型的输出质量,本质上是让模型“算得更快”。而模型压缩则是改变模型结构,减少部分计算(比如稀疏Attention)或降低计算精度(比如量化),换来更快的推理速度和更低的资源消耗,但可能会影响模型的输出质量,本质上是让模型“算得更少”。本文作为系列的第一篇,只介绍计算加速技术,我们将在下一篇文章中介绍模型压缩技术。

计算加速

任何计算的本质都是CPU/GPU执行一系列的指令,在模型结构固定的情况下,我们能做的优化无非就是以下三种:

  1. 减少需要执行的指令数量,即减少不必要的或重复的运算。
  2. 充分利用硬件的并发度,要么是让单条指令可以一次处理多条数据(SIMD),要么是利用CPU和GPU的多核心机制,同时执行多条指令。
  3. 加速内存IO速度。利用缓存局部性加速内存读取指令的执行速度,或者减少不必要的内存读写。

前两种我们称之为计算侧优化,后一种我们称之为内存IO优化

计算侧优化

KVCache

在每一个decoding phase中,我们需要计算当前token和之前所有已生成token的attention,因此需要计算所有token的k和v向量,但是前面的token的kv值在每轮decoding中都被重复计算了,因此我们可以把它们存下来,存成两个[seq_len-1, inner_dim]的Tensor,在每轮计算中只需要计算当前token的kv值即可。

KVCache是最简单直接的优化手段,一般模型的默认实现都会自带KVCache,因此并不需要我们额外实现,以Huggingface Transformers库为例,我们只需要在配置中设置use_cache=True即可。

这里还想吐槽一下,一般我们在工程中要到的cache也都是基于hash_map这种kv map的,最开始我还以为KVCache也是基于map的复杂数据结构,没想到只是简单的两个Tensor就实现了。

KVCache图解,来自Joao Lages的Medium文章

Kernel优化和算子融合

在NVIDIA GPU环境上,我们通过CUDA Kernel来执行大部分运算,矩阵乘法(GEMM),激活函数,softmax等,一般老说每个操作都对应一次kernel调用。但是每次kernel调用都有一些额外开销,比如gpu和cpu之间的通信,内存拷贝等,因此我们可以将整个Attention的计算放进同一个kernel实现,省略这些开销,在kernel中也可以实现一些Attention专用的优化。比如Facebook的xformers库就为各种Attention变种提供了高效的CUDA实现。主流的推理库也基本都自带了高效的Kernel实现。

除了手写的Kernel外,模型编译器也可以提供类似的优化机制,编译器将模型结构转换为其中间格式然后进行一系列的优化,比如我们提到的算子融合。虽然在CUDA这种主流平台上编译器的优化效果不一定比得上专用的Kernel实现,但是多平台的通用性是编译器的一大优势,比如TVM团队的MLC-LLM和微软主导的ONNX常被用来在手机等边缘设备上运行大模型。

分布式推理

在大模型的训练和推理过程中,我们有以下几种主流的分布式并行方式:

  • 数据并行(DataParallel):将模型放在多个GPU上,每个GPU都包含完整的模型,将数据集切分成多份,每个GPU负责推理一部分数据。
  • 流水线并行(PipelineParallel):将模型纵向拆分,每个GPU只包含模型的一部分层,数据在一个GPU完成运算后,将输出传给下一个GPU继续计算。
  • 张量并行(TensorParallel):将模型横向拆分,将模型的每一层拆分开,放到不同的GPU上,每一层的计算都需要多个GPU合作完成。

流水线并行一般是一台GPU内存无法放下整个模型的妥协之举,各层之间仍然是顺序运行的,并不能加速模型的计算。而另外两种并行则可以加速模型的推理。推理阶段的数据并行非常简单,因为不需要像训练一样聚合推理结果更新参数,因此我们只需要将模型单独部署在多个GPU上推理即可。而张量并行一般使用NVIDIA的Megatron库,模型内部结构改为使用Megatron的ColumnParallelLinear、RowParallelLinear、ParallelMLP和ParallelAttention等结构实现。

流水线并行和张量并行,来自Sequence Parallelism的paper

内存IO优化

在做性能优化时,我们除了要考虑单纯计算的速度,也要考虑内存访问的速度。一方面,和CPU缓存一样,GPU也有类似L1、L2这样的分级缓存(SRAM和HBM),级数越低的缓存大小越小,访问速度越快,因此我们在优化模型推理时也要考虑内存访问的局部性(Cache Locality)。另一方面,KVCache随着batch size和seq len的增加而扩张,在推理过程中会占据超过30%的内存,可能会出现因为内存不够用而限制最大并发度的问题。在并发度较高或者输入输出长度较大时,内存访问反而可能成为计算的瓶颈,而非CPU/GPU的计算量。

GPU的分级缓存,来自Flash Attention的paper

推理时的内存占用,来自vllm的paper

Flash Attention

在进行Attention计算时,QKV都是非常大的矩阵,直接进行矩阵乘法运算是非常缓存不友好的。因此我们可以考虑对矩阵进行分块乘法,每次只计算一个小的block,保证block可以放进SRAM而非HBM中。实际上这是一个很经典的思路,大部分的矩阵乘法kernel也是这样实现的。

而FlashAttention则更进一步,我们观察到Attention计算分为三步:

  1. 从HBM读取QK,计算S = QK^T,将S写回HBM
  2. 从HBM读出S,计算P = softmax(S),将P写回HBM
  3. 从HBM读出P和V,计算O=PV,将O写回HBM

我们然需要在整个计算过程中HBM读三次写三次,有没有办法只读写一次呢?如果Attention只是简单的矩阵乘法,可以通过分块计算的方法避免写回HBM,但是由于softmax的存在,我们无法直接这样做。因为softmax需要计算矩阵中每一行元素的最大值,所以我们必须等待所有分块遍历完成后才能计算下一步。

FlashAttention巧妙地利用了类似于动态规划的技巧,实现了online softmax,可以在一个循环中计算出一个分块的最终结果。FlashAttention算法在遍历过程中需要不断地对中间结果进行重新计算,但是得益于整个过程不需要读HBM,再增大了计算量的情况下仍然可以提升运算速度。

关于Flash Attention的完整介绍和数学推导,我推荐华盛顿大学的这个课件,里面非常直观地解释了Flash Attention背后的想法和推导过程。用户可以通过使用Flash Attention的库调用它的kernel,PyTorch官方也对Flash Attention提供了官方支持。

Flash Decoding

由于Flash Attention优化的是大矩阵乘法,矩阵越大优化效果应当越好。但是在在线推理的场景中,输入的batch size为1,Q矩阵实际上是一个向量而非矩阵,在这种场景下,Flash Attention无法充分地利用GPU的并发能力。而Flash Decoding通过以seq_len为维度并发,即将K和V分成多个部分,并发地与Q相乘而解决了这个问题。

与Flash Attention适合离线训练和批量推理不同,Flash Decoding在在线单次推理且上下文长度较长时效果更好。用户可以通过FlashAttention库或者xFormers的attention kernel来使用Flash Decoding。

动图封面

Flash decoding示例

Continuous Batching

在批量推理过程中我们一般使用固定的Batch Size,将多个请求Batch起来一起推理。在分配KVCache时,我们需要分配两个shape为[batch_size, seq_len, inner_dim]的tensor,但是不同的请求可能有不同输入和输出长度,而且我们无法预知最终的输出长度,无法固定seq_len,因此我们通常分配[batch_size, max_seq_len, inner_dim]这样的shape,保证所有请求的cache都放得下。

但是这样的分配策略有两个问题:

  1. 不是每个请求都可以达到max_seq_len,因此KVCache中很多的内存都被浪费掉了
  2. 即使一些请求输出长度很短,它们仍然需要等待输出较长的请求结束后才能返回

这种固定的Batch策略叫做静态Batching(Static Batching),为了解决这个问题,Orca提出了Continuous Batching策略,也叫Dynamic Batching或Inflight Batching。Continuous Batching允许输出较短的请求提前结束,并由新请求占用已结束请求的KVCache空间。

Continuous Batching示例,来自Anyscale官网,黄色为prompt,蓝色为生成的token

在批量推理场景中,Continuous Batching可以将模型的吞吐量提升两到三倍。当前主流的推理框架比如Huggingface TGI, Ray serve, vllm, TensorRT-LLM等都支持Continuous Batching策略。

Paged Attention

vLLM团队分析了推理时的内存浪费问题,认为推理中存在三种内存浪费

  1. Reservation:由于不确定每个请求的输出长度,我们需要给每个请求预留max_seq_len的空间。
  2. Internal Fragmentation:在Static Batching策略下,一个请求结束时,其剩余的空间旧被浪费掉了。
  3. External Fragmentation:由于KVCache是一个巨大的矩阵,且必须占用连续内存,操作系统如果只分配大的连续内存,势必有很多小的内存空间被浪费掉。

请求中的内存浪费,来自vLLM paper

vLLM团队认为,Continuous Batching可以部分解决Internal Fragmentation问题,但是Reservation和External Fragmentation的浪费仍然存在。因此他们提出了Paged Attention,其借鉴了操作系统中通过Page管理虚拟内存的思想:将KVCache分割为固定大小的Block,这些block不需要存储在连续内存中,由一个统一的内存分配器管理。请求按需申请内存,不需要预先留好max_seq_len大小的内存,解决了Reservation的浪费,请求结束后释放掉自己的blocks,解决了Internal Fragmentation,而系统只需要分配小的block,解决了External Fragmentation的问题。

vLLM团队的benchmark显示,使用PagedAttention可以将模型批量推理的吞吐量再提升3倍以上,达到Static Batching的6倍,而Paged Attention的另一个好处是不同的请求可以共享cache block,比如在beam search场景中,我们需要对同一个prompt生成多个结果,这些子请求就可以共享同一批prompt cache,PageAttention可以将beam search的吞吐量提升10倍以上。

用户可以通过官方的vLLM库使用Paged Attention,英伟达的TensorRT-LLM库和微软的Deepspeed-MII库也对部分模型提供了支持。

SplitFuse

微软的DeepSpeed团队观察到:如果我们要在F个前向推理中处理P个token,最高效的分配策略是将它们均分,即每个前向推理处理P/F个Token。但是在模型推理过程中,我们需要先在context phase一次性处理整个prompt,然后在genration phase一个一个生成token,是不符合最优策略的,因此deepspeed提出了SplitFuse:

  1. 将长prompt分割成多个短的输入,分在多个前向推理处理,只有最后一次推理会生成新token。
  2. 短的prompt会被合并在一起进行前向推理。
  3. 保证每次前向推理的输入token数是固定的。

在vLLM中,不同请求的prompt和generation需要分开处理,而Deepspeed SplitFuse可以把它们混合处理,图片来自deepseed blog

SplitFuse在输入长度越长时效果越明显,在Deepspeed自己的benchmark中,输入长度为2600时吞吐量可以达到vLLM的2.3倍,不过vLLM团队不久前推出了PagedAttentionV2,专门对长prompt进行了优化,不知道deepspeed的benchmark有没有对比最新的vLLM。

由于SplitFuse刚出不久,我对它的理解还不是很透彻,它声称在generation阶段也是每次输入一个>1的固定长度n,但是模型generation阶段一次只能生成一个token,是怎么做到输入n个token的,难道是直接pad成n个token,这样真的不会导致generation很浪费计算资源吗?我看到Deepspeed的benchmark里都是prompt length=2600,generation length=60,我怀疑它只适合prompt很长generation很短的情况,欢迎了解Deepspeed的读者在评论区解读。

SplitFuse目前被包含在DeepSpeed MII框架里,暂时只支持LLAMA,Mistral,OPT三种模型架构。

总结

本文是《大语言模型推理加速技术》系列的第一篇,简单介绍了大模型的计算过程和一些主流的推理加速技术。本篇所介绍的技术都是不改变模型结构和精度的前提下,以目标为最大化硬件利用率的优化技术。在后续的两篇文章中,我们将继续探讨模型压缩技术和当前主流的推理框架。

由于我并不是大模型方面的专家,本篇文章中可能出现了很多不准确的解读,也欢迎各位读者赐教~

Reference:

[1] Attention介绍:https://towardsdatascience.com/illustrated-self-attention-2d627e33b20a

[2] KVCache介绍:https://medium.com/@joaolages/kv-caching-explained-276520203249

[3] SequenceParallel: https://arxiv.org/pdf/2105.13120.pdf

[4] From Online Softmax to FlashAttention: https://courses.cs.washington.edu/courses/cse599m/23sp/notes/flashattn.pdf

[5] Flash Decoding: https://pytorch.org/blog/flash-decoding/

[6] How continuous batching enables 23x throughput in LLM inference while reducing p50 latency: Achieve 23x LLM Inference Throughput & Reduce p50 Latency

[7] vLLM: Efficient Memory Management for Large Language Model Serving with PagedAttention

[8] SplitFuse: https://github.com/microsoft/DeepSpeed/tree/master/blogs/deepspeed-fastgen

[9] xFormers: GitHub - facebookresearch/xformers: Hackable and optimized Transformers building blocks, supporting a composable construction.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/705471.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go Run - Go 语言中的简洁指令

原文:breadchris - 2024.02.21 也许听起来有些傻,但go run是我最喜欢的 Go 语言特性。想要运行你的代码?只需go run main.go。它是如此简单,我可以告诉母亲这个命令,她会立即理解。就像 Go 语言的大部分功能一样&…

微调实操四:直接偏好优化方法-DPO

在《微调实操三:人类反馈对语言模型进行强化学习(RLHF)》中提到过第三阶段有2个方法,一种是是RLHF, 另外一种就是今天的DPO方法, DPO通过直接优化语言模型来实现对其行为的精确控制,而无需使用复杂的强化学习,也可以有效学习到人类偏好,DPO相…

python中的类与对象(2)

目录 一. 类的基本语法 二. 类属性的应用场景 三. 类与类之间的依赖关系 (1)依赖关系 (2)关联关系 (3)组合关系 四. 类的继承 一. 类的基本语法 先看一段最简单的代码: class Dog():d_…

智慧公厕的目的和意义是什么?

智慧公厕是近年来城市建设中的一项重要举措,其目的在于实现公共厕所的智慧化管理,为市民群众提供更好的服务体验,助力智慧城市和数字环卫的发展,提升社会公共卫生服务水平。 与此同时,智能公厕也具有重要的意义&#x…

springboot+vue实现微信公众号扫码登录

通常在个人网站中,都会有各种第三方登录,其中微信登录需要认证才能使用,导致个人开发者不能进行使用此功能,但是我们可以使用微信公众号回复特定验证码来进行登录操作。 微信关键词处理 微信公众号关键词自动回复,具体…

60kW 可编程直流回馈负载箱的优势和特点

60kW可编程直流回馈负载箱是一种先进的电力设备,主要用于模拟电网中的负载,为电力系统提供稳定的负载环境。它具有许多优势和特点,使其在电力系统中得到了广泛的应用。 60kW可编程直流回馈负载箱具有高效的能源转换效率,能够将电能…

人机界面和三菱PLC之间以太网通信

本文主要描述人机界面WinCC如何与三菱Q系列PLC进行以太网通讯,主要介绍了CPU自带以太网口和扩展以太网模块两种情况以及分别使用TCP、UDP两种协议进行通讯组态步骤及其注意事项。 一、 说明 WinCC从V7.0 SP2版本开始增加了三菱以太网驱动程序,支持和三…

Windows常用协议

LLMNR 1. LLMNR 简介 链路本地多播名称解析(LLMNR)是一个基于域名系统(DNS)数据包格式的协议,可用于解析局域网中本地链路上的主机名称。它可以很好地支持IPv4和IPv6,是仅次于DNS 解析的名称解析协议。 2.LLMNR 解析过程 当本地hosts 和 DNS解析 当本地hosts 和 …

docker 常用指令(启动,关闭,查看运行状态)

文章目录 docker 常用指令启动 docker关闭 docker查看 docker的运行状态 docker 常用指令 启动 docker systemctl start docker关闭 docker systemctl stop docker查看 docker的运行状态 systemctl status docker如下图所示: 表示docker正在运行中

集合框架体系和使用1(Collection)

Map的不同实现类单独再搞一章讲 目录 数组的特点、弊端与集合框架体系介绍 数组 特点 弊端 Java集合框架体系(java.util包下) java.util.Collection:存储一个一个的数据(主要讲两个子接口) java.util.Map:存储一对一对的数据…

基于uniapp大学生社团活动管理系统python+java+node.js+php微信小程序

uni-app框架:使用Vue.js开发跨平台应用的前端框架,编写一套代码,可编译到Android、小程序等平台。 语言:pythonjavanode.jsphp均支持 框架支持:springboot/Ssm/thinkphp/django/flask/express均支持 运行软件:idea/eclipse/vscod…

递归和迭代【Py/Java/C++三种语言详解】LeetCode每日一题240218【树DFS】LeetCode 589、 N 叉树的前序遍历

有LeetCode算法/华为OD考试扣扣交流群可加 948025485 可上全网独家的 欧弟OJ系统 练习华子OD、大厂真题 绿色聊天软件戳 od1336了解算法冲刺训练 文章目录 题目描述解题思路代码方法一:递归法PythonJavaC时空复杂度 方法二:迭代法PythonJavaC时空复杂度 …

面试redis篇-08数据淘汰策略

原理 当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。 Redis支持8种不同策略来选择要删除的key: noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是…

JetBrains系列工具,配置PlantUML绘图

PlantUML是一个很强大的绘图工具,各种图都可以绘制,具体的可以去官网看看,或者百度。 PlantUML简述 https://plantuml.com/zh/ PlantUML语言参考指引 https://plantuml.com/zh/guide PlantUML语言是依赖Graphviz进行解析的。Graphviz是开源…

[设计模式Java实现附plantuml源码~行为型] 撤销功能的实现——备忘录模式

前言: 为什么之前写过Golang 版的设计模式,还在重新写Java 版? 答:因为对于我而言,当然也希望对正在学习的大伙有帮助。Java作为一门纯面向对象的语言,更适合用于学习设计模式。 为什么类图要附上uml 因为很…

2024程序员容器化上云之旅-第6集-Ubuntu-WSL2-Windows11版:艰难复活

故事梗概 Java程序员马意浓在互联网公司维护老旧电商后台系统。 渴望学习新技术的他在工作中无缘Docker和K8s。 他开始自学Vue3并使用SpringBoot3完成了一个前后端分离的Web应用系统,并打算将其用Docker容器化后用K8s上云。 8 复活重生 周末终于有点属于自己的…

【书籍分享 • 第三期】虚拟化与容器技术

文章目录 一、本书内容二、读者对象三、编辑推荐四、前言4.1 云计算技术的发展4.2 KVM、Docker4.3 本书内容简介4.4 作者简介 五、粉丝福利 一、本书内容 《虚拟化与容器技术》通过深入浅出的方式介绍KVM虚拟化技术与Docker容器技术的概念、原理及实现方法,内容包括…

Linux之安装Nginx、前后端分离项目部署

目录 一、安装Nginx 1.1先一键安装4个依赖 1.2下载并解压安装包 1.3安装nginx,一般我们在nginx都是要安装ssl证书的 1.4 启动nginx服务 1.5开放80端口 1.6配置nginx自启动 1.7修改/etc/rc.d/rc/local的权限 二、多个tomcat负载加后端部署 2.1创建多个tomca…

Windows已经安装了QT 6.3.0,如何再安装一个QT 5.12

要在Windows上安装Qt 5.12,您可以按照以下步骤操作: 下载Qt 5.12:访问Qt官方网站或其他可信赖的来源,下载Qt 5.12的安装包。 下载安装地址 下载安装详细教程 安装问题点 qt安装时“Error during installation process(qt.tools…

react useRef用法

1&#xff0c;保存变量永远不丢失 import React, { useState,useRef } from react export default function App() { const [count,setcount] useState(0) var mycount useRef(0)//保存变量永远不丢失--useRef用的是闭包原理 return( <div> <button onClick{()>…