TensorRT 使用 - Post-training quantization (PTQ) === # 量化的好處 * 一般的模型在使用上是以 fp32 的形式, 有較佳的精準度但同時也占據較大的儲存空間及運算效能, 量化的優點在於可使模型變小且從原本浮點數 fp32 轉為整數 int8 在運算效能上也更快 * 量化分成兩種方式 * QAT : 用於模型訓練中 * 優點 : 量化相較於 PTQ 有較佳的精準度 * 缺點 : 訓練時間可能較長 * PTQ : 用於模型訓練後 * 優點 : 量化時間較短, 且可用較小的數據集即可完成 * 缺點 : 效果會較 QAT 略差 # 量化過程 ## Quantization * 我們以 MinMax 的方式解釋, 其餘還有像 entropy, percentile 等方式, 可根據模型內的分佈選擇適當的量化方式 * 重點在於找尋 Min, Max 的方式, MinMax 可能對於長尾分佈效果較差這時候就可以考慮使用 entropy * 此過程的目的是要將 float32 的權重重新映射到 int8 [0,255] 上 * 數學公式 * $maxVal=max(W), minVal=min(W)$ * $scale=\frac{maxVal-minVal}{2^{n}-1}$ * $W_q=round(\frac{W-z}{scale}),$ $z$ is offset ## Dequantization * 此過程的目的是要將 int8 的量化模型權重重新映射到 float32 上 * 數學公式 * $W_{deq}=W_q*scale+z$ # TensorRT 實作 * 在 c++ 下我們使用 TensorRT 做int8 量化需要自行撰寫 `IInt8EntropyCalibrator2` 的類別, 裡面的內容主要是使用 Cross-entropy 的方式進行量化但需要撰寫的是資料預處理的方式 * 環境建制及相關版本可參考[TensorRT 使用 - 以 yolov9 為例](https://hackmd.io/nInP1DjFSyGGlClEesZl4w), 有踩到個坑是因為我是用的 tensorRT 版本是 10.6 可能跟網路上文章的 API 不太一樣, 因為有的已經棄用, 例如原本是setMaxworkSpace 改成了 setMemoryPoolLimit ## IInt8EntropyCalibrator2 * getBatch : 本身就是讀取資料時需要撰寫預處理的地方 * writeCalibrationCache : 將量化用到的資料寫成 Cache, 可在下次重新量化時直接讀取就不用重新做預處理 * readCalibrationCache : 用於讀取之前量化所儲存的 Cache ```cpp= #include "NvInfer.h" #include <opencv2/opencv.hpp> #include "utils.hpp" #include <algorithm> #include <vector> #include <string> #include <fstream> class MyInt8Calibrator : public nvinfer1::IInt8EntropyCalibrator2 { public: MyInt8Calibrator(const std::vector<std::string>& calibrationFiles, int inputW, int inputH) : mCalibrationFiles(calibrationFiles), mInputW(inputW), mInputH(inputH), mCurrentBatch(0) { mInputSize = inputW * inputH * 3 * batch; cudaMalloc(&mDeviceInput, mInputSize * sizeof(float)); } ~MyInt8Calibrator() override { cudaFree(mDeviceInput); } int getBatchSize() const noexcept override { return 1; } bool getBatch(void* bindings[], const char* names[], int nbBindings) noexcept override; const void* readCalibrationCache(size_t& length) noexcept override ; void writeCalibrationCache(const void* cache, size_t length) noexcept override; private: std::vector<std::string> mCalibrationFiles; int mInputW, mInputH, mInputSize, mCurrentBatch; void* mDeviceInput; std::vector<char> mCalibrationCache; objectDetection utilsTool; int batch = 1; }; ``` ## Config 設定 * config 需要設定 INT8Calibrator 需要用到的 Calibrator, 也就是我們自己定義的 Calibrator, 並啟用 INT8 模式(BuilderFlag::kINT8) * setMemoryPoolLimit 用於設定記憶體大小, 並設定 logger 用來輸出資訊 ```cpp= ## In trtInference.cpp void TRTInferenceTool::setConfig(IInt8EntropyCalibrator2 &Calibrator) { m_builder.reset(createInferBuilder(logger)); m_config.reset(m_builder->createBuilderConfig()); m_config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE,16*(1 << 20)); m_config->setFlag(nvinfer1::BuilderFlag::kDEBUG); m_config->setFlag(BuilderFlag::kINT8); // 啟用 INT8 模式 m_config->setInt8Calibrator(&Calibrator); } ## In trtInference.hpp std::unique_ptr<IBuilderConfig> m_config; std::unique_ptr<IBuilder> m_builder; std::unique_ptr<INetworkDefinition> m_network; std::unique_ptr<ICudaEngine> m_int8Engine; std::unique_ptr<IHostMemory> m_serializedModel; ``` ## ONNX 讀取 * TensorRT 再做轉換時是先讀取 ONNX 的模型內容, 這邊我們使用 ```nvonnxparser``` 讀取,取到```INetworkDefinition``` 的類別 ```cpp= ## In trtInference.cpp void TRTInferenceTool::loadOnnxModel(const std::string& onnxFile) { auto parser = nvonnxparser::createParser(*m_network.get(), logger); if (!parser->parseFromFile(onnxFile.c_str(), static_cast<int>(nvinfer1::ILogger::Severity::kWARNING))) { throw std::runtime_error("Failed to parse ONNX model: " + onnxFile); } // 检查是否有输出张量 if (m_network->getNbOutputs() == 0) { throw std::runtime_error("ONNX model must have at least one output."); } std::cout << "Load onnx sucessful" << std::endl; // delete parser; } ## In trtInference.hpp std::unique_ptr<INetworkDefinition> m_network; ## How to use m_network.reset(m_builder->createNetworkV2(0)); loadOnnxModel(input); ``` ## buildSerial * buildSerial 用於執行我們轉換 int8 的過程, 會需要先將 m_int8Engine 做設定, 也就是透過剛剛的 config 及 onnx 完成 m_int8Engine 的設定 * m_serializedModel 開始將 m_int8Engine 的資料做轉換輸出, 完成後即可拿到一個 int8 的.trt ```cpp= ## In trtInference.cpp void TRTInferenceTool::buildSerial(std::string input, std::string output) { m_network.reset(m_builder->createNetworkV2(0)); loadOnnxModel(input); std::cout << "Start reset int8 engine" << std::endl; m_int8Engine.reset(m_builder->buildEngineWithConfig(*m_network.get(), *m_config.get())); std::cout << "Start convert" << std::endl; std::ofstream outputFile(output, std::ios::binary); m_serializedModel.reset(m_int8Engine->serialize()); outputFile.write(reinterpret_cast<const char*>(m_serializedModel->data()),m_serializedModel->size()); std::cout << "Convert done" << std::endl; m_serializedModel.reset(); outputFile.close(); m_int8Engine.reset(); m_network.reset(); m_config.reset(); m_builder.reset(); } ``` ## 實際輸出 * 編譯及執行 ``` cmake CMakeList.txt make export LD_LIBRARY_PATH=`pwd`/build_x86_64/TensorRT/lib ./main ``` * 我們一樣拿 yolov9 的模型來做演練, 在這邊我是直接在之前完成的 TRTInferenceTool 做新加的功能, 但實際沒用到 yolov9-c-converted.trt * 輸出模型比較 * fp32 : 149.6MB * int8 : 31.8 MB * 成功執行會出現以下畫面 ![Screenshot from 2025-01-06 21-43-47](https://hackmd.io/_uploads/BJ9_0UYIkl.png) * 程式碼 ```cpp= #include <iostream> #include <opencv2/opencv.hpp> #include <algorithm> #include "include/trtInferenceTool.hpp" #include "include/trtCalibrate.hpp" #include "include/utils.hpp" void inference() { cv::Mat img = cv::imread("./data/horses.jpg"); objectDetection objTool; cv::Mat outImg; std::vector<float> data; objTool.preprocess(img, outImg); objTool.HWC2NormalCHW(outImg, data); std::cout << data[0] << std::endl; cv::Mat res; TRTInferenceTool infTool("./data/yolov9-c-converted-int8.trt"); infTool.run(data,res); objTool.postprocess(res); objTool.draw(img,img,objTool.m_PredBox_vector); cv::imwrite("./data/test.png",img); } void calibrate() { std::string filePath = "calibration.txt"; std::vector<std::string> calFile = loadCalibrationFiles(filePath); MyInt8Calibrator *Calibrator = new MyInt8Calibrator(calFile, 640, 640); TRTCalibrateTool calTool; calTool.setConfig(*Calibrator); calTool.buildSerial("./data/yolov9-c-converted.onnx","./data/yolov9-c-converted-int8.engine"); } std::vector<std::string> loadCalibrationFiles(const std::string& filePath) { std::vector<std::string> calibrationFiles; std::ifstream file(filePath); if (!file.is_open()) { std::cerr << "Failed to open the file: " << filePath << std::endl; return calibrationFiles; } std::string line; while (std::getline(file, line)) { if (!line.empty()) { // 避免空行 calibrationFiles.push_back(line); } } file.close(); return calibrationFiles; } int main() { calibrate() inference(); return 0; } ``` * 跟原始 fp32 比對可以看到分數有降且後面的馬雖然有匡選到但因遮擋所以範圍較小 * fp32 * ![test_origin](https://hackmd.io/_uploads/BJ0hA8YU1e.png) * int8 ![test](https://hackmd.io/_uploads/ryoH2Lt81l.png) source : https://docs.nvidia.com/deeplearning/tensorrt/release-notes/index.html#rel-10-6-0 https://github.com/WongKinYiu/yolov9 https://hackmd.io/nInP1DjFSyGGlClEesZl4w github : https://github.com/GU-Lin/AI_engine ###### tags : `AI`, `AI 部屬`