Tencent TNN ニューラル ネットワーク推論フレームワークは、マルチデバイス シングル オペレーターの畳み込み推論を手動で実装します。

ここに画像の説明を挿入

序文

  最近 Tencent のTNNニューラル ネットワーク推論フレームワークを調査したので、このブログでは主にTNN基本アーキテクチャ、モデルの定量化、手動実装x86armデバイス上でのシングル オペレーター畳み込み推論について紹介します。

1 はじめに

  TNNこれは、Tencent Youtu Lab によってオープンソース化された高性能かつ軽量のニューラル ネットワーク推論フレームワークであり、クロスプラットフォーム、高性能、モデル圧縮、コード プルーニングなどの優れた利点も数多く備えています。TNNこのフレームワークは、オリジナルのフレームワークをベースとして、モバイル デバイスのサポートとパフォーマンスの最適化をRapidnetさらにncnn強化すると同時に、業界の主流であるオープンソース フレームワークの高いパフォーマンスと優れたスケーラビリティの特性から教訓を引き出し、モバイル デバイスのサポートを拡大します。背景とX86枠組みNV GPUモバイル端末は、TNN携帯電話QQ、Weishi、地図などPの多くのアプリケーションに導入されており、同サービスはTNNTencent CloudAIの基本的な高速化フレームワークとして、多くのビジネスの導入を加速するサポートを提供してきました。

ここに画像の説明を挿入

  TNNオープンソースのアドレス: https://github.com/Tencent/TNN

2. クイックスタート

2.1 onnx から tnn

ここに画像の説明を挿入

 &emsp はTNN現在ONNXPyTorchTensorFlowなどTesorFlow-Liteを含む、業界の主流のモデル ファイル形式をサポートしています。Caffe上の図に示すように、オープン ソース コミュニティの助けを借りて、複数のモデル ファイル形式をサポートするための中間層として使用されTNNます。、 、などのモデル ファイル形式を変換する場合は、まず、対応するモデル変換ツールを使用してさまざまなモデル形式をモデル、モデルをモデル必要があります。ONNXONNXPyTorchTensorFlowCaffeTNNONNXONNXTNN

ここに画像の説明を挿入

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とモデル重みファイルの.tnnmodel2 つのファイルを入力する必要があります。ただし、特殊なニーズがあるため、このファイル方法は適していないため、ここではモデル ファイルに依存せずに手動でモデル構造を作成する例を示します。

examples/linux/srcディレクトリの下のTNNImageClassify画像の分類に従って  、ルート ディレクトリの下にと2 つのファイルを含むディレクトリdemoを作成しましたmy_cnn_modelmy_conv.cppCMakeLists.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に追加されます。具体的な内容は次のとおりです。demoTNNImageClassify.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> &params)関数とは異なり、この関数はファイルからモデルの構造と重みを解析する必要はありません。

// 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.hsource/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 インタプリタの構築

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.hsource/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その方法の量子化方法が採用されKLweightその方法の方法が採用されMIN_MAX/ADMM、テスト画像も置き換えられ、推論結果が機能しません。

ここに画像の説明を挿入

5.2 定量化プロセス

  TNNMin-Max量子化方法はデフォルトで使用されます。さらに、量子化方法もサポートされていますfeature map具体的な量子化プロセスは次のとおりです。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")
/*保存量化模型*/

  それぞれの最大値と最小値range_per_channel_を表します: channelfirst(min)second(max)

  定量化されたソース コードの場所は次のとおりですtools/quantization

5.3 特徴マップの量子化

5.3.1 range_per_channel_ の計算

  次の方法で、すべての l (を含む)per_channel最大値と最小値を計算します。feature mapinput/outputchanne

// 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_MAXKL_DIVERGENCEthreshold
  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に保存されます。畳み込み層と比較すると、データは次のとおりです。対応する名前は、具体的には次のとおりです。LayerResourceLayerResourceblobresource_mapxxx_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_pointlayerresource

// 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小数点で除算されますweightscale

// 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)

x868 ビット畳み込み推論と  組み合わせると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補足的な固定小数点畳み込み推論の実装コードを実装します。

おすすめ

転載: blog.csdn.net/qq_42730750/article/details/130082300