网站建设网络推广外包服务商,湘潭高新区最新新闻,seo单页快速排名,大气扁平网站上一篇文章我们一起看了下TensorRT有哪些特性或者支持哪些功能#xff0c;这一节我们来详细的从API出发研究一下具体的实现#xff0c;难度要上升了哦#xff0c;请系好安全带#xff0c;准备发车#xff01; 文章目录 3. The C API3.1 The Build Phase3.1.1 Creating a …上一篇文章我们一起看了下TensorRT有哪些特性或者支持哪些功能这一节我们来详细的从API出发研究一下具体的实现难度要上升了哦请系好安全带准备发车 文章目录 3. The C API3.1 The Build Phase3.1.1 Creating a Network Definition3.1.2 Importing a Model Using the ONNX Parser3.1.3 Building an Engine 3.2 Deserializing a Plan3.3 Performing Inference3.4 TensorRT tar安装及sample 3. The C API
这一节主要基于C的API我们基于ONNX模型sampleOnnxMNIST这个例子更详细的说明了怎么使用这些API我们会在后面单独研究这个例子现在我们还是先熟悉一下API。 C的API可以从NvInfer.h头文件中找到并且都是放在nvinfer1这个命名空间中的阿达为啥你的API命名总是这么随便例如几乎所有的程序在开头都是类似下面这样的
#include “NvInfer.h”using namespace nvinfer1;TensorRT的接口类Interface classesC API通常用前缀I进行表示比如ILogger, IBuilder如果在此之前不存在一个 CUDA context则TensorRT第一次调用CUDA时会自动创建CUDA context。在第一次调用TensorRT之前最好自己创建和配置CUDA context。为了让你们能清楚的知道一些对象的生命周期文档说故意没有使用智能指针就是告诉你有些对象在某些时刻被我们手动释放了但是文档推荐你使用智能指针没错就是怕你泄露。
3.1 The Build Phase
要创建builder首先必须实例化ILogger接口。这个例子捕获所有警告消息但忽略信息性消息就是让你对消息进行过滤
class Logger : public ILogger
{void log(Severity severity, const char* msg) noexcept override{// suppress info-level messagesif (severity Severity::kWARNING)std::cout msg std::endl;}
} logger;然后你可以创建一个builder的实例
IBuilder* builder createInferBuilder(logger);3.1.1 Creating a Network Definition
创建完builder以后优化网络的第一步就是创建一个网络的定义
// 左移这么多位当作flag 1U这种使用方式参考 https://stackoverflow.com/questions/2128442/what-does-1u-x-do
// 1U 2 4
uint32_t flag 1U static_castuint32_t(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); INetworkDefinition* network builder-createNetworkV2(flag);在使用ONNX parser导入模型的时候我们需要指定kEXPLICIT_BATCH这个flag可以参考 Explicit Versus Implicit Batch获取更多信息。
3.1.2 Importing a Model Using the ONNX Parser
现在必须从ONNX表示中来填充populate网络。ONNX parser API位于文件NvOnnxParser.h中且在nvonnxparser命名空间中。
#include “NvOnnxParser.h”using namespace nvonnxparser;你可以像如下代码所示来创建一个ONNX parser
IParser* parser createParser(*network, logger);然后读取模型文件顺带着打印一下错误信息
parser-parseFromFile(modelFile, static_castint32_t(ILogger::Severity::kWARNING));
for (int32_t i 0; i parser.getNbErrors(); i)
{std::cout parser-getError(i)-desc() std::endl;
}注意下TensorRT网络定义的一个重要点是它包含了一个指向模型权重的指针这些指针由builder复制到优化的engine中。由于网络是使用parser创建的所以parser拥有权重占用的内存因此在builder运行之前不应该删除parser对象就是说只有builder开始运行了parser才能够被销毁这个时候权重交由builder占用。
3.1.3 Building an Engine
下一步就是创建一个build configuration来告诉TensorRT怎么优化模型
IBuilderConfig* config builder-createBuilderConfig();这个接口有甚多你可以设置的属性。一个重要的属性就是最大空间 maximum workspace size。Layer的实现通常需要一个临时空间这个参数限制了网络中的任意layer可以使用的最大空间。如果你没有提供一个足够的空间TensorRT就无法找到一个层的实现就是放不下了。默认情况下workspace被设置为给定设备的所有全局内存大小total global memory当你需要的时候你应该来进行限定比如说你只有一个设备但是有多个engine在build
config-setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1U 20); // 2^20当你的配置指定好以后你就可以开始构建engine了
IHostMemory* serializedModel builder-buildSerializedNetwork(*network, *config);由于序列化的引擎包含必要的权重副本serializedModel所以parsernetwork definitionbuilder configuration和builder不再是必需的可以安全地删除:
delete parser;
delete network;
delete config;
delete builder;你可以把engine存到磁盘然后记得删除这个serializedModel
delete serializedModel注意Serialized engines不能跨平台或跨TensorRT版本进行移植。Engines是特定于它们所构建的确切GPU模型的(除了平台和TensorRT版本也就是建议我们在哪用就在哪构建除非你能保证版本都一致)。且由于构建engine我们把它当作一个离线的工作offline process需要花费比较多的时间可以参考Optimizing Builder Performance章节来看看怎么让builder运行的更快。
3.2 Deserializing a Plan
假设你之前已经序列化了一个优化后的模型并且想进行推理你必须创建一个Runtime接口的示例和builder很类似runtime也需要一个logger实例
IRuntime* runtime createInferRuntime(logger);然后你就可以把模型读取到buffer中了你可以对模型进行反序列化deserialize并且放到一个engine中
ICudaEngine* engine runtime-deserializeCudaEngine(modelData, modelSize); // 这里有点跳跃我们一会看例程3.3 Performing Inference
这个时候所有的模型信息都给了engine变量但是我们必须要管理中间激活 intermediate activations的附加状态真拗口啥是中间激活先有个印象。我们通过ExecutionContext接口来进行
IExecutionContext *context engine-createExecutionContext();一个engine可以有多个execution contexts允许一组权重用于多个重叠的推理任务除非使用了dynamic shapes每个optimization profile只能有一个 execution context除非指定了预览特性kPROFILE_SHARING_0806后续有机会再补充
为了运行推理你必须要对输入输出传入相应的buffersTensorRT会要求你调用setTensorAddress来进行设置这个接口要求你输入tensor的name和buffer的地址。这个我们在之前提过你可以通过查询engine来获得输入输出的名字和找到数组的正确位置
context-setTensorAddress(INPUT_NAME, inputBuffer); // 我们后面对照例程再来看一下
context-setTensorAddress(OUTPUT_NAME, outputBuffer);然后你可以调用enqueueV3()函数来使用CUDA stream进行推理
context-enqueueV3(stream);根据网络的结构和特点网络可以异步执行也可以同步执行。例如可能导致同步行为的情况包括依赖数据的形状data dependent shapes、DLA的使用、循环和同步的插件plugin。通常在内核之前和之后使用cudaMemcpyAsync()排队数据传输以便从GPU移动数据这个很有必要不要忘记不然你的数据可能怎么都不对要确定kernel(可能还有cudaMemcpyAsync())何时完成请使用标准的CUDA同步机制例如事件 events或着等待这个流结束。。
其实整个流程并不复杂我们分成两部分
构建的时候logger-parser-network-config-builder-serializedModel推理的时候runtime-engine-context
熟悉了就好了下面我们一起来看一下提供的例程完整的看一下C API的调用流程。 3.4 TensorRT tar安装及sample
我使用的官方github里面的sample没有跑通需要配置的地方还挺多。我通过tar包安装解压后的例程跑通了最终输出结果如下 我这里附上我tar包安装TensorRT的流程吧逃不掉 首先下载TensorRT的tar包地址为https://developer.nvidia.com/nvidia-tensorrt-download版本可以选择你自己想要的要和cudnn匹配。 解压压缩包 tar -xzvf TensorRT-8.4.1.5.Linux.x86_64-gnu.cuda-11.6.cudnn8.4.tar.gz添加环境变量 # 打开环境变量文件
sudo vim ~/.bashrc# 将下面三个环境变量写入环境变量文件并保存
export LD_LIBRARY_PATH/home/ubuntu/TensorRT-8.4.1.5/lib:$LD_LIBRARY_PATH
export CUDA_INSTALL_DIR/usr/local/cuda-11.4
export CUDNN_INSTALL_DIR/usr/local/cuda-11.4# 使刚刚修改的环境变量文件生效
source ~/.bashrc# 为了避免其它软件找不到TensorRT的库建议把TensorRT的库和头文件添加到系统路径下
cd TensorRT-8.4.1.5
sudo cp -r ./lib/* /usr/lib
sudo cp -r ./include/* /usr/include上面你解压的tar包里面有一个sample文件夹首先进入samples/sampleOnnxMNIST然后参考这里 $ cd samples_dir # 就是进入sampleOnnxMNIST
$ make -j4
$ cd ../bin # 其实是cd ../../bin
$ ./sample_bin # ./sample_onnx_mnist然后就可以获得结果了下面我们对照着我们上面提到的流程来看一下源码 创建builder在sampleOnnxMNIST.cpp的111行 这里使用了一个智能指针定义如下然后创建一个builder调用的接口是nvinfer1::createInferBuilder()和上面讲解的一样吧。 template typename T
using SampleUniquePtr std::unique_ptrT, InferDeleter;
...
auto builder SampleUniquePtrnvinfer1::IBuilder(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));创建网络在117行 const auto explicitBatch 1U static_castuint32_t(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
auto network SampleUniquePtrnvinfer1::INetworkDefinition(builder-createNetworkV2(explicitBatch));创建buildConfig auto config SampleUniquePtrnvinfer1::IBuilderConfig(builder-createBuilderConfig());
if (!config)
{return false;
}创建parser auto parser SampleUniquePtrnvonnxparser::IParser(nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger()));
if (!parser)
{return false;
}解析ONNX auto parsed parser-parseFromFile(locateFile(mParams.onnxFileName, mParams.dataDirs).c_str(),static_castint(sample::gLogger.getReportableSeverity()));
if (!parsed)
{return false;
}
if (mParams.fp16)
{config-setFlag(BuilderFlag::kFP16); // 配置config
}if (mParams.int8)
{config-setFlag(BuilderFlag::kINT8);// 函数注释提到如果没有使用校准数据集那么你必须为每层都设置一个动态范围集dynamic range setsamplesCommon::setAllDynamicRanges(network.get(), 127.0F, 127.0F);
}
// 最后打开DLA来进一步加速这个概念我们之前有提过
samplesCommon::enableDLA(builder.get(), config.get(), mParams.dlaCore);我们进入这个setAllDynamicRanges()函数瞅瞅其实就是逐层设置一下但是在我们之前锁接触到的文档中并没有提这个事所以有点懵懵的如果一个tensor没有设置如果它是一个pooling 节点layer的输入那么动态范围是由inRange决定的否则是由outRange决定的也就是上面函数的第二第三参数 // 首先给每层的输入设置动态范围
for (int i 0; i network-getNbLayers(); i){auto layer network-getLayer(i);for (int j 0; j layer-getNbInputs(); j){nvinfer1::ITensor* input{layer-getInput(j)};// Optional inputs are nullptr here and are from RNN layers.if (input ! nullptr !input-dynamicRangeIsSet()){ASSERT(input-setDynamicRange(-inRange, inRange));}}}
// 再给每层的输出设置动态范围
for (int i 0; i network-getNbLayers(); i)
{auto layer network-getLayer(i);for (int j 0; j layer-getNbOutputs(); j){nvinfer1::ITensor* output{layer-getOutput(j)};// Optional outputs are nullptr here and are from RNN layers.if (output ! nullptr !output-dynamicRangeIsSet()){// Pooling must have the same input and output scales.if (layer-getType() nvinfer1::LayerType::kPOOLING){ASSERT(output-setDynamicRange(-inRange, inRange));}else{ASSERT(output-setDynamicRange(-outRange, outRange));}}}
}然后我们再进入enableDLA()函数瞅瞅 if (useDLACore 0){if (builder-getNbDLACores() 0) // 如果没有DLA就拉倒了{std::cerr Trying to use DLA core useDLACore on a platform that doesnt have any DLA cores std::endl;assert(Error: use DLA core on a platfrom that doesnt have any DLA cores false);}if (allowGPUFallback){config-setFlag(nvinfer1::BuilderFlag::kGPU_FALLBACK); // 还是在配置config配置是否允许Fallback到GPU计算}if (!config-getFlag(nvinfer1::BuilderFlag::kINT8)) // 如果没有要求INT8,则使用FP16,禁止使用FP32{// User has not requested INT8 Mode.// By default run in FP16 mode. FP32 mode is not permitted.config-setFlag(nvinfer1::BuilderFlag::kFP16);}// 设置默认设备DLAconfig-setDefaultDeviceType(nvinfer1::DeviceType::kDLA);// 设置core其实这里是-1,全部使用的意思config-setDLACore(useDLACore);}配置config auto profileStream samplesCommon::makeCudaStream();if (!profileStream){return false;}config-setProfileStream(*profileStream);还有一些config需要单独配置比如profileStream: std::unique_ptrcudaStream_t, decltype(StreamDeleter) pStream(new cudaStream_t, StreamDeleter);
if (cudaStreamCreateWithFlags(pStream.get(), cudaStreamNonBlocking) ! cudaSuccess)
{pStream.reset(nullptr);
}序列化模型创建plane SampleUniquePtrIHostMemory plan{builder-buildSerializedNetwork(*network, *config)};下面就是另一个环节了开始推理。创建runtime mRuntime std::shared_ptrnvinfer1::IRuntime(createInferRuntime(sample::gLogger.getTRTLogger()));反序列化模型 mEngine std::shared_ptrnvinfer1::ICudaEngine(mRuntime-deserializeCudaEngine(plan-data(), plan-size()), samplesCommon::InferDeleter());获取输入输出维度 // 输入输出必须为一个输入维度为4,输出维度为2
ASSERT(network-getNbInputs() 1);
mInputDims network-getInput(0)-getDimensions();
ASSERT(mInputDims.nbDims 4);ASSERT(network-getNbOutputs() 1);
mOutputDims network-getOutput(0)-getDimensions();
ASSERT(mOutputDims.nbDims 2);所有准备工作做完以后开始推理咯 这里面还有一个比较重要的工作就是设置输入也就是将数据读取后进行预处理放到host的buffer中然后从host将buffer拷贝到device中。 // 和我们之前说的一样先申请buffer(注意这里的buffer是存储了engine中的所有tensor的)samplesCommon::BufferManager buffers(mEngine);// 然后获取ExecutionContextauto context SampleUniquePtrnvinfer1::IExecutionContext(mEngine-createExecutionContext());// 拷贝数据到bufferif (!processInput(buffers)){return false;}// 开始推理getDeviceBindings()函数可以让你直接获取到device buffers在enqueue和IExecutionContext时候使用bool status context-executeV2(buffers.getDeviceBindings().data());这个processInput()函数挺重要我们单独拎出来研究一下 bool SampleOnnxMNIST::processInput(const samplesCommon::BufferManager buffers)
{const int inputH mInputDims.d[2]; // 由网络query来的const int inputW mInputDims.d[3];// Read a random digit filesrand(unsigned(time(nullptr)));std::vectoruint8_t fileData(inputH * inputW);mNumber rand() % 10;// ifstream读取本地pgm放到buffer里面去大小为h*wreadPGMFile(locateFile(std::to_string(mNumber) .pgm, mParams.dataDirs), fileData.data(), inputH, inputW);// Print an ascii representationsample::gLogInfo Input: std::endl;for (int i 0; i inputH * inputW; i){sample::gLogInfo ( .:-*#%[fileData[i] / 26]) (((i 1) % inputW) ? : \n);}sample::gLogInfo std::endl;// 获取inputTensorNames的buffer就是先拿出来float* hostDataBuffer static_castfloat*(buffers.getHostBuffer(mParams.inputTensorNames[0]));for (int i 0; i inputH * inputW; i) // 逐个进行拷贝同时归一化了一下直接返回1-x/255并不是减均值除以方差{hostDataBuffer[i] 1.0 - float(fileData[i] / 255.0);}return true;
}然后需要将buffer拷贝到device中 // 这样进行调用的buffers.copyInputToDevice();-memcpyBuffers();memcpyBuffers)干了啥呢一起看下
void memcpyBuffers(const bool copyInput, const bool deviceToHost, const bool async, const cudaStream_t stream 0){for (int i 0; i mEngine-getNbBindings(); i){void* dstPtr deviceToHost ? mManagedBuffers[i]-hostBuffer.data() : mManagedBuffers[i]-deviceBuffer.data();const void* srcPtr deviceToHost ? mManagedBuffers[i]-deviceBuffer.data() : mManagedBuffers[i]-hostBuffer.data();const size_t byteSize mManagedBuffers[i]-hostBuffer.nbBytes();const cudaMemcpyKind memcpyType deviceToHost ? cudaMemcpyDeviceToHost : cudaMemcpyHostToDevice;if ((copyInput mEngine-bindingIsInput(i)) || (!copyInput !mEngine-bindingIsInput(i))){// 真正的核心就是把我们上面的hostDataBuffer拷贝到deviceBufferif (async)CHECK(cudaMemcpyAsync(dstPtr, srcPtr, byteSize, memcpyType, stream));elseCHECK(cudaMemcpy(dstPtr, srcPtr, byteSize, memcpyType));}}}获取结果和后处理 // 调用流程是buffers.copyOutputToHost();-memcpyBuffers();
void memcpyBuffers(const bool copyInput, const bool deviceToHost, const bool async, const cudaStream_t stream 0){for (int i 0; i mEngine-getNbBindings(); i){void* dstPtr deviceToHost ? mManagedBuffers[i]-hostBuffer.data() : mManagedBuffers[i]-deviceBuffer.data();const void* srcPtr deviceToHost ? mManagedBuffers[i]-deviceBuffer.data() : mManagedBuffers[i]-hostBuffer.data();const size_t byteSize mManagedBuffers[i]-hostBuffer.nbBytes();const cudaMemcpyKind memcpyType deviceToHost ? cudaMemcpyDeviceToHost : cudaMemcpyHostToDevice;if ((copyInput mEngine-bindingIsInput(i)) || (!copyInput !mEngine-bindingIsInput(i))){if (async)// 核心在这里就是把deviceBuffer拷贝到hostBuffer中去CHECK(cudaMemcpyAsync(dstPtr, srcPtr, byteSize, memcpyType, stream));elseCHECK(cudaMemcpy(dstPtr, srcPtr, byteSize, memcpyType));}}}获取完结果后进行后处理 verifyOutput(buffers);
bool SampleOnnxMNIST::verifyOutput(const samplesCommon::BufferManager buffers)
{const int outputSize mOutputDims.d[1];// 和之前一样通过名称取出output的bufferfloat* output static_castfloat*(buffers.getHostBuffer(mParams.outputTensorNames[0]));float val{0.0F};int idx{0};// Calculate Softmaxfloat sum{0.0F};for (int i 0; i outputSize; i){output[i] exp(output[i]);sum output[i];}sample::gLogInfo Output: std::endl;for (int i 0; i outputSize; i){output[i] / sum;val std::max(val, output[i]); // 获得最大值最后进行输出if (val output[i]){idx i;}sample::gLogInfo Prob i std::fixed std::setw(5) std::setprecision(4) output[i] Class i : std::string(int(std::floor(output[i] * 10 0.5F)), *) std::endl;}sample::gLogInfo std::endl;return idx mNumber val 0.9F;
}终于结束啦这个sample麻雀虽小五脏俱全里面有挺多对函数的进一步封装的操作大家可以用函数跳转慢慢看但是整个流程和我们前半部分讲的基本一致希望大家通过这篇文章可以对整个TensorRT的模型优化及推理有个大概认知先这样吧写了几乎整整一天一起加油呀