从零开始 TensorRT(6)C++ 篇:原生 API 构建网络

前言

学习资料:
B站视频配套代码 cookbook

示例:TensorRT 原生 API 构建网络

参考源码:cookbook → 01-SimpleDemo → TensorRT8.5

直接运行官方源码时需要根据自身环境修改 xxx/cookbook/include/Makefile.inc 中的部分路径。

源码

  对参考源码进行了一些简化和修改:

  1. 去除从文件读取引擎部分
  2. 网络只有一个输入张量和一个输出张量,内存和显存操作部分去除了循环操作
  3. 使用 CMakeLists 替换 Makefile
  4. 提取 cookbookHelper.cuh 中需要用到的部分,改写为 log.h

(1)文件结构

.
└── SimpleDemo
    ├── CMakeLists.txt
    ├── log.h
    └── main.cpp

(2)log.h

#include <iostream>
#include <cmath>
#include <iomanip>
#include <NvInfer.h>
#include <cuda_fp16.h>


#define CHECK(call) check(call, __LINE__, __FILE__)

inline bool check(cudaError_t e, int iLine, const char *szFile)
{
    if (e != cudaSuccess)
    {
        std::cout << "CUDA runtime API error " << cudaGetErrorName(e) << " at line " << iLine << " in file " << szFile << std::endl;
        return false;
    }
    return true;
}

class Logger : public nvinfer1::ILogger
{
public:
    Severity reportableSeverity;

    Logger(Severity severity = Severity::kINFO):
        reportableSeverity(severity) {}

    void log(Severity severity, const char *msg) noexcept override
    {
        if (severity > reportableSeverity)
        {
            return;
        }
        switch (severity)
        {
        case Severity::kINTERNAL_ERROR:
            std::cerr << "INTERNAL_ERROR: ";
            break;
        case Severity::kERROR:
            std::cerr << "ERROR: ";
            break;
        case Severity::kWARNING:
            std::cerr << "WARNING: ";
            break;
        case Severity::kINFO:
            std::cerr << "INFO: ";
            break;
        default:
            std::cerr << "VERBOSE: ";
            break;
        }
        std::cerr << msg << std::endl;
    }
};


// get the size in byte of a TensorRT data type
__inline__ size_t dataTypeToSize(nvinfer1::DataType dataType)
{
    switch (dataType)
    {
    case nvinfer1::DataType::kFLOAT:
        return 4;
    case nvinfer1::DataType::kHALF:
        return 2;
    case nvinfer1::DataType::kINT8:
        return 1;
    case nvinfer1::DataType::kINT32:
        return 4;
    case nvinfer1::DataType::kBOOL:
        return 1;
    case nvinfer1::DataType::kUINT8:
        return 1;
    case nvinfer1::DataType::kFP8:
        return 1;
    default:
        return 4;
    }
}


// print data in the array
template<typename T>
void printArrayRecursion(const T *pArray, nvinfer1::Dims32 dim, int iDim, int iStart)
{
    if (iDim == dim.nbDims - 1)
    {
        for (int i = 0; i < dim.d[iDim]; ++i)
        {
            std::cout << std::fixed << std::setprecision(3) << std::setw(6) << double(pArray[iStart + i]) << " ";
        }
    }
    else
    {
        int nElement = 1;
        for (int i = iDim + 1; i < dim.nbDims; ++i)
        {
            nElement *= dim.d[i];
        }
        for (int i = 0; i < dim.d[iDim]; ++i)
        {
            printArrayRecursion<T>(pArray, dim, iDim + 1, iStart + i * nElement);
        }
    }
    std::cout << std::endl;
    return;
}

template<typename T>
void printArrayInformation(const T *pArray, nvinfer1::Dims32 dim, std::string name = std::string(""), bool bPrintInformation = true, bool bPrintArray = false, int n = 10)
{
    // print shape information
    std::cout << std::endl;
    std::cout << name << ": (";
    for (int i = 0; i < dim.nbDims; ++i)
    {
        std::cout << dim.d[i] << ", ";
    }
    std::cout << ")" << std::endl;

    // print statistic information of the array
    if (bPrintInformation)
    {
        int nElement = 1; // number of elements with batch dimension
        for (int i = 0; i < dim.nbDims; ++i)
        {
            nElement *= dim.d[i];
        }

        double sum      = double(pArray[0]);
        double absSum   = double(fabs(double(pArray[0])));
        double sum2     = double(pArray[0]) * double(pArray[0]);
        double diff     = 0.0;
        double maxValue = double(pArray[0]);
        double minValue = double(pArray[0]);
        for (int i = 1; i < nElement; ++i)
        {
            sum += double(pArray[i]);
            absSum += double(fabs(double(pArray[i])));
            sum2 += double(pArray[i]) * double(pArray[i]);
            maxValue = double(pArray[i]) > maxValue ? double(pArray[i]) : maxValue;
            minValue = double(pArray[i]) < minValue ? double(pArray[i]) : minValue;
            diff += abs(double(pArray[i]) - double(pArray[i - 1]));
        }
        double mean = sum / nElement;
        double var  = sum2 / nElement - mean * mean;

        std::cout << "absSum=" << std::fixed << std::setprecision(4) << std::setw(7) << absSum << ",";
        std::cout << "mean=" << std::fixed << std::setprecision(4) << std::setw(7) << mean << ",";
        std::cout << "var=" << std::fixed << std::setprecision(4) << std::setw(7) << var << ",";
        std::cout << "max=" << std::fixed << std::setprecision(4) << std::setw(7) << maxValue << ",";
        std::cout << "min=" << std::fixed << std::setprecision(4) << std::setw(7) << minValue << ",";
        std::cout << "diff=" << std::fixed << std::setprecision(4) << std::setw(7) << diff << ",";
        std::cout << std::endl;

        // print first n element and last n element
        for (int i = 0; i < n; ++i)
        {
            std::cout << std::fixed << std::setprecision(5) << std::setw(8) << double(pArray[i]) << ", ";
        }
        std::cout << std::endl;
        for (int i = nElement - n; i < nElement; ++i)
        {
            std::cout << std::fixed << std::setprecision(5) << std::setw(8) << double(pArray[i]) << ", ";
        }
        std::cout << std::endl;
    }

    // print the data of the array
    if (bPrintArray)
    {
        printArrayRecursion<T>(pArray, dim, 0, 0);
    }

    return;
}
template void printArrayInformation(const float *, nvinfer1::Dims32, std::string, bool, bool, int);
template void printArrayInformation(const half *, nvinfer1::Dims32, std::string, bool, bool, int);
template void printArrayInformation(const char *, nvinfer1::Dims32, std::string, bool, bool, int);
template void printArrayInformation(const int *, nvinfer1::Dims32, std::string, bool, bool, int);
template void printArrayInformation(const bool *, nvinfer1::Dims32, std::string, bool, bool, int);

(3)main.cpp

#include "log.h"
#include <fstream>
#include <vector>


const std::string trtFile {"./model.plan"};
static Logger gLogger(nvinfer1::ILogger::Severity::kERROR);

void run() {
    nvinfer1::ICudaEngine *engine = nullptr;
    // 构建期
    nvinfer1::IBuilder             *builder = nvinfer1::createInferBuilder(gLogger);
    nvinfer1::INetworkDefinition   *network = builder->createNetworkV2(1U << int(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
    nvinfer1::IOptimizationProfile *profile = builder->createOptimizationProfile();
    nvinfer1::IBuilderConfig       *config  = builder->createBuilderConfig();
    config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30);

    nvinfer1::ITensor *inputTensor = network->addInput("inputT0", nvinfer1::DataType::kFLOAT, nvinfer1::Dims32 {3, {-1, -1, -1}});
    profile->setDimensions(inputTensor->getName(), nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims32 {3, {1, 1, 1}});
    profile->setDimensions(inputTensor->getName(), nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims32 {3, {3, 4, 5}});
    profile->setDimensions(inputTensor->getName(), nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims32 {3, {6, 8, 10}});
    config->addOptimizationProfile(profile);

    nvinfer1::IIdentityLayer *identityLayer = network->addIdentity(*inputTensor);
    network->markOutput(*identityLayer->getOutput(0));
    // 生成序列化网络
    nvinfer1::IHostMemory *engineString = builder->buildSerializedNetwork(*network, *config);
    if (engineString == nullptr || engineString->size() == 0) {
        std::cout << "Failed building serialized engine!" << std::endl;
        return;
    }
    std::cout << "Succeeded building serialized engine!" << std::endl;

    // 运行期
    nvinfer1::IRuntime *runtime {nvinfer1::createInferRuntime(gLogger)};
    engine = runtime->deserializeCudaEngine(engineString->data(), engineString->size());
    if (engine == nullptr) {
        std::cout << "Failed building engine!" << std::endl;
        return;
    }
    std::cout << "Succeeded building engine!" << std::endl;

    // 保存引擎 <fstream>
    std::ofstream engineFile(trtFile, std::ios::binary);
    if (!engineFile) {
        std::cout << "Failed opening file to write" << std::endl;
        return;
    }
    engineFile.write(static_cast<char *>(engineString->data()), engineString->size());
    if (engineFile.fail()) {
        std::cout << "Failed saving .plan file!" << std::endl;
        return;
    }
    std::cout << "Succeeded saving .plan file!" << std::endl;

    long unsigned int nIO = engine->getNbIOTensors();
    std::vector<std::string> vTensorName(nIO);
    for (int i = 0; i < nIO; ++i) {
        vTensorName[i] = std::string(engine->getIOTensorName(i));
    }
        
    // GPU进程
    nvinfer1::IExecutionContext *context = engine->createExecutionContext();
    // 输入形状
    context->setInputShape(vTensorName[0].c_str(), nvinfer1::Dims32 {3, {3, 4, 5}});

    std::vector<int> vTensorSize(nIO, 0);
    for (int i = 0; i < nIO; ++i) {
        nvinfer1::Dims32 dim = context->getTensorShape(vTensorName[i].c_str());
        int size = 1;
        for (int j = 0; j < dim.nbDims; ++j) {
            size *= dim.d[j];
        }
        vTensorSize[i] = size * dataTypeToSize(engine->getTensorDataType(vTensorName[i].c_str()));
    }

    void* inputHost = (void *)new char[vTensorSize[0]];
    void* outputHost = (void *)new char[vTensorSize[1]];
    void* inputDevice;
    void* outputDevice;
    CHECK(cudaMalloc(&inputDevice, vTensorSize[0]));
    CHECK(cudaMalloc(&outputDevice, vTensorSize[1]));

    // 输入数据
    float *pData = (float *)inputHost;
    for (int i = 0; i < vTensorSize[0] / dataTypeToSize(engine->getTensorDataType(vTensorName[0].c_str())); ++i) {
        pData[i] = float(i);
    }
    // H2D
    CHECK(cudaMemcpy(inputDevice, inputHost, vTensorSize[0], cudaMemcpyHostToDevice));
    // 设置地址
    context->setTensorAddress(vTensorName[0].c_str(), inputDevice);
    context->setTensorAddress(vTensorName[1].c_str(), outputDevice);
    // 推理
    context->enqueueV3(0);
    // D2H
    CHECK(cudaMemcpy(outputHost, outputDevice, vTensorSize[1], cudaMemcpyDeviceToHost));

    printArrayInformation((float *)inputHost, context->getTensorShape(vTensorName[0].c_str()), vTensorName[0], true, true);
    printArrayInformation((float *)outputHost, context->getTensorShape(vTensorName[1].c_str()), vTensorName[1], true, true);
    
    delete[] (char *)inputHost;
    delete[] (char *)outputHost;
    CHECK(cudaFree(inputDevice));
    CHECK(cudaFree(outputDevice));
    return;
}

int main() {
    CHECK(cudaSetDevice(0));
    run();
    return 0;
}

(4)CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(SimpleDemo)

add_definitions(-std=c++11)

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

# cuda
include_directories(/usr/local/cuda/include)
link_directories(/usr/local/cuda/lib64)
# tensorrt
include_directories(xxx/TensorRT-8.6.1.6/include)
link_directories(xxx/TensorRT-8.6.1.6/lib)

add_executable(demo main.cpp)
target_link_libraries(demo nvinfer)
target_link_libraries(demo cudart)

源码解析

  本示例与 Python 示例 从零开始 TensorRT(2)Python 篇:原生 API 构建网络 对应,C++ 和 Python 中使用的 API 大同小异,流程也基本一致,因此不再对 main.cpp 中的 TensorRT 部分进行赘述。而对于 C++ 零基础而言,log.h 中代码的语法相对更难。

Logger

  日志记录器 Logger 在构建 TensorRT 部分是必不可少的,并且不像在 Python 中简单的使用 logger = trt.Logger(trt.Logger.ERROR) 而是要手动写一个类。

"Logger 类继承自 nvinfer1::ILogger, 并且继承方式为 public"
"当不使用 public 时默认为 private"
"其区别是使用 public 后 Logger 的实例对象 gLogger 可以访问基类 nvinfer1::ILogger 中的公有成员"
class Logger : public nvinfer1::ILogger
{
public:
    Severity reportableSeverity;
    
	"构造函数, 类每次实例化时运行, 类似Python中的__init__"
	"reportableSeverity(severity)是通过成员初始化列表来初始化成员, 例如: a(value_a), b(value_b)"
	"其效果类似直接在函数体内写赋值语句, 区别是会在创建对象时就初始化成员变量, 而非在执行构造函数时"
	"当成员变量是类的对象时, 使用成员初始化列表会更高效"
	"如果使用赋值语句需要先创建临时对象, 再将临时对象的值赋给成员变量, 会引入额外的构造函数调用和拷贝"
    Logger(Severity severity = Severity::kINFO):
        reportableSeverity(severity) {}

	"先看父类 nvinfer1::ILogger 中对 log 函数的定义"
	"virtual void log(Severity severity, AsciiChar const* msg) noexcept = 0;"
	"virtual 代表虚函数, 可以在子类中重写函数体"
	"=0 代表纯虚函数, 必须在子类中重写函数体, 因此在c++中使用TensorRT需要自定义logger类"
	"noexcept 代表此函数不会抛出异常, 可以帮助编译器减少优化损失"
	"子类中 override 代表函数是被重写的, 增加可读性"
    void log(Severity severity, const char *msg) noexcept override
    {
        if (severity > reportableSeverity)
        {
            return;
        }
        switch (severity)
        {
        case Severity::kINTERNAL_ERROR:
            std::cerr << "INTERNAL_ERROR: ";
            break;
        case Severity::kERROR:
            std::cerr << "ERROR: ";
            break;
        case Severity::kWARNING:
            std::cerr << "WARNING: ";
            break;
        case Severity::kINFO:
            std::cerr << "INFO: ";
            break;
        default:
            std::cerr << "VERBOSE: ";
            break;
        }
        std::cerr << msg << std::endl;
    }
};

CHECK

"宏定义"
"#define 宏名(参数) 具体函数调用"
"__LINE__, __FILE_ 为预处理器指令, 代表当前的行号和文件名"
#define CHECK(call) check(call, __LINE__, __FILE__)

"inline 代表内联函数, 编译器会把函数体内的代码插入到函数调用的地方, 而不是像普通函数一样通过函数调用"
"1.节省函数调用的开销, 包括参数传递、栈帧的创建和销毁等"
"2.若函数体较大且在多出被调用, 会导致代码膨胀, 可执行文件变大"
"3.可能会增加编译的时间"
"4.编译器可能会忽略内联, 通常因为函数体过大、递归调用等"

"普通函数调用过程"
"1.通过寄存器或栈传递参数"
"2.为被调用的函数分配内存空间栈帧, 栈帧中包含局部变量、函数参数等必要信息"
"3.跳转到被调用函数的入口点"
"4.执行函数"
"5.通过寄存器或栈传递返回值"
"6.清理栈帧, 释放空间"
"7.跳转调用函数位置执行后续代码"
inline bool check(cudaError_t e, int iLine, const char *szFile)
{
    if (e != cudaSuccess)
    {
        std::cout << "CUDA runtime API error " << cudaGetErrorName(e) << " at line " << iLine << " in file " << szFile << std::endl;
        return false;
    }
    return true;
}

"__inline__ 是编译器的扩展, 将函数作为内联函数处理, 不同的编译器可能对内联函数的处理不同"
"__inline__ 和 inline 的作用相同, 通常优先使用 inline, inline 是C++标准中的关键字, 更具可移植性和通用性"
"size_t 用于表示对象大小或索引的数据类型, 无符号整型, 大小可容纳任何对象大小(内存字节数)"
__inline__ size_t dataTypeToSize(nvinfer1::DataType dataType)

printArray

"template 为定义模板的关键字, 模板可以创建类、函数、变量等, 它们类型相同但参数类型不同"
template<typename T>
void printArrayRecursion(const T *pArray, nvinfer1::Dims32 dim, int iDim, int iStart) {}

template<typename T>
void printArrayInformation(const T *pArray, nvinfer1::Dims32 dim, std::string name = std::string(""), bool bPrintInformation = true, bool bPrintArray = false, int n = 10)
{
	"此时 pArray 的类型并不确定, 需要在函数名后添加模板参数<T>"
	printArrayRecursion<T>(pArray, dim, 0, 0);
}
"显式实例化, 可以在编译时生成特定类型的函数, 从而减少模板实例化次数和编译时间"
"模板函数通常在被调用时进行实例化, 根据调用时的参数类型进行模板的具体化, 意味着每次调用生成新的函数实例"
template void printArrayInformation(const float *, nvinfer1::Dims32, std::string, bool, bool, int);

"此时参数类型确定, 可以省略函数名后的模板参数<float>"
printArrayInformation((float *)inputHost, context->getTensorShape(vTensorName[0].c_str()), vTensorName[0], true, true);

关于 include

  在 C++ 代码中,#include 用于将其他文件的内容包含到当前文件中。而在对示例进行简化时,有些难以确认 API 属于哪个导入的文件。例如在 Python 中,代码 os.path.join 就能直接知道此 API 属于 os,需要 import os。而在 C++ 中,例如 std::coutstd::fixedstd::setprecision 即使都归属于命名空间 std,需要导入的文件却有所不同。比如 std::cout 的声明在 iostreamstd::setprecision 的声明在 iomanipstd::fixed 的声明在 ios_base.hiomanip 导入了该文件。
  通常来说 #include 的是头文件,头文件中一般只有声明,还需要包含实现的库文件。在 CMakeLists 中就使用了 include_directories 添加特定的头文件搜索路径,link_directories 添加特定的库文件搜索路径,target_link_libraries 添加了特定的库文件。
  在示例中,nvinfer1::createInferBuilder 的声明在 NvInfer.hcudaMalloc 的声明在 cuda_runtime_api.h,但 NvInfer.h 通过多层的 #include 包含了 cuda_runtime_api.h,而两者的实现分别在 nvinfercudart 中。
  总而言之,感觉在 C++ 中似乎只能通过教程示例等来确定路径、文件、API 等的对应关系。原本示例中使用 using namespace nvinfer1; 省略命名空间后,直接阅读代码更加难以得知 API 的归属。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值