当前位置:   article > 正文

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

vs2022 cuda

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 详细代码

  1. #include<iostream>
  2. #include<cudnn.h>
  3. #include<time.h>
  4. using namespace std;
  5. //计算步长的函数
  6. void compute_stride(const int* size, int* stride) {
  7. for (int i = 4; i >= 0; i--)
  8. stride[i] = (i == 4) ? 1 : size[i + 1] * stride[i + 1];
  9. }
  10. int main() {
  11. //开始计时
  12. clock_t start_clock, end_clock;
  13. start_clock = clock();
  14. // 0 创建cudnn句柄
  15. cudnnHandle_t cudnn;
  16. auto cudnnHandle = cudnnCreate(&cudnn);
  17. if (cudnnHandle != CUDNN_STATUS_SUCCESS) {
  18. cout << "创建cudnn句柄:失败!" << endl;
  19. return -1;
  20. }
  21. cout << "创建cudnn句柄:成功。" << endl;
  22. //1 创建数据和计算相关描述符
  23. // 1.1 创建输入张量描述符
  24. int q = 1, r = 1, m = 24, n = 45, p = 26;
  25. //这里创建5维矩阵的原因是高维度卷积计算时,官方文档推荐使用 >= 4维的张量进行计算,不需要的维度定义为1即可
  26. int inputDims[5] = { q,r,m, n, p }; // 输入张量的尺寸
  27. int input_stride[5]; //输入张量描述符的步长------**重要**重要**重要**
  28. compute_stride(inputDims, input_stride);
  29. cudnnTensorDescriptor_t inputDesc;//输入张量描述符
  30. cudnnCreateTensorDescriptor(&inputDesc);
  31. cudnnStatus_t status = cudnnSetTensorNdDescriptor(inputDesc, CUDNN_DATA_FLOAT, 5, inputDims, input_stride);
  32. if (status != CUDNN_STATUS_SUCCESS) {
  33. cout << "创建输入张量描述符:失败!" << endl;
  34. return -1;
  35. }
  36. cout << "创建输入张量描述符:成功。" << endl;
  37. // 1.2 创建卷积核描述符
  38. int filterDims[5] = { 1,1, 5, 5, 5 }; // 卷积核尺寸
  39. cudnnFilterDescriptor_t filterDesc;
  40. cudnnCreateFilterDescriptor(&filterDesc);
  41. status = cudnnSetFilterNdDescriptor(filterDesc, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW, 5, filterDims);
  42. if (status != CUDNN_STATUS_SUCCESS) {
  43. cout << "创建卷积核描述符:失败!" << endl;
  44. return -1;
  45. }
  46. cout << "创建卷积核描述符:成功。" << endl;
  47. // 1.3 创建卷积运算操作描述符
  48. int conmv_padA[3] = { 0,0,0 };//填充,表示沿各个维度补0的数量,是为了解决卷积后数据尺寸缩小的问题,设为全0则表示不需要填充
  49. int conv_filterStrideA[3] = { 1,1,1 };//卷积时使用卷积核的步长,全1表示不跳过,均为单步步进
  50. int conv_dilationA[3] = { 1,1,1 };//arrayLength(arg 2)数组所指示的每个维度膨胀因子,这个参数对是对卷积核操作的,某个维度膨胀系数>1时,会把卷积沿这个维度放大,中间的缺失数据用0补齐;全1表示无膨胀
  51. cudnnConvolutionDescriptor_t convDesc;
  52. cudnnCreateConvolutionDescriptor(&convDesc);
  53. status = cudnnSetConvolutionNdDescriptor(convDesc, 3, conmv_padA, conv_filterStrideA, conv_dilationA, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT);
  54. if (status != CUDNN_STATUS_SUCCESS) {
  55. cout << "创建卷积操作描述符:失败!" << endl;
  56. return -1;
  57. }
  58. cout << "创建卷积操作描述符:成功" << endl;
  59. // 1.4 计算输出张量的尺寸,自己手算也可以,但是还是建议用它的函数来计算,刚好可以验证之前的描述符创建的是否满足自己预期
  60. // outputDim = 1 + ( inputDim + 2.*pad - (((filterDim-1).*dilation)+1) )./convolutionStride
  61. int outputDims[5];
  62. status = cudnnGetConvolutionNdForwardOutputDim(convDesc, inputDesc, filterDesc, 5, outputDims);
  63. if (status != CUDNN_STATUS_SUCCESS) {
  64. cout << "计算输出张量的尺寸:失败!" << endl;
  65. return -1;
  66. }
  67. cout << "计算输出张量的尺寸:成功。" << endl;
  68. cout << "Output size: ";
  69. for (int i = 0; i < sizeof(outputDims) / sizeof(float); i++) {
  70. if (i < sizeof(outputDims) / sizeof(float) - 1) {
  71. cout << outputDims[i];
  72. cout << " X ";
  73. }
  74. }
  75. cout << outputDims[sizeof(outputDims) / sizeof(float) - 1] << endl;
  76. // 1.5 创建输出张量描述符
  77. cudnnTensorDescriptor_t outputDesc;
  78. cudnnCreateTensorDescriptor(&outputDesc);
  79. int output_stride[5]; //输出张量描述符的步长
  80. compute_stride(outputDims, output_stride);
  81. status = cudnnSetTensorNdDescriptor(outputDesc, CUDNN_DATA_FLOAT, 5, outputDims, output_stride);
  82. if (status != CUDNN_STATUS_SUCCESS) {
  83. cout << "创建输出张量描述符:失败!" << endl;
  84. return -1;
  85. }
  86. cout << "创建输出张量描述符:成功。" << endl;
  87. // 2 数据和计算内存空间分配与初始化
  88. //2.1 计算各变量所需内存大小
  89. size_t in_bytes = 0;//输入张量所需内存
  90. status = cudnnGetTensorSizeInBytes(inputDesc, &in_bytes);
  91. if (status != CUDNN_STATUS_SUCCESS) {
  92. std::cerr << "fail to get bytes of in tensor: " << cudnnGetErrorString(status) << std::endl;
  93. return -1;
  94. }
  95. size_t out_bytes = 0;//输出张量所需内存
  96. status = cudnnGetTensorSizeInBytes(outputDesc, &out_bytes);
  97. if (status != CUDNN_STATUS_SUCCESS) {
  98. std::cerr << "fail to get bytes of out tensor: " << cudnnGetErrorString(status) << std::endl;
  99. return -1;
  100. }
  101. size_t filt_bytes = 1;//卷积核所需内存
  102. for (int i = 0; i < sizeof(filterDims) / sizeof(int); i++) {
  103. filt_bytes *= filterDims[i];
  104. }
  105. filt_bytes *= sizeof(float);
  106. //自动寻找最优卷积计算方法,函数自动选择的最优算法保存在perfResults结构体中
  107. int returnedAlgoCount = 0;
  108. cudnnConvolutionFwdAlgoPerf_t perfResults;
  109. status = cudnnGetConvolutionForwardAlgorithm_v7(cudnn, inputDesc, filterDesc, convDesc, outputDesc, 1, &returnedAlgoCount, &perfResults);
  110. if (returnedAlgoCount != 1 || status != CUDNN_STATUS_SUCCESS) {
  111. cerr << "自动适配卷积计算方法:失败!" << endl;
  112. return -1;
  113. }
  114. cout << "自动适配卷积计算方法:成功。" << endl;
  115. //计算卷积操作所需的内存空间大小,保存在workspace_bytes变量中
  116. size_t workspace_bytes{ 0 };
  117. status = cudnnGetConvolutionForwardWorkspaceSize(cudnn, inputDesc, filterDesc, convDesc, outputDesc, perfResults.algo, &workspace_bytes);
  118. if (status != CUDNN_STATUS_SUCCESS) {
  119. cout << "计算卷积操作所需的内存空间:失败!" << endl;
  120. return -1;
  121. }
  122. cout << "计算卷积操作所需的内存空间:成功。" << endl;
  123. //2.2 判断GPU上是否有足够的内存空间用于计算
  124. size_t request = in_bytes + out_bytes + workspace_bytes + filt_bytes;
  125. size_t cudaMem_free = 0, cudaMem_total = 0;
  126. cudaError_t cuda_err = cudaMemGetInfo(&cudaMem_free, &cudaMem_total);
  127. if (cuda_err != cudaSuccess) {
  128. std::cerr << "fail to get mem info: " << cudaGetErrorString(cuda_err) << std::endl;
  129. return -1;
  130. }
  131. if (request > cudaMem_free) {
  132. std::cerr << "not enough gpu memory to run" << std::endl;
  133. return -1;
  134. }
  135. //2.2 在主机上分配内存存储输入张量、卷积核和输出张量
  136. float* input = (float*)malloc(in_bytes);
  137. float* filter = (float*)malloc(filt_bytes);
  138. float* output = (float*)malloc(out_bytes);
  139. // 2.3 数据初始化
  140. // cuda要求输入是一维数据(NCHW格式),所以将原数据reshape成一行数据,输入到GPU之后会根据描述符中的维度参数还原的,所以不用担心,
  141. // 无论多少维的数据, 按顺序reshape为一行即可(但是NCHW格式和NHWC格式的reshape方式是不一样的,一定要注意,上下文匹配即可)
  142. // 2.3.1 输入张量
  143. for (int i = 0; i < inputDims[0] * inputDims[1] * inputDims[2] * inputDims[3] * inputDims[4]; i++) {
  144. input[i] = 1.0f;//创建全1.0的输入数据举例
  145. }
  146. //2.3.2 卷积核数据,我随便举的例子,可以根据自己的需求自己改,只要维度和前面定义的卷积核维度一致即可
  147. int flag = 0;
  148. for (int i = 0; i < filterDims[2]; i++) {
  149. for (int j = 0; j < filterDims[3]; j++) {
  150. for (int k = 0; k < filterDims[4]; k++) {
  151. filter[flag] = 1.0f;
  152. flag++;
  153. }
  154. }
  155. }
  156. // 2.4 在设备(gpu)上分配内存空间
  157. float* d_input, * d_filter, * d_output;
  158. cudaMalloc(&d_input, in_bytes);
  159. cudaMalloc(&d_filter, filt_bytes);
  160. cudaMalloc(&d_output, out_bytes);
  161. void* d_workspace{ nullptr };
  162. cudaMalloc(&d_workspace, workspace_bytes);
  163. // 3 将输入张量和卷积核拷贝到设备
  164. cudaMemcpy(d_input, input, in_bytes, cudaMemcpyHostToDevice);
  165. cudaMemcpy(d_filter, filter, filt_bytes, cudaMemcpyHostToDevice);
  166. // 4 进行卷积计算
  167. float alpha = 1.0f, beta = 0.0f;
  168. 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);
  169. if (status != CUDNN_STATUS_SUCCESS) {
  170. cout << "卷积计算过程:失败!" << endl;
  171. return -1;
  172. }
  173. cout << "卷积计算过程:成功。" << endl;
  174. // 5 将输出矩阵拷贝回主机
  175. cudaMemcpy(output, d_output, out_bytes, cudaMemcpyDeviceToHost);
  176. //打印输出矩阵
  177. for (int i = 0; i < out_bytes; i++) {
  178. cout << output[i] << " ";
  179. }
  180. // 6 释放资源
  181. //6.1 释放三个变量占用的内存
  182. cuda_err = cudaFree(d_input);
  183. if (cuda_err != cudaSuccess) {
  184. std::cerr << "fail to free memory of input data: " << cudaGetErrorString(cuda_err) << std::endl;
  185. return -1;
  186. }
  187. cuda_err = cudaFree(d_filter);
  188. if (cuda_err != cudaSuccess) {
  189. std::cerr << "fail to free memory of filter data: " << cudaGetErrorString(cuda_err) << std::endl;
  190. return -1;
  191. }
  192. cuda_err = cudaFree(d_output);
  193. if (cuda_err != cudaSuccess) {
  194. std::cerr << "fail to free memory of output data: " << cudaGetErrorString(cuda_err) << std::endl;
  195. return -1;
  196. }
  197. //6.2 释放描述符占用的内存
  198. status = cudnnDestroyTensorDescriptor(inputDesc);
  199. if (status != CUDNN_STATUS_SUCCESS) {
  200. std::cerr << "fail to destroy input tensor desc: " << cudnnGetErrorString(status) << std::endl;
  201. return -1;
  202. }
  203. status = cudnnDestroyFilterDescriptor(filterDesc);
  204. if (status != CUDNN_STATUS_SUCCESS) {
  205. std::cerr << "fail to destroy filter tensor desc: " << cudnnGetErrorString(status) << std::endl;
  206. return -1;
  207. }
  208. status = cudnnDestroyConvolutionDescriptor(convDesc);
  209. if (status != CUDNN_STATUS_SUCCESS) {
  210. std::cerr << "fail to destroy conv desc: " << cudnnGetErrorString(status) << std::endl;
  211. return -1;
  212. }
  213. status = cudnnDestroyTensorDescriptor(outputDesc);
  214. if (status != CUDNN_STATUS_SUCCESS) {
  215. std::cerr << "fail to destroy outout tensor desc: " << cudnnGetErrorString(status) << std::endl;
  216. return -1;
  217. }
  218. //6.3 释放cudnn句柄内存
  219. status = cudnnDestroy(cudnn);
  220. if (status != CUDNN_STATUS_SUCCESS) {
  221. std::cerr << "fail to destroy cudnn handle" << std::endl;
  222. return -1;
  223. }
  224. //6.4 释放主机上存储数组的内存
  225. free(input);
  226. free(filter);
  227. free(output);
  228. //结束计时
  229. end_clock = clock();
  230. double endtime = (double)(end_clock - start_clock) / CLOCKS_PER_SEC;
  231. cout << "Total time:" << endtime * 1000 << " ms" << endl; //ms为单位
  232. return 0;
  233. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/245716
推荐阅读
相关标签
  

闽ICP备14008679号