Featured image of post 图解深度学习模型量化[转][译]

图解深度学习模型量化[转][译]

揭开大型语言模型的压缩之谜

本文转载翻译自A Visual Guide to Quantization

顾名思义,大型语言模型(LLMs)通常由于体积过于庞大,难以在消费级硬件上运行。这些模型的参数量可能超过数十亿,通常需要配备大量显存(VRAM)的 GPU 来加速推理(inference)。

因此,越来越多的研究致力于通过改进训练方法、使用适配器(adapters)等手段来缩减这些模型的体积。该领域的一项核心技术被称为量化(quantization)

本篇文章将带领大家深入了解语言模型领域的量化技术,并逐一探讨相关概念,帮助大家建立起对这一领域的直观认识。我们将一起探索不同的量化方法、实际应用场景,以及模型量化技术的基本原理。

在这份图解指南中,有超过 50 个图表,帮助各位读者更好地理解和掌握模型量化技术。

第一部分:大型语言模型(LLMs)存在的“问题”

大型语言模型(LLMs)的名称源于其包含的参数数量。如今,这类模型通常拥有数十亿个参数(主要是权重),其存储成本可能相当高昂。

在推理过程中,输入与权重相乘会产生激活值(activations),这些激活值的规模同样可能非常庞大。

因此,我们希望尽可能高效地表示这数十亿个数值,最小化给定数值所需的存储空间。

让我们从头开始,在优化数值之前,先探讨一下数值最初是如何被表示的。

如何表示数值

一个给定的数值通常被表示为浮点数(floating point number,在计算机科学中简称为 floats):即带有小数点的正数或负数。

这些值由“位”(或二进制数字)表示。IEEE-754 标准描述了位如何通过三种功能之一来表示该值:符号、指数或尾数(或尾数部分)。

这些数值由“比特(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。它是模型损失函数的二阶导数,告诉我们模型的输出对每个权重的变化有多敏感。

简而言之,它基本上展示了每层中每个权重的(反向)重要性。

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

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

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

GGUF

第四部分:量化感知训练

在第三部分中,我们看到如何在训练后对模型进行量化。这种方法的缺点是,这种量化不考虑实际训练过程

这就是量化感知训练(QAT)的作用所在。QAT 的目标是在训练过程中学习量化过程,而不是使用训练后量化(PTQ)在训练后将模型进行量化。

结论

All Rights Reserved.(所有权利保留。禁止未经授权的复制或再分发。)
使用 Hugo 构建
主题 StackJimmy 设计