onnxruntime模型部署(番外2)模型量化之tensor量化

量化不是一个特别复杂的新技术,只是名字听着很唬人,其实本质就是将连续信号转化为离散信号的一种表现,如果把weight看作是一种连续信号,为了便于存储和计算,在减小一定精度的情况下进行离散,这既是量化的本质。

what and why

模型量化是指将模型的浮点数参数量化为定点整型,将浮点数运算都化为定点运算。
模型量化可以做到推理加速,与神经网络剪枝相比,泛用性更强。

数学原理

量化就是把浮点数化为整型,最简单的例子就是我们在将图像输入到模型中之前需要做归一化,将0到255的像素值归一化为0到1之间的浮点数。这个过程就可以理解为是反量化,模型输出的浮点数需要换算成像素值,这个过程就是量化。
根据换算方法的不同,可以分为多种量化方式。
均匀量化和非均匀量化
均匀量化和非均匀量化
对称量化和非对称量化
对称量化和非对称量化

均匀量化uniform quantization

三个参数:尺度因子scale,0点便宜offset【有些教程称为zero-point】,位宽bit-width【位宽就是量化后数据占用的bit大小,一般都是INT8,也就是8bit】
X i n t = C l i p ( R o u n d ( X s c a l e + o f f s e t ) ) X q = ( X i n t − o f f s e t ) ∗ s c a l e E r r o r = ∣ ∣ X − X q ∣ ∣ X \begin{align} X_{int} & = Clip(Round(\frac{X}{scale}+offset)) \\ X_q & = (X_{int}-offset)*scale \\ Error & = \frac{||X-X_q||}{X} \end{align} XintXqError=Clip(Round(scaleX+offset))=(Xintoffset)scale=X∣∣XXq∣∣
Round函数是指四舍五入函数,clip函数是指将所有数据限制到一个范围内,比如量化为int8大小的整型,那么该函数的意义就是将所有输入限制在0到255之间。
offset是指偏移量,scale是指缩放尺度,当offset固定为0时,称为均匀对称量化。
可以有效量化的范围是 ( − s c a l e ∗ o f f s e t , s c a l e ∗ 2 b − 1 ) (-scale*offset,scale*2^b-1) (scaleoffset,scale2b1),超过这个范围的数字都会被压缩为极限值,小于min的变成min,大于max的压缩为max,会导致误差增大。可以通过改变scale的大小来控制有效量化范围,但是scale太大又会导致量化范围内“分辨率”的稀疏,本来可以区分3.1和3.9的,scale太大之后统一量化为了3,也会导致误差增大。
所以scale的选择是一个超参数,需要平衡两种误差。
对于offset=0的对称量化,量化不会改变原参数的符号,所以需要考虑量化后的整型是否需要有符号
在这里插入图片描述
有符号,无符号,;非对称,三种量化方式量化后的范围。

scale等于2的幂次的时候称为2次幂量化,这种情况下计算时只需要进行位移即可,更加高效,但是限制scale的取值也相应会影响精度。

量化实现

量化粒度

得知了量化的数学原理,我们需要为模型的weight和activation进行分别量化。衡量一个模型的量化水平的指标称为量化粒度(quantization granularity),最常见的量化粒度就是为weight和activation各定义一个量化器,这样实现起来最简单,称为per-tesnor quantization。如果想要提高精度,可以选择使用更细的粒度进行量化,例如为不同的output channel的weight分别定义一个量化器,单独设置参数。这种称为per-hannel quantization。更精细的量化粒度,不做讨论。

为什么per_channel比per_tensor粒度要细?
因为per_tensor是指整个tensor中的所有公用一个量化器,而per_channel是指每个channel一个量化器。例如一张图片存储为一个tensor,如果用per_tesnor,那就只有一个量化器,如果用per_channel,那就有3个量化器。

量化模拟

如何判断一个量化过的模型在推理设备上运行的效果,最简单的判断就是上机实战,但是有时候条件有限没法上机实战,可以考虑在训练设备上进行量化模拟。
在这里插入图片描述
左边是实际推理设备上的模型流程,右边是量化模拟中的模型推理流程,量化模拟就是在原本模型的基础上为weight和output添加一个quantizer,其实背后的计算还是用的浮点计算,即:用浮点计算来模拟量化后的定点运算。这种方法相较于在推理设备实战以及在训练设备上搞一套runtime要简单快捷的多,这种方案的运算是依靠pytorch这种训练框架实现的,需要自己实现的部分就只有quantizer,而且要注意,这里的quantizer的输入输出也还都是浮点数,添加quantizer只不过是为了模拟量化这个过程,以查看量化对精度的影响。
这种边量化查看效果边进行训练的方式就是PTQ动态量化,所有数据都还是浮点数,但是参数更新会受到量化的影响,进而避免了训练好模型再量化如果量化不合适会较大的精度损失的情况。

实现代码

import torch
from torch import nn
import torch.nn.functional as F

# Create a model
class Model_demo(nn.Module):
    def __init__(self):
        super(Model_demo, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

# An instance of your model.
model = Model_demo()
quantized_model = torch.quantization.quantize_dynamic(model=model, qconfig_spec={torch.nn.Linear}, dtype=torch.qint8)
# Pytorch中动态量化不会对CNN的参数进行量化,可能是因为觉得CNN的参数较少,动态量化意义不大。

print(quantized_model.fc1.weight())
print(model.fc1.weight)
# 可以看到量化后的参数是有一些误差的

量化技术原理本身不难,难的是五花八门的模型结构,互不相同的框架,这些因素导致量化学起来很难,因为经常是量化完发现加速是明显了,精度损失也很明显。
至于非均匀量化以及算子融合等知识,后面再出blog讲解。
点赞+关注=催更

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这里是一个使用C++和OnnxRuntime部署Onnx模型的完整工程代码,供你参考: ```c++ #include <iostream> #include <vector> #include <string> #include <chrono> #include <onnxruntime_cxx_api.h> // 定义模型输入和输出的名称和形状 const char* INPUT_NAME = "input"; const char* OUTPUT_NAME = "output"; const std::vector<int64_t> INPUT_SHAPE = { 1, 3, 224, 224 }; const std::vector<int64_t> OUTPUT_SHAPE = { 1, 1000 }; int main(int argc, char* argv[]) { if (argc != 2) { std::cout << "Usage: " << argv[0] << " <model_path>" << std::endl; return 1; } // 创建Ort::Env和Ort::SessionOptions对象 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test"); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(1); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); try { // 创建Ort::Session对象 Ort::Session session(env, argv[1], session_options); // 获取模型的输入和输出信息 Ort::AllocatorWithDefaultOptions allocator; size_t num_input_nodes = session.GetInputCount(); size_t num_output_nodes = session.GetOutputCount(); std::cout << "Number of input nodes: " << num_input_nodes << std::endl; std::cout << "Number of output nodes: " << num_output_nodes << std::endl; // 创建模型输入数据 std::vector<float> input_data(INPUT_SHAPE[1] * INPUT_SHAPE[2] * INPUT_SHAPE[3], 1.0f); // 创建Ort::Value对象,用于存储输入和输出数据 Ort::Value input_tensor = Ort::Value::CreateTensor<float>(allocator, input_data.data(), input_data.size(), INPUT_SHAPE.data(), INPUT_SHAPE.size()); Ort::Value output_tensor = Ort::Value::CreateTensor<float>(allocator, OUTPUT_SHAPE.data(), OUTPUT_SHAPE.size()); // 运行模型 auto start = std::chrono::high_resolution_clock::now(); session.Run(Ort::RunOptions{ nullptr }, { INPUT_NAME }, { &input_tensor }, 1, { OUTPUT_NAME }, { &output_tensor }, 1); auto end = std::chrono::high_resolution_clock::now(); std::cout << "Inference time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms" << std::endl; // 获取输出数据 std::vector<float> output_data(OUTPUT_SHAPE[1]); output_tensor.CopyTo<float>(output_data.data(), OUTPUT_SHAPE[1]); // 输出前5个结果 std::cout << "Top 5 results:" << std::endl; for (int i = 0; i < 5; i++) { int max_index = std::distance(output_data.begin(), std::max_element(output_data.begin(), output_data.end())); std::cout << max_index << ": " << output_data[max_index] << std::endl; output_data[max_index] = -1.0f; } } catch (const std::exception& ex) { std::cerr << ex.what() << std::endl; return 1; } return 0; } ``` 在使用该代码之前,你需要先安装OnnxRuntime库,并在代码中添加库的头文件和链接器选项。该代码读取命令行中的模型路径,并使用OnnxRuntime加载模型、运行推理并输出结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值