查看原文
其他

【他山之石】pytorch量化备忘录

“他山之石,可以攻玉”,站在巨人的肩膀才能看得更高,走得更远。在科研的道路上,更需借助东风才能更快前行。为此,我们特别搜集整理了一些实用的代码链接,数据集,软件,编程技巧等,开辟“他山之石”专栏,助你乘风破浪,一路奋勇向前,敬请关注。

作者:知乎—皮特潘

地址:https://www.zhihu.com/people/wu-er-dong


01

什么是量化?

将高精度运算转化到低精度上运算,例如float32转化为int8数据上运算。

02

为什么要量化?

那就要说说量化的好处了,一般量化有以下好处:
  • 减小模型尺寸和存储空间,如int8量化可减少75%的模型大小;
  • 加快推理速度,访问一次32位浮点型可以访问四次int8型,运算更快;
  • 减少设备功耗,内存耗用少了推理速度快了自然减少了设备功耗;
  • 支持微处理器,有些微处理器属于8位的,低功耗运行浮点运算速度慢,需要进行8bit量化;
但是其也有一些缺点:
  • 增加了操作复杂度,有时会有一些特殊处理,甚至会有tradeoff,比如你发明了一个牛13的算子,可惜现有的量化工具不支持,自己实现又头大,只能忍痛割爱;
  • 会损失一定的精度,所以有时会有微调,但也会有损失;不过值得一提的是,每次我用openvino量化,精度不降低反而还会升高一丢丢。这是因为模型参数是非常冗余的,量化可以看成一种正则化技术,会提升模型的泛化能力,可能在测试集上会表现好一点。不过都是事后诸葛的理论分析了,具体还是要看测试指标的。

03

怎么量化?

虽然量化方法很多,但并无本质区别。记住一点就可以了:将高精度数据映射到低精度表达,在低精度上运算,然后再反量化回去,因为最终的输出我们还是要高精度的数据的。

04

pytorch怎么做?

当然只支持CPU。目前支持qnnpack 和 fbgemm 两个后端,都是来自自家的facebook
  • qnnpack
  • fbgemm

05

量化步骤

  1. 权重转为int8数据类型存储;
  2. 模型融合的conv+bn+relu;
  3. 存储每层的量化参数: 量化缩放因子和zero-point;
  4. 对模型进行量化;


06

量化方式

  • Post Training Dynamic Quantization:最简单的量化方法,Post Training指的是在浮点模型训练收敛之后进行量化操作,其中weight被提前量化,而activation在前向推理过程中被动态量化,即每次都要根据实际运算的浮点数据范围每一层计算一次scale和zero_point,然后进行量化;
  • Post Training Static Quantization:第一种不是很常见,一般说的Post Training Quantization指的其实是这种静态的方法,而且这种方法是最常用的,其中weight跟上述一样也是被提前量化好的,然后activation也会基于之前校准过程中记录下的固定的scale和zero_point进行量化,整个过程不存在量化参数(scale和zero_point)的再计算;
  • Quantization Aware Training:对于一些模型在浮点训练+量化过程中精度损失比较严重的情况,就需要进行量化感知训练,即在训练过程中模拟量化过程,数据虽然都是表示为float32,但实际的值的间隔却会受到量化参数的限制。

07

后训练量化

对于一个已经训练好的模型,post-training 量化的基本步骤如下:
1. 准备模型:
插入量化和反量化的模块:在需要 quantize 和 dequantize 操作的 module 中插入 QuantStub 和DeQuantStub。
def __init__(self): self.quant = QuantStub() self.dequant = DeQuantStub()
def forward(self, x): x = self.quant(x) # ------ # # 网络forward # ------ # x = self.dequant(x) return x
转换:非 module 对象表示的算子不能转换成 quantized module。比如 "+" 算术运算符无法直接转成 quantize module。"+=" 是一个无状态的运算符,需要替换成 nn.quantized.FloatFunctional()。这点非常需要注意。
self.skip_add = nn.quantized.FloatFunctional() z = self.skip_add.add(x, y) # 原来的是: z = x + y
2. 模块融合
为了提高精度和性能,一般将 conv + relu, conv + batchnorm + relu, linear + relu 等类似的操作 fuse 成一个操作。
resnet18_fused = torch.quantization.fuse_modules( resnet18, [['conv1', 'bn1', 'relu'], ['layer1.0.conv1', 'layer1.0.bn1'], # , 'layer1.0.relu'], ['layer1.0.conv2', 'layer1.0.bn2'], ['layer1.1.conv1', 'layer1.1.bn1'], # , 'layer1.1.relu'], ['layer1.1.conv2', 'layer1.1.bn2']])
3. 量化算法
为 activations/weights 指定量化算法 比如 symmtric/asymmtric/minmax 等等。Pytorch 采用 qconfig 来封装量化算法,一般通过将 qconfig 作为 module 的属性来指定量化算法。常用的 qconfig 有default_per_channel_qconfig, default_qconfig等。
q_backend = 'qnnpack' # "qnnpack" qconfig = torch.quantization.get_default_qconfig(q_backend) torch.backends.quantized.engine = q_backend resnet18_fused.qconfig = qconfig torch.quantization.prepare(resnet18_fused, inplace=True)
4. 传播 qconfig 和插入 observer
通过 torch.quantization.prepare() 向子 module 传播 qconfig,并为子 module 插入 observer。Observer 可以在获取 FP32 activations/weights 的动态范围。
5. calibration 校准标定
利用torch.quantization.prepare() 插入将在校准期间观察激活张量的模块,然后将校准数据集灌入模型,利用校准策略得到每层activation的scale和zero_point并存储;运行 calibration 推理程序搜集 FP32 activations 的动态范围。
with torch.no_grad(): for image, target in data_loader: output = model(image)
6. module 转化
通过 torch.quantization.convert 函数可以将 FP32 module 转化成 int8 module. 这个过程会量化 weights, 计算并存储 activation 的 scale 和 zero_point。
resnet18_fused = resnet18.cpu() torch.quantization.convert(resnet18_fused, inplace=True)
打印保存的权重, 可以看出, 网络的每一层都额外保存了scale 和 zero_point:

08

量化后怎么用?

1. 直接保存jit, 有些模型load进来,无法使用,待查找原因。
2. 每次初始化,都量化一下 convert之后再, load 权重,这里要注意一定要显式调用.eval()。
格式为 torch.quint8 , 内容为量化后的值(虽然是int形, 但以float形式存储)
features.0.0.weight features.0.0.scale features.0.0.zero_point features.0.0.bias features.1.conv.0.0.weight features.1.conv.0.0.scale features.1.conv.0.0.zero_point ...


本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。


直播预告



“他山之石”历史文章




分享、点赞、在看,给个三连击呗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存