顾名思义,大型语言模型(LLMs)通常由于体积过于庞大,难以在消费级硬件上运行。这些模型的参数量可能超过数十亿,通常需要配备大量显存(VRAM)的 GPU 来加速推理(inference)。
因此,越来越多的研究致力于通过改进训练方法、使用适配器(adapters)等手段来缩减这些模型的体积。该领域的一项核心技术被称为量化(quantization)。

本篇文章将带领大家深入了解语言模型领域的量化技术,并逐一探讨相关概念,帮助大家建立起对这一领域的直观认识。我们将一起探索不同的量化方法、实际应用场景,以及模型量化技术的基本原理。
在这份图解指南中,有超过 50 个图表,帮助各位读者更好地理解和掌握模型量化技术。
第一部分:大型语言模型(LLMs)存在的“问题”
大型语言模型(LLMs)的名称源于其包含的参数数量。如今,这类模型通常拥有数十亿个参数(主要是权重),其存储成本可能相当高昂。
在推理过程中,输入与权重相乘会产生激活值(activations),这些激活值的规模同样可能非常庞大。

因此,我们希望尽可能高效地表示这数十亿个数值,最小化给定数值所需的存储空间。
让我们从头开始,在优化数值之前,先探讨一下数值最初是如何被表示的。
如何表示数值
一个给定的数值通常被表示为浮点数(floating point number,在计算机科学中简称为 floats):即带有小数点的正数或负数。
这些数值由“比特(bits)”构成,即二进制数字。IEEE-754 标准定义了浮点数的二进制结构,由三部分组成:sign(符号位)、exponent(指数)、fraction or mantissa(尾数)。

结合这三个部分,在给定一组比特值的情况下,我们就可以计算出相应的数值:

通常,我们用来表示一个数值的比特位数越多,其精度就越高:

内存限制
使用的比特位数越多,能够表示的数值范围也就越大。

给定一种特定的表示方法,其能够涵盖的数字区间称为动态范围(dynamic range),而两个相邻数值之间的距离称为精度(precision)。

这些比特位的一个巧妙之处在于,给定一个数值,我们可以计算出设备存储其所需的内存大小。由于内存中一个字节(byte)包含 8 个比特(bits),我们可以为大多数浮点数表示形式创建一个基本公式。

注意:在实践中,推理过程中所需的(V)RAM (显)存大小还与其他诸多因素有关,例如上下文长度(context size)和模型架构。
现在我们假设有一个包含 700 亿参数的模型。大多数模型原生使用 32 位浮点数(通常称为全精度 full-precision)来表示,这就意味着仅仅将模型加载进内存就需要 280GB。

因此,尽量减少表示模型参数所需的比特数是非常有吸引力的(在训练期间也是如此!)。然而,随着精度的降低,模型的准确率通常也会随之下降。
我们希望在减少表示数值的比特数的同时保持准确率,这正是**量化(Quantization)**发挥作用的地方!
第二部分:量化简介
量化旨在将模型参数的精度从较高的位宽(如 32 位浮点数)降低到较低的位宽(如 8 位整数,INT8)。
在减少用于表示原始参数的比特数时,通常会伴随一定程度的精度(粒度)损失。
为了说明这种效果,我们可以拿一张图像作为例子,仅使用 8 种颜色来表示它:

注意放大的部分看起来比原始图像更“粗糙”,因为我们只能用更少的颜色来表示它。
量化的主要目标是在尽可能保持原始参数精度的同时,减少表示原始参数所需的比特数(颜色)。
常见数据类型
首先,让我们看看常见的数据类型,以及使用它们代替 32 位(被称为全精度或 FP32)表示所带来的影响。
FP16
让我们来看一个从 32 位到 16 位(称为半精度或 FP16)浮点数的示例:

请注意,FP16 所能表示的数值范围比 FP32 要小得多。
BF16
为了获得与原始 FP32 相似的数值范围,引入了 bfloat 16 作为“截断 FP32”的一种类型:

BF16 使用与 FP16 相同数量的比特位,但能够覆盖更广的数值范围,通常用于深度学习应用。
INT8
当我们进一步减少比特数时,就会步入基于整数(integer-based)表示而非浮点数表示的领域。例如,从 FP32 转换为仅有 8 位的 INT8 时,所需的比特数将降至原来的四分之一:

根据硬件情况,基于整数的计算可能比浮点数计算更快,但这并不总是成立。然而,使用更少的位数进行计算通常速度更快。
对于每一次位数的减少,都会执行一种映射操作来“压缩”初始的 FP32 表示到更低的比特位。
在实践中,我们不需要将整个 FP32 的范围 [-3.4e38, 3.4e38] 映射到 INT8。我们只需要找到一种方法,将我们当前数据(模型的参数)的范围映射到 INT8 即可。
常见的压缩(映射)方法包括对称量化(symmetric quantization)和非对称量化(asymmetric quantization),它们都是线性映射(linear mapping)的形式。
让我们探索这些将 FP32 量化为 INT8 的方法。
对称量化
在对称量化中,原始浮点值的范围被映射到量化空间中一个以零为中心的对称范围内。在前面的例子中,请注意量化前后的范围是如何保持以零为中心的。
这意味着浮点数空间中的零值,在量化空间中所对应的量化值恰好也是零。

对称量化的一种典型范例被称为绝对最大值(absmax)量化。
给定一组值,我们取其最大绝对值(α)作为线性映射的范围。

请注意,数值范围 [-127, 127] 表示的是受限范围。非受限范围是 [-128, 127],这取决于具体的量化方法。
由于它是一个以零为中心的线性映射,所以公式非常简单直接。
我们首先使用以下公式计算缩放因子($s$):
- $b$ 是要量化的比特数(8)
- $α$ 是最大的绝对值
然后,我们使用 $s$ 对输入 $x$ 进行量化:

将具体数值代入后,我们将得到如下结果:

要还原原始的 FP32 数值,我们可以使用先前计算的缩放因子($s$)对量化值进行反量化(dequantize)。

应用量化然后再进行反量化以还原原始值的过程如下所示:

你可以看到某些特定的值,比如 3.08 和 3.02 被分配到了同一个 INT8 值,即 36。当你对这些值进行反量化以返回到 FP32 表示时,它们失去了一些精度,变得无法区分了。
这通常被称为量化误差(quantization error),我们可以通过计算原始值与反量化值之间的差值来得出。

通常情况下,比特位数越少,我们越容易产生量化误差。
非对称量化
相比之下,非对称量化并非以零为中心对称。它将浮点数范围内的最小值($β$)和最大值($α$)映射到量化范围内的最小值和最大值。
我们即将探讨的方法被称为零点量化(zero-point quantization)

注意到 0 的位置发生了变化吗?这就是它被称为非对称量化的原因。在 [-7.59, 10.8] 这个范围内,最小值和最大值到 0 的距离是不同的。
由于位置偏移,我们必须计算出 INT8 范围的零点(zero-point)以执行线性映射。与之前一样,我们还需要计算缩放因子(s),但这次使用的是 INT8 范围([-128, 127])的差值。

请注意,这个过程稍微复杂一点,因为我们需要在 INT8 范围内计算出零点(z)来进行权重的平移(shift)。
与之前一样,让我们代入公式:

为了将量化后的 INT8 值反量化回 FP32,我们需要使用先前计算出的缩放因子($s$)和零点($z$)。
除此之外,反量化过程很简单:

当我们将对称量化和非对称量化并排放在一起时,就能迅速看出两种方法之间的区别:

请留意对称量化以零为中心的特性,对比非对称量化产生的偏移量(offset)。
范围映射与裁剪
在前面的例子中,我们探讨了如何将给定向量中的值范围映射到低位表示。尽管这允许将向量的全部值范围映射,但它存在一个主要缺点,即异常值(outliers)。
想象你有一个向量,其值如下:

注意看其中一个值比其他所有的值都大得多,它可以被视作一个异常值。如果我们对这个向量的全部范围进行映射,所有较小的值都会被映射到相同的低比特位表示中,从而失去它们的区分度:

这就是我们之前使用的 absmax 方法。请注意,如果我们不应用裁剪(clipping),同样的情况也会发生在非对称量化中。
作为替代方案,我们可以选择对某些特定值进行裁剪(clip)。裁剪为原始数值设定一个不同的动态范围,使得所有的异常值都被赋予相同的极值。
在下面的示例中,如果我们手动将动态范围设置为[-5, 5],那么所有超出该范围值的映射将无论其值如何,要么映射到-127,要么映射到 127。

其主要优势在于,非异常值的量化误差会显著减小。然而,代价是异常值的量化误差将会增加。
校准
在示例中,我展示了一种选择任意范围[-5, 5]的简单方法。选择该范围的过程称为校准,其目的是找到一个尽可能包含更多值且最小化量化误差的范围。
对于不同类型的参数,执行这一校准步骤的方法是不相同的。
权重(和偏置)
我们可以将 LLM 的权重和偏置视为静态值,因为它们在运行模型之前是已知的。例如,Llama 3 的~20GB 文件主要包含其权重和偏置。

由于偏置的数量(数百万级别)远少于权重的数量(数十亿级别),偏置通常保持在较高的精度(比如 INT16),量化的主要精力都集中在权重上。
对于静态且已知的权重,选择范围所用的校准技术包括:
- 手动选择输入范围的一个百分位数(percentile)
- 优化原始权重与量化权重之间的均方误差(MSE)
- 最小化原始值与量化值之间的熵(KL 散度)

举例来说,选择百分位数会导致与我们之前所见相似的裁剪行为。
激活值
在 LLM 运行过程中不断更新的输入部分,通常被称为“激活值(activations)”。

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

大体上,针对权重和激活值的量化方案,有两种校准方法:
- 后训练量化(PTQ):训练后量化
- 量化感知训练(QAT):训练/微调期间的量化
第三部分:训练后量化
最流行的量化技术之一是训练后量化(PTQ)。它涉及在训练模型后对模型参数(包括权重和激活值)进行量化。
权重的量化使用对称或非对称量化进行。
然而,由于我们预先不知道激活值的范围,对激活值的量化就需要对模型进行推理(inference),以此来获取其潜在的数据分布。
针对激活值的量化主要有两种形式:
- 动态量化(Dynamic Quantization)
- 静态量化(Static Quantization)
动态量化
数据通过隐藏层后,其激活值被收集:

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

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

收集到这些值后,我们可以计算执行推理时所需的 $s$ 和 $z$ 值。
在执行实际推理时,$s$ 和 $z$ 值不会重新计算,而是被全局应用于所有激活值以进行量化。
一般来说,动态量化通常更准确,因为它仅尝试为每个隐藏层计算 $s$ 和 $z$ 值。然而,这可能会增加计算时间,因为这些值需要被计算。
相比之下,静态量化准确性较低,但速度更快,因为它已经知道用于量化的 $s$ 和 $z$ 值。
4-bit 量化
实践证明,降低到 8 比特位以下的量化是一项艰巨的任务,因为随着比特位的进一步丢失,量化误差会不断攀升。幸运的是,有一些聪明的方法能够将位数降至 6 位、4 位甚至 2 位(尽管通常不建议使用这些方法降至低于 4 位)。
我们将探讨在 HuggingFace 上常见分享的两种方法:
- GPTQ(GPU 上的完整模型)
- GGUF(可能将层卸载到 CPU 上)
GPTQ
GPTQ 可以说是实践中用于将量化到 4 比特位的最知名方法之一。
它使用非对称量化,并且逐层(layer by layer)进行,即每一层都会独立处理完成后再进入下一层:

在这个逐层量化过程中,它首先计算该层权重的逆 Hessian 矩阵(inverse-Hessian)。Hessian 矩阵是模型损失函数的二阶导数,它衡量了模型输出对各权重变化的敏感程度。
简而言之,它展示了每层中各权重重要性的倒数关系(逆 Hessian 值越小,权重越重要)。
Hessian 矩阵中与较小值相关的权重更为关键,因为这些权重的微小变化可能导致模型性能发生显著变化。

在逆 Hessian 矩阵中,较低的值表示更“重要”的权重。
接下来,我们对权重矩阵的第一行进行量化和反量化:

这个过程使我们能够计算量化误差($q$),我们可以使用事先计算好的逆 Hessian($h_1$)来权衡这个误差。
本质上,我们根据权重的重要性创建了一个加权量化误差:

接下来,我们将这个加权量化误差重新分配到该行中的其他权重上。这有助于保持网络的整体功能和输出。
例如,如果我们对第二个权重,即 .3 ($x_2$) 进行量化,我们会加上量化误差 ($q$) 乘以第二个权重的逆 $Hessian$ ($h_2$)

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

我们迭代进行重新分配加权量化误差的过程,直到所有值都被量化。
这种方法之所以效果很好,是因为权重之间通常存在关联。因此,当一个权重出现量化误差时,相关的权重会相应地更新(通过逆 $Hessian$)。
注意:作者使用了一些技巧来加速计算并提高性能,例如为 Hessian 添加阻尼因子、“懒惰批处理”以及使用 Cholesky 方法预计算信息。我强烈建议你观看这个关于该主题的 YouTube 视频。
提示:如果你想要一种旨在优化性能和提高推理速度的量化方法,可以查看 EXL2。
GGUF
虽然 GPTQ 是一种在 GPU 上运行完整 LLM 的出色量化方法,但你可能并不总是具备这样的条件。相反,我们可以使用 GGUF 将 LLM 的特定层卸载(offload)到 CPU 上。
这使你能够在显存(VRAM)不足时同时利用 CPU 和 GPU。
GGUF 量化方法更新频繁,并且可能取决于位量化级别。然而,其基本原理如下。
首先,给定层的权重被分割成“超级(super)”块,每个块包含一组“子(sub)”块。从这些块中,我们提取缩放因子($s$)和最大绝对值($\alpha$):

要对给定的“子”块进行量化,我们可以使用之前使用的 absmax 量化方法。记住,它将给定的权重乘以缩放因子($s$):

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

这种分块量化使用来自“超级”块的缩放因子($s_{super}$)来量化来自“子”块的缩放因子($s_{sub}$)。
每个缩放因子的量化级别可能不同,“超级”块通常比“子”块的缩放因子具有更高的精度。
为了说明这一点,让我们探索几种量化级别(2位、4位和6位):

欲了解所有量化级别的概述,请查看原始的 pull request。此外,欲获取更多有关使用重要性矩阵(importance matrices)进行量化的信息,请查看这个 pull request。
第四部分:量化感知训练
在第三部分中,我们了解了如何在训练后对模型进行量化。这种方法的代价是量化过程没有考虑实际的训练过程。
这就是量化感知训练(Quantization Aware Training, QAT)发挥作用的地方。QAT 并不是使用训练后量化(PTQ)在模型训练完毕后再进行量化,而是旨在在训练过程中学习量化过程。

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

这个过程允许模型在训练、计算损失以及权重更新过程中考虑量化过程。
QAT 试图在损失曲面上寻找“宽阔(wide)”的极小值区域来最小化量化误差,因为“狭窄(narrow)”的极小值区域往往会导致更大的量化误差。

例如,假设我们在反向传播过程中没有考虑量化。根据梯度下降算法,我们选择具有最小损失的权重。然而,如果它位于一个“狭窄”的极小值区域内,这引入的量化误差就会很大。
相反,如果我们考虑量化,就会在一个“宽阔”的极小值区域选择一个不同的更新后权重,此时量化误差要小得多。

因此,尽管 PTQ 在高精度(例如 FP32)下的损失较低,但 QAT 在低精度(例如 INT4)下的损失更低,而这正是我们的目标。
1 比特 LLM 时代:BitNet
正如我们之前看到的那样,降至 4 位已经相当低了,但如果我们能进一步压缩呢?
这就是 BitNet 的用武之地,它将模型的权重表示为一个比特(1-bit),对于给定的权重,仅使用 -1 或 1。
它通过将量化过程直接注入到 Transformer 架构中来实现这一点。
你可能还记得,Transformer 架构被用作大多数 LLMs 的基础,它包含由线性层组成的计算:

这些线性层通常以更高的精度表示,比如 FP16,并且这是大部分权重存在的地方。
BitNet 用他们称之为 BitLinear 层取代了这些线性层:

BitLinear 层与常规的线性层工作原理相同,它根据权重乘以激活值来计算输出。
不同的是,BitLinear 层使用 1 位来表示模型的权重,使用 INT8 来表示激活值:

和量化感知训练(QAT)一样,BitLinear 层在训练期间也会执行某种形式的“伪”量化,以分析权重和激活值量化的效果:

让我们来逐步了解 BitLinear。
权重量化
在训练时,权重以 INT8 格式存储,然后使用一种称为符号(signum)函数的基本方法将其量化为 1位。
从本质上讲,它将权重分布移动到以 0 为中心,然后将所有的负值归为 -1,所有的正值归为 1:

另外,它还追踪一个值 $\beta$(平均绝对值),我们之后会用它进行反量化。
激活值量化
为了量化激活值,BitLinear 采用 absmax 量化将激活值从 FP16 转换为 INT8,由于矩阵乘法($\times$)运算的需要,它们需要保持在较高的精度中。

此外,它还跟踪最大的绝对值 $\alpha$,稍后在反量化时会用到。
反量化
我们记录了 $\alpha$(输入特征/激活值的最大绝对值)和 $\beta$(权重的平均绝对值),因为这些值将帮助我们将激活值反量化回 FP16。
输出激活通过 ${\alpha, \beta}$ 进行缩放,以此将它们反量化成原来的精度:

就是这样!这个过程相对简单,并使得模型可以用 -1 或 1 这两个值来表达。
作者发现,通过这个方法,随着模型容量的增加,1-bit 与使用 FP16 训练的模型间的性能表现差别逐渐缩小。
不过,这只适用于规模较大的模型(包含超过 300 亿参数),模型规模较小时性能表现差距还是比较大的。
所有大型语言模型都在 1.58 位
为了解决之前提到的模型规模扩展问题,人们提出了 BitNet 1.58b。
在这项新方法中,模型的每个权重不仅可以是 -1 或 1,还可以取 0,使其成为三元(ternary)权重。有趣的是,仅仅加入 0 就极大地改进了 BitNet 并且允许更快的计算。
0 的力量
为什么添加 0 会产生如此重大的改善呢?
这主要和矩阵乘法密切相关!
首先,让我们简单探讨下一般的矩阵乘法机制。计算输出结果时,我们要将权值矩阵和输入的特征向量相乘。下图展示了权重矩阵第一行的第一次乘法操作:

注意,这个乘法包含两步操作:各个权重和对应的输入数据进行相乘得出结果,并汇总全部的结果求和。
相比之下,BitNet 1.58b 成功省去了乘法运算,因为三元权重本质上只表达三种操作:
- 1: 加上这一项值
- 0: 不需要加此值
- -1: 减去这一项的值
因此,当权重量化为 1.58 位时,你只需执行加法运算:

这不仅能显著加速计算,还赋予了模型特征过滤的能力!
将某个权重设为 0,意味着直接忽略该特征,而不是像 1-bit 表示那样必须执行加法或减法。
量化
在对待实施量化的操作时,BitNet 1.58b 用了一种类似于前面的 absmax 但做了稍微转变的机制:absmean(平均绝对值) 量化法。
它对权重分布进行压缩,使用绝对均值($\alpha$)对数值进行量化,然后将结果四舍五入到 -1、0 或 1:

相比 BitNet 还有一点不一样就是在于涉及激活量化的时候。在 BitNet 1.58b 中不是将激活值缩放到 $[0, 2^{b-1}]$ 区间,而是使用 absmax 量化将其缩放到 $[-2^{b-1}, 2^{b-1}]$ 区间。
这便是全部了!可以说,1.58-bit 量化主要包含以下两项方法:
- 引入 0,构成三元表示 [-1, 0, 1]
- 对权重使用 absmean 量化
“在延迟、内存占用和能耗方面,13B 的 BitNet b1.58 比 3B 的 FP16 LLM 更高效。”
由于具备这样对运算极为有效率的仅 1.58 bits 设计特征,我们最终得以获得轻量级的模型!
结论
关于量化的话题至此暂时告一段落!希望本篇文章能够让你进一步领悟不仅是量化的本身,还有 GPTQ、GGUF 和 BitNet 的潜力。谁能知道未来的模型体积还能小到什么程度呢?!
想要阅读更多有关 LLMs 及其可视化的探讨,或对本文予以支持,欢迎看看我写的关于大型语言模型的专著!

相关资源
希望这是一次易读易懂的量化基础入门讲解!如果想要进一步深入研究,我推荐以下资源:
- HuggingFace 上一篇关于
LLM.int8()量化方法的博客:论文链接。 - HuggingFace 另一篇关于嵌入特征(embeddings)量化的博客。
- Transformer Math 101 的博客,介绍了 Transformer 计算和内存占用相关的基础数学。
- 资源一 与 资源二 是计算给定模型占用 VRAM 大小的两个绝佳网站。
- 如果想要了解更多常用于微调的量化技术 QLoRA[5],在我的新书《Hands-On Large Language Models》中有详尽介绍。
- 一个对 GPTQ 机制进行超直观解释的 YouTube 优秀视频。
- Frantar, Elias, 等人: “Gptq: Accurate post-training quantization for generative pre-trained transformers”,预印本:arXiv:2210.17323 (2022).
- 可以在 GGML 库中了解有关 GGUF 的更多情况。
- Wang, Hongyu, 等人:“Bitnet: Scaling 1-bit transformers for large language models.” arXiv 预印本文件 (2023).
- Ma, Shuming, 等人:“The era of 1-bit llms: All large language models are in 1.58 bits.” 一篇发布于 2024 年的预印本文献。
- Dettmers, Tim, 等人:“Qlora: Efficient finetuning of quantized llms.” Advances in Neural Information Processing Systems 36 (2024).
![Featured image of post 图解深度学习模型量化[转][译]](/posts/%E5%9B%BE%E8%A7%A3%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E6%A8%A1%E5%9E%8B%E9%87%8F%E5%8C%96%E8%BD%AC%E8%AF%91/e9d17077-d9af-4b37-9b9b-57ef9aaa1ca9_800x606_hu_bbf76ef3bf52e5ad.png)