极智AI | 量化实现分享一:详解min-max对称量化算法实现

本文涉及的产品
视觉智能开放平台,分割抠图1万点
视觉智能开放平台,图像资源包5000点
视觉智能开放平台,视频资源包5000点
简介: 大家好,我是极智视界,本文剖析一下 min-max 对称量化算法实现,以 Tengine 的实现为例。

大家好,我是极智视界,本文剖析一下 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 为例进行代码说明,希望我的分享能对你的学习有一点帮助。


v2-d4545564532acbec17a8d2fd69c60cb8_1440w.gif


相关文章
|
13天前
|
存储 人工智能
Scaling Law或将终结?哈佛MIT预警:低精度量化已无路可走,重磅研究掀翻AI圈
哈佛大学和麻省理工学院的研究人员最近发布了一项重磅研究,对Scaling Law在低精度量化中的应用提出严重质疑。研究表明,随着训练数据增加,低精度量化带来的性能损失也增大,且与模型大小无关。这挑战了通过增加规模提升性能的传统观点,提醒我们在追求效率时不能忽视性能损失。该研究结果在AI圈内引发广泛讨论,提示未来需探索其他方法来提高模型效率,如混合精度训练、模型压缩及新型硬件架构。论文地址:https://arxiv.org/pdf/2411.04330。
39 11
|
27天前
|
机器学习/深度学习 人工智能 算法
Transformer打破三十年数学猜想!Meta研究者用AI给出反例,算法杀手攻克数学难题
《PatternBoost: Constructions in Mathematics with a Little Help from AI》提出了一种结合传统搜索算法和Transformer神经网络的PatternBoost算法,通过局部搜索和全局优化交替进行,成功应用于组合数学问题。该算法在图论中的Ramsey数研究中找到了更小的反例,推翻了一个30年的猜想,展示了AI在数学研究中的巨大潜力,但也面临可解释性和通用性的挑战。论文地址:https://arxiv.org/abs/2411.00566
77 13
|
1月前
|
机器学习/深度学习 存储 人工智能
淘天算法工程师玩转《黑神话》,多模态大模型如何成为天命AI
淘天集团未来生活实验室的算法工程师们以ARPG游戏《黑神话:悟空》为平台,探索多模态大模型(VLM)在仅需纯视觉输入和复杂动作输出场景中的能力边界。他们提出了一种名为VARP的新框架,该框架由动作规划系统和人类引导的轨迹系统组成,成功在90%的简单和中等难度战斗场景中取得胜利。研究展示了VLMs在传统上由强化学习主导的任务中的潜力,并提供了宝贵的人类操作数据集,为未来研究奠定了基础。
|
1月前
|
人工智能
Scaling Laws终结,量化无用,AI大佬都在审视这篇论文
《Scaling Laws for Precision》论文提出“精度感知”的扩展理论,将精度纳入模型发展的核心考量,弥补了传统AI模型发展理论忽视精度的不足。研究发现低精度训练会降低模型的有效参数计数,影响性能,并预测了低精度训练和后训练量化带来的损失。作者通过大量实验验证了理论的可靠性和有效性,为计算资源有限情况下如何平衡模型规模和精度提供了新思路。然而,该研究也引发了关于精度与性能权衡复杂性的争议。
77 27
|
2月前
|
机器学习/深度学习 人工智能 算法
Enhance-A-Video:上海 AI Lab 推出视频生成质量增强算法,显著提升 AI 视频生成的真实度和细节表现
Enhance-A-Video 是由上海人工智能实验室、新加坡国立大学和德克萨斯大学奥斯汀分校联合推出的视频生成质量增强算法,能够显著提升视频的对比度、清晰度和细节真实性。
115 8
Enhance-A-Video:上海 AI Lab 推出视频生成质量增强算法,显著提升 AI 视频生成的真实度和细节表现
|
2月前
|
机器学习/深度学习 存储 人工智能
【AI系统】训练后量化与部署
本文详细介绍了训练后量化技术,涵盖动态和静态量化方法,旨在将模型权重和激活从浮点数转换为整数,以优化模型大小和推理速度。通过KL散度等校准方法和量化粒度控制,文章探讨了如何平衡模型精度与性能,同时提供了端侧量化推理部署的具体实现步骤和技术技巧。
109 1
【AI系统】训练后量化与部署
|
2月前
|
机器学习/深度学习 存储 人工智能
【AI系统】感知量化训练 QAT
本文介绍感知量化训练(QAT)流程,旨在减少神经网络从FP32量化至INT8时的精度损失。通过在模型中插入伪量化节点(FakeQuant)模拟量化误差,并在训练中最小化这些误差,使模型适应量化环境。文章还探讨了伪量化节点的作用、正向与反向传播处理、TensorRT中的QAT模型高效推理,以及QAT与PTQ的对比,提供了实践技巧,如从良好校准的PTQ模型开始、采用余弦退火学习率计划等。
118 2
【AI系统】感知量化训练 QAT
|
2月前
|
机器学习/深度学习 人工智能 算法
【AI系统】内存分配算法
本文探讨了AI编译器前端优化中的内存分配问题,涵盖模型与硬件内存的发展、内存划分及其优化算法。文章首先分析了神经网络模型对NPU内存需求的增长趋势,随后详细介绍了静态与动态内存的概念及其实现方式,最后重点讨论了几种节省内存的算法,如空间换内存、计算换内存、模型压缩和内存复用等,旨在提高内存使用效率,减少碎片化,提升模型训练和推理的性能。
112 1
|
2月前
|
存储 人工智能 缓存
【AI系统】布局转换原理与算法
数据布局转换技术通过优化内存中数据的排布,提升程序执行效率,特别是对于缓存性能的影响显著。本文介绍了数据在内存中的排布方式,包括内存对齐、大小端存储等概念,并详细探讨了张量数据在内存中的排布,如行优先与列优先排布,以及在深度学习中常见的NCHW与NHWC两种数据布局方式。这些布局方式的选择直接影响到程序的性能,尤其是在GPU和CPU上的表现。此外,还讨论了连续与非连续张量的概念及其对性能的影响。
99 3
|
3月前
|
传感器 人工智能 监控
智慧工地 AI 算法方案
智慧工地AI算法方案通过集成多种AI算法,实现对工地现场的全方位安全监控、精准质量检测和智能进度管理。该方案涵盖平台层、展现层与应用层、基础层,利用AI技术提升工地管理的效率和安全性,减少人工巡检成本,提高施工质量和进度管理的准确性。方案具备算法精准高效、系统集成度高、可扩展性强和成本效益显著等优势,适用于人员安全管理、施工质量监控和施工进度管理等多个场景。
115 0

热门文章

最新文章