首页 > 其他分享 >为 TVM 添加对 Paddle 量化模型的支持

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

时间:2024-11-29 18:10:32浏览次数:9  
标签:tvm Paddle TVM quantize 添加 input axis data op

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."""

    @classmethod
    def _impl_v13(cls, inputs, attr, params):
        data, scale, zp = inputs
        out_dtype = infer_type(zp).checked_type.dtype
        axis = attr.get("axis", 1)
        if len(infer_shape(data)) < 2:
            axis = 0
        return _qnn.op.quantize(data, scale, _op.cast(zp, "int32"), axis, out_dtype)

class DequantizeLinear(OnnxOpConverter):
    """Operator converter for QuantizeLinear."""
    
    @classmethod
    def _impl_v13(cls, inputs, attr, params):
        data, scale, zp = inputs
        axis = attr.get("axis", 1)
        if len(infer_shape(data)) <= 1:
            axis = 0
        return _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 = 0

    out = _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 = 0

    if len(infer_shape(data_node)) < 2:
        tvm_quantize_axis = 0

    out = _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_output


def infer_by_onnx(temp_model_path, temp_input_data):
    sess = rt.InferenceSession(temp_model_path, None)
    temp_input_name = sess.get_inputs()[0].name
    out_name = sess.get_outputs()[0].name
    temp_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_output


def 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 inference
    ctx = 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_output


log_file = "tune.json"
if __name__ == "__main__":
    np.random.seed(520)
    # create input data
    input_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 * 127
    paddle_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()
    tvm_quantize_scale = paddle_quantize_scale / 127.0

    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 = 0

    if len(infer_shape(data_node)) < 2:
        tvm_quantize_axis = 0

    out = _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 * 127
    paddle_quantize_scale = g.get_params(op.input("Scale")[0]).asnumpy()
    tvm_quantize_scale = paddle_quantize_scale / 127.0

    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 = 0

    out = _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 参考资料

标签:tvm,Paddle,TVM,quantize,添加,input,axis,data,op
From: https://www.cnblogs.com/Zheng-Bicheng/p/18577302

相关文章

  • 为 Paddle2ONNX 适配 swish 算子
    1简介在PaddlePaddle2.6中,swish算子在PaddleInference上发生了变化,删除掉了beta这个Attr,因此我们需要想办法自行适配它。2适配过程原解析swish算子的核心代码如下:voidSwishMapper::Opset7(){autoinput_info=GetInput("X");autooutput_info=GetOutp......
  • 为 Paddle2ONNX 适配 releu6 算子
    1简介在PaddlePaddle2.6中,relu6算子在PaddleInference上发生了变化,删除掉了threshold这个Attr,因此我们需要想办法自行适配它。2适配过程原解析relu6算子的核心代码如下:voidRelu6Mapper::Opset7(){autoinput_info=GetInput("X");autooutput_info=Ge......
  • 为 Paddle2ONNX 修复 elementwise_floordiv 算子计算错误的问题
    1简介elementwise_floordiv算子在int32/int64的情况下直接转换成了ONNX中的div算子,由于div算子是普通除操作,而不是整除操作,因此无法通过CI的校验。2实现过程原核心实现代码如下voidElementWiseFloordivMapper::Opset7(){autoinput_x_info=GetInput("X"......
  • 将 Paddle2ONNX 的项目构建方式从 setup.py 迁移到 pyproject.toml
    1简介在软件开发中,项目构建方式的选择对项目的可维护性,可扩展性及与其他工具的兼容性至关重要.随着Python生态系统的进步,使用pyproject.toml文件管理项目依赖和构建配置成为一种新兴趋势.相较于setup.py,pyproject.toml采用TOML语法,简化配置文件读写;提供灵......
  • 为 Paddle2ONNX 添加修改模型输入 shape 功能
    1简介原先的tools/paddle/infer_paddle_model_shape.py脚本使用的是PaddlePaddle2.5,这里将Paddle相关API升级到2.6.0。2实现过程Paddle2.6和Paddle2.5的在推理模型输入shape上的差别主要在读取/保存模型以及存放函数的位置上有区别。2.1修改读取函数原读取模型......
  • 为 Paddle2ONNX 搭建 Github Actions 自动发包机制
    1简介Paddle2ONNX此前一直使用手动编译所有版本的Python源码包再手动上传到PyPI的方式来分发发行版。很显然,这是一种极其低效的办法,本文介绍如何为Paddle2ONNX添加自动发包机制。2实现过程Paddle2ONNX的编译流程参考onnx的编译流程实现,因此在自动发包机制的设计上......
  • 为 Paddle2ONNX 添加对 Opset 18 的支持
    1简介随着ONNX标准的不断更新,保持Paddle2ONNX与最新版本的兼容性显得尤为重要。本篇文章将详细介绍如何为Paddle2ONNX项目升级其依赖的ONNXOpset版本。2添加对Opset18的支持2.1升级ONNX依赖库版本支持Opset18前我们需要修改ONNX的branch参数到最新的co......
  • 【Paddle2ONNX】为 Paddle2ONNX 适配自适应 ONNX IR Version 功能
    1简介最近在浏览Paddle2ONNX的Issues时,我发现有用户需要让Paddle2ONNX支持导出的ONNX模型根据OpsetVersion自适应IRVersion的功能。这个功能对于老的Runtime来说还是很重要的,于是我动手添加了这个功能,这里写一篇博客和大家分享下。能否指定IRrepresentation......
  • windows下把exe添加至服务进行进程监控自动重启开机自启(nginx等)
    方法一:使用自带的sccreateaaa binPath="/xxx/aaa.exe" 注意binPath的大小写并且等号和路径直接有一个空格,aaa为服务名称比如nginx创建成功后,可以通过任务管理器--服务--打开服务(最下面)--找到服务名称nginx 右键--属性--恢复--选择第一次和第二次失败的操......
  • Win10系统下添加无线打印机的方法
        在数字化办公和家庭环境中,打印机已成为不可或缺的工具之一。随着无线技术的普及,无线打印机因其便捷性和灵活性而受到越来越多用户的青睐。然而,对于许多用户来说,设置和添加无线打印机的过程可能会显得有些复杂和令人困惑。本文将为你提供一份详尽的指南,帮助你轻松地......