量化可视化指南揭秘:大型语言模型的压缩
量化技术图解指南
深入浅出,揭秘大语言模型的压缩之道
所谓大语言模型(LLM),“大”是其最显著的特征之一。这不仅体现在它们能够处理海量信息,更直接地体现在其庞大的体积上。这些模型动辄拥有数十亿的参数,通常需要配备大显存的顶级 GPU 才能流畅运行,这无疑将许多普通用户和开发者拒之门外。
因此,学术界和工业界正不懈探索如何为这些庞然大物“瘦身”,例如改进训练方法、引入适配器(Adapters)技术等。而在众多技术中,有一项关键技术扮演着举足轻重的角色,那就是——量化(Quantization)。

在本文中,我将以大语言模型为背景,为您详细解读量化技术。我们将通过一系列直观的图示,逐一剖析相关概念、方法论、应用案例及其背后的核心原理。
这份图解指南包含超过 50 张原创图示,旨在帮助您建立对量化技术的直观理解!
第一部分:大语言模型的“大”问题
大语言模型的“大”,体现在其海量的参数上。如今,模型的参数(主要是权重)规模已达数十亿级别,存储成本极其高昂。
不仅如此,在模型推理(即生成内容)时,输入数据与权重相乘产生的激活值(activations),其规模同样不容小觑。

因此,我们迫切需要一种更高效的方式来表示这数十亿个数值,从而最大限度地压缩它们的存储空间。
要理解如何优化,我们得先从数值的表示方法说起。
数值是如何表示的?
在计算机中,数值通常以**浮点数(float)**的形式存在,也就是我们熟悉的小数。
这些数值在底层是由“位(bits)”或二进制数字来表示的。国际标准 IEEE-754 定义了如何用位来组合成三个关键部分,共同构成一个浮点数:符号(sign)、指数(exponent)和小数(fraction)(也称尾数 mantissa)。

这三部分通过以下公式共同决定了浮点数的最终值:

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

内存的限制
可用的位数越多,能够表示的数值范围也越广。

一种表示法所能覆盖的数值区间,我们称之为动态范围(dynamic range);而两个相邻数值之间的最小间隔,则代表了其精度(precision)。

利用“位”,我们可以方便地计算出存储一个数值需要多少内存。因为 1 字节(byte)等于 8 位(bits),所以我们可以得出一个简单的换算公式。

注意:实际上,影响推理所需显存(VRAM)的因素还有很多,比如上下文长度和模型架构。此处的计算只是一个简化的估算。
现在,我们来算一笔账。假设有一个 700 亿参数的开源模型,它原生使用 32 位浮点数(即全精度)进行存储。这意味着,仅仅是把模型加载到内存中,就需要 280GB!

这个数字对于绝大多数消费级硬件来说是遥不可及的。因此,人们自然会想到,能否减少表示模型参数的位数,从而降低内存占用?然而,降低精度往往会牺牲模型的性能。
那么,有没有一种方法,既能减少位数,又能最大程度地保留模型的性能呢?答案就是量化。
第二部分:量化技术简介
量化的核心目标,就是将模型参数的表示精度从高位宽(如 32 位浮点数)降低到低位宽(如 8 位整数)。

当我们用更少的位数来表示原始参数时,通常会损失一部分精度,让表示变得“粗粒度”。
为了直观理解这个过程,我们可以把它比作图像处理:想象一下,我们把一张色彩丰富的图片,强制用 8 种颜色来重新绘制。
图片改编自 Slava Sidorov 的原作。
不难发现,放大后的局部细节变得“粗糙”了许多,因为我们可用的颜色(精度)变少了。
量化的艺术,就在于找到一种最佳的“压缩”方法,在减少位数(颜色)的同时,尽可能地保留原始参数(画面)的精髓。
常见的数据类型
首先,我们来认识几种常见的数据类型,看看用它们替代 32 位浮点数(全精度 或 FP32)会发生什么。
FP16
从 32 位浮点数(FP32)转换到 16 位浮点数(半精度 或 FP16),存储占用减半。

注意:FP16虽然节省了空间,但其动态范围比FP32小得多,这意味着它更容易出现数值溢出(过大)或下溢(过小)的问题。
BF16
为了在保持 16 位宽度的同时,获得与 FP32 相似的动态范围,业界引入了 bfloat16 (BF16)。它就像一个“截短版”的 FP32,保留了与 FP32 相同的指数位数,牺牲了部分小数精度。

解读:BF16 和 FP16 都用 16 位,但 BF16 的动态范围更大,更不容易溢出,而 FP16 的精度更高。在深度学习中,动态范围往往比极致的精度更重要,因此 BF16 越来越受欢迎。
INT8
当我们继续压缩,就进入了整数表示的领域。例如,从 FP32 转换到 8 位整数 INT8,位数直接减少到原来的四分之一。

优势:整数运算在某些硬件上通常比浮点数运算更快。更少的位数本身也意味着更快的数据传输和计算速度。
每次降低位数,我们都需要进行一次映射(Mapping),将原始的 FP32 数值“挤”进更小的表示空间里。实际上,我们并不需要将 FP32 [-3.4e38, 3.4e38] 的整个庞大范围都映射过去,只需要覆盖模型参数实际分布的范围即可。
最常见的两种线性映射方法是对称量化和非对称量化。
对称量化
在对称量化中,原始浮点值的范围被映射到一个关于零点对称的量化空间中。这意味着,浮点数中的 0,在量化后也精确地对应 0。

一个经典的对称量化方法是绝对值最大值(absmax)量化。
我们只需找到原始数值中的绝对值最大值(α),并以此作为边界来进行线性映射。

[-127, 127]是 INT8 对称量化常用的范围,它舍弃了 -128 以确保范围关于 0 完全对称。
这个过程的数学公式非常直观。首先,我们计算缩放因子(s):
- b 是目标位数(这里是 8)。
- α 是绝对值最大值。
然后,用 s 来量化输入 x:

我们来实际计算一下:

为了从量化值恢复到原始浮点数,我们可以使用相同的缩放因子(s) 进行反量化 。

量化后再反量化的完整过程如下:

可以看到,像 3.08 和 3.02 这样不同的原始值,都被映射到了同一个整数 36。当它们被反量化回浮点数时,丢失的精度无法恢复,两者变得无法区分。
原始值与反量化值之间的差异,就是量化误差。

通常,量化的位数越低,量化误差就越大。
非对称量化
与对称量化不同,非对称量化并不强求映射范围关于零点对称。它将原始浮点数范围的最小值(β) 和最大值(α),精确地映射到量化范围的两个端点。这种方法也称为** 零点量化(zero-point quantization)**。

核心区别:注意看,原始值中的 0 被映射到了量化空间中的 -45,不再是 0。这个偏移,就是非对称量化的关键。当原始数据的分布本身就是偏斜的(例如,所有值都是正数),非对称量化能更充分地利用有限的量化空间,从而可能获得更小的量化误差。
由于存在偏移,我们需要计算一个零点(zero-point, z) 来辅助线性映射。缩放因子(s) 的计算方式也略有不同,它利用了 INT8 的完整范围 [-128, 127]。

我们代入数值计算一下:

反量化时,我们需要同时使用**缩放因子(s)和零点(z)**来复原数值。

将两者并排比较,区别一目了然:

范围映射与裁剪(Clipping)
前面的例子中,我们都将原始数值的完整范围(min ถึง max)映射到了量化空间。这种做法虽然能覆盖所有值,但有一个致命弱点——离群值(outliers)。
想象一下下面这个向量:

其中有一个值(10.8)远大于其他所有值,是个典型的离群值。如果我们直接按 [-7.59, 10.8] 这个范围进行映射,那么大部分集中在 [-1, 4] 区间的“正常”值,都会被挤压到量化空间中一个很小的区域内,从而失去区分度,导致巨大的精度损失。

解读:这就像用一把最大刻度为 100 米的尺子去量一堆长度在 1 厘米左右的物体,其中只有一个是 10 米长。为了迁就那个 10 米的大家伙,所有 1 厘米的物体在尺子上看起来几乎没有差别。
为了解决这个问题,我们可以选择**裁剪(clip)**离群值。我们设定一个更合理的动态范围,将所有超出该范围的数值都视为边界值。
例如,如果我们手动将动态范围设为 [-5, 5],那么所有小于 -5 的值都会被当作 -5,所有大于 5 的值都会被当作 5,然后再进行量化。

权衡:这样做的代价是离群值的量化误差变大了,但好处是,绝大多数“正常”值的量化误差显著降低,整体性能往往更好。
校准(Calibration)
如何确定 [-5, 5] 这个最佳的裁剪范围呢?这个寻找最佳动态范围的过程,就叫做校准(calibration)。其目标是找到一个既能覆盖绝大多数数值,又能使整体量化误差最小化的黄金范围。
对不同类型的参数,校准方法也有所不同。
权重(和偏置)
LLM 的权重和偏置在模型加载后是静态的、已知的。例如,我们下载的 Llama 3 模型文件,其内容主要就是这些固定的权重和偏置。

对于这些静态值,我们可以从容地分析它们的分布,并选择合适的校准技术来确定量化范围,例如:
- 百分位数法:简单粗暴,直接取权重的某个百分位(如 99.9%)作为边界。
- 均方误差(MSE)最小化:搜索一个范围,使得原始权重与量化后权重的均方误差最小。
- KL 散度最小化:寻找一个范围,使得原始分布与量化后分布的差异(用 KL 散度衡量)最小。

激活值
与权重不同,激活值是动态的,它们是模型在处理输入数据时实时生成的中间结果。

因为不同的输入会产生完全不同的激活值,我们无法事先知道它们的分布。

总的来说,根据执行量化的时机,主要分为两大流派:
- 训练后量化(Post-Training Quantization, PTQ):在模型已经训练完成之后进行量化。
- 量化感知训练(Quantization Aware Training, QAT):在模型训练或微调的过程中就引入量化。
第三部分:训练后量化 (PTQ)
PTQ 是目前最流行、最便捷的量化技术。它无需重新训练,直接对训练好的模型进行处理。
对于权重,由于其静态特性,我们可以直接使用之前提到的校准方法进行量化。
而对于激活值,PTQ 又分为两种策略:
- 动态量化
- 静态量化
动态量化
动态量化是一种“即用即算”的策略。在推理时,每当一批数据流经模型的一层,系统会实时收集这一批数据产生的激活值,并为它们计算出专属的缩放因子(s)和零点(z),然后进行量化。

特点:精度高,因为量化参数是为每一批输入“量身定制”的。但缺点是,实时计算这些参数会带来额外的计算开销,可能影响推理速度。
静态量化
静态量化则是一种“未雨绸缪”的策略。它不是在实时推理时计算量化参数,而是在此之前就准备好。
具体做法是,我们先准备一个小的校准数据集(例如几百个样本),让它流过整个模型,并记录下每一层激活值的典型分布范围。

根据这些记录,我们为每一层计算出一组合适的、固定的缩放因子(s)和零点(z)。在之后的实际推理中,无论输入什么数据,都直接使用这套预先算好的参数进行量化。
特点:速度快,因为省去了实时计算量化参数的开销。但精度可能略低于动态量化,因为固定的参数不一定能完美适配所有不同输入的激活值分布。
挺进 4 比特时代
将量化位数降至 8 位以下(如 4 位、2 位)极具挑战性,因为精度损失会急剧增加。幸运的是,研究者们发明了一些巧妙的方法来应对这一挑战。
我们将介绍两种在 HuggingFace 社区非常流行的 4 位量化方案:
- GPTQ(通常要求整个模型在 GPU 上运行)
- GGUF(设计用于在 CPU 和 GPU 之间灵活分配模型层)
GPTQ
GPTQ 是目前最高效、最精准的 PTQ 4 位量化方法之一。
它的核心思想是逐层量化,并巧妙地在量化过程中进行误差补偿。

对于每一层,GPTQ 首先计算其权重的逆海森矩阵(inverse-Hessian)。这是一个复杂的数学概念,但我们可以将其直观地理解为一个衡量每个权重“重要性”的指标。逆海森矩阵中值越小的权重,对模型输出的影响越大,也就越“重要”。

接下来,GPTQ 逐行(或逐列)对权重进行量化。当它量化完一个权重后,会立刻计算出量化误差。然后,它并不会忽略这个误差,而是根据逆海森矩阵,将这个误差按“重要性”的比例“补偿”给本层中还未被量化的其他权重。

核心思想:这个过程就像一个团队合作。当一个成员(权重)因为量化而“牺牲”了部分精度时,其他相关的成员会主动承担起一部分责任,进行微调,以确保整个团队(模型层)的最终输出尽可能地接近原始状态。
GGUF
GGUF 是一种专为 llama.cpp 项目设计的文件格式,其最大的特点是能够让模型的一部分在 GPU 上运行,另一部分在 CPU 上运行。这对于显存不足的用户来说是巨大的福音。
GGUF 内置了一套独特的量化方案,其核心是分块量化。它将一层的权重分成多个“超级块”,每个“超级块”又包含若干“子块”。

它的巧妙之处在于嵌套量化:
- 首先,对每个“子块”内的权重进行量化。这个过程需要一个缩放因子。
- 然后,它并不会以高精度存储这些缩放因子,而是用“超级块”的量化参数,对这些“子块”的缩放因子再次进行量化。

这就像一个两级压缩系统,通过对元数据(缩放因子)本身进行压缩,实现了极致的存储优化。GGUF 提供了多种量化级别(如 Q4_K_M, Q5_K_S 等),它们在块大小、缩放因子精度等方面各有不同,为用户在模型大小和性能之间提供了丰富的选择。
第四部分:量化感知训练 (QAT)
PTQ 虽然方便,但它毕竟是在模型“成型”后做的“补救”措施,量化过程与模型的训练是脱节的。
量化感知训练(QAT)则从根源上解决了这个问题。它在模型训练或微调的过程中就模拟量化的影响,让模型“学着”去适应低精度环境。

QAT 的核心是引入“伪量化”(fake quantization)节点。在训练的正向传播中,它会将高精度的权重(如 FP32)先量化到低位宽(如 INT4),然后再立刻反量化回 FP32。

目的:这个“量化 -> 反量化”的操作,将量化误差提前引入了训练过程。模型在计算损失和更新权重时,就能“看到”量化带来的影响,从而主动调整参数,学习如何在一个对量化误差更“不敏感”的区域达到最优。
我们可以把损失函数想象成一个山谷。常规训练可能会找到一个非常“尖锐”的谷底(窄最小值),这里的性能最好,但稍有扰动(如量化误差),性能就会急剧下降。而 QAT 会引导模型去寻找一个更“平坦开阔”的谷底(宽最小值),即使存在量化误差,性能也依然稳健。

因此,QAT 通常能获得比 PTQ 更好的低位宽量化性能,但代价是需要额外的训练或微调成本。
1位LLM的时代:BitNet
我们之前看到的 4 位量化已经相当小了,还能进一步压缩吗?
这就是 BitNet 的切入点,它将模型的权重用单个 1 位来表示,每个权重的值要么是 -1 要么是 1。
它通过将量化过程直接注入到 Transformer 架构中来实现这一点。
我们知道,Transformer 架构是大多数 LLM 的基础,它由涉及线性层 的计算组成:

这些线性层通常以较高精度(如 FP16)表示,并且是模型中大部分权重所在的位置。
BitNet 将这些线性层替换为一种他们称之为 BitLinear 的结构:

BitLinear 层的工作方式与常规线性层相同,都是基于权重与激活值的乘积来计算输出。
不同之处在于,BitLinear 层用 1 位表示权重,用 INT8 表示激活值:

与量化感知训练(QAT)类似,BitLinear 层在训练期间执行一种“伪量化”,以分析权重和激活值量化的影响:

注意:在原论文中他们使用了 γ 而不是 α,但由于我们在整个示例中都使用了 α,所以这里我也用它。另外,请注意这里的 β 与我们在零点量化中使用的不同,这里指的是平均绝对值。
让我们一步步地解析 BitLinear 的过程。
权重 量化
在训练期间,权重以 INT8 格式存储,然后使用一种称为符号函数(signum function) 的基本策略量化为 1 位。
本质上,它将权重分布移动到以 0 为中心,然后将所有小于 0 的值赋为 -1,大于 0 的值赋为 1:

此外,它还跟踪一个值 β(平均绝对值),我们之后将用它来进行反量化。
激活值量化
为了量化激活值,BitLinear 利用 absmax 量化 将激活值从 FP16 转换为 INT8,因为它们在进行矩阵乘法(×)时需要有更高的精度。

此外,它还跟踪 α(最大绝对值),我们之后将用它来进行反量化。
反量化
我们跟踪了 α(激活值的最大绝对值)和 β(权重的平均绝对值),因为这些值将帮助我们将激活值反量化回 FP16。
输出的激活值会用 {α, β} 进行重新缩放,以将它们反量化回原始精度:

就是这样!这个过程相对直接,使得模型可以用 -1 或 1 这两个值来表示。
通过这个过程,作者们观察到,随着模型规模的增长,1 位模型和 FP16 训练的模型之间的性能差距会越来越小。
然而,这仅适用于超大模型(>300亿参数),对于较小的模型,性能差距仍然相当大。
LLM 的终极形态?1.58 比特
为了解决 BitNet 在中小规模模型上性能不佳的问题,研究者们提出了 BitNet b1.58。
这个名字里的 “1.58” 是一个信息论上的计算结果,它代表了一个三元(ternary) 系统。也就是说,每个权重的值可以是 -1、0 或 1。
0 的魔力
仅仅是增加了一个 0,就带来了革命性的变化。这完全改变了矩阵乘法的计算方式。
常规的矩阵乘法包含大量的乘法 和加法 运算。

但在 BitNet b1.58 中,由于权重只有三种可能,乘法运算被彻底消除了:
- 如果权重是 1,就加上对应的激活值。
- 如果权重是 -1,就减去对应的激活值。
- 如果权重是 0,就跳过,什么也不做。

重大意义:计算完全变成了加减法和条件判断,这在硬件层面可以实现极高的能效和速度。同时,将权重设为 0 还相当于进行了一种特征筛选或网络剪枝,赋予了模型更大的灵活性。
在量化方法上,BitNet b1.58 采用了一种名为绝对平均值(absmean)量化的变体来处理权重,将它们映射到 {-1, 0, 1} 这三个值上。

结果令人惊叹:一个 130 亿参数的 BitNet b1.58 模型,在推理速度、内存占用和能耗方面,甚至优于一个 30 亿参数的 FP16 模型。这预示着一个极致轻量化、高效率大模型时代的到来。
结语
我们的量化之旅到此告一段落。希望这篇图文并茂的指南能让您对量化技术有一个清晰而直观的认识。从节省显存的 PTQ,到追求极致性能的 QAT,再到颠覆计算范式的 1.58 比特模型,量化技术正不断拓展着大语言模型的应用边界。未来的模型会变得多小、多高效?让我们拭目以待!
推荐阅读
如果您想继续深入探索,以下资源可能会对您有帮助:
- 一篇关于 LLM.int8() 量化方法的 HuggingFace 博客。
- 另一篇关于**嵌入(embeddings)**量化的 HuggingFace 博客。
- 一篇关于 Transformer 数学基础的博客,详细解析了 Transformer 的计算和内存原理。
- 这个和这个工具可以帮助您估算不同模型所需的显存。
- 一个关于GPTQ原理的精彩 YouTube 视频,解释得极其直观。