LLMs之Quantization:LLM中量化技术的可视化指南之量化技术的简介、常用数据类型、校准权重和激活值的量化方法(PTQ/QAT),1-bit 的LLM的时代以及BitNet 1.58
导读:2024年7月22日,Maarten Grootendorst探讨了量化(Quantization)在大型语言模型(LLMs)中的应用,主要包含以下核心要点:
背景痛点:LLMs 拥有数十亿的参数,占用大量内存和计算资源,需要大量的显存来进行推理,在消费级硬件上运行存在挑战。需要将模型参数和激活值从32位浮点数表示压缩到更小的比特位数,以降低存储和计算开销。
解决方案:采用量化(Quantization)技术。通过将模型参数从高精度(如32位浮点数)压缩到低比特表示(如8位整数),以减少存储空间和计算开销,同时尽可能保留原始精度。
主要方法包括:
>> 数据类型:常见的量化数据类型包括FP16、BF16、INT8等。量化会引入量化误差,随着比特位数的降低,误差会增大。
>> T1、后量化(PTQ):在模型训练完成后对权重和激活值进行量化,包括动态量化和静态量化。
>> T2、量化感知训练(QAT):在训练过程中就考虑量化,通过"假量化"让模型更好地适应低比特表示。旨在寻找"宽"的损失最小区域,以降低量化误差。包括权重量化、激活量化和反量化。
● 权重量化:对称量化或非对称量化,分别围绕零对称映射和偏移映射。
● 激活值量化:动态量化或静态量化。动态量化指在推理时计算,静态量化指使用校准数据集提前计算。
>> 超低比特量化:
● GPTQ: 基于Hessian矩阵的4比特量化,逐层处理,使用逆Hessian优化。
● GGUF: 分块量化,支持卸载部分层到CPU运算
● 1比特和1.58比特量化:
● BitNet: 将权重量化到1比特(-1或1)
● BitNet 1.58b: 引入0值,形成三值表示,大幅提升性能和计算效率
量化优势:
>> 减少内存使用:通过降低比特宽度显著减少模型大小,节省存储空间和计算开销,适合在资源有限的设备上运行。
>> 提高计算效率:量化后的模型计算速度更快。
>> 灵活性:支持不同的量化策略,如动态和静态量化。后训练量化无需重新训练,便于部署
>> 性能保留:通过QAT等方法,在低比特表示中仍保持较高的模型准确性,可以最小化量化误差,保持模型性能。
>> 1比特和1.58比特量化可以大幅加速计算并支持特征过滤。1比特LLM等新技术的出现,进一步推动了量化在LLM中的应用。
这些量化技术为大型语言模型提供了高效实用的解决方案,能够在保持性能的同时显著降低计算和存储成本。总的来说,量化技术通过牺牲一定精度,实现了将大型语言模型精简和加速推理,扩展了模型在消费级设备上的应用场景。不同的量化方法权衡了尺寸压缩、精度损失和计算效率之间的平衡。
目录
LLM中量化技术的可视化指南
地址 | 文章地址:A Visual Guide to Quantization - by Maarten Grootendorst |
时间 | 2024年7月22日 |
作者 | Maarten Grootendorst |
顾名思义,大型语言模型 (LLM) 通常太大,无法在消费级硬件上运行。这些模型可能超过数十亿个参数,并且通常需要配备大量显存(VRAM)的GPU来加速推理。
因此,越来越多的研究集中于通过改进训练、适配器等方式使这些模型变得更小。该领域的一项主要技术称为量化(Quantization)。
在这篇文章中,我将在语言建模的背景下介绍量化领域,并逐步探索各个概念,以帮助我们对这个领域形成直观理解。我们将探索量化背后的各种方法、用例和原理。
作为一个视觉指南,本文将包括大量可视化图表,以帮助大家直观理解量化!
第一部分:大型语言模型(LLM)的问题
LLM 因其包含的参数数量而得名。如今,这些模型通常具有数十亿个参数(大多数是权重),这些权重的存储成本相当高。在推理过程中,激活(activations)是输入与权重的乘积,这些激活也可能相当庞大。
因此,我们希望尽可能高效地表示数十亿个值,并尽量减少存储给定值所需的空间。让我们从头开始,在优化数值之前,首先探索一下数值是如何表示的。
如何表示数值
一个给定的值通常表示为一个浮点数(或在计算机科学中称为浮点数,floats):一个带有小数点的正数或负数。
这些值由“位”(bits)或二进制数字表示。IEEE-754标准描述了如何使用位来表示一个值的三个组成部分:符号(sign)、指数(exponent)或小数部分(fraction 或称为尾数 mantissa)。
这些组成部分可以结合起来,根据一定的位值来计算出一个数值:
我们用来表示值的位越多,它通常就越精确。
内存限制
我们可用的位数越多,可以表示的数值范围就越大。
给定表示可以表示的数字区间称为动态范围(dynamic range),而两个相邻值之间的距离称为精度(precision)。
这些位的一个巧妙特性是,我们可以计算出设备存储一个给定值需要多少内存。由于一个字节的内存中有8位,我们可以为大多数形式的浮点表示创建一个基本公式。
注意:在实际中,与推理期间需要的(V)RAM数量相关的因素还有很多,比如上下文大小和架构。
现在假设我们有一个包含70B(700亿)个参数的模型。大多数模型通常用32位浮点(通常称为全精度,float32)表示,仅加载模型就需要280GB的内存。
因此,将模型参数(以及训练期间)的表示位数最小化是非常有吸引力的。
然而,随着精度的降低,模型的准确性通常也会下降。我们希望在减少表示数值的位数的同时保持精度……这就是量化的用武之地!
第二部分:量化简介
量化的目的是将模型参数的精度从较高位宽(如 32 位浮点数)降低到较低位宽(如 8 位整数)。
通常,当减少表示原始参数的位数时,会出现一些精度(或称为颗粒度)的损失。
为了说明这种效果,我们可以取任意图像并仅使用 8 种颜色来表示它:
图片改编自Slava Sidorov的原文。
请注意,放大的部分看起来比原始部分更“颗粒状”,因为我们可以使用更少的颜色来表示它。
量化的主要目标是减少表示原始参数所需的位数(颜色),同时尽可能地保持原始参数的精度。
常见的数据类型:FP16、BF16、INT8
首先,让我们看看常见的数据类型以及使用它们代替32位表示(称为全精度或FP32)时的影响。
FP16
让我们看一个从32位到16位(称为半精度或FP16)浮点数的例子:
注意,FP16的数值范围比FP32小得多。
BF16
为了获得与原始FP32相似的数值范围,引入了bfloat16作为一种“截断的FP32”:BF16使用与FP16相同的位数,但可以取更宽的数值范围,通常用于深度学习应用中。
INT8
当我们进一步减少位数时,我们接近整数表示的领域,而不是浮点表示。例如,从FP32到只有8位的INT8,结果是原始位数的四分之一:
根据硬件的不同,基于整数的计算可能比浮点计算更快,但情况并非总是如此。不过,使用较少的位数,计算速度通常会更快。
对于每次位数的减少,都会进行一次映射,将初始FP32表示“压缩”到更低的位数中。在实际中,我们不需要将整个FP32范围[-3.4e38, 3.4e38]映射到INT8。我们只需要找到一种方法将数据的范围(模型的参数)映射到INT8中。
常见的压缩/映射方法是对称和非对称量化,属于线性映射的一种形式。让我们探索这些从FP32到INT8的量化方法。
对称量化
在对称量化中,原始浮点值的范围被映射到量化空间中以零为中心的对称范围。在前面的例子中,请注意量化前后的范围如何保持以零为中心。
这意味着浮点空间中零的量化值在量化空间中正好为零。对称量化形式的一个很好的例子是绝对最大值(absmax)量化。
给定一组值,我们取最高的绝对值(α)作为进行线性映射的范围。请注意,[-127, 127]值范围表示受限范围。非受限范围是[-128, 127],取决于量化方法。由于它是以零为中心的线性映射,公式相对简单。
我们首先使用以下方法计算一个比例因子(s):
b 是我们要量化到的字节数(8),
α 是最高的绝对值,
然后,我们使用s来量化输入x:
填入这些值后,我们可以得到以下结果:
为了检索原始的FP32值,我们可以使用先前计算的比例因子(s)来反量化这些量化值。
应用量化和反量化过程来恢复原始值,如下所示:
你可以看到某些值,如3.08和3.02被分配给INT8中的36。当你将这些值反量化回FP32时,它们失去了一些精度,并且不再可区分。
这通常被称为量化误差,我们可以通过计算原始值和反量化值之间的差异来得出。通常,位数越低,量化误差越大。
非对称量化
非对称量化与对称量化相反,它并不是围绕零对称的。相反,它将浮点范围的最小(β)和最大(α)值映射到量化范围的最小和最大值。
我们要探索的方法称为零点量化(zero-point quantization)。
注意0的位置如何发生了变化?这就是它被称为非对称量化的原因。在范围[-7.59, 10.8]中,最小值和最大值到0的距离不同。
由于其位置的偏移,我们必须计算INT8范围内的零点来进行线性映射。与之前一样,我们还需要计算一个比例因子(s),但使用INT8范围的差值[-128, 127]。
注意,由于需要计算INT8范围内的零点(z)来调整权重,这比对称量化稍显复杂。
与之前一样,我们可以填入公式:
为了将量化从 INT8 反量化回 FP32,我们需要使用先前计算的比例因子(s)和零点(z)。
除此之外,反量化相对简单:
当我们将对称和非对称量化方法并列放在一起时,可以很快看出方法之间的区别:注意对称量化的以零为中心的特性与非对称量化的偏移不同。
范围映射和截断
在之前的示例中,我们探索了如何将给定向量中的值范围映射到较低位表示。虽然这允许映射整个向量值范围,但它有一个主要缺点,即离群值。
想象一下,你有一个包含以下值的向量:
注意,其中一个值远大于所有其他值,可以被认为是一个离群值。如果我们要映射这个向量的完整范围,所有小值都将映射到相同的低位表示,失去了它们的区分因子:
这是我们之前使用的absmax方法。注意,如果不应用截断,非对称量化也会发生同样的行为。相反,我们可以选择截断范围的上下限。虽然这使得INT8空间不再完全表示输入数据,但可以保留小值之间的更高区分度。
相反,我们可以选择裁剪某些值。裁剪涉及设置原始值的不同动态范围,以便所有异常值都获得相同的值。
在下面的例子中,如果我们手动将动态范围设置为 [-5, 5],则该范围之外的所有值都将被映射到 -127 或 127,无论它们的值如何:
其主要优点是非异常值的量化误差显著降低,但异常值的量化误差增大。
在实践中,离群值往往非常少见,数量上不成比例。大多数现代量化技术都应用某种形式的截断。无论量化技术如何,都存在一个折衷。
校准
在前面的例子中,我展示了一种简单的方法,即选择一个任意的范围[-5, 5]。选择这个范围的过程称为校准(calibration),其目的是找到一个包含尽可能多值的范围,同时最小化量化误差。
对所有类型的参数来说,这个校准步骤并不是相同的。
权重(Weights)和偏置(Biases)
我们可以将大型语言模型(LLM)的权重和偏置视为静态值,因为在运行模型之前这些值是已知的。例如,Llama 3 的约20GB文件主要由其权重和偏置组成。
由于偏置的数量(数百万)远少于权重(数十亿),因此偏置通常保留在较高精度(如INT16)下,而量化的主要工作集中在权重上。
对于已知的静态权重,选择范围的校准技术包括:
>> 手动选择输入范围的一个百分位数
>> 优化原始权重和量化权重之间的均方误差(MSE)
>> 最小化原始值和量化值之间的熵(KL散度)
例如,选择一个百分位数会导致类似于之前所见的截断行为。
激活值(Activations)
在整个 LLM 过程中不断更新的输入通常被称为“激活值”。注意,这些值之所以被称为激活值,是因为它们通常会经过一些激活函数,例如sigmoid或relu。
与权重不同,激活值在推理过程中随每次输入数据的变化而变化,这使得准确量化它们变得具有挑战性。由于这些值在每个隐藏层之后都会更新,因此只有在推理时随着输入数据通过模型时,我们才能知道它们的值。
两种方法用于校准权重和激活值的量化方法:PTQ、QAT
总体而言,有两种方法用于校准权重和激活值的量化方法:
T1、训练后量化(Post-Training Quantization, PTQ):在训练之后进行量化
T2、量化感知训练(Quantization Aware Training, QAT):在训练/微调期间过程中进行量化
第三部分:训练后的量化PTQ
最受欢迎的量化技术之一是训练后量化(Post-Training Quantization, PTQ)。它是在模型训练之后,对模型的参数(包括权重和激活值)进行量化。
>> 权重的量化:可以使用对称量化或非对称量化。
然而,激活值的量化需要通过推理模型来获取它们的潜在分布,因为我们事先并不知道它们的范围。
>> 激活值的量化有两种形式:动态量化(Dynamic Quantization)、静态量化(Static Quantization)。
(1)、权重的量化:对称量化或非对称量化
(2)、激活值的量化:动态量化、静态量化
T1、动态量化
数据通过一个隐藏层后,其激活值被收集:
然后利用这个激活分布来计算量化输出所需的零点(z)和比例因子(s)的值:
这个过程在每次数据通过新的层时都会重复。因此,每层都有自己独立的z和s值,从而具有不同的量化方案。
T2、静态量化
与动态量化相反,静态量化不是在推理过程中计算零点(z)和比例因子(s),而是事先提前计算这些值。为了找到这些值,会使用一个校准数据集,并将其提供给模型以收集这些潜在分布。
在这些值被收集之后,我们可以计算出推理过程中执行量化所需的s和z值。当进行实际推理时,s和z值不会被重新计算,而是将它们在所有激活值上全局使用以进行量化。
总体而言,动态量化往往更为准确,因为它只尝试计算每个隐藏层的s和z值。但是,它可能会增加计算时间,因为需要计算这些值。相比之下,静态量化不太准确,但速度更快,因为它已经知道用于量化的s和z值。
(3)、4位量化的领域
事实证明,低于 8 位量化是一项艰巨的任务,因为每损失一位,量化误差就会增加。幸运的是,有几种巧妙的方法可以将位数减少到 6 位、4 位甚至 2 位(尽管通常不建议使用这些方法将位数降至 4 位以下)。
我们将探讨HuggingFace上常见的两种方法:
T1、GPTQ(在GPU上运行整个模型)
T2、GGUF(可能将层卸载到CPU上)
T1、GPTQ
GPTQ 可以说是实践中用于量化为 4 位的最著名的方法之一。它使用非对称量化,并逐层进行,以便每一层都经过独立处理后再继续下一层:在这个逐层量化过程中,它首先将层的权重转换为Hessian 的逆。Hessian矩阵是模型损失函数的二阶导数,它告诉我们模型输出对每个权重变化的敏感性。
简而言之,它基本上展示了每个权重在一层中的(逆)重要性。与Hessian矩阵中较小值相关的权重更为关键,因为这些权重的微小变化可能导致模型性能的显著变化。在逆Hessian中,较低的值表示更“重要”的权重。
接下来,我们对权重矩阵中的第一行的权重进行量化和反量化:这个过程使我们能够计算量化误差(q),我们可以用之前计算的逆Hessian(h1)来对其进行加权。
本质上,我们根据权重的重要性创建加权量化误差:
接下来,我们将这个加权量化误差重新分配给该行中的其他权重。这可以保持网络的整体功能和输出。
例如,如果我们要对第二个权重(即0.3,x2)执行此操作,我们会添加量化误差(q)乘以第二个权重的逆Hessian(h2)
我们可以对给定行中的第三个权重执行相同的过程:我们迭代这个重新分布加权量化误差的过程,直到所有值都被量化。
这之所以有效,是因为权重通常是相互关联的。因此,当一个权重有量化误差时,相关的权重也会相应更新(通过 Hessian 逆矩阵)。
注意:作者使用了几种技巧来加速计算并提高性能,例如在Hessian中添加阻尼因子、“懒惰批处理”、以及使用Cholesky方法预计算信息。我强烈建议查看这个主题的YouTube视频。
提示:如果你想要一种针对性能优化和提高推理速度的量化方法,可以查看EXL2。
T2、GGUF
虽然GPTQ是一种很好的量化方法,可以在GPU上运行完整的 LLM,但你可能并不总是有这样的能力。相反,我们可以使用GGUF将LLM的任何层卸载到CPU上。这使得当你没有足够的显存VRAM 时,可以同时使用CPU和GPU。
量化方法GGUF经常更新,并可能取决于量化位数的水平。然而,总体原则如下。
首先,将给定层的权重拆分为“超级”块,每个“超级”块包含一组“子”块。从这些块中,我们提取比例因子(s)和alpha(α):
为了量化给定的“子”块,我们可以使用之前使用的absmax量化。请记住,它通过比例因子(s)乘以给定权重:
比例因子是使用来自“子”块的信息计算的,但使用来自具有自身比例因子的“超级”块的信息进行量化:
这种逐块量化使用“超级”块的比例因子(s_super)来量化“子”块的比例因子(s_sub)。
每个比例因子的量化水平可能不同,通常“超级”块的精度高于“子”块的比例因子。
为了说明这一点,让我们探讨几个量化级别(2位、4位和6位):
注意:根据量化类型,可能需要一个额外的最小值(m)来调整零点。这些与比例因子(s)一样被量化。
请查看原始的pull request以获取所有量化水平的概述。另外,查看此pull request以获取有关使用重要性矩阵进行量化的更多信息。
第四部分:量化感知训练
在第三部分中,我们了解了如何在训练后对模型进行量化。然而,这种量化方法的一个缺点是,它没有考虑实际的训练过程。这就是量化感知训练(QAT)的用武之地。与使用训练后量化(PTQ)在模型训练完之后再进行量化不同,QAT旨在在训练过程期间中学习量化步骤。
QAT通常比PTQ更准确,因为在训练过程中已经考虑了量化。其工作原理如下:
在训练过程中,引入了所谓的“假”量化。这是首先将权重量化为例如 INT4,然后再反量化回 FP32 的过程:这个过程使得模型在训练期间能够考虑量化过程,以及损失的计算和权重的更新。
QAT 尝试探索“宽”最小值的损失状况,以最小化量化误差,因为“窄”最小值往往会导致更大的量化误差。
例如,想象一下,如果我们在反向传播过程中不考虑量化。我们根据梯度下降选择损失最小的权重。但是,如果它处于“狭窄”的最小值中,这将引入更大的量化误差。相反,如果我们考虑量化,将会选择在“宽”最小值中的不同更新权重,这样可以大大降低量化误差。
因此,尽管PTQ在高精度(例如FP32)下具有较低的损失,但QAT在低精度(例如INT4)下损失更低,这是我们所期望的。
1-bit 的LLM的时代:BitNet
如前所述,4-bit已经非常小了,但如果我们进一步减少比特位呢?
这就是BitNet的用武之地,它用1-bit表示模型的权重,每个权重使用-1或1表示。它通过将量化过程直接注入到Transformer架构中实现这一点。请记住,Transformer架构是大多数大型语言模型(LLM)的基础,由涉及线性层的计算组成:
这些线性层通常以更高精度表示,例如FP16,并且大部分权重都在这些层中。BitNet将这些线性层替换为它们称之为BitLinear的层:
BitLinear层的工作原理与普通线性层相同,基于权重乘以激活计算输出。相比之下,BitLinear层使用1-bit表示模型的权重,并使用INT8表示激活:
BitLinear层类似于量化感知训练(QAT),在训练期间执行一种“假”量化形式,以分析权重和激活量化的效果:
注意:在论文中,他们使用了γ而不是α,但因为我们在之前的例子中一直使用α,所以我使用α。此外,注意β与我们在零点量化中使用的β不同,而是表示平均绝对值。
让我们一步一步地了解BitLinear。
权重量化
在训练期间,权重以INT8存储,然后使用一种基本策略(称为符号函数)量化为1-bit。本质上,它使权重分布集中在0附近,然后将所有小于0的部分指定为-1,所有大于0的部分指定为1:
此外,它还跟踪一个β值(平均绝对值),我们稍后会在反量化中使用。
激活值量化
为了量化激活,BitLinear利用绝对最大值(absmax)量化将激活从FP16转换为INT8,因为它们需要在矩阵乘法(×)中具有更高的精度。
此外,它还跟踪α(最高绝对值),我们稍后会在反量化中使用。
反量化
我们跟踪了α(激活的最高绝对值)和β(权重的平均绝对值),因为这些值将帮助我们将激活反量化回FP16。
输出激活使用{α, γ}重新缩放,以将它们反量化到原始精度:
就是这样!这个过程相对简单,使得模型可以仅用两个值(-1或1)表示。
使用这种方法,作者观察到随着模型大小的增加,1-bit和FP16训练之间的性能差距变得越来越小。然而,这仅适用于较大的模型(>30B参数),较小模型的差距仍然很大。
所有大型语言模型都在1.58位
BitNet 1.58被引入,以改进之前提到的缩放问题。在这种新方法中,每个权重不仅仅是-1或1,还可以取值为0,形成三元化表示。有趣的是,仅添加0这一操作极大地改善了BitNet,并允许更快的计算。
0的力量:源自矩阵乘法
那么,为什么添加0会是一个重大改进呢?这与矩阵乘法的所有内容都有关系!
首先,让我们探索一下矩阵乘法的一般工作原理。在计算输出时,我们将权重矩阵乘以输入向量。下图中可视化了权重矩阵第一层的第一次乘法:
请注意,这种乘法涉及两个操作,分别是将各个权重与输入相乘,然后将它们相加。
相反,BitNet 1.58在权重三元化的情况下,无需进行乘法计算,因为三元权重本质上告诉你以下几点:
1: 我想添加这个值
0: 我不需要这个值
-1: 我想减去这个值
因此,如果权重量化到1.58 bit,你只需要进行加法操作:这种方式不仅可以显著加快计算速度,还允许特征过滤。通过将给定的权重设为0,你现在可以忽略它,而不是像1-bit表示那样,必须添加或减去权重。
量化
为了进行权重量化,BitNet 1.58使用绝对均值(absmean)量化,这是一种我们之前看到的绝对最大值(absmax)量化的变体。它简单地压缩权重分布并使用绝对均值(α)进行量化。然后将它们四舍五入为-1、0或1:
与BitNet相比,激活量化除了一点之外是相同的。它不再将激活缩放到[0, 2ᵇ⁻¹]范围,而是使用绝对最大值(absmax)量化缩放到[-2ᵇ⁻¹, 2ᵇ⁻¹]范围。
就是这样!
1.58-bit量化主要需要两个技巧:添加0以创建三元表示[-1, 0, 1];权重的绝对均值(absmean)量化。
因此,因为它们仅有1.58个计算上高效的bit,我们得到了轻量级模型!