什么是量化?
量化(Quantization)是将模型参数和激活值从高精度表示(如FP32、FP16)转换为低精度表示(如INT8、INT4)的过程。
为什么需要量化?
以LLaMA-70B为例,看看不同精度下的存储需求:
| 精度 | 每个参数大小 | 模型总大小 | 显存占用(推理) |
|---|---|---|---|
| FP32 | 4 bytes | 280 GB | ~300 GB |
| FP16 | 2 bytes | 140 GB | ~160 GB |
| INT8 | 1 byte | 70 GB | ~80 GB |
| INT4 | 0.5 byte | 35 GB | ~45 GB |
问题:
- FP32:需要4个A100 (80GB)才能加载
- FP16:需要2个A100
- INT8:1个A100就够
- INT4:半个A100即可
量化的三大好处:
- 减少显存占用:可以在更小的设备上运行大模型
- 加速推理:INT运算比浮点运算快
- 降低成本:可以用更便宜的硬件
量化的基本概念
数值表示回顾
浮点数(Floating Point):
FP32: 1位符号 + 8位指数 + 23位尾数 = 32位
FP16: 1位符号 + 5位指数 + 10位尾数 = 16位
表示范围:
- FP32: ±3.4 × 10^38
- FP16: ±6.5 × 10^4
整数(Integer):
INT8: 有符号整数,范围 -128 到 127
INT4: 有符号整数,范围 -8 到 7
量化的本质
量化就是建立一个映射函数,将连续的浮点数空间映射到离散的整数空间:
其中:
- :原始浮点值
- :量化后的整数值
- :缩放因子(scale)
- :零点(zero point)
反量化(恢复):
量化的类型
1. 对称量化 vs 非对称量化
对称量化(Symmetric Quantization)
特点:零点固定为0,数值范围对称
缩放因子计算:
其中 是量化位数(如INT8,)
例子(INT8对称量化):
原始值范围:[-12.8, 12.8]
量化范围:[-127, 127]
s = 12.8 / 127 = 0.1007
量化:
12.8 → Round(12.8 / 0.1007) = 127
0.0 → Round(0.0 / 0.1007) = 0
-6.4 → Round(-6.4 / 0.1007) = -64
反量化:
127 → 127 × 0.1007 = 12.79
0 → 0 × 0.1007 = 0.0
-64 → -64 × 0.1007 = -6.44
优点:
- 简单,零点为0,计算快
- 适合权重(通常均值接近0)
缺点:
- 如果数据不对称(如ReLU激活值都是正数),会浪费一半量化范围
非对称量化(Asymmetric Quantization)
特点:零点可以不为0,更灵活
参数计算:
例子(INT8非对称量化):
原始值范围:[0.0, 12.8](ReLU激活值,全是正数)
量化范围:[0, 255](使用无符号INT8)
s = (12.8 - 0.0) / 255 = 0.0502
z = 0
量化:
12.8 → Round((12.8 - 0) / 0.0502) = 255
6.4 → Round((6.4 - 0) / 0.0502) = 127
0.0 → Round((0.0 - 0) / 0.0502) = 0
反量化:
255 → 255 × 0.0502 = 12.8
127 → 127 × 0.0502 = 6.37
0 → 0 × 0.0502 = 0.0
优点:
- 充分利用量化范围,精度更高
- 适合激活值(通常不对称)
缺点:
- 需要存储零点z,计算稍慢
2. 逐张量量化 vs 逐通道量化
逐张量量化(Per-Tensor Quantization)
定义:整个张量使用一组
# 权重矩阵 W: (4096, 4096)
# 计算全局的 s 和 z
s = (W.max() - W.min()) / 255
z = -round(W.min() / s)
# 量化整个矩阵
W_quant = round((W - z) / s)
优点:
- 简单,只需存储一个scale
- 计算快
缺点:
- 如果张量内不同维度的数值范围差异大,精度损失大
逐通道量化(Per-Channel Quantization)
定义:每个输出通道使用独立的
# 权重矩阵 W: (out_channels, in_channels)
# 对每个输出通道独立量化
for i in range(out_channels):
s[i] = (W[i].max() - W[i].min()) / 255
z[i] = -round(W[i].min() / s[i])
W_quant[i] = round((W[i] - z[i]) / s[i])
优点:
- 精度更高(考虑了每个通道的特性)
- 现代硬件支持良好
缺点:
- 需要存储多个scale(但相比权重本身可忽略)
对比示例
假设权重矩阵的两个通道:
# 通道1:值在 [-0.5, 0.5] 范围
# 通道2:值在 [-5.0, 5.0] 范围
逐张量量化:
全局范围:[-5.0, 5.0]
s = 10.0 / 255 = 0.0392
通道1的精度:0.0392(浪费了,因为它的范围小)
通道2的精度:0.0392
逐通道量化:
通道1: s1 = 1.0 / 255 = 0.0039(精度提升10倍!)
通道2: s2 = 10.0 / 255 = 0.0392
结论:逐通道量化是主流选择,尤其对于权重量化。
3. 训练后量化 vs 量化感知训练
训练后量化(Post-Training Quantization, PTQ)
定义:在已训练好的模型上直接应用量化
流程:
1. 加载预训练模型(FP16)
2. 校准(用少量数据确定scale和zero point)
3. 量化权重和激活
4. 保存量化模型
优点:
- 简单,无需重新训练
- 适用于任何已训练模型
缺点:
- 精度损失可能较大(特别是INT4)
- 需要校准数据集
量化感知训练(Quantization-Aware Training, QAT)
定义:在训练过程中模拟量化,让模型适应量化误差
流程:
1. 从预训练模型开始(或从零开始)
2. 在前向传播中插入"伪量化"节点
3. 正常训练(反向传播仍使用FP32)
4. 训练完成后导出真正的量化模型
伪量化(Fake Quantization):
def fake_quantize(x, scale, zero_point, num_bits=8):
# 量化
x_quant = torch.round((x - zero_point) / scale)
x_quant = torch.clamp(x_quant, 0, 2**num_bits - 1)
# 反量化(仍用FP32表示)
x_dequant = x_quant * scale + zero_point
return x_dequant # 梯度可以反向传播
优点:
- 精度更高(模型学会适应量化误差)
- 可以达到接近FP16的精度
缺点:
- 需要重新训练(成本高)
- 需要训练数据和计算资源
对比
| 方法 | 训练成本 | 精度 | 适用场景 |
|---|---|---|---|
| PTQ | 无(只需校准) | 中等 | 快速部署,资源受限 |
| QAT | 高(需重训练) | 高 | 追求极致精度 |
实际应用:
- 大模型通常使用PTQ(训练成本太高)
- 小模型或关键应用使用QAT
权重量化 vs 激活量化
权重量化(Weight-Only Quantization)
定义:只量化模型权重,激活值保持FP16
流程:
1. 权重存储为INT4/INT8
2. 推理时,反量化权重到FP16
3. 与FP16激活值进行FP16矩阵乘法
代码示例:
class QuantizedLinear(nn.Module):
def __init__(self, weight_int8, scale, zero_point):
self.weight_int8 = weight_int8 # INT8权重
self.scale = scale
self.zero_point = zero_point
def forward(self, x):
# x: FP16激活值
# 反量化权重到FP16
weight_fp16 = self.weight_int8.float() * self.scale + self.zero_point
# FP16矩阵乘法
output = F.linear(x, weight_fp16)
return output
优点:
- 显存占用减少(权重占大部分显存)
- 实现简单
- 精度损失小
缺点:
- 计算仍是FP16,速度提升有限
- 需要反量化开销
适用场景:
- 显存受限但算力充足的情况
- 大模型推理(如LLaMA-70B在单卡上运行)
激活量化(Activation Quantization)
定义:同时量化权重和激活值
流程:
1. 权重和激活都是INT8
2. 使用INT8矩阵乘法
3. 输出可能需要反量化(取决于下一层)
代码示例:
class FullyQuantizedLinear(nn.Module):
def __init__(self, weight_int8, weight_scale, weight_zp):
self.weight_int8 = weight_int8
self.weight_scale = weight_scale
self.weight_zp = weight_zp
def forward(self, x_int8, x_scale, x_zp):
# x_int8: INT8激活值
# INT8矩阵乘法(硬件加速)
output_int32 = torch.matmul(
x_int8.to(torch.int32),
self.weight_int8.to(torch.int32).T
)
# 计算输出的scale(scale相乘)
output_scale = x_scale * self.weight_scale
output_zp = 0 # 简化
# 可选:反量化到FP16(如果下一层需要)
output_fp16 = (output_int32 - output_zp).float() * output_scale
return output_fp16
优点:
- 计算速度快(INT8乘法比FP16快3-4倍)
- 显存和速度双优化
缺点:
- 精度损失较大
- 激活值的分布难以预测(动态范围)
- 需要硬件支持INT8算子
适用场景:
- 算力受限的边缘设备
- 需要极致速度的场景
对比总结
| 类型 | 量化对象 | 显存节省 | 速度提升 | 精度损失 | 复杂度 |
|---|---|---|---|---|---|
| 权重量化 | 仅权重 | 高(2-4倍) | 低(10-20%) | 小 | 低 |
| 激活量化 | 权重+激活 | 很高(2-4倍) | 高(2-4倍) | 大 | 高 |
实际选择:
- 大模型推理:主流使用权重量化(平衡精度和显存)
- 边缘设备:使用激活量化(追求速度)
大模型量化的挑战
挑战1:异常值(Outliers)
问题:Transformer模型中存在极端的异常值
# 某层的激活值分布
大部分值:[-1.0, 1.0]
异常值:[-100, 100](占比<0.1%,但影响巨大)
逐张量量化:
范围:[-100, 100]
s = 200 / 255 = 0.784
大部分值的精度:只能表示 [-1.0, 1.0] 范围内的2-3个离散值
精度损失巨大!
原因:
- 注意力机制的Softmax后,某些位置的权重特别大
- LayerNorm后某些通道的值异常
解决方案1:逐通道量化
# 对每个通道独立量化
# 异常值只影响自己的通道,不影响其他通道
解决方案2:混合精度(Mixed Precision)
# 对有异常值的通道使用FP16
# 其他通道使用INT8
解决方案3:异常值提取
# 将异常值单独存储为FP16
# 其他值量化为INT8
# 推理时分别计算再合并
挑战2:动态范围
问题:激活值的范围在不同输入下变化很大
输入1:"Hello" → 激活范围 [-5, 5]
输入2:"The quick brown fox..." → 激活范围 [-50, 50]
如果用静态scale(校准时确定),会导致精度损失或溢出
解决方案1:动态量化
def dynamic_quantize(x):
# 每次推理时重新计算scale
x_max = x.abs().max()
scale = x_max / 127
x_quant = torch.round(x / scale).clamp(-128, 127)
return x_quant, scale
缺点:需要额外计算max操作,增加延迟
解决方案2:分段量化
# 将激活值分成多个范围,每个范围用不同的scale
ranges = [(-10, -1), (-1, 1), (1, 10)]
scales = [0.08, 0.008, 0.08]
挑战3:不同层的敏感性
观察:不同层对量化的敏感度不同
实验(LLaMA-7B量化到INT4):
量化所有层:困惑度 (Perplexity) = 15.2
只量化第1-10层:困惑度 = 7.8(接近FP16的7.5)
只量化最后10层:困惑度 = 50.3(崩溃!)
结论:最后几层对量化非常敏感
解决方案:分层量化策略
# 浅层:INT4(不太敏感)
layers[0:20] → INT4
# 中层:INT8(适中)
layers[20:60] → INT8
# 深层:FP16(敏感)
layers[60:] → FP16
主流量化方案
1. GPTQ(Accurate Post-Training Quantization)
核心思想:基于二阶信息优化量化误差
问题:朴素PTQ直接round,误差大
GPTQ方法:考虑Hessian矩阵(二阶导数),优化重建误差
算法流程:
def gptq_quantize(W, X):
"""
W: 权重矩阵 (out_features, in_features)
X: 校准数据的激活值
"""
# 1. 计算Hessian矩阵
H = X @ X.T / X.shape[0]
# 2. 逐列量化(贪心算法)
W_q = torch.zeros_like(W)
for i in range(W.shape[1]):
# 量化第i列
w = W[:, i]
q = quantize(w) # 量化
# 计算量化误差
delta = (w - q)
# 用Hessian调整后续列,补偿误差
W[:, i+1:] -= delta.unsqueeze(1) @ H[i, i+1:].unsqueeze(0) / H[i, i]
W_q[:, i] = q
return W_q
特点:
- 精度高(比朴素PTQ好很多)
- 速度快(只需几小时量化70B模型)
- 支持INT4/INT3/INT2
缺点:
- 需要校准数据
- 需要计算和存储Hessian(内存开销大)
实际效果(LLaMA-65B):
| 量化方法 | 精度 | WikiText困惑度 | 相对误差 |
|---|---|---|---|
| FP16 | 16 bit | 3.53 | - |
| Round PTQ | 4 bit | 11.2 | +218% |
| GPTQ | 4 bit | 3.98 | +13% |
2. AWQ(Activation-aware Weight Quantization)
核心观察:不是所有权重都同等重要
发现:对应大激活值的权重更重要
# 某层的激活值和权重
激活值:[0.1, 0.2, 50.0, 0.3] # 第3个通道有异常值
权重: [2.0, 1.5, 0.8, 1.2]
朴素量化:所有权重同等量化
误差 = 量化误差 × 激活值
第3个通道的误差 = e × 50.0(巨大!)
AWQ:保护重要权重
第3个通道用更高精度或跳过量化
算法流程:
def awq_quantize(W, X):
"""
W: 权重矩阵
X: 激活值(校准数据)
"""
# 1. 计算每个通道的激活值幅度
salient_score = torch.abs(X).mean(dim=0) # 每个输入通道的重要性
# 2. 对权重进行缩放(放大重要通道)
s = salient_score ** 0.5 # 缩放因子
W_scaled = W * s.unsqueeze(0)
# 3. 量化缩放后的权重
W_q = quantize(W_scaled)
# 4. 量化缩放因子(或保持FP16)
s_q = quantize(s) # 可选
# 推理时:W_q / s_q(相当于恢复原始权重的量化版本)
return W_q, s_q
特点:
- 专门处理异常值问题
- 保护重要权重
- 实现简单
实际效果(LLaMA-7B,INT4):
| 量化方法 | WikiText困惑度 | 相对FP16 |
|---|---|---|
| FP16 | 5.68 | - |
| GPTQ | 6.23 | +9.7% |
| AWQ | 5.87 | +3.3% |
3. GGUF/GGML(llama.cpp量化)
特点:专为CPU推理优化的量化格式
支持的量化类型:
Q4_0: 4-bit整数,逐块量化(32个元素一组)
Q4_1: 4-bit + 1-bit符号
Q5_0: 5-bit整数
Q8_0: 8-bit整数
Q4_K_M: 4-bit混合精度(K-quants)
Q4_K_M(推荐):
# 混合精度策略
attention.wq, attention.wk → Q4_K(4-bit)
attention.wv, attention.wo → Q5_K(5-bit,稍微重要)
ffn.w1, ffn.w2, ffn.w3 → Q4_K(4-bit)
attention_norm, ffn_norm → FP16(LayerNorm保持精度)
output.weight → Q6_K(6-bit,最后一层很重要)
优点:
- CPU推理友好(无需GPU)
- 多种精度选择
- 社区生态好(HuggingFace集成)
实际效果(LLaMA-13B):
| 量化方法 | 模型大小 | 内存占用 | 速度(CPU) | 困惑度 |
|---|---|---|---|---|
| FP16 | 26 GB | 28 GB | 2 tok/s | 5.09 |
| Q8_0 | 13.5 GB | 15 GB | 8 tok/s | 5.11 |
| Q4_K_M | 7.3 GB | 9 GB | 12 tok/s | 5.23 |
| Q4_0 | 7.0 GB | 8.5 GB | 14 tok/s | 5.46 |
4. SmoothQuant
针对问题:激活值的异常值导致激活量化困难
核心思想:将激活值的难度"转移"到权重
观察:
Y = X @ W
激活X有异常值(难量化)
权重W相对平滑(易量化)
想法:能否让X变平滑,W承担更多变化?
方法:插入缩放因子
其中:
- :激活值除以缩放,变平滑
- :权重乘以缩放,吸收变化
缩放因子选择:
其中 控制平衡(通常0.5)
代码示例:
def smooth_quant(X, W, alpha=0.5):
# 计算缩放因子
x_max = torch.abs(X).max(dim=0)[0] # 每个输入通道的最大值
w_max = torch.abs(W).max(dim=0)[0] # 每个输出通道的最大值
s = (x_max ** alpha) / (w_max ** (1 - alpha))
# 应用缩放
X_smooth = X / s.unsqueeze(0)
W_smooth = W * s.unsqueeze(1)
# 量化
X_q = quantize_int8(X_smooth)
W_q = quantize_int8(W_smooth)
# 推理
Y_q = torch.matmul(X_q, W_q.T)
return Y_q
特点:
- 让激活量化成为可能
- 无需重新训练
- 适合需要激活量化的场景
实际效果(OPT-175B,INT8激活+权重):
| 方法 | WikiText困惑度 |
|---|---|
| FP16 | 10.86 |
| 朴素INT8 | 461.2(崩溃) |
| SmoothQuant | 11.08(+2%) |
实战:量化LLaMA模型
方案1:使用GPTQ(推荐)
步骤1:安装依赖
pip install auto-gptq transformers accelerate
步骤2:量化模型
from transformers import AutoTokenizer, AutoModelForCausalLM
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
# 1. 加载模型
model_id = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 2. 定义量化配置
quantize_config = BaseQuantizeConfig(
bits=4, # 量化位数
group_size=128, # 分组大小
desc_act=False, # 是否量化激活
)
# 3. 加载模型并量化
model = AutoGPTQForCausalLM.from_pretrained(
model_id,
quantize_config=quantize_config
)
# 4. 准备校准数据
from datasets import load_dataset
calibration_dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_dataset = [
tokenizer(text, return_tensors="pt").input_ids
for text in calibration_dataset["text"][:1000]
]
# 5. 执行量化
model.quantize(calibration_dataset)
# 6. 保存量化模型
output_dir = "./llama-2-7b-gptq-4bit"
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
步骤3:加载和推理
from auto_gptq import AutoGPTQForCausalLM
from transformers import AutoTokenizer
# 加载量化模型
model = AutoGPTQForCausalLM.from_quantized(
"./llama-2-7b-gptq-4bit",
device="cuda:0"
)
tokenizer = AutoTokenizer.from_pretrained("./llama-2-7b-gptq-4bit")
# 推理
prompt = "The future of AI is"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda:0")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0]))
方案2:使用bitsandbytes(最简单)
from transformers import AutoModelForCausalLM, AutoTokenizer
# 直接加载INT8模型(无需手动量化)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
load_in_8bit=True, # 自动INT8量化
device_map="auto" # 自动分配到GPU
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
# 推理(与FP16完全相同)
prompt = "Once upon a time"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0]))
INT4版本:
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
load_in_4bit=True, # INT4量化
device_map="auto",
bnb_4bit_compute_dtype=torch.float16, # 计算时用FP16
bnb_4bit_quant_type="nf4" # NF4量化类型
)
方案3:使用llama.cpp(CPU推理)
步骤1:转换模型
# 克隆llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make
# 转换HuggingFace模型到GGUF
python convert.py /path/to/llama-2-7b-hf \
--outtype f16 \
--outfile llama-2-7b-f16.gguf
# 量化到INT4
./quantize llama-2-7b-f16.gguf llama-2-7b-q4_k_m.gguf Q4_K_M
步骤2:推理
./main -m llama-2-7b-q4_k_m.gguf \
-p "The meaning of life is" \
-n 128 \
-t 8 # 使用8个CPU线程
Python接口:
from llama_cpp import Llama
# 加载量化模型
llm = Llama(
model_path="./llama-2-7b-q4_k_m.gguf",
n_ctx=2048, # 上下文长度
n_threads=8 # CPU线程数
)
# 生成
output = llm(
"Q: What is the capital of France? A:",
max_tokens=50,
temperature=0.7
)
print(output["choices"][0]["text"])
量化的性能对比
LLaMA-7B性能测试
| 量化方法 | 模型大小 | 显存占用 | WikiText困惑度 | 推理速度 | 相对FP16 |
|---|---|---|---|---|---|
| FP16 | 13 GB | 14 GB | 5.68 | 100% | - |
| INT8 (bitsandbytes) | 7 GB | 9 GB | 5.72 | 120% | +0.7% |
| INT4 (GPTQ) | 3.5 GB | 5 GB | 5.87 | 150% | +3.3% |
| Q4_K_M (llama.cpp) | 4.1 GB | 5.5 GB | 5.91 | 140% | +4.0% |
| INT4 (AWQ) | 3.5 GB | 5 GB | 5.79 | 160% | +1.9% |
LLaMA-70B性能测试
| 量化方法 | 模型大小 | 显存占用 | 能否单卡(A100 80GB) | 困惑度增加 |
|---|---|---|---|---|
| FP16 | 140 GB | 160 GB | ❌ 需要2卡 | - |
| INT8 | 70 GB | 85 GB | ❌ 勉强1卡 | +1.5% |
| INT4 (GPTQ) | 35 GB | 45 GB | ✅ 轻松1卡 | +5.2% |
| INT4 (AWQ) | 35 GB | 43 GB | ✅ 轻松1卡 | +3.8% |
不同任务的精度影响
| 任务类型 | FP16 | INT8 | INT4 | 影响程度 |
|---|---|---|---|---|
| 多项选择(MMLU) | 46.7 | 46.5 | 45.2 | 小 |
| 代码生成(HumanEval) | 32.9 | 32.1 | 29.3 | 中等 |
| 数学推理(GSM8K) | 52.3 | 51.1 | 47.8 | 较大 |
| 对话质量 | 优秀 | 优秀 | 良好 | 小 |
结论:
- INT8几乎无损
- INT4在大多数任务上可接受
- 数学推理对量化最敏感
量化的最佳实践
1. 选择合适的量化方法
决策树:
需要CPU推理?
├─ 是 → llama.cpp (Q4_K_M)
└─ 否 → 需要激活量化?
├─ 是 → SmoothQuant (INT8)
└─ 否 → 需要极致压缩?
├─ 是 → GPTQ/AWQ (INT4)
└─ 否 → bitsandbytes (INT8)
2. 校准数据的选择
原则:
- 使用与实际应用相似的数据
- 数量:128-1000个样本即可
- 覆盖不同长度和风格
推荐数据集:
- 通用:WikiText-2
- 对话:ShareGPT
- 代码:The Stack
- 多语言:mC4
3. 验证量化效果
评估指标:
# 1. 困惑度(Perplexity)
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
def compute_perplexity(model, tokenizer, text):
encodings = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**encodings, labels=encodings.input_ids)
return torch.exp(outputs.loss).item()
# 2. 下游任务评估
from lm_eval import evaluator
results = evaluator.simple_evaluate(
model=model,
tasks=["mmlu", "hellaswag", "arc_easy"],
num_fewshot=5
)
# 3. 人工评估
# 生成样本,人工打分
4. 混合精度策略
分层量化:
# 示例:LLaMA-70B混合精度
quantization_config = {
"layers.0-10": "INT4", # 浅层:激进量化
"layers.11-60": "INT8", # 中层:平衡
"layers.61-79": "FP16", # 深层:保持精度
"lm_head": "FP16", # 输出层:不量化
"layernorm": "FP16" # 归一化层:不量化
}
5. 量化后微调(QAT-Lite)
对于重要应用,可以在量化后做轻量级微调:
# 1. 加载量化模型
model = load_quantized_model("llama-7b-int4")
# 2. 冻结大部分层,只训练adapter
for name, param in model.named_parameters():
if "adapter" not in name:
param.requires_grad = False
# 3. 在目标任务上微调
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset
)
trainer.train()
量化的未来趋势
1. 更低精度(INT2, INT1)
研究方向:
- BitNet(1-bit权重)
- Ternary Quantization(三值量化:-1, 0, 1)
挑战:
- 精度损失大
- 需要特殊训练方法
2. 自适应量化
动态精度调整:
# 根据输入难度动态选择精度
def adaptive_quantization(input_text):
difficulty = estimate_difficulty(input_text)
if difficulty > 0.8:
return "FP16" # 困难任务,高精度
elif difficulty > 0.5:
return "INT8" # 中等任务
else:
return "INT4" # 简单任务,低精度
3. 硬件协同设计
专用量化加速器:
- Google TPU:INT8原生支持
- NVIDIA Tensor Core:INT4/INT8加速
- 未来:INT2/INT1硬件支持
4. 训练时量化(PTQ的替代)
直接训练量化模型:
# 从头开始训练INT4模型(而不是先FP16再量化)
model = QuantizedTransformer(
bits=4,
from_scratch=True
)
# 训练
优点:
- 无精度损失
- 训练和推理统一
挑战:
- 训练困难(梯度消失)
- 需要新的优化器
小结
量化技术对比
| 技术 | 压缩比 | 精度 | 速度 | 适用场景 |
|---|---|---|---|---|
| 权重量化(INT8) | 2x | 高 | +20% | 显存受限 |
| 权重量化(INT4) | 4x | 中等 | +50% | 极致压缩 |
| 激活量化(INT8) | 2x | 中等 | +3x | 边缘设备 |
| 混合精度 | 2-3x | 高 | +2x | 平衡方案 |
量化方法选择
| 需求 | 推荐方法 | 原因 |
|---|---|---|
| 快速部署 | bitsandbytes | 零配置,自动 |
| 最佳精度 | AWQ | 专门优化异常值 |
| 极致压缩 | GPTQ | 支持INT4/INT3 |
| CPU推理 | llama.cpp | 高度优化 |
| 激活量化 | SmoothQuant | 解决异常值问题 |
关键要点
- INT8几乎无损:适合所有场景,应作为默认选择
- INT4需谨慎:虽然压缩比高,但某些任务(如数学推理)精度损失明显
- 激活量化看硬件:CPU场景必备,GPU场景可选
- 分层量化很有效:浅层激进,深层保守
- 校准数据很重要:选择与应用相似的数据
实际建议
开发阶段:
- 使用FP16,保证精度
- 快速迭代
部署阶段:
- 默认INT8(bitsandbytes)
- 显存紧张用INT4(GPTQ/AWQ)
- CPU推理用llama.cpp
关键应用:
- 先PTQ测试
- 精度不够再考虑QAT
- 或使用混合精度
量化是大模型部署的必备技术,掌握量化可以:
- 降低70%以上的显存占用
- 提升50%以上的推理速度
- 让大模型在消费级硬件上运行
随着硬件和算法的发展,量化技术会越来越成熟,未来INT4甚至INT2可能成为主流!