記事ディレクトリ
序文
最近 Tencent のTNN
ニューラル ネットワーク推論フレームワークを調査したので、このブログでは主にTNN
基本アーキテクチャ、モデルの定量化、手動実装x86
、arm
デバイス上でのシングル オペレーター畳み込み推論について紹介します。
1 はじめに
TNN
これは、Tencent Youtu Lab によってオープンソース化された高性能かつ軽量のニューラル ネットワーク推論フレームワークであり、クロスプラットフォーム、高性能、モデル圧縮、コード プルーニングなどの優れた利点も数多く備えています。TNN
このフレームワークは、オリジナルのフレームワークをベースとして、モバイル デバイスのサポートとパフォーマンスの最適化をRapidnet
さらにncnn
強化すると同時に、業界の主流であるオープンソース フレームワークの高いパフォーマンスと優れたスケーラビリティの特性から教訓を引き出し、モバイル デバイスのサポートを拡大します。背景とX86
枠組みNV GPU
。モバイル端末は、TNN
携帯電話QQ
、Weishi、地図などP
の多くのアプリケーションに導入されており、同サービスはTNN
Tencent CloudAI
の基本的な高速化フレームワークとして、多くのビジネスの導入を加速するサポートを提供してきました。
TNN
オープンソースのアドレス: https://github.com/Tencent/TNN
2. クイックスタート
2.1 onnx から tnn
&emsp はTNN
現在ONNX
、PyTorch
、TensorFlow
、などTesorFlow-Lite
を含む、業界の主流のモデル ファイル形式をサポートしています。Caffe
上の図に示すように、オープン ソース コミュニティの助けを借りて、複数のモデル ファイル形式をサポートするための中間層として使用されTNN
ます。、 、などのモデル ファイル形式を変換する場合は、まず、対応するモデル変換ツールを使用してさまざまなモデル形式をモデル、モデルをモデル必要があります。ONNX
ONNX
PyTorch
TensorFlow
Caffe
TNN
ONNX
ONNX
TNN
convert2tnn
変換 ツールのインストールとコンパイルの手順を簡素化するために、公式はdocker
イメージを使用することを推奨しています。
# 建议直接从 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
さらに、ONNX
ダウンロードTNN
ツールを確認します。
docker run -it tnn-convert:latest python3 ./converter.py onnx2tnn -h
特定のパラメータについてはあまり詳しく説明しません。公式ドキュメントを参照してください。
この例では、これをResnet50
例として次のtnn
形式に変換します。
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 ターゲット プラットフォームの TNN エンジンをコンパイルする
コンパイルに関する注意事項については公式ドキュメントを参照してください。
arm-linux
プラットフォームのコンパイル:
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
プラットフォームのコンパイル:
cd scripts
./build_linux_native.sh
2.3 コンパイルされた TNN エンジンを使用した推論
上記の例では、特定の例をコンパイルせず、x86
プラットフォームの各タスクでエンジンをコンパイルしますTNN
。
# x86平台编译
cd examples/linux/x86
./build_linux_native.sh
# arm-linux交叉编译
# cd examples/linux/cross
# ./build_aarch64_linux.sh
画像分類タスクを実行します。
./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
推論結果も正しいです。
各タスクのソース コードの場所:
examples/linux/src
3. シングルオペレーター畳み込み推論 (浮動小数点) を手動で実装する
TNN
フレームワークはニューラル ネットワーク推論インスタンスを構築し、モデル構造ファイル.tnnproto
とモデル重みファイルの.tnnmodel
2 つのファイルを入力する必要があります。ただし、特殊なニーズがあるため、このファイル方法は適していないため、ここではモデル ファイルに依存せずに手動でモデル構造を作成する例を示します。
examples/linux/src
ディレクトリの下のTNNImageClassify
画像の分類に従って 、ルート ディレクトリの下にと2 つのファイルを含むディレクトリdemo
を作成しました。my_cnn_model
my_conv.cpp
CMakeLists.txt
my_conv.cpp
ファイルの内容は次のとおりです。
// 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;
}
shape
具体的には、 畳み込みの入力は、(1, 1, 4, 4)
畳み込みshape
は、(1, 1, 3, 3)
畳み込みの出力はshape
です(1, 1, 2, 2)
。
操作の結果は次のようになります。
CMakeLists.txt
このサンプルコードの追加に加えて 、公式の画像分類とその依存関係がファイルmy_conv.cpp
に追加されます。具体的な内容は次のとおりです。demo
TNNImageClassify.cc
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. コード分析
公式API の手順によると、ニューラル ネットワークを実行するには 5 つの手順が必要です。
# 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);
モデル解析の最初のステップではファイル操作が行われますが、理論的にはモデルファイルが他のモデルで変換された形式に従って記述されていれば、tnn
ソースコードを変更する必要はありません。ここでは読み取れないので、ソースコードを直接変更します。ソース コード分析後、手動でモデルを構築するには、主に次のように、
ニューラル ネットワーク モデルの各層を構築しlayer
、パラメーターの初期化、モデル インタープリター、および初期化を完了する必要があります。tnn
4.1 モデルの構築 (単一畳み込み層)
source/tnn/interpreter/tnn/model_interpreter.cc
新しい関数がファイルに追加されました 。ModelInterpreter::MyInterpret()
公式ModelInterpreter::Interpret(std::vector<std::string> ¶ms)
関数とは異なり、この関数はファイルからモデルの構造と重みを解析する必要はありません。
// 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;
}
これに応じて、この関数の宣言を 3 つのファイルに追加する必要があります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 インタプリタの構築
source/tnn/core/tnn_impl_default.cc
新しい関数がファイルに追加され 、TNNImplDefault::MyInit(ModelConfig& config)
関数の実装は公式関数とほぼ同じですTNNImplDefault::Init(ModelConfig& config)
が、ここでインタープリターを構築するときに関数が使用される点が異なります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)
関数の実装はsource/tnn/core/tnn_impl.cc
次のファイルにあります。
Status TNNImpl::MyInit(ModelConfig &config) {
model_config_.model_type = config.model_type;
return TNN_OK;
}
これに応じて、この関数の宣言を 2 つのファイルに追加する 必要があります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 tnnの初期化
私たちの方法に従って正しく初期化できるようにするには、初期化用の公式関数を置き換える関数を、具体的にはファイル内にtnn
追加する必要があります。TNN::MyInit(ModelConfig& config)
TNN::Init(ModelConfig& config)
source/tnn/core/tnn.cc
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);
}
これに応じて、include/tnn/core/tnn.h
この関数の宣言をファイルに追加する必要があります。
// tnn.h文件中的MyInit
Status MyInit(ModelConfig& config);
CMakeLists.txt
ここまでで、シングル オペレーター畳み込み推論の手動構築に必要な要素が完成しました。この例のコード ディレクトリを ルート ディレクトリのファイルに追加してコンパイルします。
add_subdirectory(my_cnn_model)
5. モデルの量子化
5.1 量子化ツールをコンパイルする
# 编译
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
浮動小数点モデルのサイズは98M
、量子化された固定小数点モデルは次のとおりです26M
。
量子化モデルによる推論:
./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
ここでは128
画像を量子化しているだけなので、精度の損失が大きく、推論結果は正しくありません。
1000
画像の量子化を 変更し、feature map
その方法の量子化方法が採用されKL
、weight
その方法の方法が採用されMIN_MAX/ADMM
、テスト画像も置き換えられ、推論結果が機能しません。
5.2 定量化プロセス
TNN
Min-Max
量子化方法はデフォルトで使用されます。さらに、量子化方法もサポートされていますfeature map
。具体的な量子化プロセスは次のとおりです。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")
/*保存量化模型*/
それぞれの最大値と最小値range_per_channel_
を表します: 、。channel
first(min)
second(max)
定量化されたソース コードの場所は次のとおりです
tools/quantization
。
5.3 特徴マップの量子化
5.3.1 range_per_channel_ の計算
次の方法で、すべての l (を含む)per_channel
の最大値と最小値を計算します。feature map
input/output
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 チャネルごとの間隔の計算
// 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 チャネルごとのdistribute_の計算
ここに含まれる2 つの定量化戦略feature map
の目的は、適切なしきい値を見つけることです。定量化戦略:MIN_MAX
KL_DIVERGENCE
threshold
MIN_MAX
// 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;
要約すると、次のとおりです。scale = max[abs(r_min), abs(r_max)] / 127.0
これは、NVIDIA
次の図に示すように、レポートに記載されている内容と一致しています。
KL_DIVERGENCE
定量化戦略:
// 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 スケールの計算と保存
feature map
関連情報もオブジェクトscale
に保存されます。畳み込み層と比較すると、データは次のとおりです。対応する名前は、具体的には次のとおりです。LayerResource
LayerResource
blob
resource_map
xxx_scale_data_
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 重みの量子化
5.4.1 前処理
重みは量子化される前に、weight
入力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 重みの定量化戦略
TNN
の畳み込み量子化には、MIN_MAX
量子化ストラテジとADMM
量子化ストラテジの 2 種類があります。
MIN_MAX
定量化戦略:
// 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
定量化戦略は次のように要約できます。 、weight_int8 = (weight_float * input_scale) / max_val_abs * 127
取得される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 スケールストレージ
量子化された畳み込みの場合weight
、合計は現在の合計に保存されscale
ます。zero_point
layer
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 バイアス量子化
bias
量子化された結果は浮動bias
小数点で除算されます。weight
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 8bit推論処理
現在の畳み込み層の情報が次であると仮定します。
# input: (1, 3, 224, 224)
# conv: (32, 3, 3, 3)
# output: (1, 32, 222, 222)
x86
8 ビット畳み込み推論と 組み合わせるとarm
、次のような要約が得られます。
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);
要約すると(input_data_int32 * weight_data_int32 + bias) * weight_scale
、畳み込みの浮動小数点出力浮点输出 / output_scale
(つまり、次の出力layer
)を取得しinput_scale
、畳み込みの量子化出力を取得して、それを に制限できます[-128. 127]
。
6. Im2col は畳み込み計算を実装します
ハードウェアの特定の実装に応じて、ほとんどの畳み込み計算は行列乗算に変換されます(GEMM)
。最も一般的に使用される方法は、以下に畳み込み計算の例をim2col
示す図です。このブログと合わせて読むと良いでしょう。im2col
6.1 入力は単一チャンネル、重みは単一チャンネル (出力)
6.2 入力はマルチチャンネル、重みはシングルチャンネル (出力)
6.3 入力はマルチチャンネル、重みはマルチチャンネル(出力)
結論
このブログでは、TNN の基本的な使い方、量子化ツールの使用法、およびシングルオペレーター畳み込み推論の手動実装を主に紹介します。浮動小数点畳み込み推論に加えて、固定小数点畳み込み推論も実装されていますが、現在の結果8bit
はフォローアップ 次に、8bit
補足的な固定小数点畳み込み推論の実装コードを実装します。