量化校准
根据所需的量化参数可以分为:权重量化和权重激活量化。
- 权重量化 :仅对网络中的权重执行量化,由于网络权重一般保存下来的,因此提前根据权重便可以计算出相应的量化参数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 ¶ms);
~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 ¶ms) : 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分析