Pytorch模型量化

在深度学习中,量化指的是使用更少的bit来存储原本以浮点数存储的tensor,以及使用更少的bit来完成原本以浮点数完成的计算。这么做的好处主要有如下几点:

  • 更少的模型体积,接近4倍的减少;
  • 可以更快的计算,由于更少的内存访问和更快的int8计算,可以快2~4倍。

一个量化后的模型,其部分或者全部的tensor操作会使用int类型来计算,而不是使用量化之前的float类型。当然,量化还需要底层硬件支持,x86 CPU(支持AVX2)、ARM CPU、Google TPU、Nvidia Volta/Turing/Ampere、Qualcomm DSP这些主流硬件都对量化提供了支持。

PyTorch对量化的支持目前有如下三种方式:

  • Post Training Dynamic Quantization:模型训练完毕后的动态量化;
  • Post Training Static Quantization:模型训练完毕后的静态量化;
  • QAT (Quantization Aware Training):模型训练中开启量化。

在开始这三部分之前,先介绍下最基础的Tensor的量化。

Tensor的量化

量化:$$公式1:xq=round(\frac{x}{scale}+zero_point)$$
反量化:$$公式2:x = (xq-zero_point)*scale$$
式中,scale是缩放因子,zero_point是零基准,也就是fp32中的零在量化tensor中的值

为了实现量化,PyTorch 引入了能够表示量化数据的Quantized Tensor,可以存储 int8/uint8/int32类型的数据,并携带有scale、zero_point这些参数。把一个标准的float Tensor转换为量化Tensor的步骤如下:

xdq和x的值已经出现了偏差的事实告诉了我们两个道理:

  • 量化会有精度损失
  • 我们随便选取的scale和zp太烂, 选择合适的scale和zp可以有效降低精度损失。不信你把scale和zp分别换成scale = 0.0036, zero_point = 0试试

而在PyTorch中,选择合适的scale和zp的工作就由各种observer来完成。

Tensor的量化支持两种模式:per tensor 和 per channel。

  • Per tensor:是说一个tensor里的所有value按照同一种方式去scale和offset;
  • Per channel:是对于tensor的某一个维度(通常是channel的维度)上的值按照一种方式去scale和offset,也就是 一个tensor里有多种不同的scale和offset的方式(组成一个vector),如此以来,在量化的时候相比per tensor的方式会引入更少的错误。PyTorch目前支持conv2d()、conv3d()、linear()的per channel量化。

在我们正式了解pytorch模型量化前我们再来检查一下pytorch的官方量化是否能满足我们的需求,如果不能,后面的都不需要看了

静态量化动态量化nn.linearYYnn.Conv1d/2d/3dYN (因为pytorch认为卷积参数来了个太小了,对卷积核进行量化会造成更多损失,所以pytorch选择不量化)

Ynn.RNNCellNYnn.GRUCellNYnn.LSTMCellNYnn.EmbeddingBagY(激活在fp32)Ynn.EmbeddingYNnn.MultiheadAttentionNNActivations大部分支持不变,计算停留在fp32中

第二点:pytorch模型的动态量化只量化权重,不量化偏置

Post Training Dynamic Quantization (训练后动态量化)

意思就是 对训练后的模型权重执行动态量化,将浮点模型转换为动态量化模型, 仅对模型权重进行量化,偏置不会量化。默认情况下, 仅对 Linear 和 RNN 变体量化 (因为这些layer的参数量很大,收益更高)。

参数:

  • model:浮点模型
  • qconfig_spec
  • 下面的任意一种
    • 集合:比如:qconfig_spec={nn.LSTM, nn.Linear} 。罗列 要量化的NN
    • 字典:qconfig_spec = {nn.Linear : default_dynamic_qconfig, nn.LSTM : default_dynamic_qconfig}
  • dtype: float16 或 qint8
  • mapping:就地执行模型转换,原始模块发生变异
  • inplace:将子模块的类型映射到需要替换子模块的相应动态量化版本的类型

返回:动态量化后的模型

我们来吃一个栗子:

Post Training Static Quantization (训练后静态量化)

静态量化需要把模型的权重和激活都进行量化,静态量化需要把训练集或者和训练集分布类似的数据喂给模型(注意没有反向传播),然后通过每个op输入的分布 来计算activation的量化参数(scale和zp)——称之为Calibrate(定标),因为静态量化的前向推理过程自始至终都是int计算,activation需要确保一个op的输入符合下一个op的输入。

PyTorch会使用以下5步来完成模型的静态量化:

合并一些可以合并的layer。这一步的目的是为了提高速度和准确度:

比如给fuse_modules传递下面的参数就会合并网络中的conv1、bn1、relu1:

一旦合并成功,那么原始网络中的fc就会被替换为新的合并后的module(因为其是list中的第一个元素),而relu(list中剩余的元素)会被替换为nn.Identity(),这个模块是个占位符,直接输出输入。举个例子,对于下面的一个小网络:

modules_to_fuse参数的list可以包含多个item list,或者是submodule的op list也可以,比如:[ [‘conv1’, ‘bn1’, ‘relu1’], [‘submodule.conv’, ‘submodule.relu’]]。有的人会说了,我要fuse的module被Sequential封装起来了,如何传参?参考下面的代码:

就目前来说,截止目前为止,只有如下的op和顺序才可以 (这个mapping关系就定义在DEFAULT_OP_LIST_TO_FUSER_METHOD中):

  • Convolution, BatchNorm
  • Convolution, BatchNorm, ReLU
  • Convolution, ReLU
  • Linear, ReLU
  • BatchNorm, ReLU
  • ConvTranspose, BatchNorm

qconfig要设置到模型或者Module上。

x86和arm之外目前不支持。

prepare用来给每个子module插入Observer,用来收集和定标数据。

以activation的observer为例,观察输入数据得到 四元组中的 min_val 和 max_val,至少观察个几百个迭代的数据吧,然后由这四元组得到 scale 和 zp 这两个参数的值。

这一步不是训练。是为了获取数据的分布特点,来更好的计算activation的 scale 和 zp 。至少要喂上几百个迭代的数据。

第四步完成后,各个op权重的四元组 (min_val,max_val,qmin, qmax) 中的 min_val , max_val 已经有了,各个op activation的四元组 (min_val,max_val,qmin, qmax) 中的 min_val , max_val 也已经观察出来了。那么在这一步我们将调用convert API:

我们来吃一个完整的例子:

Quantization Aware Training (边训练边量化)

这一部分我用不着,等我需要使用的时候再来补充

保存和加载量化模型

我们先把模型量化

获取量化模型的参数

其实pytorch获取量化后的模型参数是比较困难的,我们还是以上面的量化模型为例来取参数的值

我们来尝试一下获取线性层的权重和偏置

O My God, 偏置居然还是浮点类型的,只有权重被量化为了整型

好的,我们再来获取GRU的权重和偏置

第一,别问我别问我为什么取值这么麻烦,你以为我想???

第二,静态量化不支持GRU就算了,动态量化偏置还不给我量化了,哎,pytorch的量化真的是还有很长的路要走呀!

Original: https://blog.csdn.net/qq_34218078/article/details/127521819
Author: 凌逆战
Title: Pytorch模型量化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/787102/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球