赞
踩
cuda是NVIDIA官方推出的并行计算架构,结合了CPU和GPU的优点,主要用来处理密集型任务以及并行计算,在现在的各种机器学习/深度学习框架中使用很多。
度神经网络库 (cuDNN) 是一个 GPU 加速的深度神经网络基元库,能够以高度优化的方式实现标准例程(如前向和反向卷积、池化层、归一化和激活层)。
我们使用这个库的原因是因为:如果要在C++中使用cuda的并行计算能力,目前我所知的有两条路子:
1是按照cuda本身的规则,写cu文件,封装成动态库lib后,在再需要的地方引入,具体的操作步骤可以去看这篇文章;这种方式很繁琐,而且封装的这个过程,我个人感觉在使用时是会对计算速度有影响的;
2是使用cudnn的这个库,这个库的兼容性很好,只需要包含进来随时都能用,不需要自己再去封装;官方原文是:cuDNN 可加速广泛应用的深度学习框架,包括 Caffe2、Chainer、Keras、MATLAB、MxNet、PaddlePaddle、PyTorch 和 TensorFlow。
本文不是要说明如何使用cuda来搭建ML/DL ,主要实现的就是使用CUDNN调用GPU进行三维卷积的并行计算,这种计算方式在矩阵较小时时间成本不划算,甚至可能比单纯在CPU中运行时间更长,因为数据在设备端(GPU)运算,所以数据需要在主机端(CPU)设备端(GPU)之间流转。
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. 打开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>
至此所有环境都已搭建好,可以开始敲代码计算了。
如果只是希望用这个并行计算二维卷积,可以参考知乎的这个大佬的文章。三维以及更高维度的卷积(严格来说不叫卷积,叫相关运算,严格定义的卷积还需要提前进行一次旋转,但是大家一直都这么叫,也就叫卷积吧。)都可以参照这篇文章,在参数中扩充数据维度即可。
举例实现三维卷积:使用5*5*5的卷积核,去卷积28*45*26的原始数据(可以理解为28张大小为45*26的单通道灰度图堆起来的三维阵,这也就是我的数据处理需求)。
计算过程主要包括几大步骤:
1.创建输入张量、卷积核、卷积操作和输出张量的描述符;
2.在GPU中分配内存空间;
3.将输入张量和卷积核从cpu复制到GPU中;
4.进行卷积计算;
5.将计算结果复制回CPU中。
6.释放资源
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销毁之前创建的描述符和句柄。
- #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;
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。