ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

模型量化(Model Quantization)是如何工作的?从 FP32 到 INT4 的完整解析

模型量化(Model Quantization)是如何工作的?从 FP32 到 INT4 的完整解析

模型量化(Model Quantization)是把模型的数字用更低的精度来存储和计算的过程,这样模型占用的内存更少、跑得更快。用大白话说,我们用更小、更简单的数字,去替代又大又精细的数字。

在这篇博客里,我们会弄清楚模型量化到底是怎么工作的。也会看到为什么我们需要它、它是怎么把庞大的 AI 模型缩小、又是怎么让我们能把模型跑在笔记本、手机或者单张 GPU 上。

我们会覆盖以下内容:

  • 什么是模型量化?
  • 数字如何使用位(FP32、INT8、INT4)
  • 为什么位更少就意味着内存更少、速度更快
  • 核心机制:缩放因子与零点
  • 对称量化 vs 非对称量化
  • 逐张量量化 vs 逐通道量化
  • 训练后量化(PTQ)vs 量化感知训练(QAT)
  • 仅权重量化 vs 权重与激活值同时量化
  • LLM 中的离群值问题
  • 主流方法:GPTQ、AWQ、bitsandbytes、GGUF / llama.cpp
  • 精度权衡与在本地运行 LLM
  • 模型量化总结

什么是模型量化?

模型量化是把模型的数字用更低的精度来存储和计算的过程,这样模型占用的内存更少、跑得更快。

用大白话说,量化就是:我们用更小、更简单的数字,替代又大又精细的数字。

在深入之前,先搞清一件基础的事情。一个 AI 模型,本质上就是一大堆数字的集合。这些数字叫做权重(weight)。模型在训练过程中不断调整这些权重,从而完成学习。一个大语言模型可以拥有数十亿个这样的数字。

正常情况下,每一个数字都是以非常精细的高精度形式存储的。量化会把这些精细的数字,舍入成简单得多的形式。举个例子,我们把0.7319284这样的精细小数,变成94这样的普通整数。这个整数看起来数值更大,但它需要的位(bit)少得多——正是这一点让模型变小了。

打个比方:我们手机里有一张高分辨率照片,占了很大空间。我们把它压缩成更小的文件。照片看起来几乎没变,但占用的空间小了很多。量化对模型做的事是一样的:我们把它变小,同时尽量让它和原来一样好。

这就是基本思路。不过,要真正理解它怎么工作,我们得先理解数字是如何用位来存储的。

数字如何使用位(FP32、INT8、INT4)

计算机用位(bit)来存储一切。位是最小的信息单位,它只能是0或1。

要存储一个数字,计算机就会用一组位。位越多,我们能存储的数字就越精细、越精确;位越少,数字就越简单、细节越少。

来看看 AI 模型里常用的几种格式。

FP32 指的是用 32 位存储的浮点数(floating-point)。"浮点"只是计算机用来存储带小数部分的数字的方式,比如0.7319284或-12.55。FP32 非常精确,能存下极大、极小以及极其精细的数值。这是大多数模型在训练时使用的格式。

FP16 和 BF16 使用 16 位。它们同样存储小数,但细节比 FP32 少。它们的大小是 FP32 的一半。

INT8 指的是用 8 位存储的整数(integer)。"整数"就是没有小数部分的完整数字,比如94或-12。用 8 位,我们只能存储 256 个不同的值,从-128到127。

INT4 指的是只用 4 位存储的整数。用 4 位,我们只能存 16 个不同的值。

为了便于理解,我们把各种格式的尺寸并排放在一起看。

格式位数存储内容精细程度
FP3232小数,非常精确极高
FP1616小数,精度较低高
BF1616小数,范围更宽高
INT88整数,256 个值低
INT44整数,16 个值极低

可以看到,越往下走,位数越少。位越少,每个数字越简单、占用的空间越小。这就是量化为什么能省下这么多空间的根本原因。

为什么位更少就意味着内存更少、速度更快

现在我们已经理解了位,接下来看看为什么更少的位能带来这么大的好处。

内存更少。FP32 中的每个权重占 32 位,也就是 4 字节。同一个权重用 INT8 只占 8 位,也就是 1 字节。所以 INT8 小了 4 倍,INT4 比 FP32 小了 8 倍。

假设一个模型有 70 亿个权重。用 FP32,大约需要 28 GB 内存。同一个模型用 INT8,大约需要 7 GB。用 INT4,只需要大约 3.5 GB。于是,一个原本装不进我们机器的模型,现在能塞进单张 GPU,甚至能跑在一台不错的笔记本上。

一个 70 亿权重的模型缩小过程,可以画成这样:

FP32 (4 bytes each) +--------------------------------+ about 28 GB INT8 (1 byte each) +--------+ about 7 GB INT4 (half byte each)+----+ about 3.5 GB

可以看到,每个权重用的位越少,同一个模型就变得越小。模型的形状没有变,变的只是每个数字的大小。正是这一点,让过去需要数据中心的模型,变成了能装在自己机器上的模型。

速度更快。更小的数字搬运起来更快。模型运行时,计算机要不断地从内存里读取权重、对它们做运算。搬运 1 字节比搬运 4 字节快得多。除此之外,在很多芯片上整数运算本身就比小数运算更快。所以模型不仅装得下,跑得也更快,还更省电。

这就是量化登场的理由:模型更小、回答更快、成本更低。我们付出的代价是一点点精度损失,这个权衡后面会讨论。

那么关键问题来了:我们究竟怎么把一个精细的 FP32 数字,变成一个小的 INT8 数字,而又不把一切都丢掉?来理解核心机制。

核心机制:缩放因子与零点

这是量化的心脏,所以我们慢慢讲。

我们的权重是一些分布在一定范围内的十进制小数,比如从-1.0到1.0。我们希望把这个范围内的每个数字,映射到一小组整数上,比如 INT8 能容纳的 256 个值。

为此,我们用两个辅助值:缩放因子(scale)和零点(zero-point)。

缩放因子告诉我们每一个整数的步长代表多少真实数值,也就是"一步"有多大。

零点则是代表真实数值零的那个整数。

用大白话说,缩放因子把整数拉伸或压缩,让它覆盖我们的取值范围;零点把整数整体平移,让它们对得齐。

从真实数值转成整数的公式如下:

quantized_int = round(real_value / scale) + zero_point

而要从整数还原出一个近似的真实数值,用下面这个公式:

real_value ≈ (quantized_int - zero_point) * scale

可以看到,量化之后再反量化,并不能拿回一模一样的原始数字,我们拿到的只是一个接近的值。这点微小的差异就是舍入误差(rounding error),也正是精度损失的来源。

我们用一个具体的例子把它落到实处。

假设我们的权重范围是从-1.0到1.0,想映射到0到255的 INT8 值上。记住,INT8 给了我们 256 个槽位,这里我们只是把它们从0到255编号。

第一步:整个范围是1.0 - (-1.0) = 2.0。我们有 255 个步长可用。所以缩放因子是2.0 / 255 ≈ 0.00784。

第二步:真实数值零必须落在中间,所以零点大约是128。

第三步:现在取一个真实权重0.5。我们计算round(0.5 / 0.00784) + 128 = round(63.7) + 128 = 64 + 128 = 192。

第四步:要读回来,我们计算(192 - 128) * 0.00784 ≈ 0.502。

我们存进去的是0.5,拿回来的是0.502。非常接近。那点微小的差距,就是我们说的舍入误差。

这个映射过程可以想象成把一条数轴挤压到整数槽位上,就像下面这样:

real (FP32): -1.0 ........... 0.0 ........ 0.5 ......... 1.0 | | | | v v v v int (INT8): 0 ............ 128 ......... 192 ........ 255 (min) (zero-point) (max) <-- each step = scale (about 0.00784) -->

可以看到,宽阔的真实小数范围被映射到了很小的一组整数槽位上。零点128标出了真实值0.0落在哪里,缩放因子则是相邻槽位之间一步的大小。真实值0.5落在整数槽位192上,和我们前面算的完全一致。

这个引擎驱动着所有量化。其余的一切,都只是在"如何选取范围、缩放因子和零点"上做的聪明选择。

对称量化 vs 非对称量化

既然知道了缩放因子和零点,接下来看看设置它们的两种方式。

对称量化(Symmetric Quantization)使用的范围以零为中心,比如-1.0到1.0,映射到-127到127这样的有符号整数上。因为范围是围绕零平衡的,零点本身就变成了零,真实值零也能干净地映射过去。这让数学运算简单又快速。

非对称量化(Asymmetric Quantization)使用的范围不以零为中心,比如0.0到6.0。这时零点会被偏向一侧,好让整数能完整覆盖这个不对称的范围。

来理解各自适用的场景。

假设我们的数字在正负两侧均匀分布,比如模型权重。那么对称量化非常合适:范围天然平衡,不会浪费任何整数值。

但假设我们的数字永远是正的,比如某个永远不会输出负值的函数的输出。如果我们在这里用对称范围,一半的整数就会坐在负半区什么事也不干。那是被浪费的精度。于是非对称量化来救场:它紧贴真实的取值范围,把每一个整数值都用上。

对称量化的优点:数学更简单、更快。

非对称量化的优点:对单侧数据贴合更好,浪费更少。

这就是我们如何根据数据的形状,在对称与非对称之间做选择。

逐张量量化 vs 逐通道量化

接下来,理解这些缩放因子和零点是在什么层级上应用的。

张量(tensor)就是一个大的数字网格。模型里一层的权重就是一个张量,可以把它想成一张有很多行很多列的数字表。

逐张量量化(Per-tensor Quantization)对整个张量只用同一个缩放因子和同一个零点。简单、快速,但有个弱点。

假设张量里有一行的数字很小,比如0.001,另一行数字很大,比如0.9。如果我们强行用一个缩放因子覆盖两者,那些小数字就会被压成几乎相同的整数,它们的细节就丢了。

于是逐通道量化(Per-channel Quantization)登场。我们不再为整个张量用一个缩放因子,而是为每一行(也就是每一个"通道")单独用一个缩放因子。这样小的那一行有自己细腻的缩放因子,大的那一行也有自己的,每一部分都保住了细节。

Per-tensor: Per-channel: one scale for everything one scale per row [ row A: small numbers ] [ row A -> scale A ] [ row B: large numbers ] [ row B -> scale B ] [ row C: mixed numbers ] [ row C -> scale C ] one shared scale each row its own scale

可以看到,逐通道量化保护了那些数值大小差异极大的行。它要多花一点内存来存很多个缩放因子,但精度好得多。正因如此,逐通道量化在模型权重上非常常见。

训练后量化(PTQ)vs 量化感知训练(QAT)

下一个问题是:我们在什么时候做量化?主要有两种路线。

训练后量化(Post-Training Quantization,PTQ)指的是:拿一个已经训练好的模型,事后再对它做量化。我们不再训练它,只是看看权重,挑出好的缩放因子和零点,然后转换数字。

用大白话说,PTQ 就是"先训练,后缩小"。它快、便宜,不需要大型训练环境。大多数时候,我们只需要少量样本数据来测量取值范围。这是最主流的做法,因为它太简单了。

但有个坑:因为模型事先并不知道自己会被量化,舍入误差有时会把精度伤得超出我们的接受范围,尤其是在 INT4 这种极低位宽下。

于是量化感知训练(Quantization-Aware Training,QAT)来救场。

量化感知训练(QAT)指的是:在模型还在训练的时候,就教它认识量化。训练过程中我们模拟舍入误差,让模型学会应对它们。模型会调整自己的权重,使得即使在量化之后也保持精度。

用大白话说,QAT 就是"训练模型,让它做好被缩小的准备"。

两条流程可以这样对比:

PTQ: [ Train model ] --> [ Trained model ] --> [ Quantize ] --> [ Small model ] (shrink later) QAT: [ Train model with rounding simulated ] --> [ Quantize ] --> [ Small model ] (model learns to handle it) (already ready)

可以看到,PTQ 只在训练彻底结束之后才缩小模型,而 QAT 把舍入"烤"进了训练本身,模型事先就准备好了。PTQ 是结尾多加的一步,QAT 改变的则是整个训练过程怎么跑。

为了更好地理解,我把 PTQ 与 QAT 的差别列成表。

对比项PTQQAT
应用时机训练之后训练期间
成本低、快高,需要完整训练
所需数据少量样本完整训练数据
低位宽下的精度好最好
上手难度非常容易较难

可以看到,PTQ 是简单、便宜、流行的选择。QAT 给出最好的精度,但代价大得多。想要速度和简单,就选 PTQ;需要在极低精度下拿到最高精度,就选 QAT。

前沿模型在生产中会使用 QAT:DeepSeek-V4 就用 FP4 量化感知训练来训练,好让推理更快、更便宜。这部分内容在 Decoding DeepSeek-V4 里有详细展开。

仅权重量化 vs 权重与激活值同时量化

到目前为止,我们主要谈的是量化权重。但一个运行中的模型还有另一组我们必须理解的数字。

模型运行时,每一层都会产出输出数字,流进下一层。这些流动的数字叫做激活值(activation)。所以一个模型有两类数字:固定的权重,以及只在模型运行时才出现的激活值。

这就给了我们两个选择。

仅权重量化(Weight-only Quantization)指的是只量化权重,把激活值保持在 FP16 之类更高的精度。这是大语言模型最常见的选择。它已经能省下巨量内存,因为权重是模型的主体,而且对精度更安全。

权重与激活值同时量化(Weight-and-activation Quantization)指的是把权重和激活值都量化到低精度,比如两者都用 INT8。这能带来更快的速度,因为运算本身就以快速的整数形式进行。它常见于手机和小型设备上的小模型——这些设备靠自身运行 AI,没有大型服务器。

还有一点值得注意:LLM 还有第三组数字,就是它在长对话中积累起来的 KV 缓存,这同样可以被量化。这是 KV 缓存压缩(KV Cache Compression)技术之一。

那么下一个大问题是:为什么不总是把激活值也量化掉?答案会引出一个大语言模型里的著名问题。我们来理解它。

LLM 中的离群值问题

在大语言模型里,激活值并不"守规矩"。大部分激活值都很小,但少数几个会非常大。这些罕见的极端值叫做离群值(outlier)。

假设我们几乎所有的数字都落在-1到1之间,但有几个跳到了60。为了把60这一个值塞进我们的范围里,缩放因子必须大幅拉伸。一旦缩放因子被拉得很宽,所有小的、正常的数字就被挤进了屈指可数的几个整数里,细节全丢了。一个离群值毁掉了其他所有数字的精度。

Without outlier: With one outlier (60): range -1 .. 1 range -1 .. 60 fine scale, good detail huge scale, tiny values crushed [ -1 ........... 1 ] [ -1 ..... 60 ] spread nicely all small values squished here

可以看到,这就是为什么在大模型里量化激活值很难。单个离群值就逼出一个很宽的范围,伤害其他所有数字。这正是现代量化方法要解决的问题。来看看主流方法是怎么处理的。

主流方法:GPTQ、AWQ、bitsandbytes、GGUF / llama.cpp

我们不必自己去发明量化。已经有经过充分验证的方法和工具可以直接用。我们按名字过一遍主流的几个。

GPTQ 全称是 Generative Pre-trained Transformer Quantization。它是一种训练后方法,把大语言模型的权重量化到 4 位,非常谨慎地一层一层做。每舍入一个权重之后,它会轻微调整剩下的权重,来补偿刚刚引入的误差。也就是说,它不是盲目地舍入,而是一边舍入一边修正。这让它在 4 位下也能保持高精度。

AWQ 全称是 Activation-aware Weight Quantization(激活值感知的权重量化)。它的核心思想是:并非所有权重都一样重要。有些权重更关键,因为它们连接着刚才讲的那些很大的激活值。AWQ 找出这些重要权重并给予特殊待遇,让它们保住细节,而不是被舍入压扁。它在把权重压到 4 位的同时,让模型依然准确。

bitsandbytes 是一个流行且易用的库,能让我们直接把模型以 8 位或 4 位的形式加载进来。它在 Hugging Face 生态里被广泛使用,所以几行代码就能完成量化和运行。在单张 GPU 上跑大模型,它是最简单、对新手最友好的选择。它还会聪明地把离群值保持在高精度,以保护准确性。

GGUF 和 llama.cpp 是为在普通电脑上运行模型而生的,包括那些完全没有 GPU 的机器。llama.cpp 是一个在 CPU 上运行语言模型的快速引擎,GGUF 则是它用来存储量化模型的文件格式。人们会跑 4 位、5 位等各种尺寸的模型,全部打包在一个 GGUF 文件里。今天大多数人就是在自己的笔记本上用这种方式跑大语言模型的。在服务端,TensorRT-LLM 在 NVIDIA GPU 上做同样的事:在向大量用户同时提供模型服务时,套用这些更小的数字格式。

下面看一个极小的代码示例,用 bitsandbytes 以 4 位加载模型:

fromtransformersimportAutoModelForCausalLM,BitsAndBytesConfig config=BitsAndBytesConfig(load_in_4bit=True)model=AutoModelForCausalLM.from_pretrained("some-large-model",quantization_config=config,)

可以看到,我们在BitsAndBytesConfig里设置了load_in_4bit=True,并在加载模型时把它作为quantization_config传进去。这个库替我们处理了所有缩放因子与零点的数学。原本需要很多 GB 的模型,现在只占一小部分内存就能加载。它工作得很好。

这就是在实际中怎么用量化,而不用手写任何底层的数学。

精度权衡与在本地运行 LLM

现在,我们诚实地谈谈代价,因为量化不是免费的。

每当我们用更少的位,就会丢掉一点细节,这就是前面看到的舍入误差。在 INT8 下,损失通常小到我们几乎察觉不到。在 INT4 下,损失更大一些,但 GPTQ 和 AWQ 这类聪明的方法能把它压得小得惊人。而在 2 位这样的极低位宽下,模型往往开始犯明显的错误。

所以我们始终要记住这个权衡:

位越少,模型越小越快,但精度也越低。

目标就是找到那个甜点:模型轻得多、快得多,但精度依然几乎和原来一样。就今天的大多数用途而言,4 位量化对大语言模型来说完美地击中了这个甜点。

来看看这在真实世界里为什么重要。

假设我们想在自己的笔记本上跑一个强大的聊天机器人,不用云、没有月费。完整的 FP32 模型太大了,根本装不下。但同一个模型量化到 4 位,就能舒舒服服地装进去,并且以不错的速度运行。这正是llama.cpp配合 GGUF 文件每天帮我们做到的事。我们得到隐私、零成本和完全掌控,全都拜量化所赐。

这就是量化真正的力量。它把那些曾经需要昂贵数据中心硬件的模型,带到了我们的笔记本、手机和单张 GPU 上。它让强大的 AI 对每个人都触手可及。

量化并不是缩小模型的唯一途径。知识蒸馏(Knowledge Distillation)走的是另一条路:它训练一个小型的学生模型去模仿一个大型的教师模型,而不是对数字做舍入。

模型量化总结

我们快速回顾一下学到的东西,让整幅图景清晰起来。

模型是一大堆数字。量化以更低的精度来存储和计算这些数字,比如从 FP32 变成 INT8 或 INT4。位越少,内存越少、速度越快、成本越低。

核心机制是用缩放因子和零点,把一个范围内的小数映射到一小组整数上。这个映射可以是对称的或非对称的,也可以作用在整个张量上或逐个通道进行。

我们可以用 PTQ 在训练之后做量化,也可以用 QAT 训练模型让它做好准备。我们可以只量化权重,也可以把权重和激活值一起量化,同时留意大语言模型里的离群值问题。我们还可以依靠 GPTQ、AWQ、bitsandbytes,以及配合 llama.cpp 的 GGUF 这些现成方法来轻松完成这一切。

贯穿始终的,是一个简单的权衡:更少的位给出更小、更快的模型,代价是一点精度。当我们选对档位,几乎什么也不损失,却收获很多。

这就是模型量化的工作原理。

常见问题

量化之后能拿回完全一致的原始数字吗?

不能。量化再反量化,得到的是一个接近但不完全一致的数字。例如权重0.5可能还原成0.502。这点微小的差距就是舍入误差,也是量化中精度损失的来源。

量化一个 LLM 应该用多少位?

就今天的大多数用途而言,4 位量化对大语言模型来说正好落在甜点上。在 INT8 下,精度损失通常小到几乎察觉不到。在 INT4 下损失更大,但 GPTQ 和 AWQ 这类方法能把它压得很小。而在 2 位这样的极低位宽下,模型往往开始犯明显的错误。

对新手来说,量化模型最简单的方式是什么?

bitsandbytes 是最简单、对新手最友好的选择。它让我们用几行代码就能直接把模型以 8 位或 4 位的形式加载进来,并且在 Hugging Face 生态里被广泛使用。这个库替我们处理了所有缩放因子与零点的数学。

量化和知识蒸馏有什么区别?

两者都能让模型变小,但方式不同。量化保持模型本身不变,只是用更低的精度来存储它的数字,比如从 FP32 变成 INT4。知识蒸馏则是训练一个小型的学生模型去模仿一个大型的教师模型,而不是对数字做舍入。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进