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

O_o>_<o_OO_o~_~o_O

  大家好,我是极智视界,本文剖析一下格灵深瞳 EQ (Easy Quant) 量化算法实现,以 Tengine 的实现为例。

  本文是模型量化实现分享的第五篇,前面已有四篇,有兴趣的同学可以查阅:

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

  (2) 《【模型推理】量化实现分享二:详解 KL 对称量化算法实现》

  (3) 《【模型推理】量化实现分享三:详解 ACIQ 对称量化算法实现》

  (4)《【模型推理】量化实现分享四:Data-Free Quantization 香不香?详解高通 DFQ 量化算法实现》

  格灵深瞳 EQ 量化算法在论文《EasyQuant: Post-traning Quantization via Scale Optimization》中提出,EQ 量化算法有几个创新点:

  (1) 在 KLD 获得初始值后再用贪婪方式以最大化真实值&量化值间余弦相似度为优化目标,交替搜索权值 Scale 和 激活值 Scale;

  (2) 结合 Arm NEON 指令集优化,在 int7 量化时的性能表现更优。

  同样,这里不止会介绍原理,也会介绍实现。

  下面开始。

文章目录

  • 1、EQ 量化原理
  • 2、EQ 量化实现

1、EQ 量化原理

  先来看一下实验数据:

  这是 ImageNet2012 上分类模型的量化精度数据,对标 Nvidia TRT,可以看到 EQ 不管是在 int8 还是 int7 量化时表现均占优。再看一组检测模型的数据:

  以上是 SSD 目标检测模型在 VOC2007 上的量化精度 (mAP) 对比数据,同样可以看到 EQ 在 int8 & int7 量化效果占优。 以上两组数据还可以看出,EQ int7 接近 TRT int8 精度,所以有了下面这组性能对比数据 (总觉得少了组 EQ int8 的数据):

  论文里还有其他的一些实验数据,如对比 QAT 量化感知训练的实验数据等,这里就不贴了,有兴趣的同学可以自行查阅。总结来说就是 EQ 算法在精度和性能上都是 yyds。

  来讲原理。

  首先量化的过程可以表示为:

  其中 X 为输入 tensor,S 为缩放因子,Clip 为裁剪,Round 为取整,所以量化的整个过程分为三步:

  (1) 裁剪;

  (2) 缩放;

  (3) 取整;

  这样的说法其实比较宏观,再细一点,可以把 S 分为激活值 S 和权值 S,那么第 l 层的量化过程就可以表示为:

  以上量化式是对于第 l 层来说的,再结合拓展到第 l + 1 层,会有 dequant / requant 的过程,整个逻辑就是这样的:

  量化过程很清晰直观,不再赘述。然后就到了 EQ 算法的精髓。

  EQ 是用余弦相似度来衡量真实值和量化值间的相似度,优化的目标是最大化余弦相似度,可以用如下式表达:

  EQ 的搜索策略是这样的:初始值 Sa 和 Sw 由 KLD 获得,在这个基础上搜索优化 Sa 和 Sw。搜索空间为 [αS, βS] 线性划分 n 个,先固定 Sa 优化 Sw,再固定 Sw 优化 Sa,两者交替进行直至设计的余弦相似度收敛或超时。对于整网来说,EQ 搜索策略如下 (这也是 EQ 量化的核心逻辑):

  到这里原理已讲完,多说一句,前面可以看到论文实验结果对标 TRT,看了 EQ 的算法逻辑,应该很清楚,从精度上来说,EQ 根本不太可能会比 TRT 差,因为 TRT 是基于 KLD 量化的,而 EQ 是在 KLD 量化得出的 Scale 上再进一步搜索优化,结果怎么可能会弱于初始值 KLD 呢,所以精度数据那么好看也不用吃惊,这是理所当然的结果。另外一方面,,由于 EQ 是 KLD 后的二次优化,所以 EQ 的整个量化过程开销会比 KLD 量化大得多。

  到这里,主要说了精度为什么能涨点,还没说为什么能提速。这里我觉得论文是打了个擦边球,可以看到前面的性能数据对比对象是 TRT int8 vs EQ int7,这公平吗?直观来说确实不公平,不过再结合 EQ int7 精度接近 / 追平 TRT int8,再来看这个性能数据心里会平衡一些。这里也诞生了论文的一个创新点:既然前提我 int7 能做到 baseline int8 差不多的精度,那我就重心优化 int7 推理 (除非你 int7 精度比我 int7 高,不然我就用 int7 比你 int8,气死你~)。

  然后说一下为什么 int7 能提速。来看下图:

  在用 int8 量化时,中间数据一般用 int32 存储,直到 ARM V8.2-A 之前都没有能将两个 8bit 数据相乘的结果放到 32bit 寄存器的指令,替代的方法是用 SMLAL 将两个 8bit 数据相乘后存入 16bit 寄存器,再用 SADALP 合并两个 16bit 到 32bit 寄存器。对于一个常规卷积来说,用 int8 和 int7 的 NEON 乘法指令的次数差别如下:

  • int8 推理:一次能算 ((215-1)/(27-1)^2) = 2 次 SMLAL;
  • int7 推理:一次能算 ((215-1)/(26-1)^2) = 8 次 SMLAL;

  可以看到 int7 推理相比 int8 推理具有更加高效的乘法指令优化,这也正是 int7 推理效率更高的主要原因。

2、EQ 量化实现

  来看 EQ 量化的 tengine 实现。

  EQ 实现的主要代码如下:

case ALGORITHM_MM_EQ:
{if (quant_tool.scale_file.empty()){quant_tool.scale_file = "table_minmax.scale";quant_tool.activation_quant_tool();}/* Evaluate quantitative losses */if (quant_tool.evaluate){fprintf(stderr, "[Quant Tools Info]: Step Evaluate, evaluate quantitative losses\n");quant_tool.assess_quant_loss(0);}/* Enable EQ search */fprintf(stderr, "[Quant Tools Info]: Step Search, enable EQ search\n");quant_tool.quant_search();quant_tool.model_file = "save_i8_eq.tmfile";save_graph_i8_perchannel(quant_tool.model_file.c_str(), quant_tool.scale_file.c_str(), quant_tool.output_file, quant_tool.inplace, true);break;
}

  这里的主逻辑和论文里的稍微有一些出入,论文里的 EQ 搜索初始值是 KLD 得出的,而这里是 MIN-MAX 得出的。在得到 MIN-MAX 出来的 Scale 初始值后进行 EQ 的搜索,主要是这个接口:

quant_tool.quant_search();

  在这个接口里主要有两个逻辑:

  (1) 当层类型为 CONV 或 FC 时:先进行权值 Scale 的搜索 requant,再进行偏置 requant,再进行激活值 requant,最后计算余弦相似度;

  (2) 当层类型非 CONV 及 FC 时:不进行搜索也不进行权值&偏置 requant,直接进行激活值 requant,并计算余弦相似度;

  可以看到和论文里的逻辑还是不太一样,这里只进行了权值 Scale 的搜索,搜索空间为 [1.3/200 Scale, 1.3/200x201 Scale],迭代间隔 1.3/20。下面来看代码:

if (this->op_name == OP_CONV || this->op_name == OP_FC)    /// 当层类型为CONV或FC时
{this->gen_weight_scale(this->weight_tensor_fake_quant, this->weight_data_fake_quant, this->weight_tensor_fake_quant->elem_num, 8, 1, weight_tensor_fake_quant->dims[0]);this->gen_weight_scale(this->weight_tensor_fp32, this->weight_data_fp32, this->weight_tensor_fp32->elem_num, 8, 1, weight_tensor_fp32->dims[0]);std::vector<double> cosin_save(weight_tensor_fake_quant->dims[0], -1);std::vector<float> zoom_save(weight_tensor_fake_quant->dims[0], -1);for (int snum = 0; snum < 201; snum = snum + 20){     /// 搜索空间float zoom = 1.3 / 200 * (snum + 1);/* step 0 weight requant */if (snum < 200)this->weight_requant_search(weight_tensor_fake_quant, weight_data_fake_quant, weight_tensor_fake_quant->elem_num, 8, 1, weight_tensor_fake_quant->dims[0], zoom);else{this->weight_requant_search(weight_tensor_fake_quant, weight_data_fake_quant, weight_tensor_fake_quant->elem_num, 8, 1, weight_tensor_fake_quant->dims[0], zoom_save.data());float* buf = (float*)sys_malloc(weight_tensor_fake_quant->dims[0] * 4);memcpy(buf, zoom_save.data(), weight_tensor_fake_quant->dims[0] * 4);for (int bi = 0; bi < weight_tensor_fake_quant->dims[0]; bi++){buf[bi] *= weight_tensor_fp32->scale_list[bi];}weight_tensor_fp32->scale_list = buf;weight_tensor_fp32->quant_param_num = weight_tensor_fp32->dims[0];}if (interleave_size_fake != 0){int M = weight_tensor_fake_quant->dims[0];int K = weight_tensor_fake_quant->elem_num / weight_tensor_fake_quant->dims[0];this->conv_hcl_interleave_pack4_fp32(M, K, weight_data_fake_quant, interleave_buffer_fake_quant);}/* step 1 bias requant */if (node_fake_quant->ir_node->input_num > 2){struct tensor* input_tensor_fake_quant = graphn_fake_quant->tensor_list[node_fake_quant->ir_node->input_tensors[0]];struct tensor* bias_tensor_fake_quant = graphn_fake_quant->tensor_list[node_fake_quant->ir_node->input_tensors[2]];struct tensor* bias_tensor_fp32 = graphn_fp32->tensor_list[node_fp32->ir_node->input_tensors[2]];bias_size = bias_tensor_fp32->elem_num * bias_tensor_fp32->elem_size;bias_data_fp32 = (float*)bias_tensor_fp32->data;bias_data_fake_quant = (float*)bias_tensor_fake_quant->data;this->bias_requant(input_tensor_fake_quant, weight_tensor_fake_quant, bias_tensor_fake_quant, bias_data_fake_quant, bias_tensor_fake_quant->elem_num, bias_tensor_fake_quant->dims[0]);}/* step 2 activation requant */for (int imgi = 0; imgi < this->max_search_img_num; imgi++){this->set_node_input_output_tensor(i, imgi, snum);/* FP32 op run */if (snum == 0){node_ops_fp32->run(node_ops_fp32, node_fp32, exec_graph_fp32);this->execidx_elemnum[i] = output_tensor_fp32->elem_num;   //exec idx --> output elem numthis->execidx_elemsize[i] = output_tensor_fp32->elem_size; //exec idx --> output elem sizethis->execidx_nodename[i] = output_tensor_fp32->name;}/* fake quant op run */node_ops_fake_quant->run(node_ops_fake_quant, node_fake_quant, exec_graph_fake_quant);this->activation_requant(out_imgs_fake_quant[imgi].data(), output_tensor_fake_quant->elem_num, 8, 1, output_tensor_fake_quant->scale, output_tensor_fake_quant->zero_point);} output_channel = output_tensor_fp32->dims[1];/* step 3 cal cosin_similarity */if (this->op_name == OP_CONV || (this->op_name == OP_FC && this->max_search_img_num > 1))this->cosin_similarity(this->cosin, this->out_imgs_fp32, this->out_imgs_fake_quant, this->max_search_img_num, this->execidx_elemnum[i], output_channel);elsethis->cosin_similarity(this->cosin, this->out_imgs_fp32, this->out_imgs_fake_quant, this->max_search_img_num, this->execidx_elemnum[i], 1);for (int cosi = 0; cosi < output_channel; cosi++){if (cosin[cosi] > cosin_save[cosi]){cosin_save[cosi] = cosin[cosi];zoom_save[cosi] = zoom;}}if (snum == 200){if (this->op_name == OP_CONV || (this->op_name == OP_FC && this->max_search_img_num > 1))this->print_cosin(this->cosin.data(), i, output_channel);elsethis->print_cosin(this->cosin.data(), i, 1);}if (op_name == OP_CONV || op_name == OP_FC){memcpy(weight_data_fake_quant, weight_data_fp32, weight_size);memcpy(interleave_buffer_fake_quant, interleave_buffer_fp32, interleave_size_fake);if (node_fake_quant->ir_node->input_num > 2){memcpy(bias_data_fake_quant, bias_data_fp32, bias_size);}}}
}
else{/* per image run */for (int imgi = 0; imgi < this->max_search_img_num; imgi++){this->set_node_input_output_tensor(i, imgi, 0);node_ops_fp32->run(node_ops_fp32, node_fp32, exec_graph_fp32);/* step 0 activation requant */node_ops_fake_quant->run(node_ops_fake_quant, node_fake_quant, exec_graph_fake_quant);this->activation_requant(out_imgs_fake_quant[imgi].data(), output_tensor_fake_quant->elem_num, 8, 1, output_tensor_fake_quant->scale, output_tensor_fake_quant->zero_point);this->execidx_elemnum[i] = output_tensor_fp32->elem_num;   //exec idx --> output elem numthis->execidx_elemsize[i] = output_tensor_fp32->elem_size; //exec idx --> output elem sizethis->execidx_nodename[i] = output_tensor_fp32->name;}/* step 1 cal cosin_similarity */this->cosin_similarity(this->cosin, out_imgs_fp32, out_imgs_fake_quant, this->max_search_img_num, this->execidx_elemnum[i], 1);this->print_cosin(this->cosin.data(), i, 1);this->execidx_loss[i] = cosin;
}

  这样就完成了 EQ 算法的搜索过程。

  以上详细分享了格灵深瞳 EQ 算法的原理和实现,希望我的分享能对你的学习有一点帮助。

 【公众号传送】

《【模型推理】量化实现分享五:详解格灵深瞳 EQ 量化算法实现》

扫描下方二维码即可关注我的微信公众号【极智视界】,获取更多AI经验分享,让我们用极致+极客的心态来迎接AI !

极智AI | 量化实现分享五:详解格灵深瞳 EQ 量化算法实现相关推荐

  1. FRVT赛程全纪录:格灵深瞳全球排名前五

    作者 | 张德兵,格灵深瞳首席科学家&算法部负责人 来源 | 转载自知乎张德兵 最近两个月,格灵深瞳首席科学家&算法部负责人张德兵与算法团队参加了全球人脸识别算法测试(FRVT.Fac ...

  2. 格灵深瞳CTO邓亚峰:AI学习的三种路线

    11 月 23 ~ 24 日,GIAC 全球互联网架构大会将于上海举行.GIAC 是高可用架构技术社区推出的面向架构师.技术负责人及高端技术从业人员的技术架构大会.今年的 GIAC 已经有英特尔.腾讯 ...

  3. 格灵深瞳发起 AI · 爱 算法 在线编程挑战赛

    点击我爱计算机视觉标星,更快获取CVML新技术 DEEP SEA FISH 编 程 高 手 集 结 令 一 决 高 下 你热爱编程,喜欢AI么?如果你的答案是Yes,这里有一场绝不可错过的在线编程挑战 ...

  4. 格灵深瞳,天才AI公司跌入谷底之后 | 小败局

    https://www.toutiao.com/a6714998702441759246/ 文 | 苏建勋 编辑 | 杨轩 一年前入职格灵深瞳时,陈乐就觉得有些不太对劲,"没有加班费,项目提 ...

  5. 格灵深瞳将登陆科创板募资18亿,AI天才未来能否走出巨亏困局?

    数据智能产业创新服务媒体 --聚焦数智 · 改变商业 又一家顶着巨亏压力的AI公司上市了. 3月4日,上交所发布公告,格灵深瞳首次公开发行股票并在科创板上市.本次发行定价39.49元/股,发行股数不超 ...

  6. 格灵深瞳:人脸识别工业级大规模人脸识别实践探讨 | 百万人学AI

    作者 | 张德兵 编辑 | 阿司匹林 出品 | 人工智能头条(公众号ID:AI_Thinker) 人脸识别已经成为成为计算机视觉领域最热门的应用之一,很多刚入门的 AI 新手都或多或少接触过人脸识别的 ...

  7. 极智AI | 谈谈几种量化策略:MinMax、KLD、ADMM、EQ

      欢迎关注我的公众号 [极智视界],获取我的更多笔记分享   O_o   >_<   o_O   O_o   ~_~   o_O   本文主要聊一下深度学习模型量化相关策略.   模型小 ...

  8. 极智AI | Attention 中 torch.chunk 的 TensorRT 实现

      欢迎关注我的公众号 [极智视界],获取我的更多笔记分享   大家好,我是极智视界,本文介绍一下 Attention 中 torch.chunk 的 TensorRT 实现.   Attention ...

  9. 极智AI | 昇腾 CANN ATC 模型转换

      欢迎关注我的公众号 [极智视界],获取我的更多笔记分享   大家好,我是极智视界,本文介绍一下 昇腾 CANN ATC 模型转换.   昇腾 CANN 的全称是 Compute Architect ...

最新文章

  1. [洛谷P5137]polynomial
  2. Go学习之-用vscode写go代码遇到的问题
  3. 半编译半解释的Java语言和C++、Python等语言的区别
  4. 02:输出最高分数的学生姓名
  5. 不安和怀疑,美丽而又危险:看两位80后女艺术家的展览
  6. ASP.NET性能优化小结(ASP.NETC#)(转)
  7. gdbc 同步mysql_Jdbc数据同步
  8. Java实现升序排列的整形数组A,元素两两不相等找出A[i]=i的数据
  9. Android Studio(13)--- Git之使用GitHub搭建远程仓库
  10. 从 Java 替代品到打造完整生态,Kotlin 10 岁了!
  11. 如何使用VS2012进行简单程序的DEBUG(入门级)
  12. Ryu学习总结(持续更新)
  13. 文档服务器拓扑图,服务器网络结构拓扑图
  14. 笔记本硬盘坏了还能恢复数据吗 笔记本硬盘坏了怎么修复
  15. 网页打印中,A4纸对应的像素值的设定和换算
  16. 旋转卡壳简介(POJ2187)(洛谷P1452)
  17. 记一次失败的《将视频中的音频转换成文字》的经历
  18. 到Visual Studio Code en 2020中的实际实现
  19. 论文阅读_基于GCN的知识图对齐
  20. js判断身份证号码是否正确

热门文章

  1. ESP8266 发送数据和接收数据(万能模板)
  2. C语言简单编程案例(一)
  3. idea中使用git相关操作说明
  4. 【ps】ps如何画直折线
  5. mysql优化数据库对象
  6. 忘记数据库密码怎么办
  7. 小白也能读懂的 ZigBee 3.0 简介
  8. html+word+clou,AE脚本:Word Cloud 1.0.3_文字云排版动画脚本+教程
  9. 全靠我啃烂了这份2021最新面试题!吊打面试官
  10. 中国股市最“暴利”时代,而有的人活在15年的熊市!