深度学习算法优化系列二十二 | 利用TensorRT部署YOLOV3-Tiny INT8量化模型

客官°小女子只卖身不卖艺 2023-07-22 03:55 118阅读 0赞

1. 前言

上一节深度学习算法优化系列二十一 | 在VS2015上利用TensorRT部署YOLOV3-Tiny模型 分享了使用TensorRT在GPU上部署FP32的YOLOV3-Tiny模型,这一节继续分享一下如何部署INT8的YOLOV3-Tiny模型。

2. 确定走哪条路?

和上一节一样,这里仍然是走ONNX->TRT这条路,也就是说我这里的INT8量化是在TensorRT中使用nvonnxparser解析了YOLOV3-Tiny 的ONNX模型之后完成的,似乎这也是比较主流的方法。官方例子中提供了一个MNIST数据集的INT8量化,过程也是先用nvcaffeparser解析Caffe模型然后直接做量化并将原始模型序列化为TRT文件以供后面的图像推理。

所以,我这里走的路就是直接解析ONNX模型->INT8量化->序列化为TRT文件->完成推理。

3. 准备校准集

如果你懂TensorRT的量化原理,就没必要看这一节了,如果不懂也没关系后面我会单独写一篇文章来尝试解释一下。首先宏观的说一下,TensorRT对一个模型进行全INT8量化包含权重和激活值两大部分,对于权重采用的是直接非饱和量化,也就是说直接统计权重的最大值和最小值就可以完成量化。而对于激活值的量化,则需要以下步骤:

来自公众号的Ldpe2G作者,感谢

可以看到在量化激活值的时候需要利用校准集进行FP32的推理并收集每一层的激活值并统计直方图。因此,在INT8量化之前我们首先需要准备一下校准集。这里怎么准备呢?

很简单,你训练YOLOV3-Tiny的验证集抽出一部分就可以了(我这里使用了100张,NVIDIA的PPT里面说需要使用1000张,最好和PPT里面指定的图片数量一致,PPT见附录),然后将图片的路径放到一个*.txt文件里面就可以了,如下图所示:

验证集

4. TensorRT INT8量化核心步骤

接着上一次推文的介绍,你已经可以获得YOLOV3-Tiny的FP32的ONNX文件。然后我们只需要写一个新的类int8EntroyCalibrator继承Int8EntropyCalibrator这个类,然后重写一些和数据读取相关的成员函数即可。这样就可以随心所欲的去修改校验数据的读取格式,不用像官方例子那样还必须转成Caffe模型并将数据集制作为指定格式。重载后的代码如下:

  1. namespace nvinfer1 {
  2. class int8EntroyCalibrator : public nvinfer1::IInt8EntropyCalibrator {
  3. public:
  4. int8EntroyCalibrator(const int &bacthSize,
  5. const std::string &imgPath,
  6. const std::string &calibTablePath);
  7. virtual ~int8EntroyCalibrator();
  8. int getBatchSize() const override { return batchSize; }
  9. bool getBatch(void *bindings[], const char *names[], int nbBindings) override;
  10. const void *readCalibrationCache(std::size_t &length) override;
  11. void writeCalibrationCache(const void *ptr, std::size_t length) override;
  12. private:
  13. bool forwardFace;
  14. int batchSize;
  15. size_t inputCount;
  16. size_t imageIndex;
  17. std::string calibTablePath;
  18. std::vector<std::string> imgPaths;
  19. float *batchData{ nullptr };
  20. void *deviceInput{ nullptr };
  21. bool readCache;
  22. std::vector<char> calibrationCache;
  23. };
  24. int8EntroyCalibrator::int8EntroyCalibrator(const int &bacthSize, const std::string &imgPath,
  25. const std::string &calibTablePath) :batchSize(bacthSize), calibTablePath(calibTablePath), imageIndex(0), forwardFace(
  26. false) {
  27. int inputChannel = 3;
  28. int inputH = 416;
  29. int inputW = 416;
  30. inputCount = bacthSize*inputChannel*inputH*inputW;
  31. std::fstream f(imgPath);
  32. if (f.is_open()) {
  33. std::string temp;
  34. while (std::getline(f, temp)) imgPaths.push_back(temp);
  35. }
  36. int len = imgPaths.size();
  37. for (int i = 0; i < len; i++) {
  38. cout << imgPaths[i] << endl;
  39. }
  40. batchData = new float[inputCount];
  41. CHECK(cudaMalloc(&deviceInput, inputCount * sizeof(float)));
  42. }
  43. int8EntroyCalibrator::~int8EntroyCalibrator() {
  44. CHECK(cudaFree(deviceInput));
  45. if (batchData)
  46. delete[] batchData;
  47. }
  48. bool int8EntroyCalibrator::getBatch(void **bindings, const char **names, int nbBindings) {
  49. cout << imageIndex << " " << batchSize << endl;
  50. cout << imgPaths.size() << endl;
  51. if (imageIndex + batchSize > int(imgPaths.size()))
  52. return false;
  53. // load batch
  54. float* ptr = batchData;
  55. for (size_t j = imageIndex; j < imageIndex + batchSize; ++j)
  56. {
  57. //cout << imgPaths[j] << endl;
  58. Mat img = cv::imread(imgPaths[j]);
  59. vector<float>inputData = prepareImage(img);
  60. cout << inputData.size() << endl;
  61. cout << inputCount << endl;
  62. if ((int)(inputData.size()) != inputCount)
  63. {
  64. std::cout << "InputSize error. check include/ctdetConfig.h" << std::endl;
  65. return false;
  66. }
  67. assert(inputData.size() == inputCount);
  68. int len = (int)(inputData.size());
  69. memcpy(ptr, inputData.data(), len * sizeof(float));
  70. ptr += inputData.size();
  71. std::cout << "load image " << imgPaths[j] << " " << (j + 1)*100. / imgPaths.size() << "%" << std::endl;
  72. }
  73. imageIndex += batchSize;
  74. CHECK(cudaMemcpy(deviceInput, batchData, inputCount * sizeof(float), cudaMemcpyHostToDevice));
  75. bindings[0] = deviceInput;
  76. return true;
  77. }
  78. const void* int8EntroyCalibrator::readCalibrationCache(std::size_t &length)
  79. {
  80. calibrationCache.clear();
  81. std::ifstream input(calibTablePath, std::ios::binary);
  82. input >> std::noskipws;
  83. if (readCache && input.good())
  84. std::copy(std::istream_iterator<char>(input), std::istream_iterator<char>(),
  85. std::back_inserter(calibrationCache));
  86. length = calibrationCache.size();
  87. return length ? &calibrationCache[0] : nullptr;
  88. }
  89. void int8EntroyCalibrator::writeCalibrationCache(const void *cache, std::size_t length)
  90. {
  91. std::ofstream output(calibTablePath, std::ios::binary);
  92. output.write(reinterpret_cast<const char*>(cache), length);
  93. }
  94. }

有了这个类,所有的问题都解决了,接下来只需要在解析ONNX模型之后利用这个类进行INT8量化就可以了。

带注释的代码解析如下:

  1. // ONNX模型转为TensorRT引擎
  2. bool onnxToTRTModel(const std::string& modelFile, // onnx文件的名字
  3. const std::string& filename, // TensorRT引擎的名字
  4. IHostMemory*& trtModelStream) // output buffer for the TensorRT model
  5. {
  6. // 创建builder
  7. IBuilder* builder = createInferBuilder(gLogger.getTRTLogger());
  8. assert(builder != nullptr);
  9. nvinfer1::INetworkDefinition* network = builder->createNetwork();
  10. if (!builder->platformHasFastInt8()) return false;
  11. // 解析ONNX模型
  12. auto parser = nvonnxparser::createParser(*network, gLogger.getTRTLogger());
  13. //可选的 - 取消下面的注释可以查看网络中每层的详细信息
  14. //config->setPrintLayerInfo(true);
  15. //parser->reportParsingInfo();
  16. //判断是否成功解析ONNX模型
  17. if (!parser->parseFromFile(modelFile.c_str(), static_cast<int>(gLogger.getReportableSeverity())))
  18. {
  19. gLogError << "Failure while parsing ONNX file" << std::endl;
  20. return false;
  21. }
  22. // 建立推理引擎
  23. builder->setMaxBatchSize(BATCH_SIZE);
  24. builder->setMaxWorkspaceSize(1 << 30);
  25. nvinfer1::int8EntroyCalibrator *calibrator = nullptr;
  26. if (calibFile.size()>0) calibrator = new nvinfer1::int8EntroyCalibrator(BATCH_SIZE, calibFile, "F:/TensorRT-6.0.1.5/data/v3tiny/calib.table");
  27. //builder->setFp16Mode(true);
  28. std::cout << "setInt8Mode" << std::endl;
  29. if (!builder->platformHasFastInt8())
  30. std::cout << "Notice: the platform do not has fast for int8" << std::endl;
  31. builder->setInt8Mode(true);
  32. builder->setInt8Calibrator(calibrator);
  33. /*if (gArgs.runInInt8)
  34. {
  35. samplesCommon::setAllTensorScales(network, 127.0f, 127.0f);
  36. }*/
  37. //samplesCommon::setAllTensorScales(network, 1.0f, 1.0f);
  38. cout << "start building engine" << endl;
  39. ICudaEngine* engine = builder->buildCudaEngine(*network);
  40. cout << "build engine done" << endl;
  41. assert(engine);
  42. if (calibrator) {
  43. delete calibrator;
  44. calibrator = nullptr;
  45. }
  46. // 销毁模型解释器
  47. parser->destroy();
  48. // 序列化引擎
  49. trtModelStream = engine->serialize();
  50. // 保存引擎
  51. nvinfer1::IHostMemory* data = engine->serialize();
  52. std::ofstream file;
  53. file.open(filename, std::ios::binary | std::ios::out);
  54. cout << "writing engine file..." << endl;
  55. file.write((const char*)data->data(), data->size());
  56. cout << "save engine file done" << endl;
  57. file.close();
  58. // 销毁所有相关的东西
  59. engine->destroy();
  60. network->destroy();
  61. builder->destroy();
  62. return true

剩下的内容就是一些预处理和NMS后处理,这里就不再赘述了,执行完程序后就会在指定路径下生成INT8量化的Table文件以及INT8量化后的TRT序列化文件,后面就可以直接加载这个文件进行推理了。所有完整细节请看我的提供的完整源码。

5. 1050Ti的速度测试


















YOLOV3-Tiny TRT模型 Inference Time
FP32 17ms
INT8 4ms

1050Ti上运行了20个Loop测试了速度,发现前向推理的速度有4倍提升,同时TRT序列化文件的大小也减少了4倍左右。

6. 源码获取

GiantPandaCV公众号后台回复 INT8 获取完整CPP文件。注意TensorRT版本为6.0。

公众号二维码:

公众号

7. 附录

  • TensorRT INT8量化官方PPT: http://on-demand.gputechconf.com/gtc/2017/presentation/s7310-8-bit-inference-with-tensorrt.pdf
  • https://github.com/NVIDIA/TensorRT/tree/release/6.0/samples/opensource/sampleINT8

欢迎关注GiantPandaCV, 在这里你将看到独家的深度学习分享,坚持原创,每天分享我们学习到的新鲜知识。( • ̀ω•́ )✧

有对文章相关的问题,或者想要加入交流群,欢迎添加BBuf微信:

二维码

发表评论

表情:
评论列表 (有 0 条评论,118人围观)

还没有评论,来说两句吧...

相关阅读