写点什么

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

作者:AI课程
  • 2024-05-28
    浙江
  • 本文字数:15352 字

    阅读完需:约 50 分钟

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。对此,在一次量化后针对此类常数进行二次量化,进一步压缩显存空间。



  • doubleDequant 的定义:



  • QLoRA 双重反量化:c_1 是块级别常数的量化常数,c_2 是原{W}^{\text{BF16}}的量化常数 L_1**、L_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 的情况下进行泰勒级数展开:



  • loss 的泰勒级数展开


其中 g_i 是\delta u 的梯度。


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



  • Hessian 矩阵部分


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


再说 OBS。


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


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



上面的式子表示,由于是剪枝,可以直接假设存在δ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}


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



对\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_q^2}{[\mathbf{H}^{-1}]{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}{\ell}-\widehat{\mathbf{W}}{\ell} \mathbf{X}{\ell}\right|2^2 \quad \text { s.t. } \quad \mathcal{C}\left(\widehat{\mathbf{W}}{\ell}\right)>C$$


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


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


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


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


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



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


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



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


$$w_p=\operatorname{argmin}{w_p} \frac{\left(\text { quant }\left(w_p\right)-w_p\right)^2}{\left[\mathbf{H}^{-1}\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]{Q Q}\right)^{-1}\left(\mathbf{H}F^{-1}\right){:, Q} \\mathbf{H}{-Q}^{-1} & =\left(\mathbf{H}^{-1}-\mathbf{H}{:, Q}^{-1}\left(\left[\mathbf{H}^{-1}\right]{Q Q}\right)^{-1} \mathbf{H}{Q,:}^{-1}\right){-Q}\end{aligned}$$


Lazy-Batch Updates


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


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



在比较 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:



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



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


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


  1. 量化的精度损失主要来自 Round 部分带来的舍入误差。而无论如何缩放,该部分的误差都在 0~0.5 之间,平均值为 0.25

  2. 对 1%的重要权重进行缩放并不会影响整体的缩放比例


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



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


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


AWQ 提出了两种方案:


  1. 权重本身的 L2 范数

  2. 权重激活值的 L2 范数


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



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


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



目标是找到最优的 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}}^{(\mathrm{t}+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}}^{(\mathrm{t}+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和peftpip install git+https://github.com/huggingface/transformerspip install git+https://github.com/huggingface/peft.git#hqqpip install hqq#eetqgit clone https://github.com/NetEase-FuXi/EETQ.gitcd EETQ/git submodule update --init --recursivepip 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 由于需要量化集的存在,且量化会花费较长时间,因此一般在训练后(或者针对原始模型)再单独量化:


#GPTQOMP_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#AWQswift 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.0nproc_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(越大越好)。



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


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


#例子来自于https://github.com/PanQiWei/AutoGPTQfrom modelscope import AutoTokenizer, snapshot_downloadfrom auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfigimport loggingimport shutilimport 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 memorymodel = 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 modelmodel.save_quantized(quantized_model_dir)
#save quantized model using safetensorsmodel.save_quantized(quantized_model_dir, use_safetensors=True)
#load quantized model to the first GPUmodel = AutoGPTQForCausalLM.from_quantized(quantized_model_dir, device="cuda:0", trust_remote_code=True)#inference with model.generateprint(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, AutoTokenizerimport 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 系数。

发布于: 刚刚阅读数: 5
用户头像

AI课程

关注

本博客将不定期更新关于NLP等领域相关知识 2022-01-06 加入

本博客将不定期更新关于机器学习、强化学习、数据挖掘以及NLP等领域相关知识,以及分享自己学习到的知识技能,感谢大家关注!

评论

发布
暂无评论
LLM 大模型学习必知必会系列(六):量化技术解析、QLoRA技术、量化库介绍使用(AutoGPTQ、AutoAWQ)_AutoAWQ_AI课程_InfoQ写作社区