今天是2025年3月5日,星期二,北京,天气晴。
今天,我们来看看量化,这个到底是啥,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散度)。

例如,选择一个百分位数会导致之前看到的类似截断行为。
b、激活值
在LLM中不断更新的输入通常被称为“激活值”。

注意这些值被称为激活值,因为它们通常会经过某种激活函数,如sigmoid或relu。
与权重不同,激活值会随着每次输入数据进入模型而变化,这使得准确量化它们变得具有挑战性。
由于这些值在每个隐藏层之后都会更新,只有在输入数据通过模型时,才能知道它们在推理过程中的值。

一般来说,有两种方法用于权重和激活值的量化方法:训练后量化(PTQ):训练后的量化或者 量化感知训练(QAT):训练/微调过程中的量化。
六、训练后量化该如何做?GPTQ及GGUF
最受欢迎的量化技术之一是训练后量化(PTQ)。它涉及在训练完模型后对模型的参数(权重和激活值)进行量化。
权重的量化可以使用对称或非对称量化来完成,然而,激活值的量化需要对模型进行推理,以获取它们的潜在分布,因为不知道它们的范围。
激活值有两种量化形式,一个是动态量化,另一个是静态量化。
动态量化指的是当数据通过一个隐藏层后,其激活值被收集起来:

然后利用这些激活值的分布来计算量化输出所需的零点(z)和缩放因子(s)值:

每次数据通过一个新层时,这个过程都会重复。因此,每个层都有自己独立的z和s值,从而拥有不同的量化方案。
与动态量化不同,静态量化不会在推理过程中计算零点(z)和缩放因子(s),而是在推理之前完成。
为了找到这些值,会使用一个校准数据集,并将其输入模型以收集这些潜在分布。

在收集完这些值之后,可以计算出用于在推理过程中进行量化的s和z值。
在实际进行推理时,s和z值不会重新计算,而是全局应用于所有激活值以进行量化。
一般来说,动态量化会更准确一些,因为它只尝试为每个隐藏层分别计算s和z值。然而,这也可能增加计算时间,因为需要计算这些值。
相比之下,静态量化虽然不够准确,但速度更快,因为它已经知道了用于量化的s和z值。
2、4位量化的几种方案
低于8位的量化已经被证明是一个艰巨的任务,因为随着每次位数的减少,量化误差会增加。幸运的是,有一些巧妙的方法可以将位数减少到6位、4位,甚至2位(尽管通常不建议使用这些方法将位数降低到4位以下)。
有两种在HuggingFace上常见的方法, GPTQ(整个模型在GPU上运行),GGUF(可能将层卸载到CPU上)
1)GPTQ
GPTQ可以说是实际应用中最知名的4位量化方法之一,使用非对称量化,并且逐层进行,每一层都独立处理,然后再继续下一层:

在逐层量化过程中,它首先将层的权重转换为逆海森矩阵。这是模型损失函数的二阶导数,它告诉模型的输出对每个权重的变化有多敏感。
简化来说,它本质上展示了每一层中每个权重的(逆)重要性。
与海森矩阵中较小值相关的权重更为重要,因为这些权重的微小变化可能导致模型性能的显著变化。

在逆海森矩阵中,较低的值表示更“重要”的权重。
接下来,对权重矩阵的第一行的权重进行量化,然后再反量化:

这个过程允许计算量化误差(q),并使用之前计算的逆海森矩阵(h_1)对其进行加权。
本质上,根据权重的重要性创建了一个加权量化误差:

接下来,将这个加权量化误差重新分配到该行的其他权重上。这使得网络的整体功能和输出得以保持。
例如,如果对第二权重(即0.3,x_2)进行处理,将量化误差(q)乘以第二权重的逆海森矩阵(h_2)并将其添加进去:

可以对给定行中的第三个权重进行相同的操作:

通过迭代这个重新分配加权量化误差的过程,直到所有值都被量化。
这种方法之所以有效,是因为权重通常是相互关联的。因此,当一个权重出现量化误差时,相关的权重会通过逆海森矩阵相应地进行更新。
2)GGUF
尽管GPTQ是一种可以在GPU上运行完整LLM的优秀量化方法,但你并不总是有这种能力。相反,可以使用GGUF将LLM的任何一层卸载到CPU上,这允许你在没有足够显存时同时使用CPU和GPU。
GGUF的量化方法经常更新,可能取决于位量化水平。然而,一般原则如下。
首先,将给定层的权重分成“超级”块,每个“超级”块包含一组“子”块。从这些块中,提取缩放因子(s)和α:

为了量化一个“子”块,可以使用之前提到的absmax量化。记住它会将给定权重乘以缩放因子(s):

缩放因子是根据“子”块的信息计算的,但使用“超级”块的信息进行量化,而“超级”块有自己的缩放因子:

这种块级量化使用“超级”块的缩放因子(s_super)来量化“子”块的缩放因子(s_sub)。
每个缩放因子的量化水平可能不同,“超级”块通常比“子”块的缩放因子具有更高的精度。
为了说明,让探索几种量化水平(2位、4位和6位):

注意:根据量化类型,可能需要一个额外的最小值(m)来调整零点。这些值的量化方式与缩放因子(s)相同。
七、训练中的QAT量化如何实现?
训练后对模型进行量化方法的一个缺点是 这种量化并没有考虑实际的训练过程。
这就是量化感知训练(QAT)的作用所在。与训练后量化(PTQ)不同,QAT的目标是在训练过程中学习量化过程。

QAT通常比PTQ更准确,因为量化已经在训练过程中被考虑。
其工作原理如下:
在训练过程中,引入所谓的“假”量化。这个过程首先是将权重量化为例如INT4,然后再反量化回FP32:

这个过程允许模型在训练过程中考虑量化过程、损失计算和权重更新。
QAT试图探索“宽”最小值的损失,以最小化量化误差,因为“窄”最小值往往会导致较大的量化误差。

例如,假设在反向传播过程中没有考虑量化。会根据梯度下降选择损失最小的权重。然而,如果它处于“窄”最小值中,这将引入较大的量化误差。
相比之下,如果考虑量化,就会选择一个处于“宽”最小值中的不同更新权重,其量化误差要小得多。

因此,尽管PTQ在高精度(例如FP32)下的损失更低,但QAT在低精度(例如INT4)下的损失更低,而这正是所追求的。
1、1位LLM时代:BitNet
将精度降低到4位已经相当小了,但如果进一步降低呢?
这就是BitNet的作用所在,它使用单个1位来表示模型的权重,给定权重可以是-1或1。
它通过将量化过程直接注入Transformer架构来实现。
记住,Transformer架构是大多数LLM的基础,由涉及线性层的计算组成。

这些线性层通常以更高的精度(如FP16)表示,这也是大多数权重所在的地方。
BitNet用他们称为BitLlinear的层替换了这些线性层:

BitLinear层的工作方式与普通线性层相同,根据权重乘以激活值来计算输出。
相比之下,BitLinear层使用1位表示模型的权重,激活值使用INT8:

像量化感知训练(QAT)一样,BitLinear层在训练过程中进行一种“假”量化,以分析权重和激活值量化的效应:

注意:在论文中他们使用了γ而不是α,但因为之前一直使用α,所以这里继续使用α。另外,这里的β与在零点量化中使用的β不同,而是平均绝对值。
让逐步了解BitLinear的工作原理。
1、权重量化
在训练过程中,权重以INT8的形式存储,然后使用一种基本策略(称为符号函数)量化为1位。
本质上,它将权重的分布中心化到0,然后将左侧的所有值分配为-1,右侧的所有值分配为1:

此外,它还会跟踪一个值β(平均绝对值),稍后将在反量化过程中使用。
2、激活值量化
为了量化激活值,BitLinear使用absmax量化将激活值从FP16转换为INT8,因为它们需要以更高的精度进行矩阵乘法(×)。

此外,它还会跟踪α(最高绝对值),稍后将在反量化过程中使用。
3、反量化
跟踪了α(激活值的最高绝对值)和β(权重的平均绝对值),这些值将帮助将激活值反量化回FP16。
输出激活值通过{α, γ}重新缩放,以将其反量化回原始精度:

就是这样!这个过程相对简单,允许模型仅用两个值(-1或1)来表示。
使用这种方法,作者观察到,随着模型规模的增大,1位训练和FP16训练之间的性能差距越来越小。
然而,这仅适用于较大的模型(>300亿参数),对于较小的模型,差距仍然相当大。
总结
本文主要通过可视化图解的方式,对量化的过程进行了具象化,其中很有趣。
但是,具体的细节,我们还需要进一步深入了解。
参考文献
1、https://substack.com/@maartengrootendorst/p-145531349
2、https://developer.aliyun.com/article/1649946
(文:老刘说NLP)