题外话:我是黄渡理工男子职业技术学院的一个研一学生,从刚开始接触加上两段实习,搞深度学习满打满算一年了。这是我写的第一个博客,也算是实习的内容吧。以后坚持没事就在这里面更新自己的所学来帮助像我这种半途转专业的小伙伴,正好也可以相互的交流交流,然后也会同步更新一些从0开始搭建神经网络模型,还有如何去应用。大模型篇的话呢,等五六月份再开(其实我应该是专门搞大模型的,只不过实习的时候别人没要我哈哈哈)

一.基础知识

OK也是直接开始了。如果有的地方写的不对,也请大家多多指正

1.为什么要量化

想象一下,训练好的AI模型就像一个巨大的、精细的地图。

  • 训练好的模型 (FP32 精度,这个精度马上我来讲一下,很多非计算机的小伙伴可能搞不清精度和内存的关系): 这个地图上每一个细节,无论是大陆板块的轮廓,还是一粒沙子的位置,都用极其精确的“单精度浮点数”来记录坐标。它非常精确,但也非常巨大、沉重。

  • 量化后的模型 (INT8 精度): 这个地图被简化了。它只保留了重要的地标(比如国家边界、主要城市),坐标也从精确的小数变成了整数。它牺牲了一点点精度,但体积小了很多,运行起来也轻便快速。

当这个地图用来当做你的导航的时候,这时候一粒沙子的位置是不是就不是那么重要了,甚至会极大的影响你推理速度(当然如果你觉得计算资源很充足,你可以选择导航到戈壁滩的第几粒沙子,或许未来人类实现了计算资源的突破呢)因此从精确的浮点数简化为整数,必然会丢失一些信息,导致模型最终的准确率轻微下降。优秀的量化算法和工具(比如要演示的qnn)的目标就是 “无损量化”,即在几乎不降低精度的前提下,最大限度地享受上述好处。

2.精度的关系

在计算机的储存中最小的保存单位就是比特,0或者1。

字节是计算机保存的基本单位。一个字节就是8个比特  比如说[0][1][1][0][1][1][1][0]这四个排列就算一个字节,他有2**8 = 256种状态咯详细对应表看看下面。用于也写好了。

类型 字节 比特 取值范围 用途
int4 0.5 4 -8~7 极端量化
int8 1 8 -128~127 量化权重/激活(最常用)
int16 2 16 -32768~32767 音频数据
int32 4 32 -2^31~2^31-1 中间累加结果
fp16(半精度) 2 16 ±65504 混合精度训练
fp32  (单精度) 4 32 ±3.4e38 原始模型(你的pytorch模型输出就是这个精度)
fp64(双精度) 8 64 ±1.8e308 科学计算

3. 量化的计算(认真看一下)

我们先走一遍原始的fp32精度的推理,看看结果是什么样子的

1.准备数据

输入: 一个 2x2 的矩阵
卷积核: 一个 2x2 的核
bias: 0.1
激活函数: ReLU

输入数据:
[0.8  0.3]
[0.1  0.6]

卷积核:
[0.5  -0.3]
[0.2   0.4]

bias = 0.1

2.正向推理

第1步:卷积计算
(0.8 × 0.5) + (0.3 × -0.3) + (0.1 × 0.2) + (0.6 × 0.4)
= 0.4 + (-0.09) + 0.02 + 0.24
= 0.57

第2步:加bias
0.57 + 0.1 = 0.67

第3步:ReLU
max(0, 0.67) = 0.67

3.量化统计

上面就是正向推理的过程,那我们量化过程中呢。首先需要先对校准数据集进行统计,比如说校准数据集包括【0.1 ,0.2 , 0.5 ,0.3, 1】,那我们就可以先把这个极值1去掉(因为归一化之后图片大多数应该在0-1之间,也有其他归一化的方法),后面量化的时候会写这个参数叫什么。那是不是最大减去最小,除以255,就把这些值缩放到了一个小数范围内。

假设校准数据集用500张图片统计得到:

输入范围:最小0,最大1.0
用99.9%分位 = 0.95
input_scale = (0.95 - 0) / 255 = 0.95/255 = 0.003725

权重范围:最小-0.3,最大0.5
weight_scale = (0.5 - (-0.3)) / 255 = 0.8/255 = 0.003137

输出范围(ReLU后):最小0,最大2.5
用99.9%分位 = 2.0
output_scale = (2.0 - 0) / 255 = 2.0/255 = 0.007843
是不是很好奇,为啥这是小数,不是整数?因为这个是尺度,我们想用int去表示小数

!!!!!!!important!!!!!!!!
scale = 0.003725  # 意思是:每个INT8单位代表0.003725的真实值
# 就像尺子:
INT8值: 0, 1, 2, 3, ..., 255
真实值: 0, 0.003725, 0.00745, 0.011175, ..., 0.95
这个尺度就是我们统计之后要保存的东西!!!

4.这时候把要推理的数据,就是第一部的数据给过来,然后用校准数据集统计的尺度来计算!

input_scale = 0.003725 (每个INT8单位代表0.003725)
这意味着:INT8值 × 0.003725 = 实际值

所以量化就是:q = round(x / 0.003725)

0.8 / 0.003725 = 214.8 → 215
0.3 / 0.003725 = 80.5 → 81
0.1 / 0.003725 = 26.8 → 27
0.6 / 0.003725 = 161.1 → 161

量化后输入:
[215, 81]
[27, 161]

5.量化权重信息,用上面计算的尺度对卷积核的权重量化

weight_scale = 0.003137
权重范围 -0.3 到 0.5,所以用对称量化,zero_point=0

0.5 / 0.003137 = 159.4 → 159
-0.3 / 0.003137 = -95.6 → -96
0.2 / 0.003137 = 63.8 → 64
0.4 / 0.003137 = 127.5 → 128

量化后权重:
[159, -96]
[64, 128]

6.被量化完成的输入数据和卷积核数据进行相乘(卷积)

输入INT8 × 权重INT8:

位置1: 215 × 159 = 34185
位置2: 81 × (-96) = -7776
位置3: 27 × 64 = 1728
位置4: 161 × 128 = 20608

累加:34185 + (-7776) + 1728 + 20608 = 48745

7.然后把这个48745 这个整数乘以尺度,是不是就是小数了?

反量化公式:实际值 = INT32结果 × (input_scale × weight_scale)

input_scale × weight_scale = 0.003725 × 0.003137 = 0.000011685

conv_fp32 = 48745 × 0.000011685 = 0.5696

8.加上偏置和激活函数

bias = 0.1 (保持FP32)
conv_fp32 + bias = 0.5696 + 0.1 = 0.6696
max(0, 0.6696) = 0.6696

我们计算的初始是不是0.6700,量化之后的数据是0.6696

这时候又有老铁要说了,不对啊,你这中间不是还有那么多什么小数计算吗。你再细看一下推理过程,是不是尺度是在量化过程中计算的。然后用尺度把fp32的权重给整数化了,我们保存的是这个整数和尺度。所以,推理过程中,只有输入和反量化用到了小数,中间卷积操作大量的乘法都是整数相乘。(非常大节约了计算资源)

4.总结一下!!

量化保存的东西是什么?

1.量化过的卷积权重

# 原始权重是FP32,量化后存INT8
conv1_weight = [
    [159, -96],   # INT8 (1字节)
    [64, 128]     # INT8 (1字节)
]  ← 这是整数!

conv2_weight = [23, -45, 67, ...]  # 也是INT8整数

2.每一层的尺度和偏置

# scale是小数,用来把INT8"翻译"回实数
layer1 = {
    'input_scale': 0.003725,     # FP32小数
    'input_zp': 128,              # 整数
    'weight_scale': 0.003137,     # FP32小数  
    'weight_zp': 0,                # 整数
    'output_scale': 0.007843,     # FP32小数
    'output_zp': 0                 # 整数
}

layer2 = {
    'input_scale': 0.007843,      # FP32小数 (等于上一层的output_scale)
    'weight_scale': 0.002345,     # FP32小数
    'output_scale': 0.005678,     # FP32小数
    ...
}


# bias通常保持FP32,不量化
conv1_bias = 0.1       # FP32小数
conv2_bias = -0.05     # FP32小数

3.整个流程是什么样子的呢

输入图片 (FP32小数)  eg: [0.8, 0.3, 0.1, 0.6]
  │
  ▼ 第1步:量化输入
  q = round(x / input_scale) + zp
  [0.8/0.003725=215, 0.3/0.003725=81, ...]
  │
  ▼ 第2步:输入变成INT8整数
  [215, 81, 27, 161]  ←────────── 现在是整数!(这一步是推理时候做的)
  │
  ▼ 第3步:加载权重复制(也是INT8整数)
  权重 = [159, -96, 64, 128]  ←─── 整数!(这个是我们保存的东西)
  │
  ▼ 第4步:INT8卷积(全是整数运算!)
  215×159 = 34185   ← 整数乘法
  81×(-96) = -7776  ← 整数乘法
  27×64 = 1728      ← 整数乘法
  161×128 = 20608   ← 整数乘法
  │
  ▼ 第5步:累加成INT32
  34185 + (-7776) + 1728 + 20608 = 48745  ← 整数!
  │
  ▼ 第6步:反量化(唯一的小数运算!)
  scale_product = input_scale × weight_scale
                = 0.003725 × 0.003137 = 0.000011685  ← 小数
  conv_fp32 = 48745 × 0.000011685 = 0.5696  ← 变成小数了!
  │
  ▼ 第7步:加bias(bias是FP32小数)
  0.5696 + 0.1 = 0.6696  ← 小数
  │
  ▼ 第8步:ReLU(比较大小,还是小数)
  max(0, 0.6696) = 0.6696  ← 小数
  │
  ▼ 第9步:再量化(为下一层准备)
  output_int8 = round(0.6696 / 0.007843) = 85  ← 又变成整数!
  │
  ▼ 传给下一层
  [85, ...]  ←────────── 整数!(所以你看这张图,是不是很多操作都变成int,屌不屌)

ok大致内容就是这些,写完都已经快11点了。后面再举个分类的例子,从哪里下载量化工具,怎么量化,还有那些指令的含义。写到这其实我有一个打算,后面再出一点各种配置环境,科学上网的小工具,这些杂七杂八的东西算了。今天就到这了,大家一起加油!!!!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐