欢迎关注我的公众号 [极智视界],回复001获取Google编程规范

O_o>_<   o_OO_o~_~o_O

  大家好,我是极智视界,本文剖析一下 min-max 对称量化算法实现,以 Tengine 的实现为例。

   Tengine 是 OpenAILab 开源的优秀端侧深度学习推理框架,其核心主要由 C 语言实现,包裹的功能代码嵌套了 C++。量化是推理加速必不可少的优化环节,成熟的推理框架一般会把量化模块剥离出来形成独立的一套工具,如 Tengine、NCNN、昇腾、寒武纪都这么做,这主要是因为量化过程和硬件非强相关,解耦开来能干更多的事。

   min-max 和 kl 量化算法是硬件厂商适配推理引擎的基础和标配, 其中 kl 量化深受用户喜爱,如英伟达的 TensorRT 也正是采用了 kl 量化策略;而这里要介绍的 min-max 的特点是逻辑简单、效果良好,作为量化实现分享系列的开篇比较合适,这里带大家一起研究一下 Tengine 中 minx-max 量化策略的具体实现。

1、量化使用

   量化主要分为激活值(动态)量化、权值&偏置(静态)量化,而权值&偏置的量化是对精度影响比较大的,激活值的量化对整体影响较小,但也需要量化,才有可能协同达到整体满意的效果。对于一般量化来说,权值&偏置的量化会采用逐通道 perChannel 的方式,而激活值的量化一般是逐层 perLayer 的方式。解释一下为啥会这样,对于量化来说,卷积肯定是大头,对于卷积来说,若激活值量化采用逐通道方式,这和卷积核参数共享是相悖的,所以一般激活值就用逐层量化,以契合卷积参数共享。

   这里主要看一下 Tengine 量化需要的传参:

  • Input model:传入的 fp32 tmfile 模型文件;

  • Output model:生成的 int8 tmfile 模型文件;

  • Calib images:传入的激活值量化校准图片;

  • Scale file:生成的校准表文件;

  • Agorithm:量化算法,可选 MIN-MAX、KL、ACIQ、DFQ、EQ;

  • Dims:输入校准图的 shape,这里传三维 c h w,n 在代码中写死 n = 1;

  • Mean:图像预处理均值;

  • Scale:图像预处理缩放尺度;

  • BGR2RGB:通道转换;

  • Center crop:图像预处理,裁剪;

  • Letter box:图像预处理,保持横纵比的前提下对图像做 resize;

  • YOLOv5 focus:类似 yolov5 的预处理注意力机制;

  • Thread num:量化多线程设置;

2、min-max 量化

  min-max 是最简单的量化算法,主要逻辑如下:

   在 Tengine 中实现 min-max 方法的主要代码如下:

case ALGORITHM_MIN_MAX:{
   if (quant_tool.scale_file.empty()){
       quant_tool.scale_file = "table_minmax.scale";
       quant_tool.activation_quant_tool();
  }
   save_graph_i8_perchannel(quant_tool.model_file.c_str(), quant_tool.scale_file.c_str(), quant_tool.output_file, quant_tool.inplace, false);
   /* Evaluate quantitative losses */
   if (quant_tool.evaluate){
       fprintf(stderr, "[Quant Tools Info]: Step Evaluate, evaluate quantitative losses\n");
       quant_tool.assess_quant_loss(0);
  }
   break;
}

   其中最主要的量化搜索策略接口是 quant_tool.activation_quant_tool()save_graph_i8_perchannel,对于 min-max 来说这两个接口分别做了两件事:

   (1) 激活值量化,生成 table_minmax.scale

   (2) 权值&偏置量化,生成 scale_weight.txtscale_bias.txt

2.1 激活值量化

   看 Tengine 源码一定要抓住 struct graph* ir_graph,graph 这个结构体是精髓。

   激活值量化是个动态的过程,需要动态的获取每层的数据分布,这也就是为啥需要你喂一定数量校准图片的原因。

   先说一下预处理模块,这个其他量化算法是通用的:

// 将 input_tensor 和 input_data 地址绑定,而 input_tensor=>ir_graph->tensor_list。注意:这一步一定要看到,不然后续代码很难看懂
tensor_t input_tensor = get_graph_input_tensor(ir_graph, 0, 0);

if (set_tensor_shape(input_tensor, dims, 4) < 0){
   fprintf(stderr, "Set input tensor shape failed\n");
   return -1;
}

if (set_tensor_buffer(input_tensor, input_data.data(), img_size * sizeof(float)) < 0){
   fprintf(stderr, "Set input tensor buffer failed\n");
   return -1;
}

// prerun graph,做一些初始化配置
if (prerun_graph_multithread(ir_graph, this->opt) < 0){
   fprintf(stderr, "Prerun multithread graph failed.\n");
   return -1;
}

// 图像预处理,传出 input_data,这个和前面的 input_tensor & ir_graph->tensor_list[0] 输入参 绑定,修改了 input_data 即修改了 ir_graph.tensor_list,这样就能看懂
get_input_data_cv(imgs_list[nums].c_str(), input_data.data(), img_c, img_h, img_w, mean, scale, sw_RGB, center_crop, letterbox_rows, letterbox_cols, focus);

   然后 run 一下,把中间激活值记录到 ir_graph->tensor_list[i] 里:

if (run_graph(ir_graph, 1) < 0){
   fprintf(stderr, "Run graph failed\n");
   return -1;
}

   激活激活值的 min、max 值:

/* get the min/max value of activation tensor */
for (int i = 0; i < ir_graph->tensor_num; i++){
   struct tensor* act_tensor = ir_graph->tensor_list[i];
   if (act_tensor->tensor_type == TENSOR_TYPE_VAR || act_tensor->tensor_type == TENSOR_TYPE_INPUT){
       float* start_addr = (float*)act_tensor->data;
       float* end_addr = (float*)act_tensor->data + act_tensor->elem_num;
       max_activation[i] = std::max(max_activation[i], *std::max_element(start_addr, end_addr));
       min_activation[i] = std::min(min_activation[i], *std::min_element(start_addr, end_addr));}
}

  计算激活值量化尺度,对于 softmax 层 scale 默认为 1 / 127.f

/* save the calibration file with min-max algorithm */
FILE* fp_minmax = fopen("table_minmax.scale", "wb");
for (int i = 0; i < ir_graph->tensor_num; i++){
   struct tensor* t = ir_graph->tensor_list[i];
   if (t->tensor_type == TENSOR_TYPE_VAR || t->tensor_type == TENSOR_TYPE_INPUT){
       float act_scale = 1.f;
       int act_zero_point = 0;

       act_scale = std::max(std::abs(max_activation[i]), std::abs(min_activation[i])) / 127.f;

       /* the scale of softmax is always scale = 1 / 127.f */
       for (int j = 0; j < ir_graph->node_num; j++){
           struct node* noden = ir_graph->node_list[j];
           struct tensor* tensor_tmp = get_ir_graph_tensor(ir_graph, noden->output_tensors[0]);

           if (!(tensor_tmp->tensor_type == TENSOR_TYPE_INPUT || tensor_tmp->tensor_type == TENSOR_TYPE_VAR))
               continue;

           std::string tmp_op_name = get_op_name_from_type(noden->op.type);
           std::string cur_name = t->name;
           std::string tmp_name = tensor_tmp->name;

           if ((cur_name == tmp_name) && tmp_op_name == "Softmax"){
               act_scale = 1 / 127.f;
               break;}
      }

       fprintf(fp_minmax, "%s %f %d\n", ir_graph->tensor_list[i]->name, act_scale, act_zero_point);}
}

2.2 权值 & 偏置量化

  权值 & 偏置量化和激活值量化不太一样,激活值量化需要校准图片推理以获得输入数据的动态分布,而权值 & 偏置是静态的,单纯的量化过程不需执行前向推理。

2.2.1 创建 graph

   加载 tmfile,构建 graph:

struct graph* ir_graph = (struct graph*)create_graph(nullptr, "tengine", model_file);
if (nullptr == ir_graph){
fprintf(stderr, "Create graph failed.\n");
return -1;}

2.2.2 优化激活值量化 scale

   这里主要做一个 quant.inplace 的优化,这是针对非卷积算子的量化处理策略。

if (inplace == 0){
   for (int i = 0; i < ir_graph->tensor_num; i++){
      struct tensor* ir_tensor = ir_graph->tensor_list[i];
       if (ir_tensor->tensor_type == TENSOR_TYPE_VAR || ir_tensor->tensor_type == TENSOR_TYPE_INPUT){
          ir_tensor->scale = layer_scale[ir_tensor->name];
          ir_tensor->zero_point = layer_zeropoint[ir_tensor->name];}}
  }
   else{
      std::tr1::unordered_map<std::string, bool> layer_pass;
       for (int i = ir_graph->tensor_num - 1; i >= 0; i--){
          struct tensor* ir_tensor = ir_graph->tensor_list[i];
           if (ir_tensor->tensor_type == TENSOR_TYPE_VAR || ir_tensor->tensor_type == TENSOR_TYPE_INPUT){
               if (layer_pass[ir_tensor->name] == false){
                  uint32_t ir_node_idx = ir_tensor->producer;
                  struct node* t_node = ir_graph->node_list[ir_node_idx];

                  std::string op_name = get_op_name_from_type(t_node->op.type);

                  bool poolTrue = false;
                  bool reluTrue = false;
                   if (op_name == "Pooling"){
                      struct pool_param* pool_param = (struct pool_param*)t_node->op.param_mem;
                       if (pool_param->pool_method == 0)
                          poolTrue = true;
                  }
                   else if (op_name == "ReLU"){
                      struct relu_param* relu_param = (struct relu_param*)t_node->op.param_mem;
                       if (relu_param->negative_slope == 0.f)
                          reluTrue = true;
                  }
                   if (op_name == "Flatten" || op_name == "Reshape" || op_name == "Squeeze" || op_name == "Clip" || op_name == "Slice" || poolTrue || reluTrue){
                      struct tensor* t_in_tensor = ir_graph->tensor_list[t_node->input_tensors[0]];
                       if (layer_scale[ir_tensor->name] != 0){
                          ir_tensor->scale = layer_scale[ir_tensor->name];
                          ir_tensor->zero_point = layer_zeropoint[ir_tensor->name];

                           if (t_in_tensor->tensor_type == TENSOR_TYPE_VAR || t_in_tensor->tensor_type == TENSOR_TYPE_INPUT){
                              recursion_pass_through(ir_graph, ir_tensor->name, t_in_tensor, layer_used, layer_scale, layer_zeropoint, layer_pass);}}
                  }
                   else{
                      ir_tensor->scale = layer_scale[ir_tensor->name];
                      ir_tensor->zero_point = layer_zeropoint[ir_tensor->name];
                  }
                  layer_pass[ir_tensor->name] = true;}}}
}

2.2.3 权值 & 偏置量化

  量化的整个过程和激活值量化类似,即先搜索 min、max 值,后做截断缩放处理。这里不仅需要计算 scale,而且还要做截断缩放处理的原因是需要生成 int8 tmfile 量化模型文件。这里还有一点需要注意的是权值量化精度为 int8,偏置量化精度为 int32,因为权值做完矩阵乘后值很有可能就会溢出 int8,所以需要权值矩阵乘后的值用 int32 存储,然后与 int32 的偏置做加法。

   除了以上这些,和激活值量化还有个区别是,激活值量化是 perLayer 的,而权值 & 偏置量化是 perChannel 的。

   权值 int8 量化:

/* quantize the weight data from fp32 to int8 */
if (op_name == "Convolution" || op_name == "FullyConnected" || op_name == "Deconvolution"){
   struct tensor* weight_tensor = ir_graph->tensor_list[noden->input_tensors[1]];

   int channel_num = weight_tensor->dims[0];
   int cstep = int(weight_tensor->elem_num / channel_num);
   float* weight_data = (float*)weight_tensor->data;
   int8_t* i8_weight_data = (int8_t*)sys_malloc(weight_tensor->elem_num * sizeof(int8_t));

   float* weight_scale_list = (float*)sys_malloc(channel_num * sizeof(float));
   int* weight_zp_list = (int*)sys_malloc(channel_num * sizeof(int));

   fprintf(fp_weight, "%s ", weight_tensor->name);
   /* calculate the quant scale value of weight perchannel, scale = abs(min, max) / 127 */
   if (internal){
       // TODO
       for (int ch = 0; ch < channel_num; ch++){
           weight_scale_list[ch] = weight_tensor->scale_list[ch];
           weight_zp_list[ch] = 0;}
  }
   else{
       for (int ch = 0; ch < channel_num; ch++){
           float* weight_data_ch_start = weight_data + ch * cstep;
           float* weight_data_ch_end = weight_data + (ch + 1) * cstep;
           float weight_max = *std::max_element(weight_data_ch_start, weight_data_ch_end);
           float weight_min = *std::min_element(weight_data_ch_start, weight_data_ch_end);

           weight_scale_list[ch] = std::max(std::abs(weight_max), std::abs(weight_min)) / 127.f;
           weight_zp_list[ch] = 0;
           fprintf(fp_weight, "%8.8f ", weight_scale_list[ch]);
      }
       fprintf(fp_weight, "\n");
  }

   /* quantize the value of weight from Float32 to Int8, value_i8 = (value_fp32 / scale).round().clip(-127, 127) */
   for (int ch = 0; ch < channel_num; ch++){
       for (int j = 0; j < cstep; j++){
           if (weight_data[ch * cstep + j] == 0 || weight_scale_list[ch] == 0)
               i8_weight_data[ch * cstep + j] = 0;
           else{
               float int8_data = round(weight_data[ch * cstep + j] / weight_scale_list[ch]);
               int8_data = int8_data > 127.f ? 127.f : int8_data;
               int8_data = int8_data < -127.f ? -127.f : int8_data;
               i8_weight_data[ch * cstep + j] = int8_t(int8_data);}}
  }

   weight_tensor->scale_list = weight_scale_list;
   weight_tensor->zp_list = weight_zp_list;
   weight_tensor->data_type = TENGINE_DT_INT8;
   weight_tensor->elem_size = sizeof(int8_t); // int8, signed char
   weight_tensor->data = i8_weight_data;
   weight_tensor->quant_param_num = channel_num;
}

   偏置 int32 量化:

/* quantize the weight data from fp32 to int32 */
if (noden->input_num > 2){
struct tensor* input_tensor = ir_graph->tensor_list[noden->input_tensors[0]];
struct tensor* bias_tensor = ir_graph->tensor_list[noden->input_tensors[2]];

float* bias_scale_list = (float*)sys_malloc(bias_tensor->dims[0] * sizeof(float));
int* bias_zp_list = (int*)sys_malloc(bias_tensor->dims[0] * sizeof(int32_t));

float* bias_data = (float*)bias_tensor->data;
int* int32_bias_data = (int*)sys_malloc(bias_tensor->elem_num * sizeof(int32_t));

int bstep = int(bias_tensor->elem_num / channel_num);

fprintf(fp_bias, "%s ", bias_tensor->name);

/* calculate the quant scale value of bias perchannel, scale = scale_weight * scale_in */
for (int ch = 0; ch < channel_num; ch++){
bias_scale_list[ch] = weight_scale_list[ch] * input_tensor->scale;
bias_zp_list[ch] = 0;

fprintf(fp_bias, "%8.8f ", bias_scale_list[ch]);
}
fprintf(fp_bias, "\n");

/* quantize the value of bias from Float32 to Int32, value_i32 = (value_fp32 / scale).round() */
for (int ch = 0; ch < channel_num; ch++){
for (int bi = 0; bi < bstep; bi++){
if (bias_data[ch * bstep + bi] == 0 || bias_scale_list[ch] == 0)
int32_bias_data[ch * bstep + bi] = 0;
else
int32_bias_data[ch * bstep + bi] = int(round(bias_data[ch * bstep + bi] / bias_scale_list[ch]));}
}

bias_tensor->scale_list = bias_scale_list;
bias_tensor->zp_list = bias_zp_list;
bias_tensor->data_type = TENGINE_DT_INT32;
bias_tensor->elem_size = sizeof(int32_t); // int32, signed int
bias_tensor->data = int32_bias_data;
bias_tensor->quant_param_num = channel_num;
}

  到这里权值 & 偏置的量化就介绍的差不多咯。

   以上详细介绍了 min-max 量化算法的实现,主要以 Tengine 为例进行代码说明,希望我的分享能对你的学习有一点帮助。

【公众号传送】

【模型推理】量化实现分享一:详解 min-max 对称量化算法实现

【模型推理】量化实现分享一:详解 min-max 对称量化算法实现的更多相关文章

  1. 数据挖掘模型中的IV和WOE详解

    IV: 某个特征中 某个小分组的 响应比例与未响应比例之差 乘以 响应比例与未响应比例的比值取对数 数据挖掘模型中的IV和WOE详解 http://blog.csdn.net/kevin7658/ar ...

  2. [转]Vue项目全局配置微信分享思路详解

    这篇文章给大家介绍了vue项目全局配置微信分享思路讲解,使用vue作为框架,使用vux作为ui组件库,具体内容详情大家跟随脚本之家小编一起学习吧 这个项目为移动端项目,主要用于接入公众号服务.项目采用 ...

  3. 详解 volatile关键字 与 CAS算法

    (请观看本人博文 -- <详解 多线程>) 目录 内存可见性问题 volatile关键字 CAS算法: 扩展 -- 乐观锁 与 悲观锁: 悲观锁: 乐观锁: 在讲解本篇博文的知识点之前,本 ...

  4. 转载:数据挖掘模型中的IV和WOE详解

    1.IV的用途 IV的全称是Information Value,中文意思是信息价值,或者信息量. 我们在用逻辑回归.决策树等模型方法构建分类模型时,经常需要对自变量进行筛选.比如我们有200个候选自变 ...

  5. 评分卡模型中的IV和WOE详解

    1.IV的用途   IV的全称是Information Value,中文意思是信息价值,或者信息量. 我们在用逻辑回归.决策树等模型方法构建分类模型时,经常需要对自变量进行筛选.比如我们有200个候选 ...

  6. 模型 - 视图 - 控制器(MVC)详解

    模型视图控制器(MVC)一个相当实用且十分流行的设计模式.作为一位称职码农,你不可能没听说过吧. 不幸的是它难以让人理解. 在本文中,我将给出我认为是MVC的最简单的解释,以及为什么你应该使用它. 什 ...

  7. j2ee model1模型完成分页逻辑的实现 详解!

    在显示用户全部信息的页面,在显示全部数据的时候,长长的滚动条,像是没有边界的天空一样, 让用户查看数据很不方便. 于是, 我们要把这些数据分页显示, 就像office的word一样,每页显示一定数量的 ...

  8. 微信JSSDK分享功能详解

    本文以微信分享到朋友圈,分享给微信好友为例为参考,进行调用测试,想添加其他的功能,自行查看开发人员文档即可 工欲善其事,必先利其器,好好利用下边的帮助工具,都是腾讯给开发人员的工具 1.微信开发者说明 ...

  9. IO模型之AIO代码及其实践详解

    一.AIO简介 AIO是java中IO模型的一种,作为NIO的改进和增强随JDK1.7版本更新被集成在JDK的nio包中,因此AIO也被称作是NIO2.0.区别于传统的BIO(Blocking IO, ...

  10. IO模型之NIO代码及其实践详解

    一.简介 NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的( JDK 1.4中的java.nio.*包中引入新的Java I/O库).但现在都称之为Non-bl ...

随机推荐

  1. 使用pmml实现跨平台部署机器学习模型

    一.概述   对于由Python训练的机器学习模型,通常有pickle和pmml两种部署方式,pickle方式用于在python环境中的部署,pmml方式用于跨平台(如Java环境)的部署,本文叙述的 ...

  2. 什么是JIT?

    目录 什么是JIT? 为什么HotSpot虚拟机要使用解释器与编译器并存的架构? 编译的时间开销 什么是JIT? 1.动态编译(dynamic compilation)指的是"在运行时进行编 ...

  3. [ARC098B] Xor Sum 2

    关于异或运算和代数和运算有很不错的性质: \(xor_{i = 1} ^ {n}a_i \leq \sum_{i = 1} ^ n a_i\) 所以我们考虑一段区间按题目来说是合法的,即 \(xor_ ...

  4. LOJ #6207 - 米缇(杜教筛+拉格朗日插值)

    LOJ 题面传送门 首先将 \(\sigma_k(ij)\) 展开: \[\sigma_k(ij)=\sum\limits_{x\mid i}\sum\limits_{y\mid j}[x\perp ...

  5. 洛谷 P4516 [JSOI2018]潜入行动

    题面传送门 一眼树形 \(dp\) 本题有 \(2\) 大难点. 难点之一是状态的设计,这里需要四维状态,\(dp[i][j][0/1][0/1]\) 表示在以 \(i\) 为根的子树内放了 \(j\ ...

  6. 洛谷 P7520 - [省选联考 2021 A 卷] 支配(支配树)

    洛谷题面传送门 真·支配树不 sb 的题. 首先题面已经疯狂暗示咱们建出支配树对吧,那咱就老老实实建呗.由于这题数据范围允许 \(n^2\)​ 算法通过,因此可以考虑 \(\mathcal O(n^2 ...

  7. Codeforces 739C - Alyona and towers(线段树)

    Codeforces 题目传送门 & 洛谷题目传送门 可能有人会问我为什么为这道 *2500 的 D1C 写题解,我觉得大概是想要在写题解数量上 dd ycx 吧,因为 ycx 到目前为止写了 ...

  8. 【GS文献】基因组选择在植物分子育种应用的最新综述(2020)

    目录 1. 简介 2. BLUP类模型 3. Bayesian类模型 4. 机器学习 5. GWAS辅助的GS 6. 杂交育种 7. 多性状 8. 长期选择 9. 预测准确性评估 10. GS到植物育 ...

  9. Hosts文件详解

    一.缘由 关于我们工作室项目配置过程中,有一个重要但却容易被忽略的环节 - hosts文件的修改. 之前配置hosts文件的时候,只知道那么做就可以了,但并不知道其中的原因,由于开发任务的急迫,也就未 ...

  10. .Net调用Java的实现方法

    一. IKVM 1.1下载配置IKVM 1.1.1. 下载路径 http://www.ikvm.net/index.html 1.1.2. 设置路径 解压ikvm-0.42.0.3.zip,并将%IK ...