极智AI | 量化实现分享五:详解格林深瞳EQ量化算法实现

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

大家好,我是极智视界,本文剖析一下格灵深瞳 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 量化原理

先来看一下实验数据:

这是 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 推理:一次能算 ((2^15-1)/(2^7-1)^2) = 2 次 SMLAL;
  • int7 推理:一次能算 ((2^15-1)/(2^6-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 num
                this->execidx_elemsize[i] = output_tensor_fp32->elem_size; //exec idx --> output elem size
                this->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);
        else
            this->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);
            else
                this->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 num
        this->execidx_elemsize[i] = output_tensor_fp32->elem_size; //exec idx --> output elem size
        this->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 算法的原理和实现,希望我的分享能对你的学习有一点帮助。


logo_show.gif

相关实践学习
基于函数计算一键部署掌上游戏机
本场景介绍如何使用阿里云计算服务命令快速搭建一个掌上游戏机。
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
11天前
|
机器学习/深度学习 人工智能 监控
AI算法分析,智慧城管AI智能识别系统源码
AI视频分析技术应用于智慧城管系统,通过监控摄像头实时识别违法行为,如违规摆摊、垃圾、违章停车等,实现非现场执法和预警。算法平台检测街面秩序(出店、游商、机动车、占道)和市容环境(垃圾、晾晒、垃圾桶、路面不洁、漂浮物、乱堆物料),助力及时处理问题,提升城市管理效率。
AI算法分析,智慧城管AI智能识别系统源码
|
11天前
|
机器学习/深度学习 人工智能 算法
31万奖金池等你挑战!IJCAI 2024 第九届“信也科技杯”全球AI算法大赛正式开赛!聚焦AI尖端赛题!
31万奖金池等你挑战!IJCAI 2024 第九届“信也科技杯”全球AI算法大赛正式开赛!聚焦AI尖端赛题!
54 1
31万奖金池等你挑战!IJCAI 2024 第九届“信也科技杯”全球AI算法大赛正式开赛!聚焦AI尖端赛题!
|
11天前
|
机器学习/深度学习 人工智能 算法
【AI 初识】描述遗传算法概念
【5月更文挑战第2天】【AI 初识】描述遗传算法概念
|
11天前
|
机器学习/深度学习 存储 人工智能
【AI 初识】人工智能中使用了哪些不同的搜索算法?
【5月更文挑战第2天】【AI 初识】人工智能中使用了哪些不同的搜索算法?
|
11天前
|
机器学习/深度学习 数据采集 人工智能
【热门话题】AI作画算法原理解析
本文解析了AI作画算法的原理,介绍了基于机器学习和深度学习的CNNs及GANs在艺术创作中的应用。从数据预处理到模型训练、优化,再到风格迁移、图像合成等实际应用,阐述了AI如何生成艺术作品。同时,文章指出未来发展中面临的版权、伦理等问题,强调理解这些算法对于探索艺术新境地的重要性。
36 3
|
11天前
|
机器学习/深度学习 人工智能 算法
详解AI作画算法原理
AI作画算法运用深度学习和生成对抗网络(GAN),通过学习大量艺术作品,模拟艺术家风格。卷积神经网络(CNN)提取图像特征,GAN中的生成器和判别器通过对抗训练生成艺术图像。循环神经网络和注意力机制可提升作品质量。这种技术开创了艺术创作新途径。
|
11天前
|
机器学习/深度学习 人工智能 自然语言处理
|
11天前
|
机器学习/深度学习 人工智能 关系型数据库
南京大学提出量化特征蒸馏方法QFD | 完美结合量化与蒸馏,让AI落地更进一步!!!
南京大学提出量化特征蒸馏方法QFD | 完美结合量化与蒸馏,让AI落地更进一步!!!
154 0
|
11天前
|
机器学习/深度学习 存储 文字识别
模型精度不降反升!飞桨是这样改进PACT量化算法的!
模型精度不降反升!飞桨是这样改进PACT量化算法的!
60 0
|
11天前
|
机器学习/深度学习 人工智能 监控
【AI 现况分析】AI 算法偏见和歧视分析
【1月更文挑战第27天】【AI 现况分析】AI 算法偏见和歧视分析

热门文章

最新文章