深度模型量化入门(一)

在AI模型越来越大、计算需求越来越高的今天,如何在保持精度的同时让模型"瘦身",成为了每个AI工程师的必修课。

量化 的核心思想很简单:用更低精度的数据类型(如8位整数)来表示原本高精度(如32位浮点数)的模型权重和激活值

量化和反量化

用低精度的量化值来近似表示高精度的真实值,就是量化:

就是 量化值 ≈ 真实值 / 缩放值 + 零点值

反过来说,将低精度的量化值还原为高精度的真实值,就是反量化

真实值 ≈ 缩放值 ×(量化值 - 零点值

所有的量化后操作都是为了近似实现在真实值上的操作。

由此可知,一个量化变量包括了三部分:量化值、缩放值、零点值,用来代替其真实值。

对于深度模型量化后的一层网络来说,它的输入变量(input,是int或uint)、权重参数(weight,是int或uint)、偏置参数(bias,是float)、输出变量(output,是int或uint),除了偏置外,其他是量化变量。

下面用卷积层和线性层来说明量化过程:

(1)卷积量化计算过程

复制代码
输入变量(量化值x_input、输入零点input_zero_point、输入尺度input_scale):
              ↓ 数值类型转换
        如果是uint8  → 减输入零点(x_input-input_zero_point) → 有符号(int32)
        如果是int8  → 减输入零点(x_input-input_zero_point) → 有符号(int32)
              ↓ 输入
量化卷积层(权重值w、权重零点w_zero_point、权重尺度w_scale):
        权重weight是int8权重  → 减权重零点(w - w_zero_point) → 有符号权重(int32)
              ↓
        减去零点的输入(int32) 和 减去零点的权重(int32) 进行整数卷积 = 卷积结果(int32)
              ↓  
        数值类型转换:卷积结果从int32转float
              ↓  
        卷积结果 × (输入尺度input_scale × 权重尺度weight_scale) = 反量化卷积结果(float)
              ↓
        反量化卷积结果 + 卷积偏置(float) = 反量化卷积加偏置结果(float)
              ↓
        反量化卷积加偏置结果 ÷ 输出尺度 output_scale + 输出零点 output_zero_point = 反量化输出(float)
              ↓ 
        量化输出,从float转uint8或int8:
        反量化卷积加偏置结果 ÷ 输出尺度 output_scale + 输出零点 output_zero_point = 量化输出
        如果是uint8,会发生量化激活,也就是负数值在量化时会被自动截断到0。
        如果是int8,直接输出
              ↓输出
输出变量(量化值x_output、输出零点output_zero_point、输出缩放值output_scale)

量化激活(不是平时说的激活层)是指,当**使用无符号量化(如uint8,范围0-255)时,负数值在量化时会被自动截断到0。**这会产生类似ReLU的效果,但不是网络中本来就有网络层。

(2)线性层量化计算过程

复制代码
输入变量(量化值x_input、输入零点input_zero_point、输入尺度input_scale):
              ↓ 数值类型转换
        如果是uint8  → 减输入零点(x_input-input_zero_point) → 有符号(int32)
        如果是int8  → 减输入零点(x_input-input_zero_point) → 有符号(int32)
              ↓ 输入
线性层(权重值w、权重零点w_zero_point、权重尺度w_scale):
        权重weight是int8权重  → 减权重零点(w - w_zero_point) → 有符号权重(int32)
              ↓
        减去零点的输入(int32) 和 减去零点的权重(int32) 进行整数矩阵乘法 = 乘法结果(int32)
              ↓  
        数值类型转换:乘法结果从int32转float
              ↓  
        卷积结果 × (输入尺度input_scale × 权重尺度weight_scale) = 反量化卷积结果(float)
              ↓
        反量化卷积结果 + 卷积偏置(float) = 反量化乘法加偏置结果(float)
              ↓ 
        反量化输出进行四舍五入取整
              ↓
        量化输出,从float转uint8或int8:
        反量化卷积加偏置结果 ÷ 输出尺度 output_scale + 输出零点 output_zero_point = 量化输出
        如果是uint8,会发生量化激活,也就是负数值在量化时会被自动截断到0。
        如果是int8,直接输出
              ↓输出
输出变量(量化值x_output、输出零点output_zero_point、输出缩放值output_scale)

为什么不直接存储int32?

因为内存带宽是AI计算的真正瓶颈,而类型提升成本很低。在CPU流水线中,类型提升只是数据通路的一部分,几乎不增加时钟周期。内存访问是瓶颈,类型提升在寄存器中完成。4倍的内存节约 = 4倍的缓存效率。此外,整数运算单元比浮点单元更小、更快、更省电。

1. 内存带宽节省(最重要的原因)

从内存加载数据到CPU:

uint8:一次加载32字节 → 32个数值

int32:一次加载32字节 → 8个数值

内存访问次数减少4倍!

2. 缓存效率提升

// 同样大小的L1缓存:

uint8数组:可以缓存 32KB ÷ 1字节 = 32,768个元素

int32数组:只能缓存 32KB ÷ 4字节 = 8,192个元素

缓存命中率更高!

3. 计算量的真实对比

假设有1000万个权重参数:

复制代码
// 方案A:全程使用int32_t(纯浮点网络的量化模拟)
内存占用:1000万 × 4字节 = 40MB
内存带宽需求:40MB × 2(读写) = 80MB
计算:浮点乘法1000万次

// 方案B:使用uint8/int8 + 类型提升
内存占用:1000万 × 1字节 = 10MB ✅
内存带宽需求:10MB × 2 = 20MB ✅
计算:
  - 类型提升:1000万次(几乎零成本)
  - int8×int8乘法:1000万次(比浮点快3-4倍)
  - 累加到int32:1000万次
总计算量:约2000万次整数操作

4. 实际性能差异

复制代码
# 假设特征维度:4096
# 批量大小:128

# float32版本
内存加载:128 × 4096 × 4字节 = 2MB
计算:128 × 4096 × 4096次浮点乘加 ≈ 2.15亿次浮点运算

# int8版本
内存加载:128 × 4096 × 1字节 = 0.5MB (快4倍)
计算:128 × 4096 × 4096次int8乘加 + 类型提升
      ≈ 2.15亿次整数运算 + 500万次类型提升
整数运算比浮点运算快3-4倍

打个比方

想象你要搬1000本书:

float32方案:一次搬4本书,需要搬250次

int8方案:一次搬16本书,需要搬63次,但每搬一次需要花1秒钟整理

整理时间(类型提升)远小于搬运次数减少带来的收益

看似多了一步类型提升,但整体性能提升3-4倍!

训练后量化 和 量化感知训练

训练后量化(Post-Training Quantization,PTQ) 就像是给已经训练好的模型"减肥"。你先正常训练一个浮点模型 ,然后再对它进行量化处理。这种方法简单快捷,不需要重新训练,但可能会损失一定的精度。适用场景:快速部署需求、资源受限的环境、对精度损失有一定容忍度的应用

量化感知训练(Quantization-Aware Training,QAT) 则是"从娃娃抓起"的策略。你在训练过程中就模拟量化的效果 ,让模型在学习过程中就适应低精度表示。这种方法通常能获得更好的精度,但需要更多的训练时间和计算资源。适用场景:对精度要求极高的应用、有足够时间和资源进行重新训练、复杂模型的量化。

普通训练: FP32训练 → 完成模型

训练后量化: FP32训练 → FP32模型 → 量化 → 量化模型

量化感知训练: FP32训练 → 插入量化模拟 → QAT训练 → 量化模型

量化感知训练技术,也叫 **伪量化技术。**它的目的不是直接产生一个低精度(如INT8)的模型,而是在训练阶段就让模型"提前适应"将来量化时会带来的精度损失,从而在最终真正量化后,模型性能下降最小。

要理解伪量化,首先要明白标准的训练后量化流程的缺点:

标准训练后量化流程:用高精度(FP32)训练一个模型。训练完成后,直接将权值和激活值转换为低精度(如INT8)。

问题是:这个转换过程会引入误差(舍入误差、截断误差),模型在训练时从未见过这种误差,因此量化后的模型精度可能会有明显下降,尤其是对于敏感的任务或轻量级模型。

伪量化的解决方案:在训练阶段,就在前向传播中插入模拟量化操作,让模型在FP32的计算中,"感受"到量化带来的数值变化和误差。在反向传播时,使用技巧(如直通估计器)让梯度能够正常回传。这样训练出的模型,其权重本身就"知道"将来会被量化,从而学会了在量化噪声下保持鲁棒性。最终量化时,精度损失非常小,甚至没有损失。

动态量化 和 静态量化

动态量化(Dynamic Quantization) 是一种"运行时决定"的策略。在这种方法中,模型的权重在加载时就被量化,但激活值(activations)的量化是在推理过程中动态进行的工作原理 :模型权重在推理前就量化为8位整数;激活值在推理过程中根据实际数据范围动态量化;量化参数(如缩放因子)在每次推理时重新计算。优点 :适应性强,能处理输入数据分布变化大的情况;实现相对简单;内存节省明显。缺点:推理时有额外的计算开销(需要计算量化参数);可能不如静态量化高效。

静态量化(Static Quantization) 采用"预先计划"的策略。它需要在离线阶段收集代表性数据 ,统计出激活值的分布范围,然后固定量化参数。工作原理 :使用代表性校准数据集运行模型;收集各层激活值的统计信息(最小/最大值或分布);根据统计信息确定每层的量化参数(缩放因子和零点);应用固定的量化参数进行推理。优点 :推理速度快,无额外计算开销;更极致的性能优化;适合硬件加速。缺点:需要代表性校准数据;对数据分布变化的适应性较差;实现更复杂。

通过一个具体的数值例子来解释动态量化和静态量化的区别。

假设我们有一个简单的神经网络层,处理一个输入向量 x = [1.0, -2.0, 3.0]。该层的权重为 W = [0.5, 0.8, -0.2](为了简化,我们只做逐元素乘,类似卷积核)。

浮点运算过程如下:

复制代码
输出 y = x * W = [1.0×0.5, -2.0×0.8, 3.0×(-0.2)] = [0.5, -1.6, -0.6]

现在我们要对这个计算进行8位整数量化(范围0-255)。

(1)动态量化的过程

步骤1:权重预先量化(离线)

权重 W 量化:

原始 W = [0.5, 0.8, -0.2]

观察范围:min=-0.2,max=0.8

计算缩放因子(scale_w)= (max - min) / 255 = (0.8 - (-0.2)) / 255 = 1.0 / 255 ≈ 0.00392

计算零点(zero_point_w)= round(0 - min/scale_w) = round(0 - (-0.2)/0.00392) = round(51.02) = 51

量化公式:W_q = round(W / scale_w + zero_point_w)

0.5/0.00392 + 51 ≈ 127.55 + 51 ≈ 178.55 → 179

0.8/0.00392 + 51 ≈ 204.08 + 51 ≈ 255.08 → 255

-0.2/0.00392 + 51 ≈ -51.02 + 51 ≈ -0.02 → 0

量化后权重:W_q = [179, 255, 0]

步骤2:推理时激活值动态量化(在线)

输入 x = [1.0, -2.0, 3.0] 的量化:

实时观察这批输入的范围:min=-2.0,max=3.0

计算缩放因子(scale_x)= (3.0 - (-2.0)) / 255 = 5.0 / 255 ≈ 0.0196

计算零点(zero_point_x)= round(0 - (-2.0)/0.0196) = round(102.04) = 102

量化输入:x_q = round(x / scale_x + zero_point_x)

1.0/0.0196 + 102 ≈ 51.02 + 102 ≈ 153.02 → 153

-2.0/0.0196 + 102 ≈ -102.04 + 102 ≈ -0.04 → 0

3.0/0.0196 + 102 ≈ 153.06 + 102 ≈ 255.06 → 255

量化后输入:x_q = [153, 0, 255]

步骤3:整数计算

复制代码
# 整数计算(需要反量化才能得到浮点结果)
# 先反量化权重和输入
W_deq = (W_q - zero_point_w) × scale_w
       = ([179-51, 255-51, 0-51]) × 0.00392
       = [128, 204, -51] × 0.00392
       ≈ [0.502, 0.800, -0.200]  # 接近原始权重

x_deq = (x_q - zero_point_x) × scale_x
       = ([153-102, 0-102, 255-102]) × 0.0196
       = [51, -102, 153] × 0.0196
       ≈ [1.000, -2.000, 3.000]  # 接近原始输入

# 或者直接使用量化计算(需要处理缩放因子的乘积)
y_q = (W_q - zero_point_w) × (x_q - zero_point_x)  # 这是整数计算
    = [128, 204, -51] × [51, -102, 153]
    = [6528, -20808, -7803]

# 最终反量化输出
scale_y = scale_w × scale_x = 0.00392 × 0.0196 ≈ 0.0000768
y = y_q × scale_y = [6528, -20808, -7803] × 0.0000768
  ≈ [0.501, -1.598, -0.599]  # 接近浮点结果 [0.5, -1.6, -0.6]

动态量化特点: 这次推理的 scale_x=0.0196 和 zero_point_x=102 是针对这批输入 [1.0, -2.0, 3.0] 计算的。如果下一批输入不同,这些参数会重新计算。

(2)静态量化的过程

步骤1:使用校准数据集统计激活值范围

假设我们用代表性数据多次运行模型,收集到输入x的统计信息:

输入x的典型范围:min=-2.5,max=3.5(注意:这比单次输入的范围更广)

步骤2:预先计算并固定所有量化参数

权重量化(与动态量化相同):

W_q = [179, 255, 0]

scale_w = 0.00392,zero_point_w = 51

输入量化(使用统计范围而非实时计算):

使用统计范围:min=-2.5,max=3.5

scale_x_static = (3.5 - (-2.5)) / 255 = 6.0 / 255 ≈ 0.0235

zero_point_x_static = round(0 - (-2.5)/0.0235) = round(106.38) = 106

注意:这些参数在模型部署时就固定了

步骤3:推理时使用固定参数

复制代码
# 用固定的scale_x_static和zero_point_x_static量化输入
x_q_static = round(x / 0.0235 + 106)
   1.0/0.0235 + 106 ≈ 42.55 + 106 ≈ 148.55 → 149
  -2.0/0.0235 + 106 ≈ -85.11 + 106 ≈ 20.89 → 21
   3.0/0.0235 + 106 ≈ 127.66 + 106 ≈ 233.66 → 234
x_q_static = [149, 21, 234]

# 整数计算
y_q_static = (W_q - 51) × (x_q_static - 106)
           = [128, 204, -51] × [149-106, 21-106, 234-106]
           = [128, 204, -51] × [43, -85, 128]
           = [5504, -17340, -6528]

# 输出反量化(输出也有固定的scale_y_static)
# scale_y_static = scale_w × scale_x_static = 0.00392 × 0.0235 ≈ 0.0000921
y_static = y_q_static × 0.0000921
         ≈ [0.507, -1.597, -0.601]  # 仍然接近浮点结果

做个类比,动态量化相当于对数据做自身的极差归一化,而静态归一化相当于对数据做固定参数的归一化,参数是提前算好的。

如果输入变为 x = [4.0, -3.0, 2.0](超出静态统计范围):

动态量化:重新计算scale_x=0.0275,仍能较好处理

静态量化:仍用scale_x=0.0235,x=4.0会被饱和截断到255,可能损失精度

静态量化的固定参数允许硬件预分配资源,实现更高效的整数运算。动态量化的变化参数需要更灵活的处理单元。这个例子展示了为什么静态量化更快但需要代表性数据校准,而动态量化更灵活但计算开销更大。

四种量化方式

"训练后量化 vs 量化感知训练" 说的是 "什么时候做量化" 的问题。
"动态量化 vs 静态量化" 说的是 "具体怎么做量化" 的问题。

复制代码
                "量化具体怎么做?"
         ┌─────────────┬─────────────┐
         │  动态量化   │  静态量化   │
         │ (边做边定) │ (预先定好) │
┌────────┼─────────────┼─────────────┤
│训练后  │ 训练后+动态 │ 训练后+静态 │
│量化    │ (常见组合) │ (最常用组合)│
├────────┼─────────────┼─────────────┤
│量化感知│ 感知训练+动态│ 感知训练+静态│
│训练    │ (较少见)   │ (常见组合)  │
└────────┴─────────────┴─────────────┘

组合1:训练后 + 静态量化(最常用)

先训练好模型,然后用一批数据校准,固定所有量化参数。

python 复制代码
# 1. 训练一个正常模型(精致大餐)
model = train_fp32_model()

# 2. 准备校准数据(调研顾客喜好)
calibration_data = load_calibration_dataset()

# 3. 进行训练后静态量化
quantized_model = torch.quantization.quantize_static(
    model, 
    calibration_data
)

组合2:训练后 + 动态量化(较简单)

python 复制代码
# 权重提前量化,激活值运行时动态量化
model = torch.quantization.quantize_dynamic(
    model,  # 训练好的模型
    {torch.nn.Linear},  # 只量化这些层
    dtype=torch.qint8
)

组合3:量化感知训练 + 静态量化(精度最高)

训练时就模拟量化,训练完用静态量化部署。

python 复制代码
# 1. 训练时插入"伪量化"节点模拟量化效果
model.qconfig = torch.quantization.get_default_qconfig('fbgemm')
model_prepared = torch.quantization.prepare_qat(model)
model_trained = train_model(model_prepared)  # 训练时就知道自己要变快餐

# 2. 转换为真正的量化模型(固定套餐)
model_quantized = torch.quantization.convert(model_trained)
相关推荐
轻造科技2 小时前
包装管理系统+尺寸匹配算法:根据产品规格自动推荐包装方案,材料浪费减少25%
人工智能·mes·mes系统
DS随心转APP2 小时前
deepseek公式复制方法
人工智能·ai·deepseek·ds随心转
BHXDML2 小时前
计算视视觉:实验一车牌检测与识别
人工智能·计算机视觉
迦蓝叶2 小时前
Javaluator 与 Spring AI 深度集成:构建智能表达式计算工具
人工智能·spring·ai·语言模型·tools·spring ai·mcp
爱学习的张大2 小时前
transform基础练习(从细节里面理解)
人工智能·pytorch·深度学习
木土雨成小小测试员2 小时前
Python测试开发之后端一
开发语言·数据库·人工智能·python·django·sqlite
轴测君2 小时前
卷积神经网络的开端:LeNet−5
人工智能·神经网络·cnn
老周聊架构2 小时前
构建AI观察者:生成式语义工作区(GSW)深度解析与技术全瞻
人工智能
叫我:松哥2 小时前
spark+flask的新能源车数据分析与智能推荐系统,融合大数据分析、机器学习和人工智能技术
人工智能·机器学习·信息可视化·数据分析·spark·flask·bootstrap