深度学习TensorRT量化实战分析

量化校准

  根据所需的量化参数可以分为:权重量化和权重激活量化。
- 权重量化 :仅对网络中的权重执行量化,由于网络权重一般保存下来的,因此提前根据权重便可以计算出相应的量化参数S和Z,而不需要额外的校准数据集。一般来说,推理过程中,权重值的数量远小于激活值,仅对权重执行量化加速效果一般。
- 权重激活量化:不仅对网络中的权重进行量化,还对激活值(神经网络层的输出)进行量化。由于激活值的范围通常不容易提前获得,因此需要在网络推理过程中进行计算或者根据模型大致的预测。因此引入了量化校准步骤。

TensorRT进行PTQ量化

  TnesorRT进行量化操作时,主要针对权重激活量化所需的量化校准步骤进行介绍。tensorRT中的calibrator类一共存在五种,分别为nvinfer1::IInt8EntropyCalibrator2、nvinfer1::IInt8MinMaxCalibrator、nvinfer1::IInt8EntropyCalibrator、 nvinfer1::IInt8LegacyCalibrator、 nvinfer1::IInt8Calibrator。
nvinfer1::IInt8EntropyCalibrator2:是tensorRT 7.0引入的接口,实现基于熵的INT8量化校准器。(默认情况下优先使用它)
nvinfer1::IInt8MinMaxCalibrator:实现基于最大值最小值的INT8量化校准器。
nvinfer1::IInt8EntropyCalibrator:是tensorRT 7.0之前的接口,实现基于熵的INT8量化校准器。(目前已被弃用)
nvinfer1::IInt8LegacyCalibrator:是tensorRT 6.x之前的接口。(目前已被弃用),IInt8MinMaxCalibrator和IInt8EntropyCalibrator可以完全取代它。
nvinfer1::IInt8Calibrator:是 IInt8LegacyCalibrator 的基类,用于向后兼容。如果要实现自定义的 INT8 量化校准器,可以选择从该基类派生,并实现所需的校准逻辑。
因此优先使用IInt8EntropyCalibrator2接口

使用tensorRT进行量化时,首先需要创建一个自定义calibrator类,该类继承上述几种接口。并且实现其虚函数。

class Int8EntropyCalibrator : public nvinfer1::IInt8MinMaxCalibrator
        {
            // class Int8EntropyCalibrator: public nvinfer1::IInt8EntropyCalibrator {

        public:
            Int8EntropyCalibrator(CalibratorParams &params);
            ~Int8EntropyCalibrator(){};

            int getBatchSize() const noexcept override { return params_.batch_size; };
            bool getBatch(void *bindings[], const char *names[], int nbBindings) noexcept override;
            const void *readCalibrationCache(std::size_t &length) noexcept override;
            void writeCalibrationCache(const void *ptr, std::size_t legth) noexcept override;

        private:
            CalibratorParams params_;
            std::vector<std::string> image_list_;
            std::vector<char> calibration_cache_;

            float *device_input_{nullptr};
            bool read_cache_{true};
            int image_index_;
        };

如上所示,需要重载getBatchSize()、getBatch()、readCalibrationCache()、writeCalibrationCache()这四个函数,其中getBatchSize() 主要用于外部获取网络校准时的batch_size;getBatch() 主要用于获取每个batch经过数据预处理之后的网络输入张量数据,将输入存放至bindings中;readCalibrationCache() 主要用于读取calibration table的信息来创建INT8的推理引擎,将calibration table的信息存储到calibration cache,这样可以防止每次创建int推理引擎的时候都需要跑一次calibration,如果没有calibration table的话就会直接跳过这一步,之后调用writeCalibrationCache来创建calibration table;writeCalibrationCache() 主要是用于将calibration cache的信息写入到calibration table中。
函数实现部分如下:

 /**
         * @brief calibrator的构造函数,把calibration所需要的数据集准备好,需要保证数据集的数量可以被batchSize整除,同时由于calibration是在device上进行的,所以需要分配空间
         *
         * @param params
         */
        Int8EntropyCalibrator::Int8EntropyCalibrator(CalibratorParams &params) : params_(params), image_index_(0)
        {
            std::cout << "init Int8EntropyCalibrator " << std::endl;
            image_list_ = loadDataList(params_.calibration_set_path);                                           // 加载量化校准数据
            image_list_.resize(static_cast<int>(image_list_.size() / params_.batch_size) * params_.batch_size); // 量化校准的尺寸
            std::random_shuffle(image_list_.begin(), image_list_.end(),
                                [](int i)
                                { return rand() % i; });                                                     // 随机打乱数据
            CUDA_CHECK(cudaMalloc(&device_input_, params_.batch_size * params_.input_size * sizeof(float))); // 开辟数据数据的大小
            std::cout << "init success,images num" << image_list_.size() << std::endl;
        }

        /**
         * @brief  获取做calibration的时候的一个batch的图片,之后上传到device上,需要注意的是,这里面的一个batch中的每一个图片,都需要做与真正推理是一样的前处理。
         *
         * @param bindings   网络数据的输入
         * @param names
         * @param nbBindings
         * @return true
         * @return false
         */
        bool Int8EntropyCalibrator::getBatch(void *bindings[], const char *names[], int nbBindings) noexcept
        {
            if (image_index_ + params_.batch_size >= image_list_.size() + 1)
                return false;

            LOG("%3d/%3d (%3dx%3d): %s",
                image_index_ + 1, image_list_.size(), params_.input_h, params_.input_w, image_list_.at(image_index_).c_str());

            /*
             * 对一个batch里的所有图像进行预处理
             * 这里可有以及个扩展的点
             *  1. 可以把这个部分做成函数,以函数指针的方式传给calibrator。因为不同的task会有不同的预处理
             *  2. 可以实现一个bacthed preprocess
             * 这里留给当作今后的TODO
             */
            cv::Mat input_image;
            for (int i = 0; i < params_.batch_size; i++)
            {
                input_image = cv::imread(image_list_.at(image_index_++));
                TRT::ImagePreProcess::TransInfo trans;
                TRT::ImagePreProcess::AffineMatrix affine_matrix;
                TRT::ImagePreProcess::keepRatioResizeBGR2RGBNormalizeGpu(input_image, device_input_ + i * params_.input_size, params_.input_h, params_.input_w, params_.mean, params_.std, trans, affine_matrix);
            }

            bindings[0] = device_input_;
            std::cout << "get batch success" << std::endl;
            return true;
        }

        /**
         * @brief  读取calibration table的信息来创建INT8的推理引擎,将calibration table的信息存储到calibration cache,这样可以防止每次创建int推理引擎的时候都需要跑一次calibration,如果没有calibration table的话就会直接跳过这一步,之后调用writeCalibrationCache来创建calibration table
         *
         * @param length     读取的校准表的长度
         * @return const void*
         */
        const void *Int8EntropyCalibrator::readCalibrationCache(size_t &length) noexcept
        {
            void *output;
            calibration_cache_.clear();

            ifstream input(params_.calibration_table_path, ios::binary);
            input >> noskipws;
            if (read_cache_ && input.good())
                copy(istream_iterator<char>(input), istream_iterator<char>(), back_inserter(calibration_cache_));

            length = calibration_cache_.size();
            if (length)
            {
                LOG("Using cached calibration table to build INT8 trt engine...");
                output = &calibration_cache_[0];
            }
            else
            {
                LOG("Creating new calibration table to build INT8 trt engine...");
                output = nullptr;
            }
            return output;
        }

        /**
         * @brief 将calibration cache的信息写入到calibration table中
         *
         * @param cache  量化过程中的存储信息
         * @param length  量化信息的长度
         */
        void Int8EntropyCalibrator::writeCalibrationCache(const void *cache, size_t length) noexcept
        {
            ofstream output(params_.calibration_table_path, ios::binary);
            output.write(reinterpret_cast<const char *>(cache), length);
            output.close();
        }

  定义好calibrator类之后,需要在config对象中设定类的属性,将int8校准器注册进入配置参数中,以便后续engine对象进行调用,模型序列化操作。

        config->setFlag(nvinfer1::BuilderFlag::kINT8);
        config->setFlag(nvinfer1::BuilderFlag::kPREFER_PRECISION_CONSTRAINTS);
        config->setInt8Calibrator(calibrator.get());
        // 敏感层转为float形式,不使用int8
        for (int i = 0; i < network->getNbLayers(); i++)
        {
            auto layer = network->getLayer(i);
            if (layer->getName() == "/model.22/proto/cv3/act/Mul")  // 将table中的特定层设置为float类型,防止掉点严重。
            {
                layer->setPrecision(nvinfer1::DataType::kFLOAT);
                layer->setOutputType(0, nvinfer1::DataType::kFLOAT);
            }
         }

量化结果分析

  当出现量化掉点严重情况时,展开下述分析讨论:

  • 是否在input/output附近进行int8量化。
      如果是输入输出附近量化造成的掉点严重,一般所有输出头的预测结果都发生偏差。若存在个别head结果预测掉点严重,但是其余head正常,则一般不是该原因造成。
  • 若存在多任务(multi-task)实现,是否所有task都掉点严重。
      如果是所有的head输出结果均掉点严重,可能是输入输出量化、batch size设置存在问题以及数据集选用不正确等等。
  • 是否calibration数据集选用不正确。
  • calibration过程中,batch size选用是否存在问题。
  • calibration的方案选取是否合适。
      TensorRT一共提供了五种Calibrator类,对应IInt8Entropycalibrator2、IInt8MinMaxCalibrator、IInt8EntropyCalibrator、IInt8LegacyCalibrator、IInt8calibrator。
      当出现掉点后,将head输出打印观察,若发现部分较大的置信度值被截断,导致最终的预测结果发生变化,那么需要切换Calibrator类型,将Calibrator转为IInt8MinMaxCalibrator进行校准。
  • 某些计算是否需要进行量化。
  • 使用polygraphy分析
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值