为 TVM 添加对 Paddle 量化模型的支持

news/2024/11/29 18:11:04/文章来源:https://www.cnblogs.com/Zheng-Bicheng/p/18577302

1 简介

随着深度学习应用的广泛使用,量化模型作为一种有效的模型压缩技术,能够在保持模型精度的同时减少模型的计算和存储开销。本文将介绍如何在 TVM 上为 Paddle 深度学习框架中的量化模型提供解析支持。

2 量化方法

目前主流的的量化方法主要分为 QOperator 和 QDQ(Quantize and DeQuantize) 两种方法,ONNX 中对这两个量化模型的表示方式表述为:

There are two ways to represent quantized ONNX models:

  • Operator-oriented (QOperator). All the quantized operators have their own ONNX definitions, like QLinearConv, MatMulInteger and etc.

  • Tensor-oriented (QDQ; Quantize and DeQuantize). This format inserts DeQuantizeLinear(QuantizeLinear(tensor)) between the original operators to simulate the quantization and dequantization process. In Static Quantization, the QuantizeLinear and DeQuantizeLinear operators also carry the quantization parameters. In Dynamic Quantization, a ComputeQuantizationParameters function proto is inserted to calculate quantization parameters on the fly. Models generated in the following ways are in the QDQ format:

    • Models quantized by quantize_static or quantize_dynamic API, explained below, with quant_format=QuantFormat.QDQ.

    • Quantization-Aware training (QAT) models converted from Tensorflow or exported from PyTorch.

    • Quantized models converted from TFLite and other frameworks.

For the latter two cases, you don’t need to quantize the model with the quantization tool. ONNX Runtime can run them directly as a quantized model.

两种量化格式的在模型结构上的区别可以由下图直观展示(左边为 QOperator,右边为 Quantize and DeQuantize)

Paddle 的量化模型格式与 ONNX 的 QDQ 量化格式类似,类似通过类似的方法储存量化信息节点,Paddle 量化模型可视化后结果如下:

3 在 TVM 中注册并实现对量化 OP

TVM 的 Python 源代码文件 paddlepaddle.py 中记录了读取 Paddle 模型的全过程,其中 _convert_map 这个字典变量记录了当前支持的 OP 名字以及 OP 转换为 TVM relay 的方法。通过阅读可以发现,该字典未添加对 dequantize_linearquantize_linear 这两个算子的转换方法,我们需要手动添加他,我们可以在字典末尾添加对应格式的代码,添加后代码如下:

_convert_map = {"abs": convert_unary_op,......"where_index": convert_where_index,# Quantized"dequantize_linear": convert_dequantize_linear,"quantize_linear": convert_quantize_linear,
}

接下来我们需要完成 convert_dequantize_linearconvert_quantize_linear 函数,上文提到 Paddle 量化模型的格式和 ONNX 量化模型类似,因此我们可以参考一下 ONNX 的写法,ONNX 中转换算子的关键代码如下:

class QuantizeLinear(OnnxOpConverter):"""Operator converter for QuantizeLinear."""@classmethoddef _impl_v13(cls, inputs, attr, params):data, scale, zp = inputsout_dtype = infer_type(zp).checked_type.dtypeaxis = attr.get("axis", 1)if len(infer_shape(data)) < 2:axis = 0return _qnn.op.quantize(data, scale, _op.cast(zp, "int32"), axis, out_dtype)class DequantizeLinear(OnnxOpConverter):"""Operator converter for QuantizeLinear."""@classmethoddef _impl_v13(cls, inputs, attr, params):data, scale, zp = inputsaxis = attr.get("axis", 1)if len(infer_shape(data)) <= 1:axis = 0return _qnn.op.dequantize(data, scale, _op.cast(zp, "int32"), axis)

我们针对 Paddle 模型也添加类似的代码:

def convert_quantize_linear(g, op, block):"""Operator converter for dequantize_linear."""data_node_name = op.input("X")[0]data_node = g.get_node(data_node_name)tvm_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()tvm_quantize_zp = g.get_params(op.input("ZeroPoint")[0]).asnumpy()tvm_quantize_axis = op.attr("quant_axis")if tvm_quantize_axis == -1:tvm_quantize_axis = 0out = _qnn.op.quantize(data=data_node,output_scale=_op.const(tvm_quantize_scale, "float32"),output_zero_point=_op.const(tvm_quantize_zp, "int32"),axis=tvm_quantize_axis,)g.add_node(op.output("Y")[0], out)def convert_dequantize_linear(g, op, block):"""Operator converter for dequantize_linear."""data_node_name = op.input("X")[0]data_node = g.get_node(data_node_name)tvm_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()tvm_quantize_zp = g.get_params(op.input("ZeroPoint")[0]).asnumpy()tvm_quantize_axis = op.attr("quant_axis")if tvm_quantize_axis == -1:tvm_quantize_axis = 0if len(infer_shape(data_node)) < 2:tvm_quantize_axis = 0out = _qnn.op.dequantize(data=data_node,input_scale=_op.const(tvm_quantize_scale, "float32"),input_zero_point=_op.const(tvm_quantize_zp, "int32"),axis=tvm_quantize_axis,)g.add_node(op.output("Y")[0], out)

为了测试我们编写的代码是否正常,我们可以编写以下测试脚本,该脚本对 TVM、Paddle、ONNX 模型的推理结果进行了相互的比较。

import paddle
import tvm
from tvm import relay
from tvm.contrib import graph_executor
import numpy as np
import onnx
import onnxruntime as rt# Model Attr
input_shape = [1, 3, 224, 224]
input_name = "inputs"def infer_by_paddlepaddle(temp_prefix, temp_input_data):paddle.enable_static()exe = paddle.static.Executor(paddle.CPUPlace())temp_prog, feed_target_names, fetch_targets = paddle.static.load_inference_model(temp_prefix, exe)temp_output, = exe.run(temp_prog, feed={feed_target_names[0]: temp_input_data}, fetch_list=fetch_targets)return temp_prog, temp_outputdef infer_by_onnx(temp_model_path, temp_input_data):sess = rt.InferenceSession(temp_model_path, None)temp_input_name = sess.get_inputs()[0].nameout_name = sess.get_outputs()[0].nametemp_onnx_output = sess.run([out_name], {temp_input_name: temp_input_data})[0]temp_onnx_model = onnx.load_model(temp_model_path)return temp_onnx_model, temp_onnx_outputdef infer_by_tvm(temp_model, temp_input_data):if isinstance(temp_model, paddle.static.Program):# model is loaded by `paddle.static.load_inference_model`mod, params = relay.frontend.from_paddle(temp_model, shape_dict={input_name: input_shape})else:mod, params = relay.frontend.from_onnx(temp_model, shape={input_name: input_shape})with tvm.transform.PassContext(opt_level=5):lib = relay.build(mod, target="llvm", params=params)# tvm inferencectx = tvm.cpu()tvm_model = graph_executor.GraphModule(lib['default'](ctx))tvm_model.set_input(input_name, temp_input_data)tvm_model.run()tvm_output = tvm_model.get_output(0).asnumpy()return tvm_outputlog_file = "tune.json"
if __name__ == "__main__":np.random.seed(520)# create input datainput_data = np.random.randn(1, 3, 224, 224).astype(np.float32)paddle_prefix = "MobileNetV1_QAT/inference"paddle_model, paddle_output = infer_by_paddlepaddle(paddle_prefix, input_data)onnx_model_path = "MobileNetV1_QAT/inference.onnx"onnx_model, onnx_output = infer_by_paddlepaddle(paddle_prefix, input_data)# 对比测试Paddle模型和ONNX模型的输出(通过测试)np.testing.assert_allclose(paddle_output[0], onnx_output[0], rtol=1e-5, atol=1e-5)# 测试TVM_Paddle模型和TVM_ONNX模型的输出(通过测试)tvm_paddle_result = infer_by_tvm(paddle_model, input_data)tvm_onnx_result = infer_by_tvm(onnx_model, input_data)np.testing.assert_allclose(tvm_paddle_result[0], tvm_onnx_result[0], rtol=1e-5, atol=1e-5)# 测试Paddle模型和TVM_Paddle模型的输出np.testing.assert_allclose(tvm_paddle_result[0], paddle_output[0], rtol=1e-5, atol=1e-5)# 测试ONNX模型和TVM_ONNX模型的输出np.testing.assert_allclose(tvm_onnx_result[0], onnx_output[0], rtol=1e-5, atol=1e-5)

通过运行测试代码,我们会发现以下三个现象,这有助于帮助我们分析代码的现状:

  • Paddle 和 ONNX 模型的输出是一致的
  • Paddle 模型和 TVM 模型的输出大面积不一致
  • ONNX 模型和 TVM 模型的输出不一致,但是在可控的范围内

该测试 ONNX 模型是 Paddle 量化模型使用 Paddle2ONNX 导出后的模型

4 分析现状并修复代码

Paddle 模型和 ONNX 模型的输出一致说明 ONNX 模型的导出是没有问题的,ONNX 模型和 TVM 模型输出相近说明当前 ONNX 模型的转换基本上是没有问题的,Paddle 模型和 TVM 模型的输出大面积不一致说明 Paddle 模型的转换出现了问题。

一般来说,算子的运算结果出现错误是由于对参数的读取导致的。于是我们可以阅读 Paddle 模型是如何转换为 ONNX 模型的来帮助我们发现当前转换方式存在的问题。在 Paddle2ONNX中,我们可以发现在将 Paddle 的 dequantize_linearquantize_linear 这两个算子进行转换时,对 scale 的处理过程如下:

void QuantizeLinearMapper::Opset10() {std::vector<float> scales;Assert(TryGetInputValue("Scale", &scales),"Failed to read tensor value of `Scale`.");std::vector<float> onnx_scales;onnx_scales.reserve(scales.size());for (auto &i : scales) {onnx_scales.push_back(i / 127);}
}void DequantizeLinearMapper::Opset10() {std::vector<float> scales;Assert(TryGetInputValue("Scale", &scales),"Failed to read tensor value of `Scale`.");std::vector<float> onnx_scales;onnx_scales.reserve(scales.size());for (auto &i : scales) {onnx_scales.push_back(i / 127);}
}

由上述代码我们发现,在将 Paddle 算子转换为 ONNX 算子的过程中, scale 是要除127的。我们可以用同样的逻辑来在 TVM 读取 Paddle 模型时对scale做同样的操作,代码如下:

def convert_dequantize_linear(g, op, block):"""Operator converter for dequantize_linear."""data_node_name = op.input("X")[0]data_node = g.get_node(data_node_name)# paddle_scale = tvm_scale * 127paddle_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()tvm_quantize_scale = paddle_quantize_scale / 127.0tvm_quantize_zp = g.get_params(op.input("ZeroPoint")[0]).asnumpy()tvm_quantize_axis = op.attr("quant_axis")if tvm_quantize_axis == -1:tvm_quantize_axis = 0if len(infer_shape(data_node)) < 2:tvm_quantize_axis = 0out = _qnn.op.dequantize(data=data_node,input_scale=_op.const(tvm_quantize_scale, "float32"),input_zero_point=_op.const(tvm_quantize_zp, "int32"),axis=tvm_quantize_axis,)g.add_node(op.output("Y")[0], out)def convert_quantize_linear(g, op, block):"""Operator converter for dequantize_linear."""data_node_name = op.input("X")[0]data_node = g.get_node(data_node_name)# paddle_scale = tvm_scale * 127paddle_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()tvm_quantize_scale = paddle_quantize_scale / 127.0tvm_quantize_zp = g.get_params(op.input("ZeroPoint")[0]).asnumpy()tvm_quantize_axis = op.attr("quant_axis")if tvm_quantize_axis == -1:tvm_quantize_axis = 0out = _qnn.op.quantize(data=data_node,output_scale=_op.const(tvm_quantize_scale, "float32"),output_zero_point=_op.const(tvm_quantize_zp, "int32"),axis=tvm_quantize_axis,)g.add_node(op.output("Y")[0], out)

再次运行第一次测试的代码,发现此时 Paddle 模型与 TVM 模型的误差与 ONNX 模型和 TVM 模型的误差一致,说明移植已经成功。

TVM 对算子推理有着自己的运算机制,运算结果出现一定的误差是正常的

5 参考资料

  • Quantize ONNX Models
  • Paddle2ONNX dequantize_linear.cc
  • Paddle2ONNX quantize_linear.cc
  • [Frontend][PaddlePaddle] PaddlePaddle model with NCHW data format that supports quantization

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/843553.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tp6 url 规则

http://tp.cc/index.php/index/test return view( index, [info => $info, sj=>$sj] );

拥抱 OpenTelemetry:阿里云 Java Agent 演进实践

我们用了接近一年的时间完成了基于 OTel Java Agent 的升级重构,并于今年 5 月份发布了 4.x 探针的第一个版本 4.1.0,经过接近半年时间的验证、回归、优化,目前最新的稳定版本 4.1.12[11]已经正式发布,欢迎大家了解使用。作者:陈承 背景 在 2018 年的 2 月,ARMS Java Age…

BIM技术:数字孪生城区建设的加速器

在智慧城市建设的浪潮中,BIM(建筑信息模型)技术以其独特的优势,成为推动数字孪生城区建设的重要技术力量。本文将探讨BIM技术如何成为数字孪生城区建设的加速器,以及其在建设过程中的关键作用。BIM技术与数字孪生城区的融合数字孪生城区是指在数字空间构建一个与实体城市相…

ADF - [01] 概述

题记部分 001 || 简介Azure Data Factory (ADF) 是微软 Azure 云平台提供的一种数据集成服务,它允许用户创建、安排和管理数据管道,以实现从不同来源抽取数据、转换数据并加载到目标存储的过程。这个过程通常被称为 ETL(Extract, Transform, Load)。002 || 适用场景 【1】…

使用Powershell运行脚本报错的处理方法

最近在一台办公电脑上运行powershell脚本时报错如下: 系统上禁止运行脚本。有关详细信息,请参阅 https :/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。 所在位置 行:1 字符: 1 + .\CreateFolders.ps1 + ~~~~~~~~~~~~~~~~~~~ + CategoryInfo : S…

20222408 2021-2022-2 《网络与系统攻防技术》实验七实验报告

1.实验内容 1.1实验内容简述 (1)应用SET工具建立冒名网站。 (2)利用ettercap完成DNS欺骗攻击。 (3)结合应用两种技术,使被攻击者会通过域名访问到冒名网站。 (4)结合攻击过程,提出具体防范方法。 1.2学习内容简述 (1)学习SET工具和ettercap工具的使用。 (2)理解A…

Vulnhub sick0s1.1

0x01:端口扫描 主机发现 nmap -sn 192.168.231.0/24全端口扫描 nmap --min-rate 10000 -p- 192.168.231.14122ssh,3128squid-http,但8080http是关闭的Squid 是一个高性能的开源代理服务器软件,它支持多种协议,包括 HTTP、HTTPS、FTP 等。它通常用于以下几种用途: 1、Web代…

Gemini

Gemini: 大规模DNN芯片阵列加速器的布局和架构 摘要 chiplet概要目标Chiplet(芯片阵列)技术允许在单一加速器上集成不断增加的晶体管的数量,在前摩尔定律时代获得了更高的效果,体现了在快速AI迭代进步中需要的大量算力。 但是,这样也引进了更高昂的大包开销,以及大量的d2…

hhdb数据库介绍(10-19)

监控 智能物理拓扑 物理拓扑图主要以服务器为视角展示集群组件与服务器的所属关系,同时可查看服务器资源的使用情况以及各集群组件服务运行状态。使用前需保证为集群服务器配置了可用的SSH连接信息,否则只能查看当前服务器与集群组件的所属关系,无法查看服务器与组件程序的状…

IDEA 2024.3 安装激活教程(至2099年)

IntelliJ IDEA简介 IntelliJ IDEA是一款非常强大的Java集成开发环境(IDE),由JetBrains公司开发。它提供了丰富的功能和工具,帮助开发者更高效地编写、调试和部署代码。 要求 在开始之前,请确保您的计算机满足以下系统要求:操作系统:Windows、macOS或Linux 处理器:至少1 GH…

证书安装后为什么还显示证书无效

在数字化时代,网络安全和数据保护变得尤为重要。SSL/TLS证书作为保护网站和用户数据安全的重要工具,其正确安装和有效性是网站运营者必须关注的问题。然而,有时候即使证书已经安装,用户仍然会遇到“证书无效”的提示,这可能由多种原因引起。本文将探讨在证书安装后,为何用…

windows下netstat及网络查看工具的使用

1.打开cmd: win+R 输入cmd 2.查看工具相关指令可以看到相关指令能配置查看的内容 3.查看相应内容 例如我想查看当前主机UDP协议所使用的端口: netstat -ano -p UDP同理查看TCP所使用的端口: netstat -ano -p TCP二、windows 自带的网络监视工具的使用 1.打开资源管理器 快捷键…