El marco de razonamiento de la red neuronal Tencent TNN implementa manualmente el razonamiento de convolución de un solo operador de dispositivos múltiples

inserte la descripción de la imagen aquí

prefacio

  Recientemente investigué TNNel marco de razonamiento de la red neuronal de Tencent, por lo que este blog presenta principalmente TNNla arquitectura básica, la cuantificación del modelo, la implementación manual x86y armel razonamiento de convolución de un solo operador en el dispositivo.

1. Introducción

  TNNEs un marco de inferencia de red neuronal liviano y de alto rendimiento de código abierto de Tencent Youtu Lab. También tiene muchas ventajas sobresalientes, como multiplataforma, alto rendimiento, compresión de modelos y poda de código. TNNSobre la base del marco original Rapidnet, ncnnel marco fortalece aún más el soporte de dispositivos móviles y la optimización del rendimiento. Al mismo tiempo, se basa en las características de alto rendimiento y buena escalabilidad del marco de código abierto convencional en la industria, y expande el soporte para el fondo y el X86marco NV GPU. El terminal móvil se ha implementado en muchas aplicaciones, como TNNteléfonos móviles QQ, Weishi y mapas. El servicio , como marco básico de aceleración de Tencent Cloud, ha brindado soporte acelerado para la implementación de muchas empresas.PTNNAI

inserte la descripción de la imagen aquí

  TNNDirección de fuente abierta: https://github.com/Tencent/TNN

2. Inicio rápido

2.1 onnx a tnn

inserte la descripción de la imagen aquí

 &emsp actualmente TNNes compatible con los principales formatos de archivos de modelos de la industria, incluidos ONNX, PyTorch, TensorFlow, etc.TesorFlow-Lite CaffeComo se muestra en la figura anterior, TNNse ONNXusará como una capa intermedia ONNXpara admitir múltiples formatos de archivo de modelo con la ayuda de la comunidad de código abierto. Si desea convertir formatos de archivo de modelo como PyTorch, yTensorFlow , primero debe usar la herramienta de conversión de modelo correspondiente para convertir uniformemente varios formatos de modelo al formato de modelo y luego convertir el modelo al modelo .CaffeTNNONNXONNXTNN

inserte la descripción de la imagen aquí

  Para simplificar los pasos de instalación y compilación de convert2tnnla herramienta de conversión , la recomendación oficial es utilizar dockerla imagen:

# 建议直接从 docker hub 上拉取镜像
docker pull ccr.ccs.tencentyun.com/qcloud/tnn-convert

# 对 docker 镜像进行重命名
docker tag ccr.ccs.tencentyun.com/qcloud/tnn-convert tnn-convert:latest
docker rmi ccr.ccs.tencentyun.com/qcloud/tnn-convert

# 通过打印 convert2tnn 的帮助信息来验证下 docker 镜像能够正常使用
docker run -it tnn-convert:latest python3 ./converter.py -h

inserte la descripción de la imagen aquí
  Además, compruebe la herramienta ONNXde descarga TNN:

docker run -it tnn-convert:latest python3 ./converter.py onnx2tnn -h

inserte la descripción de la imagen aquí

  Los parámetros específicos no se describirán con demasiado detalle, consulte la documentación oficial .

  Este ejemplo lo toma Resnet50como ejemplo y lo convierte al tnnformato:

import torch
from torchvision.models.resnet import resnet50


if __name__ == '__main__':
    model = resnet50()
    model.load_state_dict(torch.load('model/resnet50-0676ba61.pth'))
    model.eval()

    input_data = torch.randn(size=(1, 3, 224, 224), dtype=torch.float32)
    input_names, output_names = ["input"], ["output"]
    torch.onnx.export(model, input_data, "model/resnet50.onnx", input_names=input_names, output_names=output_names)
# 当然,也可以直接使用onnx格式的resnet50,下载链接为:https://github.com/onnx/models/tree/main/vision/classification/resnet/model
# 启动docker
docker run -v /home/liyanpeng/tnn_docker:/home/liyanpeng/tnn_docker --rm -it tnn-convert:latest /bin/bash
# cd /opt/TNN/tools/convert2tnn(default)
# onnx2tnn
python3 ./converter.py onnx2tnn /home/liyanpeng/tnn_docker/model/resnet50.onnx -in input:1,3,224,224

inserte la descripción de la imagen aquí

2.2 Compilar el motor TNN de la plataforma de destino

Consulte la documentación oficial   para conocer las precauciones relacionadas con la compilación .

  arm-linuxCompilación de la plataforma:

apt-get install g++-aarch64-linux-gnu gcc-aarch64-linux-gnu
apt-get install g++-arm-linux-gnueabihf gcc-arm-linux-gnueabihf
# apt-get install vim gdb

cd scripts
./build_aarch_linux.sh

  x86-linuxCompilación de la plataforma:

cd scripts
./build_linux_native.sh

inserte la descripción de la imagen aquí

2.3 Inferencia utilizando el motor TNN compilado

  El anterior no compila un ejemplo específico y luego compila el motor x86en cada tarea en la plataforma TNN:

# x86平台编译
cd examples/linux/x86
./build_linux_native.sh

# arm-linux交叉编译
# cd examples/linux/cross
# ./build_aarch64_linux.sh

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí
  Realice una tarea de clasificación de imágenes:

./demo_x86_imageclassify -p /home/liyanpeng/tnn_docker/model/resnet50.tnnproto -m /home/liyanpeng/tnn_docker/model/resnet50.
tnnmodel -i /home/liyanpeng/tnn_docker/model/tiger_cat.jpg

  El resultado del razonamiento también es correcto:

inserte la descripción de la imagen aquí

  La ubicación del código fuente de cada tarea:examples/linux/src

3. Implementar manualmente el razonamiento de convolución de un solo operador (coma flotante)

  TNNEl marco construye una instancia de inferencia de red neuronal y necesita ingresar dos archivos, uno es el archivo de estructura del modelo .tnnprotoy el otro es el archivo de peso del modelo .tnnmodelEstos dos archivos son obligatorios. Sin embargo, debido a algunas necesidades especiales, este método de archivo no es adecuado, por lo que aquí proporciono un ejemplo de creación manual de una estructura de modelo sin depender del archivo de modelo.

Siguiendo la clasificación de las imágenes examples/linux/srcbajo el directorio   , creé un directorio bajo el directorio raíz, que incluye dos archivos.TNNImageClassifydemomy_cnn_modelmy_conv.cppCMakeLists.txt

  my_conv.cppEl contenido del archivo es el siguiente:

// Author:   xiayouran
// Email:    [email protected]
// Datetime: 2023/4/8 15:17
// Filename: my_conv.cpp
#include "tnn/core/tnn.h"
#include "tnn/interpreter/abstract_model_interpreter.h"
#include "tnn/interpreter/tnn/model_interpreter.h"

using namespace TNN_NS;

int main(int argc, char* argv[]) {
    
    
    auto model_type = MODEL_TYPE_TNN;
    auto device_type = DEVICE_X86;// DEVICE_ARM
    auto data_type = DATA_TYPE_FLOAT;// DATA_TYPE_INT8

    ModelConfig model_config;
    model_config.model_type = model_type;

    NetworkConfig net_config;
    net_config.device_type = device_type;

    TNN tnn;
    Status status = tnn.MyInit(model_config);
    auto instance = tnn.CreateInst(net_config, status);

    BlobMap input_blobs;
    status = instance->GetAllInputBlobs(input_blobs);
    Blob* input_blob = input_blobs.begin()->second;
    float* data_ptr = static_cast<float*>(input_blob->GetHandle().base);
    for (int i = 0; i < 1 * 1 * 4 * 4; i++) {
    
    
        data_ptr[i] = (float)1.0 + i;
    }

    status = instance->Forward();

    BlobMap output_blobs;
    status = instance->GetAllOutputBlobs(output_blobs);
    Blob* output_blob = output_blobs.begin()->second;
    float* out_data_ptr = static_cast<float*>(output_blob->GetHandle().base);
    for (int i = 0; i < 1 * 1 * 2 * 2; i++) {
    
    
        std::cout << out_data_ptr[i] << std::endl;
    }

    return 0;
}

  La entrada de convolución shapees (1, 1, 4, 4), la convolución shapees (1, 1, 3, 3)y la salida de convolución shapees (1, 1, 2, 2), específicamente:

inserte la descripción de la imagen aquí
  El resultado de la operación es el siguiente:

inserte la descripción de la imagen aquí
  Además CMakeLists.txtde agregar este código de muestra my_conv.cpp, se agrega al archivo la clasificación oficial de la imagen y sus dependencias.El democontenido TNNImageClassify.ccespecífico es el siguiente:

file(GLOB MyCNNModel_SRCS my_conv.cpp)
file(GLOB ImageClassify_SRCS ${
    
    CMAKE_CURRENT_SOURCE_DIR}/../examples/linux/src/TNNImageClassify/TNNImageClassify.cc)

message(${
    
    MyCNNModel_SRCS})
message(${
    
    ImageClassify_SRCS})

#include_directories(../include)
#include_directories(../source)

include_directories(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../examples/base)
include_directories(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../examples/base/utils)
include_directories(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../examples/utils)
add_subdirectory(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../third_party/gflags ${
    
    CMAKE_CURRENT_SOURCE_DIR}/../third_party/gflags)
get_target_property(GFLAGS_INCLUDE_DIRS gflags INTERFACE_INCLUDE_DIRECTORIES)
include_directories(BEFORE "${GFLAGS_INCLUDE_DIRS}")
link_libraries(gflags)
file(GLOB FLAG_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../examples/linux/src/*.cc")
file(GLOB_RECURSE BASE_SRC
        "${CMAKE_CURRENT_SOURCE_DIR}/../examples/base/*.cc"
        "${CMAKE_CURRENT_SOURCE_DIR}/../examples/base/utils/*.cc")
file(GLOB_RECURSE UTIL_SRC
        "${CMAKE_CURRENT_SOURCE_DIR}/../examples/utils/*.cc")

include_directories(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../source/tnn/interpreter/tnn)
include_directories(${
    
    CMAKE_CURRENT_SOURCE_DIR}/../third_party/stb)

add_executable(my_conv_cmd ${
    
    MyCNNModel_SRCS})
add_executable(demo_x86_imageclassify_cmd ${
    
    ImageClassify_SRCS} ${
    
    BASE_SRC} ${
    
    UTIL_SRC} ${
    
    FLAG_SRC})

target_link_libraries(my_conv_cmd TNN)
target_link_libraries(demo_x86_imageclassify_cmd TNN)

set_target_properties(my_conv_cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${
    
    PROJECT_BINARY_DIR})
set_target_properties(demo_x86_imageclassify_cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${
    
    PROJECT_BINARY_DIR})

4. Análisis de código

  De acuerdo con las instrucciones oficiales de la API , se requieren cinco pasos para ejecutar una red neuronal:

# Step1. 模型解析
model_config.params.push_back(proto_buffer);# proto文件内容存入proto_buffer
model_config.params.push_back(model_buffer);# model文件内容存入model_buffer
Status ret = tnn.Init(model_config);

# Step2. 网络构建
auto net_instance = tnn.CreateInst(config, status);

# Step3. 输入设定
auto status = net_instance->SetInputMat(input_mat, input_cvt_param);

# Step4. 网络运行
auto status = net_instance->Forward();

# Step5. 输出获取
auto status = instance->GetOutputMat(output_mat);

  En el primer paso del análisis del modelo, las operaciones de archivo están involucradas. En teoría, siempre que tnnel archivo del modelo se escriba de acuerdo con el formato convertido por otros modelos, el código fuente no necesita ser modificado. Esta parte del código fuente es No se lee aquí, por lo que el código fuente se modifica directamente.
  Después del análisis del código fuente, la construcción manual de un modelo requiere principalmente la construcción de cada capa del modelo de red neuronal layery la inicialización de los parámetros, el intérprete del modelo y tnnla inicialización, de la siguiente manera:

4.1 Construcción de un modelo (capa convolucional única)

source/tnn/interpreter/tnn/model_interpreter.ccSe ha agregado una nueva función en el archivo   A ModelInterpreter::MyInterpret()diferencia de la ModelInterpreter::Interpret(std::vector<std::string> &params)función oficial, esta función no necesita analizar la estructura y el peso del modelo del archivo:

// Interpret the proto and model without file.
Status ModelInterpreter::MyInterpret() {
    
    
    Status status = TNN_OK;
    /****************初始化卷积层参数****************/
    NetStructure *structure = GetNetStructure();

    structure->source_model_type = MODEL_TYPE_TNN;
    DimsVector &input_shape = structure->inputs_shape_map["input"];
    input_shape.push_back(1);
    input_shape.push_back(1);
    input_shape.push_back(4);
    input_shape.push_back(4);
    DataType data_type = DATA_TYPE_FLOAT;// DATA_TYPE_FLOAT
    structure->input_data_type_map["input"] = data_type;
    structure->outputs.insert("output");

    auto cur_layer = std::make_shared<LayerInfo>();
    std::string type_str = "Convolution";
    type_str = Transfer(type_str);
    LayerType type = GlobalConvertLayerType(type_str);
    cur_layer->type = type;
    cur_layer->type_str = type_str;
    cur_layer->name = Transfer("Conv_0");
    cur_layer->inputs.clear();
    cur_layer->outputs.clear();

    cur_layer->inputs.push_back("input");
    structure->blobs.insert("input");
    cur_layer->outputs.push_back("output");
    structure->blobs.insert("output");

    LayerParam *layer_param = NULL;
    LayerParam** param = &layer_param;
    auto p = CreateLayerParam<ConvLayerParam>(param);

    p->input_channel = 1;
    p->output_channel = 1;

    p->kernels = {
    
    3, 3};
    p->strides = {
    
    1, 1};
    p->pads = {
    
    0, 0, 0, 0};
    p->dialations = {
    
    1, 1};

    p->bias = 0;
    p->pad_type = -1;
    p->group = 1;
    p->activation_type = 0;

    layer_param->type = cur_layer->type_str;
    layer_param->name = cur_layer->name;
    if (data_type == DATA_TYPE_INT8) {
    
    
        layer_param->quantized = true;
    }

    cur_layer->param = shared_ptr<LayerParam>(layer_param);
    structure->layers.push_back(cur_layer);
    /**************卷积层参数初始化结束**************/

    /****************初始化卷积层权重****************/
    NetResource *net_resource = GetNetResource();

    LayerResource *layer_resource = NULL;
    LayerResource** resource = &layer_resource;

    auto layer_res = CreateLayerRes<ConvLayerResource>(resource);
    layer_res->filter_format = OIHW;

    // weight
    RawBuffer weight_buf;
    DimsVector weight_dims = {
    
    1, 1, 3, 3};
    weight_buf = TNN_NS::RawBuffer(1*1*3*3*4);
    weight_buf.SetDataType(data_type);
    weight_buf.SetBufferDims(weight_dims);
    float weight_data[1][1][3][3] = {
    
    {
    
    {
    
    {
    
    1.0, 0.0, 0.0},
                                       {
    
    0.0, 1.0, 0.0},
                                       {
    
    0.0, 0.0, 1.0}}}};
    memcpy(weight_buf.force_to<float*>(), weight_data, 1*1*3*3*4);
    layer_res->filter_handle = weight_buf;

    // bias
    RawBuffer bias_buf;
    DimsVector bias_dims = {
    
    1};
    bias_buf = TNN_NS::RawBuffer(4);
    bias_buf.SetDataType(data_type);
    bias_buf.SetBufferDims(bias_dims);
    float bias_data[1] = {
    
    0.0};
    memcpy(bias_buf.force_to<float*>(), bias_data, 1*4);
    layer_res->bias_handle = bias_buf;

	/****************以下操作浮点推理非必须****************/
    // scale
    RawBuffer scale_buf;
    DimsVector scale_dims = {
    
    1};
    scale_buf = TNN_NS::RawBuffer(4);
    scale_buf.SetDataType(DATA_TYPE_FLOAT);
    scale_buf.SetBufferDims(scale_dims);
    float scale_data[1] = {
    
    1.0};
    memcpy(scale_buf.force_to<float*>(), scale_data, 1*4);
    layer_res->scale_handle = scale_buf;
	
    // zero_point
    RawBuffer zero_point_buf;
    DimsVector zero_point_dims = {
    
    1};
    zero_point_buf = TNN_NS::RawBuffer(1);
    zero_point_buf.SetDataType(DATA_TYPE_INT8);
    zero_point_buf.SetBufferDims(zero_point_dims);
    int8_t zero_point_data[1] = {
    
    0};
    memcpy(zero_point_buf.force_to<int8_t*>(), zero_point_data, 1*1);
    layer_res->zero_point_handle = zero_point_buf;
	/****************以上操作浮点推理非必须****************/
	
    net_resource->resource_map["Conv_0"] = std::shared_ptr<LayerResource>(layer_resource);

    // 不用解析constant_map
    /**************卷积层权重初始化结束**************/

    return status;
}

  En consecuencia, la declaración de esta función debe agregarse a los tres archivos source/tnn/interpreter/tnn/model_interpreter.h:source/tnn/interpreter/abstract_model_interpreter.hsource/tnn/interpreter/ncnn/ncnn_model_interpreter.h

// model_interpreter.h文件中的ModelInterpreter
virtual Status MyInterpret();

// abstract_model_interpreter.h文件中的AbstractModelInterpreter
virtual Status MyInterpret() = 0;

// ncnn_model_interpreter.h文件中的NCNNModelInterpreter
virtual Status MyInterpret();

4.2 Construyendo el intérprete

source/tnn/core/tnn_impl_default.ccSe agrega una nueva función al archivo   , TNNImplDefault::MyInit(ModelConfig& config)y la implementación de la función es aproximadamente la misma que la TNNImplDefault::Init(ModelConfig& config)función oficial, excepto que la función se usa al construir el intérprete aquí MyInterpret():

Status TNNImplDefault::MyInit(ModelConfig& config) {
    
    
    auto status = TNNImpl::MyInit(config);
    if (status != TNN_OK) {
    
    
        return status;
    }

    auto interpreter = CreateModelInterpreter(config.model_type);
    if (!interpreter) {
    
    
        return Status(TNNERR_NET_ERR, "interpreter is nil");
    }
    interpreter_ = std::shared_ptr<AbstractModelInterpreter>(interpreter);
    return interpreter_->MyInterpret();
}

  TNNImpl::MyInit(config)La implementación de la función está en source/tnn/core/tnn_impl.ccel archivo:

Status TNNImpl::MyInit(ModelConfig &config) {
    
    
    model_config_.model_type = config.model_type;
    return TNN_OK;
}

  En consecuencia, debe agregar la declaración de esta función en los dos archivos source/tnn/core/tnn_impl_default.h:source/tnn/core/tnn_impl.h

// tnn_impl_default.h文件中的MyInit
virtual Status MyInit(ModelConfig& config);

// tnn_impl.h文件中的MyInit
virtual Status MyInit(ModelConfig& config);

4.3 Inicializar tnn

  Para tnnpoder inicializar correctamente según nuestro método, necesitamos agregar TNN::MyInit(ModelConfig& config)una función para reemplazar la TNN::Init(ModelConfig& config)función oficial de inicialización, específicamente en source/tnn/core/tnn.ccel archivo:

Status TNN::MyInit(ModelConfig& config) {
    
    
    impl_ = TNNImplManager::GetTNNImpl(config.model_type);
    if (!impl_) {
    
    
        LOGE("Error: not support mode type: %d. If TNN is a static library, link it with option -Wl,--whole-archive tnn -Wl,--no-whole-archive on android or add -force_load on iOS\n", config.model_type);
        return Status(TNNERR_NET_ERR, "unsupported mode type, If TNN is a static library, link it with option -Wl,--whole-archive tnn -Wl,--no-whole-archive on android or add -force_load on iOS");
    }
    return impl_->MyInit(config);
}

  En consecuencia, include/tnn/core/tnn.hla declaración de esta función debe agregarse al archivo:

// tnn.h文件中的MyInit
Status MyInit(ModelConfig& config);

CMakeLists.txtHasta ahora, se han completado los elementos necesarios para la construcción manual de la inferencia de convolución de un solo operador. Agregue el directorio de código de este ejemplo al archivo   en el directorio raíz para compilar:

add_subdirectory(my_cnn_model)

5. Cuantificación del modelo

5.1 Compilación de herramientas de cuantificación

# 编译
cd platforms/linux/
./build_quanttool.sh -c

# 执行量化
cd build_quantize/
./quantization_cmd -p /home/liyanpeng/tnn_docker/model/resnet50.tnnproto -m /home/liyanpeng/tnn_docker/model/resnet50.tnnmodel -i /home/liyanpeng/tnn_docker/imagenet128/ -o resnet50

inserte la descripción de la imagen aquí
  El tamaño del modelo de punto flotante es 98M, y el modelo de punto fijo cuantificado es 26M:
inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

  Inferencia con modelos cuantificados:

./demo_x86_imageclassify -p /opt/TNN/platforms/linux/build_quantize/resnet50.quantized.tnnproto -m /opt/TNN/platforms/linux/build_quantize/resnet50.quantized.tnnmodel -i /home/liyanpeng/tnn_docker/model/tiger_cat.jpg

  Aquí solo hay 128cuantización con una imagen, por lo que la pérdida de precisión es grande y el resultado del razonamiento no es correcto:

inserte la descripción de la imagen aquí
  Se cambió 1000la cuantificación de una imagen, feature mapse adopta el método de cuantificación del método KL, weightse adopta el método del método MIN_MAX/ADMMy también se reemplaza la imagen de prueba, y los resultados del razonamiento no funcionan:

inserte la descripción de la imagen aquí

5.2 Proceso de cuantificación

  TNNMin-MaxEl método de cuantificación se utiliza de forma predeterminada . Además, se admiten métodos de cuantificación feature map. El proceso de cuantificación específico es el siguiente:KLweightADMM

calibration.Init(net_config, model_config)
/*根据输入shape,计算出每个网络层的输出shape*/

calibration.SetCalibrationParams(cali_params)
/*设置量化方式为MIN_MAX*/

calibration.RunCalibration(dataset)
/*scale计算和量化*/
    CalBlobScale(dataset);// Compute Feature Scale
        InitFeatureMap();// Init Feature map(在此之前进行了reshape),初始化每个feature map的range_per_channel_等参数
        UpdateBlobRange(dataset);// Collect the Range of Feature map,更新range_per_channel_
            UpdateRange()
        UpdateBlobDistribute(dataset);// Calculate Distribute of Feature map
            ResetDistribute()// 根据range_per_channel_计算valid_channel_和interval_per_channel_,并初始化distribute_per_channel_
            UpdateDistribute()// 
        CalculateScale(scale_vec, zero_point_vec);// Compute Scale of Feature map and save to resource map
    QuantizeParams();// Quantize params
    MergeBlobScale();// Merge Blob Scale of some layers

calibration.Serialize(output_name + ".quantized.tnnproto", output_name + ".quantized.tnnmodel")
/*保存量化模型*/

  Que range_per_channel_representa channellos valores máximos y mínimos en cada uno: first(min), second(max).

  La ubicación del código fuente cuantificado es: tools/quantization.

5.3 Cuantificación del mapa de características

5.3.1 Cálculo de rango_por_canal_

Calcule los valores máximo y mínimo per_channelpara todos feature map(incluido input/output) l   de la siguiente manera:channe

// tools/quantization/scale_calculator.cc --> ScaleCalculator::UpdateRange()
// Collect the Range of Feature map
// 在这里也叫 blob
int batch       = origin_blob_->GetBlobDesc().dims[0];// 1
int channel     = origin_blob_->GetBlobDesc().dims[1];// 3
int hxw         = DimsVectorUtils::Count(origin_blob_->GetBlobDesc().dims, 2);// 224*224
float* data_ptr = reinterpret_cast<float*>(static_cast<char*>(origin_blob_->GetHandle().base) +
                                           origin_blob_->GetHandle().bytes_offset);

for (int b = 0; b < batch; ++b) {
    
    
    for (int c = 0; c < channel; ++c) {
    
    
        int channel_idx = c;
        if (merge_channel_) {
    
    
            channel_idx = 0;
        }

        float* p = data_ptr + b * channel * hxw + c * hxw;

        for (int i = 0; i < hxw; ++i) {
    
    
            float val = p[i];

            if (val < range_per_channel_[channel_idx].first) {
    
    
                range_per_channel_[channel_idx].first = val;//first记录当前channel中的最小值
            }
            if (val > range_per_channel_[channel_idx].second) {
    
    
                range_per_channel_[channel_idx].second = val;//second记录当前channel中的最大值
            }
        }
    }
}

5.3.2 Cálculo de intervalo_por_canal_

// tools/quantization/scale_calculator.cc --> ScaleCalculator::ResetDistribute()
for (unsigned int i = 0; i < interval_per_channel_.size(); ++i) {
    
    
    float max_val     = std::max(std::abs(range_per_channel_[i].first), std::abs(range_per_channel_[i].second));
    valid_channel_[i] = max_val > 0.00001;
    if (valid_channel_[i]) {
    
    
        // bin_nums_ 默认值为 2048
        interval_per_channel_[i] = (float)bin_nums_ / max_val;
    }
}

5.3.3 Cálculo de distribuir_por_canal_

feature mapEl propósito de las MIN_MAXdos estrategias de cuantificación   involucradas aquí KL_DIVERGENCEes encontrar un umbral adecuado threshold.
  MIN_MAXEstrategia de cuantificación:

// tools/quantization/scale_calculator.cc --> ScaleCalculator::CalculateScalePerDis
const int target_bin_nums = 128;
int threshold             = target_bin_nums;
threshold = bin_nums_ - 1;// 2047
output = ((float)threshold + 0.5) / interval / 127.0;

  En resumen, es: scale = max[abs(r_min), abs(r_max)] / 127.0, lo cual NVIDIAes consistente con lo dado en el informe, como se muestra en la siguiente figura:

inserte la descripción de la imagen aquí
  KL_DIVERGENCEEstrategia de cuantificación:

// tools/quantization/scale_calculator.cc --> ScaleCalculator::CalculateScalePerDis
const int target_bin_nums = 128;
int threshold             = target_bin_nums;

// normalize
float sum = 0;
std::for_each(distribute.begin(), distribute.end(), [&](float n) {
    
     sum += n; });
std::for_each(distribute.begin(), distribute.end(), [sum](float& n) {
    
     n /= sum; });

float kl_val_min          = 1e6;
float sum_after_threshold = 0.0f;
std::for_each(distribute.begin() + target_bin_nums, distribute.end(),
              [&](float n) {
    
     sum_after_threshold += n; });
for (int i = target_bin_nums; i < bin_nums_; ++i) {
    
    
    // 1. get referenced distribute
    std::vector<float> distribute_ref(i);
    std::copy(distribute.begin(), distribute.begin() + i, distribute_ref.begin());
    distribute_ref[i - 1] += sum_after_threshold;
    sum_after_threshold -= distribute[i];  // for next loop

    // 2. quantize the distribute within threshold scope as target bins
    std::vector<float> distribute_quantized(target_bin_nums);
    const float bin_interval = (float)i / (float)target_bin_nums;

    for (int j = 0; j < target_bin_nums; ++j) {
    
    
        const float start = j * bin_interval;
        const float end   = start + bin_interval;

        const int left_upper = static_cast<int>(std::ceil(start));
        if (left_upper > start) {
    
    
            const float left_scale = left_upper - start;
            distribute_quantized[j] += left_scale * distribute[left_upper - 1];
        }
        const int right_lower = static_cast<int>(std::floor(end));
        if (right_lower < end) {
    
    
            const float right_scale = end - right_lower;
            distribute_quantized[j] += right_scale * distribute[right_lower];
        }
        std::for_each(distribute.begin() + left_upper, distribute.begin() + right_lower,
                      [&](float n) {
    
     distribute_quantized[j] += n; });
    }

    // 3. expand target bins to i bins to calculate kl
    std::vector<float> distribute_expanded(i);
    for (int j = 0; j < target_bin_nums; ++j) {
    
    
        const float start    = j * bin_interval;
        const float end      = start + bin_interval;
        float count          = 0;
        const int left_upper = static_cast<int>(std::ceil(start));
        float left_scale     = 0.0f;
        if (left_upper > start) {
    
    
            left_scale = left_upper - start;
            if (distribute[left_upper - 1] != 0) {
    
    
                count += left_scale;
            }
        }
        const int right_lower = static_cast<int>(std::floor(end));
        float right_scale     = 0.0f;
        if (right_lower < end) {
    
    
            right_scale = end - right_lower;
            if (distribute[right_lower] != 0) {
    
    
                count += right_scale;
            }
        }

        std::for_each(distribute.begin() + left_upper, distribute.begin() + right_lower, [&](float n) {
    
    
            if (n != 0) {
    
    
                count += 1;
            }
        });

        if (count == 0) {
    
    
            continue;
        }
        const float to_expand_val = distribute_quantized[j] / count;
        if (left_upper > start && distribute[left_upper - 1] != 0) {
    
    
            distribute_expanded[left_upper - 1] += to_expand_val * left_scale;
        }
        if (right_lower < end && distribute[right_lower] != 0) {
    
    
            distribute_expanded[right_lower] += to_expand_val * right_scale;
        }

        for (int k = left_upper; k < right_lower; ++k) {
    
    
            if (distribute[k] != 0) {
    
    
                distribute_expanded[k] += to_expand_val;
            }
        }
    }

    // 4. calculate kl val
    const float kl_val_cur = KlDivergence(distribute_ref, distribute_expanded);

    // 5. get the threshold of min kl val
    if (kl_val_cur < kl_val_min) {
    
    
        kl_val_min = kl_val_cur;
        threshold  = i;
    }
}
output = ((float)threshold + 0.5) / interval / 127.0;

5.3.4 Cálculo y almacenamiento de escala

  feature mapLa scaleinformación relevante también se almacenará en LayerResourceel objeto. En comparación con la capa convolucional LayerResource, aquí están bloblos datos, y el resource_mapnombre correspondiente es xxx_scale_data_, específicamente:

val.resize(valid_channel_.size());
std::fill(val.begin(), val.end(), 0.0f);

for (unsigned int c = 0; c < range_per_channel_.size(); ++c) {
    
    
    int ret = -1;
    ret = CalculateScalePerDis(distribute_per_channel_[c], interval_per_channel_[c], val[c]);
}
// val存储的就是CalculateScalePerDis计算出的output,也即是feature map的scale

// tools/quantization/calibration.cc --> Calibration::CalBlobScale()
// 将scale_vec和zero_point_vec写入net_resource->resource_map中
LayerResource* blob_scale_res;
blob_scale_res = CreateIntScale(scale_vec, zero_point_vec);
net_resource->resource_map[input_scale_name] = std::shared_ptr<LayerResource>(blob_scale_res);
// input_scale_name: xxx_scale_data_

// tools/quantization/calibration.cc --> Calibration::CreateIntScale()
IntScaleResource* int8scale = new IntScaleResource();
// scale
RawBuffer scale(scale_vec.size() * sizeof(float));
float* k_data = scale.force_to<float*>();
memcpy(k_data, scale_vec.data(), scale_vec.size() * sizeof(float));
int8scale->scale_handle = scale;

// zero_point
RawBuffer zero_point(zero_point_vec.size() * sizeof(char));
zero_point.SetDataType(DATA_TYPE_INT8);
int8_t* sb_data = zero_point.force_to<int8_t*>();
memcpy(sb_data, zero_point_vec.data(), zero_point_vec.size() * sizeof(char));
int8scale->zero_point_handle = zero_point;

// bias
RawBuffer bias(scale_vec.size() * sizeof(int32_t));
bias.SetDataType(DATA_TYPE_INT32);
int32_t* b_data = bias.force_to<int32_t*>();
memset(b_data, 0, scale_vec.size() * sizeof(int32_t));
int8scale->bias_handle = bias;

5.4 cuantificación de peso

5.4.1 Preprocesamiento

  Antes de cuantificar el peso, se multiplica por weightla entrada , en concreto:feature mapscale

// tools/quantization/calibration.cc --> Calibration::QuantizeConvParams()
std::vector<float> weight_multiby_inputscale(size);
// multi weights by input_scale
// input_scale就是上面feature map的scale
float* input_scale_data = input_scale->scale_handle.force_to<float*>();
auto filter_handle      = resource->filter_handle;
float* weight_data = filter_handle.force_to<float*>();
// conv(32, 3, 3, 3)
for (int group_idx = 0; group_idx < group; group_idx++) {
    
          // 1
    for (int oc = 0; oc < output_channel_per_group; ++oc) {
    
        // 32
        for (int ic = 0; ic < input_channel_per_group; ++ic) {
    
     // 3
            int s_idx = ic + group_idx * input_channel_per_group;
            for (int i = 0; i < kernel_size; ++i) {
    
                // 3*3
                int idx = (group_idx * output_channel_per_group + oc) * oc_stride + ic * kernel_size + i;
                if (is_depthwise) {
    
    
                    weight_multiby_inputscale[idx] = weight_data[idx];
                } else {
    
    
                    weight_multiby_inputscale[idx] = weight_data[idx] * input_scale_data[s_idx];
                }
            }
        }
    }
} 

5.4.2 Estrategia de cuantificación de peso

  TNNHay dos tipos de cuantificación de convolución en: MIN_MAXestrategia de cuantificación y ADMMestrategia de cuantificación.
  MIN_MAXEstrategia de cuantificación:

// tools/quantization/calibration.cc --> Calibration::CalQuantizedWeights()
// MIN_MAX
int weight_scale_count = merge_channel ? 1 : output_channel;
int s_size             = size / weight_scale_count;// 32*3*3*3 / 32
for (int s_idx = 0; s_idx < weight_scale_count; ++s_idx) {
    
    
    const float* weight_start = weights + s_idx * s_size;
    int8_t* weight_q_start    = quantized_weights + s_idx * s_size;
    auto minmax               = std::minmax_element(weight_start, weight_start + s_size);
    float max_val_abs         = std::max(std::abs(*minmax.first), std::abs(*minmax.second));

    weight_scale[s_idx]    = max_val_abs / 127.0f;
    float scale_float2int8 = 1.0f;
    if (max_val_abs != 0)
        scale_float2int8 = 1 / weight_scale[s_idx];

    // quantize weights
    for (int i = 0; i < s_size; ++i) {
    
    
        int value         = static_cast<int>(std::round(weight_start[i] * scale_float2int8));
        weight_q_start[i] = std::min(127, std::max(-127, value));
    }
}

  MIN_MAXLa estrategia de cuantificación se puede resumir de la siguiente manera: , el rango de valores de weight_int8 = (weight_float * input_scale) / max_val_abs * 127obtenido es . La estrategia cuantitativa es la siguiente:weight_int8[-127, 127]
  ADMM

// tools/quantization/calibration.cc --> Calibration::CalQuantizedWeights()
// ADMM
int weight_scale_count  = merge_channel ? 1 : output_channel;
int s_size              = size / weight_scale_count;
const int quantize_bits = 8;

InitWeightScaleADMM(weights, size, output_channel, merge_channel, weight_scale, quantize_bits);

int iter           = 0;
float pre_sum      = 0;
float cur_sum      = 0;
const int max_iter = 1000;

for (int i = 0; i < size; i++) {
    
    
    pre_sum += std::fabs(weights[i]);
}
// update weights quan
while (iter < max_iter) {
    
    
    UpdateQuantizedWeightsADMM(weights, size, output_channel, merge_channel, weight_scale, quantize_bits,
                               quantized_weights);
    UpdateAlphaADMM(weights, size, output_channel, merge_channel, weight_scale, quantized_weights);
    iter++;
}

for (int i = 0; i < size; i++) {
    
    
    cur_sum += std::fabs(quantized_weights[i] * weight_scale[i / s_size]);
}

5.4.3 almacenamiento a escala

  Para convoluciones cuantificadas weight, la suma scalese zero_pointguarda en la layeractual resource:

// weight_quantized 对应上述的 quantized_weights(weight_quantized_data) int8_t
// weight_scale 对应上述的 weight_scale(weight_scale_data) float
// weight_zero_point 对应上述的 weight_zero_point(weight_zero_point_data) int8_t
resource->filter_handle = weight_quantized;
resource->scale_handle  = weight_scale;
resource->zero_point_handle  = weight_zero_point;

5.5 cuantificación de sesgo

  biasEl resultado cuantificado se divide por biascoma weightflotante scale:

// tools/quantization/calibration.cc
auto fp32_bias_handle = ConvertHalfHandle(resource->bias_handle);
float* bias_data      = fp32_bias_handle.force_to<float*>();
RawBuffer bias_quantized(output_channel * sizeof(int32_t));
bias_quantized.SetDataType(DATA_TYPE_INT32);
int32_t* bias_quantized_data = bias_quantized.force_to<int32_t*>();

for (int oc = 0; oc < output_channel; ++oc) {
    
    
    if (weight_scale_data[oc] == 0) {
    
    
        bias_quantized_data[oc] = 0;
    } else {
    
    
        int weight_scale_idx = oc;
        bias_quantized_data[oc] = static_cast<int32_t>(bias_data[oc] / weight_scale_data[weight_scale_idx]);
    }
}

resource->bias_handle = bias_quantized;

5.6 Proceso de razonamiento de 8 bits

  Supongamos que la información de la capa convolucional actual es:

# input:  (1, 3, 224, 224)
# conv:   (32, 3, 3, 3)
# output: (1, 32, 222, 222)

  Combinado x86con armel razonamiento de convolución de 8 bits, se hace el siguiente resumen:

const float *w_scale        = conv_res->scale_handle.force_to<float *>();

const float *o_scale =
    reinterpret_cast<BlobInt8 *>(outputs[0])->GetIntResource()->scale_handle.force_to<float *>();

RawBuffer temp_buffer(total_byte_size);// 32个数 128字节
float *temp_ptr = temp_buffer.force_to<float *>();
for (int i = 0; i < dims_output[1]; i++) {
    
    
    int scale_idx_w = scale_len_w == 1 ? 0 : i;
    int scale_idx_o = scale_len_o == 1 ? 0 : i;

    temp_ptr[i] = w_scale[scale_idx_w] / o_scale[scale_idx_o];
}

// source/tnn/device/arm/acc/compute/compute_int8.cc
// ARM
dstTemp[j] += (int32_t)src_z[i] * (int32_t)weight_j[i];
auto res = static_cast<float>(dstTemp[j] + bias[j]) * scale[j];
dst_x[j] = float2int8(res);

  En resumen, (input_data_int32 * weight_data_int32 + bias) * weight_scalepuede obtener la salida de punto flotante de la convolución 浮点输出 / output_scale(es decir, la siguiente layer), input_scaleobtener la salida cuantificada de la convolución y luego limitarla a [-128. 127].

6. Im2col implementa el cálculo de convolución

  De acuerdo con la implementación específica del hardware, la mayoría de los cálculos de convolución se convertirán en multiplicación de matrices (GEMM). El método más utilizado es im2coldar algunos im2coldiagramas de ejemplo de cálculos de convolución a continuación. ¡Será mejor comer junto con este blog !

La entrada 6.1 es de un solo canal, el peso es de un solo canal (salida)

inserte la descripción de la imagen aquí

La entrada 6.2 es multicanal, el peso es de un solo canal (salida)

inserte la descripción de la imagen aquí

La entrada 6,3 es multicanal, el peso es multicanal (salida)

inserte la descripción de la imagen aquí

conclusión

  Este blog presenta principalmente el uso básico de TNN, el uso de herramientas de cuantificación y la implementación manual de la inferencia de convolución de un solo operador. Además de la inferencia de convolución de punto flotante, también se implementa la inferencia de convolución de punto fijo, pero los resultados actuales 8bittienen seguimiento Luego implemente 8bitel código de implementación de la inferencia de convolución de punto fijo suplementario.

Supongo que te gusta

Origin blog.csdn.net/qq_42730750/article/details/130082300
Recomendado
Clasificación