本文提出了一种高效微调方法QLoRA,通过量化减少显存使用,实现了在单个48G GPU上对65B模型进行微调,仅仅需要在单个GPU上训练24小时就能达到ChatGPT 99.3%的效果。QLoRA引入多项创新,在不牺牲效果的情况下,显著降低了显存占用量:
- 4-bit NormalFloat(NF4)数据类型
- 双重量化:Double Quantization
- 分页优化器:Paged Optimizer。
微调大型语言模型 (LLM) 是提高其性能的一种非常有效的方法。然而,微调非常大的模型非常昂贵; LLAMA 65B 参数模型的常规 16 位微调需要超过 780 GB 的 GPU 内存。虽然最近的量化方法可以减少LLM的内存占用,但这种技术只适用于推断,在训练过程中还是会出现因为资源问题导致训练失败。
本文提出QLoRA方法只要是使用一种新的高精度技术将预训练模型量化为int4,然后添加一小组可学习的低秩适配器权重。它是通过量化权重反向传播梯度来调整的。
我理解只是梯度计算的时候反量化到BF16,模型本身始终以NF4储存。
基础概念
分块量化(Block-wise Quantization)
量化是将输入从存储更多信息的表征映射为存储较少信息的表征的过程,它通常意味着采用具有更多位的数据类型并将其转换为更少的位,如将FP32的数据转化为INT8,能够节省大量的内存。为了确保使用整个低位数据类型范围,输入数据类型通常通过输入元素的绝对最大值进行归一化来重新调整到目标数据类型,这些元素通常构造为张量。例如,将 32 位浮点 (FP32) 张量量化为范围为 int8 张量。量化和反量化过程如下所示:
其中c是量化常数或者量化尺度。
这种全局量化方式存在一个问题,即当输入中存在极大值或者离群值时,一些较小的参数无法被精确的表示,因此量化后的神经网络效果会下降很多。为了缓解这个问题,作者采用了分块量化,每个块都有自己的量化常数 c,即将输入划分为多个block,每个block分别量化。我们通过将输入张量展平并将线性段分割成 n = (b × h)/B 块,将输入张量 X ∈ 分成 n 个大小为 B 的连续块。我们用公式1独立量化这些块,以创建量化张量和n个量化常数 。全局量化和分块量化示意如下图所示:
从图中可以看到,分块量化能够明显减少量化过程中的误差(0.64 -> 0.241)。
LoRA
低秩适配器 (LoRA) 微调是一种通过使用一小组可训练参数(通常称为适配器)来减少内存需求的方法,同时不更新保持固定的完整模型参数。随机梯度下降期间的梯度通过固定的预训练模型权重传递给适配器,该适配器被更新以优化损失函数。LoRA 通过额外的分解投影来增强线性投影,给定一个投影 , 其中 。LoRA 计算:
其中 和 是标量。
详细内容可参考该篇博文
PEFT
一个重要的讨论点是 LoRA 在训练期间的内存要求,无论是在使用的适配器的数量和大小方面。由于 LoRA 的内存占用非常小,我们可以使用更多的适配器来提高性能,而不会显着增加使用的总内存。虽然 LoRA 被设计为一种参数高效的微调 (PEFT) 方法,但 LLM 微调的大部分内存占用来自激活梯度,而不是来自学习的 LoRA 参数。对于在 FLAN v2 上训练的 7B LLaMA 模型,批量大小为 1,LoRA 权重等效于原始模型权重中常用的 0.2%,而 RA 输入梯度的内存占用为 567 MB,而 LoRA 参数仅占 26 MB。通过梯度检查点,输入梯度平均减少到每个平均 18 MB,而 LoRA 被设计为一个序列,使它们比所有 LoRA 权重组合更多的内存密集。相比之下,4 位基础模型消耗了 5048 MB 的内存。这突出了梯度检查点很重要,但也表明积极减少 LoRA 参数的数量只会产生很小的内存好处。这意味着我们可以使用更多的适配器,而不会显着增加整体训练内存占用。如前所述,这对于恢复完整的 16 位精度性能至关重要。
QLORA量化原理
4-bit NormalFloat(NF4)
作者提出的4-bit NormlFLoat量化是对Quantile Quantization(分位量化)进行了改进,并结合上诉Block-wise Quantization,降低计算复杂度和误差。
Quantile Quantization
量化是要将高精度参数映射到低精度上的某一个值。以4-bit为例,映射后的参数就只能从(0~15)中选一个,通常的量化方法是量化后直接赋最接近的哪个值(有点像四舍五入)。这种量化方法最明显的缺点是,量化后参数整体的分布与原始的分布差别很大,例如出现一个很大的值,可能到大部分参数都会量化到0,这样效果就会下降很明显。 为了避免这种情况,作者采用了分位量化(Quantile Quantization),那么什么是分位量化呢?还是以量化到4-bit为例,一共有也就是16个数字可以选,那么可以先将输入参数从小到大进行排序再等分为16份,每一份映射一个值,最小的一块映射到量化后的第一个数,第二块映射到量化后的第二个数,以此类推。。这种分位量化方法量化出的参数就能保证分布尽可能与原始分布相差不大。
NormFloat
上述分位量化会额外引入明显的计算开销,因为每次有参数输入进来都需要对齐进行排序并等分。
作者发现预训练的参数基本上都服从均值为0的正态分布,因此可以直接缩放到指定的范围内,在文章中使用的是[-1, 1] 的范围。同时可以将正态分布N(0,1)划分为份,并缩放到[-1, 1]的范围中。这样就能直接将参数映射到对应的分位,不用每次都对参数进行排序。 但是这样做饭也会有一个缺点,参数0量化后可能不在0的位置上了,就没法表达0的特殊意义了。为此作者还做了一点改进,即分别将负数和整数部分划分为份,参数0还是放在0原本的位置上。
从官方的代码中可以看到:
1 | import torch |
我们看看它这个函数主要做了什么
- 在这段代码中,作者使用了正态分布的分位数函数(percent point function,ppf),也就是正态分布的逆累积分布函数。
norm.ppf
函数接受一个介于0和1之间的概率值,并返回对应的z分数。例如,norm.ppf(0.975)
将返回大约1.96,因为在标准正态分布下,约有97.5%的值小于1.96。 torch.linspace(offset, 0.5, n)
函数生成一个等差数列,起始值为offset
,终止值为0.5,共有n
个元素。这个数列被用作norm.ppf
函数的输入,生成一组z分数;use_extra_value
参数决定了映射表中非零值的数量。如果use_extra_value
为True,映射表中将有15个非零值;否则,将有14个非零值;
双重量化(Double Quantization)
分块量化中每个block都会额外产生一个量化常数c。以量化32bit参数、block大小64为例,每个block会引入32bit的量化常数,对应每个参数会额外引入32/64=0.5bit的额外开销。双量化有助于减少量化常数的内存占用。
假设权重近似服从均值为0的正态分布,因此可以用其标准差表示其分布。所以,将一个权重张量进行量化后,不仅需要将保存量化后的张量,还需要额外一个32位的浮点数以表示其标准差(即 ),其占用32个比特的空间。因此,如果只做第一次量化,则需要额外存储的空间(除了存储量化张量以外)为32个比特,假如张量的大小(blocksize,即张量各个维度的乘积)为64,则其实就是对64个数字进行量化,那 额外需要的32比特平均到每个数字上,就是32/64=0.5比特。
为此作者采用了双重量化方法,如下图所示:
更具体地说,双量化将第一个量化的量化常数cFP322视为第二个量化的输入。第二步产生量化量化常数cFP82和第二级量化常数 。我们使用块大小为 256 的 8 位 Floats 进行第二次量化,因为 8 位量化没有观察到性能下降,这与 Dettmers 和 Zettlemoyer 的结果一致。由于为正,我们在量化前从 c2 中减去平均值以将值居中为零并利用对称量化。平均而言,对于 64 的块大小,这种量化将每个参数的内存占用从 32/64 = 0.5 位减少到 8/64 + 32/(64 · 256) = 0.127 位,每个参数减少了 0.373 位。
分页优化器(Paged Optimizer)
在 GPU 偶尔运行内存不足的情况下,使用 NVIDIA 统一内存功能在 CPU 和 GPU 之间自动页面到页面传输进行无错误的 GPU 处理。该功能适用于 CPU RAM 和磁盘之间的常规内存分页。我们使用此功能为优化器状态分配页码内存,当 GPU 运行内存不足时,当优化器更新步骤中需要内存时,这些状态会被自动门出到 CPU RAM。
结果
我们的结果表明,具有 NF4 数据类型的 4 位 QLORA 在具有完善的评估设置的学术基准上与 16 位完全微调和 16 位 LoRA 微调性能相匹配。我们还表明,NF4 比 FP4 更有效,双量化不会降低性能。结合,这形成了令人信服的证据,证明 4 位 QLORA 调整可靠地产生与 16 位方法匹配的结果。