C++(VS022)调用cuda(cudnn)加速三维卷积计算

0 背景

cuda是NVIDIA官方推出的并行计算架构,结合了CPU和GPU的优点,主要用来处理密集型任务以及并行计算,在现在的各种机器学习/深度学习框架中使用很多。

度神经网络库 (cuDNN) 是一个 GPU 加速的深度神经网络基元库,能够以高度优化的方式实现标准例程(如前向和反向卷积、池化层、归一化和激活层)。

我们使用这个库的原因是因为:如果要在C++中使用cuda的并行计算能力,目前我所知的有两条路子:

1是按照cuda本身的规则,写cu文件,封装成动态库lib后,在再需要的地方引入,具体的操作步骤可以去看这篇文章;这种方式很繁琐,而且封装的这个过程,我个人感觉在使用时是会对计算速度有影响的;

2是使用cudnn的这个库,这个库的兼容性很好,只需要包含进来随时都能用,不需要自己再去封装;官方原文是:cuDNN 可加速广泛应用的深度学习框架,包括 Caffe2ChainerKerasMATLABMxNetPaddlePaddlePyTorch 和 TensorFlow

本文不是要说明如何使用cuda来搭建ML/DL ,主要实现的就是使用CUDNN调用GPU进行三维卷积的并行计算,这种计算方式在矩阵较小时时间成本不划算,甚至可能比单纯在CPU中运行时间更长,因为数据在设备端(GPU)运算,所以数据需要在主机端(CPU)设备端(GPU)之间流转。

1 环境说明and环境搭建

1.1 环境说明

win11,IDE为 Visual studio2022,cuda版本12.2,cudnn版本8.9.4.25

VS的安装无需多说,这里主要说一下cuda和cudnn的安装中的一些注意事项:

cuda和cudnn的详细安装可以见其他大佬的这篇文章,我在这里就贴一个去下载cuda toolkit和cudnn的传送门吧:cuda工具包cudnn.

除了c站大佬的文章,也可以去Installation Guide - NVIDIA Docs

我要额外说的东西是:官方文档里的安装教程中的这个zlibwapi.lib(图1.1)没有在官方下载的包里,需要自己额外去下载一下图1.2所示的文件,链接在此,然后如安装cudnn的过程一样,把对应的文件复制到cuda文件下对应文件中就可以了。后续使用使直接在工程中包含cuda根目录bin就可以。

图1.1

图1.2

1.2 VS环境搭建:

右键项目名称-> 属性:

1. 打开VC++目录 -> 将cuda安装目录下的include文件夹添加到包含目录:

2.链接器  ->常规-> 将cuda安装目录下的lib文件夹添加到附加库目录:

3. 链接器  ->输入,,将cudnn.lib;cublas.lib;cublasLt.lib;cudart.lib;cudadevrt.lib;zlibwapi.lib;添加到附加依赖项。这里最后一个zlibwapi.lib就是动态链接库就是我在1.1中强调的,官方包里没有,需要去自己下载的。

4.在项目中包含头文件:

#include<cudnn.h>

至此所有环境都已搭建好,可以开始敲代码计算了。

3 代码和数据

如果只是希望用这个并行计算二维卷积,可以参考知乎的这个大佬的文章。三维以及更高维度的卷积(严格来说不叫卷积,叫相关运算,严格定义的卷积还需要提前进行一次旋转,但是大家一直都这么叫,也就叫卷积吧。)都可以参照这篇文章,在参数中扩充数据维度即可。

举例实现三维卷积:使用5*5*5的卷积核,去卷积28*45*26的原始数据(可以理解为28张大小为45*26的单通道灰度图堆起来的三维阵,这也就是我的数据处理需求)。

3.1 计算过程:

计算过程主要包括几大步骤:

1.创建输入张量、卷积核、卷积操作和输出张量的描述符;

2.在GPU中分配内存空间;

3.将输入张量和卷积核从cpu复制到GPU中;

4.进行卷积计算;

5.将计算结果复制回CPU中。

6.释放资源

3.2 几个易错点详述

1. 在创建张量描述符时,函数中有个参数:stride,这个参数表示步长,最开始以为创建为全1即表示在各个维度都是单步,不跳过数据,结果就是描述符可以创建成功,但是后面参数中用到该描述符的函数全都报错BAD_PARAM,一直查不出来错(头发都要debug没了),后来在nvidia cudnn的论坛上看到个大佬的代码,才反应过来这个stride参数是要从后往前连乘的(真反人类),譬如你的输入张量尺寸是{1,1,6,8,9},单步的stride应该是stride = {432,432,72,9,1} = {1*6*8*9*1,6*8*9*1,8*9*1,9*1,1},自己理解就行,实际使用时具体的计算代码就是3.3中的compute_stride函数。

2. 要进行三维卷积,就是要创建5维及以上的张量才可以,因为NCHW格式,前两维会用来表示数据的batch数和通道数(feather maps),后面剩下的维度用来表示数据的维度,剩下三维刚好用来表述三维数据(也可以创建更高维的张量来表述,比如对上面5维张量描述的{1,1,6,8,9},也可以创建为{1,1,1,6,8,9}的6维张量来表达,低维到高维是可以加个维度直接升维的,只不过加出来的维度尺寸为1)。如果要进行4维卷积运算,则需要创建的为>=6维的张量描述符,才能将数据表述完全。卷积核的描述符同理。

3. 卷积操作的描述符又和上面一点不一样,要进行几维的卷积,cudnnSetConvolutionNdDescriptor函数的arg(2)就要是几,同时这个函数进去的参数也是几维,比如3.3详细底代码中的1.3(line60-70)中,需要进行三维卷积,那么创建卷积操作的描述符函数中,第二个参数为3,参数pad,Stride和dilation都是三维。

4.因为cudnn的函数没有实现内存的自动回收,为防止内存泄漏,需要在运算完成之后使用cudaFree手动释放内存,以及使用cudnnDestroy销毁之前创建的描述符和句柄。

3.3 详细代码

#include<iostream>
#include<cudnn.h>
#include<time.h>
using namespace std;

//计算步长的函数
void compute_stride(const int* size, int* stride) {
	for (int i = 4; i >= 0; i--)
		stride[i] = (i == 4) ? 1 : size[i + 1] * stride[i + 1];
}


int main() {

	//开始计时
	clock_t start_clock, end_clock;
	start_clock = clock();


	// 0 创建cudnn句柄
	cudnnHandle_t cudnn;
	auto cudnnHandle = cudnnCreate(&cudnn);
	if (cudnnHandle != CUDNN_STATUS_SUCCESS) {
		cout << "创建cudnn句柄:失败!" << endl;
		return -1;
	}
	cout << "创建cudnn句柄:成功。" << endl;


	//1 创建数据和计算相关描述符
	// 1.1 创建输入张量描述符
	int q = 1, r = 1, m = 24, n = 45, p = 26;
	//这里创建5维矩阵的原因是高维度卷积计算时,官方文档推荐使用 >= 4维的张量进行计算,不需要的维度定义为1即可
	int inputDims[5] = { q,r,m, n, p }; // 输入张量的尺寸 
	int input_stride[5]; //输入张量描述符的步长------**重要**重要**重要**
	compute_stride(inputDims, input_stride);
	cudnnTensorDescriptor_t inputDesc;//输入张量描述符
	cudnnCreateTensorDescriptor(&inputDesc);
	cudnnStatus_t status = cudnnSetTensorNdDescriptor(inputDesc, CUDNN_DATA_FLOAT, 5, inputDims, input_stride);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "创建输入张量描述符:失败!" << endl;
		return -1;
	}
	cout << "创建输入张量描述符:成功。" << endl;

	// 1.2 创建卷积核描述符
	int filterDims[5] = { 1,1, 5, 5, 5 };   // 卷积核尺寸
	cudnnFilterDescriptor_t filterDesc;
	cudnnCreateFilterDescriptor(&filterDesc);
	status = cudnnSetFilterNdDescriptor(filterDesc, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW, 5, filterDims);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "创建卷积核描述符:失败!" << endl;
		return -1;
	}
	cout << "创建卷积核描述符:成功。" << endl;

	// 1.3 创建卷积运算操作描述符
	int  conmv_padA[3] = { 0,0,0 };//填充,表示沿各个维度补0的数量,是为了解决卷积后数据尺寸缩小的问题,设为全0则表示不需要填充
	int conv_filterStrideA[3] = { 1,1,1 };//卷积时使用卷积核的步长,全1表示不跳过,均为单步步进
	int conv_dilationA[3] = { 1,1,1 };//arrayLength(arg 2)数组所指示的每个维度膨胀因子,这个参数对是对卷积核操作的,某个维度膨胀系数>1时,会把卷积沿这个维度放大,中间的缺失数据用0补齐;全1表示无膨胀
	cudnnConvolutionDescriptor_t convDesc;
	cudnnCreateConvolutionDescriptor(&convDesc);
	status = cudnnSetConvolutionNdDescriptor(convDesc, 3, conmv_padA, conv_filterStrideA, conv_dilationA, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "创建卷积操作描述符:失败!" << endl;
		return -1;
	}
	cout << "创建卷积操作描述符:成功" << endl;

	// 1.4 计算输出张量的尺寸,自己手算也可以,但是还是建议用它的函数来计算,刚好可以验证之前的描述符创建的是否满足自己预期
	// outputDim = 1 + ( inputDim + 2.*pad - (((filterDim-1).*dilation)+1) )./convolutionStride
	int outputDims[5];
	 status = cudnnGetConvolutionNdForwardOutputDim(convDesc, inputDesc, filterDesc, 5, outputDims);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "计算输出张量的尺寸:失败!" << endl;
		return -1;
	}
	cout << "计算输出张量的尺寸:成功。" << endl;
	cout << "Output size: ";
	for (int i = 0; i < sizeof(outputDims) / sizeof(float); i++) {
		if (i < sizeof(outputDims) / sizeof(float) - 1) {
			cout << outputDims[i];
			cout << " X ";
		}
	}
	cout << outputDims[sizeof(outputDims) / sizeof(float) - 1] << endl;

	// 1.5 创建输出张量描述符
	cudnnTensorDescriptor_t outputDesc;
	cudnnCreateTensorDescriptor(&outputDesc);
	int output_stride[5]; //输出张量描述符的步长
	compute_stride(outputDims, output_stride);
	status = cudnnSetTensorNdDescriptor(outputDesc, CUDNN_DATA_FLOAT, 5, outputDims, output_stride);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "创建输出张量描述符:失败!" << endl;
		return -1;
	}
	cout << "创建输出张量描述符:成功。" << endl;


	// 2 数据和计算内存空间分配与初始化
	//2.1 计算各变量所需内存大小
	size_t in_bytes = 0;//输入张量所需内存
	status = cudnnGetTensorSizeInBytes(inputDesc, &in_bytes);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to get bytes of in tensor: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	size_t  out_bytes = 0;//输出张量所需内存
	status = cudnnGetTensorSizeInBytes(outputDesc, &out_bytes);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to get bytes of out tensor: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	size_t filt_bytes = 1;//卷积核所需内存
	for (int i = 0; i < sizeof(filterDims) / sizeof(int); i++) {
		filt_bytes *= filterDims[i];
	}
	filt_bytes *= sizeof(float);
	//自动寻找最优卷积计算方法,函数自动选择的最优算法保存在perfResults结构体中
	int returnedAlgoCount = 0;
	cudnnConvolutionFwdAlgoPerf_t perfResults;
	status = cudnnGetConvolutionForwardAlgorithm_v7(cudnn, inputDesc, filterDesc, convDesc, outputDesc, 1, &returnedAlgoCount, &perfResults);
	if (returnedAlgoCount != 1 || status != CUDNN_STATUS_SUCCESS) {
		cerr << "自动适配卷积计算方法:失败!" << endl;
		return -1;
	}
	cout << "自动适配卷积计算方法:成功。" << endl;
	//计算卷积操作所需的内存空间大小,保存在workspace_bytes变量中
	size_t workspace_bytes{ 0 };
	status = cudnnGetConvolutionForwardWorkspaceSize(cudnn, inputDesc, filterDesc, convDesc, outputDesc, perfResults.algo, &workspace_bytes);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "计算卷积操作所需的内存空间:失败!" << endl;
		return -1;
	}
	cout << "计算卷积操作所需的内存空间:成功。" << endl;

	//2.2 判断GPU上是否有足够的内存空间用于计算
	size_t request = in_bytes + out_bytes + workspace_bytes + filt_bytes;
	size_t cudaMem_free = 0, cudaMem_total = 0;
	cudaError_t cuda_err = cudaMemGetInfo(&cudaMem_free, &cudaMem_total);
	if (cuda_err != cudaSuccess) {
		std::cerr << "fail to get mem info: " << cudaGetErrorString(cuda_err) << std::endl;
		return -1;
	}
	if (request > cudaMem_free) {
		std::cerr << "not enough gpu memory to run" << std::endl;
		return -1;
	}

	//2.2 在主机上分配内存存储输入张量、卷积核和输出张量
	float* input = (float*)malloc(in_bytes);
	float* filter = (float*)malloc(filt_bytes);
	float* output = (float*)malloc(out_bytes);

	// 2.3 数据初始化
	// cuda要求输入是一维数据(NCHW格式),所以将原数据reshape成一行数据,输入到GPU之后会根据描述符中的维度参数还原的,所以不用担心,
	// 无论多少维的数据, 按顺序reshape为一行即可(但是NCHW格式和NHWC格式的reshape方式是不一样的,一定要注意,上下文匹配即可)
	// 2.3.1 输入张量   
	for (int i = 0; i < inputDims[0] * inputDims[1] * inputDims[2] * inputDims[3] * inputDims[4]; i++) {
		input[i] = 1.0f;//创建全1.0的输入数据举例
	}
	//2.3.2 卷积核数据,我随便举的例子,可以根据自己的需求自己改,只要维度和前面定义的卷积核维度一致即可

	int flag = 0;
	for (int i = 0; i < filterDims[2]; i++) {
		for (int j = 0; j < filterDims[3]; j++) {
			for (int k = 0; k < filterDims[4]; k++) {
				filter[flag] = 1.0f;
				flag++;
			}
		}
	}

	// 2.4 在设备(gpu)上分配内存空间
	float* d_input, * d_filter, * d_output;
	cudaMalloc(&d_input, in_bytes);
	cudaMalloc(&d_filter, filt_bytes);
	cudaMalloc(&d_output, out_bytes);
	void* d_workspace{ nullptr };
	cudaMalloc(&d_workspace, workspace_bytes);


	// 3 将输入张量和卷积核拷贝到设备
	cudaMemcpy(d_input, input, in_bytes, cudaMemcpyHostToDevice);
	cudaMemcpy(d_filter, filter, filt_bytes, cudaMemcpyHostToDevice);


	// 4 进行卷积计算
	float alpha = 1.0f, beta = 0.0f;
	status = cudnnConvolutionForward(cudnn, &alpha, inputDesc, d_input, filterDesc, d_filter, convDesc, CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_GEMM, d_workspace, workspace_bytes, &beta, outputDesc, d_output);
	if (status != CUDNN_STATUS_SUCCESS) {
		cout << "卷积计算过程:失败!" << endl;
		return -1;
	}
	cout << "卷积计算过程:成功。" << endl;


	// 5 将输出矩阵拷贝回主机
	cudaMemcpy(output, d_output, out_bytes, cudaMemcpyDeviceToHost);

	//打印输出矩阵
	for (int i = 0; i < out_bytes; i++) {
		cout << output[i] << " ";
	}


	// 6 释放资源
	//6.1 释放三个变量占用的内存
	cuda_err = cudaFree(d_input);
	if (cuda_err != cudaSuccess) {
		std::cerr << "fail to free memory of input data: " << cudaGetErrorString(cuda_err) << std::endl;
		return -1;
	}
	cuda_err = cudaFree(d_filter);
	if (cuda_err != cudaSuccess) {
		std::cerr << "fail to free memory of filter data: " << cudaGetErrorString(cuda_err) << std::endl;
		return -1;
	}
	cuda_err = cudaFree(d_output);
	if (cuda_err != cudaSuccess) {
		std::cerr << "fail to free memory of output data: " << cudaGetErrorString(cuda_err) << std::endl;
		return -1;
	}
	//6.2 释放描述符占用的内存
	status = cudnnDestroyTensorDescriptor(inputDesc);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to destroy input tensor desc: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	status = cudnnDestroyFilterDescriptor(filterDesc);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to destroy filter tensor desc: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	status = cudnnDestroyConvolutionDescriptor(convDesc);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to destroy conv desc: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	status = cudnnDestroyTensorDescriptor(outputDesc);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to destroy outout tensor desc: " << cudnnGetErrorString(status) << std::endl;
		return -1;
	}
	//6.3 释放cudnn句柄内存
	status = cudnnDestroy(cudnn);
	if (status != CUDNN_STATUS_SUCCESS) {
		std::cerr << "fail to destroy cudnn handle" << std::endl;
		return -1;
	}
	//6.4 释放主机上存储数组的内存
	free(input);
	free(filter);
	free(output);


	//结束计时
	end_clock = clock();
	double endtime = (double)(end_clock - start_clock) / CLOCKS_PER_SEC;
	cout << "Total time:" << endtime * 1000 << " ms" << endl;	//ms为单位

	return 0;
}

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值