这是《大模型量化从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 权重大小
FP32 4 Byte 约 28 GB 约 52 GB 约 280 GB
FP16 / BF16 2 Byte 约 14 GB 约 26 GB 约 140 GB
INT8 1 Byte 约 7 GB 约 13 GB 约 70 GB
INT4 0.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。

常见记法如下:

记法 权重精度 激活精度 含义
W16A16 16-bit 16-bit 常规 FP16/BF16 推理
W8A16 8-bit 16-bit 只压缩权重
W4A16 4-bit 16-bit 常见 4-bit 权重量化
W8A8 8-bit 8-bit 权重和激活都量化
W4A8 4-bit 8-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:量化后的编码;
  • :反量化后的近似值;
  • s:scale,缩放因子;
  • z:zero-point,零点;
  • q_minq_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)

得到:

原值 x x / s 量化值 q 反量化值 x̂
0.1 0.875 1 0.1143
-0.5 -4.375 -4 -0.4571
0.8 7 7 0.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_allocatedmemory_reservednvidia-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 / 磁盘 Offload GPU 中放不下的数据 通常明显变慢
Flash Attention 注意力中间结果和显存读写 通常降低注意力开销并加速
Paged KV Cache KV 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. 显存、质量和速度必须一起看

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

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

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


一句话总结:

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

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐