进阶任务三 LMDeploy 量化部署实践闯关任务
1 大模型部署基本知识
1.1 LMDeploy部署模型
定义
- 在软件工程中,部署通常指的是将开发完毕的软件投入使用的过程。
- 在人工智能领域,模型部署是实现深度学习算法落地应用的关键步骤。简单来说,模型部署就是将训练好的深度学习模型在特定环境中运行的过程。
场景
- 服务器端:CPU部署,单GPU/TPU/NPU部署,多卡/集群部署……
- 移动端/边缘端:移动机器人,手机……
LMDeploy 针对丰富的部署场景,提供了多种服务与功能。支持多种推理接口,量化,引擎,服务等技术,同时可实现多种 LLM 和 VLM 的部署。
而且 LMDeploy 的推理性能在业内也具有领先地位,相比 vllm 具有更好的推理性能。
1.2 大模型缓存推理技术
大模型是一个 decoder-only 的模型,核心是 transformer 的 decoder 架构。该架构的核心算子是注意力机制,在注意力机制中,对于输入的张量 X,要通过 3 个线性变换将其转换为 查询 Query Q,键 Key K,值 Value V。通过 Q 和 K 的内积计算,可以得到注意力得分 Attention Score,然后通过注意力得分和 V 进行计算以实现注意力汇聚。
大模型的推理可以分为两个阶段:输入阶段,生成阶段。
输入阶段:可以理解为我们与大模型交互时输入问题的阶段。我们输入的问题是一个包含多种token的序列,这些 token被一次性同时输入大模型。在计算注意力机制时,针对输入序列进行线性投影,同时得到所有的 Q K V 是没有任何问题的。这个阶段也被称为 pre-filling 预填充。
生成阶段:可以理解为大模型给出回复的阶段。在生成阶段,token是逐个迭代生成的。每一次生成token的迭代,大模型都会接受一个新的 X,这个新的 X 和历史上所有的 X,经过线性投影,更新 Q,K,V。而这里的问题是,历史上的很多 Q K V是已经 计算过的,重新计算没有意义,而且我们也只关心新生成的 token,历史上的token意义是有限的。我们可以针对这一问题进行优化,即 KV Cache。
KV Cache:全称是 key-value cache,可以理解为大模型推理过程中的 key-value 缓存优化。对于新的生成迭代,我们只计算新的 X 对应的 Q K V,把历史上的 K V 缓存起来。然后只把新的 Q 和所有的 K (历史的+新的)进行注意力分数的计算,然后与所有的 V(历史的+新的)进行注意力汇聚来得到新的 Y。
-
对于新的请求Query,需要与历史的Key、Value计算注意力分数;
-
如果每次都重新计算历史的 Key, Value,会浪费大量计算资源;
-
每轮新迭代时将Key, Value 进行缓存,共下次迭代使用。
LMDeploy 的 KV Cache 的实现细节 -
LMDeploy 实现了一个 KV Cache 管理器,方便管理内存和缓存,但这个对用户一般是无感的。采取预先申请策略,减少运行时因申请/释放内存的消耗时间。比如,会观察到模型量化前后的显存占用都是8G,实际上模型对内存的占用通过量化确实减少了,更多的内存分配给了 KV Cache 以实现更长的上下文推理;
-
我们可以通过设置 cache_max_entry_count 参数调节 KV Cache 占用内存的大小,为占用剩余显存的比例。下面的例子为,告诉 LMDeploy 加载完模型权重就,KV Cache 可以利用剩余显存的 20%;
1.3 大模型量化技术
量化技术将传统的表示方法中的浮点数转换为整数或其他离散形式,以減轻深度学习模型的存储和计算负担。
为什么要做量化:
-
减少模型权重的内存占用,有更多的显存分配给 KV Cahce
-
提升推理速度
–速度更快的 kernel
– 降低 I/O 延迟 -
增加上下文长度
-
降低推理成本
举个例子, LlaMa3.1 是一个 405B 的超大模型,传统的 16 bit/位 float 存储方式种每一个参数需要 2 byte 显存,那么 405B 尺寸的模型需要占用 810G 显存。而业界即使使用 8卡满血 A100,也只有 640G 的显存。
所以,如果用传统的 FB16 来加载模型权重,单机八卡的服务器也无法存下权重。但如果能把权重量化为 4 比特的一个整数,即从 16bit 浮点数转化为 4bit 整数,模型权重体积就会降低为原来的 1/4,只需要 202G,这个体积对单机八卡服务器而言是绰绰有余了。节省下来的显存便可以分配给 KV Cache,从而支持更长的上下文推理,甚至是高并发支持以使用一个服务器服务多个客户。
大模型量化技术核心思路:对于原来的这个浮点数区间做一个线性映射,映射为一系列整数。 以INT8为例,INT8是八位二进制数,八位二进制数所能表示的这个整数范围是从0~255,共是256个数。可以把原来的这个浮点数的范围,按照最大最小值平均分成256份,然后按照这个大小到相对顺序进行一个线性的映射。每一个区间的数被线性映射到一个对应的整数上,这就是我们这个量化的一个通用的一个思想。
量化技术可以按照量化方法分类:
按量化对象分
- KV Cache 量化
- 模型权重量化
- 激活值量化:此激活并非神经网络激活函数之激活。举例,线性层数学本质就是 Y = WX + B,W是模型权重,X就是激活值。我们可以对 W 量化,就是模型权重量化;也可以对 X 量化,这就是激活值量化
按量化阶段分:
- 量化感知训练 QAT
- 量化感知微调 QAF
- 训练后量化 PTQ:即模型训练好后通过一些简单的数据集标定或其他方式,不需要重复训练就可以完成量化的过程,这个策略是比较优越的。而 QAT 和 QAF 在训练后还需要一些额外的训练微调步骤来进行量化,在生产实践中是不会做的,而一般都采用 PTQ 的方式。
LMDeploy 主要的量化方案是:
- KV Cache 量化
- 在线KV Cache INT4/INT8量化,粒度为per-head per-token,是一种很细粒度的方法
- 与FP16相比,INT4/INT8的KV Block数量分别可以提升4倍和2倍。意义:更长的上下文、更高的并发吞吐
- 精度上INT8几乎无损,INT4略有损失
- 模型权重量化 W4A16 量化
W4A16 指 对权重 W 进行 4bit 量化,对激活值不做量化。即对权重进行 4比特 量化进行存储以节省显存,但计算时需要将权重反量化为FP16后再与激活值进行计算;
这里引申出一个问题:我们为什么要对模型进行量化?我们想进行量化也可能是为了调用这个硬件底层的一些定点数的算子,比如英伟达显卡都是有INT8的计算单元。这种专门的计算单元比LP16或者FP32的计算单元的算力是要更高的。所以很多人机械式的认为,我们是为了对模型进行量化,把这个浮点数转成了一个定点数从而调用这个算力计算单元,实现对模型的加速,但实际上这种说法其实是比较片面的。
实际上,LMDeploy 的 4bit 量化仅仅是为了节省存储空间,但是在计算时非但没使用整数计算单元,还将整数反量化为一个浮点数,最终在浮点数计算单元上计算,最终也起到一个比较好的加速效果。 因为 Transformer 的 decode 阶段,即 generation 阶段,是一个超高仿存的操作,即大模型在实际推理的时候计算瓶颈本身并不是在计算上,而是在仿存上,即可能体现为显卡数据通信的带宽上的瓶颈。通过 4bit 量化,推理过程中通信所需要的数据量缩小到了原来的 1/4,减少了 IO操作,从而起到加速的效果。
- AWQ算法,W4A16 指 对权重 W 进行 4bit 量化,对激活值不做量化。即对权重进行 4 比特量化进行存储以节省显存,但计算时需要将权重反量化为FP16后再与激活值进行计算;
- 性能是FP16的 2.4 倍以上
- 权重大小、显存降为FP16的的 1 / 4
AWQ 量化原理:
核心观点 1:权重井不等同重要,仅有 0.1 ~ 1%小部分显著权重对推理结果影的较大;
-
尝试将这 0.1 ~ 1 % 的小部分显著权重保持FP16,对剩余权重进行低比特量化,可以大幅降低内存占用
-
问题来了:如果选出显著权重?
-
- 随机挑选 - 听天由命
-
- 其于权重分布挑选 - 好像应该这样
-
- 基于激活值挑选 - 竟然是这样
在实践中,可以基于激活值,按通道“组团”挑选显著权重:
- 基于激活值挑选 - 竟然是这样
-
为了避免实现上过于复杂,在挑选显著权重时,并非在 “元素” 级别进行挑选,而是在 “通道” 级别进行挑选
-
首先将激活值对每一列 (channel) 求绝对值的平均值,把平均值较大一列对应的通道对应的权重值视作显著权重;
然而,理想很丰满,现实很骨感, -
我们理想希望能做到:显著权重 INT4,非显著权重 FP16;
-
但什么样的权重是“显著”的?
-
如何实现一个通道上的混合精度 kernel?
-
硬件不友好!
-
开发者不友好!
核心观点 2: 量化时对显著权重进行方法可以降低量化误差;
考虑权重矩阵W,线性运算写作 Y = WX。对权重矩阵量化后,可以写作 Y=Q(W)X,Q(W) 定义如下:
Q ( w ) = Δ ⋅ Round ( w Δ ) , Δ = max ( ∣ w ∣ ) 2 N − 1 Q(\mathbf{w})=\Delta \cdot \operatorname{Round}\left(\frac{\mathbf{w}}{\Delta}\right), \quad \Delta=\frac{\max (|\mathbf{w}|)}{2^{N-1}} Q(w)=Δ⋅Round(Δw),Δ=2N−1max(∣w∣)
-
右侧式子为计算获得量化单位 Delta Δ \Delta Δ,分母中的 N 为量化的比特数,比如:4比特量化中,4比特能表示的最大值是 2 4 − 1 2^4-1 24−1;
-
左侧式子,首先 Round() 中的 W 本来一般是一个浮点数张量,比如 LP16,在量化过程中 W 除以量化单位 Δ \Delta Δ,然后通过 Round() 取整,便得到了这个量化后的结果。如果 右侧式子 N 取 4,就将原来的 LP16 浮点数矩阵量化成了 INT4 整数矩阵。可以只用花费原来 1/4 的 IO 开销。而我们也提到,INT4 仅仅是为了存储,实际的计算依然使用 LP16,还有一个反量化的过程,即量化结果乘以量化单元,变得到了 LP16。
由于量化过程中存在一个取整 Round(),它一定会带来一部分的精度损失。这也引出上面的核心观点 2,即对显著权重放大可以一定程度上降低量化带来的误差损失。
核心观点 2 的证明如下:
将视野从考虑权重矩阵转变为考虑权重单个元素。缩放因子用来放大显著权重,乘上这个缩放因子再除以新的量化单位。而新的量化单位也是最大的 ws 去除以
2
N
−
1
2^N-1
2N−1,即量化过程,后面的反量化过程再乘以 1/s。这样一来,量过过程中基于 w 乘上 s 再除以量化单位,反量化过程中又乘以 1/s。综合来看,这个效果和(1)式是等价的。
但是,这样等价的效果,在(2)中其实有效降低了量化带来的误差。(2)式中唯一的误差来源是取整函数 Round(),且其中的显著权重 w 只是(1)中的 W 矩阵众多元素中非常少量的元素,哪怕它乘上一个缩放银子,他也不一定有权重矩阵 W 中的最大的元素大。这是一个概率问题,毕竟 w 只占 W 中的 0.1% ~ 1%,所以很大概率上,即使对这个显著权重乘以了 s,它也不会影响总体上的量化单位。
而
Δ
\Delta
Δ 和
Δ
’
\Delta’
Δ’ 是大致相等的。那么,(1)(2) 的误差几乎只由 s 决定:
在
Δ
\Delta
Δ 和
Δ
’
\Delta’
Δ’ 是大致相等时,相对误差项基本可以表示为
1
/
s
1/s
1/s:
- s >1, s 越大,相对误差越低;
- s 也不能太大,否则 ws 会超过 W矩阵中的最大元素,影响成立条件;
我们可以通过实验来验证上面的推导是否正确:
- 随着 s 增大,假设成立的概率条件越来越低,但是 s < 2 之前,概率还是很低的(<5%);
- 在一定范围内,随着 s 增大,误差比值越来越小,完全支持作者观点;
这也表明了一种量化策略,
- 所有权重均低比特量化;
- 显著权重乘以较大 s,等效于降低量化误差;
- 非显著权重乘以较小的 s,等效于给予更少的关注;
而实际操作中,LMDeploy 将分组计算每个通道的缩放系数:
以上便是 AWQ 算法整体的思路;
- 训练后量化 PTQ
1.4 大模型外推技术
什么是外推?长度外推性是一个训练和预测的长度不一致的问题;
比如,我们训练可能只有 4096 等训练数据集的长度,而在预测时可能会外推到更长的文本,比如 16K,32K,128K,1M 等。这就会导致模型在训练和预测阶段所接触的序列长度是不一致的,这样的外推会引发潜在的两大问题:
- 预测阶段用到了训练过的位置编码:模型不可避免地在一定程度上对位置编码 “过拟合”
- 预则注意力时注意力机制所处理的 token 数量远超训练时的数量:导致计算注意力 “熵” 的差异较大
在这里,我们主要介绍从位置编码角度解决外推的问题。
首先,大模型依赖的 Transformer 为什么需要位置编码?
在transformer之前,人们处理序列问题的时候都是用循环神经网络。循环神经网络最大的一个问题是就在输入序列时候需要逐个输入不能并行输入。自注意力机制虽然解决了并行输入的问题,也导致了一个新的问题,就是理论上输入的多个嵌入向量对于注意力机制是等价的。即注意力机制并不具备区分token相对位置的能力,需要给每一个embedding向量再增加一个位置编码,从而使注意力机制能够识别这个embedding向量的相对位置关系。
在使用位置编码时,很常见的思想是将位置编码添加到 token embedding 上,成为一个新的 embedding。那么,如何设计位置编码?下面有一些基本的思路:
- 直接使用一个整数作为位置信息提供给模型,但数值跨度较大,这对深度学习的梯度优化器是不友好的,会导致学习困难;
- 对上面提到的整数编码所放到 0 ~ 1 取件,以此类推,但这种数值跨度又太小了,也是不适合做位置编码的;
- 深度学习模型还是比较挑剔的,数值跨度太大或太小都不合适;
- 一个比较合适的想法是,考虑使用一组向量来表示位置:以 “10 进制” 为例,位置 1234 可以表示为4维向量 [1 2 3 4]。更一般的,位置 N 中的向量可以用以下方式表示:
- 当然,我们也可不用 “10 进制”,而是尝试使用
β
\beta
β 进制(这也是 beta 进制编码基本的理念,而关键点是 mod 带来的关键的周期性):
- 还有一种很经典的编码方式,即 Attention is All you need 中提出的 Sinusoidal 位置编码。而这种 sin/cos 和 beta 编码中的取余操作 mod 具有一定的等效性,因为都具有周期性的特性。所以 Sinusoidal 位置编码也可以认为是一种特殊的 beta 进制编码。
而在外推情况下, - 以“10 进制”为例,假设训练只在最大长度为 1K 的训练集上训练(位置编码 0~999,最长 3 位)
- 现在需要外推到 2K,需要 4 位位置编码,如何处理?
我们的方案是,在训练阶段就预留好足够的位数,而 Transformer 原作者也是这么想的,认为预留好位数后模型就能具备对位置编码的泛化性 - 而现实情况中,模型很难按照期望进行泛化,实验发现模型外推到 20% ~ 30% 就已经很不错的,超过后模型的效果就会断崖下跌,表现为模型的输出会呈现出一种乱码状态;
- 还有一种原因就是在训练的时候虽然保留了高位,但是大多数高位始终是“0”。这就会导致这个高位的位置编码没有被充分的训练,使得模型无法处理这些新的位置编码。
因此,业内提出了一种新的方法,即 线性内插法: - 方案: 把 “新长度范围”,等比例缩放至训练阶段的长度范围;
- 如训练时使用 1K 训练,需外推至 4K,就将 [0, 4K] 的范围线性缩放至 [0, 1K];
- 但是呢,这种方法也会导致最低位变得非常“拥挤”,通常需要微调,使模型适应拥挤的映射关系。
- 并且,各维度差异较大!其他位置差异为 1,个位差异较小,模型不易分辨,效果不佳;
于是,人们又提出一种全新的方法,即 进制转换: - LLM 其实并不知道我们输入的位置编码具体是多少 “进制” 的,他只对相对大小关系敏感;
- 能否通过 “进制轻换” 来等效 “内插” ?
- 如:10进制下,3位表示范围是0~999;16进制下,3位表示范围是0 ~ FFF (16 进制下 FFF 为 10进制下的 4095) ! 下面的例子里,外推时 4 位的十进制 [2 3 5 0] 转换为 十六进制 的 [9 2 14];
- 虽然每一位都可能出现大于“9”的数,但相对大小差异总是为 1,模型具有泛化能力;
- 进制转换把内插的压力分摊到了每一位上;
实际上的外推中,我们采取 NTK-aware 外推技术:
sin ( n ( β λ ) i ) = sin ( n ( θ 2 / d k 2 / ( d − 2 ) ) i ) = sin ( n ( θ k d / ( d − 2 ) ) 2 i ) \sin \left(\frac{n}{(\beta \lambda)^i}\right)=\sin \left(\frac{n}{\left(\theta^{2 / d} k^{2 /(d-2)}\right)^i}\right)=\sin \left(\frac{n}{\left(\theta k^{d /(d-2)}\right)^{2 i}}\right) sin((βλ)in)=sin((θ2/dk2/(d−2))in)=sin((θkd/(d−2))2in)
- 预测阶段,计算系数,对位置编码的底数 base 进行缩放:
n ( β λ ) d / 2 − 1 = n / k β d / 2 − 1 \frac{n}{(\beta \lambda)^{d / 2-1}}=\frac{n / k}{\beta^{d / 2-1}} (βλ)d/2−1n=βd/2−1n/k - n 是实际预测长度,k 是实际长度预训练长度的比值。求得:
λ = k 2 / ( d − 2 ) \lambda=k^{2 /(d-2)} λ=k2/(d−2)
1.5 Function Calling
Function Calling 即为让 LLM 调用外部函数解决问题,从而拓展 LLM 的能力边界;
其意义在于:
- 解决时效性问题:今天那年那日?今天天气如何?
- 拓展 LLM 能力边界:帮我算一下 e^8/12345 等于多少?帮我搜一下这篇论文?
2 LMDeploy 量化部署实践
2.1 开发机选择和环境配置
对于 internlm2_5-1_8b-chat
模型,查看其 HuggingFace 码仓 中的 config 文件,发现模型权重存储为 bfloat16 格式,
"rope_theta": 1000000,
"tie_word_embeddings": false,
"torch_dtype": "bfloat16",
"transformers_version": "4.41.0",
"use_cache": true,
"vocab_size": 92544
对这个 1.8B (18亿) 模型,每个参数 parameter 都是用 16 位浮点数(16 bits,2 Bytes),模型权重为:
18
×
1
0
9
parameters
×
2
Bytes/Parameter
=
3.6
GB
18 \times 10^9 \text{ parameters}\times 2 \text{ Bytes/Parameter} = 3.6 \text{ GB}
18×109 parameters×2 Bytes/Parameter=3.6 GB,1 GB 有 1 B 个 Byte;
需要的 GPU 显存至少要大于 3.6 GB,需要至少选择 30% A100 开发机(24GB)。
创建好开发机后,配置环境:
conda create -n lmdeploy python=3.10 -y
conda activate lmdeploy
conda install pytorch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 pytorch-cuda=12.1 -c pytorch -c nvidia -y
pip install timm==1.0.8 openai==1.40.3 lmdeploy[all]==0.5.3
2.2 InternStudio 环境获取模型
运行以下指令,创建文件夹并设置软链接以方便访问 InternStudio 预先准备好的模型:
mkdir /root/models
ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2_5-7b-chat /root/models
ln -s /root/share/new_models/Shanghai_AI_Laboratory/internlm2_5-1_8b-chat /root/models
ln -s /root/share/new_models/OpenGVLab/InternVL2-26B /root/models
官方教程使用 internlm2_5-7b-chat 和InternVL2-26B 作为演示,但这两个模型量化会消耗大量时间 (约8h)。在本文实践中,使用internlm2_5-1_8b-chat模型完成。
2.3 LMDeploy 验证启动模型文件与本地部署 InternLM2.5 大模型
量化工作开始前,验证获取的模型文件是否能正常工作:
conda activate lmdeploy
lmdeploy chat /root/models/internlm2_5-1_8b-chat
然后,我们可以在CLI (“命令行界面” Command Line Interface的缩写) 中和InternLM2.5 进行交互了,注意输入内容完成后需要按两次回车才能够执行,
此时,我们可以看到,LMDeploy 部署 internlm2_5-1_8b-chat
后显存占用大约为 20GB
而在之前的分析中,internlm2_5-1_8b-chat
的权重仅需要 3.6GB,这是为什么呢?
由于 LMDeploy 默认设置参数 cache-max-entry-count
为0.8,即kv cache占用剩余显存的80%:
此时,30% A100 具有 24GB 显存,权重占用 3.6GB,剩余显存 24 - 3.6 = 20.4 GB,因此 KV Cache 将占用 20.4 * 0.8 = 16.32 GB,总共占用 3.6 + 16.32 = 19.92 GB,接近 20 GB;
这也表明,如果我们是用 50%A100 40GB 显存,那么 KV Cahce 将占用 (40-3.6) * 0.8 = 29.12 GB,总共占用 3.6 + 29.12 = 32.72 GB;
同时,实际加载模型后,除了模型权重和 KV Cache占用显存,还会有其他部分占用显存,所以剩余显存比理论值会偏低,最终的实际占用也会偏高于 19.92 GB 或 32.72 GB。
此外,也可以新开一个终端输入如下两条指令的任意一条,查看显存占用情况,实现显存资源的监控:
nvidia-smi
studio-smi
注释:实验室提供的环境为虚拟化的显存,nvidia-smi是NVIDIA GPU驱动程序的一部分,用于显示NVIDIA GPU的当前状态,故当前环境只能看80GB单卡 A100 显存使用情况,无法观测虚拟化后30%或50%A100等的显存情况。针对于此,实验室提供了studio-smi 命令工具,能够观测到虚拟化后的显存使用情况。
2.4 LMDeploy API部署InternLM2.5 大模型
上一节我们将大模型加载权重部署在本地。在实际应用中,我们常将大模型部署封装为 API接口服务,供客户端访问。
2.4.1 启动 API 服务器
我们依然使用 lmdeploy,将 InternLM2.5-1.8b 部署为 API 服务:
conda activate lmdeploy
lmdeploy serve api_server \
/root/models/internlm2_5-1_8b-chat \
--model-format hf \
--quant-policy 0 \
--server-name 0.0.0.0 \
--server-port 23333 \
--tp 1
命令解释:
lmdeploy serve api_server
:这个命令用于启动API服务器。/root/models/internlm2_5-1_8b-chat
:这是模型的路径。--model-format hf
:这个参数指定了模型的格式。hf代表“Hugging Face”格式。--quant-policy 0
:这个参数指定了量化策略。--server-name 0.0.0.0
:这个参数指定了服务器的名称。在这里,0.0.0.0是一个特殊的IP地址,它表示所有网络接口。--server-port 23333
:这个参数指定了服务器的端口号。在这里,23333是服务器将监听的端口号。--tp 1
:这个参数表示并行数量(GPU数量)。
运行指令后,我们已将 internlm2_5-1_8b-chat
模型部署为 API 服务,完成端口映射后,边可以在本地机器访问运行在开发机上的大模型服务。
2.4.2 链接 API 服务器
2.4.2.1 命令行形式链接API服务器
将模型部署为 API 服务器后,可以使用命令行形式链接,
conda activate lmdeploy
lmdeploy serve api_client http://localhost:23333
完成链接后,便可以在命令行中进行与模型的交互:
2.4.2.2 Gradio 网页形式链接 API 服务器
可以输入以下指令,运用 Gradio 作为前段,启动网页:
lmdeploy serve gradio http://localhost:23333 \
--server-name 0.0.0.0 \
--server-port 6006
完成端口映射后,可以在本地浏览器中访问 Gradio 前端的模型服务:
2.5 LMDeploy Lite
在本例中,使用 internlm2_5-1_8b-chat
不是一个很大的模型,但实际中会涉及很大的模型,比如 70B,甚至 405B,我们不可避免的需要用大模型压缩技术来降低模型部署成本,这不仅是为了能够成功的部署,也是为了提升模型的推理性能。LMDeploy 提供了权重量化和 KV Cache 两种策略。
2.5.1 KV Cache 缓存大小配置
KV Cache 是一种缓存技术,通过存储 键值对(Key-Value Pair) 的形式来复用计算结果。在大规模训练和推理中,可以显著减少重复计算量,从而提升模型的推理速度,最终提高性能和降低内存消耗。理想情况下,KV Cache 全部存储于显存,以加快访存速度。
模型在运行时,占用的显存可大致分为三部分:
- 模型参数权重本身占用显存
- KV Cache 占用显存
- 中间运算结果占用的显存
LMDeploy 的 KV Cache 管理器可以通过设置 --cache-max-entry-count
参数,控制 KV Cache 占用剩余显存的最大比例。默认的比例为0.8。
我们在轻量化部署前,模型的显存占用大约为 20GB,这也符合我们之前的分析
此时,30% A100 具有 24GB 显存,权重占用 3.6GB,剩余显存 24 - 3.6 = 20.4 GB,因此 KV Cache 将占用 20.4 * 0.8 = 16.32 GB,总共占用 3.6 + 16.32 = 19.92 GB,接近 20 GB;
然后,我们改变 KV Cache 的显存占用配置,重新部署一个模型
lmdeploy chat /root/models/internlm2_5-1_8b-chat --cache-max-entry-count 0.4
发现此时显存变为 12.5GB,这种变化如何而来?
在 cache-max-entry-count
设置为 0.4 时,
1、在 BF16 精度下,1.8B 模型权重占用14GB:18×10^9 parameters×2 Bytes/parameter=3.6 GB
2、KV cache 占用 8.16 GB:剩余显存 24-3.6=20.4 GB,KV cache默认占用80%,即20.4*0.4=8.16 GB
此时,模型权重和 KV Cache 共占用约 11.76 GB,再加上模型部署涉及的其他项,反推约 0.8GB,共占用 12.5 GB。
2.5.2 在线 KV Cache INT4/INT8 量化
自 v0.4.0 起,LMDeploy 支持在线 KV Cache INT4/INT8 量化,量化方式为 per-head per-token 的非对称量化。
- 此外,通过 LMDeploy 应用 KV Cache 量化非常简单,只需要设定
quant_policy
和cache-max-entry-count
参数。- 目前,LMDeploy 规定
quant_policy=4
表示 KV INT4 量化,quant_policy=8
表示 KV INT8 量化。
在前面的内容中,我们使用 lmdeploy,将 InternLM2.5-1.8b 部署为 API 服务时运用以下指令:
conda activate lmdeploy
lmdeploy serve api_server \
/root/models/internlm2_5-1_8b-chat \
--model-format hf \
--quant-policy 0 \
--server-name 0.0.0.0 \
--server-port 23333 \
--tp 1
此时,
--quant-policy = 0
没有进行量化;cache-max-entry-count
为默认 0.8;
下面,我们设置 KV INT4 量化,设置 KV Cache 对剩余显存占用比例为 0.4,将模型部署为 API:
lmdeploy serve api_server \
/root/models/internlm2_5-1_8b-chat \
--model-format hf \
--quant-policy 4 \
--cache-max-entry-count 0.4\
--server-name 0.0.0.0 \
--server-port 23333 \
--tp 1
可以看到,此时的显存占用也是 12.5GB:
那这里的 12.5GB 和之前不设置 quant-policy = 4
时占用的 12.5GB 有什么区别呢?
- 首先,默认的BF16精度下 internLM2.5 1.8B 模型,占用显存 3.6GB,因此 30% A100 下将剩余 20.4 GB;
- 由于
cache-max-entry-count
为 0.4,那么 KV Cache 将只占用 20.4*0.4 = 8.16 GB 的显存; - 由于
quant-policy = 4
,将采取 INT4 量化。所以,LMDeploy 也会按照 INT4 精度提前申请 8.16 GB 的 KV Cache;而像之前那种方法,如不采用 INT4 精度量化,则会按照 BF16 精度申请显存; - 但是,相比之前的 BF16 精度的显存下装载的 KV Cache,INT4 精度只需 4 bit 来存储一个参数,而之前的 BF16 需要 16 bit。那么,在相同大小的显存下,INT4精度的 KV Cache 可以存储的元素数量也会是 BF16 的 4 倍!
2.5.3 W4A16 模型权重量化和部署
上一节,我们对 KV Cache 进行量化,本节我们将尝试对模型的权重进行量化。
准确说,模型量化是一种优化技术,旨在减少机器学习模型的大小并提高其推理速度。量化通过将模型的权重和激活从**高精度(如16位浮点数)转换为低精度(如8位整数、4位整数、甚至二值网络)**来实现。
W4A16解释:
- W4:一般指权重量化为 4位整数(INT4)。这说明模型中的权重参数将从它们原始的浮点表示(例如 FP32、BF16 或 FP16,InternLM2.5 默认精度为 BF16,即每个参数都是 16bit 的浮点数)转换为 4bit 的整数表示(INT4)。从高精度转换为低精度,显著减少模型的大小。
- A16:一般表示激活**(或输入/输出)仍然保持在16bit 浮点数(例如FP16或BF16)**。激活是在神经网络中传播的数据,通常在每层运算之后产生,即 Y=WX+A 中的 X。
因此,W4A16的量化配置指:
- 权重W 被量化为 4位整数(INT4)。
- 激活X 保持为16位浮点数(BF16 或 FP16)。
在最新的版本中,LMDeploy 使用的是AWQ(自动权重量化)算法,能够实现模型的4bit 权重量化。输入以下指令,执行量化推理工作,总体耗时接近 1 个小时:
lmdeploy lite auto_awq \
/root/models/internlm2_5-1_8b-chat \
--calib-dataset 'ptb' \
--calib-samples 128 \
--calib-seqlen 2048 \
--w-bits 4 \
--w-group-size 128 \
--batch-size 1 \
--search-scale False \
--work-dir /root/models/internlm2_5-1_8b-chat-w4a16-4bit
命令解释:
lmdeploy lite auto_awq
: lite这是 LMDeploy的命令,用于启动量化过程,而auto_awq代表自动权重量化(auto-weight-quantization)。/root/models/internlm2_5-1_8b-chat
: 模型文件的路径。--calib-dataset 'ptb'
: 这个参数指定了一个校准数据集,这里使用的是’ptb’(Penn Treebank,一个常用的语言模型数据集)。--calib-samples 128
: 这指定了用于校准的样本数量—128个样本--calib-seqlen 2048
: 这指定了校准过程中使用的序列长度—2048--w-bits 4
: 这表示权重(weights)的位数将被量化为4位。--work-dir /root/models/internlm2_5-1_8b-chat-w4a16-4bit
: 这是工作目录的路径,用于存储量化后的模型和中间结果。
推理完成后,便可以在设置的目标文件夹--work-dir /root/models/internlm2_5-1_8b-chat-w4a16-4bit
看到对应的模型文件。而推理后的模型和原本的模型最明显区别在于:模型文件大小和占据显存大小;
可以输入如下指令查看在当前目录中显示所有子目录的大小:
cd /root/models/
du -sh *
(lmdeploy) root@intern-studio-50055:~/models# du -sh *
0 InternVL2-26B
0 internlm2_5-1_8b-chat
1.5G internlm2_5-1_8b-chat-w4a16-4bit # 量化后占用 1.5GB
0 internlm2_5-7b-chat
可见,W4A16 设置下的 AWQ 算法对原本 3.6 GB 的 1.8B 模型量化后,得到 1.5G 的 internlm2_5-1_8b-chat-w4a16-4bit
按照 INT4 存储参数的模型;
我们也可以用以下指令查看原模型的大小:
cd /root/share/new_models/Shanghai_AI_Laboratory/
du -sh *
(lmdeploy) root@intern-studio-50055:~/share/new_models/Shanghai_AI_Laboratory# du -sh *
17G internlm-xcomposer2-4khd-7b
17G internlm-xcomposer2-7b
6.6G internlm-xcomposer2-7b-4bit
4.6G internlm-xcomposer2-vl-1_8b
17G internlm-xcomposer2-vl-7b
74G internlm2-20b-chat-xxx
3.6G internlm2-chat-1_8b # 量化前占用 3.6GB
7.1G internlm2-chat-1_8b-sft
37G internlm2-chat-20b
37G internlm2-chat-20b-sft
15G internlm2-chat-7b
15G internlm2-chat-7b-sft
15G internlm2-math-7b
15G internlm2-math-base-7b
37G internlm2-math-plus-20b
38G internlm2-wqx-20b
41G internlm2-wqx-vl-20b
3.6G internlm2_5-1_8b
3.6G internlm2_5-1_8b-chat
37G internlm2_5-20b-chat
15G internlm2_5-7b-chat
15G internlm2_5-7b-chat-1m
然后,我们可以用以下指令运行部署量化后的模型
lmdeploy chat /root/models/internlm2_5-1_8b-chat-w4a16-4bit/ --model-format awq
而此时内存占用情况,约 20.2GB
相比权重量化前的模型,少了 0.4 GB,在 W4A16 配置的 AWQ 量化下:
- INT4 精度量化下,1.8 B 占用内存 3.6/4 = 0.9 GB;
注释:bfloat16是16bit 的浮点数格式,占用 2 byte(16位)的存储空间。INT4 是 4bit 的整数格式,占用 0.5 Byte(4bit)的存储空间。因此,从 bfloat16到 INT4 的转换理论上可以将模型权重的大小减少到原来的1/4,即1.8B 个 INT4 参数仅占用 0.9GB 的显存。
- KV Cache 默认占用剩余显存 80%:(24 -0.9)*0.8 = 18.48 GB
- 其他项占用约 0.8GB
最终占用 0.9 + 18.48 + 0.8 = 20.18 GB;
2.5.4 W4A16 量化 + KV Cache + KV Cache 量化 实践
我们可以同时使用多种量化手段来优化大模型的部署:
- W4A16 量化:我们之前通过量化得到了量化后的模型权重
internlm2_5-1_8b-chat-w4a16-4bit
和model-format awq
- KV Cache:设置剩余显存占用为 40%,
--cache-max-entry-count 0.4
- KV Cache 量化:采用 KV INT4 量化,
--quant-policy 4
我们用以下指令同时使用多种量化方法并把模型部署为 API:
conda activate lmdeploy
lmdeploy serve api_server \
/root/models/internlm2_5-1_8b-chat-w4a16-4bit \
--model-format awq \
--cache-max-entry-count 0.4 \
--quant-policy 4 \
--server-name 0.0.0.0 \
--server-port 23333 \
--tp 1
经过多种量化方法:W4A16 量化 + KV Cache + KV Cache 量化,此时模型的部署后仅仅占用 11.3GB!
然后,我们构建以下脚本,借助 FastAPI 封装一个 API 让 LMDeploy 自行访问,
touch /root/internlm2_5.py
# 导入openai模块中的OpenAI类,这个类用于与OpenAI API进行交互
from openai import OpenAI
# 创建一个OpenAI的客户端实例,需要传入API密钥和API的基础URL
client = OpenAI(
api_key='YOUR_API_KEY',
# 替换为你的OpenAI API密钥,由于我们使用的本地API,无需密钥,任意填写即可
base_url="http://0.0.0.0:23333/v1"
# 指定API的基础URL,这里使用了本地地址和端口
)
# 调用client.models.list()方法获取所有可用的模型,并选择第一个模型的ID
# models.list()返回一个模型列表,每个模型都有一个id属性
model_name = client.models.list().data[0].id
# 使用client.chat.completions.create()方法创建一个聊天补全请求
# 这个方法需要传入多个参数来指定请求的细节
response = client.chat.completions.create(
model=model_name,
# 指定要使用的模型ID
messages=[
# 定义消息列表,列表中的每个字典代表一个消息
{"role": "system", "content": "你是一个友好的小助手,负责解决问题."},
# 系统消息,定义助手的行为
{"role": "user", "content": "帮我讲述一个关于孙悟空和蜘蛛精的小故事"},
# 用户消息,询问时间管理的建议
],
temperature=0.8,
# 控制生成文本的随机性,值越高生成的文本越随机
top_p=0.8
# 控制生成文本的多样性,值越高生成的文本越多样
)
# 打印出API的响应结果
print(response.choices[0].message.content)
然后,我们运行以下指令:
conda activate lmdeploy
python /root/internlm2_5.py
于是,我们便可以运用脚本,进行和模型的交互,并完成了一次和大模型的对话:
messages=[
# 定义消息列表,列表中的每个字典代表一个消息
{"role": "system", "content": "你是一个友好的小助手,负责解决问题."},
# 系统消息,定义助手的行为
{"role": "user", "content": "帮我讲述一个关于孙悟空和蜘蛛精的小故事"},
# 用户消息,询问时间管理的建议
],
而此时,在多种量化优化技术的加成在,显存占用仅仅需要 11.3GB !