CUDA和TensorRT入门

CUDA

官方教程:CUDA C++ Programming Guide (nvidia.com)

一、基础知识

首先看一下显卡、GPU、和CUDA的关系介绍:

显卡、GPU和CUDA简介_吴一奇的博客-CSDN博客

延迟:一条指令返回的时间间隔;

吞吐量:单位时间内处理的指令数量;

CPUs

ppIMVc8.png

设计是按照延迟导向设计的;

主要有以下几个特点:

1、内存大:多级缓存结构提高访存速度;

2、控制复杂:分支预测机制(if-else判断)、流水线数据前送;

3、运算单元强大:整型浮点型复杂运算速度快;

GPUs

ppIMu7j.png

设计是按照吞吐导向设计的;

主要有以下几个特点:

1、缓存小:提高内存吞吐;

2、控制简单:没有分支预测,没有数据转发;

3、精简运算单元:需要大量线程来容忍延迟,流水线实现高吞吐量;

注意:显存其实和内存一样,也是用来暂存资料的存储空间,不过显存是帮GPU存储的,而内存是帮CPU存储的;

总结:

CPU相比于GPU,单条复杂指令延迟快10倍以上;

GPU相比于CPU,单位时间执行指令数量10倍以上;

思考:

什么样的问题适合GPU?

计算密集:数值计算的比例要远大于内存操作,因此内存访问的延迟可以被计算掩盖;

数据并行:大任务可以拆解成执行相同指令的小任务,因此对复杂流程的需求控制较低;

CUDA

CUDA:由英伟达公司2007年开始推出,初衷是为GPU增加一个易用的编程接口,让开发者无需学习复杂的着色语言或者图形处理原语;

OpenCL:是2008年发布的异构平台并行编程的开放标准,也是一个编程框架;

下面是CUDA编程的具体结构图:

在这里插入图片描述

其中Device代表GPU,Host代表CPU,Kernel代表GPU上运行的函数;

术语:内存模型的层次:

  • 每个线程处理器(SP)都有自己的registers(寄存器)
  • 每个SP都有自己的local memory(局部内存),寄存器和局部内存只能被线程自己访问;
  • 每个多核处理器(SM)内都有自己的shared memory(共享内存),可以被线程块内所有线程访问;
  • 一个GPU的所有SM共有一块global memory(全局内存),不同线程块的线程都可使用;

术语:软件:

在CUDA中,具体对应结构如下

线程处理器(SP)对应线程;

多核处理器(SM)对应线程块;

设备端(device)对应线程块组合体;

一个kernel一次只能在一个GPU上执行;

线程块的概念:

将线程数组分成多个块,块内的线程通过共享内存、原子操作和屏障同步进行协作,不同块中的线程不能协作;

网格(grid)并行线程块组合的概念:

CUDA中的核函数由线程网格执行,每个线程都有一个索引,用于计算内存地址和做出控制决策;

对于每个线程需要处理的数据,是通过对线程块和线程定义具体的id,根据索引来决定要处理的数据;

线程束(warp)概念:

SM采用的SIMT(单指令多线程)架构,warp(线程束)是最基本的执行单元,一个warp包含32个并行thread,这些thread以不同数据资源执行相同的指令,warp本质是线程在GPU上运行的最小单元;

当一个kernel被执行时,grid中的线程块被分配到SM上,一个线程块的thread只能在一个SM上调度,SM一般可以调度多个线程块,大量thread可能被分到不同的SM上,每个thread拥有它自己的程序计数器和状态寄存器,并且用该线程自己的数据执行指令,这就是所谓的SIMT;

由于warp的大小为32,所以block所含的thread的大小一般要设置为32的倍数;

在这里插入图片描述

案例:向量相加

说明:向量相加满足GPU运行的条件,计算简单且支持并行,内存访问也少;

在CPU中实现两个向量的相加:

void vecAdd(float* A, float* B, float* C, int n)
{
    for (i = 0, i < n, i++)
    C[i] = A[i] + B[i];
}

需要利用循环,依次对两个向量中的数据进行相加,再将结果保存在新的向量中;

在GPU中实现两个向量的相加:

主要分为以下几个步骤:

  • 为数据分配内存空间

    cudaError_t cudaMalloc (void **devPtr, size_t size):两个参数为地址、申请内存大小;

    作用:在设备全局内存中分配对象;

    cudaError_t cudaMalloc (void **devPtr, size_t size):参数为需要释放的指针对象地址

    作用:从设备全局内存中释放对象;

  • 定义核函数(计算函数)

  • 数据传输

    这里涉及到一个数据拷贝,函数为cudaError_t cudaMemcpy (void *dst, const void *src, size_t count, cudaMemcpyKind kind)

    cudaMemcpyKind 支持的四种选项:cudaMemcpyHostToDevice、cudaMemcpyDeviceToHost、cudaMemcpyDeviceToDevice

    作用:内存数据在主机端和设备端的传输;

核函数的定义和调用:

  • 在GPU上执行的函数;
  • 一般通过标识符__ global __修饰;
  • 函数调用通过<<<参数1,参数2>>>,参数用于说明核函数中的线程数量,以及线程的组织;
  • 以网格(grid)的形式组织,每个线程格由若干个线程块(block)组成,而每个线程块又由若干个线程(thread)组成;
  • 调用时必须声明内核函数的执行参数;
  • 在编程时,必须先为kernel函数中用到的数组或变量分配好足够的空间,再调用kernel函数,否则在GPU计算时会发生错误;

CUDA编程的标识符号:

__ global __ : 核函数的返回一定要用void,也是最常见的;

在这里插入图片描述

CUDA编程流程:

在这里插入图片描述

主要有几种编译的方式,比如逐文件编译,整个cuda文件编译成动态库,以及cmake编译(最常用)!

代码实现:

CPU中向量相加的实现:

void vecAdd(float* A, float* B, float* C, int n) {
    for (int i = 0; i < n; i++) {
        C[i] = A[i] + B[i];
    }
}

GPU中向量相加的实现:

void vecAddKernel(float* A_d, float* B_d, float* C_d, int n)
{
    int i = threadIdx.x + blockDim.x * blockIdx.x;
    if (i < n) C_d[i] = A_d[i] + B_d[i];
}

// 下面是调用,上面是定义核函数
vecAddKernel <<< blockPerGrid, threadPerBlock >>> (da, db, dc, n);

在实际环境下编译后运行两个代码看看耗时:

在这里插入图片描述

可以看出,在飞浆的环境下,十万维度的向量,GPU会比CPU快上9倍左右;

案例:矩阵相乘

矩阵相乘作为深度学习任务中最常见的计算,也是GPU优化的重点;

正常的矩阵乘法是行和列之间的相乘得到一个元素,也就是一个线程负责计算一个元素;

在这里插入图片描述

这里存在最主要的问题就是,数据的读取过于频繁,将会浪费大量时间在读取数据上;

优化思路:

将多次访问的数据放到共享内存中,减少重复读取的次数,充分利用共享内存的延迟低的优势;

CUDA中的内存读取速度:

  • 各自线程寄存器(1周期)
  • 线程块共享内存(5周期)
  • Grid全局内存(500周期)
  • Grid常量内存(5周期)

在这里插入图片描述

CUDA中的共享内存:

概念:一种特殊类型的内存,内容在源码中被显式声明和使用;

(位于处理器中,以更高速度访问,被内存访问指令访问,别名暂存存储器)

特点:

  • 读取速度等同于缓存,在很多显卡上,缓存和内存使用的是同一块硬件,并且可以配置大小;
  • 共享内存属于线程块,可以被一个线程块内所有线程访问;
  • 共享内存有两种申请空间方式,静态申请和动态申请;
  • 共享内存大小只有十几K,过度使用共享内存会降低程序的并行性;

使用方法:

  • 使用__ shared __ 关键字;
  • 注意数据存在交叉,应该将边界上的数据拷贝进来;

这里有个线程同步的函数——__syncthreads():

概念:是cuda的内建函数,用于块内线程通信;

申请共享内存的两种方式:

1、静态方式:

__shared__ int s[64];

共享内存大小明确;

2、动态方式:

__shared__ int s[64];

共享内存大小不明确;

平铺矩阵乘法:

原理:将内核的执行分解为多个阶段,使每个阶段的数据访问集中在一个(Md和Nd)的子集上;

在这里插入图片描述

当然,需要使用内置函数__syncthreads()来确保平铺矩阵中的所有元素都被加载使用;

理论上加速的比例:原始矩阵乘法需要从全局内存中取2mnk次,平铺矩阵乘法只需要取2mnk/block_size次,加速了block_size倍,考虑到同步函数和共享内存的读写,实际上加速效率比这个低;

代码实现:

CPU下矩阵相乘的代码:

void  multiplicateMatrixOnHost(float *array_A, float *array_B, float *array_C, int M_p, int K_p, int N_p)
{
	for (int i = 0; i < M_p; i++)
	{
		for (int j = 0; j < N_p; j++)
		{
			float sum = 0;
			for (int k = 0; k < K_p; k++)
			{
				sum += array_A[i*K_p + k] * array_B[k*N_p + j];
			}
			array_C[i*N_p + j] = sum;
		}
	}
}

GPU下矩阵相乘代码:

// 下面是在GPU上不适用共享内存的实现
__global__ void multiplicateMatrixOnDevice(float *array_A, float *array_B, float *array_C, int M_p, int K_p, int N_p)
{
	int ix = threadIdx.x + blockDim.x*blockIdx.x;//row number
	int iy = threadIdx.y + blockDim.y*blockIdx.y;//col number

	if (ix < N_p && iy < M_p)
	{
		float sum = 0;
		for (int k = 0; k < K_p; k++)
		{
			sum += array_A[iy*K_p + k] * array_B[k*N_p + ix];
		}
		array_C[iy*N_p + ix] = sum;
	}
}

// 下面是在GPU上使用共享内存的实现
__global__ void matrixMultiplyShared(float *A, float *B, float *C,
	int numARows, int numAColumns, int numBRows, int numBColumns, int numCRows, int numCColumns)
{
	__shared__ float sharedM[BLOCK_SIZE][BLOCK_SIZE];
	__shared__ float sharedN[BLOCK_SIZE][BLOCK_SIZE];

	int bx = blockIdx.x;
	int by = blockIdx.y;
	int tx = threadIdx.x;
	int ty = threadIdx.y;

	int row = by * BLOCK_SIZE + ty;
	int col = bx * BLOCK_SIZE + tx;

	float Csub = 0.0;

	for (int i = 0; i < (int)(ceil((float)numAColumns / BLOCK_SIZE)); i++)
	{
		if (i*BLOCK_SIZE + tx < numAColumns && row < numARows)
			sharedM[ty][tx] = A[row*numAColumns + i * BLOCK_SIZE + tx];
		else
			sharedM[ty][tx] = 0.0;

		if (i*BLOCK_SIZE + ty < numBRows && col < numBColumns)
			sharedN[ty][tx] = B[(i*BLOCK_SIZE + ty)*numBColumns + col];
		else
			sharedN[ty][tx] = 0.0;
		__syncthreads();			// 线程同步

		for (int j = 0; j < BLOCK_SIZE; j++)
			Csub += sharedM[ty][j] * sharedN[j][tx];
		__syncthreads();			// 线程同步
	}

	if (row < numCRows && col < numCColumns)
		C[row*numCColumns + col] = Csub;
}

// 在cuda的内置库中,可以直接通过cublasSgemm()这个函数实现矩阵在共享内存中的相乘

下面是运行时间:

在这里插入图片描述

结论:

可以看出来GPU确实有实现一定效率的提升,但提升的并不多,这是因为这里的时间计算将数据的拷贝也包含了进去,如果在大的矩阵相乘的情况下,那么数据拷贝的时间可以忽略不计,而计算的效率可以看出GPU是比CPU快上很多的;

二、进阶学习

CUDA Stream

概念:CUDA Stream是GPU上task的执行队列,所有CUDA操作(kernel,内存拷贝等)都是在stream上执行的;

种类:

1、隐式流:又称为默认流、NULL流;

所有的CUDA操作默认运行在隐式流里,隐式流里的GOU和CPU端计算是同步的,也就是串行的;

在这里插入图片描述

2、显示流:显示申请的流;

显示流里的GPU task和CPU端计算是异步的,不同显示流内的GPU task执行也是异步的、并行的;

在这里插入图片描述

简单代码案例:

// 创建两个流
cudaStream_t stream[2];		// 定义流对象
for (int i = 0; i < 2; ++i){
	cudaStreamCreate(&stream[i]);
}
float* hostPtr;
cudaMallocHost(&hostPtr, 2 * size);
...
// 两个流,每个流有三个命令
for (int i = 0; i < 2; ++i){
	// 从主机内存复制数据到设备内存
	cudaMemcpyAsync(inputDevPtr + i * size, hostPtr + i * size, size, cudaMemcpyHostToDevice, stream[i]);
	// 执行kernel处理
	MyKernrl <<grid, block, 0, stream[i]>>(outputDevPtr + i * size, inputDevPtr + i * size, size);
	// 从设备内存复制数据到主机内存
	cudaMemcpyAsync(hostPtr + i * size, outputDevPtr + i * size, size, cudaMemcpyD	eviceToHost, stream[i]);
}
// 同步流
for (int i = 0; i < 2; i++){
	cudaStreamSyncchronize(stream[i]);
	...
}
// 销毁流
for (int i = 0; i < 2; ++i){
	cudaStreamDestory(stream[i]);
}

优点:

  • CPU计算和kernel计算并行;
  • CPU计算和数据传输并行;
  • 数据传输和kernel计算并行;
  • kernel计算并行;

注意点:

显示流里的GPU task和GPU端的task的执行是异步的,使用stream一定要注意同步;

以下接口作用是同步流:

cudaStreamSyncchronize():同步一个流;

cudaDeviceSynchronize():同步设备上所有流;

cudaStreamQuery():查询一个流任务是否完成;

下面看一个案例:数据传输和GPU计算通过流实现并行

在这里插入图片描述

注意:从下面的任务执行顺序可以看出,数据传输并不是重叠到一起的,这是因为CPU和GPU数据的传输是经过PCle总线的,PCle的操作是顺序的;

提问:

1、CUDA Stream为什么有效?

  • PCle总线传输速度慢,是瓶颈,会导致数据传输的时候GPU处于空闲状态,多流可以实现数据传输与kernel计算的并行;
  • 一个kernel往往用不了整个GPU的算力,多流可以让多个kernel同时计算;
  • 不是流越多越好,类似于CPU的多核一样,也是有数量限制的;

这里还有一个优化的策略,就是将小任务合并成大任务;

在这里插入图片描述

还需要注意一点,当同时存在默认流和显示流时,编译需要加上一个参数;

nvcc --default-stream per-thread ./stream_test.cu -o stream_per-thread

CUDA Event

CUDA Event是在stream中插入一个事件,类似于打一个标记位,用来记录stream是否执行到当前位置,Event有两个状态,已被执行和未被执行;

最常用的用法是来测时间:

// 使用event计算时间
float time_elapsed = 0;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);

cudaEventRecord(start, 0);	// 记录当前时间
mul<<<blocks, threads, 0, 0>>>(dev_a, NUM);
cudaEventRecord(stop, 0);	// 记录当前时间

cudaEventSynchronize(start);
cudaEventSynchronize(stop);
cudaEventElapsedTime(&time_elapsed, start, stop); // 计算时间差

cudaEventDestroy(start);
cudaEventDestroy(stop);
printf("执行时间: %f(ms)\n", time_elapsed);

CUDA同步操作(分为四类):

  • device synchronize:影响很大,必须等待全部kernel执行完才能执行CPU端任务;
  • stream synchronize:影响单个流和CPU,需要等待这个流执行完才能继续CPU;
  • event synchronize:影响CPU,更细粒度的同步;
  • synchronizing across streams using an event:高级控制;

NVVP

概念:NVIDIA Visual Profiler(NVVP)是NVIDIA推出的跨平台的CUDA程序性能分析工具;

主要有以下特点:

  • 随着CUDA安装,不需要额外安装;
  • 有图形界面,可以快速找到程序中的性能瓶颈;
  • 以时间线的形式展示CPU和GPU的操作;
  • 可以查看数据传输和kernel的各种软件参数(速度)和硬件参数(L1 cache命中率);

在Window端打开可视化功能还需要参考以下文章安装:

(66条消息) Package | Windows10 CUDA10.2 JDK8 环境下安装NVidia Visual Profiler(nvvp)安装Bug笔记_1LOVESJohnny的博客-CSDN博客

Cublas

概念:是一个BLAS的实现,允许用户使用NVIDIA的GPU计算资源,使用cuBLAS的时候,应用应该分配矩阵或向量所需的GPU内存空间,并加载数据,调用所需的cuBLAS函数,然后从GPU的内存空间上传计算结果至主机,cuBLAS也提供了一些帮助函数来写或者读取数据从GPU中;

学习网站:cuBLAS (nvidia.com)

说明:这一部分主要是在GPU上的线性的一些函数用法,主要用于处理向量标量、向量矩阵、矩阵矩阵的一些函数,由于短期内还用不到,这里不进行深入学习;

Cudnn

概念:NVIDIA cuDNN是用于深度神经网络的GPU加速库,它强调性能、易用性和低内存开销,并且可以继承倒更高级的机器学习框架中;

学习网站:NVIDIA cuDNN Documentation

实现步骤:

// 1、创建cuDNN句柄
cudnnStatus_t cudnnCreate(cudnnHandle_t *handle)
// 2、以Host方式调用在Device上运行的函数
比如卷积函数: cudnnConvolutionForward等
//3、释放cuDNN句柄
cudnnStatus_t cudnnDestroy(cudnnHandle_t handle)
// 4、将CUDA流设置&返回成cudnn句柄
cudnnStatus_t cudnnSetStream( cudnnHandle_t handle, cudaStream_t streamId)
cudnnStatus_t cudnnGetStream( cudnnHandle_t handle, cudaStream_t *streamId)

三、TensorRT学习

基础概念

首先要明确TensorRT的定位,是一个推理框架:

在这里插入图片描述

具有以下几个特点:

  • 高性能深度学习推理优化器和加速库;
  • 低延迟和高吞吐量;
  • 部署到超大规模数据中心、嵌入式或汽车产品;
  • 相对于其他推理框架,闭源也是其特点之一;

其实现步骤主要分为两步:转换优化引擎和执行引擎;

在这里插入图片描述

其中最常见的优化方式也就是量化(低精度)和算子融合(例如将卷积池化和激活层融合成一层)

使用流程

其使用流程分为以下几个步骤:

在这里插入图片描述

其中构建网络分为两种方式,一种是API构建,也就是网络的每一层都重新用代码构建,相对来说比较复杂;一种是用Parser来构建,也就是特定的网络有其特定的框架有对应的加载接口,只需要简单几行代码就可以构建网络结构;

模型转换

ONNX转trt:https://github.com/onnx/onnx-tensorrt/tree/6872a9473391a73b96741711d52b98c2c3e25146

Pytorch转trt:NVIDIA-AI-IOT/torch2trt: An easy to use PyTorch to TensorRT converter (github.com)

TensorFlow转trt:tensorflow/tensorflow/compiler/tf2tensorrt at 1cca70b80504474402215d2a4e55bc44621b691d · tensorflow/tensorflow (github.com)

这里介绍一个转换工具网站:https://convertmodel.com/

其中具体的一些转换的技巧还需要在实践中去探索,但最好将模型转换为onnx,再通过onnx转换为trt;

简单案例

官方源码中给出了很多案例:

https://github.com/NVIDIA/TensorRT/tree/release/6.0/samples/opensource/sampleMNIST

这是其中一个MNIST的数字识别的案例;

在AIStudio中跑了一下案例,可以得到如下结果:

在这里插入图片描述

四、TensorRT进阶

plugin用法

作用:

1、trt支持的算子有限,可以实现不支持的算子;

2、进行深度优化、合并算子;

工作流程:

在这里插入图片描述

API的讲解:

首先要清楚有两种类型的定义:

  • Dynamic Shape:输入维度是动态的,继承IPluginV2IOExt基础类;
  • Static Shape:输入维度是静态的,继承IPluginV2DynamicExt基础类;

构造函数:

1、用于network definition阶段,PluginCreator创建该插件时调用的构造函数,需要传递权重信息以及参数。 也可用于clone阶段,或者再写一个clone构造函数;

MyCustomPlugin(int in_channel, nvinfer1::Weights const& weight, nvinfer1::Weights const& bias);

2、 用于在deserialize阶段,用于将序列化好的权重和参数传入该plugin并创建;

MyCustomPlugin(void const* serialData, size_t serialLength);

3、注意把默认构造函数删掉;

MyCustomPlugin() = delete;

析构函数:

析构函数则需要执行terminate,terminate函数就是释放这个op之前开辟的一些显存空间;

MyCustomPlugin::~MyCustomPlugin() {
	terminate();
}

输出相关函数:

1、获得layer的输出个数;

int getNbOutputs() const;

2、根据输入个数和输入维度,获得第index个输出的维度;

nvinfer1::Dims getOutputDimensions(int index, const nvinfer1::Dims* inputs, int nbInputDims); 

3、根据输入个数和输入类型,获得第index个输出的类型;

nvinfer1::DataType getOutputDataType(int index, const nvinfer1::DataType* inputTypes, int nbInputs) const; 

序列化和反序列化相关函数:

1、返回序列化时需要写多少字节到buffer中;

size_t getSerializationSize() const;

2、序列化函数,将plugin的参数权值写入到buffer中;

void serialize(void* buffer) const;

3、获得plugin的type和version,用于反序列化使用;

const char* getPluginType() const;
const char* getPluginVersion() const;

初始化、配置和销毁函数:

1、初始化函数,在这个插件准备开始run之前执行。一般申请权值显存空间并copy权值;

int initialize(); 

2、terminate函数就是释放initialize开辟的一些显存空间;

void terminate(); 

3、释放整个plugin占用的资源;

void destroy();

4、配置这个插件op,判断输入和输出类型数量是否正确;

void configurePlugin(const nvinfer1::PluginTensorDesc* in, int nbInput, const nvinfer1::PluginTensorDesc* out, int nbOutput);

5、判断pos索引的输入/输出是否支持inOut[pos].format和inOut[pos].type指定的格式/数据类型;

bool supportsFormatCombination(int pos, const nvinfer1::PluginTensorDesc* inOut, int nbInputs, int nbOutputs) const;

运行时相关函数:

1、获得plugin所需要的显存大小。最好不要在plugin enqueue中使用cudaMalloc申请显存;

size_t getWorkspaceSize(int maxBatchSize) const;

2、推理函数;

int enqueue(int batchSize, const void* const* inputs, void** outputs, void* workspace, cudaStream_t stream);

IPluginCreator相关函数:

1、获得pluginname和version,用于辨识creator;

const char* getPluginName() const; 
const char* getPluginVersion() const; 

2、通过PluginFieldCollection去创建plugin 将op需要的权重和参数一个一个取出来,然后调用上文提到的第一个构造函数:

const nvinfer1::PluginFieldCollection* getFieldNames(); 
nvinfer1::IPluginV2* createPlugin(const char* name, const nvinfer1::PluginFieldCollection* 
fc);

3、反序列化,调用反序列化那个构造函数,生成plugin;

nvinfer1::IPluginV2* deserializePlugin(const char* name, const void* serialData, size_t serialLength);

建议参考官方的案例进行学习,来巩固代码的实现流程;

优化

首先可以了解下FP32、FP16类型的具体定义:

参考:ARM CPU性能优化:FP32 、FP16 和BF16区别 - 知乎 (zhihu.com)

INT8量化的含义:

将基于浮点的模型转换成低精度的int8数值进行运算,以加快推理速度;

在这里插入图片描述

INT8和FP16加速推理的原理:

通过指令或硬件技术,在单位时钟周期内,FP16和INT8类型的运算次数大于FP32类型的运算次数;

INT8量化为什么不会大幅损失精度?

由于神经网络具有以下特性:具有一定的鲁棒性;

原因:训练数据一般都是有噪声的,神经网络的训练过程往往就是从噪声中识别出有效信息,可以将降低精度计算造成的损失理解成另一种噪声;

INT8量化的分类:

动态对称量化算法(ONNX量化、torch动态量化)

动态非对称量化算法(Google Gemmlowp)

静态对称量化算法(torch静态量化、TensorRT、NCNN)

动态对称量化算法:

在这里插入图片描述

计算公式:

scale = |max| * 2/256;

real_value = scale * quantized_value;

其中real_value为真实值(float类型),quantized_value为INT8量化的结果(char类型)

优点:算法简单,量化步骤耗时短;

缺点:会造成位宽浪费,影响精度,也就是说像可能转换成8位的值,有一位的值可能是空的;

动态非对称量化算法:

在这里插入图片描述

计算公式:

scale = |max - min| / 256;

real_value = scale * (quantized_value - zero_point);

其中real_value位真实值(float类型),quantized_value为INT8量化的结果(char类型),zero_point为零点值

优点:不会造成bit位宽浪费,精度有保障;

缺点:算法比较复杂,量化步骤耗时较长;

静态对称量化算法:

在这里插入图片描述

动态量化:推理时实时统计数值|max|;

静态量化:推理时使用预先统计的缩放阈值,截断部分阈值外的数据;

优点:算法最简单,量化耗时最短,精度也有一定保证;

缺点:构建量化网络比较麻烦;

主要采用KL散度来计算量化的阈值,可以参考下面文章:

(72条消息) TensorRT INT8量化原理与实现(非常详细)_Nicholson07的博客-CSDN博客

源代码案例:TensorRT/samples/opensource/sampleINT8 at release/7.2 · NVIDIA/TensorRT · GitHub

INT8量化大规模上线:

在这里插入图片描述

总结

1、对于深度神经网络的推理,TRT可以充分发挥GPU的算力,以及节省GPU的存储空间;

2、要多参考官方源码的sample案例,尝试替换现有模型,再深入了解API进行网络的搭建;

3、如果要使用自定义组件,至少先了解CUDA基本架构以及常用属性;

4、推荐使用FP16(定义很少变量,明显能提高速度,精度影响不大)和INT8(更大的潜力,可能导致精度下降)这两种量化模式;

5、在不同架构的GPU或者不同的软件版本的设备上,引擎不能通用,要重新生成一个;

猜你喜欢

转载自blog.csdn.net/weixin_40620310/article/details/130194428