深度学习最终BOSS——TensorRT

API学习

创建Runtime->创建Engine->创建Context->获取输入输出索引->创建buffers->为输入输出开辟GPU显存->创建cuda流->从CPU到GPU(拷贝input数据)->异步推理->从GPU到CPU(拷贝output数据)->同步cuda流->释放资源

使用流程:

Tensor RT C++ 使用流程_我为什么这么菜.的博客-CSDN博客

TensorRT 在所有支持的平台上提供 C++ 实现,在 Linux 上提供 Python 实现。 Windows 或 QNX 目前不支持 Python。

TensorRT的关键接口是:网络定义  Network Definition,为应用程序提供了定义网络的方法,可以指定输入和输出张量,并且可以添加和配置层,例如卷积层和循环层,插件层类型同样允许应用程序实现TensortRT本身不支持的功能(通过Network可以完成神经网络中的操作)。

优化配置文件 Optimization Profile 优化配置文件指定对动态维度的约束

构建器配置 Builder Configuration (Config) 构建器配置接口指定了创建引擎的详细信息。它允许应用程序指定  优化配置文件、最大工作空间大小、最小可接受精度水平、自动调整的时序迭代计数以及用于量化网络以 8 位精度运行的接口。

构建器Builder Builder 接口允许根据网络定义和构建器配置创建优化引擎。

引擎 Engine 允许应用程序执行推理,支持同步和一步执行、分析、枚举和查询引擎输入和输出绑定,单个引擎可以有多个执行上下文,允许使用一组经过训练的参数同时执行多个推理。(构造图纸,一切的思路源泉,其他的都是框架容器)

什么是执行上下文?(execution context) 可以理解为多线程执行?

ExecutionContext(执行上下文)综述 - 大师兄石头 - 博客园

ONNX解析器 parser 用于解析ONNX模型

C++ API 与 Python API  理论上,C++ API 和 Python API 在支持您的需求方面应该接近相同。 C++ API 应该用于任何对性能至关重要的场景,以及安全很重要的情况,例如在汽车中。 Python API 的主要好处是数据预处理和后处理易于使用,因为您可以使用各种库,如 NumPy 和 SciPy。

(实用还是选择C++) 

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

CUDA Runtime API :: CUDA Toolkit DocumentationCUDAStream: CUDA Runtime API :: CUDA Toolkit Documentation 

 **************************************************************************************************************什么是Logger?

日志组件,用于管理builder,engine和runtime的日志信息。

该类为 TensorRT 工具和示例提供了一个通用接口来将信息记录到控制台,并支持记录两种类型的消息:- 具有相关严重性(信息、警告、错误或内部错误/致命)的调试消息 - 测试通过/失败消息与直接发送到 stdout/stderr 相比,让所有样本都使用此类进行日志记录的优势在于,控制样本输出的详细程度和格式的逻辑集中在一个位置。 将来,可以扩展此类以支持将测试结果转储到某种标准格式(例如,JUnit XML)的文件中,并提供额外的元数据(例如,对测试运行的持续时间进行计时)。

logger会作为一个必须的参数传递给builder runtime parser的实例化接口:

IBuilder* builder = createInferBuilder(gLogger);
IRuntime* runtime = createInferRuntime(gLogger);
auto parser = nvonnxparser::createParser(*network, gLogger);

Logger在内部被视为单例,因此 IRuntime 和/或 IBuilder 的多个实例必须都使用相同的Logger。

TensorRT: nvinfer1::ILogger Class Reference

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

Execution Context 执行上下文

 定义:TensorRT: NvInferRuntime.h Source File

 实用引擎执行推理的上下文,具有功能不安全的特性。

一个 ICudaEngine 实例可能存在多个执行上下文,允许使用同一个引擎同时执行多个批处理。 如果引擎支持动态形状,则并发使用的每个执行上下文必须使用单独的优化配置文件。

警告 不要从此类继承,因为这样做会破坏 API 和 ABI 的向前兼容性。 

使用方法:应用接口IExecutionContext,首先应该先创建一个ICudaEngine引擎类型的对象,构建器运行时将使用与创建线程关联的GPU上下文创建,建议在创建运行时或构建器对象之前创建和配置 CUDA 上下文。

const ICudaEngine& engine =context.getEngine();
IExecutionContext* context = engine->createExecutionContext();
context->destroy();
context.enqueue(batchSize,buffers,stream,nullptr);
//TensorRT execution is typically asynchronous, so enqueue the kernels on a CUDA stream.
//It is common to enqueue asynchronous memcpy() before and after the kernels to move data from the GPU if it is not already there. 
//The final argument to enqueueV2() is an optional CUDA event which will be signaled when the input buffers have been consumed and their memory may be safely reused.
//For more information, refer to enqueue() for implicit batch networks and enqueueV2() for explicit batch networks. 
//In the event that asynchronous is not wanted, see execute() and executeV2().
//The IExecutionContext contains shared resources, therefore, calling enqueue or enqueueV2 in from the same IExecutionContext object with different CUDA streams concurrently results in undefined behavior. 
//To perform inference concurrently in multiple CUDA streams, use one IExecutionContext per CUDA stream.

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

TensorRT: nvinfer1::IExecutionContext Class Reference

Engine

所属类:ICudaEngine,定义在 NvInferRuntime.h 中

IBuilderConfig* config = builder->createBuilderConfig();
config->setMaxWorkspaceSize(1<<20);
ICudaEngine* engine = builder->buildEngineWithConfig(*network,*config);

在这之前需要搭建完整网络

TensorRT: nvinfer1::ICudaEngine Class Reference

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

Network

 作为构建器输入的网络定义

网络定义了网络结构,和IBuilderConfig结合使用IBuilder构建到引擎中,INetworkDefinition 可以具有在运行时指定的隐式批处理维度,或所有维度显式、完全维度模式。使用 createNetwork() 创建网络后,仅支持隐式批量大小模式。函数 hasImplicitBatchDimension() 用于查询网络的模式。

INetworkDefinition* network = builder->createNetworkV2(0U);
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createnetworkV2(1U << static_cast<unit32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));

//将输入层添加到网络,具有输入维度,包括动态批处理
ITensor* data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W});
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{-1, 1, INPUT_H, INPUT_W});

//添加卷积层
IConvolutionLayer* conv1 = network->addConvolutionNd(*data, 6, DimsHW{5, 5}, weightMap["conv1.weight"], weightMap["conv1.bias"]);
conv1->setStrideNd(DimsHW{1, 1});
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});

//添加池化层
IPoolingLayer* pool1 = network->addPoolingNd(*relu1->getOutput(0), PoolingType::kAVERAGE, DimsHW{2, 2});
pool1->setStrideNd(DimsHW{2, 2});
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});

//使用 ReLU 算法添加激活层
IActivationLayer* relu1 = network->addActivation(*conv1->getOutput(0), ActivationType::kRELU);
auto relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);

//添加全连接层
IFullyConnectedLayer* fc1 = network->addFullyConnected(*pool2->getOutput(0), 120, weightMap["fc1.weight"], weightMap["fc1.bias"]);
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]);

//添加 SoftMax 层以计算最终概率并将其设置为输出:
ISoftMaxLayer* prob = network->addSoftMax(*fc3->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
network->markOutput(*prob->getOutput(0));
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
network->markOutput(*prob->getOutput(0));

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

TensorRT: nvinfer1::INetworkDefinition Class Reference

 解析器Parser

解析器主要用于解析ONNX模型并将其转换为TensorRT模型,所属类:IParser

使用INetwork定义作为输入创建ONNX解析器:

auto parser = nvonnxparser::createParser(*network, gLogger);

Developer Guide :: NVIDIA Deep Learning TensorRT Documentation

TensorRT: nvonnxparser::IParser Class Reference

基本流程

// 1. 读取 engine 文件
std::vector<char> engineData(fsize);
engineFile.read(engineData.data(), fsize);
util::UniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger())};
util::UniquePtr<nvinfer1::ICudaEngine> mEngine(runtime->deserializeCudaEngine(engineData.data(), fsize, nullptr));

// 2. engine的输入输出初始化(也可以理解为 engine context 初始化)
// engine的输入是input,数据类型是float,shape是(1, 3, height, width)
auto input_idx = mEngine->getBindingIndex("input");
assert(mEngine->getBindingDataType(input_idx) == nvinfer1::DataType::kFLOAT);
auto input_dims = nvinfer1::Dims4{1, 3 /* channels */, height, width};
context->setBindingDimensions(input_idx, input_dims);
auto input_size = util::getMemorySize(input_dims, sizeof(float));
// engine的输出是output,数据类型是int32,自动获取输出数据shape
auto output_idx = mEngine->getBindingIndex("output");
assert(mEngine->getBindingDataType(output_idx) == nvinfer1::DataType::kINT32);
auto output_dims = context->getBindingDimensions(output_idx);
auto output_size = util::getMemorySize(output_dims, sizeof(int32_t));

// 3. inference 准备工作
// 为输入输出开辟显存空间
void* input_mem{nullptr};
cudaMalloc(&input_mem, input_size);
void* output_mem{nullptr};
cudaMalloc(&output_mem, output_size); 
// 定义图像norm操作
const std::vector<float> mean{0.485f, 0.456f, 0.406f};
const std::vector<float> stddev{0.229f, 0.224f, 0.225f};
auto input_image{util::RGBImageReader(input_filename, input_dims, mean, stddev)};
input_image.read();
auto input_buffer = input_image.process();
// 将处理好的数据转移到显存中
cudaMemcpyAsync(input_mem, input_buffer.get(), input_size, cudaMemcpyHostToDevice, stream);

// 4. 执行 inference 操作
// 通过 executeV2 or enqueueV2 激发 inference 的具体执行
void* bindings[] = {input_mem, output_mem};
bool status = context->enqueueV2(bindings, stream, nullptr);
// 获取预测结果
auto output_buffer = std::unique_ptr<int>{new int[output_size]};
cudaMemcpyAsync(output_buffer.get(), output_mem, output_size, cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
// 释放资源
cudaFree(input_mem);
cudaFree(output_mem);

// 5. 输出预测结果
const int num_classes{21};
const std::vector<int> palette{
	(0x1 << 25) - 1, (0x1 << 15) - 1, (0x1 << 21) - 1};
auto output_image{util::ArgmaxImageWriter(output_filename, output_dims, palette, num_classes)};
output_image.process(output_buffer.get());
output_image.write();

函数学习

cudaMalloc

cudaMalloc的原型为: cudaError_t cudaMalloc(void** devPtr, size_t size),第一个参数为地址指针

在C语言中,有函数malloc:int *a = (int )malloc(nsizeof(int)),返回的是一个int型指针,指向大小为n个int型数据的连续内存地址的首地址,可以理解为a是这个数组的首地址。

在CUDA编程时 ,第一步需要在GPU内分配内存,与数组的声明步骤是一样的。假如我们要在GPU内申明一段n个大小的float型数组,我们需要定义float *addr,用于指向GPU内这个地址的首地址。因此,addr这个变量中存的就是用户在GPU中声明的float型数组的首地址。

在CUDA中的调用方法为:cudaMalloc(float(**)&addr,n*sizeof(float))

 在addr内部存放的是指向GPU数组中的首地址,如果需要对addr内容进行改变,必须采用引用的方式进行形参传递(类似C语言中的引用调用)。

addr是指向地址的指针,cudaMalloc完成了*addr的内容的改变后,需要转换数据类型。把它转换成指针型指针是对于主机端而言的(GPU称为设备端),addr这个变量是指向我们在GPU内部声明的连续地址的首地址,因此,我们对addr进行第一次引用计算,得到的是首地址的值。需要通过这个值来在GPU的内存进行操作,因此需要再做一次引用计算,得到的就是GPU中连续地址的第一个单元,接下来就可以进行主机端设备端的内存内容拷贝了。

cudaError_t cudaMemcpy(void *dist, const void* src,size_t count,CudaMemcpyKind kind)

前两个参数分别是目的地址和源地址。完成了内存声明后,我们把addr作为形参,这时,dist指针指向了addr的地址。对dist做引用运算后,返回的就是我们声明好的GPU内存的首地址了。

CUDA入门——cudaMalloc函数的理解_蔡裕星的博客-CSDN博客_cudamalloc

CUDA流 

CUDA 将以下操作公开为可以彼此同时操作的独立任务:

  • 在主机上计算;
  • 设备上的计算;
  • 从主机到设备的内存传输;
  • 从设备到主机的内存传输;
  • 在给定设备的内存中进行内存传输;
  • 设备之间的内存传输。

 这些操作之间实现的并发级别将取决于设备的功能和计算能力:

主机和设备之间的并发执行

 在设备完成请求的任务之前,异步库函数将控制权返回给宿主线程,从而促进了主机的并发执行。使用异步调用,许多设备操作可以在适当的设备资源可用时排队,由CUDA驱动程序执行。这减轻了主机线程管理设备的大部分责任,让它自由地执行其他任务。

CUDA流

应用程序通过流管理并发操作,流是按顺序执行的命令序列,不同的流可能会彼此乱序或同时执行它们的命令。当满足命令的所有依赖项时,可以执行在流上发出的命令。 依赖关系可以是先前在同一流上启动的命令或来自其他流的依赖关系。 同步调用的成功完成保证了所有启动的命令都完成了。

创建与销毁

 流是通过创建一个流对象并将其指定为一系列内核启动和主机 <-> 设备内存拷贝的流参数来定义的。 以下代码示例创建两个流并在锁页内存中分配一个浮点数组 hostPtr

cudaStream_t stream[2];
for (int i = 0; i < 2; ++i)
    cudaStreamCreate(&stream[i]);
float* hostPtr;
cudaMallocHost(&hostPtr, 2 * size);

这些流中的每一个都由以下代码示例定义为从主机到设备的一次内存复制、一次内核启动和从设备到主机的一次内存复制的序列:

for (int i = 0; i < 2; ++i) {
    cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size,
                    size, cudaMemcpyHostToDevice, stream[i]);
    MyKernel <<<100, 512, 0, stream[i]>>>
          (outputDevPtr + i * size, inputDevPtr + i * size, size);
    cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size,
                    size, cudaMemcpyDeviceToHost, stream[i]);
}

每个流将其输入数组 hostPtr 的部分复制到设备内存中的数组 inputDevPtr,通过调用 MyKernel() 处理设备上的 inputDevPtr,并将结果 outputDevPtr 复制回 hostPtr 的同一部分。 重叠行为描述了此示例中的流如何根据设备的功能重叠。 请注意,hostPtr 必须指向锁页主机内存才能发生重叠。

通过调用 cudaStreamDestroy() 释放流:

for (int i = 0; i < 2; ++i)
    cudaStreamDestroy(stream[i]);

如果调用 cudaStreamDestroy() 时设备仍在流中工作,则该函数将立即返回,并且一旦设备完成流中的所有工作,与流关联的资源将自动释放。

显式同步

有多种方法可以显式地同步流。

cudaDeviceSynchronize() 一直等待,直到所有主机线程的所有流中的所有先前命令都完成。

cudaStreamSynchronize() 将流作为参数并等待,直到给定流中的所有先前命令都已完成。 它可用于将主机与特定流同步,允许其他流继续在设备上执行。

cudaStreamWaitEvent() 将流和事件作为参数(有关事件的描述,请参阅事件),并在调用 cudaStreamWaitEvent() 后使添加到给定流的所有命令延迟执行,直到给定事件完成。

cudaStreamQuery() 为应用程序提供了一种方法来了解流中所有前面的命令是否已完成。
 

猜你喜欢

转载自blog.csdn.net/qq_60609496/article/details/128034742