LLM 大模型学习必知必会系列(六):量化技术解析、QLoRA技术、量化库介绍使用(AutoGPTQ、AutoAWQ)

LLM 大模型学习必知必会系列(六):量化技术解析、QLoRA技术、量化库介绍使用(AutoGPTQ、AutoAWQ)

模型的推理过程是一个复杂函数的计算过程,这个计算一般以矩阵乘法为主,也就是涉及到了并行计算。一般来说,单核CPU可以进行的计算种类更多,速度更快,但一般都是单条计算;而显卡能进行的都是基础的并行计算,做矩阵乘法再好不过。如果把所有的矩阵都加载到显卡上,就会导致显卡显存的占用大量增加,尤其是LLM模型大小从7b、14b、34b到几百b不等,占用显存的大小就是惊人的数字,如何在减少运算量和显存占用的条件下,做到推理效果不下降太多呢?在这里需要引入浮点数和定点数的概念。

1.量化的定义和基本原理

量化是将模型浮点数变为定点数运行的过程。

双精度浮点数:在PyTorch中用torch.float64表示,或者在其他语言中也称为double类型,在LLM训练中一般比较少用

全精度浮点数:在PyTorch中用torch.float32表示

低精度浮点数:在PyTorch中用torch.bfloat16和torch.float16表示。这两个浮点数的差别在上图中可以表示:

  1. bfloat16的小数部分较短,整数部分较长,这会有利于在训练中减少梯度爆炸的情况(即梯度累加值超过了最大值),但是这种数据类型是在N系列显卡Ampere系列才支持的,即30系列显卡。
  2. float16的小数部分较长,这意味着在精度控制上float16更好,但整数部分较短,比较容易梯度爆炸。

那么是否有更加减少显存占用和计算量的数值表达方式呢?那么可以考虑是否把浮点数转换为定点数(整数),整数计算更快更省显存,如果计算精度下降不大就很完美了。这种用整数计算代替浮点数计算的方法就是量化

量化的基本原理是根据每个tensor的浮点型最大值和最小值,将其映射为一个固定范围的整形数值集合,比如[-127~127]。假设一个简单的公式:qweight=round(weight/scale),其中qweight代表量化后权重,weight代表量化前权重,scale代表缩放因子,可以看到在进行缩放后为了将浮点型转换为整数过程中增加了round操作丢失了小数部分。在后续计算或反量化为浮点型时存在无法完全还原的情况,这就是精度损失。

按照量化发生的步骤区分,可以划分为PTQ(训练后量化,或离线量化)和QAT(训练感知型量化,或在线量化)。PTQ量化可以分为data-free和calibration两种,前者不使用数据集进行校准直接计算量化因子,后者会根据少量真实数据进行统计分析并对量化因子进行额外校准,但耗费的时间更长。QAT量化会先在待量化的算子上增加一个伪量化结构,并在训练时模拟量化过程并实时更新计算量化因子(类似反向传播过程)及原始权重。QAT由于较为复杂一般作为辅助措施存在,用于改进PTQ量化的技术手段。

按照量化方法可以划分为线性量化、非线性量化(如对数量化)等多种方式,目前较为常用的是线性量化。其中线性量化又可以按照对称性划分为对称量化和非对称量化,非对称量化为了解决weight分布不均匀问题,其在公式中增加了zero_point项:qweight=round(weight/scale + zero_point),使稠密数据部分可以得到更宽泛的数值范围。

浮点数计算机存储方式示意图

按照量化粒度划分可以分为逐层量化(每层使用一套量化因子)、逐组量化(在每层中按照group使用一套量化因子)、逐通道量化(按channel划分量化因子)等几种方式。

按照量化最大值的阈值区分,可以分为饱和量化和不饱和量化两种。不饱和量化按照浮点数最大值和量化后最大值的比例计算量化因子,由于原始weight的非均匀性会导致某些整形数值范围存在权重空缺。饱和量化会计算一个中间值以计算出量化因子,因此会舍弃一部分不重要数据,将重要数据尽量均匀的分布到量化数值范围内。

按照量化后的比特数划分,可以分为2比特量化,4比特量化,8比特量化等类型。

一般来说,PyTorch中量化模块的forward过程会先对量化权重进行反量化后使用浮点数进行计算。

量化简单来说:将用小数计算结果的模型,转换成用整数计算,中间自然有精度损失(因为小数位没了,而且浮点数翻译成整形再转回来是有损压缩过程)。

有了这个定义,我们就可以继续下面要讲的部分。在继续下面的内容之前,还是建议大家把上面的模型量化基础读一遍。下面会基于之前的文章,侧重几个方面进行技术分析:

  • BnB/HQQ/AWQ/GPTQ等几种量化方法的原理
  • 这几种量化方法一般怎么使用

1.1 原理篇

1.1.1 BnB量化

BnB全称是BitsAndBytes,是几乎最早集成到transformers框架中的量化算法。

论文地址:

LLM.int8():https://arxiv.org/pdf/2208.07339

QLoRA:https://arxiv.org/abs/2305.14314

我们回顾一下量化的基本思路:

  1. 按照整数位数,定义最大值和最小值
  2. 按照浮点数和整数的范围比例,对浮点数进行缩放
  3. 在forward时,将hidden_states按1-2步骤进行缩放,或者将weights反量化为浮点数进行计算

1.1.2 absmax量化

bitsandbytes.LLM.int8()算法也是基于上面的思路的,特殊之处在于,在分析weights矩阵的稀疏性质后,总结了下面的特性:

  1. 模型weights和hidden_states中存在离群值,比例不到1%
  2. 离群值比例虽然低,但是对量化造成了性能恶化

针对离群值的量化算法其实有很多方式,比如分段量化,BnB采用了针对离群值保持低精度浮点数的做法:

  1. 从输入的隐藏状态中,按列提取离群值
  2. 对离群值以低精度浮点型进行矩阵乘法,对非离群值以int8进行矩阵乘法
  3. 对非离群值的结果反量化,将两部分加起来得到最终结果

离群值/非离群值量化

在实际实验中,BnB算法发现以6为尺度分割出离群值是最佳的。

在transformers中使用BnB算法比较简单:

from transformers import BitsAndBytesConfig, AutoModelForCausalLM


bnb_config = BitsAndBytesConfig(
   load_in_8bit=True,
)

model = AutoModelForCausalLM.from_pretrained(some-model-id, quantization_config=bnb_config)

由于BnB量化不需要任何校准数据集,因此其量化速度很快,这也是为什么在QLoRA训练时,会直接传入BitsAndBytesConfig直接对原始模型量化后训练。

而在QLoRA论文中,作者介绍了4bit量化、双重量化和分页optimizer的技术方案。

  • 4bit量化支持两种数据类型:fp4和nf4。fp4是四bit浮点数,包含一位符号位,两位整数位和一位小数位。nf4全称是4-bit NormalFloat,和fp4类似,但是其数值分布并不均匀,呈现正态分布。这是因为一般LLM的矩阵参数概率密度也是呈现正态分布的。在4bit量化中,也更推荐使用nf4数据类型,因为可以比较好的契合参数特性。

  • nf4的量化思路可以理解为:一般模型weights是均值为0,标准差为σ的的分布,因此该分布可以转换为标准高斯分布。这样可以从标准高斯分布中取样一定分位数的量化间隔并设定量化值(该值采用两边分位数的均值),并且正则化到[-1, 1]区间中。后续就可以将模型weights通过absmax量化到该区间中。

  • 双重量化指的是针对量化常数的二次量化。由于BnB的量化是块量化(block-wise),因此块级别的常数存储也会占用GPU memory。对此,在一次量化后针对此类常数进行二次量化,进一步压缩显存空间。

$$
\mathbf{Y}^{\text{BF16}} = \mathbf{X}^{\text{BF16}} \text{doubleDequant}(c_1^{\text{FP32}}, c_2^{k\text{-bit}}, \mathbf{W}^{\text{NF4}}) + \mathbf{X}^{\text{BF16}} \mathbf{L}_1^{\text{BF16}} \mathbf{L}_2^{\text{BF16}}
$$

  • doubleDequant的定义:

$$
\text{doubleDequant}(c_1^{\text{FP32}}, c_2^{k\text{-bit}}, \mathbf{W}^{k\text{-bit}}) = \text{dequant}(\text{dequant}(c_1^{\text{FP32}}, c_2^{k\text{-bit}}), \mathbf{W}^{4\text{bit}}) = \mathbf{W}^{\text{BF16}}
$$

  • QLoRA双重反量化:c_1是块级别常数的量化常数,c_2是原^{\text{BF16}}的量化常数 L_1L_2分别是LoRA的两个矩阵

    • optimizer分页可以同比理解为CPU的分页内存概念,防止在长文本时出现的显存爆炸。

下面我们放一点代码帮助理解。

在transformers.intergrations.bitsandbytes.py中:

这里是替换Linear和Conv算子为bnb的起始点。

bitsandbytes.nn.modules.py:

双重量化。可以看到在weights迁移到cuda的时机进行量化。

继续到C代码bitsandbytes.csrc.kernels.cu:

可以看到针对离群点进行了阈值判断并有选择地量化。如果大于离群阈值则直接置0。

  • 4bit量化:

可以看到量化后针对偶数index的参数左移四位,和相邻参数构成了一个完整的byte。

使用QLoRA进行训练请参考下个章节。

1.1.3 GPTQ量化

说到GPTQ,就要说起它的老祖先OBD、OBS、OBC等剪枝算法(否则无法理解GPTQ)。本质上来说,参数剪枝是参数量化的一种特殊情况(把参数直接置为0这个特殊量化值)。

先说OBD。

论文:https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=17c0a7de3c17d31f79589d245852b57d083d386e

假设模型的loss为E,在参数有微小扰动\delta u的情况下进行泰勒级数展开:

$$
\delta E = \sum_i g_i \delta u_i + \frac{1}{2} \sum_i h_{ii} \delta u_i^2 + \frac{1}{2} \sum_{i \ne j} h_{ij} \delta u_i \delta u_j + O(||\delta \mathbf{u}||^3)
$$

  • loss的泰勒级数展开

其中g_i是\delta u的梯度。

假设模型已经在训练集上训练至收敛,则可以假设E在U点处的一阶导为0。在忽略高阶无穷小分量后,上式仅剩余二阶梯度部分。如果对海森矩阵进行对角近似,则上式在优化后仅剩余下面的部分:

$$
\delta E = \frac{1}{2} \sum_i h_{ii} \delta u_i^2
$$

  • Hessian矩阵部分

由于训练收敛,则海森矩阵是正定的,任何的扰动都会导致loss增加。剪枝的目标就是找到对参数进行重要性评估,将对E影响最小的参数置为0。

再说OBS。

论文:https://proceedings.neurips.cc/paper/1992/file/303ed4c69846ab36c2904d3ba8573050-Paper.pdf

基于OBD的结论,容易看到对角近似的假设是可能存在问题的。OBD主要对这部分进行了改进:

$$
\delta w_q + w_q = 0 \quad \text{or more generally} \quad \mathbf{e}_q^T \cdot \delta \mathbf{w} + w_q = 0
$$

上面的式子表示,由于是剪枝,可以直接假设存在δWq分量,该分量使得原权重Wq变为0。或者,假设存在\delta w矩阵,在e_q这个在q位置为1其他位置为0的单位向量的乘积下和为0。即:

$$
\min_{q} \left{ \min_{\delta \mathbf{w}} \left{ \frac{1}{2} \delta \mathbf{w}^T \cdot \mathbf{H} \cdot \delta \mathbf{w} \right} \text{ such that } \mathbf{e}_q^T \cdot \delta \mathbf{w} + w_q = 0 \right}
$$

在具有约束条件的情况下,引入松弛变量构建拉格朗日函数:

$$
L = \frac{1}{2} \delta \mathbf{w}^T \cdot \mathbf{H} \cdot \delta \mathbf{w} + \lambda \left( \mathbf{e}_q^T \cdot \delta \mathbf{w} + w_q \right)
$$

对\delta w 求导并令其为0,求解后可得:

$$
\delta \mathbf{w} = -\frac{w_q}{[\mathbf{H}^{-1}]_{qq}} \mathbf{H}^{-1} \cdot \mathbf{e}q \quad \text{and} \quad L_q = \frac{1}{2} \frac{w_q2}{[\mathbf{H}]{qq}}
$$

L_q部分就是模型参数对Loss的重要性评估公式。

然后说OBC。

论文:https://openreview.net/pdf?id=ksVGCOlOEba

OBC基于OBS的结论,做了下面的两个假设:

  1. 假设了Loss的形式是:

$$
\operatorname{argmin}{\widehat{\mathbf{w}}{\ell}}\left|\mathbf{W}{\ell} \mathbf{X}-\widehat{\mathbf{W}}{\ell} \mathbf{X}\right|2^2 \quad \text { s.t. } \quad \mathcal{C}\left(\widehat{\mathbf{W}}\right)>C
$$

即为剪枝后的激活值和剪枝前激活值的MSE loss。

  1. 每个权重的行独立影响激活值的行

第一个假设可以把海森矩阵简化为H=2XXT,第二个假设可以单独的处理每行的剪枝,并只使用col*col大小的海森矩阵。由于H和W无关,因此H全局计算一次即可。而H-1在重要度估计和参数更新时比较重要,因此在剪枝某个参数后H的对应行列会被删除,因此仅需要使用Gauss-Jordan消元法重新计算逆矩阵即可。最终推导的H计算公式为:

$$
\mathbf{H}{-p}{-1}=\left(\mathbf{H}-\frac{1}{\left[\mathbf{H}^{-1}\right]{p p}} \mathbf{H}{:, p}^{-1} \mathbf{H}^{-1}\right)_{-p}
$$

在整体矩阵尺度上,OBS在分别评估每一行的待剪枝的参数后,整体更新global mask并计算出剪枝后的W:

其中的重要性估计和参数更新公式原型来自OBS。

由于前提假设是剪枝,如果改为量化只需要修改松弛变量条件:

$$
\mathbf{e}_{\mathbf{q}}^{\mathbf{T}} \cdot \boldsymbol{\Delta} \mathbf{w}+w_q=\operatorname{quant}\left(w_q\right)
$$

可以得到量化版本的OBC(OBQ)的权重更新公式和重要性评估公式为:

$$
w_p=\operatorname{argmin}{w_p} \frac{\left(\text { quant }\left(w_p\right)-w_p\right)2}{\left[\mathbf{H}\right]{p p}}, \quad \boldsymbol{\delta}{\boldsymbol{p}}=-\frac{w_p-\text { quant }\left(w_p\right)}{\left[\mathbf{H}^{-1}\right]{p p}} \cdot \mathbf{H}_{:, p}^{-1}
$$

OBQ会按照参数重要性对参数进行逐个量化。

终于到了GPTQ。

论文:https://arxiv.org/pdf/2210.17323

基于OBS,GPTQ在实现上进行了优化。

  1. OBQ总是使用贪婪算法来优先量化对E影响最小的参数,而GPTQ通过实验发现,针对不同的行的量化顺序并不会影响最终结果,这使得量化可以按列顺序进行,且行量化可以并行进行,提高量化速度
  2. 按照OBQ的公式,每次量化一个参数后需要更新整体参数矩阵,但是对当前列进行量化时,后面的列参数对当前的量化无影响,而是受到当前列量化的影响(当然这是个近似估计)。因此可以按照B=128进行分块,使得块内部参数立即更新,块外部的后续列只是暂时记录更新的值,等128列整体更新完成,在一起更新后续的列参数。这样可以高效利用GPU的并行计算能力,减小吞吐压力。

$$
\begin{aligned}
\boldsymbol{\delta}F & =-\left(\mathbf{w}Q-\text { quant }\left(\mathbf{w}Q\right)\right)\left(\left[\mathbf{H}F^{-1}\right]\right){-1}\left(\mathbf{H}_F\right) \
\mathbf{H}
^{-1} & =\left(\mathbf{H}^{-1}-\mathbf{H}
{-1}\left(\left[\mathbf{H}\right]{Q Q}\right)^{-1} \mathbf{H}^{-1}\right)_{-Q}
\end{aligned}
$$

Lazy-Batch Updates

其中的Q代表batch更新的量化索引。

  1. 用 Cholesky 分解求海森矩阵的逆。尤其由于2的块更新步骤,使得近似计算对量化过程的累积误差变得严重,并出现了H^{-1}变得非正定的问题。在实验中,小模型可以在H的对角线元素中添加阻尼(对角线元素平均值的1%)来解决。但是对于超大模型而言,该方法仍然不够稳定。因此使用了数值稳定的Cholesky 矩阵分解来代替Gauss-Jordan消元法求H{-1},并省去了在循环中更新H的操作。结合阻尼可以稳定地进行超大模型的量化。

在比较BnB和GPTQ后,我们可以看到这样的差异:

  1. BnB量化使用了离群值和非离群值区分处理的策略,因此速度快,不需要数据集。
  2. GPTQ的量化操作基于泰勒级数分解,其评估公式依赖于海森矩阵,并和输入X强相关,因此需要迭代更新,速度慢但更准确。

1.1.4 AWQ量化

论文:https://arxiv.org/pdf/2306.00978

了解了BnB量化后,对AWQ量化的理解会更简单一些。BnB量化的思路是按照阈值切分离群值和非离群值,整体进行混合精度训练。而AWQ的论文认为:

  1. 按照离群值划分不够精确,应当按照“权重的显著性(salient)”来划分
  2. 保持显著性权重为fp16会造成硬件实现的困难和速度的降低,因此应当想办法针对这部分权重进行单独量化

AWQ量化的具体方式仍然是absmax:

$$
Q(\mathbf{w})=\Delta \cdot \operatorname{Round}\left(\frac{\mathbf{w}}{\Delta}\right), \quad \Delta=\frac{\max (|\mathbf{w}|)}{2^{N-1}}
$$

不同的是,它在absmax的基础上增加了新的缩放因子s(s>1):

$$
Q(w \cdot s) \cdot \frac{x}{s}=\Delta^{\prime} \cdot \operatorname{Round}\left(\frac{w s}{\Delta^{\prime}}\right) \cdot x \cdot \frac{1}{s}
$$

缩放因子s可以追溯到一篇叫做SmoothQuant的工作,这里我们不详述。

之所以能够增加因子s,是因为有几个推论:

  1. 量化的精度损失主要来自Round部分带来的舍入误差。而无论如何缩放,该部分的误差都在0~0.5之间,平均值为0.25
  2. 对1%的重要权重进行缩放并不会影响整体的缩放比例

但是对重要权重进行缩放后,输出的激活值的误差会变为:

$$
\begin{aligned}
\operatorname{Err}(Q(w) x) & =\Delta \cdot \operatorname{RoundErr}\left(\frac{w}{\Delta}\right) \cdot x \
\operatorname{Err}\left(Q(w \cdot s)\left(\frac{x}{s}\right)\right) & =\Delta^{\prime} \cdot \operatorname{RoundErr}\left(\frac{w s}{\Delta^{\prime}}\right) \cdot x \cdot \frac{1}{s}
\end{aligned}
$$

上文提到,增加s对缩放比例和RoundErr的影响比较小,因此重要权重的整体Err会被缩小到1/s倍。

下面的问题就是如何确定这些重要参数有哪些呢?

AWQ提出了两种方案:

  1. 权重本身的L2范数
  2. 权重激活值的L2范数

经过试验发现,权重本身的L2范数对评估参数重要性没有显著作用,而权重激活值的L2范数可以提现权重的重要性特征,因此采用权重激活值来评估权重本身的重要性。

那么s的值是不是越大越好呢?由于s增大可能会影响缩放比例,因此会对非重要参数起到不良影响。根据实验,s=2可以达到最好的效果。

为了让效果更好,AWQ针对每个量化通道都进行了单独的s值评估:

$$
\begin{gathered}
\mathbf{s}^*=\underset{\mathbf{s}}{\arg \min } \mathcal{L}(\mathbf{s}) \
\mathcal{L}(\mathbf{s})=\left|Q(\mathbf{W} \cdot \operatorname{diag}(\mathbf{s}))\left(\operatorname{diag}(\mathbf{s})^{-\mathbf{1}} \cdot \mathbf{X}\right)-\mathbf{W} \mathbf{X}\right|
\end{gathered}
$$

目标是找到最优的s是Loss最小。由于无法直接训练(因为量化部分无法求导),因此采用了一个简单的做法:

$$
\mathbf{s}=\mathbf{s}{\mathbf{X}}{ }^\alpha, \quad \alpha^*=\underset{\alpha}{\arg \min } \mathcal{L}\left(\mathbf{s}{\mathbf{X}}{ }^\alpha\right)
$$

即,假设S_x是通道内激活值的平均值,设定0<=α<=1,直接进行网格搜索找到最佳值。

1.1.5 HQQ量化

blog:https://mobiusml.github.io/hqq_blog/

HQQ量化的思路和上面的量化方式不太一样。HQQ量化的问题定义在如何在零点量化中取得最优的z和s(零点和缩放倍数)。

原问题可以定义为:

HQQ求解该问题引入了额外参数W_e:

$$
\underset{\mathrm{z}, \mathrm{W}{\mathrm{e}}}{\operatorname{argmin}} \varphi\left(\mathrm{W}{\mathrm{e}}\right)+\frac{\beta}{2} | \mathrm{W}{\mathrm{e}}-\left(\mathrm{W}-\mathrm{Q}{\mathrm{z}}^{-1}\left(\mathrm{Q}_{\mathrm{z}}(\mathrm{W})\right) |_2^2\right.
$$

可以看到该问题形式类似于类似Lasso回归,可以使用类似软阈值法求解。在定义了新的参数后,可以将该问题分解为两个子问题:

第一步:固定其他参数,找到使loss最低的

第二步:固定W_e,找到最优的z

其中,为了让求解更简单,HQQ将缩放尺度s进行固定,仅优化z(零点)值。

在第一步中,可以使用软阈值法进行求解,在HQQ中作者使用了另一个通用软阈值求解器,以适应范数小于1的情形。

第二步可以进行等式变换变为如下形式:

$$
\begin{gathered}
\mathrm{z}^{(\mathrm{t}+1)} \leftarrow \underset{\mathrm{z}}{\operatorname{argmin}} \frac{1}{2}\left|\mathrm{z}-\left(\mathrm{W}_{\mathrm{q}}{(\mathrm{t}+1)}-\frac{\left(\mathrm{W}-\mathrm{W}_{\mathrm{e}}+1)}\right)}{\mathrm{s}}\right)\right|2^2 \
\mathrm{~W}
{\mathrm{q}}^{(\mathrm{t}+1)}=\operatorname{round}\left(\mathrm{W} / \mathrm{s}+\mathrm{z}^{(\mathrm{t})}\right)
\end{gathered}
$$

其解可以直接设置为每个block内部,右侧子式的均值:

$$
\mathrm{z}^{(\mathrm{t}+1)} \leftarrow\left\langle\mathrm{W}_{\mathrm{q}}{(\mathrm{t}+1)}-\frac{\left(\mathrm{W}-\mathrm{W}_{\mathrm{e}}+1)}\right)}{\mathrm{s}}\right\rangle
$$

可以看到该方法和输入无关,因此不需要量化集。

小结

我们在这里做个总结:

前提:量化是把模型的浮点型参数转换为整形(至少是更低的bit数)的过程,减少显存占用。

  1. BnB量化建议设立阈值,将离群点和非离群点分开处理,其中离群点不进行量化,非离群点进行8bit量化。同时,在4bit量化时,支持了nf4格式,该格式的数值分布并不均匀(为正态分布),使数值分布更符合LLM参数分布。
  2. GPTQ使用了泰勒级数分解,使用海森矩阵评估参数重要性以及更新量化后的参数,并利用现代GPU的特点,进行了并行计算,使显存占用和处理速度大大增加,但是它需要量化集辅助量化。
  3. AWQ认为部分参数更加重要,通过激活值尺度评估了重要参数后,对这些参数按组进行了缩放,达到了减小Loss的效果,由于需要激活值,因此AWQ也需要量化集辅助量化。
  4. HQQ通过对零点量化的公式转换,将其分解为两个子问题分别求解,找到最优的z,该迭代对输入无要求,因此不需要量化集。

2.QLoRA

LoRA部分可以参考另外一篇文章:

简单来说,LoRA是附着在模型上的额外参数矩阵,在训练时冻结原模型,仅训练LoRA部分。如果原模型是量化后的weights(即左边的Pretrained Weights部分),那么和LoRA可以很匹配:原模型占用的显存大大减小了,LoRA部分保持fp16/bf16可以正常forward/backward。

2.1 应用

除上面介绍的量化方法外,还有很多其他类型的量化方法,比如AQLM、EETQ、GGUF等,这么多的量化方式,一个一个了解使用太麻烦了,在不修改训练代码的情况下适配多种量化策略是非常重要的

在这里使用了魔搭社区的SWIFT框架来进行量化训练。该框架在github上是开源的:

https://github.com/modelscope/swift

或者通过pip安装:

pip install ms-swift

#autoawq和cuda版本有对应关系,请按照`https://github.com/casper-hansen/AutoAWQ`选择版本
pip install autoawq -U

#auto_gptq和cuda版本有对应关系,请按照`https://github.com/PanQiWei/AutoGPTQ#quick-installation`选择版本
pip install auto_gptq -U

#hqq和eetq使用暂时需要从源码下载transformers和peft
pip install git+https://github.com/huggingface/transformers
pip install git+https://github.com/huggingface/peft.git
#hqq
pip install hqq
#eetq
git clone https://github.com/NetEase-FuXi/EETQ.git
cd EETQ/
git submodule update --init --recursive
pip install .

回顾下上面提到的量化方式,bnb/hqq/eetq是不需要量化数据集的,因此可以在训练前直接量化模型,速度很快。因此推荐即时量化后直接QLoRA训练模型:

swift sft --model_type llama3-8b-instruct --dataset alpaca-en --quantization_bit 8 --quant_method bnb --sft_type lora

也可以替换为hqq或者eetq:

swift sft --model_type llama3-8b-instruct --dataset alpaca-en --quantization_bit 8 --quant_method eetq --sft_type lora
#--quant_method eetq

其中bnb支持4/8 bit量化,eetq支持8bit量化,hqq支持1/2/3/4/8bit量化。

而GPTQ和AWQ由于需要量化集的存在,且量化会花费较长时间,因此一般在训练后(或者针对原始模型)再单独量化:

#GPTQ
OMP_NUM_THREADS=14 swift export --model_type llama3-8b-instruct --quant_method gptq --dataset alpaca-zh alpaca-en sharegpt-gpt4-mini --quant_seqlen 4096 --quant_bits 4
#AWQ
swift export --model_type llama3-8b-instruct --quant_bits 4 --quant_method awq --quant_n_samples 64 --quant_seqlen 4096 --dataset alpaca-zh alpaca-en sharegpt-gpt4-mini

注意,实际使用GPTQ量化时需要指定OMP_NUM_THREADS=N,否则会出现CPU占满阻塞的问题。

swift export指令会使用指定的数据集对模型进行量化,并在本地保存量化后的模型,默认的保存路径为

'{model_type}-{quant_method}-{quant_bits}',也可以通过--quant_output_dir来指定

QLoRA可以支持FSDP(完全分片数据并行技术),因此可以使用BNB+LoRA在两张24G显卡上运行一个70B模型的训练:

#源代码clone
#cd examples/pytorch/llm
#vim fsdp.sh并写入下面的内容
#pip install bitsandbytes>=0.43.0
nproc_per_node=2

CUDA_VISIBLE_DEVICES=0,1 \
accelerate launch --config_file "./scripts/llama2_70b_chat/qlora_fsdp/fsdp_offload.json" \
    llm_sft.py \
    --model_type llama2-70b-chat \
    --model_revision master \
    --sft_type lora \
    --tuner_backend peft \
    --template_type AUTO \
    --dtype bf16 \
    --output_dir output \
    --dataset leetcode-python-en \
    --train_dataset_sample -1 \
    --num_train_epochs 1 \
    --max_length 2048 \
    --check_dataset_strategy warning \
    --quantization_bit 4 \
    --bnb_4bit_comp_dtype AUTO \
    --bnb_4bit_quant_storage bfloat16 \
    --lora_rank 8 \
    --lora_alpha 32 \
    --lora_dtype AUTO \
    --lora_dropout_p 0.05 \
    --lora_target_modules DEFAULT \
    --gradient_checkpointing true \
    --batch_size 1 \
    --weight_decay 0.1 \
    --learning_rate 1e-4 \
    --gradient_accumulation_steps $(expr 16 / $nproc_per_node) \
    --max_grad_norm 0.5 \
    --warmup_ratio 0.03 \
    --eval_steps 50 \
    --save_steps 50 \
    --save_total_limit 2 \
    --logging_steps 10 \

如果只是想体验量化后的模型推理阶段,可以借助不需要校准数据集的量化方法,使用swift infer来量化模型并推理,大大减少模型推理所需的显存占用

CUDA_VISIBLE_DEVICES=0 swift infer \
    --model_type qwen1half-7b-chat \
    --quant_method bnb \
    --quantization_bit 4

CUDA_VISIBLE_DEVICES=0 swift infer \
    --model_type qwen1half-7b-chat \
    --quant_method hqq \
    --quantization_bit 4

CUDA_VISIBLE_DEVICES=0 swift infer \
    --model_type qwen1half-7b-chat \
    --quant_method eetq \
    --dtype fp16

3.常见量化库

3.1AutoGPTQ

AutoGPTQ是一个易于使用的低延迟语言模型(LLM)量化软件包,具有用户友好的API,基于GPTQ算法。一个基于 GPTQ 算法,简单易用且拥有用户友好型接口的大语言模型量化工具包。

官方链接:https://github.com/AutoGPTQ/AutoGPTQ

  • 推理速度

以下结果通过这个脚本生成,文本输入的 batch size 为 1,解码策略为 beam search 并且强制模型生成 512 个 token,速度的计量单位为 tokens/s(越大越好)。

model GPU num_beams fp16 gptq-int4
llama-7b 1xA100-40G 1 18.87 25.53
llama-7b 1xA100-40G 4 68.79 91.30
moss-moon 16b 1xA100-40G 1 12.48 15.25
moss-moon 16b 1xA100-40G 4 OOM 42.67
moss-moon 16b 2xA100-40G 1 06.83 06.78
moss-moon 16b 2xA100-40G 4 13.10 10.80
gpt-j 6b 1xRTX3060-12G 1 OOM 29.55
gpt-j 6b 1xRTX3060-12G 4 OOM 47.36

量化模型通过能够最大化推理速度的方式加载。

该库需要引入额外的校准数据集进行量化校准。相比bitsandbytes量化精度较高,推理速度较快,但训练后不支持合并adapter

#例子来自于https://github.com/PanQiWei/AutoGPTQ
from modelscope import AutoTokenizer, snapshot_download
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
import logging
import shutil
import os

logging.basicConfig(
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S"
)

pretrained_model_dir = snapshot_download("qwen/Qwen-1_8B-Chat")
quantized_model_dir = "qwen-1_8B-4bit"

shutil.rmtree(quantized_model_dir, ignore_errors=True)
shutil.copytree(pretrained_model_dir, quantized_model_dir)
for _file in os.listdir(quantized_model_dir):
    if ".safetensors" in _file or ".bin" in _file:
        os.remove(os.path.join(quantized_model_dir, _file))

tokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, use_fast=True, trust_remote_code=True)
examples = [
    tokenizer(
        "auto-gptq is an easy-to-use model quantization library with user-friendly apis, based on GPTQ algorithm."
    )
]

quantize_config = BaseQuantizeConfig(
    bits=4,  # quantize model to 4-bit
    group_size=128,  # it is recommended to set the value to 128
    desc_act=False,  # set to False can significantly speed up inference but the perplexity may slightly bad
)

#load un-quantized model, by default, the model will always be loaded into CPU memory
model = AutoGPTQForCausalLM.from_pretrained(pretrained_model_dir, quantize_config, trust_remote_code=True).to(0)

#quantize model, the examples should be list of dict whose keys can only be "input_ids" and "attention_mask"
model.quantize(examples)

#save quantized model
model.save_quantized(quantized_model_dir)

#save quantized model using safetensors
model.save_quantized(quantized_model_dir, use_safetensors=True)

#load quantized model to the first GPU
model = AutoGPTQForCausalLM.from_quantized(quantized_model_dir, device="cuda:0", trust_remote_code=True)
#inference with model.generate
print(tokenizer.decode(model.generate(**tokenizer("auto_gptq is", return_tensors="pt").to(model.device))[0]))

在SWIFT中,可以使用已经量化好的AutoGPTQ模型直接进行训练:

swift sft --model_id_or_path qwen/Qwen-7B-Chat-Int4 --model_revision master --sft_type lora --tuner_backend swift --template_type qwen --dtype fp16 --output_dir output --dataset leetcode-python-en --train_dataset_sample -1 --num_train_epochs 1 --max_length 512 --check_dataset_strategy warning --lora_rank 8 --lora_alpha 32 --lora_dropout_p 0.05 --lora_target_modules ALL --gradient_checkpointing true --batch_size 1 --weight_decay 0.01 --learning_rate 1e-4

上面的命令行中,qwen/Qwen-7B-Chat-Int4是已经量化好的Qwen-7B-Chat模型。

3.2 Bitsandbytes

bitsandbytes是一种data-free的量化库。该量化方法速度较快(因为其不需要数据校准),因此可以在模型加载时动态量化,且该方法训练速度较快,因此训练兼容性较好,一般用于QLoRA训练中,且训练后可以合并adapter。当由于其没有数据校准过程,因此精度较AutoGPTQ较低。

官网链接:https://github.com/TimDettmers/bitsandbytes

  • bitsandbytes的特点
    • 混合精度分解的8位矩阵乘法
    • LLM.int8()推断
    • 8位优化器:Adam、AdamW、RMSProp、LARS、LAMB、Lion(节省75%的内存)
    • 稳定的嵌入层:通过更好的初始化和归一化改进稳定性
    • 8位量化:分位数、线性和动态量化
    • 快速分位数估计:比其他算法快100倍
from modelscope import AutoModelForCausalLM, AutoTokenizer
import torch

model = AutoModelForCausalLM.from_pretrained(
  'qwen/Qwen-1_8B-Chat',
  load_in_8bit=True,
  trust_remote_code=True)

tokenizer = AutoTokenizer.from_pretrained('qwen/Qwen-1_8B-Chat', trust_remote_code=True)

print(model(**tokenizer('how are you?', return_tensors='pt')))

3.3 GGML

官网链接:https://github.com/ggerganov/ggml

GGML和GGUF是GGML C++推理库的两种量化格式,其中GGUF格式较新,可以保留模型版本等其他自定义信息。这两种格式也是PTQ形式的量化算法,但GGML和GGUF格式的量化算法更适配于CPU推理,因此在CPU上运行更快,而GPTQ量化对GPU更加友好,两者的推理精度相仿。因此,*.cpp类型使用了GGML推理库的推理框架都更适配于CPU推理。

GGML是一个专注于机器学习的C库。它由Georgi Gerganov创建,这就是缩写“GG”的含义。这个库不仅提供了机器学习的基础元素,如张量,而且还提供了一种独特的二进制格式来分发LLM(Machine Learning Models)。最近,这个格式改为了GGUF。这种新格式被设计为可扩展的,以便新功能不会影响现有模型的兼容性。

它还将所有的元数据集中到一个文件中,例如特殊标记、RoPE缩放参数等等。简言之,它解决了一些历史上的痛点,并且应该具备未来兼容性。更多信息,请访问此地址查看规范。

接下来的文章中,我们将称之为“GGML模型”的所有模型,无论是使用GGUF还是之前的格式。

GGML是为与 Georgi Gerganov 创作的 llama.cpp 库一起使用而设计的。该库是用 C/C++ 编写的,用于高效地推断 Llama 模型。它可以加载 GGML 模型并在 CPU 上运行。最初,这是与 GPTQ 模型的主要区别,后者是在 GPU 上加载和运行的。然而,现在您可以使用 llama.cpp 将 LLM 的某些层卸载到 GPU 上。举个例子,7b 参数模型有 35 个层。这极大地加快了推断速度,并使您能够运行不适合 VRAM 的 LLM。

如果命令行工具是你的菜,llama.cpp和GGUF支持已经集成到许多图形界面中,例如oobabooga的文本生成Web界面,koboldcpp,LM Studio或ctransformers。

你可以使用这些工具加载你的GGML模型并以类似ChatGPT的方式与它们进行交互。幸运的是,许多量化模型可以直接在Hugging Face Hub上获取。你很快就会注意到大部分模型都是由LLM社区的知名人物TheBloke进行量化的

3.4 AWQ

网址:https://github.com/mit-han-lab/llm-awq

https://arxiv.org/abs/2306.00978

https://github.com/casper-hansen/AutoAWQ

AWQ量化方式假设不是所有权重都影响模型性能,因此在量化过程中会对特殊权重进行特殊处理以减轻量化过程中的精度损失。因此在和GPTQ量化保持类似推理速度的同时可以具备更好的精度。

AWQ是一种对模型权重进行低比特量化的方法,使用该方法可以将模型权重(Weight)量化为4bit,并在计算激活值(Activation)时反量化为FP16,即W4A16。也可以基于AWQ方法将权重量化为3bit/8bit,并在计算时是使用4bit/8bit/16bit,由此衍生出W4A4、W4A8等一系列方法。作者在原文中指出,W4A16可以在精度损失较小的情况下,大幅降低内存占用,且提升模型推理速度,是最常用的方法,因此AWQ和W4A16同镜率较高。

AWQ(Activation-aware Weight Quantization )方法由 MIT、SJTU、Tsinghua University 联合提出的方法,一种对大模型仅权重量化方法。该方法基于”权重并不同等重要“的观察,仅保护1%的显著权重(salient weight)可以大大减少量化误差。AWQ不依赖于任何反向传播或重建,因此可以很好地保持LLM在不同领域和模式上的泛化能力,而不会过拟合到校准集;它也不依赖于任何数据布局重新排序,保持硬件效率。AWQ在多种语言建模、常识问答和领域特定基准测试中优于现有工作。得益于更好的泛化能力,它在指令微调LM和首次实现多模态LM方面取得了出色的量化性能。论文还实现了有效的张量核心内核,以加速AWQ的无重新排序在线反量化,实现速度比GPTQ快1.45倍,比cuBLAS FP16实现快1.85倍。

目前VLLM对AWQ的支持较好, 可以考虑在推理加速时使用AWQ量化方式。

  • AWQ 量化与 GPTQ 量化对比

AWQ 量化精度比 GPTQ 高一点,并且 AWQ 比 GPTQ 更容易实现,计算性能更高。

相比 AWQ 采用 heuristic 的方法来寻找最佳的 scale 和 clip 系数,新的 OminiQuant 则采用训练的方式来获得相应的系数,论文数据比 AWQ 获得更高的量化准确度。

AWQ 的原理非常简单,就是计算一个 scale 系数 tensor,shape 为 [k],k 为矩阵乘的权重 reduce 的维度大小。对激活除以该 tensor,并对矩阵乘的权重乘以该 tensor,这降低了权重量化的难度,使得权重可以采用常规的 group 量化 (直接根据最大最小值计算 scale, zero point)。AWQ 的核心技术一是这个对激活和权重应用 scale 的方法,另外就是如何计算这个 scale tensor。因为激活是 fp16 不量化,对激活进行 scale 一般不会牺牲精度,因此可以对权重进行一些处理降低量化的难度。

虽然 AWQ 与 GPTQ 两者都采用 group 量化,对 shape 为 [k, n] 的矩阵乘权重都生成(k/group) * n 套量化系数。但是 GPTQ 通常采用 act_order=True 选项,这个导致每一个 group 并非使用一组相同的 scale 和 zero point 系数,而是每个 k 位置对应的向量都对应不同的 scale 和 zero point(不同 k 位置共享一组系数,但是这个位置是随机的),每读取一个元素都要读取 scale 和 zero point,导致反量化效率很低。而 act_order=False 时,每一个向量 group size 元素都共享同一组 scale 和 zero point 系数,这样反量化只需要每隔 group size 个元素才需要重新读取一次 scale 和 zero point,反量化效率很高。AWQ 反量化跟 GPTQ act_order=False 是一样的,因此计算效率比较高。

另外 AWQ 虽然要对激活乘以一个 scale tensor,但是这个 tensor 通常可以合并到前面的 RMS NORM 上面,使得这个操作不会引入额外计算。

  • AWQ 量化实践

awq 量化例子 llama_example.sh 给了 4 个步骤

MODEL=llama-7b
 
#run AWQ search (optional; we provided the pre-computed results)
python -m awq.entry --model_path /dataset/llama-hf/$MODEL \
    --w_bit 4 --q_group_size 128 \
    --run_awq --dump_awq awq_cache/$MODEL-w4-g128.pt
 
#evaluate the AWQ quantize model (simulated pseudo quantization)
python -m awq.entry --model_path /dataset/llama-hf/$MODEL \
    --tasks wikitext \
    --w_bit 4 --q_group_size 128 \
    --load_awq awq_cache/$MODEL-w4-g128.pt \
    --q_backend fake
 
#generate real quantized weights (w4)
python -m awq.entry --model_path /dataset/llama-hf/$MODEL \
    --w_bit 4 --q_group_size 128 \
    --load_awq awq_cache/$MODEL-w4-g128.pt \
    --q_backend real --dump_quant quant_cache/$MODEL-w4-g128-awq.pt
 
#load and evaluate the real quantized model (smaller gpu memory usage)
python -m awq.entry --model_path /dataset/llama-hf/$MODEL \
    --tasks wikitext \
    --w_bit 4 --q_group_size 128 \
    --load_quant quant_cache/$MODEL-w4-g128-awq.pt

第一步生成 scale 和 clip 数据并保存文件。

第二步为加载第一步生成的量化系数,并评估量化性能。

第三步加载第一步生成的量化系数,对模型真实权重进行量化和保存量化模型权重。

第四步为评估真实量化模型。

当然这几个步骤是可以通过参数配置合并为一个的。

第一步会下载一个数据集,在 utils/calib_data.py。默认的数据集可能无法下载,可以进行替换,或者手动下载下来用本地路径进行替换。

AWQ 量化通过 auto_scale_block 和 auto_clip_block 方法对每个权重生成一组 scale 和 clip tensor,通过一个 list 存放到量化系数结果里面。

auto_scale_block 的核心为_auto_get_scale,基于当前 transformer layer 的输入,一个 module2inspect 层用于评估 loss,然后通过 grid search 的方式来搜索最佳的 scale 系数。

posted @ 2024-05-28 10:41  汀、人工智能  阅读(486)  评论(1编辑  收藏  举报