Directorio de artículos
-
- prefacio
- 1. Introducción
- 2. Inicio rápido
- 3. Implementar manualmente el razonamiento de convolución de un solo operador (coma flotante)
- 4. Análisis de código
- 5. Cuantificación del modelo
- 6. Im2col implementa el cálculo de convolución
- conclusión
prefacio
Recientemente investigué TNN
el marco de razonamiento de la red neuronal de Tencent, por lo que este blog presenta principalmente TNN
la arquitectura básica, la cuantificación del modelo, la implementación manual x86
y arm
el razonamiento de convolución de un solo operador en el dispositivo.
1. Introducción
TNN
Es 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. TNN
Sobre la base del marco original Rapidnet
, ncnn
el 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 X86
marco NV GPU
. El terminal móvil se ha implementado en muchas aplicaciones, como TNN
telé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.P
TNN
AI
TNN
Dirección de fuente abierta: https://github.com/Tencent/TNN
2. Inicio rápido
2.1 onnx a tnn
&emsp actualmente TNN
es compatible con los principales formatos de archivos de modelos de la industria, incluidos ONNX
, PyTorch
, TensorFlow
, etc.TesorFlow-Lite
Caffe
Como se muestra en la figura anterior, TNN
se ONNX
usará como una capa intermedia ONNX
para 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 .Caffe
TNN
ONNX
ONNX
TNN
Para simplificar los pasos de instalación y compilación de convert2tnn
la herramienta de conversión , la recomendación oficial es utilizar docker
la 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
Además, compruebe la herramienta ONNX
de descarga TNN
:
docker run -it tnn-convert:latest python3 ./converter.py onnx2tnn -h
Los parámetros específicos no se describirán con demasiado detalle, consulte la documentación oficial .
Este ejemplo lo toma Resnet50
como ejemplo y lo convierte al tnn
formato:
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
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-linux
Compilació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-linux
Compilación de la plataforma:
cd scripts
./build_linux_native.sh
2.3 Inferencia utilizando el motor TNN compilado
El anterior no compila un ejemplo específico y luego compila el motor x86
en 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
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:
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)
TNN
El marco construye una instancia de inferencia de red neuronal y necesita ingresar dos archivos, uno es el archivo de estructura del modelo .tnnproto
y el otro es el archivo de peso del modelo .tnnmodel
Estos 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/src
bajo el directorio , creé un directorio bajo el directorio raíz, que incluye dos archivos.TNNImageClassify
demo
my_cnn_model
my_conv.cpp
CMakeLists.txt
my_conv.cpp
El 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 shape
es (1, 1, 4, 4)
, la convolución shape
es (1, 1, 3, 3)
y la salida de convolución shape
es (1, 1, 2, 2)
, específicamente:
El resultado de la operación es el siguiente:
Además CMakeLists.txt
de agregar este código de muestra my_conv.cpp
, se agrega al archivo la clasificación oficial de la imagen y sus dependencias.El demo
contenido TNNImageClassify.cc
especí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 tnn
el 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 layer
y la inicialización de los parámetros, el intérprete del modelo y tnn
la inicialización, de la siguiente manera:
4.1 Construcción de un modelo (capa convolucional única)
source/tnn/interpreter/tnn/model_interpreter.cc
Se ha agregado una nueva función en el archivo A ModelInterpreter::MyInterpret()
diferencia de la ModelInterpreter::Interpret(std::vector<std::string> ¶ms)
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.h
source/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.cc
Se 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.cc
el 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 tnn
poder 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.cc
el 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.h
la declaración de esta función debe agregarse al archivo:
// tnn.h文件中的MyInit
Status MyInit(ModelConfig& config);
CMakeLists.txt
Hasta 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
El tamaño del modelo de punto flotante es 98M
, y el modelo de punto fijo cuantificado es 26M
:
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 128
cuantización con una imagen, por lo que la pérdida de precisión es grande y el resultado del razonamiento no es correcto:
Se cambió 1000
la cuantificación de una imagen, feature map
se adopta el método de cuantificación del método KL
, weight
se adopta el método del método MIN_MAX/ADMM
y también se reemplaza la imagen de prueba, y los resultados del razonamiento no funcionan:
5.2 Proceso de cuantificación
TNN
Min-Max
El 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:KL
weight
ADMM
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 channel
los 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_channel
para 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 map
El propósito de las MIN_MAX
dos estrategias de cuantificación involucradas aquí KL_DIVERGENCE
es encontrar un umbral adecuado threshold
.
MIN_MAX
Estrategia 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 NVIDIA
es consistente con lo dado en el informe, como se muestra en la siguiente figura:
KL_DIVERGENCE
Estrategia 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 map
La scale
información relevante también se almacenará en LayerResource
el objeto. En comparación con la capa convolucional LayerResource
, aquí están blob
los datos, y el resource_map
nombre 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 weight
la entrada , en concreto:feature map
scale
// 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
TNN
Hay dos tipos de cuantificación de convolución en: MIN_MAX
estrategia de cuantificación y ADMM
estrategia de cuantificación.
MIN_MAX
Estrategia 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_MAX
La 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 * 127
obtenido 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 scale
se zero_point
guarda en la layer
actual 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
bias
El resultado cuantificado se divide por bias
coma weight
flotante 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 x86
con arm
el 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_scale
puede obtener la salida de punto flotante de la convolución 浮点输出 / output_scale
(es decir, la siguiente layer
), input_scale
obtener 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 im2col
dar algunos im2col
diagramas 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)
La entrada 6.2 es multicanal, el peso es de un solo canal (salida)
La entrada 6,3 es multicanal, el peso es multicanal (salida)
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 8bit
tienen seguimiento Luego implemente 8bit
el código de implementación de la inferencia de convolución de punto fijo suplementario.