第4章 LLM 4-bit 量化实战:LLM 4-bit 量化到底意味着什么?
本章目录:
- 4.1 4-bit 让模型“小这么多”的原理
- 4.2 INT4 的基本表示
- 4.3 低到 4-bit 仍然可以工作的原因
- 4.4 W4A16 到底是什么?
- 4.5 第一次真实 4-bit 推理
- 4.6 本章小结
前面第1章和第2章,我们已经回答了两个基础问题:
第一:
为什么 LLM 需要量化?
因为模型参数越来越大,FP16/BF16 权重会占用大量显存和存储空间。
第二:
量化到底做了什么?
本质上是:
C o n t i n u o u s V a l u e → D i s c r e t e V a l u e \boxed{ Continuous\ Value \rightarrow Discrete\ Value } Continuous Value→Discrete Value
并通过: x q = r o u n d ( x / s ) x_q=round(x/s) xq=round(x/s) 将高精度浮点数映射到低比特整数表示。
第3章进一步学习了 PTQ:
P o s t T r a i n i n g Q u a n t i z a t i o n \boxed{ Post\ Training\ Quantization } Post Training Quantization
即模型训练完成以后再进行量化。
那么一个自然的问题出现了:
到底什么叫“4-bit 量化”?
例如我们经常看到:
Llama 7B 4-bit
Qwen 7B 4-bit
GPTQ 4-bit
AWQ 4-bit
GGUF Q4
这里的:
4 − b i t \boxed{4-bit} 4−bit
到底意味着什么?
为什么从: F P 16 FP16 FP16 降低到:
I N T 4 INT4 INT4
以后,模型可以一下子变得这么小?
为什么 4-bit 只有: 2 4 = 16 2^4=16 24=16 种状态,却还能保存一个拥有几十亿参数的大语言模型?
更重要的是:
为什么量化到如此低的精度以后,模型还能够正常工作?
本章就从这些最基础的问题开始,一步一步把:
4 − b i t \boxed{4-bit} 4−bit
真正讲明白。
4.1 4-bit 让模型“小这么多”的原理
先从一个最简单的问题开始:
一个参数到底需要多少空间?
假设一个模型有: N N N 个参数。
如果每个参数使用: b b b 个 bit。
那么仅仅从理论上看,权重存储空间就是: M e m o r y = N × b 8 Memory=N\times\frac{b}{8} Memory=N×8b 也就是:
M e m o r y ∝ B i t W i d t h \boxed{ Memory\propto BitWidth } Memory∝BitWidth
这就是量化能够显著压缩模型的根本原因。
4.1.1 FP32
FP32: 32 b i t = 4 B y t e 32bit=4Byte 32bit=4Byte 如果有:
7 B 7B 7B
参数: 7 × 10 9 × 4 7\times10^9\times4 7×109×4 约等于:
28 G B 28GB 28GB
所以:
7 B F P 32 ≈ 28 G B \boxed{ 7B\ FP32\approx28GB } 7B FP32≈28GB
仅仅是权重。
4.1.2 FP16
FP16: 16 b i t = 2 B y t e 16bit=2Byte 16bit=2Byte 于是:
7 B × 2 B y t e ≈ 14 G B 7B\times2Byte \approx14GB 7B×2Byte≈14GB
所以:
7 B F P 16 ≈ 14 G B \boxed{ 7B\ FP16\approx14GB } 7B FP16≈14GB
这也是为什么很多 7B 模型使用 FP16 加载以后,显存压力会明显增加。
4.1.3 INT8
INT8: 8 b i t = 1 B y t e 8bit=1Byte 8bit=1Byte 那么:
7 B × 1 B y t e ≈ 7 G B 7B\times1Byte \approx7GB 7B×1Byte≈7GB
也就是:
7 B I N T 8 ≈ 7 G B \boxed{ 7B\ INT8\approx7GB } 7B INT8≈7GB
和 FP16 相比: 14 G B → 7 G B 14GB\rightarrow7GB 14GB→7GB 理论上直接减少一半。
4.1.4 INT4
再进一步: 4 b i t = 0.5 B y t e 4bit=0.5Byte 4bit=0.5Byte 因此:
7 B × 0.5 B y t e ≈ 3.5 G B 7B\times0.5Byte \approx3.5GB 7B×0.5Byte≈3.5GB
所以:
7 B I N T 4 ≈ 3.5 G B \boxed{ 7B\ INT4\approx3.5GB } 7B INT4≈3.5GB
这就是 4-bit 最直观的价值。
4.1.5 FP16 → INT8 → INT4
现在把它们放在一起:
| 数据类型 | 每参数占用 | 7B 权重理论大小 |
|---|---|---|
| FP32 | 4 Byte | ≈28 GB |
| FP16 | 2 Byte | ≈14 GB |
| INT8 | 1 Byte | ≈7 GB |
| INT4 | 0.5 Byte | ≈3.5 GB |
因此: F P 16 → I N T 8 FP16\rightarrow INT8 FP16→INT8 理论上:
M e m o r y ÷ 2 Memory\div2 Memory÷2
而: F P 16 → I N T 4 FP16\rightarrow INT4 FP16→INT4 理论上:
M e m o r y ÷ 4 Memory\div4 Memory÷4
这就是为什么 4-bit 量化会如此受到关注。
4.1.6 但是 3.5GB ≠ 实际运行显存
如果: 7 B I N T 4 ≈ 3.5 G B 7B\ INT4\approx3.5GB 7B INT4≈3.5GB 并不意味着:
“运行这个模型只需要 3.5GB 显存。”
因为运行模型时还需要: K V C a c h e KVCache KVCache 以及:
A c t i v a t i o n Activation Activation
和: R u n t i m e B u f f e r Runtime\ Buffer Runtime Buffer 所以更准确地写:
$$
\boxed{
M_{total}
M_{weight}
+
M_{KV}
+
M_{activation}
+
M_{buffer}
}
$$
因此:
M o d e l S i z e ≠ R u n t i m e M e m o r y ModelSize \neq RuntimeMemory ModelSize=RuntimeMemory
这一点在后面的实际部署章节中会越来越重要。
4.1.7 13B 模型呢?
同样计算。
FP16
13 B × 2 B y t e ≈ 26 G B 13B\times2Byte \approx26GB 13B×2Byte≈26GB
INT8
13 B × 1 B y t e ≈ 13 G B 13B\times1Byte \approx13GB 13B×1Byte≈13GB
INT4
13 B × 0.5 B y t e ≈ 6.5 G B 13B\times0.5Byte \approx6.5GB 13B×0.5Byte≈6.5GB
所以:
| 模型 | FP16 | INT8 | INT4 |
|---|---|---|---|
| 7B | ≈14 GB | ≈7 GB | ≈3.5 GB |
| 13B | ≈26 GB | ≈13 GB | ≈6.5 GB |
注意:
这里仍然只是权重理论值。
4.1.8 70B 模型呢?
这时候差别会更加明显。
FP16
70 B × 2 B y t e ≈ 140 G B 70B\times2Byte \approx140GB 70B×2Byte≈140GB
INT8
70 B × 1 B y t e ≈ 70 G B 70B\times1Byte \approx70GB 70B×1Byte≈70GB
INT4
70 B × 0.5 B y t e ≈ 35 G B 70B\times0.5Byte \approx35GB 70B×0.5Byte≈35GB
因此:
| 模型 | FP16 | INT8 | INT4 |
|---|---|---|---|
| 70B | ≈140 GB | ≈70 GB | ≈35 GB |
这就出现一个非常有意义的结果:
一个 FP16 需要大约 140GB 权重存储的模型,理论上使用 4-bit 后可以降到大约 35GB。
也就是说:
140 G B → 35 G B \boxed{ 140GB\rightarrow35GB } 140GB→35GB
这就是低比特量化对大模型部署最大的吸引力之一。
4.1.9 4-bit 真的是“模型缩小 4 倍”吗?
如果比较: F P 16 FP16 FP16 和:
I N T 4 INT4 INT4
理论上: 16 4 = 4 \frac{16}{4}=4 416=4 所以可以说:
权重存储理论上缩小约 4 倍。
但实际文件一般不会恰好: 4.00 × 4.00\times 4.00× 因为还需要保存:
Scale
Zero Point
Group Metadata
Tensor Metadata
Alignment
等等辅助信息。
所以实际结果通常是:
接近 4 倍,而不是严格 4 倍 \boxed{ 接近4倍,而不是严格4倍 } 接近4倍,而不是严格4倍
4.2 INT4 的基本表示
现在进入本章第二个核心问题:
4-bit 到底能表示多少个数?
答案: 2 4 = 16 2^4=16 24=16 也就是说:
4 b i t = 16 s t a t e s \boxed{ 4bit=16\ states } 4bit=16 states
4.2.1 只有 16 个状态的由来
一个 bit 只有:
0
1
所以: 2 1 = 2 2^1=2 21=2 两个 bit:
2 2 = 4 2^2=4 22=4
三个 bit: 2 3 = 8 2^3=8 23=8 四个 bit:
2 4 = 16 2^4=16 24=16
因此:
4 b i t → 16 p o s s i b l e c o d e s \boxed{ 4bit\rightarrow16\ possible\ codes } 4bit→16 possible codes
4.2.2 如果是无符号 INT4
一种简单的理解是: 0 ∼ 15 0\sim15 0∼15 一共:
16 16 16
个状态。
例如:
0
1
2
3
...
15
4.2.3 如果是有符号 INT4
如果采用有符号表示,可以理解成一个有限的正负整数范围。
例如常见的对称量化思路可以把量化结果映射到一个以零附近为中心的离散区间。
重点不是记住某一个具体端点,而是理解:
原始浮点数的连续空间,被压缩成了很少的离散等级。
4.2.4 连续空间 → 16 个量化点
假设原来的权重范围: [ − 1 , 1 ] [-1,1] [−1,1] 现在只有 16 个等级。
那么我们可以想象:
-1.0 ───────────────────────── +1.0
↓ 量化
●──●──●──●──●──●──●──●──●──●──●──●──●──●──●──●
原来:
任意浮点值都可以出现。
现在:
只能选择附近的某一个离散点。
4.2.5 量化到底损失了什么?
假设: x = 0.73 x=0.73 x=0.73 经过 4-bit 量化以后,可能只能表示成:
0.67 0.67 0.67
或者: 0.73 → 0.75 0.73\rightarrow0.75 0.73→0.75 于是产生:
E r r o r = x − x ^ Error=x-\hat x Error=x−x^
例如: E r r o r = 0.73 − 0.75 = − 0.02 Error=0.73-0.75=-0.02 Error=0.73−0.75=−0.02 这就是:
Q u a n t i z a t i o n E r r o r \boxed{ Quantization\ Error } Quantization Error
4.2.6 Scale 的作用
我们在第2章已经学习过: x q = r o u n d ( x / s ) x_q=round(x/s) xq=round(x/s) 其中:
s s s
就是 Scale。
再通过: x ^ = s x q \hat x=sx_q x^=sxq 恢复到浮点空间。
所以:
完整写成:
x → x q → x ^ x \rightarrow x_q \rightarrow \hat x x→xq→x^
4.2.7 不能直接把 0.73 变成一个 4-bit 数
因为: 0.73 0.73 0.73 本身是一个连续浮点数。
而 INT4 只能表示: 16 16 16 个离散状态。
所以必须建立一个映射:
C o n t i n u o u s R a n g e → 16 d i s c r e t e l e v e l s \boxed{ Continuous\ Range \rightarrow 16\ discrete\ levels } Continuous Range→16 discrete levels
Scale 就是这个映射中的关键参数。
4.2.8 一个简单的 4-bit 例子
假设: x ∈ [ − 1 , 1 ] x\in[-1,1] x∈[−1,1] 把它划分成 16 个离散等级。
可以粗略想象:
-1.00
-0.87
-0.73
-0.60
-0.47
...
0.47
0.60
0.73
0.87
1.00
真实量化器的具体离散点会根据:
- 对称/非对称
- Scale
- Zero Point
- Quantization Scheme
而不同。
但基本思想不变:
把连续空间切成有限数量的格子。
4.2.9 这就产生了一个重要问题
如果所有参数都使用:
同一个 Scale
会怎么样?
假设整个 Layer 中:
99% 参数
范围:[-0.5, 0.5]
1% 参数
范围:[-20, 20]
如果 Scale 为了覆盖: [ − 20 , 20 ] [-20,20] [−20,20] 那么大量普通参数就会被压缩得非常粗糙。
因此:
不是所有权重都应该简单地共用一个 Scale。
4.2.10 Group Size
这就是:
G r o u p − w i s e Q u a n t i z a t i o n \boxed{ Group-wise\ Quantization } Group−wise Quantization
的思想。
例如:
1000 个权重
不要:
而是:
例如:
Group 1 → Scale 1
Group 2 → Scale 2
Group 3 → Scale 3
...
4.2.11 Group Size 越小通常越灵活的原因
假设:
Group Size = 128
那么: 128 128 128 个权重共享一个 Scale。
如果:
Group Size = 32
那么: 32 32 32 个权重共享一个 Scale。
显然: 32 32 32 个数值的分布更容易接近。
所以:
S m a l l e r G r o u p → M o r e L o c a l S c a l e s → U s u a l l y L e s s Q u a n t i z a t i o n E r r o r \boxed{ Smaller\ Group \rightarrow More\ Local\ Scales \rightarrow Usually\ Less\ Quantization\ Error } Smaller Group→More Local Scales→Usually Less Quantization Error
但是代价是: M o r e M e t a d a t a More\ Metadata More Metadata 也就是:
更好的精度可能换来更多额外存储和实现复杂度。
4.2.12 Group Size 是一个重要的工程权衡
可以理解成:
所以:
$$
\boxed{
Group\ Size
Accuracy
\ vs
Overhead
}
$$
这也是后面 GPTQ/AWQ 等 4-bit 方法中经常出现:
group_size = 128
等参数的原因。
4.3 低到 4-bit 仍然可以工作的原因
现在来到本章最核心的问题。
已经知道: 4 b i t = 16 s t a t e s 4bit=16\ states 4bit=16 states 这看起来非常粗糙。
那么:
一个拥有几十亿参数的大模型,把权重压到 4-bit 后仍然可以正常工作。
这背后的原因不是:
“因为 4-bit 足够精确。”
实际上:
4-bit 一点也不精确。
它之所以仍然有效,是因为神经网络本身具有很强的:
R e d u n d a n c y \boxed{Redundancy} Redundancy
也就是:
一定程度的冗余性和容错能力。
4.3.1 第一:LLM 权重并不是完全随机的
神经网络中的权重通常不是:
[-1000000, 1000000]
这样毫无规律地分布。
很多参数会集中在一个相对有限的范围内。
可以想象:
*
* * *
** ******* **
*******************
**************************
------------------------------------------→ Weight
即:
大部分权重集中在中心区域,少量值可能更大。
因此,如果合理设计量化区间:
可以使用有限的离散等级描述大部分权重。
4.3.2 第二:神经网络具有冗余性
假设一个模型有: 7 B 7B 7B 个参数。
并不是说:
每一个参数都承担完全独立、不可替代的功能。
神经网络通常具有一定冗余性。
可以简单理解成:
所以其中某些参数发生: Δ w \Delta w Δw 并不一定导致输出发生灾难性变化。
这给量化留下了空间。
4.3.3 量化误差不一定会被无限放大
设: W W W 量化为:
W ^ = W + Δ W \hat W=W+\Delta W W^=W+ΔW
原始计算: Y = W X Y=WX Y=WX 量化后:
Y ^ = W ^ X \hat Y=\hat WX Y^=W^X
所以:
$$
\hat Y
(W+\Delta W)X
$$
于是:
$$
\hat Y-Y
\Delta W X
$$
因此输出误差由: Δ W \Delta W ΔW 和输入:
X X X
共同决定。
只要量化误差在合理范围内:
最终输出不一定发生巨大变化。
4.3.4 第三:Transformer 有大量参数协同工作
一个 Transformer Block 并不是:
而是:
所以一个单独的权重出现微小变化: w → w + ϵ w\rightarrow w+\epsilon w→w+ϵ 通常不会直接等价于:
“模型突然不会回答问题了。”
这也是低比特量化能够工作的一个重要原因。
4.3.5 但是不是所有权重都同样重要?
不是。
这点非常重要。
有些参数变化: Δ w \Delta w Δw 可能对模型输出影响很小。
但另一些参数可能: Δ w \Delta w Δw 就会导致比较明显的变化。
所以实际的 4-bit 量化研究中才会出现:
“如何保护重要权重?”
这也是后面 AWQ 等算法需要重点解决的问题。
注意:
本章只理解这个问题,不提前进入 AWQ。
4.3.6 4-bit 的真正逻辑不是“4-bit 很精确”
而是:
4 − b i t 虽然粗糙 → 但模型允许一定程度的误差 \boxed{ 4-bit虽然粗糙 \rightarrow 但模型允许一定程度的误差 } 4−bit虽然粗糙→但模型允许一定程度的误差
进一步:
量化方案足够好 → 误差可以控制 \boxed{ 量化方案足够好 \rightarrow 误差可以控制 } 量化方案足够好→误差可以控制
最终:
模型仍然可以工作 \boxed{ 模型仍然可以工作 } 模型仍然可以工作
4.3.7 Quantization ≠ Pruning
这里要避免一个常见误解:“权重有冗余"不等于"可以随便删权重”。
4-bit 量化并不是删掉 75% 的参数,而是仍然保存全部参数,只是用更低精度的数值表示它们。例如 0.7321 0.7321 0.7321 变成 0.7 0.7 0.7——参数还在原来的位置,只是记录得更"粗"。这和 Pruning(剪枝)是完全不同的两件事:
Q u a n t i z a t i o n ≠ P r u n i n g \boxed{ Quantization \neq Pruning } Quantization=Pruning
可以这样理解量化的直觉:原始模型说"这个参数是 0.731482",4-bit 模型说"这个参数大约在 0.7 这一档"。单个参数的信息确实明显减少,但几十亿参数放在一起时,整体仍能保留大量有价值的结构信息——这就是 4-bit 能工作的根本原因。
4.4 W4A16 到底是什么?
现在我们经常看到:
W 4 A 16 \boxed{W4A16} W4A16
这个名字一眼看起来很专业,实际上非常简单。
把它拆开: W 4 A 16 W4A16 W4A16 其中:
W = W e i g h t W=Weight W=Weight A = A c t i v a t i o n A=Activation A=Activation 所以:
$$
\boxed{
W4A16
Weight\ 4-bit
+
Activation\ 16-bit
}
$$
4.4.1 W4A16
也就是:
因此:
W e i g h t = I N T 4 \boxed{ Weight=INT4 } Weight=INT4
而:
A c t i v a t i o n = F P 16 / B F 16 \boxed{ Activation=FP16/BF16 } Activation=FP16/BF16
这里的“16”通常表示 16-bit 浮点计算,如 FP16 或 BF16,而不是必须存在一种名为“A16”的数据类型。
4.4.2 Weight 可以 4-bit 的原因
因为 Weight: W W W 在推理过程中是固定的。
模型加载以后:
W
不会随着每个输入发生变化。
因此:
可以提前把 Weight 量化。
4.4.3 Activation 保留 16-bit 的原因
因为 Activation: A A A 取决于输入:
X X X
也就是说: A = f ( X , W ) A=f(X,W) A=f(X,W) 换一个 Prompt:
X 1 X_1 X1
可能得到: A 1 A_1 A1 另一个 Prompt:
X 2 X_2 X2
则: A 2 A_2 A2 所以:
A c t i v a t i o n i s i n p u t d e p e n d e n t \boxed{ Activation\ is\ input\ dependent } Activation is input dependent
它的分布比固定 Weight 更难提前处理。
4.4.4 W4A16 的直观计算过程
假设: Y = W X Y=WX Y=WX W4A16 可以理解为:
所以:
Weight 使用低比特存储,Activation 仍保持较高精度。
4.4.5 这种方式流行的原因
因为它刚好解决了两个问题:
第一
Weight 占模型绝大部分存储。
因此: W 4 W4 W4 能够大幅减少:
M e m o r y Memory Memory
第二
Activation 不量化到 4-bit。
因此:
可以减少部分 Activation 量化带来的精度风险。
于是形成:
W 4 A 16 \boxed{ W4A16 } W4A16
这样的折中方案。
4.4.6 W4A16 本质上是一种“折中”
它不是:
“所有东西都 4-bit。”
而是:
Weight
→ 4-bit
→ 极力压缩
Activation
→ 16-bit
→ 保持较高精度
所以:
M e m o r y E f f i c i e n t + R e l a t i v e l y S t a b l e \boxed{ Memory\ Efficient + Relatively\ Stable } Memory Efficient+Relatively Stable
4.4.7 W8A16 又是什么?
同理:
W 8 A 16 \boxed{ W8A16 } W8A16
就是:
Weight
→ INT8
Activation
→ FP16/BF16
而: W 4 A 16 W4A16 W4A16 进一步把 Weight 从 8-bit 降到了 4-bit。
所以:
4.4.8 W8A8 又是什么?
继续:
W 8 A 8 \boxed{ W8A8 } W8A8
表示:
Weight → 8-bit
Activation → 8-bit
于是:
W8A16
= Weight 8 + Activation 16
W4A16
= Weight 4 + Activation 16
W8A8
= Weight 8 + Activation 8
这样我们就可以从名称直接判断:
哪个部分被量化了。
4.4.9 一张表看懂
| 方案 | Weight | Activation | 主要特点 |
|---|---|---|---|
| FP16 | 16-bit | 16-bit | 高精度基线 |
| W8A16 | 8-bit | 16-bit | 主要压缩 Weight |
| W4A16 | 4-bit | 16-bit | 强力压缩 Weight |
| W8A8 | 8-bit | 8-bit | Weight + Activation 都量化 |
4.4.10 W4A16 特别适合 LLM 推理的原因
LLM 推理中的一个重要问题是:
模型权重非常大。
例如: 7 B 7B 7B 模型。
FP16: 14 G B 14GB 14GB 如果变成:
W 4 W4 W4
理论上约: 3.5 G B 3.5GB 3.5GB 因此:
W e i g h t M e m o r y ↓ Weight\ Memory \downarrow Weight Memory↓
同时: A c t i v a t i o n Activation Activation 仍然保持:
F P 16 / B F 16 FP16/BF16 FP16/BF16
所以可以获得一个很好的折中:
4-bit Weight
+
16-bit Activation
4.4.11 W4A16 是不是意味着 GPU 全程执行 INT4?
不是。
这是另一个非常常见的误解。 W 4 A 16 W4A16 W4A16 主要描述:
权重和激活的表示/计算精度配置。
实际 Runtime 怎样执行还取决于:
- Hardware
- Kernel
- Dequantization
- Accumulation
- Backend
所以:
“Weight 是 4-bit”不等于“整个矩阵乘法从头到尾都是纯 INT4 数学”。
不同硬件和 Runtime 可以采用不同的实现方式。
4.4.12 “4-bit”不等于“速度一定 4 倍”
例如:
因此: M e m o r y T r a f f i c Memory\ Traffic Memory Traffic 可能显著减少,但:
C o m p u t a t i o n Computation Computation
并不会简单变成原来的: 1 4 \frac14 41 所以:
4 b i t C o m p r e s s i o n ≠ 4 × S p e e d u p \boxed{ 4bit\ Compression \neq 4\times Speedup } 4bit Compression=4×Speedup
4.4.13 4-bit 的第一个真正价值是什么?
如果只记一个:
M e m o r y \boxed{ Memory } Memory
例如: F P 16 → I N T 4 FP16\rightarrow INT4 FP16→INT4 权重理论上:
14 G B → 3.5 G B 14GB\rightarrow3.5GB 14GB→3.5GB
于是:
原来需要很大显存的模型,现在可能进入更小的设备。
所以量化首先解决:
M o d e l F i t \boxed{ Model\ Fit } Model Fit
4.4.14 第二个价值:Memory Bandwidth
GPU 推理中:
模型权重需要不断从显存层级读取。
如果 Weight 从: 16 b i t 16bit 16bit 变成:
4 b i t 4bit 4bit
那么理论上需要搬运的数据量大幅下降。
因此: M e m o r y T r a f f i c ↓ Memory\ Traffic\downarrow Memory Traffic↓ 这在某些 LLM Decode 场景下尤其重要。
4.4.15 第三个价值:成本
如果模型太大:
需要 4 张 GPU
量化以后可能:
只需要 2 张 GPU
那么量化带来的已经不只是:
“模型小了。”
而是:
I n f r a s t r u c t u r e C o s t ↓ \boxed{ Infrastructure\ Cost\downarrow } Infrastructure Cost↓
4.5 第一次真实 4-bit 推理
现在进入本章最后一个部分。
理论已经知道:
FP16
→ 大
INT8
→ 小
INT4
→ 更小
那么:
实际跑起来到底有什么差别?
我们做一个非常简单的实验。
4.5.1 实验目标
比较:
FP16
INT8
INT4
观察:
1. Model Size
2. Memory
3. Load Time
4. Tokens/s
5. Output Quality
本章目录也正是按照这一组指标设计第一次真实 4-bit 推理。
4.5.2 实验环境
建议:
Python
PyTorch
Transformers
CUDA GPU
例如:
python --version
然后:
python -c "import torch; print(torch.__version__)"
例如本节实验所用的环境为:
Python 3.12.3
torch 2.7.0+cu126 # PyTorch 2.7.0 + CUDA 12.6
检查 GPU:
nvidia-smi
本节实验所用的 GPU:
GPU: NVIDIA RTX 4090 (24GB) # Driver 575.51.03, CUDA 12.9
4.5.3 三档实验:可直接运行的完整代码
为了方便你动手复现,本节把整个实验整理成三段可直接复制运行的代码。三段共用同一个模型、同一个 prompt,只改变精度,最后对比资源与输出。
说明:三段实验使用同一个模型(默认
Qwen/Qwen2.5-7B-Instruct,需约 14GB 显存跑 FP16,RTX 4090 24GB 足够;显存不足可换Qwen/Qwen2.5-1.5B-Instruct)。INT8 / INT4 不是去下载"别人量化好的模型",而是下载同一份 FP16 原始权重,在本地加载时即时量化。再补一点容易搞混的:bitsandbytes 的量化只发生在显存里,不会写回磁盘、也不会覆盖原始模型——磁盘上始终只有那份原始 FP16 权重,每次以 INT8/INT4 加载都会重新量化一遍,程序退出后量化结果即消失。如果想把量化后的模型存成一份新的 INT4 模型,需要用第 5 章的 GPTQ(
save_pretrained()会新建目录保存,默认也不覆盖原模型)。
第 1 步:下载模型(会缓存到默认路径)
第一次运行会把模型下载到本地默认缓存目录,之后各步直接读缓存、不再重复下载。
- Windows 默认缓存:
C:\Users\<你的用户名>\.cache\huggingface\hub - Linux/macOS 默认缓存:
~/.cache/huggingface/hub
# ===== 第 1 步:下载模型到本地默认缓存 =====
import os
# 国内网络:走 HF 镜像(必须在 import transformers 之前设置)
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HUB_DISABLE_XET"] = "1" # 关闭 Xet,退回普通下载(镜像才能代理,避免 401)
from huggingface_hub import snapshot_download
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" # 24GB 显存(如 RTX 4090)可跑;显存不足可换 "Qwen/Qwen2.5-1.5B-Instruct"
# 下载到默认缓存目录并返回本地路径
local_path = snapshot_download(repo_id=MODEL_ID)
print("模型已下载到:", local_path)
第 2 步:本地量化前,先跑一次 FP16 答复(基准)
在做任何量化之前,先用原始 FP16 模型对同一个 prompt 生成一次答复,作为对比基准。
# ===== 第 2 步:FP16 基准(量化前) =====
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HUB_DISABLE_XET"] = "1" # 关闭 Xet,退回普通下载(镜像才能代理,避免 401)
import time, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
# 三组都用完全相同的 prompt(这是对比的前提)
PROMPT = "请解释什么是 Transformer,并说明 Self-Attention 的基本工作原理。"
def run_once(model, tokenizer, prompt, max_new_tokens=128):
"""跑一次生成,返回 (答复文本, tokens/s)。用贪心解码保证可复现。"""
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
if torch.cuda.is_available():
torch.cuda.synchronize()
t0 = time.perf_counter()
out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
dt = time.perf_counter() - t0
new_tokens = out.shape[-1] - inputs["input_ids"].shape[-1]
text = tokenizer.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
return text, new_tokens / dt
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
t0 = time.perf_counter()
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID, torch_dtype=torch.float16, device_map="auto"
)
load_time = time.perf_counter() - t0
text, tps = run_once(model, tokenizer, PROMPT)
peak = torch.cuda.max_memory_allocated() / 1024**3 if torch.cuda.is_available() else 0
print("===== FP16 =====")
print(f"加载耗时: {load_time:.1f}s | 峰值显存: {peak:.2f} GB | 速度: {tps:.1f} tokens/s")
print("答复:\n", text)
# 释放显存,给后面的量化实验腾地方
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
在本节实验环境(RTX 4090 24GB,Qwen2.5-7B-Instruct)下,第 2 步的实际运行结果如下:
===== FP16 =====
加载耗时: 4.2s | 峰值显存: 14.21 GB | 速度: 54.0 tokens/s
答复:
Transformer 是一种基于自注意力机制(Self-Attention)的神经网络架构,最初由 Vaswani 等人在 2017 年提出,
主要用于自然语言处理任务,如机器翻译、文本生成等。Transformer 架构在处理序列数据时表现出色,因为它能够
并行处理输入序列中的所有元素,从而大大提高了训练和推理的速度。
Self-Attention 是 Transformer 中的核心组件之一,它允许模型关注输入序列中不同位置的信息。
Self-Attention 的基本工作原理如下:
1. 输入:给定一个长度为 N 的输入序列,每个元素可以是一个词或词向量。
2. 查询(Query)、键(Key)和值(Value):对于序列中的每个元素,我们计算三个向量:查询向量、键向量和
值向量。这些向量通常通过线性变换从原始输入向量得到。
3. 计算注意力分数:对于序列中的每个元素,我们计算与其他元素之间的注意力分数。这可以通过计算查询向量与
键向量的点积来实现,然后应用一个缩放因子和一个激活函数(如 softmax)来归一化分数。
4. 加权求和:根据注意力分数对值向量进行加权求和,得到最终的输出向量。这个过程可以看作是将序列中不同位置
的信息加权组合在一起。
通过 Self-Attention,Transformer 可以有效地捕捉输入序列中的长距离依赖关系,而无需像传统的循环神经网络
(RNN)那样依赖于顺序处理。此外,Self-Attention 还具有并行化的优势,使得 Transformer 在处理大规模序列
数据时更加高效。
其中峰值显存 14.21 GB 与 7B 模型 FP16 理论占用(约 7 B × 2 Byte ≈ 14 GB 7\text{B}\times 2\text{Byte}\approx 14\text{GB} 7B×2Byte≈14GB)高度吻合,这就是后面 INT8 / INT4 两组要对比的基准线。
提示:默认
max_new_tokens=128可能导致答复被截断;想看完整答复可调大(如 512)。但做三档对比时,三组必须用相同的max_new_tokens,速度和质量对比才公平。
第 3 步:分批量化并各跑一次答复(INT8 / INT4)
下面用一个循环,依次以 INT8、INT4 加载同一个模型并生成答复。每一组:加载 → 记录加载时间/峰值显存 → 生成答复 → 记录 tokens/s → 释放显存,再进入下一组。
# ===== 第 3 步:分批量化 + 各跑一次答复 =====
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["HF_HUB_DISABLE_XET"] = "1" # 关闭 Xet,退回普通下载(镜像才能代理,避免 401)
import time, torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
PROMPT = "请解释什么是 Transformer,并说明 Self-Attention 的基本工作原理。"
def run_once(model, tokenizer, prompt, max_new_tokens=128):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
if torch.cuda.is_available():
torch.cuda.synchronize()
t0 = time.perf_counter()
out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
if torch.cuda.is_available():
torch.cuda.synchronize()
dt = time.perf_counter() - t0
text = tokenizer.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True)
new_tokens = out.shape[-1] - inputs["input_ids"].shape[-1]
return text, new_tokens / dt
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
# 两组量化配置:INT8 和 INT4
configs = {
"INT8": BitsAndBytesConfig(load_in_8bit=True),
"INT4": BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16),
}
results = {}
for name, quant_config in configs.items():
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
t0 = time.perf_counter()
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID, quantization_config=quant_config, device_map="auto"
)
load_time = time.perf_counter() - t0
text, tps = run_once(model, tokenizer, PROMPT)
peak = torch.cuda.max_memory_allocated() / 1024**3 if torch.cuda.is_available() else 0
results[name] = (load_time, peak, tps, text)
print(f"===== {name} =====")
print(f"加载耗时: {load_time:.1f}s | 峰值显存: {peak:.2f} GB | 速度: {tps:.1f} tokens/s")
print("答复:\n", text, "\n")
del model
if torch.cuda.is_available():
torch.cuda.empty_cache()
跑完这三步,你就得到了同一个模型在 FP16 / INT8 / INT4 三种精度下的:加载时间、峰值显存、生成速度,以及对同一个 prompt 的三份答复——把它们并排放在一起,就是 4.5.1 说的那组对比。
在本节实验环境(RTX 4090 24GB,Qwen2.5-7B-Instruct)下,第 3 步的实际运行结果如下:
===== INT8 =====
加载耗时: 8.6s | 峰值显存: 8.34 GB | 速度: 21.7 tokens/s
答复:
Transformer 是一种基于注意力机制的神经网络架构,最初被用于自然语言处理任务,特别是机器翻译。它由
Vaswani 等人在 2017 年提出,是当前许多先进 NLP 模型的基础。
Transformer 由编码器和解码器两部分组成,其中编码器将输入序列转换为固定长度的向量表示,而解码器则根据
这些向量生成输出序列。编码器和解码器都包含多个相同的层,每一层都包括自注意力(Self-Attention)机制、
前馈神经网络(Feed-Forward Neural Network)以及残差连接(Residual Connection)和层归一化(Layer
Normalization)等技术。
Self-Attention 是 Transformer 中的核心组件之一,它的基本工作原理如下:
1. 输入:给定一个序列,例如一个句子中的单词或字符。每个元素(如单词或字符)都有一个对应的嵌入向量。
2. Q、K 和 V 向量:对于序列中的每个元素,我们计算三个向量:查询向量(Query, Q)、键向量(Key, K)和
值向量(Value, V)。这些向量通常通过线性变换从原始嵌入向量中得到。
3. 计算注意力分数:对于序列中的每个元素,我们计算与其他所有元素之间的注意力分数。这可以通过计算 Q 和 K
向量的点积并除以根号下 K 向量维度来实现。这样可以确保分数不会过大或过小。
4. 应用 softmax 函数:将注意力分数进行归一化,使得每个元素的总和为 1。这一步骤确保了注意力权重的合理分配。
5. 加权求和:使用 softmax 函数得到的权重对 V 向量进行加权求和,得到最终的注意力表示。
6. 输出:将注意力表示与原始嵌入向量相加,得到最终的注意力输出。
通过 Self-Attention 机制,Transformer 可以在不依赖于顺序信息的情况下捕捉到序列中的长距离依赖关系。这使得
Transformer 在处理自然语言等序列数据时具有强大的建模能力。同时,Self-Attention 机制也使得模型能够并行
计算,从而提高了训练速度。需要注意的是,Self-Attention 仅关注序列内部的信息,因此被称为“自我”注意力。
在实际应用中,Transformer 还会结合其他技术,
===== INT4 =====
加载耗时: 3.4s | 峰值显存: 5.57 GB | 速度: 85.1 tokens/s
答复:
Transformer 是一种用于处理序列数据的神经网络架构,它在自然语言处理领域取得了巨大的成功。Transformer
通过引入自注意力机制(Self-Attention)来替代传统的循环神经网络和卷积神经网络中的递归和卷积操作。
Self-Attention 的基本工作原理如下:
1. 输入:首先将输入序列进行嵌入表示,得到一个矩阵形式的表示。
2. Q、K 和 V:将矩阵按照列分成三个不同的矩阵,分别称为 Query (Q)、Key (K) 和 Value (V)。
3. 计算注意力分数:对于每个位置 i,计算它与其他所有位置 j 的注意力分数。注意力分数通常使用点积相似度
计算,即 Q_i 和 K_j 的点积除以它们的维度平方根。
4. 归一化:将注意力分数进行归一化,通常使用 softmax 函数。
5. 加权求和:将归一化后的注意力分数与对应的 V 矩阵相乘,然后求和,得到最终的输出向量。
通过 Self-Attention,Transformer 可以关注到整个序列中的任意一对元素之间的关系,而不需要像 RNN 那样依赖于
前一个时间步的信息。此外,Self-Attention 还可以并行计算,从而提高了模型的效率。这些特性使得 Transformer
在处理长序列时具有明显的优势。
三档对比小结
| 指标 | FP16 | INT8 | INT4 |
|---|---|---|---|
| 峰值显存 | 14.21 GB | 8.34 GB | 5.57 GB |
| 加载耗时 | 4.2s | 8.6s | 3.4s |
| 速度(tokens/s) | 54.0 | 21.7 | 85.1 |
| 输出质量 | 完整准确 | 完整准确 | 准确、略简洁 |
从这组数据可以读出几个重要结论:
- 显存随精度显著下降:14.2 GB → 8.3 GB → 5.6 GB,与"4-bit 约为 FP16 的 1/3 ~ 1/4"的理论完全吻合。这是量化最直接的收益。
- 速度并不是"位数越低越慢":这里 INT8 反而最慢(21.7),INT4 反而最快(85.1,甚至超过 FP16)。原因在于 kernel 实现:bitsandbytes 的 INT8(LLM.int8())会做"离群值用 FP16、其余用 INT8"的混合精度分解,小 batch 推理时这个拆分/合并开销明显;而 INT4(NF4)用了高度优化的融合 kernel,加上显存带宽压力更小,所以最快。低比特是否更快,取决于 kernel,而不是位数本身。
- 质量损失很小:三档答复都正确,INT4 只是表述更简洁(省略了残差/LayerNorm 等细节),核心原理无误——说明 4-bit 对这个 7B 模型几乎无损。
注:三档必须使用相同的
max_new_tokens,上面的速度对比才公平。不同硬件、不同 bitsandbytes 版本,速度数字会有差异,但"显存大幅下降、INT4 kernel 更快"这两个趋势通常一致。
提示:
bitsandbytes在 Windows 上的支持偶有问题。若第 3 步报bitsandbytes相关错误,可改用 GPTQ / AWQ 预量化模型(见第 5 章),或在 Linux / WSL2 环境下运行本节实验。安装:pip install -U "bitsandbytes>=0.46.1"。
4.5.4 你可能看到的典型趋势
虽然具体数字需要实际测试,但通常你会观察到类似趋势:
注意:
这是一种理解趋势,不是固定 Benchmark 数字。
4.5.5 INT4 质量可能下降的原因
现在我们可以用本章的知识解释。
因为: 4 b i t = 16 s t a t e s 4bit=16\ states 4bit=16 states 所以:
Q u a n t i z a t i o n E r r o r Quantization\ Error Quantization Error
通常会比: 8 b i t 8bit 8bit 更加明显。
于是:
F P 16 → I N T 8 → I N T 4 FP16 \rightarrow INT8 \rightarrow INT4 FP16→INT8→INT4
随着 bit 降低: Q u a n t i z a t i o n E r r o r Quantization\ Error Quantization Error 可能增加。
不过要注意"可能"两个字——它是趋势,不是必然。回到 4.5.3 的实验:我们对 Qwen2.5-7B-Instruct 分别用 FP16 / INT8 / INT4 回答同一个关于 Transformer 的问题,三档答复都准确、无明显错误,INT4 只是表述更简洁(省略了残差、LayerNorm 等细节),核心原理并没有讲错。也就是说,对这个 7B 模型、这个任务,4-bit 带来的质量下降几乎观察不到。
那"质量可能下降"体现在哪里?通常在更苛刻的场景才明显,例如:
- 更小的模型(参数越少,冗余越少,越经不起量化误差);
- 更依赖精确数值的任务(数学、代码、长链推理);
- 更长的生成(误差会随着 token 逐步累积)。
所以正确的理解是:bit 降低会增大量化误差的上界,但是否表现为可感知的质量下降,取决于模型、任务和量化方法——这正好引出下一节。
4.5.6 有些 INT4 模型几乎没变化的原因
从 4.5.3 的实验也能直接感受到这一点:同样是把 7B 模型压到 4-bit,显存从 14.21 GB 降到 5.57 GB(省了约 60%),但答复质量几乎没变。为什么"位数砍到 1/4,质量却没怎么掉"?
因为量化误差并不是简单:
B i t ↓ ⇒ Q u a l i t y ↓ Bit\downarrow \Rightarrow Quality\downarrow Bit↓⇒Quality↓
还受到:
Scale
Group Size
Weight Distribution
Quantization Method
Calibration
Model Architecture
等因素影响。
结合本节实验看:我们用的 bitsandbytes INT4 采用 NF4 这种更贴合权重分布的量化格式,加上 7B 模型本身权重冗余较大,所以 4-bit 下质量损失很小。但换一个量化方法(比如朴素的均匀量化)、或换一个更小/更敏感的模型,同样是 4-bit,结果可能明显变差。
所以:
同样是 4-bit,不同量化方法、不同模型可能出现明显不同的效果。
这就是后面 GPTQ、AWQ 等章节存在的原因——它们通过更聪明的量化策略(如误差补偿、按重要性缩放),在同样的 4-bit 预算下把质量损失压得更低。
4.5.7 4-bit 的真正意义是什么?
现在可以回答本章标题:
LLM 4-bit 量化到底意味着什么?
它并不是简单地说:
“把模型变成 4 个 bit。”
真正含义是:
用极少的离散数值等级近似表示原来的高精度权重 \boxed{ 用极少的离散数值等级近似表示原来的高精度权重 } 用极少的离散数值等级近似表示原来的高精度权重
同时通过:
Scale
Group
Quantization Scheme
等手段尽量降低: Q u a n t i z a t i o n E r r o r Quantization\ Error Quantization Error 最终实现:
M e m o r y ↓ S t o r a g e ↓ B a n d w i d t h ↓ \boxed{ Memory\ ↓ Storage\ ↓ Bandwidth\ ↓ } Memory ↓Storage ↓Bandwidth ↓
同时尽量维持:
M o d e l Q u a l i t y \boxed{ Model\ Quality } Model Quality
4.5.8 4-bit 不是魔法,而是一个取舍
可以把整个过程理解成:
通常: B i t W i d t h ↓ BitWidth\downarrow BitWidth↓ 意味着:
M e m o r y ↓ Memory\downarrow Memory↓
但: Q u a l i t y Quality Quality 可能受到更大影响。于是出现 Memory 与 Quality 之间的基本权衡:
M e m o r y ↔ Q u a l i t y \boxed{ Memory \leftrightarrow Quality } Memory↔Quality
而 4-bit 恰好落在这个权衡里一个很有吸引力的位置: F P 16 → I N T 8 FP16 \rightarrow INT8 FP16→INT8 已能显著减少显存, I N T 8 → I N T 4 INT8 \rightarrow INT4 INT8→INT4 又进一步减半,在 Memory、Quality、Speed 之间形成了一个平衡区域。
所以 4-bit 之所以重要,不是因为它"理论上完美",而是因为它在工程上非常有价值。
4.5.9 但是不同 4-bit 方案不能混为一谈
这一点从本章开始就需要建立。
后面我们会看到:
INT4
GPTQ-4bit
AWQ-4bit
NF4
GGUF-Q4
FP4
这些名字都可能带: 4 b i t 4bit 4bit 但它们:
不是完全相同的技术。
所以看到:
4-bit
以后还应该继续问:
什么 4-bit?
怎么量化?
怎么分组?
怎么存?
怎么计算?
哪个 Runtime?
4.5.10 本章与后面章节的关系
现在可以把后面的学习路线理解得更加自然:
这样每一章都在回答一个新的问题,而不是重复介绍“量化”。
4.5.11 本章核心代码实验
可以把整个第4章实验浓缩成下面三个模型:
统一测试:
Model Size
VRAM
Load Time
Tokens/s
Output Quality
最终得到:
results.csv
这张表会成为后面第13章综合 Benchmark 的基础。
4.6 本章小结
这一章我们没有学习复杂的 GPTQ,也没有学习 AWQ,更没有进入 QAT。
我们只做了一件非常重要的事情:
把“4-bit”真正理解清楚。
现在我们知道:
4 b i t = 16 d i s c r e t e s t a t e s \boxed{ 4bit=16\ discrete\ states } 4bit=16 discrete states
它意味着:
原来的连续高精度权重,被映射到有限的离散数值等级。
4.6.1 4-bit 让模型变小的原理
因为:
$$
Memory
Parameters\times
\frac{Bits}{8}
$$
例如: 7 B F P 16 ≈ 14 G B 7B\ FP16\approx14GB 7B FP16≈14GB 而:
7 B I N T 4 ≈ 3.5 G B 7B\ INT4\approx3.5GB 7B INT4≈3.5GB
理论上降低约: 4 × 4\times 4× 权重存储。
4.6.2 4-bit 仍能工作的原因
因为:
LLM 权重具有一定结构
+
模型存在冗余性
+
合理量化可以控制误差
+
不是每一个权重都同等敏感
因此:
L o w P r e c i s i o n ≠ N o I n f o r m a t i o n \boxed{ Low\ Precision \neq No\ Information } Low Precision=No Information
4-bit 虽然丢失了很多精度,但仍然可以保留足够的有效信息。
4.6.3 W4A16 是什么?
$$
\boxed{
W4A16
4-bit\ Weight
+
16-bit\ Activation
}
$$
核心思想:
Weight
→ 强力压缩
Activation
→ 保持较高精度
所以它是一个非常重要的:
M e m o r y ↔ Q u a l i t y \boxed{ Memory \leftrightarrow Quality } Memory↔Quality
折中。
4.6.4 最重要的几个概念
4.6.6 从本章进入第5章
现在我们已经知道:
4-bit 有巨大的价值,但也有明显的量化误差。
于是下一个问题自然产生:
如果只是简单地
round(),4-bit 精度可能损失比较大,那么有没有一种更加聪明的方法,让 4-bit 尽可能接近原始 FP16 模型?
答案就是:
G P T Q \boxed{ GPTQ } GPTQ
下一章我们将从:
“简单 R o u n d ” \boxed{ “简单 Round” } “简单Round”
开始,逐步理解:
“误差最小化” \boxed{ “误差最小化” } “误差最小化”
最后回答:
GPTQ 为什么不是简单地把每一个 Weight Round 一下,而是要利用 Hessian、Layer Reconstruction 和 Error Compensation?
这也正是从“理解 4-bit”进入“理解真正的 LLM 4-bit 量化算法”的关键一步。

360

被折叠的 条评论
为什么被折叠?



