今天,我们来看看量化,这个到底是啥,GPTQ(整
个模型在GPU上运行),GGUF(可能将层卸载到CPU上)又是啥?本质是啥?这些都是困扰已久的问题。
而
更具象化地理解一个技术,是很有益的
。最近看到一个工作,https://substack.com/@maartengrootendorst/p-145531349,讲了关于量化的一些事儿,觉得蛮好的,供大家参考。专题化,体系化,会有更多深度思考。大家一起加油。
一、先从几个定性的结论说起
大模型(LLMs)通常过于庞大,可能包含数十亿甚至数千亿个参数,通常需要配备大量显存(VRAM)的GPU来加速推理。
因此,
越来越多的研究集中在通过改进训练、适配器等方式使这些模型变得更小。这一领域的一个主要技术被称为量化
。
这块就不多说了,有个文章写的很清晰,参考:https://developer.aliyun.com/article/1649946中所说。
量化是一种
将较大尺寸的模型(如 LLM 或任何深度学习模型)压缩为较小尺寸的方法,比如最开始训练出的权重是32位的浮点数,但是实际使用发现用16位来表示也几乎没有什么损失,但是模型文件大小降低一般,显存使用降低一半,处理器和内存之间的通信带宽要求也降低了,这意味着更低的成本、更高的收益
。低精度运算通常比高精度运算速度快,单次乘法、加法的耗时更短
说下最终的结论,量化的核心思想就是在尽可能保持
每个参数及权重的值能够彼此区分开
的前提下,来减少
表示空间占据
,也就是,希望在保持准确性的同时减少表示值的位数,你可以把它们表示的不那么精确,但是,还得保证,你能区分他们。例如,将模型参数的精度从高比特宽度(如32位浮点数)降低到低比特宽度(如8位整数),拥有的位数越多,可以表示的值的范围就越大。
并且,
这个量化是不可逆的
,一旦损失之后,再返回回去,是有误差的。你可以看到某些值,如3.08和3.02被分配到INT8,即36。当你将值反量化回FP32时,它们会失去一些精度,且不再可区分,一般来说,位数越少,倾向于有越多的量化误差。
也就是这个错误:
当然,对于量化,我
有个不太贴切的具象化例子,参数就是分辨率
,4k,1080p,720p,360p,量化,可以类比成像素点采样。本来很清晰的,采样一些,也不会太损失,还能看的清楚。本来就勉强能看的,再采样一下,就糊了。
更为形象的是,应该是叫像素通道
,量化之前是有32种颜色来区分,量化之后会检索通道数,这样一来,所能表示的色彩丰富度是不够的。
也有另一个比喻,
这就像按照菜谱做菜,你需要确定每种食材的重量。你可以使用一个非常精确的电子秤,它可以精确到0.01克,这固然很好,因为你可以非常精确地知道每样食材的重量。但是,如果你只是做一顿家常便饭,实际上并不需要这么高的精度,你可以使用一个简单又便宜的秤,最小刻度是1克,虽然不那么精确,但是足以用来做一顿美味的晚餐
。
左侧为基础模型大小计算(单位:GB),右侧为量化后的模型大小计算(单位:GB)在上图中,基础模型Llama38B的大小为32GB。
经过Int8量化后,大小减少到8GB(减少了75%)。使用Int4量化后,大小进一步减少到4GB(减少约90%)。这使模型大小大幅减少
。
这个思想跟在不重新训练模型的情况下有效地扩展上下文窗口的防范一样,乳位置插值(PI)方法 ,通过线性下调位置索引,可以将原本的[0, 4096]位置范围内的位置信息“压缩”到[0, 2048]内,使得模型可以处理更长的文本,就像处理2048个token一样有效。
那么,从技术上讲,量化有哪些分类?
如果按照量化
时间点
分类,可以分为
后训练量化
(Post-Training Quantization, PTQ,在模型训练完成后对模型进行量化的过程,简单易行,适用于已经训练好的模型,但可能会带来一定的精度损失)以及
量化感知训练
(Quantization-Aware Training, QAT,在训练阶段引入量化机制,让模型在训练过程中“感知”到量化的影响,从而尽量减少量化带来的精度损失。虽然训练过程更为复杂且耗时较长,但它可以在保持较高精度的同时实现模型压缩)。
如果按照
量化粒度
分,可以
分为Per-tensor量化
(整个张量或层级共享相同的量化参数(scale和zero-poin,这种方式的优点是存储和计算效率较高,但可能导致精度损失);
Per-channel量化
(每个通道或轴都有自己的量化参数。这种方式可以更准确地量化数据,因为每个通道可以根据自身特性调整动态范围,但会增加存储需求和计算复杂度);Per-group量化(将数据分组处理,每组有自己的量化参数,介于上述两者之间)。
如果按照是否
线性映射
分,可以分为
线性量化
(Linear Quantization,采用线性映射的方式将浮点数映射到整数范围内,进一步细分为对称量化和非对称量化两种形式);
非线性量化
(Non-linear Quantization,对数量化,它不是简单的线性变换,而是基于某种函数关系来进行映射)
如果按照
量化后的数值范围
来分,可以分为
二值量化
(Binary Quantization,将权重限制在+1和-1两个值之间);
三值量化
(Ternary Quantization,允许使用三个离散值,通常是-1、0和+1);
定点数量化
(Fixed-Point Quantization,最常见的是INT8和INT4,它们分别用8位和4位整数表示权重);
非均匀量化
(Non-uniform Quantization,根据待量化参数的概率分布计算量化节点,以适应特定的数据分布模式)。
二、LLMs目前太大的“问题”带来的量化问题
LLMs之所以得名,是因为它们包含的参数数量。如今,这些模型通常包含数十亿个参数(大多是权重),存储成本相当高昂。
在推理过程中,输入与权重的乘积会生成激活值,这些值同样可能非常庞大。
因此,希望尽可能高效地表示数十亿个值,最小化存储给定值所需的空间。
让从头开始,探索数值是如何表示的,然后再对其进行优化。
1、如何表示数值?
一个给定的值通常被表示为浮点数(在计算机科学中称为floats):一个带有小数点的正数或负数。
这些值通过“位”或二进制数字来表示。IEEE-754标准描述了位如何表示三种功能以表示值:符号、指数或小数(或尾数)。
这三者结合起来,可以根据一组特定的位值计算出一个值:
使用的位数越多,表示的值通常越精确:
2、内存限制
拥有的位数越多,可以表示的值的范围就越大。
给定表示可以取的可表示数字的区间称为动态范围,而两个相邻值之间的距离称为精度。
这些位的一个巧妙之处在于,可以计算你的设备存储给定值所需的内存。由于1字节内存中有8位,可以为大多数浮点表示形式创建一个基本公式。
注意
:在实践中,推理过程中所需的(V)RAM数量还与上下文大小和架构等因素有关。
现在假设有一个包含700亿个参数的模型。大多数模型以32位浮点数(通常称为全精度)的形式原生表示,仅加载模型就需要280GB的内存。
因此,将模型参数的表示位数最小化(以及在训练过程中!)是非常有吸引力的。然而,随着精度的降低,模型的准确性通常也会下降。
希望在保持准确性的同时减少表示值的位数……这就是量化的作用!
三、什么是量化?
量化旨在将模型参数的精度从高比特宽度(如32位浮点数)降低到低比特宽度(如8位整数)。
在减少表示原始参数的位数时,通常会有一些精度(粒度)的损失。
为了说明这种效果,可以取任何图像,并仅用8种颜色来表示它:
图像改编自Slava Sidorov的原图。
注意,放大后的部分看起来比原始图像更“颗粒化”,因为只能用更少的颜色来表示它。
量化的首要目标是在尽可能保留原始参数精度的情况下,减少表示原始参数所需的位数(颜色)。
1、常见数据类型
首先,来看看常见数据类型以及使用它们而不是32位(称为全精度或FP32)表示的影响。
1)FP16
让来看一个从32位到16位(称为半精度或FP16)浮点数的例子:
注意FP16可以取的值的范围比FP32小得多。
2)BF16
为了获得与原始FP32相似的值范围,引入了bfloat16作为一种“截断的FP32”:
BF16使用的位数与FP16相同,但可以表示更广泛的值范围,常用于深度学习应用。
3)INT8
当进一步减少位数时,逐渐进入基于整数的表示领域,而不是浮点表示。例如,从FP32到INT8,后者只有8位,结果是原始位数的四分之一:
根据硬件的不同,基于整数的计算可能比浮点计算更快,但这并不总是如此。然而,使用更少的位数时,计算通常会更快。
对于每次位数的减少,都会进行映射,以“压缩”初始的FP32表示到更低位数。
在实践中,不需要将整个FP32范围[-3.4e38, 3.4e38]映射到INT8。只需要找到一种方法,将数据范围(模型的参数)映射到INT8。
四、如何从FP32量化到INT8?
常见的压缩/映射方法是对称量化和非对称量化,它们是线性映射。
1、对称量化
在对称量化中,原始浮点值的范围被映射到量化空间中围绕零的对称范围。在前面的例子中,注意量化前后的范围都以零为中心。
这意味着浮点空间中零的量化值在量化空间中正好是零。
对称量化的一个典型例子是绝对最大值(absmax)量化。
给定一组值,取最高绝对值(α)作为线性映射的范围。
注意[-127, 127]范围的值表示受限范围。不受限范围是[-128, 127],取决于量化方法。
由于它是一个以零为中心的线性映射,公式非常简单。
首先计算一个缩放因子(s),b是想要量化的字节数(8),α是最高绝对值,然后,使用s来量化输入x:
填入值后,得到以下结果:
为了恢复原始的FP32值,可以使用之前计算的缩放因子(s)来对量化值进行反量化。
应用量化和反量化过程以恢复原始值如下所示:
你可以看到某些值,如3.08和3.02被分配到INT8,即36。当你将值反量化回FP32时,它们会失去一些精度,且不再可区分。
这通常被称为量化误差,可以通过计算原始值和反量化值之间的差异来得出。
一般来说,位数越少,倾向于有越多的量化误差。
2、非对称量化
与对称量化不同,
非对称量化并不围绕零对称
。相反,它将浮点范围中的最小值(β)和最大值(α)映射到量化范围的最小值和最大值。
核心点就是零点量化。
注意
0的位置发生了偏移
,这就是为什么它被称为
非对称量化
。
范围[-7.59, 10.8]中的最小值和最大值与0的距离不同。
由于其偏移位置,
需要计算INT8范围的零点(z)以进行线性映射
。与之前一样,还需要计算缩放因子(s),但使用INT8范围的差值[-128, 127]。
注意,由于需要计算INT8范围中的零点(z)以偏移权重,这变得更加复杂。
与之前一样,来填入公式:
为了将从INT8反量化回FP32,需要使用之前计算的缩放因子(s)和零点(z)
。
除此之外,反量化过程非常简单:
当把对称量化和非对称量化放在一起比较时,可以迅速看出两种方法的区别:
注意对称量化的零中心特性与非对称量化的偏移。
再温习下,形象的理解下:也就是将
原始张量范围(Wmin, Wmax)中的值映射到量化张量范围(Qmin, Qmax)中的值
。
图中的“A”部分展示了量化过程,即[Wmin,Wmax]->[Qmin,Qmax]的映射,图中的“B”部分展示了反量化过程,即[Qmin,Qmax]->[Wmin,Wmax]的映射。
其中:
Wmin,Wmax
表示原始张量的最小值和最大值(数据类型:FP32,32位浮点)。在大多数现代LLM中,权重张量的默认数据类型是FP32。
Qmin,Qmax
表示量化张量的最小值和最大值(数据类型:INT8,8位整数)。也可以选择其他数据类型,如INT4、INT8、FP16和BF16来进行量化。
缩放值(S)
表示在量化过程中,缩放值将原始张量的值缩小以获得量化后的张量。在反量化过程中,它将量化后的张量值放大以获得反量化值。缩放值的数据类型与原始张量相同,为FP32。
零点(Z)
是量化张量范围中的一个非零值,它直接映射到原始张量范围中的值0。零点的数据类型为INT8,因为它位于量化张量范围内。
但是,
这种映射会有个异常值
。如果Z值超出范围怎么办?可以使用简单的if-else逻辑将Z值调整为Qmin,如果Z值小于Qmin;若Z值大于Qmax,则调整为Qmax?
如果Q值超出范围怎么办
?可以在PyTorch中,有一个名为 clamp 的函数,它可以将值调整到特定范围内(在我们的示例中为-128到127)。因此,clamp函数会将Q值调整为Qmin如果它低于Qmin,将Q值调整为Qmax如果它高于Qmax。
所以,
这块的方案,其实又变成艺术
了,可以更细化地具体情况具体分析。
五、如何处理量化中的异常值?
在前面的例子中,探讨了如何将给定向量中的值范围映射到低比特表示。尽管这允许将向量的全部值范围进行映射,但它有一个主要缺点,即
异常值
。
假设你有一个向量,其值如下:
注意其中一个值远大于其他值,可以被视为异常值。
如果映射这个向量的全部范围,所有小值都会被映射到相同的低比特表示,并失去它们的区分
特征:
这就是之前使用的absmax方法。注意,如果不应用截断,非对称量化也会出现相同的行为。
相反,可以选择对某些值进行截断。截断涉及设置原始值的不同动态范围,使得所有异常值都获得相同的值
。
在下面的例子中,如果手动将动态范围设置为[-5, 5],那么超出该范围的所有值都将被映射为-127或127,无论它们的实际值如何:
主要优点是,非异常值的量化误差显著降低。然而,异常值的量化误差增加了。
所以,
选择这个范围的过程称为校准,其目的是找到一个尽可能包含更多值的范围,同时最小化量化误差
。
对于所有类型的参数,执行校准步骤并不相同,所以会需要针对不同的参数类型进行量化。
1、权重(和偏置)
可以将LLM的权重和偏置视为静态值,因为它们在运行模型之前就已经确定。例如,Llama 3的~20GB文件主要由其权重和偏置组成。
由于偏置的数量(百万级)远少于权重(十亿级),
偏置通常保留更高的精度(如INT16)
,而量化的主要努力集中在权重上。
对于静态且已知的权重,选择范围的校准技术包括:
手动选择输入范围的百分位数;优化原始权重和量化权重之间的均方误差(MSE);最小化原始值和量化值之间的熵(KL散度)