- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言
- 1. 量化基础:理解数值表示方式
- 2. 训练后量化(Post-Training Quantization, PTQ)
- 3. 量化感知训练(Quantization-Aware Training, QAT)
- 4. PyTorch 量化 API
- 5. GPTQ:精确的训练后量化
- 6. AWQ:激活感知权重量化
- 7. GGUF/GGML:llama.cpp 生态系统
- 8. bitsandbytes:LLM 量化库
- 9. SmoothQuant:W8A8 量化
- 10. SpQR:稀疏量化表示
- 11. 量化基准对比
- 12. 实战指南:选择最佳量化方法
- 结语
- 参考资料
引言
随着深度学习模型规模不断增长,推理(Inference)成本和内存需求急剧膨胀。GPT-3 拥有 175B 参数,Llama 3 拥有 70B 参数,若以 FP32 全精度(Full Precision)存储,分别需要 700GB 和 280GB 的内存 — 普通 GPU 甚至无法运行。
模型量化(Model Quantization)是解决这一问题的核心技术。把 32 位浮点数(FP32)权重压缩为 8 位、4 位整数,可以将内存降低 4~8 倍,并将推理速度提升 2~4 倍。而质量损失小得惊人。
本文将从量化的数学原理出发,一路深入到 GPTQ、AWQ、GGUF、bitsandbytes 等最新技术。
1. 量化基础:理解数值表示方式
1.1 浮点数表示(Floating Point)
理解现代深度学习中使用的浮点数格式,是学习量化的起点。
FP32 (Float32)
- 符号位(1 位) + 指数位(8 位) + 尾数位(23 位) = 共 32 位
- 表示范围:约 -3.4e38 ~ 3.4e38
- 精度:约 7 位十进制数字
FP16 (Float16)
- 符号位(1 位) + 指数位(5 位) + 尾数位(10 位) = 共 16 位
- 表示范围:-65504 ~ 65504(比 FP32 窄得多)
- 精度:约 3 位十进制数字
- 存在溢出风险,训练时需要 gradient scaling
BF16 (Brain Float16)
- 符号位(1 位) + 指数位(8 位) + 尾数位(7 位) = 共 16 位
- 保持与 FP32 相同的指数范围,只减少尾数位
- 没有溢出风险,对深度学习训练更安全
- 由 Google Brain 开发,最新 GPU(A100、H100)原生支持
import torch
import numpy as np
# 查看各数据类型每个元素占用的内存大小
x_fp32 = torch.tensor([1.5, -2.3, 0.7], dtype=torch.float32)
x_fp16 = torch.tensor([1.5, -2.3, 0.7], dtype=torch.float16)
x_bf16 = torch.tensor([1.5, -2.3, 0.7], dtype=torch.bfloat16)
print(f"FP32: {x_fp32.element_size()} bytes per element") # 4 bytes
print(f"FP16: {x_fp16.element_size()} bytes per element") # 2 bytes
print(f"BF16: {x_bf16.element_size()} bytes per element") # 2 bytes
# 模型内存计算示例(7B 参数模型)
params = 7e9
fp32_memory_gb = params * 4 / 1e9
fp16_memory_gb = params * 2 / 1e9
int8_memory_gb = params * 1 / 1e9
int4_memory_gb = params * 0.5 / 1e9
print(f"\n7B 模型内存需求:")
print(f"FP32: {fp32_memory_gb:.1f} GB") # 28.0 GB
print(f"FP16: {fp16_memory_gb:.1f} GB") # 14.0 GB
print(f"INT8: {int8_memory_gb:.1f} GB") # 7.0 GB
print(f"INT4: {int4_memory_gb:.1f} GB") # 3.5 GB
1.2 整数表示(Integer)
量化的核心在于把浮点数值映射为整数。
INT8: -128 ~ 127(有符号)或 0 ~ 255(无符号) INT4: -8 ~ 7(有符号)或 0 ~ 15(无符号) INT2: -2 ~ 1(有符号)或 0 ~ 3(无符号)
1.3 量化公式
将浮点数值 x 转换为整数 q 的基本公式:
q = clamp(round(x / scale) + zero_point, q_min, q_max)
反量化(Dequantization):
x_approx = scale * (q - zero_point)
其中:
- scale:量化缩放因子(scale = (max_val - min_val) / (q_max - q_min))
- zero_point:整数 0 所对应的实数值偏移量
- q_min、q_max:整数范围边界(INT8 为 -128、127)
import torch
import numpy as np
def symmetric_quantize(x: torch.Tensor, num_bits: int = 8):
"""对称量化实现"""
q_max = 2 ** (num_bits - 1) - 1 # INT8 时为 127
q_min = -q_max # -127
# 计算 scale
max_abs = x.abs().max()
scale = max_abs / q_max
# 量化
q = torch.clamp(torch.round(x / scale), q_min, q_max).to(torch.int8)
return q, scale
def asymmetric_quantize(x: torch.Tensor, num_bits: int = 8):
"""非对称量化实现"""
q_max = 2 ** num_bits - 1 # UINT8 时为 255
q_min = 0
# 计算 scale 和 zero_point
min_val = x.min()
max_val = x.max()
scale = (max_val - min_val) / (q_max - q_min)
zero_point = q_min - torch.round(min_val / scale)
zero_point = torch.clamp(zero_point, q_min, q_max).to(torch.int32)
# 量化
q = torch.clamp(torch.round(x / scale) + zero_point, q_min, q_max).to(torch.uint8)
return q, scale, zero_point
def dequantize(q: torch.Tensor, scale: torch.Tensor, zero_point: torch.Tensor = None):
"""反量化"""
if zero_point is None:
return scale * q.float()
return scale * (q.float() - zero_point.float())
# 测试
x = torch.randn(100)
print(f"原始数据范围: [{x.min():.4f}, {x.max():.4f}]")
# 对称量化
q_sym, scale_sym = symmetric_quantize(x)
x_reconstructed_sym = dequantize(q_sym, scale_sym)
error_sym = (x - x_reconstructed_sym).abs().mean()
print(f"对称量化平均误差: {error_sym:.6f}")
# 非对称量化
q_asym, scale_asym, zp_asym = asymmetric_quantize(x)
x_reconstructed_asym = dequantize(q_asym, scale_asym, zp_asym)
error_asym = (x - x_reconstructed_asym).abs().mean()
print(f"非对称量化平均误差: {error_asym:.6f}")
1.4 对称 vs 非对称量化
对称量化(Symmetric Quantization)
- zero_point = 0
- 正负范围对称
- 适合权重量化(大多数呈以 0 为中心的分布)
- 运算更简单:x_approx = scale * q
非对称量化(Asymmetric Quantization)
- zero_point != 0
- 可以表示任意范围
- 适合激活量化(ReLU 之后始终为正)
- 运算更复杂:x_approx = scale * (q - zero_point)
1.5 量化粒度(Quantization Granularity)
决定同一个 scale/zero_point 要应用到多少个参数上。
Per-Tensor: 整个张量使用一个 scale
- 内存开销最小
- 精度损失最大
Per-Channel(Per-Row/Column): 每个通道使用独立 scale
- 权重矩阵的每一行/列各有一个独立 scale
- 能有效处理不同通道之间的分布差异
Per-Group(Per-Block): 每个固定大小的分组使用独立 scale
- group_size = 128 较常见
- 是 Per-Channel 与 Per-Tensor 之间的折衷
- GPTQ、AWQ 中主要采用
import torch
def per_group_quantize(weight: torch.Tensor, group_size: int = 128, num_bits: int = 4):
"""Per-Group 量化实现"""
rows, cols = weight.shape
# 按分组切分
weight_grouped = weight.reshape(-1, group_size)
# 每个分组的最大/最小值
max_vals = weight_grouped.max(dim=1, keepdim=True)[0]
min_vals = weight_grouped.min(dim=1, keepdim=True)[0]
q_max = 2 ** num_bits - 1 # INT4 时为 15
# 计算 scale
scales = (max_vals - min_vals) / q_max
zero_points = torch.round(-min_vals / scales)
# 量化
q = torch.clamp(torch.round(weight_grouped / scales) + zero_points, 0, q_max)
# 反量化
weight_dequant = scales * (q - zero_points)
weight_dequant = weight_dequant.reshape(rows, cols)
return q, scales, zero_points, weight_dequant
# 示例:Transformer 权重量化
weight = torch.randn(4096, 4096) # Llama 风格权重
q, scales, zp, weight_dequant = per_group_quantize(weight, group_size=128, num_bits=4)
error = (weight - weight_dequant).abs().mean()
print(f"Per-Group INT4 量化平均误差: {error:.6f}")
print(f"压缩率: {weight.element_size() * weight.numel() / (q.numel() / 2 + scales.numel() * 4):.2f}x")
2. 训练后量化(Post-Training Quantization, PTQ)
PTQ 是在不重新训练的情况下,对已训练完成的模型进行量化的方法。因为实用性高,是使用最广泛的方式。
2.1 校准数据集(Calibration Dataset)
PTQ 使用少量校准数据来确定合适的 scale/zero_point。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from datasets import load_dataset
def collect_calibration_data(model_name: str, num_samples: int = 128):
"""收集校准数据"""
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 通常使用 WikiText-2 或 C4 数据集
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
texts = []
for item in dataset:
if len(item['text'].strip()) > 100:
texts.append(item['text'].strip())
if len(texts) >= num_samples:
break
# 分词
encoded = [
tokenizer(text, return_tensors="pt", max_length=2048, truncation=True)
for text in texts
]
return encoded
# 用校准数据收集激活统计信息
def collect_activation_stats(model, calibration_data, layer_name: str):
"""收集特定层的激活统计信息"""
stats = {"min": float("inf"), "max": float("-inf"), "histogram": []}
def hook_fn(module, input, output):
with torch.no_grad():
act = output.detach().float()
stats["min"] = min(stats["min"], act.min().item())
stats["max"] = max(stats["max"], act.max().item())
# 注册钩子
target_layer = dict(model.named_modules())[layer_name]
handle = target_layer.register_forward_hook(hook_fn)
# 运行校准数据
model.eval()
with torch.no_grad():
for batch in calibration_data[:32]:
model(**batch)
handle.remove()
return stats
2.2 最小-最大校准(Min-Max Calibration)
最简单的方法,使用整个校准数据集的最小值和最大值。
class MinMaxCalibrator:
"""最小-最大校准器"""
def __init__(self):
self.min_val = float("inf")
self.max_val = float("-inf")
def update(self, tensor: torch.Tensor):
self.min_val = min(self.min_val, tensor.min().item())
self.max_val = max(self.max_val, tensor.max().item())
def compute_scale_zp(self, num_bits: int = 8, symmetric: bool = True):
q_max = 2 ** (num_bits - 1) - 1 if symmetric else 2 ** num_bits - 1
if symmetric:
max_abs = max(abs(self.min_val), abs(self.max_val))
scale = max_abs / q_max
zero_point = 0
else:
scale = (self.max_val - self.min_val) / q_max
zero_point = -round(self.min_val / scale)
return scale, zero_point
2.3 直方图校准(Histogram Calibration)
为降低异常值的影响,基于分布直方图寻找最优范围。
import numpy as np
from scipy import stats
class HistogramCalibrator:
"""基于直方图的校准器(最小化 KL 散度)"""
def __init__(self, num_bins: int = 2048):
self.num_bins = num_bins
self.histogram = None
self.bin_edges = None
def update(self, tensor: torch.Tensor):
data = tensor.detach().float().numpy().flatten()
if self.histogram is None:
self.histogram, self.bin_edges = np.histogram(data, bins=self.num_bins)
else:
new_hist, _ = np.histogram(data, bins=self.bin_edges)
self.histogram += new_hist
def compute_optimal_range(self, num_bits: int = 8):
"""寻找最小化 KL 散度的最优范围"""
num_quantized_bins = 2 ** num_bits - 1
best_kl = float("inf")
best_threshold = None
# 遍历不同的 threshold
for i in range(num_quantized_bins, len(self.histogram)):
# 把直方图压缩为 num_quantized_bins 个区间
reference = self.histogram[:i].copy().astype(float)
reference /= reference.sum()
# 计算 KL 散度(近似)
quantized = np.zeros(i)
bin_size = i / num_quantized_bins
for j in range(num_quantized_bins):
start = int(j * bin_size)
end = int((j + 1) * bin_size)
quantized[start:end] = reference[start:end].sum() / (end - start)
# 处理值为 0 的区间
quantized = np.where(quantized == 0, 1e-10, quantized)
reference_clipped = np.where(reference == 0, 1e-10, reference)
kl = stats.entropy(reference_clipped, quantized)
if kl < best_kl:
best_kl = kl
best_threshold = self.bin_edges[i]
return -best_threshold, best_threshold
2.4 对困惑度的影响
衡量量化质量最常用的指标是困惑度(Perplexity, PPL)。
import torch
import math
from transformers import AutoModelForCausalLM, AutoTokenizer
def compute_perplexity(model, tokenizer, text: str, device: str = "cuda"):
"""计算困惑度"""
encodings = tokenizer(text, return_tensors="pt")
input_ids = encodings.input_ids.to(device)
max_length = 1024
stride = 512
nlls = []
prev_end_loc = 0
for begin_loc in range(0, input_ids.size(1), stride):
end_loc = min(begin_loc + max_length, input_ids.size(1))
trg_len = end_loc - prev_end_loc
input_ids_chunk = input_ids[:, begin_loc:end_loc]
target_ids = input_ids_chunk.clone()
target_ids[:, :-trg_len] = -100
with torch.no_grad():
outputs = model(input_ids_chunk, labels=target_ids)
neg_log_likelihood = outputs.loss
nlls.append(neg_log_likelihood)
prev_end_loc = end_loc
if end_loc == input_ids.size(1):
break
ppl = torch.exp(torch.stack(nlls).mean())
return ppl.item()
# 各模型 PPL 对比示例
# FP16: PPL ≈ 5.68
# INT8: PPL ≈ 5.71(约增加 0.5%)
# INT4 (GPTQ): PPL ≈ 5.89(约增加 3.7%)
# INT4 (naive): PPL ≈ 6.52(约增加 14.8%)
3. 量化感知训练(Quantization-Aware Training, QAT)
QAT 在训练过程中模拟量化,使模型能够适应量化噪声。
3.1 伪量化(Fake Quantization)
用 FP32 模拟量化效果,而不进行真正的 INT8 运算。
import torch
import torch.nn as nn
import torch.nn.functional as F
class FakeQuantize(nn.Module):
"""伪量化模块"""
def __init__(self, num_bits: int = 8, symmetric: bool = True):
super().__init__()
self.num_bits = num_bits
self.symmetric = symmetric
self.register_buffer('scale', torch.tensor(1.0))
self.register_buffer('zero_point', torch.tensor(0))
self.register_buffer('fake_quant_enabled', torch.tensor(1))
if symmetric:
self.q_min = -(2 ** (num_bits - 1))
self.q_max = 2 ** (num_bits - 1) - 1
else:
self.q_min = 0
self.q_max = 2 ** num_bits - 1
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.fake_quant_enabled[0] == 0:
return x
# 更新 scale(滑动平均)
if self.training:
with torch.no_grad():
if self.symmetric:
max_abs = x.abs().max()
new_scale = max_abs / self.q_max
else:
new_scale = (x.max() - x.min()) / (self.q_max - self.q_min)
# 用指数滑动平均更新 scale
self.scale.copy_(0.9 * self.scale + 0.1 * new_scale)
# 伪量化:先量化再反量化
x_scaled = x / self.scale
x_clipped = torch.clamp(x_scaled, self.q_min, self.q_max)
x_rounded = torch.round(x_clipped)
x_dequant = x_rounded * self.scale
return x_dequant
3.2 STE(Straight-Through Estimator)
class STERound(torch.autograd.Function):
"""round() 的 Straight-Through Estimator"""
@staticmethod
def forward(ctx, x):
return torch.round(x)
@staticmethod
def backward(ctx, grad_output):
# 反向传播时直接让梯度穿过 round()(用恒等函数近似)
return grad_output
class STEClamp(torch.autograd.Function):
"""clamp() 的 Straight-Through Estimator"""
@staticmethod
def forward(ctx, x, min_val, max_val):
ctx.save_for_backward(x)
ctx.min_val = min_val
ctx.max_val = max_val
return torch.clamp(x, min_val, max_val)
@staticmethod
def backward(ctx, grad_output):
x, = ctx.saved_tensors
# 只在 clamp 范围内传递梯度
grad = grad_output * ((x >= ctx.min_val) & (x <= ctx.max_val)).float()
return grad, None, None
class QATLinear(nn.Module):
"""应用了 QAT 的 Linear 层"""
def __init__(self, in_features, out_features, num_bits=8):
super().__init__()
self.linear = nn.Linear(in_features, out_features)
self.weight_fake_quant = FakeQuantize(num_bits=num_bits)
self.act_fake_quant = FakeQuantize(num_bits=num_bits, symmetric=False)
def forward(self, x):
# 激活量化
x_q = self.act_fake_quant(x)
# 权重量化
w_q = self.weight_fake_quant(self.linear.weight)
# FP32 运算(实际部署中为 INT8)
return F.linear(x_q, w_q, self.linear.bias)
3.3 什么时候需要 QAT?
- PTQ 造成的质量损失过大时:对小模型(如 BERT-small)尤其有效
- 量化到 INT4 以下时:在极端压缩下维持质量的必要手段
- 特殊任务:目标检测(Object detection)、语音识别(ASR)等对精度敏感的任务
# QAT 训练工作流
import torch.optim as optim
from torch.quantization import prepare_qat, convert
def train_qat_model(model, train_loader, num_epochs=10):
"""QAT 训练示例"""
# 准备 QAT
model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')
model_prepared = prepare_qat(model.train())
optimizer = optim.Adam(model_prepared.parameters(), lr=1e-5)
for epoch in range(num_epochs):
for batch in train_loader:
inputs, labels = batch
outputs = model_prepared(inputs)
loss = F.cross_entropy(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 转换为 INT8 模型
model_prepared.eval()
model_quantized = convert(model_prepared)
return model_quantized
4. PyTorch 量化 API
4.1 torch.ao.quantization
PyTorch 官方提供的量化 API。
import torch
from torch.ao.quantization import (
get_default_qconfig,
get_default_qat_qconfig,
prepare,
prepare_qat,
convert
)
# 静态量化(PTQ)
def static_quantization_example():
"""静态量化示例"""
model = MyModel()
model.eval()
# 后端设置(fbgemm: x86,qnnpack: ARM)
model.qconfig = get_default_qconfig('fbgemm')
# 准备校准
model_prepared = prepare(model)
# 用校准数据收集统计信息
with torch.no_grad():
for data in calibration_loader:
model_prepared(data)
# 转换为 INT8 模型
model_quantized = convert(model_prepared)
return model_quantized
# 动态量化(对 LSTM、Linear 效果好)
def dynamic_quantization_example():
"""动态量化示例"""
model = MyModel()
model_quantized = torch.quantization.quantize_dynamic(
model,
{nn.Linear, nn.LSTM}, # 要量化的层类型
dtype=torch.qint8
)
return model_quantized
4.2 FX 图模式量化
更灵活、更强大的量化方式。
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
from torch.ao.quantization import QConfigMapping
def fx_quantization_example(model, calibration_data):
"""FX 图模式量化"""
model.eval()
# 配置 QConfig
qconfig_mapping = QConfigMapping().set_global(
get_default_qconfig('fbgemm')
)
# 示例输入
example_inputs = (torch.randn(1, 3, 224, 224),)
# 基于 FX 图准备
model_prepared = prepare_fx(
model,
qconfig_mapping,
example_inputs
)
# 校准
with torch.no_grad():
for batch in calibration_data:
model_prepared(batch)
# 转换
model_quantized = convert_fx(model_prepared)
return model_quantized
5. GPTQ:精确的训练后量化
GPTQ 是 2022 年发布的 LLM 专用量化算法,即便在 INT4 量化下也能把质量损失降到最低。(arXiv:2209.05433)
5.1 GPTQ 算法原理
GPTQ 基于 OBQ(Optimal Brain Quantization)。核心思路是:逐层依次量化权重,同时把已量化权重产生的误差补偿到剩余权重上。
OBQ 误差最小化目标函数:
argmin_Q ||WX - QX||_F^2
其中 W 是原始权重,Q 是量化后的权重,X 是输入激活。
基于海森矩阵的权重更新:
在量化每个权重之后,用海森矩阵的逆 H^(-1) 把产生的误差传播到剩余权重上。
# GPTQ 核心算法实现(简化版)
import torch
import math
def gptq_quantize_weight(weight: torch.Tensor,
hessian: torch.Tensor,
num_bits: int = 4,
group_size: int = 128,
damp_percent: float = 0.01):
"""
用 GPTQ 算法量化权重
Args:
weight: [out_features, in_features] 权重矩阵
hessian: [in_features, in_features] 海森矩阵 (H = 2 * X @ X.T)
num_bits: 量化位数
group_size: 分组大小
damp_percent: 用于稳定海森矩阵的阻尼比例
"""
W = weight.clone().float()
n_rows, n_cols = W.shape
# 海森矩阵阻尼(数值稳定性)
H = hessian.clone().float()
dead_cols = torch.diag(H) == 0
H[dead_cols, dead_cols] = 1
W[:, dead_cols] = 0
damp = damp_percent * H.diag().mean()
H.diagonal().add_(damp)
# 用 Cholesky 分解求海森矩阵的逆
H_inv = torch.linalg.cholesky(H)
H_inv = torch.cholesky_inverse(H_inv)
H_inv = torch.linalg.cholesky(H_inv, upper=True)
Q = torch.zeros_like(W)
Losses = torch.zeros_like(W)
q_max = 2 ** (num_bits - 1) - 1
for col_idx in range(n_cols):
w_col = W[:, col_idx] # 当前列的权重
h_inv_diag = H_inv[col_idx, col_idx] # 海森逆矩阵的对角元素
# 按分组计算 scale
if group_size != -1 and col_idx % group_size == 0:
group_end = min(col_idx + group_size, n_cols)
w_group = W[:, col_idx:group_end]
max_abs = w_group.abs().max(dim=1)[0].unsqueeze(1)
scale = max_abs / q_max
scale = torch.clamp(scale, min=1e-8)
# 量化
q_col = torch.clamp(torch.round(w_col / scale.squeeze()), -q_max, q_max)
q_col = q_col * scale.squeeze()
Q[:, col_idx] = q_col
# 量化误差
err = (w_col - q_col) / h_inv_diag
Losses[:, col_idx] = err ** 2 / 2
# 把误差传播到剩余权重上(核心步骤!)
W[:, col_idx + 1:] -= err.unsqueeze(1) * H_inv[col_idx, col_idx + 1:].unsqueeze(0)
return Q, Losses
def collect_hessian(model_layer, calibration_data, device='cuda'):
"""用校准数据收集海森矩阵"""
hessians = {}
def make_hook(name):
def hook(module, input, output):
inp = input[0].detach().float()
if inp.dim() == 3:
inp = inp.reshape(-1, inp.size(-1))
if name not in hessians:
hessians[name] = torch.zeros(inp.size(1), inp.size(1), device=device)
hessians[name] += 2 * inp.T @ inp
return hook
handles = []
for name, module in model_layer.named_modules():
if isinstance(module, torch.nn.Linear):
handles.append(module.register_forward_hook(make_hook(name)))
with torch.no_grad():
for batch in calibration_data:
model_layer(batch.to(device))
for h in handles:
h.remove()
return hessians
5.2 AutoGPTQ 使用方法
实际使用中,GPTQ 量化一般通过 AutoGPTQ 库来完成。
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
import torch
def quantize_with_gptq(
model_name: str,
output_dir: str,
bits: int = 4,
group_size: int = 128
):
"""用 AutoGPTQ 对模型进行量化"""
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
# 量化配置
quantize_config = BaseQuantizeConfig(
bits=bits, # 4 或 8
group_size=group_size, # 推荐 128
damp_percent=0.01, # 海森矩阵阻尼
desc_act=False, # 激活重排序(提升质量,降低速度)
sym=True, # 对称量化
true_sequential=True # 逐层顺序量化
)
# 加载模型
model = AutoGPTQForCausalLM.from_pretrained(
model_name,
quantize_config=quantize_config
)
# 准备校准数据
from datasets import load_dataset
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_data = []
for text in dataset["text"][:128]:
if len(text.strip()) > 50:
encoded = tokenizer(
text.strip(),
return_tensors="pt",
max_length=2048,
truncation=True
)
calibration_data.append(encoded["input_ids"].squeeze())
# 执行 GPTQ 量化
print(f"开始 GPTQ {bits}bit 量化...")
model.quantize(calibration_data)
# 保存
model.save_quantized(output_dir, use_safetensors=True)
tokenizer.save_pretrained(output_dir)
print(f"量化完成: {output_dir}")
return model, tokenizer
def load_gptq_model(model_dir: str, device: str = "cuda"):
"""加载 GPTQ 量化模型"""
model = AutoGPTQForCausalLM.from_quantized(
model_dir,
device=device,
use_triton=False, # 是否使用 Triton 内核
disable_exllama=False, # 使用 ExLlama 内核(提升速度)
inject_fused_attention=True,
inject_fused_mlp=True
)
tokenizer = AutoTokenizer.from_pretrained(model_dir)
return model, tokenizer
# 使用示例
# model, tokenizer = quantize_with_gptq("meta-llama/Llama-2-7b-hf", "./llama2-7b-gptq-4bit")
# model, tokenizer = load_gptq_model("./llama2-7b-gptq-4bit")
6. AWQ:激活感知权重量化
AWQ 是 2023 年发表的技术,通过分析激活分布来保护重要的权重通道。(arXiv:2306.00978)
6.1 与 GPTQ 的区别
| 项目 | GPTQ | AWQ |
|---|---|---|
| 方法 | 基于海森矩阵的误差补偿 | 基于激活的缩放 |
| 校准数据 | 需要(128+ 样本) | 需要(32+ 样本) |
| 速度 | 慢(1-4 小时) | 快(数十分钟) |
| 质量 | 优秀 | 优秀(相当或更好) |
| 特点 | 按通道优化 | 处理激活异常值 |
6.2 AWQ 核心思路
LLM 权重中存在重要通道。这些通道的激活幅度较大,量化时如果误差较大,会严重影响整体性能。AWQ 通过用缩放因子放大重要通道的权重,来降低量化误差。
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
def quantize_with_awq(
model_name: str,
output_dir: str,
bits: int = 4,
group_size: int = 128
):
"""用 AutoAWQ 对模型进行量化"""
tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True
)
model = AutoAWQForCausalLM.from_pretrained(
model_name,
low_cpu_mem_usage=True,
use_cache=False
)
# AWQ 量化配置
quant_config = {
"zero_point": True, # 非对称量化
"q_group_size": group_size,
"w_bit": bits,
"version": "GEMM" # GEMM 或 GEMV(适合小批量)
}
# 执行量化
print(f"开始 AWQ {bits}bit 量化...")
model.quantize(tokenizer, quant_config=quant_config)
# 保存
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"AWQ 量化完成: {output_dir}")
return model
def load_awq_model(model_dir: str, device: str = "cuda"):
"""加载 AWQ 量化模型"""
model = AutoAWQForCausalLM.from_quantized(
model_dir,
fuse_layers=True, # 层融合以提升速度
trust_remote_code=True,
safetensors=True
)
tokenizer = AutoTokenizer.from_pretrained(model_dir)
return model, tokenizer
# 与 Hugging Face transformers 集成
from transformers import AutoModelForCausalLM
def load_awq_with_transformers(model_dir: str):
"""用 transformers 加载 AWQ 模型"""
model = AutoModelForCausalLM.from_pretrained(
model_dir,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_dir)
return model, tokenizer
7. GGUF/GGML:llama.cpp 生态系统
GGUF(GPT-Generated Unified Format)是 llama.cpp 项目的模型格式,让 LLM 即使在 CPU 上也能高效运行。
7.1 理解 GGUF 格式
GGUF 于 2023 年推出,取代了 GGML 格式。它把模型元数据、超参数、分词器信息都整合进单一文件。
GGUF 文件结构:
┌─────────────────────────────┐
│ 魔数 (GGUF) │
│ 版本 │
│ 张量数量 │
│ 元数据 KV 对 │
│ - 模型架构 │
│ - 上下文长度 │
│ - 注意力头数 │
│ - 嵌入维度 │
├─────────────────────────────┤
│ 张量信息 (名称、类型、形状) │
├─────────────────────────────┤
│ 张量数据 │
└─────────────────────────────┘
7.2 量化级别对比
| 格式 | 位数 | 内存(7B) | PPL 增幅 | 推荐用途 |
|---|---|---|---|---|
| Q2_K | 2.6 | 2.8 GB | 高 | 极端压缩 |
| Q3_K_S | 3.0 | 3.3 GB | 中等 | 节省内存 |
| Q4_0 | 4.0 | 3.8 GB | 低 | 均衡 |
| Q4_K_M | 4.1 | 4.1 GB | 极低 | 通用推荐 |
| Q5_0 | 5.0 | 4.7 GB | 最小 | 高质量 |
| Q5_K_M | 5.1 | 4.8 GB | 最小 | 高质量推荐 |
| Q6_K | 6.0 | 5.5 GB | 几乎没有 | 接近 FP16 |
| Q8_0 | 8.0 | 7.2 GB | 无 | 用于参照 |
| F16 | 16.0 | 13.5 GB | 无 | 基准线 |
K-quants(Q4_K_M、Q5_K_M 等)把部分层保持在更高精度,以提升质量。
7.3 构建与使用 llama.cpp
# 克隆并构建 llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
# 支持 CUDA 的构建
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j $(nproc)
# 仅 CPU 构建
cmake -B build
cmake --build build --config Release -j $(nproc)
# 把 HuggingFace 模型转换为 GGUF
python convert_hf_to_gguf.py \
--model meta-llama/Llama-2-7b-hf \
--outfile llama2-7b-f16.gguf \
--outtype f16
# GGUF 量化 (Q4_K_M)
./build/bin/llama-quantize \
llama2-7b-f16.gguf \
llama2-7b-q4_k_m.gguf \
Q4_K_M
# 执行推理
./build/bin/llama-cli \
-m llama2-7b-q4_k_m.gguf \
-p "The future of AI is" \
-n 100 \
--ctx-size 4096 \
--threads 8 \
--n-gpu-layers 35
7.4 Python 绑定(llama-cpp-python)
from llama_cpp import Llama
# 加载模型
llm = Llama(
model_path="./llama2-7b-q4_k_m.gguf",
n_ctx=4096, # 上下文长度
n_gpu_layers=35, # 卸载到 GPU 的层数(-1 表示全部)
n_threads=8, # CPU 线程数
verbose=False
)
# 文本生成
output = llm(
"Once upon a time",
max_tokens=200,
temperature=0.7,
top_p=0.9,
stop=["</s>", "\n\n"]
)
print(output["choices"][0]["text"])
# 对话补全格式
response = llm.create_chat_completion(
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is machine learning?"}
],
max_tokens=500,
temperature=0.7
)
print(response["choices"][0]["message"]["content"])
# 流式输出
for chunk in llm.create_chat_completion(
messages=[{"role": "user", "content": "Tell me a joke"}],
stream=True
):
delta = chunk["choices"][0].get("delta", {})
if "content" in delta:
print(delta["content"], end="", flush=True)
8. bitsandbytes:LLM 量化库
bitsandbytes 由 Tim Dettmers 开发,与 HuggingFace transformers 完美集成。
8.1 LLM.int8() — 8 位混合精度
LLM.int8() 在矩阵乘法过程中把激活异常值用 FP16 处理,其余部分使用 INT8。
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# 加载 INT8 模型
model_8bit = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
load_in_8bit=True,
device_map="auto"
)
# 查看内存占用
def print_model_size(model, label):
"""打印模型内存占用"""
total_params = sum(p.numel() for p in model.parameters())
total_bytes = sum(
p.numel() * p.element_size() for p in model.parameters()
)
print(f"{label}: {total_params/1e9:.2f}B params, {total_bytes/1e9:.2f} GB")
print_model_size(model_8bit, "INT8 模型")
# INT8 模型: 6.74B params, ~7.0 GB
8.2 4 位量化(QLoRA 中使用)
import bitsandbytes as bnb
from transformers import BitsAndBytesConfig
# NF4 量化配置(QLoRA)
bnb_config_nf4 = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4 或 FP4
bnb_4bit_compute_dtype=torch.bfloat16, # 运算时使用的数据类型
bnb_4bit_use_double_quant=True, # 二次量化(连量化常数也量化)
)
# FP4 量化配置
bnb_config_fp4 = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="fp4",
bnb_4bit_compute_dtype=torch.float16,
)
# 加载模型
model_4bit = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-hf",
quantization_config=bnb_config_nf4,
device_map="auto"
)
print_model_size(model_4bit, "NF4 模型")
# NF4 模型: 6.74B params, ~4.0 GB(含二次量化)
# QLoRA 微调设置
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
model_4bit = prepare_model_for_kbit_training(model_4bit)
lora_config = LoraConfig(
r=64,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model_lora = get_peft_model(model_4bit, lora_config)
model_lora.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 3,504,607,232 || trainable%: 0.1197
8.3 NF4 vs FP4
NF4 (Normal Float 4)
- 假设正态分布的非线性 4 位量化
- 利用权重分布接近正态分布这一特性
- 在相同位数下表达力更强
FP4 (Float 4)
- 基于浮点数的 4 位表示
- 可以表示更宽的范围
import numpy as np
import matplotlib.pyplot as plt
# NF4 量化点可视化
def get_nf4_quantization_points():
"""NF4 的 16 个量化点"""
# 正态分布的 1/16 分位数
nf4_points = []
for i in range(16):
quantile = (i + 0.5) / 16
nf4_points.append(scipy.stats.norm.ppf(quantile))
# 归一化
max_val = max(abs(p) for p in nf4_points)
nf4_points = [p / max_val for p in nf4_points]
return nf4_points
# NF4: [-1.0, -0.6961, -0.5250, -0.3949, -0.2844, -0.1848, -0.0911, 0.0000,
# 0.0796, 0.1609, 0.2461, 0.3379, 0.4407, 0.5626, 0.7230, 1.0]
9. SmoothQuant:W8A8 量化
SmoothQuant 把权重(W)和激活(A)都量化为 INT8,以实现更快的推理速度。
9.1 激活异常值问题
LLM 的激活分布在某些特定通道上会出现非常大的值(异常值)。这使 W8A8 量化变得困难。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
def analyze_activation_outliers(model, tokenizer, text: str, threshold: float = 100.0):
"""分析激活异常值"""
activations = {}
def make_hook(name):
def hook(module, input, output):
act = output.detach().float()
max_val = act.abs().max().item()
outlier_ratio = (act.abs() > threshold).float().mean().item()
activations[name] = {
"max": max_val,
"outlier_ratio": outlier_ratio,
"std": act.std().item()
}
return hook
handles = []
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
handles.append(module.register_forward_hook(make_hook(name)))
input_ids = tokenizer(text, return_tensors="pt").input_ids.cuda()
with torch.no_grad():
model(input_ids)
for h in handles:
h.remove()
# 按异常值多少对层排序
sorted_acts = sorted(
activations.items(),
key=lambda x: x[1]["max"],
reverse=True
)
print("异常值最大的 Top 10 层:")
for name, stats in sorted_acts[:10]:
print(f" {name}: max={stats['max']:.1f}, outlier_ratio={stats['outlier_ratio']:.3%}")
return activations
9.2 迁移缩放(Migration Scaling)
SmoothQuant 的核心:把激活端的困难转移到权重上。
Y = (X * diag(s)^(-1)) * (diag(s) * W)
= X_smooth * W_smooth
def smooth_quantize(
model,
calibration_samples,
alpha: float = 0.5
):
"""
应用 SmoothQuant
Args:
alpha: 迁移强度(0=仅权重,1=仅激活)
推荐值:0.5(均等分配)
"""
# 收集激活统计信息
act_scales = {}
def collect_scales(name):
def hook(module, input, output):
inp = input[0].detach()
if inp.dim() == 3:
inp = inp.reshape(-1, inp.size(-1))
channel_max = inp.abs().max(dim=0)[0]
if name not in act_scales:
act_scales[name] = channel_max
else:
act_scales[name] = torch.maximum(act_scales[name], channel_max)
return hook
handles = []
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
handles.append(module.register_forward_hook(collect_scales(name)))
with torch.no_grad():
for sample in calibration_samples:
model(**sample)
for h in handles:
h.remove()
# 计算并应用 scale
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear) and name in act_scales:
act_scale = act_scales[name]
weight_scale = module.weight.abs().max(dim=0)[0]
# 计算迁移 scale
smooth_scale = (act_scale ** alpha) / (weight_scale ** (1 - alpha))
smooth_scale = torch.clamp(smooth_scale, min=1e-5)
# 把 scale 应用到权重上
module.weight.data = module.weight.data / smooth_scale.unsqueeze(0)
# 对前一层(LayerNorm 等)的输出 scale 做逆向调整
# (实际实现中需要找到前一层并修改)
return model, act_scales
10. SpQR:稀疏量化表示
SpQR 把重要的权重(异常值)单独以 FP16 保存,其余部分做低精度量化。
import torch
def spqr_quantize(weight: torch.Tensor,
num_bits: int = 3,
outlier_threshold_percentile: float = 1.0):
"""
SpQR 量化(简化版)
核心思路:把 top p% 的异常值以 FP16 保存,其余部分做低位量化
"""
# 计算异常值阈值
threshold = torch.quantile(weight.abs(), 1 - outlier_threshold_percentile / 100)
# 异常值掩码
outlier_mask = weight.abs() > threshold
# 保存异常值(FP16)
outlier_values = weight.clone()
outlier_values[~outlier_mask] = 0
# 量化剩余部分
regular_weight = weight.clone()
regular_weight[outlier_mask] = 0
# 应用 Per-group 量化
q_max = 2 ** (num_bits - 1) - 1
group_size = 16
rows, cols = regular_weight.shape
regular_grouped = regular_weight.reshape(-1, group_size)
max_abs = regular_grouped.abs().max(dim=1, keepdim=True)[0]
scales = max_abs / q_max
scales = torch.clamp(scales, min=1e-8)
q = torch.clamp(torch.round(regular_grouped / scales), -q_max, q_max).to(torch.int8)
regular_dequant = (scales * q.float()).reshape(rows, cols)
# 最终重建
reconstructed = regular_dequant + outlier_values
error = (weight - reconstructed).abs().mean().item()
# 计算内存占用
outlier_memory = outlier_mask.sum().item() * 2 # FP16 = 2 bytes
regular_memory = (~outlier_mask).sum().item() * (num_bits / 8)
total_memory = outlier_memory + regular_memory
original_memory = weight.numel() * weight.element_size()
compression_ratio = original_memory / total_memory
print(f"异常值比例: {outlier_mask.float().mean():.2%}")
print(f"平均重建误差: {error:.6f}")
print(f"压缩率: {compression_ratio:.2f}x")
return q, scales, outlier_values, outlier_mask
11. 量化基准对比
11.1 以 Llama-2-7B 为基准的对比
import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import psutil
import GPUtil
def benchmark_quantization(model, tokenizer, device="cuda", num_runs=50):
"""量化模型基准测试"""
prompt = "The history of artificial intelligence began"
inputs = tokenizer(prompt, return_tensors="pt").to(device)
# 内存占用
if device == "cuda":
torch.cuda.synchronize()
gpu = GPUtil.getGPUs()[0]
memory_used_gb = gpu.memoryUsed / 1024
else:
memory_used_gb = psutil.virtual_memory().used / 1e9
# 预热
with torch.no_grad():
for _ in range(5):
outputs = model.generate(
**inputs,
max_new_tokens=50,
do_sample=False
)
# 测量速度
if device == "cuda":
torch.cuda.synchronize()
start = time.time()
with torch.no_grad():
for _ in range(num_runs):
outputs = model.generate(
**inputs,
max_new_tokens=50,
do_sample=False
)
if device == "cuda":
torch.cuda.synchronize()
elapsed = time.time() - start
avg_time = elapsed / num_runs
tokens_per_second = 50 / avg_time
return {
"memory_gb": memory_used_gb,
"avg_time_ms": avg_time * 1000,
"tokens_per_second": tokens_per_second
}
# 结果示例(基于 A100 80GB,Llama-2-7B)
benchmark_results = {
"FP16": {"memory_gb": 13.5, "tokens_per_second": 52.3, "ppl": 5.68},
"INT8 (bitsandbytes)": {"memory_gb": 7.8, "tokens_per_second": 38.1, "ppl": 5.71},
"INT4 GPTQ": {"memory_gb": 4.5, "tokens_per_second": 65.2, "ppl": 5.89},
"INT4 AWQ": {"memory_gb": 4.3, "tokens_per_second": 68.7, "ppl": 5.86},
"Q4_K_M (GGUF)": {"memory_gb": 4.1, "tokens_per_second": 45.2, "ppl": 5.91}, # CPU
"INT4 NF4": {"memory_gb": 4.0, "tokens_per_second": 31.5, "ppl": 5.94},
}
print("=" * 80)
print(f"{'方法':<25} {'内存(GB)':<12} {'Tok/s':<12} {'PPL':<8}")
print("=" * 80)
for method, stats in benchmark_results.items():
print(f"{method:<25} {stats['memory_gb']:<12.1f} {stats['tokens_per_second']:<12.1f} {stats['ppl']:<8.2f}")
12. 实战指南:选择最佳量化方法
12.1 按模型规模制定策略
7B 及以下的小模型:
- GGUF Q4_K_M:本地 CPU 运行的最佳选择
- AWQ INT4:推荐用于 GPU 服务器部署
- 内存充裕时也可以考虑 FP16(24GB GPU 以下)
13B-30B 的中型模型:
- GPTQ INT4 或 AWQ INT4:单张 24GB GPU 即可运行
- GGUF Q4_K_M:16GB 内存也能运行
70B 以上的大型模型:
- GPTQ INT4:单张 A100 80GB 即可运行
- GPTQ INT2:需要极端压缩时使用
- 结合多 GPU + Tensor Parallel
12.2 按任务制定策略
def recommend_quantization(
task: str,
model_size_b: float,
gpu_memory_gb: float,
cpu_only: bool = False,
fine_tuning_needed: bool = False
):
"""根据任务和环境推荐量化方案"""
recommendations = []
if cpu_only:
recommendations.append({
"method": "GGUF Q4_K_M",
"reason": "针对 CPU 推理优化,基于 llama.cpp",
"library": "llama-cpp-python"
})
return recommendations
if fine_tuning_needed:
recommendations.append({
"method": "bitsandbytes NF4 + QLoRA",
"reason": "可微调,额外约 4GB 内存即可训练 LoRA 适配器",
"library": "bitsandbytes + peft"
})
return recommendations
# 计算内存需求
fp16_memory = model_size_b * 2 # FP16 = 每参数 2 bytes
int8_memory = model_size_b * 1 # INT8 = 每参数 1 byte
int4_memory = model_size_b * 0.5 # INT4 = 每参数 0.5 bytes
if fp16_memory <= gpu_memory_gb * 0.8:
recommendations.append({
"method": "FP16(基础)",
"reason": "内存充裕,质量最佳",
"memory_gb": fp16_memory
})
if int8_memory <= gpu_memory_gb * 0.8:
if task in ["chat", "completion", "summarization"]:
recommendations.append({
"method": "AWQ INT8",
"reason": "质量与速度的最佳平衡",
"library": "autoawq",
"memory_gb": int8_memory
})
if int4_memory <= gpu_memory_gb * 0.8:
recommendations.append({
"method": "AWQ INT4",
"reason": "高速推理,质量出色",
"library": "autoawq",
"memory_gb": int4_memory
})
recommendations.append({
"method": "GPTQ INT4",
"reason": "最佳 INT4 质量,量化速度较慢",
"library": "auto-gptq",
"memory_gb": int4_memory
})
return recommendations
# 使用示例
recommendations = recommend_quantization(
task="chat",
model_size_b=7.0,
gpu_memory_gb=16.0,
fine_tuning_needed=False
)
for rec in recommendations:
print(f"\n方法: {rec['method']}")
print(f"理由: {rec['reason']}")
if 'library' in rec:
print(f"库: {rec['library']}")
if 'memory_gb' in rec:
print(f"预计内存: {rec['memory_gb']:.1f} GB")
12.3 完整的量化流水线
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from awq import AutoAWQForCausalLM
import json
import os
class QuantizationPipeline:
"""统一的量化流水线"""
def __init__(self, model_name: str, output_base_dir: str):
self.model_name = model_name
self.output_base_dir = output_base_dir
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
os.makedirs(output_base_dir, exist_ok=True)
def quantize_gptq(self, bits: int = 4, group_size: int = 128):
"""GPTQ 量化"""
output_dir = os.path.join(self.output_base_dir, f"gptq-{bits}bit")
config = BaseQuantizeConfig(
bits=bits,
group_size=group_size,
sym=True,
desc_act=False
)
model = AutoGPTQForCausalLM.from_pretrained(
self.model_name,
quantize_config=config
)
# 校准数据
calibration_data = self._prepare_calibration_data()
model.quantize(calibration_data)
model.save_quantized(output_dir)
self.tokenizer.save_pretrained(output_dir)
print(f"GPTQ {bits}bit 已保存: {output_dir}")
return output_dir
def quantize_awq(self, bits: int = 4, group_size: int = 128):
"""AWQ 量化"""
output_dir = os.path.join(self.output_base_dir, f"awq-{bits}bit")
model = AutoAWQForCausalLM.from_pretrained(
self.model_name,
low_cpu_mem_usage=True
)
quant_config = {
"zero_point": True,
"q_group_size": group_size,
"w_bit": bits,
"version": "GEMM"
}
model.quantize(self.tokenizer, quant_config=quant_config)
model.save_quantized(output_dir)
self.tokenizer.save_pretrained(output_dir)
print(f"AWQ {bits}bit 已保存: {output_dir}")
return output_dir
def _prepare_calibration_data(self, num_samples: int = 128):
"""准备校准数据"""
from datasets import load_dataset
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
data = []
for text in dataset["text"]:
if len(text.strip()) > 50:
encoded = self.tokenizer(
text.strip(),
return_tensors="pt",
max_length=2048,
truncation=True
)
data.append(encoded["input_ids"].squeeze())
if len(data) >= num_samples:
break
return data
def evaluate_all(self, test_text: str = None):
"""评估所有量化模型"""
if test_text is None:
from datasets import load_dataset
dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="test")
test_text = " ".join(dataset["text"][:10])
results = {}
# FP16 基准线
model_fp16 = AutoModelForCausalLM.from_pretrained(
self.model_name,
torch_dtype=torch.float16,
device_map="auto"
)
# 计算 PPL
from transformers import pipeline
# 输出各模型的评估结果
print("\n=== 量化评估结果 ===")
print(f"模型: {self.model_name}")
print(f"{'方法':<20} {'PPL':<10} {'内存(GB)':<12}")
print("-" * 42)
return results
# 执行完整流水线
pipeline = QuantizationPipeline(
model_name="meta-llama/Llama-2-7b-hf",
output_base_dir="./quantized_models"
)
# GPTQ 4bit 量化
gptq_dir = pipeline.quantize_gptq(bits=4)
# AWQ 4bit 量化
awq_dir = pipeline.quantize_awq(bits=4)
结语
模型量化是 LLM 民主化的核心技术。整理本文内容:
- 基础理解:从 FP32 压缩到 INT4 的数学原理(scale、zero_point)
- PTQ vs QAT:无需重新训练的 PTQ 更实用,QAT 则是极端压缩下的必需品
- GPTQ:基于海森矩阵的误差补偿,带来最佳的 INT4 质量
- AWQ:基于激活分布,实现快速且高效的量化
- GGUF:为 CPU 运行而优化,支持多种质量级别
- bitsandbytes:与 HuggingFace 集成,QLoRA 微调的必备工具
推荐策略:
- 本地运行:GGUF Q4_K_M
- GPU 服务器部署:AWQ 4bit
- 追求高质量:GPTQ 4bit 或 FP16
- 需要微调:bitsandbytes NF4 + QLoRA
量化技术正在快速发展,QuIP#、AQLM 等 2 位量化技术也已经出现。让模型变得更小、更快的这段旅程,仍在继续。
参考资料
- GPTQ: arXiv:2209.05433
- AWQ: arXiv:2306.00978
- llama.cpp: github.com/ggerganov/llama.cpp
- bitsandbytes: github.com/TimDettmers/bitsandbytes
- SmoothQuant: arXiv:2211.10438
- SpQR: arXiv:2306.03078
- PyTorch 量化: pytorch.org/docs/stable/quantization.html