使用TensorRT优化部署模型

写个笔记记录一下,感谢shouxieai。

1.使用TensorRT常见算子构建模型

1.1 构建一个input-->linear-->sigmoid-->output的简单网络

step1: 使用tensorRT部署模型的时候,必要的warning和info是必要的,在开始前先准备好logger类

#include <NvInfer.h>         // tensorRT include
#include <NvInferRuntime.h>  // 
#include <cuda_runtime.h>    // cuda include
#include <stdio.h>           // system include

class TRTLogger : public nvinfer1::ILogger{
public:
    virtual void log(Severity severity, nvinfer1::AsciiChar const* msg) noexcept override{
        if(severity <= Severity::kVERBOSE){
            printf("%d: %s\n", severity, msg);
        }
    }
};

step2: 加载tensorRT模型算子的权重参数,这里的权重参数格式需要被tensorRT识别,权重参数保存函数

nvinfer1::Weights make_weights(float* ptr, int n){
    nvinfer1::Weights w;
    w.count = n;     // 参数个数
    w.type = nvinfer1::DataType::kFLOAT;
    w.values = ptr;  // 权重参数的地址
    return w;
}

step3: 构建模型所需要的必要组件:

TRTLogger logger; // logger是必要的,用来捕捉warning和info等

//形象的理解是你需要一个builder去build这个网络,网络自身有结构,这个结构可以有不同的配置
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);

// 创建一个构建配置,指定TensorRT应该如何优化模型,tensorRT生成的模型只能在特定配置下运行
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();

// 创建网络定义,其中createNetworkV2(1)表示采用显性batch size,新版tensorRT(>=7.0)时,不建议采用0非显性batch size
// 因此贯穿以后,请都采用createNetworkV2(1)而非createNetworkV2(0)或者createNetwork
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1);

step4: 准备好输入,模型结构和输出的基本信息

const int num_input = 3;   // in_channel
const int num_output = 2;  // out_channel
float layer1_weight_values[] = {1.0, 2.0, 0.5, 0.1, 0.2, 0.5}; // 前3个给w1的rgb,后3个给w2的rgb 

float layer1_bias_values[]   = {0.3, 0.8};

//输入指定数据的名称、数据类型和完整维度,将输入层添加到网络
nvinfer1::ITensor* input = network->addInput("image", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4(1, num_input, 1, 1));

nvinfer1::Weights layer1_weight = make_weights(layer1_weight_values, 6);

nvinfer1::Weights layer1_bias   = make_weights(layer1_bias_values, 2);

//添加全连接层
auto layer1 = network->addFullyConnected(*input, num_output, layer1_weight, layer1_bias);      // 注意对input进行了解引用

//添加激活层 
auto prob = network->addActivation(*(layer1->getOutput(0)),nvinfer1::ActivationType::kSIGMOID); // 注意更严谨的写法是*(layer1->getOutput(0)) 即对getOutput返回的指针进行解引用
    
// 将我们需要的prob标记为输出
network->markOutput(*prob->getOutput(0));

printf("Workspace Size = %.2f MB\n", (1 << 28) / 1024.0f / 1024.0f); // 256Mib

step5: 配置模型文件

//TensorRT 7.1.0版本已弃用buildCudaEngine方法,统一使用buildEngineWithConfig方法

nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
if(engine == nullptr){
    printf("Build engine failed.\n");
    return -1;
}

step6: 对模型序列化保存并释放内存

// 将模型序列化,并储存为文件
nvinfer1::IHostMemory* model_data = engine->serialize();
FILE* f = fopen("engine.trtmodel", "wb");
fwrite(model_data->data(), 1, model_data->size(), f);
fclose(f);

// 卸载顺序按照构建顺序倒序
model_data->destroy();
engine->destroy();
network->destroy();
config->destroy();
builder->destroy();
printf("Done.\n");

1.2 构建一个input-->cnn-->relu-->output的简单网络

step1: 使用tensorRT部署模型的时候,必要的warning和info是必要的,在开始前先准备好logger类

#include <NvInfer.h>         // tensorRT include
#include <NvInferRuntime.h>  // 
#include <cuda_runtime.h>    // cuda include
#include <stdio.h>           // system include

class TRTLogger : public nvinfer1::ILogger{
public:
    virtual void log(Severity severity, nvinfer1::AsciiChar const* msg) noexcept override{
        if(severity <= Severity::kVERBOSE){
            printf("%d: %s\n", severity, msg);
        }
    }
};

step2: 加载tensorRT模型算子的权重参数,这里的权重参数格式需要被tensorRT识别,权重参数保存函数

nvinfer1::Weights make_weights(float* ptr, int n){
    nvinfer1::Weights w;
    w.count = n;     // 参数个数
    w.type = nvinfer1::DataType::kFLOAT;
    w.values = ptr;  // 权重参数的地址
    return w;
}

step3: 构建模型所需要的必要组件:

TRTLogger logger; // logger是必要的,用来捕捉warning和info等

//形象的理解是你需要一个builder去build这个网络,网络自身有结构,这个结构可以有不同的配置
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);

// 创建一个构建配置,指定TensorRT应该如何优化模型,tensorRT生成的模型只能在特定配置下运行
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();

// 创建网络定义,其中createNetworkV2(1)表示采用显性batch size,新版tensorRT(>=7.0)时,不建议采用0非显性batch size
// 因此贯穿以后,请都采用createNetworkV2(1)而非createNetworkV2(0)或者createNetwork
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1);

step4: 准备好输入,模型结构和输出的基本信息

const int num_input = 1;

const int num_output = 1;

float layer1_weight_values[] = {
    1.0, 2.0, 3.1, 
    0.1, 0.1, 0.1, 
    0.2, 0.2, 0.2
}; // 行优先

float layer1_bias_values[]   = {0.0};

// 如果要使用动态shape,必须让NetworkDefinition的维度定义为-1,in_channel是固定的
nvinfer1::ITensor* input = network->addInput("image", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4(-1, num_input, -1, -1));

nvinfer1::Weights layer1_weight = make_weights(layer1_weight_values, 9);

nvinfer1::Weights layer1_bias   = make_weights(layer1_bias_values, 1);

auto layer1 = network->addConvolution(*input, num_output, nvinfer1::DimsHW(3, 3), 
layer1_weight, layer1_bias);

layer1->setPadding(nvinfer1::DimsHW(1, 1));

auto prob = network->addActivation(*layer1->getOutput(0), 

nvinfer1::ActivationType::kRELU); // *(layer1->getOutput(0))
     
// 将我们需要的prob标记为输出
network->markOutput(*prob->getOutput(0));

int maxBatchSize = 10;
printf("Workspace Size = %.2f MB\n", (1 << 28) / 1024.0f / 1024.0f);
// 配置暂存存储器,用于layer实现的临时存储,也用于保存中间激活值
config->setMaxWorkspaceSize(1 << 28);

注释:

1. 这里的workspaceSize是工作空间大小,某些layer需要使用额外存储时,不会自己分配空间,而是为了内存复用,直接找tensorRTworkspace空间。

2. markOutput表示是该模型的输出节点,mark几次,就有几个输出,addInput几次就有几个输入。这与推理时相呼应

step5: 配置模型文件

//TensorRT 7.1.0版本已弃用buildCudaEngine方法,统一使用buildEngineWithConfig方法

nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
if(engine == nullptr){
    printf("Build engine failed.\n");
    return -1;
}

step6: 对模型序列化保存并释放内存

// 将模型序列化,并储存为文件
nvinfer1::IHostMemory* model_data = engine->serialize();
FILE* f = fopen("engine.trtmodel", "wb");
fwrite(model_data->data(), 1, model_data->size(), f);
fclose(f);

// 卸载顺序按照构建顺序倒序
model_data->destroy();
engine->destroy();
network->destroy();
config->destroy();
builder->destroy();
printf("Done.\n");

小结:

1. 从上面的两个简单的例子可以看出来,使用tensorRT构建模型的过程是固定的。按照这个固定流程就可以完成模型的构建过程。

2. 对于不同的模型的构建过程中,主要的变化是模型的输入,模型结构和输出。

2. 对构建后的模型进行推理

2.1 推理input-->linear-->sigmoid-->output模型

step1. 在推理之前,需要加载构建好的模型文件

vector<unsigned char> load_file(const string& file){
    ifstream in(file, ios::in | ios::binary);
    if (!in.is_open())
        return {};

    in.seekg(0, ios::end);
    size_t length = in.tellg();

    std::vector<uint8_t> data;
    if (length > 0){
        in.seekg(0, ios::beg);
        data.resize(length);

        in.read((char*)&data[0], length);
    }
    in.close();
    return data;
}

step2. 加载模型、创建runtime、对模型反序列化

TRTLogger logger;

auto engine_data = load_file("engine.trtmodel");

// 执行推理前,需要创建一个推理的runtime接口实例。与builer一样,runtime需要logger:
nvinfer1::IRuntime* runtime   = nvinfer1::createInferRuntime(logger);

// 将模型从读取到engine_data中,则可以对其进行反序列化以获得engine
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(engine_data.data(), engine_data.size());

if(engine == nullptr){
    printf("Deserialize cuda engine failed.\n");
    runtime->destroy();
    return;
}

step3. 创建执行上下文

nvinfer1::IExecutionContext* execution_context = engine->createExecutionContext();


cudaStream_t stream = nullptr;
// 创建CUDA流,以确定这个batch的推理是独立的
cudaStreamCreate(&stream);

step4. 准备好推理的数据,并且把数据搬运到GPU上。

float input_data_host[] = {1, 2, 3};

float* input_data_device = nullptr;

float output_data_host[2];

float* output_data_device = nullptr;

cudaMalloc(&input_data_device, sizeof(input_data_host));

cudaMalloc(&output_data_device, sizeof(output_data_host));

cudaMemcpyAsync(input_data_device, input_data_host, sizeof(input_data_host), 

cudaMemcpyHostToDevice, stream);

// 用一个指针数组指定input和output在gpu中的指针。
float* bindings[] = {input_data_device, output_data_device};

这里的bindings是tensorRT对输入输出张量的描述,bindings = input-tensor + output-tensor。比如inputaoutputb, c, d,那么bindings = [a, b, c, d]bindings[0] = abindings[2] = c。此时看到engine->getBindingDimensions(0)你得知道获取的是什么

step5. 对模型进行推理,并且把数据从GPU搬回CPU

bool success      = execution_context->enqueueV2((void**)bindings, stream, nullptr);
    
cudaMemcpyAsync(output_data_host, output_data_device, sizeof(output_data_host), 

cudaMemcpyDeviceToHost, stream);
    
cudaStreamSynchronize(stream);

    
printf("output_data_host = %f, %f\n", output_data_host[0], output_data_host[1]);


   
printf("Clean memory\n");
    
cudaStreamDestroy(stream);
    
execution_context->destroy();
    
engine->destroy();
    
runtime->destroy();

2.2 推理input-->cnn-->relu-->output模型

step1. 在推理之前,需要加载构建好的模型文件

vector<unsigned char> load_file(const string& file){
    ifstream in(file, ios::in | ios::binary);
    if (!in.is_open())
        return {};

    in.seekg(0, ios::end);
    size_t length = in.tellg();

    std::vector<uint8_t> data;
    if (length > 0){
        in.seekg(0, ios::beg);
        data.resize(length);

        in.read((char*)&data[0], length);
    }
    in.close();
    return data;
}

step2. 加载模型、创建runtime、对模型反序列化

TRTLogger logger;

auto engine_data = load_file("engine.trtmodel");

// 执行推理前,需要创建一个推理的runtime接口实例。与builer一样,runtime需要logger:
nvinfer1::IRuntime* runtime   = nvinfer1::createInferRuntime(logger);

// 将模型从读取到engine_data中,则可以对其进行反序列化以获得engine
nvinfer1::ICudaEngine* engine = runtime->deserializeCudaEngine(engine_data.data(), engine_data.size());

if(engine == nullptr){
    printf("Deserialize cuda engine failed.\n");
    runtime->destroy();
    return;
}

step3. 创建执行上下文

nvinfer1::IExecutionContext* execution_context = engine->createExecutionContext();


cudaStream_t stream = nullptr;
// 创建CUDA流,以确定这个batch的推理是独立的
cudaStreamCreate(&stream);

step4. 准备好推理的数据,并且把数据搬运到GPU上。

float input_data_host[] = {
    // batch 0
    1,   1,   1,
    1,   1,   1,
    1,   1,   1,

    // batch 1
    -1,   1,   1,
    1,   0,   1,
    1,   1,   -1
};
float* input_data_device = nullptr;

// 3x3输入,对应3x3输出
int ib = 2;
int iw = 3;
int ih = 3;
float output_data_host[ib * iw * ih];
float* output_data_device = nullptr;
cudaMalloc(&input_data_device, sizeof(input_data_host));
cudaMalloc(&output_data_device, sizeof(output_data_host));
cudaMemcpyAsync(input_data_device, input_data_host, sizeof(input_data_host), cudaMemcpyHostToDevice, stream);


    
// 明确当前推理时,使用的数据输入大小
    
execution_context->setBindingDimensions(0, nvinfer1::Dims4(ib, 1, ih, iw));
    
float* bindings[] = {input_data_device, output_data_device};

这里需要注意不同的推理的模型,预处理的数据是不一样的。注意需要把host数据移动到device上。这里都有固定的步骤。

step5. 对模型进行推理,并且把数据从GPU搬回CPU

bool success      = execution_context->enqueueV2((void**)bindings, stream, nullptr);
    
cudaMemcpyAsync(output_data_host, output_data_device, sizeof(output_data_host), 

cudaMemcpyDeviceToHost, stream);
    
cudaStreamSynchronize(stream);



    
for(int b = 0; b < ib; ++b){
    printf("batch %d. output_data_host = \n", b);
    for(int i = 0; i < iw * ih; ++i){
        printf("%f, ", output_data_host[b * iw * ih + i]);
        if((i + 1) % iw == 0)
            printf("\n");
    }
}

    
printf("Clean memory\n");
    
cudaStreamDestroy(stream);
    
cudaFree(input_data_device);
    
cudaFree(output_data_device);
    
execution_context->destroy();
    
engine->destroy();
    
runtime->destroy();

对于模型处理后的数据还需要根据模型的实际输出的含义对输出的数据解码

小结:

1. 通过两个简单的模型inference过程,使用tensorRT推理模型的流程基本是固定的

2. 对于不同的模型部署,输出的结果需要根据实际模型输出的含义解码得到需要的结果。

3. 使用nvonnxparser自动构建模型

step1. 必要的头文件和logger类

// tensorRT include
// 编译用的头文件
#include <NvInfer.h>

// onnx解析器的头文件
#include <NvOnnxParser.h>

// 推理用的运行时头文件
#include <NvInferRuntime.h>

// cuda include
#include <cuda_runtime.h>

// system include
#include <stdio.h>
#include <math.h>

#include <iostream>
#include <fstream>
#include <vector>

using namespace std;

inline const char* severity_string(nvinfer1::ILogger::Severity t){
    switch(t){
        case nvinfer1::ILogger::Severity::kINTERNAL_ERROR: return "internal_error";
        case nvinfer1::ILogger::Severity::kERROR:   return "error";
        case nvinfer1::ILogger::Severity::kWARNING: return "warning";
        case nvinfer1::ILogger::Severity::kINFO:    return "info";
        case nvinfer1::ILogger::Severity::kVERBOSE: return "verbose";
        default: return "unknow";
    }
}

class TRTLogger : public nvinfer1::ILogger{
public:
    virtual void log(Severity severity, nvinfer1::AsciiChar const* msg) noexcept override{
        if(severity <= Severity::kINFO){
            // 打印带颜色的字符,格式如下:
            // printf("\033[47;33m打印的文本\033[0m");
            // 其中 \033[ 是起始标记
            //      47    是背景颜色
            //      ;     分隔符
            //      33    文字颜色
            //      m     开始标记结束
            //      \033[0m 是终止标记
            // 其中背景颜色或者文字颜色可不写
            // 部分颜色代码 https://blog.csdn.net/ericbar/article/details/79652086
            if(severity == Severity::kWARNING){
                printf("\033[33m%s: %s\033[0m\n", severity_string(severity), msg);
            }
            else if(severity <= Severity::kERROR){
                printf("\033[31m%s: %s\033[0m\n", severity_string(severity), msg);
            }
            else{
                printf("%s: %s\n", severity_string(severity), msg);
            }
        }
    }
} logger;

step2. 使用tensorRT构建模型的必要三件套

TRTLogger logger;
    
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
    
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
    
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1);

step3. 通过onnx模型构建完整的模型结构

// 通过onnxparser解析的结果会填充到network中,类似addConv的方式添加进去
    
nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, logger);
    
if(!parser->parseFromFile("demo.onnx", 1)){
    printf("Failed to parser demo.onnx\n");

    // 注意这里的几个指针还没有释放,是有内存泄漏的,后面考虑更优雅的解决
    return false;
}

注释:使用IParser把onnx模型的结构以及参数自动填充到network中。

nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, logger)用来创建创建onnxparser,用来解析onnx文件。

parser->parseFromFile("demo.onnx", 1)这个是调用onnxparser的parseFromFile方法解析onnx文件。

step4. 后面步骤就是用来创建文件以及序列化保存文件

nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
if(engine == nullptr){
    printf("Build engine failed.\n");
    return false;
}

   
// 将模型序列化,并储存为文件
    
nvinfer1::IHostMemory* model_data = engine->serialize();
    
FILE* f = fopen("engine.trtmodel", "wb");
    
fwrite(model_data->data(), 1, model_data->size(), f);
    
fclose(f);

    
// 卸载顺序按照构建顺序倒序
    
model_data->destroy();
    
parser->destroy();
    
engine->destroy();
    
network->destroy();
    
config->destroy();
    
builder->destroy();
    
printf("Done.\n");

小结:使用tensorRT来部署模型的一条可行且较简单的通路是pt-->onnx-->engine。对于复杂的模型结构而言,如果每个网络层都使用算子写,模型的部署将变得非常不友好。这里的onnxparser::IParser是一个好的工具,不管onnx多么复杂,都可以自动把onnx的模型结构自动填充到network中。

4. 使用智能指针管理tensorRT构建器

在前面的部署过程中间,每次都需要对申请的内存进行释放。这不利于代码的简洁

下面我们使用cpp中的智能智能来管理TensorRT的构建过程。

4.1. 构建模型过程

#include "NvInfer.h"
#include "NvOnnxParser.h" // onnxparser头文件
#include "logger.h"
#include "common.h"
#include "buffers.h"
#include "cassert"


// =========== 1. 创建builder ===========
auto builder = std::unique_ptr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));
if (!builder)
{
    std::cerr << "Failed to create builder" << std::endl;
    return -1;
}

// ========== 2. 创建network:builder--->network ==========
// 显性batch
const auto explicitBatch = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
// 调用builder的createNetworkV2方法创建network
auto network = std::unique_ptr<nvinfer1::INetworkDefinition>(builder->createNetworkV2(explicitBatch));
if (!network)
{
    std::cout << "Failed to create network" << std::endl;
    return -1;
}


// 创建onnxparser,用于解析onnx文件
auto parser = std::unique_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger()));
// 调用onnxparser的parseFromFile方法解析onnx文件
auto parsed = parser->parseFromFile(onnx_file_path, static_cast<int>(sample::gLogger.getReportableSeverity()));
if (!parsed)
{
    std::cout << "Failed to parse onnx file" << std::endl;
    return -1;
}

// ========== 3. 创建config配置:builder--->config ==========
auto config = std::unique_ptr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
if (!config)
{
    std::cout << "Failed to create config" << std::endl;
    return -1;
}


// 设置精度,不设置是FP32,设置为FP16
config->setFlag(nvinfer1::BuilderFlag::kFP16);


// 设置最大工作空间(新版本的TensorRT已经废弃了setWorkspaceSize)
config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30);

// 创建流,用于设置profile
auto profileStream = samplesCommon::makeCudaStream();
if (!profileStream)
{
    return -1;
}
config->setProfileStream(*profileStream);

    
// ========== 4. 创建engine:builder--->engine(*nework, *config) ==========

// 使用buildSerializedNetwork方法创建engine,可直接返回序列化的engine(原来的buildEngineWithConfig方法已经废弃,需要先创建engine,再序列化)
auto plan = std::unique_ptr<nvinfer1::IHostMemory>(builder->buildSerializedNetwork(*network, *config));
if (!plan)
{
    std::cout << "Failed to create engine" << std::endl;
    return -1;
}

// ========== 5. 序列化保存engine ==========
std::ofstream engine_file("./weights/yolov5.engine", std::ios::binary);
assert(engine_file.is_open() && "Failed to open engine file");
engine_file.write((char *)plan->data(), plan->size());
engine_file.close();

4.2 推理模型过程

step1. 加载模型文件的函数

#include "NvInfer.h"
#include "NvOnnxParser.h"
#include "logger.h"
#include "common.h"
#include "buffers.h"

// 加载模型文件
std::vector<unsigned char> load_engine_file(const std::string &file_name)
{
    std::vector<unsigned char> engine_data;
    std::ifstream engine_file(file_name, std::ios::binary);
    assert(engine_file.is_open() && "Unable to load engine file.");
    engine_file.seekg(0, engine_file.end);
    int length = engine_file.tellg();
    engine_data.resize(length);
    engine_file.seekg(0, engine_file.beg);
    engine_file.read(reinterpret_cast<char *>(engine_data.data()), length);
    return engine_data;
}

step2. 推理过程


    // ========= 1. 创建推理运行时runtime =========
    auto runtime = std::unique_ptr<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger()));
    if (!runtime)
    {
        std::cout << "runtime create failed" << std::endl;
        return -1;
    }
    // ======== 2. 反序列化生成engine =========
    // 加载模型文件
    auto plan = load_engine_file(engine_file);
    // 反序列化生成engine
    auto mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(plan.data(), plan.size()));
    if (!mEngine)
    {
        return -1;
    }

    // ======== 3. 创建执行上下文context =========
    auto context = std::unique_ptr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());
    if (!context)
    {
        std::cout << "context create failed" << std::endl;
        return -1;
    }



//----------------这里使用buffers管理器来自动分配内存,同时把数据从CPU搬运到GPU----------------
    // ========== 4. 创建输入输出缓冲区 =========
    samplesCommon::BufferManager buffers(mEngine);
    process_input_cpu(frame, (float *)buffers.getDeviceBuffer(kInputTensorName));
//----------------这里使用buffers管理器来自动分配内存,同时把数据从CPU搬运到GPU----------------


// ---------------这里把模型的输出结果同样存放到buffers中,然后把数据从GPU搬运到CPU----------------
// ========== 5. 执行推理 =========
        context->executeV2(buffers.getDeviceBindings().data());
        // 拷贝回host
        buffers.copyOutputToHost();
// ---------------这里把模型的输出结果同样存放到buffers中,然后把数据从GPU搬运到CPU----------------

注意:这里的samplesCommon::BufferManager buffers(mEngine)是TensorRT光放实现的用来管理内存的方法类。mEngine是反序列化后的模型。

这里还不完全清楚,后面研究清楚后,会在后面的部署代码加上

step3:根据实际需求实现后面的解码部分

如果需要把模型的后处理部分在GPU上实现,先实现解码,然后把结果复制回到host,

如果需要把模型的后处理部分在host上实现,就先把数据复制到host,然后再解码。

不同模型的后处理逻辑不一样。。。。

5. 为模型的构建过程配置一些优化条件

5.1 动态模型的设置

动态模型通常指的是在channel维度上,不使用固定参数来优化模型,推理模型的过程

在构建模型的过程中,可以使用设置输入动态shape。

step1. 配置网络动态shape,从nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger)和nvinfer1::IBuilderConfig* config = builder->createBuilderConfig()这两个构建器设置

    nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
    nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();

step2. 为模型配置动态shape参数

config->setMaxWorkspaceSize(1 << 28);
    

// 如果模型有多个输入,则必须多个profile
    
auto profile = builder->createOptimizationProfile();

    
// 配置最小允许1 x 3 x 640 x 640
    
profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims4(1, 3, 640, 640));
    
profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims4(1, 3, 640 640));

    
// 配置最大允许10 x 3 x 640 x 640

    
profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims4(10, 3, 640, 640));
    
config->addOptimizationProfile(profile);

这里的input是需要获取到模型的输入节点的指针

nvinfer1::OptProfileSelector::kMIN、nvinfer1::OptProfileSelector::kOPT、nvinfer1::OptProfileSelector::kMAX这三个参数分别表示模型的动态输入的最小,最优最大shape.

更加完整的来设置模型的动态shape的优秀代码如下:

// 我们需要告诉tensorrt我们最终运行时,输入图像的范围,batch size的范围。这样tensorrt才能对应为我们进行模型构建与优化。
    auto input = network->getInput(0);                                                                             // 获取输入节点
    auto profile = builder->createOptimizationProfile();                                                           // 创建profile,用于设置输入的动态尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims4{1, 3, 640, 640}); // 设置最小尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims4{1, 3, 640, 640}); // 设置最优尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims4{1, 3, 640, 640}); // 设置最大尺寸

    // ========== 3. 创建config配置:builder--->config ==========
    auto config = std::unique_ptr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
    if (!config)
    {
        std::cout << "Failed to create config" << std::endl;
        return -1;
    }
    // 使用addOptimizationProfile方法添加profile,用于设置输入的动态尺寸
    config->addOptimizationProfile(profile);

通过以上方法就可以在构建模型的过程中,为模型配置好了动态shape。

5.2 优化模型精度的设置

通常,模型在训练时使用32位精度的权重。然而,在部署阶段,32位精度可能会影响推理速度。为此,TensorRT提供了16位半精度优化和INT8量化,以提升模型的运行效率。

深度学习量化就是将深度学习模型中的参数(例如权重和偏置)从浮点数转换成整数或者定点数的过程。这样做可以减少模型的存储和计算成本,从而达到模型压缩和运算加速的目的。如int8量化,让原来模型中32bit存储的数字映射到8bit再计算(范围是[-128,127])。

  • 加快推理速度:访问一次32位浮点型可以访问4次int8整型数据;
  • 减少存储空间和内存占用:在边缘设备(如嵌入式)上部署更实用。

当然,提升速度的同时,量化也会带来精度的损失,为了能尽可能减少量化过程中精度的损失,需要使用各种校准方法来降低信息的损失。TensorRT 中支持两种 INT8 校准算法:

  • 熵校准 (Entropy Calibration)
  • 最小最大值校准 (Min-Max Calibration)

熵校准是一种动态校准算法,它使用 KL 散度 (KL Divergence) 来度量推理数据和校准数据之间的分布差异。在熵校准中,校准数据是从实时推理数据中采集的,它将 INT8 精度量化参数看作概率分布,根据推理数据和校准数据的 KL 散度来更新量化参数。这种方法的优点是可以更好地反映实际推理数据的分布。

最小最大值校准使用最小最大值算法来计算量化参数。在最小最大值校准中,需要使用一组代表性的校准数据来生成量化参数,首先将推理中的数据进行统计,计算数据的最小值和最大值,然后根据这些值来计算量化参数。

这两种校准方法都需要准备一些数据用于在校准时执行推理,以统计数据的分布情况。一般数据需要有代表性,即需要符合最终实际落地场景的数据。实际应用中一般准备500-1000个数据用于量化。

 config->setFlag(nvinfer1::BuilderFlag::kFP16);

这行代码用于启用TensorRT的半精度 (FP16) 模型构建。使用半精度可以减少内存占用并提高推理速度,同时在许多情况下保持较好的精度。

int8量化需要额外添加校准器,在后面的文章中会详细介绍。

6. 模型推理部分的注意细节

6.1. 模型数据的搬运

TensorRT优化模型的过程是在cuda上使用tensor core来优化模型的不同网络层之间的矩阵乘积以及网络层的融合。即onnx模型在cuda上来优化模型的结构,但是输入模型的数据流如何在模型上进行运算呢。

在tensorRT中,使用bindings来处理模型的优化完成的模型的输入和输出部分,这里的bindings是tensorRT对输入输出张量的描述,bindings = input-tensor + output-tensor。比如inputaoutputb, c, d,那么bindings = [a, b, c, d]bindings[0] = abindings[2] = c。此时看到engine->getBindingDimensions(0)

使用bool success      = execution_context->enqueueV2((void**)bindings, stream, nullptr);这行代码,把模型的输入和输出全都存放到bindings内,然后对模型进行推理。

模型的输入和输出:

float* m_bindings[2];                                           
float* m_inputMemory[2];
float* m_outputMemory[2];


nvinfer1::Dims m_inputDims   = context_->getBindingDimensions(0);
    
nvinfer1::Dims m_outputDims  = context_->getBindingDimensions(1);

    
CUDA_CHECK(cudaStreamCreate(&m_stream));

    
int m_inputSize   = this->kInputH * this->kInputW * this->ImageC * sizeof(float);
    
int m_imgArea     = this->kInputH * this->kInputW;
    
int m_outputSize  = m_outputDims.d[0] * m_outputDims.d[1] * m_outputDims.d[2] * sizeof(float);

    
int rawImageSize = this->rawImageH * this->rawImageW * this->ImageC;
    
int m_rawImgArea   = this->rawImageH * this->rawImageW; 

    
// 这里对host和device上的memory一起分配空间
    
CUDA_CHECK(cudaMallocHost(&m_inputMemory[0], m_inputSize));
    
CUDA_CHECK(cudaMallocHost(&m_outputMemory[0], m_outputSize));
    
CUDA_CHECK(cudaMalloc(&m_inputMemory[1], m_inputSize));
    
CUDA_CHECK(cudaMalloc(&m_outputMemory[1], m_outputSize));

    
CUDA_CHECK(cudaMallocHost(&copyImageMemory, rawImageSize));

    
// 创建m_bindings,之后再寻址就直接从这里找
    
m_bindings[0] = m_inputMemory[1];
    
m_bindings[1] = m_outputMemory[1];

(1)申请模型输入内存大小,申请模型输出内存大小

(2)分别在host端和device端申请内存

(3)把申请的内存和m_bingdings匹配起来。

这里需要注意,对于模型的输入的前处理和模型输出的后处理部分,分别可以在device端和host端处理。不同的处理方式,数据的搬运也不一样,比如,模型的输入的前处理部分希望在host端部处理。此时,可以先对模型预处理后,把处理好的数据搬运到device上即可。如果需要把数据在device上处理数据,就是先把数据搬运到device端,使用kernel函数直接处理数据。对于模型的输出的后处理部分同样的处理方式。

6.2 对构建的模型推理方式

TensorRT模型的推理部分,推理的接口有同步推理和异步推理

// 同步推理
context->executeV2(buffers.getDeviceBindings().data());

//异步推理
bool success      = execution_context->enqueueV2((void**)bindings, stream, nullptr);

对于两种推理方式

同步推理

context->executeV2(buffers.getDeviceBindings().data());

  • 执行方式:调用该函数会阻塞当前线程,直到推理完成。
  • 优点
    • 简单易用,容易实现。
    • 适合需要立即获取结果的场景,比如实时推理。
  • 缺点
    • 可能导致CPU资源闲置,因为在推理完成之前,无法执行其他任务。

异步推理

bool success = execution_context->enqueueV2((void**)bindings, stream, nullptr);

  • 执行方式:该函数会立即返回,推理在后台进行,允许CPU继续执行其他任务。
  • 优点
    • 更高效,能够利用CPU和GPU并行执行,提高整体吞吐量。
    • 适合批量处理或需要处理多个请求的场景。
  • 缺点
    • 实现相对复杂,需要管理推理的生命周期和结果的同步。
    • 可能需要额外的同步机制来处理结果。
  • 总结
  • 选择同步推理:当需要简单实现且对延迟要求较高时。
  • 选择异步推理:当需要更好的性能和资源利用率,特别是在处理大量数据时。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值