TensorRT 양자화 및 CUDA 병렬 프로그래밍을 사용하여 0에서 1로 GPU 성능 활성화

TensorRT 연구 노트

예비 요약: TensorRT 모델 최적화 및 추론: 0에서 1로, GPU의 성능 활성화: TensorRT를 사용하여 딥 러닝 모델을 최적화 및 실행, TensorRT 입문 가이드 이 기사에서는
TensorRT 및 CUDA 병렬 컴퓨팅 프로그래밍에서 모델 양자화를 소개합니다.

TensorRT 모델 양자화

모델 양자화는 딥 러닝 모델 최적화를 위한 기술로, 딥 러닝 모델의 매개변수(예: 가중치 및 편향)를 부동 소수점 숫자에서 정수 또는 고정 소수점 숫자로 변환하는 프로세스입니다. 주요 목표는 모델의 매개변수를 32비트 부동 소수점(FP32)에서 8비트 정수(INT8) 또는 16비트 부동 소수점(FP16)과 같은 더 낮은 정밀도 형식으로 줄이는 것입니다. 이렇게 하면 모델의 저장 및 계산 비용을 줄일 수 있으므로 모델 압축 및 작업 가속화의 목적을 달성할 수 있습니다. int8 양자화와 같이 원래 모델에서 32bit에 저장된 숫자를 8bit로 매핑한 다음 계산하자(범위는 [-128,127]).

모델 양자화 작업은 세 가지 주요 이점을 가져올 수 있습니다.

  1. 축소된 모델 크기 : 모델의 가중치를 나타내는 데 더 적은 비트를 사용하면 모델의 크기를 크게 줄여 모델이 차지하는 메모리와 디스크 공간을 줄일 수 있습니다. 예를 들어 모델을 FP32에서 INT8로 변환하면 모델 크기를 4배로 줄일 수 있습니다.
  2. 추론 속도 향상 : 모델이 작을수록 순방향 전파(즉, 예측)를 수행할 때 처리할 데이터가 적습니다. 또한 GPU 및 전용 AI 가속기와 같은 일부 하드웨어는 단일 작업에서 여러 개의 낮은 정밀도 값을 처리하기 위한 특수 하드웨어 지원을 제공합니다. 이 두 요소 모두 모델의 추론 속도를 크게 높일 수 있습니다.32비트 부동 소수점 데이터에 한 번 액세스하면 int8 정수 데이터에 4번 액세스할 수 있습니다.
  3. 자원 소비 감소 : 모델이 작을수록 처리 속도가 빨라질 뿐만 아니라 필요한 에너지 소비도 줄어듭니다. 이는 모바일 장치 및 임베디드 시스템과 같은 리소스가 제한된 환경에서 모델을 실행하는 데 특히 중요합니다.

이 장점은 양자화 프로세스 중에 일부 정확도 정보가 손실될 수 있기 때문에 일부 모델 정확도를 희생하는 대가를 치르기도 합니다. 대부분의 경우 이러한 정확도 손실은 모델의 전체 성능에 거의 영향을 미치지 않기 때문에 허용됩니다. 그러나 높은 정밀도가 필요한 일부 응용 프로그램에서는 양자화가 성능 저하를 유발할 수 있습니다. 따라서 양자화 보정이 핵심 단계입니다 목표는 모델의 정확도를 최대한 유지하면서 32비트 부동 소수점 값을 낮은 정밀도 값(예: INT8)으로 변환하는 이상적인 매핑을 찾는 것입니다. 가능한.

모델 양자화 알고리즘 소개

일반적인 모델 양자화 알고리즘은 다음과 같습니다.

  1. 엔트로피 보정 : 엔트로피 보정은 동적 보정 알고리즘입니다. 엔트로피 보정 방법의 주요 아이디어는 양자화된 데이터 분포(INT8로 표현됨)와 원래 데이터 분포(FP32로 표현됨) 사이의 Kullback-Leibler(KL) 발산을 최소화하기 위한 최적의 양자화 임계값을 찾는 것입니다. 통계에서 KL 발산은 두 확률 분포 간의 차이를 측정한 것입니다. 따라서 이 방법의 목표는 양자화된 데이터 분포를 원래 데이터 분포에 최대한 가깝게 만드는 것입니다.
  2. Min-Max Calibration : 이 방법은 비교적 직관적이고 간단합니다. 보정 데이터 세트의 최소값과 최대값을 양자화 임계값으로 직접 사용하기 때문입니다. 즉, 최소값은 INT8의 최소값(예: -128)에 매핑되고 최대값은 INT8의 최대값(예: 127)에 매핑됩니다. 이 접근 방식은 계산 속도가 빠르다는 장점이 있지만 모든 경우에 최상의 성능을 달성하지는 못할 수 있습니다.
  3. 파라메트릭 보정 : 이 방법은 양자화 프로세스 중에 모델의 매개변수를 최적화하고, 양자화 프로세스 중에 파라메트릭 보정을 최적화하여 양자화된 모델 출력을 원래 모델 출력에 최대한 가깝게 유지합니다. 파라메트릭 캘리브레이션을 할 때 각 레이어의 파라미터를 미세 조정하여 양자화 오류를 최소화합니다. 하지만 각 매개변수를 최적화해야 하기 때문에 양자화 오차를 보다 정밀하게 제어할 수 있어 상대적으로 많은 양의 계산으로 이어진다. 정밀도가 중요한 시나리오에서는 파라메트릭 보정이 최선의 선택일 수 있습니다. 일부 응용 프로그램에서 이 방법은 엔트로피 보정 또는 최소-최대 보정보다 더 나은 결과를 얻을 수 있습니다.
  4. 백분위수 보정 : 백분위수 보정은 잡음이나 이상값이 있는 데이터에 대한 보정 방법입니다. 이 방법은 먼저 모든 데이터의 분포를 계산한 다음 이 임계값 이상의 데이터가 모든 데이터의 작은 비율(예: 1% 또는 0.1%)만 설명하는 임계값을 찾습니다. 노이즈 또는 이상치로 간주되는 이러한 데이터는 무시되고 다른 데이터는 양자화 매개변수를 계산하는 데 사용됩니다. 백분율 잘림 보정의 장점은 잡음이 있거나 이상치 데이터를 처리할 때 높은 정확도를 유지한다는 것입니다. 그러나 단점은 일부 중요한 정보가 손실될 수 있다는 것입니다. 특히 노이즈나 이상값으로 간주되는 이러한 데이터가 실제로 모델의 예측 결과에 상당한 영향을 미치는 경우 더욱 그렇습니다. 백분율 잘림 보정은 데이터에 많은 잡음이나 이상값이 있는 시나리오에 적합합니다.

이 경우 주로 TensorRT에서 엔트로피 보정과 최소-최대 보정 의 C++ 코딩을 소개할 것입니다.두 보정 방법 모두 데이터 분포를 통계로 보정하는 동안 추론을 수행하기 위한 일부 데이터를 준비해야 합니다. 교정 데이터 세트로 알려진 이러한 데이터는 정량화 프로세스의 핵심 부분입니다. 이 데이터 세트를 통해 TensorRT는 데이터 분포를 이해하고 이를 사용하여 부동 소수점 숫자에서 낮은 정밀도 표현으로 가중치 및 활성화를 줄이는 최선의 방법을 결정할 수 있습니다. 모델의 로딩 속도를 최적화하기 위해 TensorRT는 보정 테이블을 .cache 파일로 저장할 수 있습니다. TensorRT에서 INT8 양자화를 수행할 때 보정 데이터 세트는 보정 테이블을 생성하기 위해 처음 실행하는 동안 보정에 사용됩니다. 이 보정 테이블은 후속 모델 로드 및 추론을 위해 .cache 파일에 저장됩니다.

보정 캐시 테이블을 설계하면 두 가지 주요 이점이 있습니다.

  1. **모델 로딩 속도 향상:** 처음 실행할 때 보정 프로세스에 시간이 걸릴 수 있습니다. 그러나 보정 테이블을 생성하여 .cache 파일로 저장하면 후속 모델 로드 시 다시 보정할 필요 없이 이 .cache 파일을 바로 로드할 수 있습니다. 이렇게 하면 모델 로딩 속도가 크게 빨라질 수 있습니다.
  2. **일관성 보장:** 보정 테이블을 저장한 후 모델이 로드될 때마다 동일한 양자화 매개변수가 사용되어 추론의 일관성을 보장합니다.

이 .cache 파일은 원본 모델 및 보정 데이터 세트와 밀접하게 관련되어 있습니다. 즉, 동일한 .cache 파일은 모델이나 보정 데이터 세트가 모두 변경되지 않은 경우에만 사용할 수 있습니다. 모델 또는 보정 데이터 세트가 변경되면 다시 보정하고 새 .cache 파일을 생성해야 합니다.

캘리브레이션 데이터 세트를 준비할 때 일반 데이터는 대표성이 있어야 합니다. 즉, 최종 실제 착륙 장면에 부합해야 하는 데이터입니다. **교정 데이터 세트가 최종 실제 응용 프로그램 데이터를 나타내지 않는 경우 양자화 프로세스로 인해 모델의 정확도가 떨어질 수 있습니다. 실제 응용 분야에서는 일반적으로 정량화를 위해 500-1000개의 데이터가 준비됩니다(특정 수치는 모델 및 응용 분야에 따라 조정해야 할 수 있음). 예를 들어 모델이 이미지를 처리하는 데 사용되는 경우 보정 데이터 세트에는 다양한 장면, 조명 조건, 대상 개체 등을 비롯한 다양한 이미지가 포함되어야 합니다.

TensorRT는 최대 및 최소 보정을 달성합니다.

TensorRT에서 엔트로피 보정 또는 최소-최대 보정은 IInt8EntropyCalibrator2인터페이스 또는 인터페이스를 구현하여 IInt8MinMaxCalibrator수행할 수 있으며 몇 가지 가상 함수 메서드를 구현해야 합니다.

  • getBatch()방법: 교정 데이터 배치를 제공하는 데 사용됩니다.
  • readCalibrationCache()writeCalibrationCache()방법: 부팅할 때마다 보정 데이터를 다시 로드하지 않도록 캐싱 메커니즘을 구현합니다.

인터페이스는 INT8 모델의 오프라인 정적 보정을 위한 build.cu코드에서 구현됩니다 ( 결과 비교를 위해 엔트로피 보정으로 IInt8MinMaxCalibrator대체할 수 있음 ).IInt8EntropyCalibrator2

// 定义校准数据读取器
// 如果要用entropy的话改为:IInt8EntropyCalibrator2
class CalibrationDataReader : public IInt8MinMaxCalibrator
{
    
    
  ....
}
  • 생성자가 전달해야 하는 매개변수에는 데이터 디렉토리, 데이터 목록 및 BatchSize 가 포함됩니다 . 데이터 디렉토리는 교정 데이터가 저장되는 폴더 경로이고 데이터 목록은 교정 데이터 파일의 이름이 포함된 목록입니다. 이러한 매개변수를 전달하는 목적은 보정 데이터가 있는 위치와 데이터를 처리할 배치 크기를 보정기에 알리는 것입니다. 생성자에서 입력 텐서의 차원과 크기도 모델의 요구 사항에 따라 초기화되고 해당 메모리가 장치에 할당됩니다 . 이는 TensorRT가 추론 계산을 수행하는 데 이 정보가 필요하기 때문입니다.
CalibrationDataReader(const std::string& dataDir, const std::string& list, int batchSize = 1)
	: mDataDir(dataDir), mCacheFileName("weights/calibration.cache"), mBatchSize(batchSize), mImgSize(kInputH* kInputW)
{
    
    
	// 设置网络输入的尺寸
	mInputDims = {
    
     1, 3, kInputH, kInputW };
	// 计算输入的元素总数
	mInputCount = mBatchSize * samplesCommon::volume(mInputDims);
	// 初始化CUDA预处理环境,为图像大小分配空间
	cuda_preprocess_init(mImgSize);
	// 在设备上为批处理数据分配空间
	cudaMalloc(&mDeviceBatchData, kInputH * kInputW * 3 * sizeof(float));
	// 加载校准数据集文件列表
	std::ifstream infile(list);
	std::string line;
	while (std::getline(infile, line))
	{
    
    
		sample::gLogInfo << line << std::endl;
		mFileNames.push_back(line);
	}
	// 计算总批次数量
	mBatchCount = mFileNames.size() / mBatchSize;
	std::cout << "CalibrationDataReader: " << mFileNames.size() << " images, " << mBatchCount << " batches." << std::endl;
}
  • getBatch()방법의 작업은 보정 프로세스에 대한 데이터 배치를 제공하는 것입니다. 이 방법을 사용하려면 디스크에서 CPU 메모리로 보정 데이터의 현재 배치를 읽은 다음 GPU 장치 메모리로 복사해야 합니다. 이 과정은 딥러닝 모델의 정방향 전파 과정, 즉 입력 레이어에서 시작하여 각 숨겨진 레이어를 차례로 거쳐 출력 레이어에 도달하는 과정에 해당합니다.
bool getBatch(void* bindings[], const char* names[], int nbBindings) noexcept override
{
    
    
	// 检查是否还有更多批次的数据
	if (mCurBatch + 1 > mBatchCount)
	{
    
    
		return false;
	}

	// 每个图像的偏移量
	int offset = kInputW * kInputH * 3 * sizeof(float);
	for (int i = 0; i < mBatchSize; i++)
	{
    
    
		int idx = mCurBatch * mBatchSize + i;
		std::string fileName = mDataDir + "/" + mFileNames[idx];
		cv::Mat img = cv::imread(fileName);
		int new_img_size = img.cols * img.rows;
		if (new_img_size > mImgSize)
		{
    
    
			mImgSize = new_img_size;
			cuda_preprocess_destroy();      // 如果新图像的大小超过之前的内存空间,释放之前的内存
			cuda_preprocess_init(mImgSize); // 并重新分配适应新图像的内存
		}
		// 使用GPU处理输入图像,并把结果写入到设备内存
		process_input_gpu(img, mDeviceBatchData + i * offset);
	}
	for (int i = 0; i < nbBindings; i++)
	{
    
    
		if (!strcmp(names[i], kInputTensorName))
		{
    
    
			// 把设备内存的地址绑定到输入张量
			bindings[i] = mDeviceBatchData + i * offset;
		}
	}

	// 更新当前批次索引
	mCurBatch++;
	return true;
}
  • readCalibrationCache()이 방법의 목표는 캐시 파일에서 교정 캐시를 읽는 것입니다. 이 메서드는 캐시된 데이터에 대한 포인터와 캐시된 데이터의 크기를 반환합니다. 데이터가 캐시되지 않은 경우 반환됩니다 nullptr. 보정 캐싱은 여기에서 중요한 개념입니다. 모델 추론 속도를 향상시키기 위해 일반적으로 보정 프로세스의 결과를 저장하여 다음에 추론을 수행할 때 다시 보정할 필요 없이 저장된 보정 캐시를 직접 읽어서 추론의 효율성을 향상시킵니다. .
const void* readCalibrationCache(std::size_t& length) noexcept override 
{
    
    
	// 清空校准缓存
	mCalibrationCache.clear();

	// 以二进制形式打开缓存文件
	std::ifstream input(mCacheFileName, std::ios::binary);
	input >> std::noskipws;

	// 如果文件状态良好,即文件可读且没有其他错误
	if (input.good())
	{
    
    
		// 从输入流中拷贝数据到校准缓存
		std::copy(std::istream_iterator<char>(input), std::istream_iterator<char>(),
			std::back_inserter(mCalibrationCache));
	}

	// 获取缓存数据的大小
	length = mCalibrationCache.size();

	// 如果有缓存数据,则返回指向缓存数据的指针;否则返回 nullptr
	return length ? mCalibrationCache.data() : nullptr;
}
  • writeCalibrationCache()방법은 보정 캐시를 캐시 파일에 쓰는 것입니다. 캐시 데이터 포인터와 캐시 데이터의 크기를 파일 출력 스트림에 전달하고 캐시 파일에 기록해야 합니다. 이 프로세스는 다음 번에 바로 읽고 사용할 수 있도록 보정 프로세스의 결과를 실제로 저장합니다.
// writeCalibrationCache() 将校准缓存写入到缓存文件中
// 在该方法中,需要将缓存数据指针和缓存数据的大小传递给文件输出流,并将其写入到缓存文件中
void writeCalibrationCache(const void* cache, std::size_t length) noexcept override 
{
    
    
	// 将校准缓存写入到文件中
	std::ofstream output(mCacheFileName, std::ios::binary);
	output.write(reinterpret_cast<const char*>(cache), length);
}

특정 비즈니스 코드에서 먼저 현재 플랫폼이 INT8 추론을 지원하는지 확인합니다. 지원하지 않는 경우 경고 메시지가 출력되고 모델의 추론 정확도가 FP16모드로 설정됩니다. 이는 모델이 INT8을 지원하지 않는 플랫폼에서 여전히 추론을 수행할 수 있도록 하기 위한 것입니다. CalibrationDataReader그렇지 않으면 유형의 개체가 생성되고 calibratorINT8 교정기로 설정됩니다. 그런 다음 INT8 모드 플래그를 구성 개체 config에 설정합니다.

// 检查当前平台是否支持 INT8 推理
if (!builder->platformHasFastInt8())
{
    
    
	// 如果不支持 INT8 推理,则打印警告信息并将引擎设置为 FP16 模式
	sample::gLogInfo << "设备不支持int8." << std::endl;
	config->setFlag(nvinfer1::BuilderFlag::kFP16);
}
else
{
    
    
	// 如果支持 INT8 推理,创建一个 CalibrationDataReader 对象,并将其设置为 INT8 校准器
	auto calibrator = new CalibrationDataReader(calib_dir, calib_list_file);
	// 为配置对象设置 INT8 模式标志
	config->setFlag(nvinfer1::BuilderFlag::kINT8);
	// 设置 INT8 校准器
	config->setInt8Calibrator(calibrator);
}

전체 코드

#include "NvInfer.h"
#include "NvOnnxParser.h" 
#include "logger.h"
#include "common.h"
#include "buffers.h"
#include "cassert"
#include "utils/config.h"
#include "utils/preprocess.h"
#include "utils/types.h"

// 定义校准数据读取器, 最大最小值校准
// 如果要用熵校准entropy的话改为:IInt8EntropyCalibrator2
class CalibrationDataReader : public nvinfer1::IInt8MinMaxCalibrator
{
    
    
private:
    std::string mDataDir;
    std::string mCacheFileName;
    std::vector<std::string> mFileNames;
    int mBatchSize;
    nvinfer1::Dims mInputDims;
    int mInputCount;
    float *mDeviceBatchData {
    
     nullptr };
    int mBatchCount;
    int mImgSize;
    int mCurBatch{
    
    0};
    std::vector<char> mCalibrationCache;

private:
    void load_dataClassFile(const std::string& filepath)
    {
    
    
        std::ifstream ifile(filepath);
        std::string Line;
        while (std::getline(ifile, Line))
        {
    
    
            sample::gLogInfo << Line << std::endl;
            mFileNames.push_back(Line);
        }
        mBatchCount = mFileNames.size() / mBatchSize;
        std::cout << "CalibrationDataReader: " << mFileNames.size() 
                  << " images, " << mBatchCount << " batches." << std::endl;
    }

public:
    // 构造函数需要传递的参数包括数据目录、数据列表、BatchSize。
    // 通常会根据模型的需求,初始化输入张量的维度和大小,并在设备上分配相应的内存。
    CalibrationDataReader(const std::string& dataDir, const std::string& filepath, int batchSize = 1)
        : mDataDir(dataDir), mCacheFileName("weights/calibration.cache"),
          mBatchSize(batchSize), mImgSize(kInputH * kInputW)
    {
    
    
        mInputDims = {
    
    1, 3, kInputH, kInputW};
        mInputCount = mBatchSize * samplesCommon::volume(mInputDims);
        cuda_preprocess_init(mImgSize);
        cudaMalloc(&mDeviceBatchData, kInputH * kInputW * 3 * sizeof(float));
        load_dataClassFile(filepath);
    }

    int32_t getBatchSize() const noexcept override
    {
    
    
        return mBatchSize;
    }

    bool getBatch(void* bindings[], const char *names[], int nbBindings) noexcept override
    {
    
    
        if (mCurBatch + 1 > mBatchCount)
        {
    
    
            return false;
        }
        int offset = kInputW * kInputH * 3 * sizeof(float);
        for (int i = 0; i < mBatchSize; i++)
        {
    
    
            int idx = mCurBatch * mBatchSize + i;
            std::string filename = mDataDir + "/" + mFileNames[idx];
            cv::Mat image = cv::imread(filename);
            int new_img_size = image.cols * image.rows;
            if (new_img_size > mImgSize)
            {
    
    
                mImgSize = new_img_size;
                cuda_preprocess_destroy();
                cuda_preprocess_init(mImgSize);
            }
            process_input_gpu(image, mDeviceBatchData + i * offset);
        }
        for (int i = 0; i < nbBindings; i++)
        {
    
    
            if (!strcmp(names[i], kInputTensorName))
            {
    
    
                bindings[i] = mDeviceBatchData + i * offset;
            }
        }
        mCurBatch++;
        return true;
    }

    const void* readCalibrationCache(std::size_t& length) noexcept override
    {
    
    
        mCalibrationCache.clear();

        std::ifstream input(mCacheFileName, std::ios::binary);
        input >> std::noskipws;
        if (input.good())
        {
    
    
            std::copy(std::istream_iterator<char>(input), std::istream_iterator<char>(),
                      std::back_inserter(mCalibrationCache));
        }
        length = mCalibrationCache.size();
        return length ? mCalibrationCache.data() : nullptr;
    }

    void writeCalibrationCache(const void *cache, std::size_t length) noexcept override
    {
    
    
        std::ofstream output(mCacheFileName, std::ios::binary);
        output.write(reinterpret_cast<const char*>(cache), length);
    }
};


int main(int argc, char** argv)
{
    
    
    if (argc != 4)
    {
    
    
        std::cerr << "请输入onnx文件位置: ./build/[onnx_file] [calib_dir] [calib_list_file]" << std::endl;
        return -1;
    }
    // 命令行获取onnx文件路径、校准数据集路径、校准数据集列表文件
    char* onnx_file = argv[1];
    char* calib_dir = argv[2];
    char* calib_list_file = argv[3];
    // ========== 1. 创建builder:创建优化的执行引擎(ICudaEngine)的关键工具 ==========
    // 在几乎所有使用TensorRT的场合都会使用到IBuilder
    // 只要TensorRT来进行优化和部署,都需要先创建和使用IBuilder。
    std::unique_ptr<nvinfer1::IBuilder> builder = std::unique_ptr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));
    if (!builder)
    {
    
    
        std::cerr << "Failed to create build" << std::endl;
        return -1;
    } 
    std::cout << "Successfully to create builder!!" << std::endl;

    // ========== 2. 创建network:builder--->network ==========
    // 设置batch, 数据输入的批次量大小
    // 显性设置batch
    const unsigned int explicitBatch = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
    std::unique_ptr<nvinfer1::INetworkDefinition> network = std::unique_ptr<nvinfer1::INetworkDefinition>(builder->createNetworkV2(explicitBatch));
    if (!network)
    {
    
    
        std::cout << "Failed to create network" << std::endl;
        return -1;
    }

    // 创建onnxparser,用于解析onnx文件
    std::unique_ptr<nvonnxparser::IParser> parser = std::unique_ptr<nvonnxparser::IParser>(nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger()));
    // 调用onnxparser的parseFromFile方法解析onnx文件
    bool parsed = parser->parseFromFile(onnx_file, static_cast<int>(sample::gLogger.getReportableSeverity()));
    if (!parsed)
    {
    
    
        std::cerr << "Failed to parse onnx file!!" << std::endl;
        return -1;
    }
    // 配置网络参数
    // 我们需要告诉tensorrt我们最终运行时,输入图像的范围,batch size的范围。这样tensorrt才能对应为我们进行模型构建与优化。
    nvinfer1::ITensor* input = network->getInput(0); // 获取了网络的第一个输入节点。
    nvinfer1::IOptimizationProfile* profile = builder->createOptimizationProfile(); // 创建了一个优化配置文件。
    // 网络的输入节点就是模型的输入层,它接收模型的输入数据。
    // 在 TensorRT 中,优化配置文件(Optimization Profile)用于描述模型的输入尺寸和动态尺寸范围。
    // 通过优化配置文件,可以告诉 TensorRT 输入数据的可能尺寸范围,使其可以创建一个适应各种输入尺寸的优化后的模型。

    // 设置最小尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims4(1, 3, 640, 640));
    // 设置最优尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims4(1, 3, 640, 640));
    // 设置最大尺寸
    profile->setDimensions(input->getName(), nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims4(1, 3, 640, 640));

    // ========== 3. 创建config配置:builder--->config ==========
    // 配置解析器
    std::unique_ptr<nvinfer1::IBuilderConfig> config = std::unique_ptr<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
    if (!config)
    {
    
    
        std::cout << "Failed to create config" << std::endl;
        return -1;
    }
    // 添加之前创建的优化配置文件(profile)到配置对象(config)中
    // 优化配置文件(profile)包含了输入节点尺寸的设置,这些设置会在模型优化时被使用。
    config->addOptimizationProfile(profile);
    // 设置精度
    if (!builder->platformHasFastInt8())
    {
    
    
        sample::gLogInfo << "设备不支持int8,本次将默认使用int16" << std::endl;
        config->setFlag(nvinfer1::BuilderFlag::kFP16);
    }
    else {
    
    
        sample::gLogInfo << "设备支持int8,本次将使用int8量化" << std::endl;
        auto calibrator = new CalibrationDataReader(calib_dir, calib_list_file);
        config->setFlag(nvinfer1::BuilderFlag::kINT8);
        config->setInt8Calibrator(calibrator);
    }

    // config->setFlag(nvinfer1::BuilderFlag::kFP16);
    builder->setMaxBatchSize(1);
    config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1 << 30);

    // 创建流,用于设置profile
    auto profileStream = samplesCommon::makeCudaStream();
    if (!profileStream)
    {
    
    
        std::cerr << "Failed to create CUDA profileStream File" << std::endl;
        return -1;
    }
    config->setProfileStream(*profileStream);

    // ========== 5. 序列化保存engine ==========
    // 使用之前创建并配置的 builder、network 和 config 对象来构建并序列化一个优化过的模型。
    std::unique_ptr<nvinfer1::IHostMemory> plan = std::unique_ptr<nvinfer1::IHostMemory>(builder->buildSerializedNetwork(*network, *config));
    std::ofstream engine_file("./weights/best.engine", std::ios::binary);
    assert(engine_file.is_open() && "Failed to open engine file");
    engine_file.write((char *)plan->data(), plan->size());
    engine_file.close();

    // ========== 6. 释放资源 ==========
    std::cout << "Engine build success!" << std::endl;
    return 0;
}

CUDA-GPU 병렬 컴퓨팅: 이론에서 이미지 처리 실습까지

CUDA-GPU 개발 소개

CUDA는 효율적인 병렬 컴퓨팅을 위해 GPU(그래픽 처리 장치)를 사용할 수 있는 NVIDIA에서 출시한 병렬 컴퓨팅 플랫폼 및 프로그래밍 모델입니다. CUDA로 프로그래밍하면 이미지 처리, 과학 컴퓨팅, 머신 러닝, 딥 러닝 등과 같은 계산 집약적인 애플리케이션의 성능을 향상시킬 수 있습니다. 직렬 컴퓨팅에 CPU를 사용하는 것과 비교하여 병렬 컴퓨팅에 GPU를 사용하면 컴퓨팅 속도와 효율성을 크게 향상시킬 수 있습니다. 이미지 처리 또는 딥 러닝에서는 일반적으로 이미지 크기 조정, 정규화, 채널 교환 등과 같은 이미지 전처리를 수행합니다. 이러한 작업은 이미지의 각 픽셀을 처리해야 하므로 CPU를 직렬 처리에 사용하면 많은 컴퓨팅 리소스와 시간을 소비하게 됩니다. 따라서 처리 속도와 효율성을 향상시키기 위해 일반적으로 병렬 처리에 CUDA를 사용합니다 .

CUDA 프로그래밍 단계 개요

CUDA 프로그래밍의 기본 단계는 다음 부분으로 요약할 수 있습니다.

  1. 커널 함수 정의 : 커널 함수는 GPU에서 실행되는 병렬 코드이며 __global__함수로 정의됩니다. 이러한 함수는 일반적으로 입력 배열의 단일 요소에서 작동합니다. 간단한 커널 함수는 픽셀 색상 변경과 같은 간단한 작업을 수행할 수 있습니다. 예를 들어 간단한 벡터 추가 커널 함수는 다음과 같습니다.
__global__ void vectorAdd(const float *A, const float *B, float *C, int numElements)
{
    int i = blockDim.x * blockIdx.x + threadIdx.x;

    if (i < numElements)
    {
        C[i] = A[i] + B[i];
    }
}

이 예에서 blockIdx.x및 는 threadIdx.x각각 현재 블록 인덱스와 스레드 인덱스를 나타내는 CUDA에서 제공하는 내장 변수입니다.

  1. 메모리 할당 및 데이터 초기화 : CUDA는 각각 GPU에 메모리를 할당하고 CPU(호스트라고도 함)에서 GPU(장치라고도 함)로 데이터를 복사하기 위한 및 와 같은 API 기능을 제공 cudaMalloc()합니다 . cudaMemcpy()예를 들어:
int numElements = 50000;
size_t size = numElements * sizeof(float);
float *d_A = nullptr;
cudaMalloc((void **)&d_A, size);

이 예제에서는 먼저 할당해야 하는 메모리 크기를 계산한 다음 cudaMalloc()함수를 사용하여 GPU에 메모리를 할당합니다. d_AGPU 메모리를 가리키는 장치 포인터입니다.

  1. 커널 기능 시작<<<...>>> : 구문을 사용하여 커널 기능을 시작합니다 . 구문의 매개변수는 커널을 시작할 pthread 그리드의 크기를 지정합니다. 예를 들어:
int threadsPerBlock = 256;
int blocksPerGrid =(numElements + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, numElements);

이 예에서는 처리할 요소 수에 따라 스레드 그리드의 크기를 조정합니다.

  1. GPU에서 다시 호스트로 결과 복사 : 계산이 완료되면 cudaMemcpy()함수를 사용하여 GPU 메모리에서 다시 CPU 메모리로 결과를 복사할 수 있습니다. 예를 들어:
float *h_C = (float *)malloc(size);
cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost);

이 예제에서는 먼저 결과 배열을 위해 CPU에 메모리를 할당한 다음 cudaMemcpy()함수를 사용하여 GPU에서 다시 CPU로 결과를 복사합니다.

  1. 여유 메모리 : 마지막으로 함수를 사용하여 cudaFree()GPU 메모리를 확보하고 표준 C 또는 C++ 함수를 사용하여 CPU 메모리를 확보합니다. 예를 들어:
cudaFree(d_A);
cudaFree(d_B);
cudaFree(d_C);
free(h_A);
free(h_B);
free(h_C);

이 예에서는 GPU 및 CPU에 할당된 모든 메모리를 해제합니다.

CUDA 실행 단위 - 스레드 블록

CUDA 프로그래밍 모델에서 코드는 스레드 수준에서 병렬로 작성됩니다 . CUDA 프로그래밍에서 CUDA 커널은 많은 스레드(스레드)로 구성되며 이러한 스레드는 하나 이상의 블록(블록)으로 구성될 수 있으며 이러한 블록은 하나 이상의 그리드(그리드)로 구성될 수 있습니다 . 아래 그림과 같이:

이미지
  • 쓰레드 : 쓰레드는 CUDA에서 가장 기본적인 실행 단위로 각 쓰레드는 같은 연산을 수행하지만 연산의 데이터는 다르다.
  • BLock : 쓰레드 블록은 쓰레드의 집합체로, 모든 쓰레드는 같은 쓰레드 블록의 공유 메모리를 공유 하고 쓰레드 블록 내에서 동기화를 통해 통신할 수 있다.
  • 그리드 : 그리드는 스레드 블록의 집합체입니다. 그리드의 모든 스레드 블록은 동시에 실행될 수 있습니다 . 각 스레드 블록의 스레드는 서로 독립적이며 블록 간에는 직접적인 통신이 없습니다.

그리드는 여러 블록을 포함할 수 있고 블록은 1차원, 2차원 또는 3차원일 수 있으며 블록의 스레드는 1차원, 2차원 또는 3차원일 수도 있습니다. 각 스레드에는 서로 다른 데이터 및 메모리 위치에 액세스하는 데 사용할 수 있는 고유한 스레드 ID가 있습니다. 동일한 스레드 블록에서 스레드 ID는 0부터 시작하여 연속적으로 번호가 지정되며 threadIdx내장 변수를 통해 얻을 수 있습니다.

// 获取本线程的索引,blockIdx 指的是线程块的索引,blockDim 指的是线程块的大小,threadIdx 指的是本线程块中的线程索引
int tid = blockIdx.x * blockDim.x + threadIdx.x;    

CUDA 프로그래밍에서는 일반적으로 GPU 컴퓨팅 성능을 최대한 활용하기 위해 컴퓨팅 작업의 특성에 따라 블록 및 스레드의 수와 크기를 조정해야 합니다. 예를 들어 대규모 병렬 컴퓨팅 작업의 경우 GPU의 병렬 처리 기능을 최대한 활용하기 위해 더 많은 스레드와 스레드 블록을 사용할 수 있습니다. 덜 계산 집약적인 작업의 경우 더 적은 수의 스레드와 스레드 블록을 사용하는 것이 더 효율적일 수 있습니다.

// 计算需要的线程总量(高度 x 宽度):640*640=409600
int jobs = dst_height * dst_width;
// 一个线程块包含256个线程
int threads = 256;
// 计算线程块的数量
int blocks = ceil(jobs / (float)threads);

// 调用kernel函数
preprocess_kernel<<<blocks, threads>>>(img_buffer_device, dst, dst_width, dst_height, jobs); // 函数的参数

CUDA 커널 커널 커널 기능

CUDA 프로그래밍에서 핵심 개념은 GPU에서 수행되는 병렬 컴퓨팅의 엔터티인 커널 함수입니다. 이러한 함수는 특수 호출 구문을 통해 GPU에서 병렬로 실행되는 스레드를 시작합니다. 커널 기능이 시작되면 각 스레드는 동일한 코드를 실행하여 대규모 병렬 처리를 허용합니다.

커널 함수를 표시하는 방법은 __global__키워드를 사용하여 이 함수가 CPU가 아닌 GPU에서 실행됨을 컴파일러에 알리는 것입니다. 그 외에 커널 함수는 일반 함수와 유사하며 입력 및 출력 매개변수를 가질 수 있고 제어 흐름 및 로컬 변수를 가질 수 있으며 다른 함수를 호출할 수도 있습니다.

커널 함수 내에서 CUDA 는 각 스레드의 특정 위치와 컨텍스트를 이해하는 데 도움이 되는 , threadIdxblockIdx같은 몇 가지 기본 제공 변수를 제공합니다 . blockDim이러한 변수를 사용하여 병렬 작업의 실행 경로를 효과적으로 제어할 수 있습니다.

커널 기능을 시작하려면 특수 구문이 필요합니다 <<<...>>>. 이 구문에서 첫 번째 매개변수는 스레드 블록(block)의 수를 지정하는 것이고, 두 번째 매개변수는 각 스레드 블록의 스레드 수를 지정하는 것입니다. 이러한 매개변수는 정수 또는 유형일 수 있으며 dim3후자는 x, y, z 방향의 스레드 수를 지정할 수 있습니다 . 정수만 제공되면 x 방향의 스레드 수로 해석되며 y 및 z 방향의 스레드 수는 기본적으로 1입니다.

다음은 간단한 벡터 추가 커널 함수의 간단한 예입니다.

// 向量加法
__global__ void add(int *a, int *b, int *c, int N)
{
    
       
    // 获取本线程块的索引,blockIdx 指的是线程块的索引,blockDim 指的是线程块的大小,threadIdx 指的是线程的索引
    int tid = blockIdx.x * blockDim.x + threadIdx.x;    
    if (tid < N)
        c[tid] = a[tid] + b[tid];
}

여기서 함수는 커널 함수임을 나타내기 위해 add로 표시됩니다 . __global__함수 내에서 blockIdx.x, blockDim.x및 를 사용하여 threadIdx.x고유한 스레드 ID를 계산하므로 각 스레드가 요소를 독립적으로 처리할 수 있습니다.

그런 다음 이 커널 함수를 다음과 같이 호출할 수 있습니다.

// 调用kernel函数
add<<<n_blocks, n_threads>>>(dev_a, dev_b, dev_c, N); 

그 중 n_blocks및 는 각각 스레드 블록 n_threads수와 각 스레드 블록의 스레드 수이며, , dev_a커널 함수에 전달되는 매개변수입니다.dev_bdev_cN

CUDA 커널 함수 샘플 코드

이 CUDA 코드는 주로 CUDA를 사용하여 간단한 병렬 계산을 수행하는 방법을 보여주고 동일한 계산에 대해 CPU와 GPU 간의 시간 차이를 비교합니다. 주로 다음 작업을 수행합니다.

  1. GPU 병렬 함수(커널 함수) 정의add : 이 함수의 목적은 두 개의 정수 배열에 요소별로 추가하는 것입니다. 이 함수는 두 개의 입력 배열 ab하나의 출력 배열을 사용하며 c모두 GPU에 저장됩니다. 동시에 이 함수는 N배열의 크기를 나타내는 매개변수도 받습니다. 함수에서 각 스레드의 인덱스는 tid현재 스레드가 위치한 스레드 블록의 인덱스 blockIdx.x, 스레드 블록의 크기 blockDim.x및 자신이 위치한 스레드 블록 내의 스레드 인덱스 로 threadIdx.x결정됩니다 . tid보다 작으면 스레드 N해당 요소를 에 추가하고 에 저장 a합니다 .bc
  2. 함수 main에서 일련의 작업을 수행합니다 .
    • 명령줄 인수 확인 : 명령줄 인수의 수가 2가 아니면 프로그램은 오류 메시지를 출력하고 종료합니다.
    • 데이터 초기화 : 먼저 CPU 메모리에서 두 개의 배열 a과 합계를 초기화합니다 b. 여기서 각 요소의 값은 해당 인덱스와 같습니다. 그런 다음 cudaMalloc함수를 사용하여 배열 및 에 대한 GPU 메모리 공간을 a할당 b합니다 c.
    • CPU에 배열 추가 : 프로그램은 먼저 CPU에 배열 ab요소를 추가하고 그 결과를 배열에 저장합니다 c. 또한 CUDA 이벤트를 사용하여 이 프로세스의 경과 시간을 측정합니다.
    • GPU에서 배열 추가a : 프로그램은 CPU 메모리 에서 GPU 메모리로 배열 합계의 내용을 복사한 b다음 GPU 병렬 함수를 호출하여 합계를 요소별로 추가하고 결과를 adda저장 합니다 . 이 함수는 스레드 블록 으로 구성된 GPU 병렬 구성을 사용하며 각 스레드 블록에는 스레드가 포함됩니다. 동시에 프로그램은 CUDA 이벤트를 사용하여 이 프로세스의 경과 시간을 측정합니다.bcaddn_blocksn_threads
    • CPU와 GPU 연산 결과 일치 여부 확인 : 프로그램은 GPU 연산 결과를 다시 CPU 메모리로 복사하여 CPU 연산 결과와 일치하는지 확인합니다.
    • 무료 GPU 메모리 : 마지막으로 프로그램은 함수를 사용하여 , cudaFree에 대해 GPU에 할당된 메모리를 해제합니다.abc
#include <stdio.h>

__global__ void add(int *a, int *b, int *c, int N)
{
    
    
    // 获取本线程的索引,blockIdx 指的是线程块的索引,blockDim 指的是线程块的大小,threadIdx 指的是本线程块中的线程索引
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    // printf("tid: %d blockIdx.x: %d blockDim.x: %d threadIdx.x: %d \n", tid, blockIdx.x, blockDim.x, threadIdx.x);
    if (tid < N)
        c[tid] = a[tid] + b[tid];
}

int main(int argc, char **argv)
{
    
    
    // 检查命令行参数
    if (argc != 2)
    {
    
    
        fprintf(stderr, "Usage: ./test <N>");
    }
    int N = std::atoi(argv[1]);
    int a[N], b[N], c[N], c_from_gpu[N];
    int *dev_a, *dev_b, *dev_c;

    // 在设备端分配内存
    cudaMalloc((void **)&dev_a, N * sizeof(int));
    cudaMalloc((void **)&dev_b, N * sizeof(int));
    cudaMalloc((void **)&dev_c, N * sizeof(int));

    // 初始化数组
    for (int i = 0; i < N; i++)
    {
    
    
        a[i] = i;
        b[i] = i;
    }

    // 统计CPU上运行时间
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start, 0);
    for (int i = 0; i < N; i++)
    {
    
    
        c[i] = a[i] + b[i];
    }
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    float time;
    cudaEventElapsedTime(&time, start, stop);
    printf("Time spent on CPU: %f ms\n", time);

    // 将数据从主机端复制到设备端
    cudaMemcpy(dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice);

    // 调用kernel函数,在GPU上运行并发计算
    // 一个线程块包含256个线程
    int n_threads = 256;
    // 计算线程块的数量
    int n_blocks = std::ceil(N * 1.0f / n_threads);

    // 统计时间
    cudaEventRecord(start, 0);
    // 调用kernel函数,传递线程块数量和大小
    add<<<n_blocks, n_threads>>>(dev_a, dev_b, dev_c, N); 
    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Time spent on GPU: %f ms\n", time);

    // 将数据从设备端复制到主机端
    cudaMemcpy(c_from_gpu, dev_c, N * sizeof(int), cudaMemcpyDeviceToHost);

    // 检查结果是否一致
    for (int i = 0; i < N; i++)
    {
    
    
        if (c[i] != c_from_gpu[i])
        {
    
    
            printf("Error: inconsistent results!\n");
        }
    }

    // 释放设备端内存
    cudaFree(dev_a);
    cudaFree(dev_b);
    cudaFree(dev_c);

    return 0;
}
# ./build/test 500000
Time spent on CPU: 2.163136 ms
Time spent on GPU: 0.029248 ms
createInferRuntime
Runtime创建成功
deserializeCudaEngine
Engine创建成功
createExecutionContext
Context创建成功
BufferManager类
缓冲区创建成功
VideoCapture类
cuda_preprocess_init
视频读取成功
GPU内存申请成功
process_input
executeV2
copyOutputToHost
getHostBuffer
yolo_nms
创建推理运行时的runtime
读取模型文件并反序列生成engine
创建执行上下文
创建输入输出缓冲区
读入视频与申请GPU内存
图像预处理 推理与结果输出
IRuntime 实例
优化阶段和推理阶段
ICudaEngine 实例
推理的执行
IExecutionContext 实例
执行推理
输入/输出缓冲区
执行模型推理, 并获取推理结果
读入视频流
申请GPU内存
准备视频推理
利用GPU进行处理
图像预处理
模型推理
结果提取
从buffer中提取推理结果
非极大值抑制,得到最后的检测框

요약하다

우리 기사가 마음에 들거나 소스 코드의 전체 텍스트가 필요한 경우 VX 공개 수직 계정을 팔로우할 수 있습니다: 01 프로그래밍 하우스, 소스 코드의 전체 텍스트를 얻으려면 tensorrt를 보내십시오.

추천

출처blog.csdn.net/weixin_43654363/article/details/131886371