本节将介绍如何使用tensorRT C++ API 进行网络模型创建。
还是通过 tensorRT官方给的一个例程来学习。
还是mnist手写体识别的例子。上一节主要是用 tensorRT提供的NvCaffeParser来将 Caffe中的model 转换成tensorRT中特有的模型结构。NvCaffeParser是tensorRT封装好的一个用以解析Caffe模型的工具 (较顶层的API),同样的还有 NvUffPaser是用于解析TensorFlow的工具。
除了以上两个封装好的工具之外,还可以使用tensorRT提供的C++ API(底层的API)来直接在tensorRT中创建模型。这时 tensorRT 相当于是一个独立的深度学习框架了,这个框架和其他框架(Caffe, TensorFlow,MXNet等)一样都具备搭建网络模型的能力(只有前向计算没有反向传播)。
不同之处在于:
使用C++ API函数部署网络主要分为四个步骤:
以上,第1,2,4步骤在使用 NvCaffeParser 时也是有的。只有第3步是本节所讲的方法中特有的,其实对于NvCaffeParser 工具来说,他只是把 第 3步封装起来了而已。
如下,对比一下 NvCaffeParser 的使用方法,下面的代码中只列出了关键部分的代码。完整代码请看上一节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//build phase INetworkDefinition* network = builder->createNetwork(); //1. 创建网络 CaffeParser* parser = createCaffeParser(); std::unordered_map const IBlobNameToTensor* blobNameToTensor = //3. 添加各种各样的层 parser->parse(locateFile(deployFile).c_str(), //NvCaffeParser 工具 locateFile(modelFile).c_str(), //把添加层的内容封装起来了 *network, DataType::kFLOAT); for (auto& s : outputs) network->markOutput(*blobNameToTensor->find(s.c_str())); // 4. 设定网络输出 ICudaEngine* engine = builder->buildCudaEngine(*network); //创建engine //省略一些内容……………… //execution phase IExecutionContext *context = engine->createExecutionContext(); //创建 context int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME), outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME); //2.为网络添加输入 //省略一些内容……………… context.enqueue(batchSize, buffers, stream, nullptr); //调用cuda核计算 cudaStreamSynchronize(stream); //同步cuda 流 |
上述四个步骤对应部分已在注释标出。可见 NvCaffeParser 工具中最主要的是 parse 函数,这个函数接受网络模型文件(deploy.prototxt)、权值文件(net.caffemodel)为参数,这两个文件是caffe的模型定义文件和训练参数文件。parse 函数会解析这两个文件并对应生成 tensorRT的模型结构。
对于NvCaffeParser 工具来说,是需要三个文件的,分别是:
以下分步骤说明四个步骤:
先创建一个tensorRT的network,这个network 现在只是个空架子,比较简单:
1 |
INetworkDefinition* network = builder->createNetwork(); |
所有的网络都需要明确输入是哪个blob,因为这是数据传送的入口。
1 2 |
// Create input of shape { 1, 1, 28, 28 } with name referenced by INPUT_BLOB_NAME auto data = network->addInput(INPUT_BLOB_NAME, dt, DimsCHW{ 1, INPUT_H, INPUT_W}); |
INPUT_BLOB_NAME 是为输入 blob起的名字;
dt是指数据类型,有kFLOAT(float 32), kHALF(float 16), kINT8(int 8)等类型;
1 2 3 4 5 6 7 8 |
//位于 NvInfer.h 文件 enum class DataType : int { kFLOAT = 0, //!< FP32 format. kHALF = 1, //!< FP16 format. kINT8 = 2, //!< INT8 format. kINT32 = 3 //!< INT32 format. 这个是TensorRT新增的 }; |
DimsCHW{ 1, INPUT_H, INPUT_W} 是指,batch为1(省略),channel 为1,输入height 和width分别为 INPUT_H, INPUT_W的blob;
1 2 3 4 5 6 |
// Create a scale layer with default power/shift and specified scale parameter. float scale_param = 0.0125f; Weights power{DataType::kFLOAT, nullptr, 0}; Weights shift{DataType::kFLOAT, nullptr, 0}; Weights scale{DataType::kFLOAT, &scale_param, 1}; auto scale_1 = network->addScale(*data, ScaleMode::kUNIFORM, shift, scale, power); |
主要就是 addScale 函数,后面接受的参数是这一层需要设置的参数。
scale 层的作用是对每个输入数据进行幂运算
f(x)= (shift + scale * x) ^ power
层类型:Power
可选参数:
power: 默认为1
scale: 默认为1
shift: 默认为0
就是一种激活层。
Weights 类的定义如下:
1 2 3 4 5 6 7 8 9 |
//NvInfer.h 文件 class Weights { public: DataType type; //!< the type of the weights const void* values; //!< the weight values, in a contiguous array int64_t count; //!< the number of weights in the array }; |
以上是不包含训练参数的层,还有 Relu层,Pooling层等。
包含训练参数的层,比如卷积层,全连接层,要先加载权值文件。
1 2 3 4 5 6 7 8 9 |
// Add convolution layer with 20 outputs and a 5x5 filter. // 加载权值文件,加载一次即可 std::map //添加卷积层 IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); //设置步长 conv1->setStride(DimsHW{1, 1}); |
第6行添加卷积层:
1 |
IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); |
*scale_1->getOutput(0) :获取上一层 scale层的输出
20:卷积核个数,或者输出feature map 层数
DimsHW{5, 5}:卷积核大小
weightMap["conv1filter"], weightMap["conv1bias"]:权值系数矩阵
上面的 mnistapi.wts 文件,是用于存放网络中各个层间的权值系数的,该文件位于 /usr/src/tensorrt/data
文件夹中。
可以用notepad打开看一下,如下:
可见每一行都是一层的一些参数,比如 conv1bias 是指第一个卷积层的偏置系数,后面的0 指的是 kFLOAT 类型,也就是 float 32;后面的20是系数的个数,因为输出是20,所以偏置是20个;下面一行是 卷积核的系数,因为是20个 5×5的卷积核,所以有 20×5×5=500个参数。其它层依次类推。
这个文件是例程中直接给的,感觉像是 用caffe等工具训练后,将weights系数从caffemodel 中提取出来的。直接读取caffemodel应该也是可以的,稍微改一下接口:解析caffemodel文件然后将层名和权值参数键值对存到一个map中,网上大概找了一下,比如 这个 ,解析后的caffemodel如下所示:
conv1 最下面有一个 blobs结构,这个是weights系数;每一个包含参数的层(卷积,全连接等;激活层,池化层没有参数)都有一个 blobs结构。只需将这些参数提取出来,保存到一个map中。
除此之外也可以添加很多其他的层,比如反卷积层,池化层,全连接层等,具体参考 英伟达官方API 。
添加层的过程就相当于 NvCaffeParser 工具中 parse 函数解析 deploy.prototxt 文件的过程。
网络必须知道哪一个blob是输出的。
如下代码,在网络的最后添加了一个softmax层,并将这个层命名为 OUTPUT_BLOB_NAME,之后指定为输出层。
1 2 3 4 |
// Add a softmax layer to determine the probability. auto prob = network->addSoftMax(*ip2->getOutput(0)); prob->getOutput(0)->setName(OUTPUT_BLOB_NAME); network->markOutput(*prob->getOutput(0)); |
那直接使用底层API有什么好处呢?看下表
Feature | C++ | Python | NvCaffeParser | NvUffParser |
---|---|---|---|---|
CNNs | yes | yes | yes | yes |
RNNs | yes | yes | no | no |
INT8 Calibration | yes | yes | NA | NA |
Asymmetric Padding | yes | yes | no | no |
上表列出了 tensorRT 的不同特点与 API 对应的情况。可以看到对于 RNN,int8校准(float 32 转为 int8),不对称 padding 来说,NvCaffeParser是不支持的,只有 C++ API 和 Python API,才是支持的。
所以说如果是针对很复杂的网络结构使用tensorRT,还是直接使用底层的 C++ API,和Python API 较好。底层C++ API还可以解析像 darknet 这样的网络模型,因为它需要的就只是一个层名和权值参数对应的map文件。
例程位于 /usr/src/tensorrt/samples/sampleMNISTAPI
1 2 3 4 5 |
//这个是main函数中的代码片段 // create a model using the API directly and serialize it to a stream IHostMemory *modelStream{nullptr}; //调用APIToModel函数,手动创建网络模型 APIToModel(1, &modelStream); |
APIToModel函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void APIToModel(unsigned int maxBatchSize, IHostMemory** modelStream) { // Create builder IBuilder* builder = createInferBuilder(gLogger); //下面这个createMNISTEngine函数才是真正手动创建网络的过程 // Create model to populate the network, then set the outputs and create an engine ICudaEngine* engine = createMNISTEngine(maxBatchSize, builder, DataType::kFLOAT); assert(engine != nullptr); // Serialize the engine (*modelStream) = engine->serialize(); // Close everything down engine->destroy(); builder->destroy(); } |
createMNISTEngine函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// Creat the engine using only the API and not any parser. ICudaEngine* createMNISTEngine(unsigned int maxBatchSize, IBuilder* builder, DataType dt) { INetworkDefinition* network = builder->createNetwork(); // Create input tensor of shape { 1, 1, 28, 28 } with name INPUT_BLOB_NAME ITensor* data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W}); assert(data); // Create scale layer with default power/shift and specified scale parameter. const float scaleParam = 0.0125f; const Weights power{DataType::kFLOAT, nullptr, 0}; const Weights shift{DataType::kFLOAT, nullptr, 0}; const Weights scale{DataType::kFLOAT, &scaleParam, 1}; IScaleLayer* scale_1 = network->addScale(*data, ScaleMode::kUNIFORM, shift, scale, power); assert(scale_1); // Add convolution layer with 20 outputs and a 5x5 filter. // 加载权值文件,加载一次即可 std::map // 添加卷积层 IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]); assert(conv1); //设置步长 conv1->setStride(DimsHW{1, 1}); // Add max pooling layer with stride of 2x2 and kernel size of 2x2. IPoolingLayer* pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2}); assert(pool1); pool1->setStride(DimsHW{2, 2}); // Add second convolution layer with 50 outputs and a 5x5 filter. IConvolutionLayer* conv2 = network->addConvolution(*pool1->getOutput(0), 50, DimsHW{5, 5}, weightMap["conv2filter"], weightMap["conv2bias"]); assert(conv2); conv2->setStride(DimsHW{1, 1}); // Add second max pooling layer with stride of 2x2 and kernel size of 2x3> IPoolingLayer* pool2 = network->addPooling(*conv2->getOutput(0), PoolingType::kMAX, DimsHW{2, 2}); assert(pool2); pool2->setStride(DimsHW{2, 2}); // Add fully connected layer with 500 outputs. IFullyConnectedLayer* ip1 = network->addFullyConnected(*pool2->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]); assert(ip1); // Add activation layer using the ReLU algorithm. IActivationLayer* relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU); assert(relu1); // Add second fully connected layer with 20 outputs. IFullyConnectedLayer* ip2 = network->addFullyConnected(*relu1->getOutput(0), OUTPUT_SIZE, weightMap["ip2filter"], weightMap["ip2bias"]); assert(ip2); // Add softmax layer to determine the probability. ISoftMaxLayer* prob = network->addSoftMax(*ip2->getOutput(0)); assert(prob); prob->getOutput(0)->setName(OUTPUT_BLOB_NAME); network->markOutput(*prob->getOutput(0)); // Build engine builder->setMaxBatchSize(maxBatchSize); builder->setMaxWorkspaceSize(1 << 20); ICudaEngine* engine = builder->buildCudaEngine(*network); // Don't need the network any more network->destroy(); // Release host memory for (auto& mem : weightMap) { free((void*) (mem.second.values)); } return engine; } |
可见里面包含了很多 add* 函数,都是用于添加各种各样的层的。可参考英伟达官方API 。
deploy阶段基本与之前的无异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main(int argc, char** argv) { ……………… ……………… // Deserialize engine we serialized earlier // 创建运行时环境 IRuntime对象,传入 gLogger 用于打印信息 IRuntime* runtime = createInferRuntime(gLogger); assert(runtime != nullptr); ICudaEngine* engine = runtime->deserializeCudaEngine(trtModelStream->data(), trtModelStream->size(), nullptr); assert(engine != nullptr); trtModelStream->destroy(); //创建上下文环境,主要用于inference 函数中启动cuda核 IExecutionContext* context = engine->createExecutionContext(); assert(context != nullptr); //2.deploy 阶段:调用 inference 函数,进行推理过程 // Run inference on input data float prob[OUTPUT_SIZE]; doInference(*context, data, prob, 1); ……………… ……………… } |
doInference函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void doInference(IExecutionContext& context, float* input, float* output, int batchSize) { const ICudaEngine& engine = context.getEngine(); // Pointers to input and output device buffers to pass to engine. // Engine requires exactly IEngine::getNbBindings() number of buffers. assert(engine.getNbBindings() == 2); void* buffers[2]; // In order to bind the buffers, we need to know the names of the input and output tensors. // Note that indices are guaranteed to be less than IEngine::getNbBindings() const int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME); const int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME); // Create GPU buffers on device CHECK(cudaMalloc(&buffers[inputIndex], batchSize * INPUT_H * INPUT_W * sizeof(float))); CHECK(cudaMalloc(&buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float))); // Create stream cudaStream_t stream; CHECK(cudaStreamCreate(&stream)); // DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host CHECK(cudaMemcpyAsync(buffers[inputIndex], input, batchSize * INPUT_H * INPUT_W * sizeof(float), cudaMemcpyHostToDevice, stream)); context.enqueue(batchSize, buffers, stream, nullptr); CHECK(cudaMemcpyAsync(output, buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float), cudaMemcpyDeviceToHost, stream)); cudaStreamSynchronize(stream); // Release stream and buffers cudaStreamDestroy(stream); CHECK(cudaFree(buffers[inputIndex])); CHECK(cudaFree(buffers[outputIndex])); } |