实践教程|旋转目标检测模型-TensorRT 部署(C++)

简介: 实践教程|旋转目标检测模型-TensorRT 部署(C++)

仓库地址(TensorRT,ncnn)github.com/Crescent-Ao/GGHL-Deployment:

https://github.com/Crescent-Ao/GGHL-Deployment

这次工程部署主要选择了比较熟悉的旋转选择框架-GGHL。如果没有特殊算子的检测框架,依然可以使用下面的这个Pipeline, 旋转目标检测主要分成五参数和八参数的表征方法,分别对应的 x,y,w,h,θ, x,y,w,h,\theta, x,y,w,h,\theta,.以及对应的八参数的转化求法 x1,y1,x2,y2,x3,y3,x4,y4x_1,y_1,x_2,y_2,x_3,y_3,x_4,y_4x_1,y_1,x_2,y_2,x_3,y_3,x_4,y_4 。这两种方式在后处理的时候可以互相转换,我们这里选择后者。

TensorRT安装

找到TensorRT的下载地址,我这边选择的是TensorRT8,TensorRT支持以下几种方式安装,分别是deb,tar,rpm。我这边的系统是Ubuntu 22.04,CUDA版本是11.6,选择的安装方式是tar进行安装。

TensorRT地址developer.nvidia.com/tensorrt-getting-started:

https://developer.nvidia.com/tensorrt-getting-started

关于tar的安装方式,可以参考Nvidia官方文档中关于tar的部分。NVIDIA Deep Learning TensorRT Documentation(https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html%23installing-tar)关于tar的安装方式,可以参考Nvidia官方文档中关于tar的部分。这部分会说的比较详细。

https://docs.nvidia.com/deeplearning/tensorrt/install-guide/index.html%23installing-tar

然后需要配置CUDA和TensorRT环境变量以及代码仓库的地址,下面是我zshrc的详细配置,包括TensorRT、CUDA、以及OpenCV。

zshrc

在安装好之后,我们需要对TensorRT进行编译,记住TensorRT的安装位置,我这边是/home/crescent/TensorRT-8.4.15,在命令函数输入下面的命令。

cd /home/crescent/TensorRT-8.4.15
 mkdir -p build && cd build
 cmake .. -DTRT_LIB_DIR=$TRT_LIBPATH -DTRT_OUT_DIR=`pwd`/out
 make -j$(nproc)

等待一段时间完成编译后,如果不报错,那么按照英伟达github上官方SampleMnist的测试来检查你的TensorRT是否安装成功。出现下面的结果,代表之前的安装策略都没有错,可以进行下一步,否则要仔细检查编译阶段的问题,可以去stackoverflow找到相应的解决方法。

&&&& RUNNING TensorRT.sample_mnist # ./sample_mnist
[I] Building and running a GPU inference engine for MNIST
[I] Input:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@#-:.-=@@@@@@@@@@@@@@
@@@@@%=     . *@@@@@@@@@@@@@
@@@@% .:+%%%  *@@@@@@@@@@@@@
@@@@+=#@@@@@# @@@@@@@@@@@@@@
@@@@@@@@@@@%  @@@@@@@@@@@@@@
@@@@@@@@@@@: *@@@@@@@@@@@@@@
@@@@@@@@@@- .@@@@@@@@@@@@@@@
@@@@@@@@@:  #@@@@@@@@@@@@@@@
@@@@@@@@:   +*%#@@@@@@@@@@@@
@@@@@@@%         :+*@@@@@@@@
@@@@@@@@#*+--.::     +@@@@@@
@@@@@@@@@@@@@@@@#=:.  +@@@@@
@@@@@@@@@@@@@@@@@@@@  .@@@@@
@@@@@@@@@@@@@@@@@@@@#. #@@@@
@@@@@@@@@@@@@@@@@@@@#  @@@@@
@@@@@@@@@%@@@@@@@@@@- +@@@@@
@@@@@@@@#-@@@@@@@@*. =@@@@@@
@@@@@@@@ .+%%%%+=.  =@@@@@@@
@@@@@@@@           =@@@@@@@@
@@@@@@@@*=:   :--*@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[I] Output:
0:
1:
2:
3: **********
4:
5:
6:
7:
8:
9:
&&&& PASSED TensorRT.sample_mnist # ./sample_mnist

完成TensorRT的安装后,我们开始下面的部署工作。

模型的中间表达的转换

GGHL只涉及样本分配的策略,这个不会给整体的模型增加任何参数和复杂度,也非常简单。采用的主要架构就是YoloV3中策略,包括DarkNet,FPN+PANet,解耦头等等。有兴趣的话,可以查看相关的代码解读,这里简要概括。这些你都可以不看,我们只要它的权重文件就可。

GGHL主要策略


直接从release下载对应的权重文件,我们需要将这个转换成ONNX等TensorIR等中间表达方式。给出下面的代码,转换过程中,由于结构附带解码头,所以实际获得输出是三个FPN输出和最后一个Concat输出的结构,也就是四个输出name,我们需要将这剔除前面三个输出头。最后利用onnxsim将一些胶水算子简化掉,下面我们用Netron可视化这网络的结构。

def main():
    args = make_parser().parse_args()
    logger.info("args value: {}".format(args))
    
    print("loading weight file from :{}".format(args.weight_path))
    model = GGHL().eval()
    ckpt = torch.load(os.path.join(args.weight_path),map_location='cpu')
    model.load_state_dict(ckpt)
    logger.info("loading checkpoint done")
    dummy_input = torch.randn(1,3,cfg.TEST["TEST_IMG_SIZE"],cfg.TEST["TEST_IMG_SIZE"])
    dummy_output = model(dummy_input)
    torch.onnx.export(model,
                      dummy_input,
                      args.onnx_filename,
                      input_names=[args.input],
                      output_names=[args.output,args.output2,'demo','output'],
                      opset_version = 11)
    # Network has dynamic or shape inputs, but no optimization profile has been defined.
    # 将onnx导入之后,需要剔除三个输出节点
    model_onnx = onnx.load(args.onnx_filename)
    graph = model_onnx.graph
    out = graph.output
    for i in range(len(graph.output)-1):
        graph.output.remove(out[0])
    #去除掉上面三个的头节点,转换成
    onnx.checker.check_model(model_onnx)
    onnx.save(model_onnx,args.onnx_filename)
    logger.info("generate onnx name {}".format(args.onnx_filename))
    if(args.onnxsim):
        model = onnx.load("GGHL.onnx")
        model_sim,chenck = onnxsim.simplify(model)
        logger.info("generate onnx_sim name {}".format("GGHL_sim.onnx"))
        onnx.save(model_sim,"GGHL_sim.onnx")
    if(args.jittrace):
        model = model.train()
        jit_model = torch.jit.trace(model,dummy_input)
        torch.jit.save(jit_model,'GGHL_jit.pt')
        logger.info("generate torchscripy format{}".format("GGHL_jit.pth"))

下面是GGHL中三个解码头的部分,涉及了大量胶水算子,但是可以省去后续C++解码的部分。我们直接获得也就是这种带有解码头的结构。至于不带有解码头的结构,我们会在后续的NCNN中实现,敬请期待(Nihui大佬TQL)。

C++

转换结束后,我们会获得GGHL.onnx,GGHL_sim.onnx,GGHL.jit,这三个都是TensorIR,不同的框架支持有所不同。直接利用torch.onnx.export就可以直接转换,相关的学习文件可以参考MMDeploy中有关的介绍性文章,收益良多。

接下来我们需要将模型转换成TensorRT需要的范式。至于后面的onnx->Trt。请根据onnx-tensorrt中安装好。可能需要安装,protobuf,onnx等一系列工具,按照README安装就好。


C++ 部署实现

本文只介绍C++的部署的实现,Python版本中也有实现(这个和权重模型加载后再进行推理差不多)。由于TensorRT几乎每一步都需要传入Logger这个类,为了简要的实现,我们使用Nvidia官方示例中的samplelogger这个类。下面介绍一下main.cpp中类中成员函数,和全部的流程。

class GGHLONNX
{
public:
    GGHLONNX(const string &onnx_file, const string &engine_file) : m_onnx_file(onnx_file), m_engine_file(engine_file){};
    vector<float> prepareImage(const cv::Mat &img);
    bool onnxToTRTModel(nvinfer1::IHostMemory *trt_model_stream);
    bool loadEngineFromFile();
    void doInference(const cv::Mat &img);
private:
    const string m_onnx_file;
    const string m_engine_file;
    samplesCommon::Args gArgs;
    nvinfer1::ICudaEngine *m_engine;
    bool constructNetwork(nvinfer1::IBuilder *builder, nvinfer1::INetworkDefinition *network, nvinfer1::IBuilderConfig *config, nvonnxparser::IParser *parser);
    bool saveEngineFile(nvinfer1::IHostMemory *data);
    std::unique_ptr<char[]> readEngineFile(int &length);
    int64_t volume(const nvinfer1::Dims &d);
    unsigned int getElementSize(nvinfer1::DataType t);
};

GGHLONNX类主要负责TensorRT两个阶段分别是Builder Phase, Runtime Phase。下面介绍一下这两个阶段的作用。

TensorRT阶段的最高级别接口为Builder,Builder用来负责优化一个模型,并且产生一个Engine

  • 构建一个网络的定义
  • 修改Builder的配置
  • 调用Builder返回引擎

NetworkDefinition接口用来定义模型,最通用的方式转化一个模型至TensorRT是用onnx中间格式输出网络,TensorRT onnx解释器填充网络的定义。必须定义网络的输入和输出。未标记的输出的张量由构建器优化掉的瞬态值,输入和输出张量必须命名,所以在Runtime的时候,TensorRT知道如何将对应的缓存给到模型的定义。

BuilderConfig被用来如何优化的模型,TensorRT可以在降低计算精度的能力,控制内存和运行时的执行速度之间进行权衡,和限制CUDA内核的选择。Builder在完成网络定义和BuilderConfig之后,可以消除无效计算,折叠张量,重新排序在GPU更加高效地计算。可以选择降低浮点运算的精度,FP16,或者量化至INT8。还可以使用不同数据格式对每一层进行实现记录模型的最佳时间。构建序列化形式的引擎过程称为a plan, 反序列的话可以保存至磁盘。

NetworkDefinition接口用来定义模型,最通用的方式转化一个模型至TensorRT是用onnx中间格式输出网络,TensorRT onnx解释器填充网络的定义。必须定义网络的输入和输出。未标记的输出的张量由构建器优化掉的瞬态值,输入和输出张量必须命名,所以在Runtime的时候,TensorRT知道如何将对应的缓存给到模型的定义。1BuilderConfig被用来如何优化的模型,TensorRT可以在降低计算精度的能力,控制内存和运行时的执行速度之间进行权衡,和限制CUDA内核的选择。Builder在完成网络定义和BuilderConfig之后,可以消除无效计算,折叠张量,重新排序在GPU更加高效地计算。可以选择降低浮点运算的精度,FP16,或者量化至INT8。还可以使用不同数据格式对每一层进行实现记录模型的最佳时间。构建序列化形式的引擎过程称为a plan, 反序列的话可以保存至磁盘。

bool GGHLONNX::onnxToTRTModel(nvinfer1::IHostMemory *trt_model_stream)
{
    nvinfer1::IBuilder *builder = nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger());
    assert(builder != nullptr);
    const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
    nvinfer1::INetworkDefinition *network = builder->createNetworkV2(explicitBatch);
   //配置config
    nvinfer1::IBuilderConfig *config = builder->createBuilderConfig();
    config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE,1U<<30);
// 分配内存空间
    auto parser = nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger());
// 使用nvonnxparser 将network转换成 trt
// 利用construct  network 传入 config,network两个定义,完成buider的构建,并反序列化相应的engine文件,保存到磁盘上
    // 构建网络
    if (!constructNetwork(builder, network, config, parser))
    {
        return false;
    }
    trt_model_stream = m_engine->serialize();
//序列化engine文件
    nvinfer1::IHostMemory *data = builder->buildSerializedNetwork(*network,*config);
    saveEngineFile(data);
    delete config;
    delete network;
    delete parser;
    // m_engine->destroy();
    return true;
}

接下来是Runtime 阶段

runtime 需要执行下面的步骤

  • 反序列化来创建引擎
  • 从引擎中创建执行`context`
  • 从接口中填充输入缓存
  • 调用enqueue() or execute()函数 在执行context 来运行接口
    其中`Engine`接口表示优化模型,可以查询引擎获取有关网络的输入和输出的张量信息--维度/数据格式/数据类型。`ExecutionContext`接口,是调用推理的主要接口。包含特定关联的所有状态,可以包括单个`Engine`的上下文,并且并行运行他们。调用接口是,必须在合适的位置设置输入输出的缓存,依赖于数据的具体特性,来将他们放入相应的CPU和GPU ,输入输出缓存设置好了,接口调用同步进行推理或者一步排序等等。对应的GGHL中实现是这样。
void GGHLONNX::doInference(const cv::Mat &img)
{
    cv::Mat copy_img = img.clone();
    nvinfer1::IExecutionContext *context = m_engine->createExecutionContext();
    assert(context != nullptr);
    int nbBindings = m_engine->getNbBindings();
    assert(nbBindings == 2); // 输入和输出,一共是2个
    // 为输入和输出创建空间,参考CUDA中的cudaMalloc 和 CUDA freed等范式
    void *buffers[2];                 // 待创建的空间  为指针数组
    std::vector<int64_t> buffer_size; // 要创建的空间大小
    buffer_size.resize(nbBindings);
    for (int i = 0; i < nbBindings; i++)
    {
        nvinfer1::Dims dims = m_engine->getBindingDimensions(i);    // (3, 224, 224)  (1000)
        nvinfer1::DataType dtype = m_engine->getBindingDataType(i); // 0, 0 也就是两个都是kFloat类型
        // std::cout << static_cast<int>(dtype) << endl;
        int64_t total_size = volume(dims) * 1 * getElementSize(dtype);
        buffer_size[i] = total_size;
        CHECK(cudaMalloc(&buffers[i], total_size));
    }
    cudaStream_t stream;
    CHECK(cudaStreamCreate(&stream)); // 创建异步cuda流
    auto out_dim = m_engine->getBindingDimensions(1);
    auto output_size = 1;
    for (int j = 0; j < out_dim.nbDims; j++)
    {
        output_size *= out_dim.d[j];
    }
    float *out = new float[output_size];
    // 开始推理
    auto t_start = std::chrono::high_resolution_clock::now();
    vector<float> cur_input = prepareImage(img);
    auto t_end = std::chrono::high_resolution_clock::now();
    float duration = std::chrono::duration<float, std::milli>(t_end - t_start).count();
    std::cout << "loading image takes " << duration << "ms" << std::endl;
    if (!cur_input.data())
    {
        std::cout << "failed to prepare image" << std::endl;
    }
    // 将输入传递到GPU
    CHECK(cudaMemcpyAsync(buffers[0], cur_input.data(), buffer_size[0], cudaMemcpyHostToDevice, stream));
    // 异步执行
    t_start = std::chrono::high_resolution_clock::now();
    context->enqueueV2(&buffers[0],stream,nullptr);
   
    // 输出传回给CPU
    CHECK(cudaMemcpyAsync(out, buffers[1], buffer_size[1], cudaMemcpyDeviceToHost, stream));
    cudaStreamSynchronize(stream);
    //主机和设备相同步
    vector<float> original_result;
    for (int i = 0; i < output_size; i++)
    {
        original_result.push_back(*(out + i));
    }
    cout << output_size << endl;
    vector<OBBInfo> convert_middle;
    convert_result(original_result,convert_middle);
    cout << convert_middle.size() << endl;
    vector<OBBInfo8> middle;
//后处理的部分
    convert_pred(convert_middle, 800, 1024, BOX_CONF_THRESH,middle);
    vector<OBBInfo8> result;
//nms过程
    non_max_supression_8_points(middle, BOX_CONF_THRESH, NMS_THRESH, result);
    t_end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration<float, std::milli>(t_end - t_start).count();
    cout << duration << "time" << endl;
    cout << duration << "time" << endl;
    draw_objects(copy_img, result);
    cout << result.size() << endl;
    delete[] out;
}

Cpp版本的函数名称,同Python版本的函数相一致。8点的nms参考了DOTA Devkit的实现方式,同理也可以使用Opencv的旋转框nms实现,OBBDet.h为后处理主要的头文件,传入参数和传出参数,使用了引用和指针两种分配方式。由于这个部分的实现方式有些繁琐,这里就不详细叙述了,大致的流程同runtime流程相一致,大家可以去看代码的中的实现形式就ok。大致就是全部的实现的流程。

如何使用TensorRT版本的GGHL捏,需要准备cmake以及上述必要的package,CUDA和OpenCV可以查看其他的流程。

mkdir build
cd build
cmake ..
make

生成可执行文件后,并且指定需要的图片就可以获得了推理后的图片了。

nms之后

速度的话可以看下这里

  • FP-16 load image 16.32ms
  • FP-16+nms 48.32ms
  • FP-16 inference 5ms
相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
3月前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
58 2
|
20天前
|
机器学习/深度学习 人工智能 自然语言处理
C++构建 GAN 模型:生成器与判别器平衡训练的关键秘籍
生成对抗网络(GAN)是AI领域的明星,尤其在C++中构建时,平衡生成器与判别器的训练尤为关键。本文探讨了GAN的基本架构、训练原理及平衡训练的重要性,提出了包括合理初始化、精心设计损失函数、动态调整学习率、引入正则化技术和监测训练过程在内的五大策略,旨在确保GAN模型在C++环境下的高效、稳定训练,以生成高质量的结果,推动AI技术的发展。
46 10
|
5月前
|
存储 C++ Cloud Native
云原生部署问题之C++ 中的 nullptr 和 NULL 区别如何解决
云原生部署问题之C++ 中的 nullptr 和 NULL 区别如何解决
65 0
|
2月前
|
存储 C++
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
35 2
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
|
2月前
|
算法 数据挖掘 Shell
「毅硕|生信教程」 micromamba:mamba的C++实现,超越conda
还在为生信软件的安装配置而烦恼?micromamba(micromamba是mamba包管理器的小型版本,采用C++实现,具有mamba的核心功能,且体积更小,可以脱离conda独立运行,更易于部署)帮你解决!
76 1
|
2月前
|
存储 编译器 C语言
C++类与对象深度解析(一):从抽象到实践的全面入门指南
C++类与对象深度解析(一):从抽象到实践的全面入门指南
53 8
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
61 1
|
2月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
357 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
|
3月前
|
C++
c++继承层次结构实践
这篇文章通过多个示例代码,讲解了C++中继承层次结构的实践应用,包括多态、抽象类引用、基类调用派生类函数,以及基类指针引用派生类对象的情况,并提供了相关的参考链接。
|
5月前
|
程序员 编译器 C语言
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
云原生部署问题之C++中的nullptr相比C语言中的NULL优势如何解决
56 10
下一篇
DataWorks