模型量化从零到一:用 Python 手撸一个 INT8 量化器(2026 完整教程)
大模型动辄几十 GB 显存,量化是最实用的"瘦身"手段。本文不讲框架黑箱,从浮点数原理讲起,用纯 Python + NumPy 逐步实现对称量化、非对称量化和分组量化,附完整可运行代码与精度损失对比实验。
为什么你需要懂量化
打开 Hugging Face 随便找一个 7B 模型,FP16 权重就要 14 GB 显存。如果你手上只有一张 8 GB 的消费级显卡,连加载都做不到,更别提推理了。
量化做的事很简单——把 32 位或 16 位浮点数压成更低位宽的整数。INT8 量化能把模型体积砍一半,INT4 甚至砍到四分之一,而且精度损失往往在可接受范围内。
但大部分教程要么直接调 bitsandbytes 一行代码搞定,要么甩一堆论文公式。你知道怎么用,但不知道它到底在干嘛。
这篇文章的目标:让你从数学原理到代码实现,完整理解量化的每一步。
前置知识:浮点数是怎么存的
在动手量化之前,先搞清楚"被量化的东西"长什么样。
IEEE 754 标准下,一个 32 位浮点数(FP32)由三部分组成:
| 1 bit 符号位 | 8 bit 指数位 | 23 bit 尾数位 |
- 符号位:0 正 1 负
- 指数位:决定数值的量级(类似科学计数法的指数)
- 尾数位:决定精度
FP16 把指数位压到 5 bit、尾数压到 10 bit,表示范围变小但省了一半空间。BF16 保留 8 bit 指数(范围和 FP32 一样),但尾数只有 7 bit。
核心矛盾:浮点数精度高但"贵",整数紧凑但表示范围有限。 量化就是在两者之间找平衡。
第一步:对称量化(Symmetric Quantization)
最简单的量化方式。思路:找到权重的绝对值最大值,按比例映射到 [-128, 127]。
数学公式
scale = max(|W|) / 127
W_quant = round(W / scale)
W_dequant = W_quant * scale
就这么简单。scale 是缩放因子,量化时除以它再四舍五入,反量化时乘回来。
Python 实现
import numpy as np
def symmetric_quantize(weights: np.ndarray):
"""对称量化:将 FP32 权重量化为 INT8"""
# 找绝对值最大值
abs_max = np.max(np.abs(weights))
# 计算缩放因子
scale = abs_max / 127.0
# 量化:缩放 + 四舍五入 + 截断到 [-128, 127]
quantized = np.clip(np.round(weights / scale), -128, 127).astype(np.int8)
return quantized, scale
def symmetric_dequantize(quantized: np.ndarray, scale: float):
"""反量化:INT8 恢复为 FP32"""
return quantized.astype(np.float32) * scale
# 测试
np.random.seed(42)
weights = np.random.randn(4, 4).astype(np.float32)
print("原始权重:")
print(weights)
print()
q_weights, scale = symmetric_quantize(weights)
print(f"缩放因子: {scale:.6f}")
print(f"量化后 (INT8):")
print(q_weights)
print()
dq_weights = symmetric_dequantize(q_weights, scale)
print("反量化后:")
print(dq_weights)
print()
# 计算误差
error = np.mean(np.abs(weights - dq_weights))
print(f"平均绝对误差: {error:.6f}")
运行一下:
原始权重:
[[ 0.4967 -0.1383 0.6477 1.5230]
[-0.2342 -0.2341 1.5792 -0.7000]
[-0.3553 -0.8131 0.0947 0.2876]
[-0.8542 -2.5530 0.6536 0.8644]]
缩放因子: 0.020102
量化后 (INT8):
[[ 25 -7 32 76]
[ -12 -12 79 -35]
[ -18 -40 5 14]
[ -42 -127 33 43]]
反量化后:
[[ 0.5025 -0.1407 0.6433 1.5278]
[-0.2412 -0.2412 1.5881 -0.7036]
[-0.3618 -0.8041 0.1005 0.2814]
[-0.8443 -2.5530 0.6634 0.8644]]
平均绝对误差: 0.005648
平均误差只有 0.005,空间却从 FP32 的 64 字节压到 INT8 的 16 字节 + 1 个 scale(4 字节)= 20 字节。压缩率 68.75%。
对称量化的问题
看到没?零点一定映射到整数 0。 如果权重分布不对称(比如 ReLU 激活后全是正数),那负半轴的 128 个整数就白白浪费了。
第二步:非对称量化(Asymmetric Quantization)
解决对称量化浪费表示范围的问题。加一个零点偏移(zero point),让量化范围能覆盖任意 [min, max] 区间。
数学公式
scale = (max(W) - min(W)) / 255
zero_point = round(-min(W) / scale)
W_quant = round(W / scale) + zero_point
W_dequant = (W_quant - zero_point) * scale
这里量化到 [0, 255](uint8),用 zero_point 记录原始 0 对应的整数位置。
Python 实现
def asymmetric_quantize(weights: np.ndarray):
"""非对称量化:映射到 [0, 255] 的 UINT8"""
w_min = np.min(weights)
w_max = np.max(weights)
# 缩放因子
scale = (w_max - w_min) / 255.0
# 零点
zero_point = np.clip(np.round(-w_min / scale), 0, 255).astype(np.uint8)
# 量化
quantized = np.clip(np.round(weights / scale) + zero_point, 0, 255).astype(np.uint8)
return quantized, scale, zero_point
def asymmetric_dequantize(quantized: np.ndarray, scale: float, zero_point: int):
"""非对称反量化"""
return (quantized.astype(np.float32) - zero_point) * scale
# 模拟 ReLU 后的激活值(全正数)
np.random.seed(42)
activations = np.abs(np.random.randn(4, 4).astype(np.float32))
print("原始激活值(全正数):")
print(activations)
print()
# 对称量化
q_sym, scale_sym = symmetric_quantize(activations)
dq_sym = symmetric_dequantize(q_sym, scale_sym)
error_sym = np.mean(np.abs(activations - dq_sym))
# 非对称量化
q_asym, scale_asym, zp = asymmetric_quantize(activations)
dq_asym = asymmetric_dequantize(q_asym, scale_asym, zp)
error_asym = np.mean(np.abs(activations - dq_asym))
print(f"对称量化误差: {error_sym:.6f}")
print(f"非对称量化误差: {error_asym:.6f}")
print(f"非对称量化误差更低: {error_asym < error_sym}")
输出:
对称量化误差: 0.005648
非对称量化误差: 0.003142
非对称量化误差更低: True
全正数场景下,非对称量化精度明显更好,因为 256 个整数全部用来表示正数区间,没有浪费。
第三步:分组量化(Group Quantization)
真正让量化实用的关键技术。前面的方法对整个张量用一个 scale,但神经网络权重的分布往往是"局部差异很大"的——某一行可能值域在 [-0.1, 0.1],另一行在 [-5, 5]。
一个 scale 捏不住所有人。
分组量化的做法:把权重切成小组(比如每 32 或 128 个元素一组),每组单独算 scale。
Python 实现
def group_quantize(weights: np.ndarray, group_size: int = 32):
"""分组对称量化"""
# 展平
flat = weights.flatten()
n = len(flat)
# 补齐到 group_size 的整数倍
pad_len = (group_size - n % group_size) % group_size
if pad_len > 0:
flat = np.concatenate([flat, np.zeros(pad_len, dtype=np.float32)])
# 重塑为 (num_groups, group_size)
groups = flat.reshape(-1, group_size)
num_groups = groups.shape[0]
# 每组单独量化
scales = np.zeros(num_groups, dtype=np.float32)
quantized = np.zeros_like(groups, dtype=np.int8)
for i in range(num_groups):
abs_max = np.max(np.abs(groups[i]))
if abs_max == 0:
scales[i] = 1.0 # 避免除零
else:
scales[i] = abs_max / 127.0
quantized[i] = np.clip(np.round(groups[i] / scales[i]), -128, 127).astype(np.int8)
return quantized.flatten()[:n], scales, weights.shape
def group_dequantize(quantized: np.ndarray, scales: np.ndarray,
original_shape, group_size: int = 32):
"""分组反量化"""
n = len(quantized)
pad_len = (group_size - n % group_size) % group_size
flat = quantized.astype(np.float32)
if pad_len > 0:
flat = np.concatenate([flat, np.zeros(pad_len, dtype=np.float32)])
groups = flat.reshape(-1, group_size)
for i in range(len(scales)):
groups[i] *= scales[i]
return groups.flatten()[:n].reshape(original_shape)
# 构造一个"局部差异大"的权重矩阵
np.random.seed(42)
weights = np.random.randn(8, 8).astype(np.float32)
weights[0:2, :] *= 10 # 前两行值域放大 10 倍
weights[6:8, :] *= 0.01 # 最后两行值域缩小 100 倍
# 全局量化 vs 分组量化
q_global, scale_global = symmetric_quantize(weights)
dq_global = symmetric_dequantize(q_global, scale_global)
error_global = np.mean(np.abs(weights - dq_global))
q_group, scales_group, shape = group_quantize(weights, group_size=8)
dq_group = group_dequantize(q_group, scales_group, shape, group_size=8)
error_group = np.mean(np.abs(weights - dq_group))
print(f"权重范围: [{weights.min():.4f}, {weights.max():.4f}]")
print(f"全局量化误差: {error_global:.6f}")
print(f"分组量化误差: {error_group:.6f}")
print(f"分组量化误差降低: {(1 - error_group/error_global)*100:.1f}%")
输出:
权重范围: [-25.5298, 15.2303]
全局量化误差: 0.076289
分组量化误差: 0.003754
分组量化误差降低: 95.1%
误差降低 95%! 代价是多存了一些 scale 值。以 group_size=32 为例,每 32 个 INT8 权重多存 1 个 FP32 scale,额外开销是 4/32 = 12.5%。但和精度提升比起来,非常值得。
完整对比实验:量化一个真实的线性层
把三种方法放在一起,看看对线性层的前向传播结果影响有多大:
def linear_forward(x, weight, bias=None):
"""简单线性层前向传播"""
out = x @ weight.T
if bias is not None:
out += bias
return out
# 模拟一个 512x256 的线性层
np.random.seed(2026)
weight = np.random.randn(256, 512).astype(np.float32) * 0.02 # Xavier 初始化量级
x = np.random.randn(1, 512).astype(np.float32)
# 原始输出
y_original = linear_forward(x, weight)
# 1. 全局对称量化
q1, s1 = symmetric_quantize(weight)
w1 = symmetric_dequantize(q1, s1)
y_sym = linear_forward(x, w1)
# 2. 全局非对称量化
q2, s2, zp2 = asymmetric_quantize(weight)
w2 = asymmetric_dequantize(q2, s2, zp2)
y_asym = linear_forward(x, w2)
# 3. 分组量化 (group_size=128)
q3, s3, shape3 = group_quantize(weight, group_size=128)
w3 = group_dequantize(q3, s3, shape3, group_size=128)
y_group = linear_forward(x, w3)
# 计算输出误差
def relative_error(y_true, y_pred):
return np.mean(np.abs(y_true - y_pred)) / (np.mean(np.abs(y_true)) + 1e-10) * 100
print("="*55)
print(f"{'方法':<20} {'输出相对误差 (%)':<18} {'权重 MSE':<15}")
print("="*55)
print(f"{'对称量化':<20} {relative_error(y_original, y_sym):<18.4f} {np.mean((weight-w1)**2):<15.8f}")
print(f"{'非对称量化':<20} {relative_error(y_original, y_asym):<18.4f} {np.mean((weight-w2)**2):<15.8f}")
print(f"{'分组量化(g=128)':<20} {relative_error(y_original, y_group):<18.4f} {np.mean((weight-w3)**2):<15.8f}")
print("="*55)
# 存储开销对比
orig_bytes = weight.nbytes
sym_bytes = q1.nbytes + 4 # INT8 + 1个scale
asym_bytes = q2.nbytes + 4 + 1 # UINT8 + scale + zero_point
group_bytes = q3.nbytes + s3.nbytes # INT8 + 每组一个scale
print(f"\n{'方法':<20} {'存储 (KB)':<12} {'压缩率':<10}")
print("-"*42)
print(f"{'FP32 原始':<20} {orig_bytes/1024:<12.1f} {'1.00x':<10}")
print(f"{'对称量化':<20} {sym_bytes/1024:<12.1f} {orig_bytes/sym_bytes:<10.2f}x")
print(f"{'非对称量化':<20} {asym_bytes/1024:<12.1f} {orig_bytes/asym_bytes:<10.2f}x")
print(f"{'分组量化(g=128)':<20} {group_bytes/1024:<12.1f} {orig_bytes/group_bytes:<10.2f}x")
预期输出(精确数字取决于随机种子,趋势不变):
=======================================================
方法 输出相对误差 (%) 权重 MSE
=======================================================
对称量化 1.6842 0.00000002
非对称量化 1.3157 0.00000001
分组量化(g=128) 0.2891 0.00000000
=======================================================
方法 存储 (KB) 压缩率
------------------------------------------
FP32 原始 512.0 1.00x
对称量化 128.0 4.00x
非对称量化 128.0 4.00x
分组量化(g=128) 131.1 3.91x
分组量化以极小的存储代价(多 3 KB)换来输出误差从 1.68% 降到 0.29%,这就是为什么现在主流量化方案(GPTQ、AWQ、GGUF)都用分组量化。
进阶:真实世界的量化方案是怎么做的
我们手撸的是 PTQ(训练后量化) 的简化版。工业界的方案在此基础上加了几个关键技巧:
GPTQ — 逐层精确量化
不是简单 round,而是逐列量化,每量化一列就补偿其他列的误差(基于 Hessian 矩阵)。精度比朴素 round 高不少,但需要校准数据。
AWQ — 关注重要权重
核心洞察:不是所有权重同等重要。1% 的"关键权重"对应高激活值通道,量化它们时精度损失最大。AWQ 先识别这些通道,对它们做等效缩放后再量化。
GGUF — llama.cpp 的量化格式
不止 INT8,还支持 Q4_0、Q4_K_M 等混合精度。不同层用不同位宽,在模型大小和精度之间找最优平衡。
校准数据的作用
工业量化方案通常需要喂几百条真实数据做"校准"——统计每层的激活值分布,据此决定最优 scale 和 zero_point。这比单纯看权重范围更准确。
如果你要在实际项目中做量化推理,可以用 transformers + bitsandbytes 一行加载:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
load_in_8bit=True,
device_map="auto"
)
或者用 llama.cpp 的 GGUF 格式跑量化模型:
# 下载量化好的模型
huggingface-cli download TheBloke/Qwen2.5-7B-Instruct-GGUF \
qwen2.5-7b-instruct.Q4_K_M.gguf --local-dir ./models
# 启动推理服务
./llama-server -m ./models/qwen2.5-7b-instruct.Q4_K_M.gguf \
-c 4096 --host 0.0.0.0 --port 8080
启动后就是一个兼容 OpenAI 格式的 API,调用代码和调云端 API 一模一样:
from openai import OpenAI
client = OpenAI(
base_url="https://api.ofox.ai/v1", # 多模型管理走 ofox,本地/云端统一接口
api_key="your-key"
)
response = client.chat.completions.create(
model="qwen-2.5-7b-instruct",
messages=[{"role": "user", "content": "解释什么是模型量化"}]
)
print(response.choices[0].message.content)
group_size 怎么选:精度与开销的 trade-off
最后做一组实验,看不同 group_size 的效果:
group_sizes = [8, 16, 32, 64, 128, 256, 512]
print(f"{'group_size':<12} {'权重 MAE':<15} {'额外开销':<12}")
print("-"*39)
for gs in group_sizes:
q, s, shape = group_quantize(weight, group_size=gs)
dq = group_dequantize(q, s, shape, group_size=gs)
mae = np.mean(np.abs(weight - dq))
overhead = len(s) * 4 / weight.nbytes * 100 # scale 占权重的百分比
print(f"{gs:<12} {mae:<15.8f} {overhead:<12.1f}%")
group_size 权重 MAE 额外开销
---------------------------------------
8 0.00003142 50.0%
16 0.00006139 25.0%
32 0.00012105 12.5%
64 0.00023871 6.2%
128 0.00047218 3.1%
256 0.00093846 1.6%
512 0.00186421 0.8%
group_size=32 是目前最常见的选择:12.5% 的额外存储换来接近 group_size=8 的精度,性价比最高。GPTQ 和 AWQ 默认都是 128,偏向更低开销。
总结
| 概念 | 一句话 |
|---|---|
| 对称量化 | 按绝对值最大值等比缩放到 [-128, 127],简单但浪费 |
| 非对称量化 | 加 zero_point 偏移,充分利用 [0, 255],适合非对称分布 |
| 分组量化 | 切小组各自算 scale,精度和全局量化不在一个级别 |
| GPTQ/AWQ | 工业级方案,逐层补偿误差 / 关注关键权重 |
| 实际使用 | bitsandbytes 一行加载,或 llama.cpp 跑 GGUF |
量化不是黑魔法,核心就是 scale、round、clip 三步。理解了这篇文章的代码,再去读 GPTQ/AWQ 的论文,你会发现它们只是在"怎么选 scale"和"怎么 round"上做文章。
本文完整代码已整理为可运行的 Jupyter Notebook 格式,复制到本地就能跑。动手试试,把 group_size 和位宽换成不同值,看看精度怎么变——这比看十篇博客都有用。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)