大模型量化从0到1(一):为什么要量化?显存、精度、速度的三角权衡
这是《大模型量化从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
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)
得到:
| 原值 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_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 / 磁盘 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. 显存、质量和速度必须一起看
量化始终是在这个三角中做选择:
显存占用
模型质量
推理速度
而硬件和运行时,是决定这个三角最终形状的隐藏变量。
一句话总结:
量化不是把模型“无损压缩一下”,而是在硬件、显存、速度和模型质量之间,设计一套可控的数值近似。
量化真正的价值,也不是让模型文件看起来更小,而是让原本昂贵甚至无法部署的大模型,在有限硬件上以可接受的代价运行起来。
更多推荐




所有评论(0)