程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

LLM量化背后的概念、方法、应用和原理

balukai 2025-07-23 13:07:41 文章精选 5 ℃

原文:https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-quantization

大型语言模型 (LLMs) 由于参数数量巨大,无法在消费类硬件上运行。这些模型可能超过数十亿个参数,通常需要具有大量 VRAM 的 GPU 来加速推理。

因此,越来越多的研究集中在通过改进训练、适配器等来缩小这些模型的大小。该领域的一项主要技术称为量化

这篇文章通过语言建模来介绍量化领域,逐步讲解其概念、方法、应用和原理,帮助大家更好地理解。

第 1 部分: LLMs 的“问题”

大型语言模型(LLMs)因其包含大量参数而得名。如今,这些模型通常拥有数十亿个参数(主要是权重),存储成本相当高。

在推理过程中,激活是输入和权重的乘积,也可能非常大。

因此,我们希望高效表示这些数十亿个值,以最大限度地减少存储空间。

首先,了解数值的基本表示方法,然后再探讨优化策略。

如何表示数值

数值通常表示为浮点数,即带小数点的正数或负数。

这些值由“位”或二进制数字表示。根据IEEE-754标准,这些位可以表示三个部分:符号、指数和尾数(或分数)。

这三个部分共同决定了特定位值所代表的数值:

用来表示一个值的位数越多,通常它就越精确。

内存限制

位数越多,可以表示的值范围就越大。

给定表示可以采用的可表示数字的间隔称为动态范围,而两个相邻值之间的距离称为精度。

我们可以计算出设备需要多少内存来存储一个值。由于内存中的一个字节有8位,因此我们可以为大多数形式的浮点表示创建一个基本公式。

注意:

在实践中,推理期间所需的(V)RAM量还取决于上下文大小和架构。

现在假设我们有一个包含700亿参数的模型。大多数模型使用32位浮点(全精度)表示,仅加载模型就需要280GB内存。

因此,减少表示模型参数的位数是非常重要的。

然而,随着精度的降低,模型的准确性通常也会降低。

我们希望在保持准确性的同时减少表示值的位数,这就是量化的用武之地!

第 2 部分:量化简介

量化的目的是将模型参数的精度从较高位宽(如32位浮点)降低到较低位宽(如8位整数)。

减少表示参数的位数通常会导致一些精度(或粒度)丧失。

为说明这一点,我们可以将任何图像用8种颜色表示:


注意:

放大的部分看起来比原始部分更有“颗粒感”,因为使用的颜色更少。

常见数据类型

首先,我们来看看常见的数据类型,以及使用它们而不是32位(全精度或FP32)表示形式的影响。

FP16

来看一个将32位浮点数(FP32)转换为16位浮点数(FP16)的示例:

请注意,FP16 的取值范围比 FP32 小很多。

BF16

为了获得与原始 FP32 类似的值范围,引入了 bfloat 16(BF16)作为“截断的 FP32”类型:

BF16 使用与 FP16 相同的位数,但可以表示更广泛的值,因此通常用于深度学习应用中。

INT8

进一步减少位数时,我们转向基于整数的表示形式,而不是浮点数。将 FP32 转换为只有 8 位的 INT8,结果是原始位数的四分之一:

根据硬件的不同,基于整数的计算可能比浮点计算更快,但这并不总是如此。然而,使用较少的位时,计算通常会更快。

每减少一位,都会进行映射,将初始 FP32 表示“压缩”到较低位。

实际上,我们不需要将整个 FP32 范围 [-3.4e38, 3.4e38] 映射到 INT8。只需要找到一种方法,将模型参数的数据范围映射到 INT8。

常见的压缩/映射方法有对称和非对称量化,通常是线性映射。

让我们探索一下从FP32到INT8的量化方法。

对称量化

在对称量化中,原始浮点值的范围被映射到量化空间中以零为中心的对称范围。在前面的示例中,可以看到量化前后的范围如何保持以零为中心。

这意味着,浮点空间中的零值在量化空间中也恰好对应为零。

对称量化的一个常见例子是绝对最大(absmax)量化。

给定一个值列表,我们使用最大绝对值 α 作为线性映射的范围。

注意:

[-127, 127] 是限制范围的值范围,而无限制范围为 [-128, 127],具体取决于量化方法。

由于对称量化是以零为中心的线性映射,因此公式非常简单。

首先,我们使用以下方法计算比例因子:

  • b 是我们要量化为的字节数(例如8位),
  • α 是最大绝对值,

然后,我们使用比例因子s来量化输入x

填写这些值后,我们得到以下结果:

为了检索原始 FP32 值,我们可以使用之前计算的缩放因子对量化值进行反量化。

应用量化和反量化过程来恢复原始值,如下所示:

可以看到,一些值,例如 3.08 和 3.02,被量化为 INT8 中的 36。当这些值反量化回 FP32 时,它们会丢失一些精度,变得不可区分。

这种现象通常被称为量化误差,我们可以通过计算原始值和反量化值之间的差异来量化它。

一般来说,位数越低,量化误差就越大。

非对称量化

相对而言,非对称量化在零附近是不对称的。它将浮点数范围的最小值 β 和最大值 α 映射到量化范围的最小值和最大值。

我们要探索的具体方法称为零点量化

注意到零的位置发生了移动吗?这就是为什么它被称为非对称量化。最小/最大值到零的距离不同,范围为 [-7.59, 10.8]。

由于零位置的偏移,我们必须计算 INT8 范围的零点以进行线性映射。同时,我们还需计算比例因子,但使用的是 INT8 范围的差值 [-128, 127]。

注意:

由于需要计算 INT8 范围内的零点 z 以移动权重,这个过程会复杂一些。

接下来,我们填写公式:

要将量化后的 INT8 值反量化回 FP32,我们需要使用之前计算的比例因子 s 和零点 z

反量化过程相对简单:

将对称量化和非对称量化进行对比,可以清晰地看到这两种方法的差异:

注意对称量化的零中心性质与非对称量化的偏移量。

范围映射和裁剪

在前面的示例中,我们探讨了如何将给定向量的值范围映射到较低位的表示。虽然这种映射可以覆盖整个向量的范围,但它有一个主要缺点:异常值

假设我们有一个包含以下值的向量:

注意其中一个值比所有其他值大得多,这可以被视为异常值。

如果我们映射整个向量的范围,那么所有较小的值会被映射到相同的低位表示,从而丧失它们的区分度:

这就是我们之前使用的 absmax 方法的效果。如果不应用裁剪,非对称量化也会产生类似的行为。

另一种方法是选择裁剪某些值。裁剪涉及将原始值的动态范围限制到一定范围内,使得所有超出范围的异常值都映射到相同的值。

例如,如果我们手动将动态范围设置为 [-5, 5],那么该范围之外的所有值都会被映射到 -127 或 127,无论其具体值是多少:

主要优点是非异常值的量化误差显著降低。然而,异常值的量化误差会增加。

校准

在示例中,我展示了一种选择任意范围 [-5, 5] 的简单方法。选择此范围的过程称为校准,目的是找到一个包含尽可能多值的范围,同时最小化量化误差

执行此校准步骤并不适用于所有类型的参数。

权重(和偏差)

LLM 的权重和偏差通常被视为静态值,因为它们在模型运行之前是已知的。例如,Llama 3 的约 20GB 文件主要由其权重和偏差组成。

由于偏差(数百万)比权重(数十亿)少得多,偏差通常保持在更高的精度(例如 INT16),而量化的主要精力集中在权重上。

对于静态且已知的权重,选择范围的校准技术包括:

  • 手动选择输入范围的百分位数
  • 优化原始权重和量化权重之间的均方误差 (MSE)
  • 最小化原始值和量化值之间的熵(KL 散度)

例如,选择一个百分位数会导致类似我们之前看到的裁剪行为。

激活

在整个 LLM 中不断更新的输入通常称为“激活”。

这些值称为激活,因为它们通常通过一些激活函数(如 sigmoid 或 ReLU)进行处理。

与权重不同,激活在推理过程中随着每个输入的变化而变化,这使得准确量化它们变得具有挑战性。

由于这些值在每个隐藏层之后更新,我们只能在输入数据通过模型时观察它们的变化情况。

一般来说,校准权重和激活的量化方法有两种:

  • o 训练后量化(PTQ)
  • o 量化感知训练(QAT):在训练/微调期间进行量化

第 3 部分:训练后量化

最流行的量化技术之一是训练后量化(PTQ)。它涉及在训练模型后对模型的参数(权重和激活)进行量化。

使用对称或非对称量化来执行权重的量化。

然而,激活的量化需要通过推理过程获得它们的潜在分布,因为我们不知道它们的范围。

激活的量化有两种形式:

  • Dynamic Quantization(动态量化)
  • Static Quantization(静态量化)

Dynamic Quantization 动态量化

在动态量化中,数据通过隐藏层后,将收集其激活:

然后,使用该激活分布来计算量化输出所需的零点 z 和比例因子 s 值:

每次数据通过新层时都会重复该过程。因此,每一层都有自己单独的 zs 值,因此有不同的量化方案。

静态量化

与动态量化相反,静态量化在推理过程之外预先计算零点 z 和比例因子 s

为了找到这些值,使用校准数据集并将其提供给模型以收集这些潜在分布:

收集这些值后,我们可以计算在推理过程中执行量化所需的 sz 值。

在实际推理时,不会重新计算 s 和 z 值,而是在所有激活中全局使用它们来量化。

一般来说,动态量化通常更准确一些,因为它计算每个隐藏层的 sz 值。然而,这可能会增加计算时间。

相反,静态量化虽然不如动态量化准确,但速度更快,因为它已经预先计算了用于量化的 sz 值。

4 位量化领域

低于 8 位的量化是一项艰巨的任务,因为量化误差随着每一位的丢失而增加。

幸运的是,有几种方法可以将位减少到 6 位、4 位,甚至 2 位(尽管通常不建议使用这些方法将位减少到低于 4 位)。

我们将探讨 HuggingFace 上常见的两种方法:

  • GPTQ(GPU 上的完整模型)
  • GGUF(可能卸载到 CPU 上的层)

GPTQ

GPTQ 是实践中用于量化至 4 位的最著名方法之一。

它使用非对称量化并逐层进行,以便在继续下一层之前独立处理每一层:

在这个逐层量化过程中,它首先将层的权重转换为逆 Hessian 矩阵。

逆 Hessian 矩阵是模型损失函数的二阶导数,表明模型的输出对每个权重的变化有多敏感。

简单来说,它展示了层中每个权重的(逆)重要性。

与 Hessian 矩阵中较小值相关的权重更为重要,因为这些权重的微小变化可能会导致模型性能的显著变化。

在逆 Hessian 矩阵中,较低的值表示更“重要”的权重。

接下来,我们对权重矩阵中第一行的权重进行量化和反量化:

这个过程允许我们计算量化误差 q ,并使用预先计算的逆 Hessian 矩阵 h1 来衡量该误差。

本质上,我们根据权重的重要性创建加权量化误差:

接下来,我们将这个加权量化误差重新分配到该行中的其他权重上。这可以保持网络的整体功能和输出。

例如,如果我们对第二个权重(即 0.3(x_2)$执行此操作,我们将添加量化误差 $q 乘以第二个权重 h2 的逆 Hessian 矩阵:

对给定行中的第三个权重执行相同的过程:

我们迭代重新分配加权量化误差的过程,直到所有值都被量化。

这种方法非常有效,因为权重通常是相互关联的。因此,当一个权重存在量化误差时,相关权重会相应更新(通过逆Hessian)。

注意:作者使用了多种技巧来加速计算和提高性能,例如向 Hessian 添加阻尼因子、“惰性批处理”以及使用 Cholesky 方法预计算信息。

提示:如果您想要一种旨在优化性能和提高推理速度的量化方法,请查看EXL2。

GGUF

虽然 GPTQ 是一种很好的量化方法,可以在 GPU 上运行完整的 LLM,但有时您可能没有足够的 GPU 资源。这时,可以使用 GGUF 将 LLM 的任何层卸载到 CPU,以便在没有足够 VRAM 的情况下同时使用 CPU 和 GPU。

量化方法 GGUF 经常更新,可能取决于位量化的级别。然而,一般原则如下:

首先,给定层的权重被分成“超级”块,每个“超级”块包含一组“子”块。从这些块中,我们提取比例因子 sα

为了量化给定的“子”块,我们可以使用之前使用过的 absmax 量化。它将给定权重乘以比例因子:

比例因子是使用来自“子”块的信息计算的,但使用来自“超级”块的信息进行量化,“超级”块有自己的比例因子:

这种按块量化使用“超级”块中的比例因子 (s_super) 来量化“子”块中的比例因子 (s_sub)。

每个比例因子的量化水平可能不同,“超级”块通常比“子”块的比例因子具有更高的精度。

为了说明这一点,我们来探讨几个量化级别(2 位、4 位和 6 位):

注意: 根据量化类型,需要额外的最小值 (m) 来调整零点。它们的量化方式与比例因子相同。

第 4 部分:量化感知训练

在第 3 部分中,我们探讨了训练后量化(PTQ)。这种方法的缺点是量化过程没有融入模型的训练阶段。

这时,量化感知训练(QAT)就显得尤为重要。QAT 的目标是在训练过程中就学习如何进行量化,而不是在训练完成后再对模型进行量化。

QAT 往往比 PTQ 更准确,因为它在训练阶段就考虑了量化的影响。其工作原理如下:

在训练过程中,引入了所谓的“假”量化。这个过程包括将权重量化为 INT4,然后再反量化回 FP32。

这个过程允许模型在训练期间考虑量化的影响,从而优化损失计算和权重更新。

QAT 试图探索“宽”最小值的损失情况,以最大限度地减少量化误差。相反,“窄”最小值往往会导致更大的量化误差。

例如,假设我们在反向传播过程中忽略了量化。如果我们仅根据梯度下降来选择损失最小的权重,那么可能会进入一个“窄”最小值,这通常会引入较大的量化误差。

而如果我们在训练过程中考虑了量化,那么模型会选择在“宽”最小值中进行更新,这样可以显著降低量化误差。

因此,尽管 PTQ 在高精度(如 FP32)下具有较低的损失,QAT 在较低精度(如 INT4)下通常能实现更低的损失,这正是我们希望达到的目标。

1位时代LLMs:BitNet

如前所述,4 位已经相当小了,但如果我们进一步减少呢?

这就是 BitNet 的用途,它通过将模型的单个权重表示为 1 位,使用 -1 或 1 作为给定权重。它通过将量化过程直接嵌入到 Transformer 架构中来实现这一点。

Transformer 架构是许多 LLMs 的基础,其计算涉及线性层:

这些线性层通常以更高的精度表示,例如 FP16,并且是大多数权重所在的位置。

BitNet 通过一种称为 BitL Linear 的方法来替换这些线性层:

BitLinear 层的工作方式与常规线性层相同,也是通过将权重乘以激活来计算输出。

然而,与传统的线性层不同,BitLinear 层使用 1 位表示模型的权重,同时使用 INT8 表示激活。

BitLinear 层在训练期间进行“假”量化,以评估权重和激活的量化效果。

请注意:在论文中,他们使用 γ 代替 α,但由于我们在整个示例中使用了 α,因此我使用了 α。同时,β 与我们在零点量化中使用的 β 不同,它表示平均绝对值。

让我们逐步了解 BitLinear。

Weight Quantization 权重量化

在训练过程中,权重首先以 INT8 格式存储,然后使用一种称为 Signum 函数的基本策略量化为 1 位。

这种方法将权重分布调整为以 0 为中心,然后将所有低于 0 的值分配为 -1,将所有高于 0 的值分配为 1:

此外,它还跟踪一个值 β(平均绝对值),该值将在反量化时使用。

Activation Quantization 激活量化

对于激活的量化,BitLinear 使用 absmax 量化将激活从 FP16 转换为 INT8,因为矩阵乘法 (×) 需要更高的精度。

此外,它还跟踪 α(激活的最高绝对值)和 β(权重的平均绝对值),这些值将在反量化过程中使用。

反量化

我们跟踪 α(激活的最高绝对值)和 β(权重的平均绝对值),因为这些值将帮助我们将激活反量化回 FP16。

输出激活用 {α, β} 重新缩放,以将它们反量化到原始精度:

该过程相对简单,并允许模型仅用两个值(-1 或 1)进行表示。

作者观察到,随着模型规模的增加,1 位训练和 FP16 训练之间的性能差距逐渐缩小。

然而,这一现象仅适用于较大的模型(参数数量超过 30 亿),对于较小的模型,这一差距仍然较为显著。

所有大型语言模型均为 1.58 位

BitNet 1.58b 的引入旨在改善之前提到的扩展性问题。

在这一新方法中,模型的每个权重不仅限于-1或1,还引入了0作为可能的值,从而形成了三元权重。这一变化显著提升了 BitNet 的性能,并加快了计算速度。

0的力量

那么,为什么添加 0 会带来如此显著的改进呢?

这与矩阵乘法的过程密切相关!

首先,我们需要理解矩阵乘法的基本原理。在计算输出时,我们将权重矩阵与输入向量相乘。以下是权重矩阵第一层的第一次乘法的可视化示例:

请注意,这个乘法过程包括两个步骤:首先将每个权重与输入值相乘,然后将所有结果相加。

相比之下,BitNet 1.58b 成功地简化了这一过程,因为三元权重本质上提供了以下信息:

  • 1:我希望添加这个值
  • 0:我不需要这个值
  • -1:我希望减去这个值

因此,当权重量化为 1.58 位时,我们只需执行加法操作:

这种方法不仅显著加快了计算速度,还支持特征过滤。

通过将权重设置为 0,您可以选择忽略这些权重,而不是像 1 位表示中那样对权重进行加法或减法操作。

量化

为了实现权重量化,BitNet 1.58b 采用了 absmean 量化方法,这是对之前介绍的 absmax 量化的一种变体。

这种方法通过压缩权重分布,并使用绝对平均值 α 来量化权重值。然后,将这些值四舍五入为 -1、0 或 1:

除了一个方面,激活量化与权重量化的过程相似。

激活量化不再将激活值缩放到范围 [0, 21],而是缩放到 [-21, 21],使用的是 absmax 量化方法。

总结一下,1.58 位量化主要依赖两个技巧:

  • 添加 0 以创建三元表示 [-1, 0, 1]
  • 使用权重的绝对均值进行量化

“在延迟、内存使用和能耗方面,13B BitNet 1.58 比 3B FP16 LLM 更高效。”

最终,我们得到了一个轻量级模型,因为只有 1.58 位的计算有效位!

结论

我们的量化探索到此为止!希望这篇文章能帮助您更好地理解量化、GPTQ、GGUF 和 BitNet 的潜力。

未来的模型会变得多小呢?让我们拭目以待!

最近发表
标签列表