这是《大模型量化从0到1》系列的第 1 篇。整个系列会带你把 AWQ、GPTQ、GGUF、bitsandbytes、FP8、NF4 等主流量化方案,从基本原理到真实模型实战亲手跑一遍。

不过,在下载量化工具、准备校准数据、填写 group_size 之前,我们必须先回答一个最根本的问题:

为什么要量化?量化到底在用什么换什么?它省下来的显存,又是以什么为代价?

想不清楚这些问题,后面看到 INT4、Q4_K_M、W4A16、NF4、FP8 时,就很容易陷入“位数越低越好”“文件越小越快”的误区。

先埋一个伏笔:AWQ、GPTQ、GGUF、bitsandbytes、FP8 严格来说并不属于同一个类别。

  • GPTQ、AWQ 更接近量化算法及其权重表示方案;
  • GGUF 是模型文件格式和运行生态;
  • bitsandbytes 是运行时量化与低精度计算库;
  • INT8、INT4、FP8、NF4 是数值表示格式。

为了方便讨论,系列中有时会把它们统称为“量化方案”,后续文章再逐个拆开。

本文暂时不急着比较谁更好。我们的目标只有一个:

建立一套判断量化方案的底层思维框架。


目录

  • 一、先算一笔账:大模型到底有多“重”
  • 二、量化到底是什么:不要只把它理解成“转成整数”
  • 三、量化的数学本质:scale、zero-point 与量化误差
  • 四、三角权衡:显存、精度、速度不可能同时拉满
  • 五、上手实测:FP16 和 4-bit 到底差多少显存
  • 六、量化与其他省显存手段有什么区别
  • 七、常见误区与一套实用决策方法
  • 八、小结

一、先算一笔账:大模型到底有多“重”

我们经常听到“7B 模型”“14B 模型”“70B 模型”。

这里的 B 是 Billion,也就是十亿。7B 大致表示模型拥有 70 亿个参数,70B 则大致表示 700 亿个参数。

参数并不是一个抽象概念。

模型加载到内存或显存后,每一个参数都要用某种数值格式存下来。一个参数占多少空间,取决于它使用多少比特。

最基础的估算公式是:

理论权重大小
≈ 参数量 × 每个参数的位数 ÷ 8

例如,一个 7B 模型使用 FP16:

7 × 10⁹ × 16 bit ÷ 8
= 14 × 10⁹ Byte
≈ 14 GB

下面这张表建议直接记住。

权重精度理论每参数字节数7B 权重大小13B 权重大小70B 权重大小
FP324 Byte约 28 GB约 52 GB约 280 GB
FP16 / BF162 Byte约 14 GB约 26 GB约 140 GB
INT81 Byte约 7 GB约 13 GB约 70 GB
INT40.5 Byte约 3.5 GB约 6.5 GB约 35 GB

这里计算的是十进制 GB。操作系统、PyTorch 和 nvidia-smi 有时会使用 GiB、MiB 或不同统计口径,所以你看到的实际数字可能略有差异。

1.1 INT4 的 0.5 Byte 只是理论下限

看到表格后,很多人会自然得出一个结论:

FP16 是 16 bit,INT4 是 4 bit,所以 INT4 模型一定正好是 FP16 的四分之一。

实际并没有这么理想。

一个可用的量化模型除了 4-bit 权重编码,还可能需要保存:

  • 每组权重对应的 scale;
  • 非对称量化使用的 zero-point;
  • 分组信息和张量元数据;
  • 权重打包、对齐所产生的额外空间;
  • 没有被量化的 Embedding、LayerNorm、输出层等模块;
  • 框架自身的缓存和运行时对象。

以 bitsandbytes 为例,它会把部分线性层替换成量化线性层,但其他模块仍然可能保留在 FP16、BF16 或模型配置指定的精度。因此,“4-bit 模型”通常表示主要权重被压缩到了 4-bit 左右,而不是整个模型的每一个字节都严格按照 4 bit 存储。(Hugging Face)

因此,更准确的说法是:

INT4 理论权重下限 ≈ FP16 的 1/4

实际模型占用 > 理论 INT4 权重大小

1.2 一张 24GB 显卡到底能跑多大的模型

以拥有 24GB 显存的 RTX 4090 为例:(NVIDIA)

7B FP16

理论权重约 14GB。

通常可以加载,但剩余显存还要留给:

  • KV Cache;
  • 输入和中间激活;
  • CUDA 上下文;
  • 注意力计算工作区;
  • 显存碎片;
  • 推理框架缓存。

所以“权重能装下”不等于“可以随便开 128K 上下文和大 batch”。

13B FP16

理论权重约 26GB。

仅权重就已经超过 24GB,无法完整放进单张 RTX 4090。

13B INT4

理论权重约 6.5GB。

即使加上量化元数据、未量化模块和运行时开销,通常也会比 FP16 宽松得多。省下来的显存可以用来增加:

  • 上下文长度;
  • 并发请求数;
  • batch size;
  • KV Cache 容量。

70B INT4

理论权重约 35GB。

请注意,这仍然大于 RTX 4090 的 24GB 显存。

因此,不能简单地说:

“70B 量化成 INT4 后,一张 4090 就能完整跑起来。”

更准确的说法应该是:

70B 模型量化到 4-bit 后,单卡部署门槛大幅降低,但 24GB 显卡通常仍需要 CPU offload、分层卸载或多卡切分。

这样做也许能“运行”,但因为部分权重需要经过 PCIe 在 CPU 内存和 GPU 显存之间搬运,速度可能和完整驻留显存相差很大。


1.3 权重只是显存账单的一部分

推理时的总显存可以粗略写成:

总显存
≈ 模型权重
+ KV Cache
+ 激活值与临时工作区
+ CUDA / 框架开销
+ 显存碎片

分别来看。

模型权重

这是参数本身占用的空间。

它主要由参数量和权重位宽决定,也是权重量化最直接压缩的部分。

KV Cache

大模型逐 token 生成时,不会每次都把前面的所有 token 从头计算一遍。

每一层注意力模块会缓存历史 token 对应的 Key 和 Value,后续 token 直接复用,这部分数据就是 KV Cache。

一个简化后的 KV Cache 估算公式是:

KV Cache 大小
≈ 2
× 层数
× batch size
× token 数
× KV Head 数
× 每个 Head 的维度
× 每个元素的字节数

最前面的 2 代表 Key 和 Value 两份缓存。

假设某个模型有:

32 层
8 个 KV Head
Head Dim = 128
上下文长度 = 8192
batch size = 1
KV Cache 精度 = FP16,也就是每元素 2 Byte

那么:

2 × 32 × 1 × 8192 × 8 × 128 × 2
= 1,073,741,824 Byte
≈ 1 GiB

这只是 batch size 为 1 的结果。

如果 batch size 增加到 4,KV Cache 大致也会增加到 4GiB。如果模型没有采用 GQA 或 MQA,而是拥有更多 KV Head,占用还会继续增大。

因此,长上下文场景中很容易出现一种情况:

权重已经量化得很小了,最终却被 KV Cache 撑爆显存。

这也是为什么后续除了权重量化,还会出现:

  • KV Cache INT8;
  • KV Cache FP8;
  • 分页式 KV Cache;
  • KV Cache offload;
  • 滑动窗口注意力。

激活值与临时工作区

模型前向传播时,会产生各种中间结果。

在单请求逐 token 解码阶段,这部分通常不像训练时那么夸张;但在长 Prompt 的 Prefill 阶段、较大 batch 或高并发服务中,激活和临时工作区仍然可能占据可观显存。

CUDA 和框架开销

PyTorch、CUDA Context、cuBLAS、注意力内核和显存分配器也需要空间。

这部分不会因为模型只有 1B 参数就自动消失,所以小模型实测时,经常会发现:

实际显存占用
明显大于
参数量 × 每参数字节数

1.4 量化真正解决了什么问题

量化最直接解决的是:

单位参数占用空间太大。

但它带来的价值不只是“让本来跑不了的模型跑起来”。

当模型原本就能装进显存时,量化仍然可以把省下来的空间用于:

  • 更长的上下文;
  • 更大的 batch;
  • 更多并发会话;
  • 更大的 KV Cache;
  • 同卡部署多个模型;
  • 降低服务所需 GPU 数量;
  • 提高单位硬件的请求吞吐。

所以量化有两种完全不同的价值:

个人本地部署:
从“跑不了”变成“能跑”

生产服务部署:
从“能跑”变成“更便宜地跑更多请求”

二、量化到底是什么:不要只把它理解成“转成整数”

一句话定义:

量化就是用一个更小的可表示数值集合,去近似原来的高精度数值。

原本模型参数可能使用 FP32、FP16 或 BF16 存储。

这些格式能表示非常多的数值。量化之后,我们只允许参数从一个更小的集合中取值。

例如:

  • INT8 一共有 2⁸ = 256 种编码;
  • INT4 一共有 2⁴ = 16 种编码;
  • INT2 一共只有 2² = 4 种编码。

你可以把它想象成尺子的刻度。

原来有一把拥有几万个刻度的精密尺子,可以记录非常细微的差异。

现在换成一把只有 16 个刻度的小尺子。尺子更轻、更省空间,但每个数都只能被放到距离它最近的刻度上。

比如原来的权重是:

0.137

量化刻度里可能没有 0.137,只有:

0.125
0.150

那么它只能被近似成其中一个。

原值和近似值之间的差,就是量化误差。


2.1 量化并不一定等于“转成普通整数”

初学者经常把量化理解成:

FP16 → INT8 → INT4

这只是最常见的一类量化。

现代大模型使用的低精度表示还包括:

  • FP8;
  • FP4;
  • BF16;
  • NF4;
  • 非均匀码本;
  • 向量量化;
  • 混合精度编码。

例如 NF4 并不是简单地把区间平均切成 16 份,而是针对近似正态分布的权重设计了一组非均匀表示点。QLoRA 论文同时提出了 NF4 和 Double Quantization,用来进一步降低量化常量本身的存储开销。(arXiv)

所以更通用的理解是:

量化
=
把高精度数值
映射到有限的低精度表示集合

这个集合可以是整数刻度,也可以是浮点格式或特殊码本。


2.2 存储精度和计算精度是两件事

这是新手最容易混淆的地方。

假设一个模型被称为“4-bit 模型”,它通常意味着:

权重用 4-bit 形式存储

但矩阵乘法不一定真的全部使用 INT4 输入、INT4 累加和 INT4 输出。

很多常见方案实际是:

权重存储:4-bit
激活值:FP16 / BF16
计算或累加:FP16 / BF16 / FP32

这种方案常写作:

W4A16

其中:

  • W4:Weight,权重 4-bit;
  • A16:Activation,激活或主要计算精度 16-bit。

常见记法如下:

记法权重精度激活精度含义
W16A1616-bit16-bit常规 FP16/BF16 推理
W8A168-bit16-bit只压缩权重
W4A164-bit16-bit常见 4-bit 权重量化
W8A88-bit8-bit权重和激活都量化
W4A84-bit8-bit更激进的低精度推理

bitsandbytes 的 4-bit 线性层会从量化存储格式中解包权重,并在内核中将其转换到配置的 compute_dtype 参与计算。因此:

load_in_4bit=True

并不等于:

模型所有计算都变成了 4-bit

(Hugging Face)


2.3 为什么不直接把所有东西都变成 INT4

因为不同数据对误差的敏感程度不同。

模型中至少有几类数值:

  • 权重;
  • 激活值;
  • KV Cache;
  • 归一化统计;
  • Softmax 输入与输出;
  • 累加结果。

权重是静态的,可以提前分析和量化,因此相对容易处理。

激活值会随着输入变化,分布更动态,可能还包含明显离群值,因此通常更难量化。

累加过程尤其敏感。即使输入是 INT8,如果大量乘法结果也一直用很低精度累加,误差可能快速放大。因此很多硬件会采用:

低精度乘法
+
更高精度累加

量化并不是简单粗暴地把模型全部改成 INT4,而是在不同位置选择合适的精度。


三、量化的数学本质:scale、zero-point 与量化误差

前面建立了直觉,现在来看最基础的数学。

不用担心,这一节只讲理解后续文章所需的最小知识。对称量化、非对称量化和 per-group 的完整推导会放到下一篇。


3.1 最基础的均匀量化

假设有一个浮点数 x,我们希望把它映射为低比特编码 q。

最常见的仿射量化形式是:

量化:

q = clamp(round(x / s) + z, q_min, q_max)

反量化:

x̂ = (q - z) × s

其中:

  • x:原始浮点数;
  • q:量化后的编码;
  • x̂:反量化后的近似值;
  • s:scale,缩放因子;
  • z:zero-point,零点;
  • q_min、q_max:量化格式能表示的最小值和最大值;
  • clamp:把超出范围的数截断到边界。

为什么要有 scale?

因为整数编码和真实权重的取值范围并不相同。

例如权重范围可能是:

[-0.8, 0.8]

而为了讲解方便,我们假设一个对称 4-bit 量化器使用:

[-7, 7]

那就需要一个 scale,把这两个范围连接起来。


3.2 对称量化

对称量化让量化区间围绕 0 对称。

此时通常令:

z = 0

scale 可以简单取为:

s = max(|x|) / q_max

假设权重为:

[0.1, -0.5, 0.8, -0.3]

并使用教学化的对称 INT4 范围:

[-7, 7]

那么:

max(|x|) = 0.8

s = 0.8 / 7
  ≈ 0.1143

量化:

q = round(x / s)

得到:

原值 xx / s量化值 q反量化值 x̂
0.10.87510.1143
-0.5-4.375-4-0.4571
0.8770.8000
-0.3-2.625-3-0.3429

可以看到:

原值:     0.1000
反量化值: 0.1143
误差:    -0.0143

原来的浮点值没有被精确保存,而是被吸附到了最近的量化刻度。

这就是量化误差。

实际框架可能使用 [-8, 7]、[-7, 7] 或经过特殊编码的 16 个值,具体取决于量化格式和内核。这里使用 [-7, 7] 只是为了把原理讲清楚。


3.3 非对称量化

如果数据分布并不围绕 0 对称,比如:

[0.1, 0.2, 0.5, 1.8]

仍然使用关于 0 对称的量化范围,就可能浪费一部分刻度。

非对称量化会引入 zero-point:

s = (x_max - x_min) / (q_max - q_min)

z = round(q_min - x_min / s)

反量化时:

x̂ = (q - z) × s

zero-point 的作用,可以理解为把整数刻度整体平移,让量化区间更贴合真实数据范围。

简单对比:

方式额外参数优点代价
对称量化scale计算简单、适合近似对称分布可能浪费表示范围
非对称量化scale + zero-point更好贴合偏移分布存储和计算稍复杂

常见工程经验是:

  • 权重分布经常近似对称,因此常使用对称量化;
  • 激活分布更可能偏移,因此非对称量化更常见。

但这只是一种常见选择,不是绝对规律。


3.4 最大值并不一定是最好的量化边界

最直接的量化方案使用:

最大绝对值

来确定 scale。

但假设一组权重是:

[-0.42, -0.31, -0.18, 0.07, 0.21, 0.39, 7.8]

绝大部分数据都集中在:

[-0.5, 0.5]

只有一个离群值是 7.8。

如果 scale 必须覆盖 7.8,16 个 INT4 刻度就会被摊到一个非常大的区间里。结果是:

  • 离群值被完整保留;
  • 绝大多数普通权重却只能挤在少数几个刻度上;
  • 普通权重的量化精度显著下降。

因此,很多量化方法不会机械地保留完整最值范围,而是先寻找一个裁剪阈值 α:

x_clip = clamp(x, -α, α)

然后再量化。

这会产生两类误差:

总误差
≈ 舍入误差
+ 裁剪误差
  • α 太大:离群值保住了,但普通值的刻度太粗;
  • α 太小:普通值刻度更细,但大量离群值被截断;
  • 合适的 α:在两种误差之间取得平衡。

所以,量化算法真正做的事情,并不是简单调用一次 round()。

它要决定:

  • 哪个范围最合适;
  • 哪些值可以裁剪;
  • 哪些通道更重要;
  • 哪些权重值得保留更高精度;
  • 一个 scale 应该服务多少个权重。

3.5 量化粒度:多少个权重共用一个 scale

同一个权重矩阵,可以按照不同粒度量化。

Per-tensor

整个张量共用一个 scale:

一个权重矩阵
→ 一个 scale

优点:

  • 元数据最少;
  • 实现简单。

缺点:

  • 只要局部出现离群值,整个张量都会受到影响;
  • 精度通常较差。

Per-channel

每个输出通道或输入通道拥有自己的 scale:

每一行或每一列
→ 一个 scale

这样不同通道可以根据自己的数值范围独立量化,通常比 per-tensor 精确得多。

Per-group

把每一行继续切成多个小组:

每 128 个权重
→ 一个 scale

这就是很多量化配置中:

group_size = 128

的大致含义。

粒度越细,量化器越能贴合局部数据分布,但需要保存更多 scale 和 zero-point。

可以把它理解为地图比例尺:

  • per-tensor 像一张全国地图,开销小,但看不到街道;
  • per-channel 像城市地图;
  • per-group 像街区地图,更精细,但需要更多图纸。

因此:

分组越小
→ 通常精度越好
→ 元数据越多
→ 内核实现可能越复杂

这也是为什么两个都叫“4-bit”的模型,最终文件大小和模型效果仍然可能不同。


3.6 权重误差小,不代表模型效果一定好

最朴素的量化目标,是让量化前后的权重尽量接近:

最小化:

||W - Ŵ||²

但模型真正执行的是:

Y = WX

量化后执行的是:

Ŷ = ŴX

因此,更有意义的目标其实是:

最小化:

||WX - ŴX||²

这两个目标并不完全一样。

假设两个权重的量化误差都是 0.01:

  • 第一个权重对应的输入激活几乎总是接近 0;
  • 第二个权重对应的输入激活经常很大。

虽然权重误差相同,第二个权重对最终输出的影响会大得多。

所以现代量化算法开始关注:

  • 哪些权重对输出更敏感;
  • 哪些通道经常出现较大激活;
  • 如何在量化一个权重后补偿后续权重;
  • 如何使用校准数据近似真实输入分布。

GPTQ 使用近似二阶信息来降低逐层权重量化造成的输出误差;AWQ 则利用激活统计识别更重要的权重通道并进行保护。(arXiv)

你现在不需要理解它们的完整推导。

只需要记住:

好的量化算法不是让所有权重拥有相同误差,而是尽量把误差放到模型不敏感的位置。


四、三角权衡:显存、精度、速度不可能同时拉满

现在来到这篇文章最重要的思维模型。

量化通常要在三个目标之间做权衡:

                  模型质量
                    /\
                   /  \
                  /    \
                 /      \
                /        \
          显存占用 -------- 推理速度

理想情况当然是:

显存最低
速度最快
效果完全不掉

但现实中,这三个目标很难同时达到最优。


4.1 显存与模型质量

通常来说,位数越低,单个参数占用越小,但可表示的数值越少。

FP16:可表示的数值很多
INT8:256 种整数编码
INT4:16 种编码
INT2:4 种编码

编码数量越少,多个不同的原始权重就越容易被映射到同一个量化值。

因此,量化误差通常会随位数下降而增加。

不过,不能简单地把效果写成:

8-bit 一定很好
4-bit 一定掉一点
3-bit 一定不能用

实际效果还取决于:

  • 模型架构;
  • 模型规模;
  • 量化算法;
  • 分组大小;
  • 是否使用 zero-point;
  • 裁剪策略;
  • 校准数据;
  • 哪些层没有量化;
  • 推理任务;
  • 生成参数。

工程上可以先建立一个粗略认知:

8-bit

通常是更保守的选择。

优势是量化刻度较多,对复杂量化优化的依赖相对较小,适合模型质量优先、显存压力中等的场景。

4-bit

是目前非常常见的实用档位。

相比 FP16,理论权重空间可减少约 75%;同时,配合合适的量化方法,许多模型仍能保持较好的可用性。

3-bit、2-bit

压缩更加激进。

并不代表一定无法使用,但往往更依赖:

  • 更复杂的量化算法;
  • 更细的分组;
  • 混合精度保护;
  • 更有代表性的校准数据;
  • 更严格的任务评测。

位数越低,对算法和模型本身的要求越高。


4.2 量化不等于一定加速

这是量化领域最常见的误区之一。

量化后能否更快,取决于至少四件事:

1. 有没有对应的低精度计算内核
2. 反量化是否和矩阵乘法融合
3. 当前阶段是算力瓶颈还是带宽瓶颈
4. batch、上下文和并发有多大

情况一:有高效低精度内核

假设运行时拥有针对特定格式优化的 INT4 或 INT8 Kernel:

  • 权重以紧凑格式从显存读取;
  • 解包和反量化在 Kernel 内完成;
  • 不产生巨大的中间 FP16 权重;
  • 矩阵乘法针对硬件进行优化。

这种情况下,量化有机会同时:

减少显存
+
减少显存带宽压力
+
提高推理速度

GPTQ 和 AWQ 的论文都不仅讨论了压缩,还配合专门实现展示了低比特推理的加速潜力。(arXiv)

情况二:没有对应 Kernel

如果运行时不认识这种量化布局,可能需要:

读取 4-bit 权重
→ 单独反量化成 FP16
→ 再调用普通 FP16 矩阵乘法

此时就多了一道转换操作。

如果转换不能有效融合,量化模型甚至可能比原始 FP16 更慢。

所以:

量化格式是否先进,不如目标推理引擎是否真正支持它重要。


4.3 Prefill 和 Decode 的瓶颈并不一样

大模型生成可以粗略分为两个阶段。

Prefill:处理输入 Prompt

假设用户输入了 4000 个 token,模型需要先并行处理这 4000 个 token,建立 KV Cache。

这个阶段通常具有:

  • 较大的矩阵乘法;
  • 较多并行计算;
  • 大量注意力计算;
  • 对上下文长度较敏感。

Prefill 的性能可能更容易受到:

  • GPU 算力;
  • 注意力实现;
  • Tensor Core 利用率;
  • batch size;
  • Prompt 长度;

等因素影响。

权重量化可能有帮助,但不保证一定按照模型体积比例加速。

Decode:逐 token 生成

进入生成阶段后,模型通常一次只生成一个新 token。

每生成一个 token,模型都需要经过所有层。小 batch 下,每一步都要读取大量权重,但实际参与计算的 token 数量很少。

这类场景经常更接近:

显存带宽瓶颈

也就是 GPU 不一定缺乘法能力,而是在等待权重从显存搬到计算单元。

量化后权重变小,相同时间内可以搬运更多参数,因此权重量化在低并发逐 token 解码中更容易带来速度收益。AWQ 也把模型大小和显存带宽视为大模型部署的重要限制。(arXiv)

可以简化成下面这张表:

阶段主要工作常见瓶颈权重量化的潜在效果
Prefill并行处理整段 Prompt算力、注意力、显存读写不一定按压缩比例加速
Decode一次生成一个 token小 batch 下常受权重带宽限制更容易从权重压缩中受益

注意,“常见瓶颈”不等于所有硬件和所有 batch 都如此。

当并发和 batch 很大时,Decode 也可能逐渐转向计算瓶颈。


4.4 显存与速度之间也要做选择

有些省显存方法只是把数据换了一个地方。

比如 CPU offload:

GPU 显存放不下
→ 一部分权重放到 CPU 内存
→ 计算前再传到 GPU

它确实降低了 GPU 显存需求,但没有减少整个模型的数据量。

如果频繁经过 PCIe 搬运权重,速度会明显受到影响。

这和量化的区别在于:

Offload:
数据没变小,只是换了存放位置

量化:
数据本身变小了

所以对于本来就处于显存带宽瓶颈的推理,量化有机会既省显存又提速;而 offload 往往是用速度换可运行性。


4.5 这个三角还有一个“隐藏的第四角”

严格来说,显存、质量、速度之外,还有一个经常被忽略的变量:

硬件与推理引擎

完全相同的量化模型,在不同运行时中可能表现完全不同。

例如某个 4-bit 格式:

  • 在一个引擎里有高度优化的 CUDA Kernel;
  • 在另一个引擎里只能回退到通用实现;
  • 在 CPU 上可能没有对应向量化支持;
  • 在某张 GPU 上可能无法使用特定低精度指令。

所以不能只问:

这个模型是几 bit?

还要问:

它要运行在哪个硬件、哪个框架、哪个 Kernel 上?

一个量化方案必须和部署环境一起评估。


4.6 不要只测 tokens/s

评价一个量化方案,至少应关注五类指标。

1. 模型权重占用

量化后模型本身到底小了多少。

2. 峰值显存

包括:

  • 权重;
  • KV Cache;
  • 激活;
  • 临时工作区;
  • 框架开销。

模型文件小,不代表运行时峰值显存一定低。

3. 首 token 延迟

通常也叫 TTFT,Time To First Token。

它较多受到 Prompt 长度和 Prefill 性能影响。

4. 解码速度

也就是进入生成阶段后,每秒能生成多少 token。

5. 模型质量

可以使用:

  • 困惑度 PPL;
  • 选择题准确率;
  • 数学、代码、知识任务;
  • 长上下文任务;
  • 指令跟随测试;
  • 自己的业务数据集。

只测 PPL 不够,只看两三个聊天样例也不够。

量化有时不会让模型明显“语无伦次”,但可能影响:

  • 数学计算稳定性;
  • 代码细节;
  • 罕见知识;
  • 长链推理;
  • 多轮一致性;
  • 小概率 token 的排序。

五、上手实测:FP16 和 4-bit 到底差多少显存

下面使用 bitsandbytes 做一次直观实验。

这一节的目标不是完整讲解 bitsandbytes,而是观察:

同一个模型
FP16 加载
和
4-bit 加载
到底差多少显存

bitsandbytes 的完整原理和参数会在本系列第 10 篇详细展开。


5.1 环境准备

下面以 NVIDIA CUDA 环境为例:

pip install -U torch transformers accelerate bitsandbytes

当前 Transformers 官方文档支持通过 BitsAndBytesConfig 将兼容模型的线性层替换成 8-bit 或 4-bit 量化线性层,并可以使用 get_memory_footprint() 查看模型占用。(Hugging Face)

先确认 PyTorch 能检测到显卡:

python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"

预期类似:

True
NVIDIA GeForce RTX 4090

5.2 为什么不要在同一个进程里直接先后测

很多演示代码会这样写:

加载 FP16
记录显存
删除模型
torch.cuda.empty_cache()
加载 INT4
记录显存

它能大致展示趋势,但不够严谨。

原因是:

  • CUDA Context 不会随模型删除而消失;
  • PyTorch 有显存缓存分配器;
  • 某些 Kernel、工作区和库对象会被保留;
  • memory_allocated、memory_reserved 和 nvidia-smi 的统计口径不同;
  • 第一次运行还可能包含初始化开销。

PyTorch 将 memory_allocated 定义为当前由张量占用的 GPU 内存,将 max_memory_allocated 定义为统计周期内张量占用的峰值;memory_reserved 则包含缓存分配器已经保留的显存。(PyTorch Docs)

为了更公平,我们让 FP16 和 INT4 分别在两个独立进程中运行。


5.3 编写测试脚本

保存为:

measure_quant.py

代码如下:

import argparse
import time

import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)


GIB = 1024 ** 3


def to_gib(num_bytes: int) -> float:
    return num_bytes / GIB


def print_cuda_memory(tag: str) -> None:
    """打印 PyTorch 统计到的 CUDA 显存。"""
    allocated = torch.cuda.memory_allocated(0)
    reserved = torch.cuda.memory_reserved(0)
    peak_allocated = torch.cuda.max_memory_allocated(0)

    print(f"\n[{tag}]")
    print(f"当前张量显存 allocated : {to_gib(allocated):.3f} GiB")
    print(f"分配器保留 reserved    : {to_gib(reserved):.3f} GiB")
    print(f"张量峰值 peak allocated: {to_gib(peak_allocated):.3f} GiB")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--mode",
        choices=["fp16", "int4"],
        required=True,
        help="fp16 表示半精度加载,int4 表示 bitsandbytes 4-bit 加载",
    )
    parser.add_argument(
        "--model",
        default="Qwen/Qwen2.5-1.5B-Instruct",
        help="Hugging Face 模型名称或本地模型目录",
    )
    parser.add_argument(
        "--max-new-tokens",
        type=int,
        default=96,
    )
    args = parser.parse_args()

    if not torch.cuda.is_available():
        raise RuntimeError("没有检测到 CUDA GPU,请先检查驱动和 PyTorch 安装。")

    print(f"GPU   : {torch.cuda.get_device_name(0)}")
    print(f"模型  : {args.model}")
    print(f"模式  : {args.mode}")

    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats(0)

    load_kwargs = {
        # 强制整个模型放入第 0 张 GPU。
        # 这样显存不够时会直接报错,而不是悄悄 offload 到 CPU。
        "device_map": {"": 0},

        # 未量化模块以及主要计算使用 FP16。
        "dtype": torch.float16,

        "low_cpu_mem_usage": True,
    }

    if args.mode == "int4":
        load_kwargs["quantization_config"] = BitsAndBytesConfig(
            load_in_4bit=True,

            # NF4 是针对近似正态分布权重设计的 4-bit 格式。
            bnb_4bit_quant_type="nf4",

            # 这里得到的是典型的 W4A16 路线:
            # 权重以 4-bit 存储,主要计算使用 FP16。
            bnb_4bit_compute_dtype=torch.float16,

            # 再量化一次量化常量,进一步减少元数据占用。
            bnb_4bit_use_double_quant=True,
        )

    load_start = time.perf_counter()

    model = AutoModelForCausalLM.from_pretrained(
        args.model,
        **load_kwargs,
    )
    model.eval()

    torch.cuda.synchronize()
    load_seconds = time.perf_counter() - load_start

    tokenizer = AutoTokenizer.from_pretrained(args.model)

    print(f"\n模型加载耗时: {load_seconds:.2f} 秒")
    print(
        "model.get_memory_footprint(): "
        f"{to_gib(model.get_memory_footprint()):.3f} GiB"
    )

    print_cuda_memory("模型加载完成")

    messages = [
        {
            "role": "user",
            "content": "请用通俗的一句话解释什么是大模型量化。",
        }
    ]

    prompt = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )

    inputs = tokenizer(
        prompt,
        return_tensors="pt",
    )

    inputs = {
        key: value.to("cuda:0")
        for key, value in inputs.items()
    }

    input_length = inputs["input_ids"].shape[1]

    # 记录生成开始前的基础占用。
    base_allocated = torch.cuda.memory_allocated(0)

    # 重置峰值,单独观察生成阶段。
    torch.cuda.reset_peak_memory_stats(0)

    generate_start = time.perf_counter()

    with torch.inference_mode():
        output_ids = model.generate(
            **inputs,
            max_new_tokens=args.max_new_tokens,
            do_sample=False,
            use_cache=True,
            pad_token_id=tokenizer.eos_token_id,
        )

    torch.cuda.synchronize()
    generate_seconds = time.perf_counter() - generate_start

    new_token_count = output_ids.shape[1] - input_length
    peak_during_generate = torch.cuda.max_memory_allocated(0)
    incremental_peak = max(0, peak_during_generate - base_allocated)

    response = tokenizer.decode(
        output_ids[0][input_length:],
        skip_special_tokens=True,
    )

    print("\n模型输出:")
    print(response)

    print(f"\n生成 token 数 : {new_token_count}")
    print(f"本次生成耗时  : {generate_seconds:.2f} 秒")

    if generate_seconds > 0:
        print(
            "粗略生成速度  : "
            f"{new_token_count / generate_seconds:.2f} token/s"
        )

    print(
        "生成阶段新增峰值: "
        f"{to_gib(incremental_peak):.3f} GiB"
    )

    print_cuda_memory("生成完成")


if __name__ == "__main__":
    main()

5.4 分别运行 FP16 和 INT4

先运行 FP16:

python measure_quant.py --mode fp16

进程结束后,再运行 INT4:

python measure_quant.py --mode int4

重点对比三个数字:

model.get_memory_footprint()
当前张量显存 allocated
生成阶段新增峰值

你大概率会看到:

INT4 模型权重占用
明显低于
FP16 模型权重占用

但通常不会严格变成四分之一。

原因包括:

  • scale 和量化元数据;
  • 部分模块没有量化;
  • 权重打包和内存对齐;
  • CUDA Context;
  • PyTorch 缓存;
  • 生成阶段的 KV Cache;
  • 模型具体架构。

官方 bitsandbytes 配置中的 Double Quantization 会继续量化第一次量化产生的常量,从而进一步减少平均每参数占用,但端到端显存仍然不会只由权重位宽决定。(Hugging Face)


5.5 这个速度数据为什么只能叫“粗略速度”

脚本会输出一次生成的 token/s,但不要直接把它当成严谨性能结论。

第一次推理可能包含:

  • Kernel 初始化;
  • CUDA 懒加载;
  • 缓存建立;
  • 内存分配;
  • 图优化;
  • 模型文件系统缓存差异。

严谨测速至少应该:

1. 先预热
2. 固定输入长度
3. 固定输出长度
4. 重复多次
5. 分开统计 Prefill 和 Decode
6. 固定 batch 和并发
7. 使用同一套生成参数
8. 报告平均值和分位数

所以这一节只验证两件事:

量化确实能显著降低模型权重占用
量化后的模型仍然可以正常生成

至于谁更快、快多少,需要专门的基准测试。


六、量化与其他省显存手段有什么区别

量化不是唯一的显存优化手段。

但不同技术解决的是不同问题。

技术主要解决哪部分是否改变数值典型速度影响
权重量化模型权重是有合适 Kernel 时可能加速
KV Cache 量化KV Cache是可降低长上下文显存,可能增加转换开销
CPU / 磁盘 OffloadGPU 中放不下的数据否通常明显变慢
Flash Attention注意力中间结果和显存读写否通常降低注意力开销并加速
Paged KV CacheKV Cache 分配和碎片管理否有利于高并发服务
Tensor Parallel单张卡的模型占用否引入多卡通信
梯度检查点训练激活值否训练变慢,对纯推理意义不大
模型剪枝参数或结构数量是是否加速取决于稀疏支持
知识蒸馏用小模型学习大模型是小模型通常更快,但需要训练

6.1 量化与 Offload

量化

把每个权重本身变小

例如:

FP16:每参数 16 bit
INT4:每参数约 4 bit

Offload

权重大小不变
只是把一部分权重放到 CPU 或磁盘

Offload 通常不会直接引入数值误差,但数据搬运可能成为性能瓶颈。

两者也可以组合:

4-bit 量化
+
部分层 CPU offload

这种组合能让超出单卡显存的模型运行起来,但速度仍然取决于有多少权重需要跨设备传输。


6.2 量化与 Flash Attention

Flash Attention 不是权重量化。

它通过分块计算和减少 HBM 与片上 SRAM 之间的读写,避免显式保存巨大的完整注意力矩阵,从而降低注意力计算的内存开销。它计算的仍然是精确注意力,而不是通过近似降低精度。(arXiv)

需要特别区分:

Flash Attention:
减少注意力中间矩阵和显存读写

KV Cache 量化:
直接减少 Key / Value 缓存的每元素位数

Flash Attention 不会自动把 FP16 KV Cache 变成 INT8,也不会自动让模型权重变小。


6.3 量化与模型剪枝

剪枝的思路是:

删除不重要的权重、通道、层或结构

量化的思路是:

参数数量基本不变
但每个参数使用更少的位数

如果只是把大量权重变成 0,却没有对应的稀疏 Kernel,硬件仍可能按照稠密矩阵进行计算,实际速度未必提升。

而量化后的稠密权重通常更容易映射到现有矩阵乘法内核,因此生态更成熟。


6.4 量化与知识蒸馏

知识蒸馏是让一个较小模型学习大模型的行为。

它改变的是:

模型结构和参数量

量化改变的是:

同一模型中参数的表示精度

二者也可以组合:

先训练或蒸馏一个更小的模型
再对小模型进行量化

七、常见误区与一套实用决策方法

误区一:4-bit 模型所有计算都是 INT4

错。

很多常见 4-bit 方案只是:

权重 4-bit 存储
激活和主要计算使用 FP16 或 BF16

也就是 W4A16。

判断一个量化模型时,至少要问清楚:

  • 权重是什么精度;
  • 激活是什么精度;
  • 累加是什么精度;
  • KV Cache 是什么精度;
  • 哪些层没有量化。

误区二:INT4 一定正好比 FP16 小四倍

错。

理论权重编码是四分之一,但实际还包括:

  • scale;
  • zero-point;
  • 分组元数据;
  • 张量对齐;
  • 未量化层;
  • 运行时开销。

因此,实际模型占用通常大于理论 4-bit 下限。


误区三:文件更小,运行时就一定更省显存

不一定。

文件大小只描述磁盘上的权重表示。

加载后可能发生:

  • 权重解包;
  • 转换到其他布局;
  • 部分层升回 FP16;
  • 建立额外索引;
  • 分配工作区;
  • 创建 KV Cache。

所以要看实际运行时峰值显存,而不是只看下载文件大小。


误区四:量化一定能加速

错。

量化速度取决于:

  • Kernel 是否支持;
  • 权重布局是否匹配;
  • 反量化是否融合;
  • 硬件是否适合;
  • batch 是否足够大;
  • 当前是 Prefill 还是 Decode;
  • 瓶颈是带宽还是算力。

没有优化 Kernel 时,量化可能只省显存,不提速,甚至变慢。


误区五:位数越低越好

错。

位数不是排行榜分数。

真正目标应该是:

在满足模型质量要求的前提下
尽可能降低部署成本

如果 4-bit 已经能满足显存预算,继续压到 3-bit 后:

  • 模型效果下降;
  • 格式兼容性变差;
  • Kernel 选择减少;
  • 调试成本增加;

那么更低位数就没有实际价值。


误区六:所有 4-bit 模型都差不多

错。

即使都写着 4-bit,也可能在以下方面完全不同:

  • 对称还是非对称;
  • INT4、FP4 还是 NF4;
  • group size 是 32、64、128 还是更大;
  • 是否使用 zero-point;
  • 是否裁剪离群值;
  • 校准数据是什么;
  • 哪些层被跳过;
  • 是否保护重要通道;
  • 权重如何打包;
  • 使用哪个推理 Kernel。

“4-bit”只是一个入口标签,不是完整配置。


误区七:所有量化都需要校准数据

不完全正确。

不同量化方式不同。

动态或加载时量化

例如一些 bitsandbytes 加载方案,可以在加载模型时完成量化,不要求用户先准备完整的离线校准流程。

GPTQ、AWQ 等 PTQ 方法

这类方案通常需要一批校准样本,用来估计:

  • 激活分布;
  • 权重敏感度;
  • 裁剪参数;
  • 重构误差;
  • 通道缩放参数。

校准数据不一定很多,但需要尽可能代表真实输入。

例如你要部署的是代码模型,校准数据却全部来自日常中文对话,量化结果可能无法很好覆盖实际代码分布。


误区八:看起来还能聊天,就说明量化没有掉点

错。

模型量化后仍然能生成流畅文本,只能说明它没有完全崩坏。

很多能力退化并不会表现成明显乱码,而可能表现为:

  • 数学题更容易在最后一步出错;
  • 代码更容易漏边界条件;
  • 长文本信息召回下降;
  • 罕见知识回答不稳定;
  • 指令遵循出现细微偏差;
  • 多轮对话一致性下降;
  • 推理链更容易提前终止。

因此,需要结合任务评测,而不是只问模型:

“你好,请介绍一下自己。”

误区九:显存够,就没有必要量化

不一定。

显存够只能说明模型“能加载”。

服务端还要考虑:

  • 同时处理多少请求;
  • 每个请求多长上下文;
  • KV Cache 能放多少;
  • 每张 GPU 能承载多少副本;
  • 单位 token 成本;
  • 功耗;
  • 延迟;
  • 吞吐。

量化省下来的显存,往往可以转化成更高并发和更低服务成本。


一套实际可用的量化决策顺序

面对一个模型,不要上来先问:

用 AWQ 还是 GPTQ?

建议按照下面的顺序判断。

第一步:确定目标硬件

先明确模型运行在哪里:

NVIDIA GPU
AMD GPU
Apple Silicon
纯 CPU
移动端
边缘设备

硬件决定了哪些低精度指令和 Kernel 可用。


第二步:确定推理引擎

确认最终使用:

  • Transformers;
  • llama.cpp;
  • vLLM;
  • TensorRT-LLM;
  • TGI;
  • ExLlama;
  • 其他运行时。

先选运行时,再选它真正支持和优化过的量化格式。

不要先量化完,最后才发现推理引擎无法加载。


第三步:做完整显存预算

不要只算权重。

至少估算:

权重
+
目标上下文下的 KV Cache
+
目标 batch
+
运行时余量

例如:

显存 24GB
权重实际占用 18GB

并不代表还剩 6GB 可以全部给 KV Cache,因为还存在框架、工作区和碎片开销。

实际部署时应保留安全余量。


第四步:确定业务更看重什么

是下面哪一种?

A. 只要能在本地运行
B. 模型效果优先
C. 单请求延迟优先
D. 高并发吞吐优先
E. 最低硬件成本优先
F. 最长上下文优先

不同目标会得到不同答案。

例如:

  • 本地 CPU 推理可能优先考虑 GGUF 生态;
  • NVIDIA GPU 服务可能更关心 AWQ、GPTQ、FP8 与服务框架支持;
  • QLoRA 微调可能优先考虑 bitsandbytes NF4;
  • 高吞吐服务可能更看重批处理 Kernel 和 KV Cache 管理。

第五步:从保守位数开始

一个稳妥策略是:

先测试 8-bit
再测试 4-bit
最后才考虑 3-bit 或更低

如果 4-bit 已满足:

  • 显存预算;
  • 速度要求;
  • 质量要求;

就没有必要为了更小的文件,盲目继续降位数。


第六步:使用自己的数据评测

公开基准只能说明通用趋势。

真正决定能否上线的,应该是:

你的 Prompt
你的语言
你的上下文长度
你的任务
你的生成参数
你的硬件
你的并发模式

至少准备一套包含以下类型的测试集:

  • 常见请求;
  • 困难请求;
  • 超长输入;
  • 结构化输出;
  • 代码或数学任务;
  • 容易触发错误的边界样例。

第七步:同时记录质量、速度和显存

最终对比表建议至少包含:

方案模型占用峰值显存首 token 延迟解码速度任务得分
FP16
INT8
INT4-A
INT4-B

不要只记录其中一项。

因为真正优秀的量化方案不是某个单项冠军,而是:

在你的硬件和任务上,提供最合适的整体平衡。


八、小结

这一篇最重要的,不是记住某个命令,而是建立下面这些认知。

1. 大模型为什么需要量化

模型参数必须真实占用内存。

理论权重大小可以粗略估算为:

参数量 × 每参数位数 ÷ 8

70B 模型使用 FP16 时,理论权重约 140GB;压缩到 4-bit 后理论约 35GB。


2. 权重不是显存的全部

实际推理显存还包括:

KV Cache
激活值
临时工作区
CUDA 上下文
框架缓存
显存碎片

长上下文和高并发场景下,KV Cache 可能成为主要压力。


3. 量化不是普通无损压缩

量化会把多个高精度数值映射到有限的低精度表示点。

反量化后通常不能精确恢复原值,因此它本质上是一种有损近似。


4. 4-bit 不代表所有计算都是 4-bit

常见方案往往是:

权重 4-bit
激活与主要计算 FP16 / BF16

也就是 W4A16。

必须区分:

  • 存储精度;
  • 计算精度;
  • 累加精度;
  • KV Cache 精度。

5. 量化算法的核心是控制误差

真正的问题不是“要不要四舍五入”,而是:

  • scale 怎么选;
  • 是否需要 zero-point;
  • 是否裁剪离群值;
  • 一个 scale 管多少权重;
  • 哪些权重对输出更重要;
  • 如何使用校准数据降低输出误差。

6. 低位数不等于更好的方案

更低位数可以节省更多空间,但也会:

  • 增加量化误差;
  • 提高对算法的要求;
  • 减少可用 Kernel;
  • 增加部署和调试成本。

真正目标是在质量要求内降低成本,而不是追求最低 bit 数。


7. 量化不一定加速

速度收益依赖于:

硬件
推理引擎
Kernel
权重布局
Prefill / Decode 阶段
batch 与并发

没有高效低精度 Kernel 时,量化可能只省显存,并不更快。


8. 显存、质量和速度必须一起看

量化始终是在这个三角中做选择:

显存占用
模型质量
推理速度

而硬件和运行时,是决定这个三角最终形状的隐藏变量。


一句话总结:

量化不是把模型“无损压缩一下”,而是在硬件、显存、速度和模型质量之间,设计一套可控的数值近似。

量化真正的价值,也不是让模型文件看起来更小,而是让原本昂贵甚至无法部署的大模型,在有限硬件上以可接受的代价运行起来。

更多推荐