找回密码
 立即注册
首页 业界区 业界 NIVIDIA高性能计算CUDA笔记(四)cuDNN深度神经网络计算 ...

NIVIDIA高性能计算CUDA笔记(四)cuDNN深度神经网络计算库简介及卷积操作示例

撷监芝 昨天 19:00
NIVIDIA高性能计算CUDA笔记(四)

cuDNN深度神经网络计算库简介及卷积操作示例

​        \(cuDNN\),全称为NIVIDIA  CUDA  Deep Neural  Network Library,是深度神经网络算子层级GPU加速库集合,提供了深度学习算法中常见算子的高效实现,专门为深度学习框架(如\(TensorFlow\),\(PyTorch\),\(Caffe\), \(MXNet\)等)实现常见的神经网络层提供极致的优化的实现 ,所以也直接成为了很多上层推理引擎底层调优的算子备选实现,比如\(TensorRT\),比如\(TVM\)。再换个角度理解,之前在该系列笔记的第一篇提到宗旨“解读介绍英伟达软件生态之CUDA”中介绍了GPU的编程语言\(\text{CUDA}\) C“,调用\(\text{CUDA}\) C其实就是为了写一些kernel,而这里的\(cuDNN\) 则是提供一些写好的高效的CUDA C kernel的集合。正如,该系列笔记介绍的\(cuFFT\),\(cuBLAS\) ,区别就在于它们面向的应用场景不同,这里的\(cuDNN\)就是面向深度神经网络算子的高效实现。

1.cuDNN的句柄与描述子

在\(\text{cuDNN}\)里,几乎所有操作都需要以下几个基本对象,我们这些基本对象进行描述:
A.句柄(cudnnHandle_t)

​        在使用cuDNN进行任何操作之前,需要初始化cuDNN上下文。可以使用cudnnCreate()函数来创建一个cuDNN上下文句柄,后续的所有cuDNN操作都将基于这个句柄进行。句柄(cudnnHandle_t)是cuDNN中一个非常重要的概念,它本质是一个指向\(cuDNN\)内部状态数据数据结构的指针,句柄的作用类似于“会化ID”,cuDNN通过它识别并管理当前的计算环境,确保所有操作在正确的上下文(如特定GPU设备、资源分配状态等)中执行。
​       handle是cudnnHandle_t类型的变量的指针。cudnCreate 函数接收这个地址后,就能在函数内部将创建的上下文句柄直接赋值到cudnn变量所在的内存空间,这样在函数调用结束后,外部的handle变量就保存了有效的上下文句柄,供后续操作使用。
  1. cudnnStatus_t cudnnCreate(cudnnHandle_t *handle);
  2. cudnnStatus_t cudnnDestroy(cudnnHandle_t handle);
复制代码

  • 作用:创建\(\text{cuDNN}\)上下文句柄,类似于\(\text{CUDA runtime}\)的\(cudaStream\)。所有操作都依赖它;
  • 参数:\(handle\) 是\(\text{cuDNN}\)运行环境的句柄;
  • 创建与销毁:句柄通过cudnnCreate()函数创建,在使用完成后,必须通过cudnnDestroy()函数进行销毁,以释放句柄所占用的资源,避免内存泄漏;
  • 线性安全性:cuDNN句柄不是线性安全的,每个线程应该使用独立的句柄对象。如果在多个线程环境中使用同一个句柄,可能导致不可预测的错误。因此,在多线程编程时,需要为每个线程单独创建和管理句柄。
  • 与CUDA上下文的关联:cuDNN句柄通常是管理GPU资源的基础,cuDNN句柄通过关联的CUDA上下文来访问GPU硬件资源,进行数据传输和计算操作。
cudnnStatus_t是cuDNN所有函数调用的返回状态类型,理解它非常重要,因为它决定了你能否成功调用\(\text{cuDNN API}\) 。
  1. typedef enum {
  2.     CUDNN_STATUS_SUCCESS = 0,
  3.     CUDNN_STATUS_NOT_INITIALIZED = 1,
  4.     CUDNN_STATUS_ALLOC_FAILED = 2,
  5.     CUDNN_STATUS_BAD_PARAM = 3,
  6.     CUDNN_STATUS_INTERNAL_ERROR = 4,
  7.     CUDNN_STATUS_INVALID_VALUE = 5,
  8.     CUDNN_STATUS_ARCH_MISMATCH = 6,
  9.     CUDNN_STATUS_MAPPING_ERROR = 7,
  10.     CUDNN_STATUS_EXECUTION_FAILED = 8,
  11.     CUDNN_STATUS_NOT_SUPPORTED = 9,
  12.     CUDNN_STATUS_LICENSE_ERROR = 10,
  13.     CUDNN_STATUS_RUNTIME_PREREQUISITE_MISSING = 11
  14. } cudnnStatus_t;
复制代码
枚举值含义说明CUDNN_STATUS_SUCCESS成功调用成功,没有错误CUDNN_STATUS_NOT_INITIALIZED未初始化cuDNN handle 未创建或初始化失败CUDNN_STATUS_ALLOC_FAILED内存分配失败GPU 内存不足,cudaMalloc 失败CUDNN_STATUS_BAD_PARAM参数错误函数参数有误,类型或维度不匹配CUDNN_STATUS_INTERNAL_ERROR内部错误cuDNN 内部执行错误,不是你传的参数问题CUDNN_STATUS_INVALID_VALUE数值错误数值超出范围,例如 stride、padding 不合理CUDNN_STATUS_ARCH_MISMATCH架构不匹配当前 GPU 架构不支持所选算法CUDNN_STATUS_MAPPING_ERROR内存映射错误GPU 内存映射出错CUDNN_STATUS_EXECUTION_FAILED执行失败kernel 执行失败,例如溢出或 cuda kernel 错误CUDNN_STATUS_NOT_SUPPORTED不支持该功能或数据类型不被支持CUDNN_STATUS_LICENSE_ERROR许可错误需要授权许可,但未满足条件CUDNN_STATUS_RUNTIME_PREREQUISITE_MISSING依赖缺失运行时依赖库缺失或版本不匹配​     描述符是\(cuDNN\) 中用于描述各种对象属性的数据结构,它为cuDNN函数提供了执行操作所需的元数据信息。cuDNN中存在多种类型的描述符,如张量描述符(cudnnTensorDescriptor_t),卷积描述符(cudnnConvolutionDescriptor_t),池化描述符(cudnnPoolingDescriptor_t)等不同类型的描述符用于描述不同的操作。

B.张量描述子(cudnnTensorDescriptor_t)

​       张量描述符(cudnnTensorDescriptor_t) 用于描述各种对象属性的数据结构,它为cuDNN函数提供了执行操作所需的元数据信息。包括数据张量的(如批量大小、通道数、高度、宽度等)、数据类型(如CUDNN_DATA_FLOAT表示32位浮点数)、张量布局(如CUDNN_TENSOR_NCHW表示数据批量-通道-高度-宽度的布局)。通过cudnnCreateTensorDescriptor() 创建,cudnnSetTensor4dDescriptor()等函数设置属性,cudnnDestroyDescriptor()销毁。
  1. cudnnStatus_t cudnnCreateTensorDescriptor(cudnnTensorDescriptor_t *tensorDesc);
  2. cudnnStatus_t cudnnSetTensor4dDescriptor(
  3.     cudnnTensorDescriptor_t tensorDesc,
  4.     cudnnTensorFormat_t format,   // NCHW 或 NHWC
  5.     cudnnDataType_t dataType,     // float, half, double...
  6.     int n, int c, int h, int w    // 维度
  7. );
  8. cudnnStatus_t cudnnDestroyTensorDescriptor(cudnnTensorDescriptor_t tensorDesc);
复制代码
作用:描述输入、输出张量的维度和数据类型。
张量在\(cuDNN\)中通常按照\(\text{4D格式(N,C,H,W)}\)(假设NCHW格式)存储:

  • N:batch size,代表一批量输入样本的数量;
  • C:通道数(例如\(RGB\)图像就是3个通道;灰度图就是1个通道);
  • H:图像的行数,高度
  • W: 图像的列数,宽度
cudnnTensorFormat_t代表张量的格式
  1. typedef enum {
  2.     CUDNN_TENSOR_NCHW = 0,      // [batch, channels, height, width]
  3.     CUDNN_TENSOR_NHWC = 1,      // [batch, height, width, channels]
  4.     CUDNN_TENSOR_NCHW_VECT_C = 2  // 向量化通道格式 (主要用于部分卷积加速)
  5. } cudnnTensorFormat_t;
复制代码
枚举值说明CUDNN_TENSOR_NCHW最常用格式,张量按 [N, C, H, W] 存储,适合大多数卷积、批量计算。CUDNN_TENSOR_NHWC张量按 [N, H, W, C] 存储,在某些 GPU 架构(如 Tensor Core)上可能更快CUDNN_TENSOR_NCHW_VECT_C向量化通道存储格式,一般用于卷积加速,通道数必须是 4 的倍数cudnnDataType_t 是\(\text{cuDNN}\)库中用于指定张量数据类型的枚举类型,它定义了深度学习操作中支持的数据格式,包括精度和存储方式。
  1. typedef enum {
  2.     CUDNN_DATA_FLOAT = 0,       // 32-bit float
  3.     CUDNN_DATA_DOUBLE = 1,      // 64-bit double
  4.     CUDNN_DATA_HALF = 2,        // 16-bit float (FP16)
  5.     CUDNN_DATA_INT8 = 3,        // 8-bit integer
  6.     CUDNN_DATA_INT32 = 4,       // 32-bit integer
  7.     CUDNN_DATA_INT8x4 = 5,      // 8-bit integer pack of 4
  8.     CUDNN_DATA_UINT8 = 6,       // 8-bit unsigned integer
  9.     CUDNN_DATA_INT8x32 = 7      // 8-bit integer pack of 32
  10. } cudnnDataType_t;
复制代码
其枚举值的含义如下:
cudnnDataType_t枚举值数据类型参数说明CUDNN_DATA_FLOATfloat32最常用数据类型,精度高CUDNN_DATA_DOUBLEfloat64双精度,GPU 上使用少,性能低CUDNN_DATA_HALFfloat16半精度,适合 Tensor Core 加速CUDNN_DATA_INT8int8量化神经网络使用CUDNN_DATA_INT32int32常用于累加或输出量化后的结果CUDNN_DATA_INT8x4 / INT8x32int8 pack8-bit 数据打包存储,加速卷积CUDNN_DATA_UINT8uint8无符号 8-bit 数据C.卷积描述子(cudnnConvolutionDescriptor_t)

​     卷积描述符(cudaConvolutionDescriptor_t) 用于描述卷积张量操作的参数,如填充大小(pad_h,pad_w),步长(stride_h,stride_w),dilation(用于空洞卷积)、卷积模式(如CUDNN_CROSS_CORRELATION表示互相关模式,在深度学习中通常等同于卷积操作)。由cudnnCreateConvolutionDescriptor()创建、cudnnSetConvolution2dDescriptor()设置属性,cudnnDestroyConvolutionDescriptor() 销毁。
  1. cudnnStatus_t cudnnCreateConvolutionDescriptor(cudnnConvolutionDescriptor_t *convDesc);
  2. cudnnStatus_t cudnnSetConvolution2dDescriptor(
  3.     cudnnConvolutionDescriptor_t convDesc,
  4.     int pad_h, int pad_w,         // 填充
  5.     int u, int v,                 // 步长 (stride)
  6.     int dilation_h, int dilation_w,
  7.     cudnnConvolutionMode_t mode,  // CUDNN_CONVOLUTION or CUDNN_CROSS_CORRELATION
  8.     cudnnDataType_t computeType   // 计算精度
  9. );
  10. cudnnStatus_t cudnnDestroyConvolutionDescriptor(cudnnConvolutionDescriptor_t convDesc);
复制代码
cudnnConvolutionMode_t  是cuDNN库中用于指定卷积操作类型的枚举类型,主要定义两种模式:标准卷积互相关
  1. typedef enum {
  2.     CUDNN_CONVOLUTION = 0,           // 标准卷积
  3.     CUDNN_CROSS_CORRELATION = 1      // 互相关 (默认)
  4. } cudnnConvolutionMode_t;
复制代码
其枚举值含义:
枚举值含义说明CUDNN_CONVOLUTION卷积卷积核会翻转 180° 再做滑动窗口求和。数学上是标准卷积公式。CUDNN_CROSS_CORRELATION互相关卷积核不翻转,直接在输入上做滑动窗口求和。cuDNN 默认使用这个模式。注意:大多数深度学习框架(PyTorch、TensorFlow)默认使用互相关,所以\(\text{cuDNN}\)也默认使用CUDNN_CROSS_CORRELATION 。
​     过滤器(卷积核)描述子(cudnnFilterDescriptor_t) 是cuDNN中用于描述卷积滤波器(权重)张量的核心数据类型。它是cuDNN卷积操作中不同或缺的组成部分。由cudnnCreateConvolutionDescriptor()来创建,cudnnSetFilter4dDescriptor()来设置卷积算子,cudnnDestroyFilterDescriptor() 销毁;
  1. cudnnStatus_t cudnnCreateFilterDescriptor(cudnnFilterDescriptor_t *filterDesc);
  2. cudnnStatus_t cudnnSetFilter4dDescriptor(
  3.     cudnnFilterDescriptor_t filterDesc,
  4.     cudnnDataType_t dataType, cudnnTensorFormat_t format,
  5.     int k, int c, int h, int w
  6. );
  7. cudnnStatus_t cudnnDestroyFilterDescriptor(cudnnFilterDescriptor_t filterDesc);
复制代码
D. 卷积操作算子(cudnnConvolutionForward)

计算卷积算子的输出尺寸;
  1. cudnnStatus_t cudnnGetConvolution2dForwardOutputDim(
  2.     const cudnnConvolutionDescriptor_t convDesc, // 卷积描述子
  3.     const cudnnTensorDescriptor_t inputTensorDesc, // 输入张量描述子
  4.     const cudnnFilterDescriptor_t filterDesc,      // 卷积核描述子
  5.     int* n, int* c, int* h, int* w                 // 输出张量尺寸
  6. );
复制代码
参数类型说明convDesccudnnConvolutionDescriptor_t卷积参数描述子,包括 padding、stride、dilation、卷积模式等inputTensorDesccudnnTensorDescriptor_t输入张量描述子,包含 N、C、H、W 以及数据类型、存储格式filterDesccudnnFilterDescriptor_t卷积核描述子,包含 K、C、R、Sn, c, h, wint*输出张量的 N、C、H、W,由函数计算后返回对于输入尺寸\(H_{in}\times W_{in}\) 和卷积核\(R\times{S}\):

\[\begin{aligned}&H_{out}=\left[\frac{H_{in}+2\cdot pad_h-dilation_h\cdot(R-1)-1}{2}+1\right]\\&W_{out}=\left[\frac{W_{out}+2\cdot pad_h-dilation_w\cdot(S-1)-1}{2}+1\right]\end{aligned}\]
选择卷积算法:
调用 cuDNN 核心 API,在 GPU 上执行 3D 卷积计算,结果存储到 GPU 输出内存 d_output 中。
  1. float alpha = 1.0f, beta = 0.0f;
  2. CHECK_CUDNN(cudnnConvolutionForward(cudnn, &alpha, inputDesc, d_input, filterDesc, d_filter,convDesc, perfResults.algo, d_workspace, workspace_bytes, &beta, outputDesc, d_output));
复制代码
alpha=1.0f、beta=0.0f:卷积计算的系数,对应公式:output=α×conv(input,kernel)+β×output
cudnnConvolutionForward:cuDNN 前向卷积核心 API,参数依次是:
cuDNN 句柄 → 系数 α → 输入描述符 → GPU 输入数据 → 卷积核描述符 → GPU 卷积核 → 卷积规则描述符 → 最优算法 → 工作空间 → 工作空间大小 → 系数 β → 输出描述符 → GPU 输出数据。
2.卷积案例
  1. void compute_stride(const int* size, int* stride) {
  2.     for (int i = 4; i >= 0; i--)
  3.         stride[i] = (i == 4) ? 1 : size[i + 1] * stride[i + 1];
  4. }
  5. void cudnn_conv_3d(const float* input, const float* kernel,
  6.     float* output, const int input_dims[3],
  7.     const int kernel_dims[3]) {
  8.     // -------------------------- 1. 初始化CUDNN句柄 --------------------------
  9.     cudnnHandle_t cudnn;
  10.     CHECK_CUDNN(cudnnCreate(&cudnn));
  11.     // -------------------------- 2. 构造输入张量描述符 --------------------------
  12.     int inputDims[5] = { 1, 1, input_dims[0], input_dims[1], input_dims[2] }; // N=1, C=1, D/H/W
  13.     int input_stride[5];
  14.     compute_stride(inputDims, input_stride);
  15.     cudnnTensorDescriptor_t inputDesc;
  16.     CHECK_CUDNN(cudnnCreateTensorDescriptor(&inputDesc));
  17.     CHECK_CUDNN(cudnnSetTensorNdDescriptor(inputDesc, CUDNN_DATA_FLOAT, 5, inputDims, input_stride));
  18.     // -------------------------- 3. 构造卷积核描述符 --------------------------
  19.     int filterDims[5] = { 1, 1, kernel_dims[0], kernel_dims[1], kernel_dims[2] }; // 输出通道=1, 输入通道=1, 核尺寸
  20.     cudnnFilterDescriptor_t filterDesc;
  21.     CHECK_CUDNN(cudnnCreateFilterDescriptor(&filterDesc));
  22.     CHECK_CUDNN(cudnnSetFilterNdDescriptor(filterDesc, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW, 5, filterDims));
  23.     // -------------------------- 4. 计算SAME填充并构造卷积描述符 --------------------------
  24.     int conmv_padA[3];
  25.     for (int i = 0; i < 3; i++) {
  26.         conmv_padA[i] = (kernel_dims[i] - 1) / 2; // 计算SAME填充
  27.     }
  28.     int conv_filterStrideA[3] = { 1, 1, 1 };       // 步长=1
  29.     int conv_dilationA[3] = { 1, 1, 1 };           // 膨胀=1
  30.     cudnnConvolutionDescriptor_t convDesc;
  31.     CHECK_CUDNN(cudnnCreateConvolutionDescriptor(&convDesc));
  32.     CHECK_CUDNN(cudnnSetConvolutionNdDescriptor(convDesc, 3, conmv_padA, conv_filterStrideA,
  33.         conv_dilationA, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT));
  34.     // -------------------------- 5. 计算输出张量尺寸并构造描述符 --------------------------
  35.     int outputDims[5];
  36.     CHECK_CUDNN(cudnnGetConvolutionNdForwardOutputDim(convDesc, inputDesc, filterDesc, 5, outputDims));
  37.     int output_stride[5];
  38.     compute_stride(outputDims, output_stride);
  39.     cudnnTensorDescriptor_t outputDesc;
  40.     CHECK_CUDNN(cudnnCreateTensorDescriptor(&outputDesc));
  41.     CHECK_CUDNN(cudnnSetTensorNdDescriptor(outputDesc, CUDNN_DATA_FLOAT, 5, outputDims, output_stride));
  42.     // -------------------------- 6. 选取最优卷积算法 --------------------------
  43.     size_t in_bytes, out_bytes;
  44.     CHECK_CUDNN(cudnnGetTensorSizeInBytes(inputDesc, &in_bytes));
  45.     CHECK_CUDNN(cudnnGetTensorSizeInBytes(outputDesc, &out_bytes));
  46.     size_t filt_bytes = 1;
  47.     for (int i = 0; i < 5; i++) filt_bytes *= filterDims[i];
  48.     filt_bytes *= sizeof(float);
  49.     // 获取最优卷积算法
  50.     int returnedAlgoCount = 0;
  51.     cudnnConvolutionFwdAlgoPerf_t perfResults;
  52.     CHECK_CUDNN(cudnnGetConvolutionForwardAlgorithm_v7(cudnn, inputDesc, filterDesc, convDesc, outputDesc,
  53.         1, &returnedAlgoCount, &perfResults));
  54.     // 计算工作空间大小
  55.     size_t workspace_bytes = 0;
  56.     CHECK_CUDNN(cudnnGetConvolutionForwardWorkspaceSize(cudnn, inputDesc, filterDesc, convDesc, outputDesc,
  57.         perfResults.algo, &workspace_bytes));
  58.     // -------------------------- 7. 分配设备内存并拷贝数据 --------------------------
  59.     float* d_input = nullptr, * d_filter = nullptr, * d_output = nullptr;
  60.     void* d_workspace = nullptr;
  61.     CHECK_CUDA(cudaMalloc((void**)&d_input, in_bytes));
  62.     CHECK_CUDA(cudaMalloc((void**)&d_filter, filt_bytes));
  63.     CHECK_CUDA(cudaMalloc((void**)&d_output, out_bytes));
  64.     if (workspace_bytes > 0) {
  65.         CHECK_CUDA(cudaMalloc((void**)&d_workspace, workspace_bytes));
  66.     }
  67.     // 主机到设备拷贝
  68.     CHECK_CUDA(cudaMemcpy(d_input, input, in_bytes, cudaMemcpyHostToDevice));
  69.     CHECK_CUDA(cudaMemcpy(d_filter, kernel, filt_bytes, cudaMemcpyHostToDevice));
  70.     // -------------------------- 8. 执行卷积计算 --------------------------
  71.     float alpha = 1.0f, beta = 0.0f;
  72.     CHECK_CUDNN(cudnnConvolutionForward(cudnn, &alpha, inputDesc, d_input, filterDesc, d_filter,
  73.         convDesc, perfResults.algo, d_workspace, workspace_bytes,
  74.         &beta, outputDesc, d_output));
  75.     // -------------------------- 9. 设备到主机拷贝输出 --------------------------
  76.     CHECK_CUDA(cudaMemcpy(output, d_output, out_bytes, cudaMemcpyDeviceToHost));
  77.     // -------------------------- 10. 资源释放 --------------------------
  78.     CHECK_CUDA(cudaFree(d_input));
  79.     CHECK_CUDA(cudaFree(d_filter));
  80.     CHECK_CUDA(cudaFree(d_output));
  81.     CHECK_CUDA(cudaFree(d_workspace));
  82.     CHECK_CUDNN(cudnnDestroyTensorDescriptor(outputDesc));
  83.     CHECK_CUDNN(cudnnDestroyConvolutionDescriptor(convDesc));
  84.     CHECK_CUDNN(cudnnDestroyFilterDescriptor(filterDesc));
  85.     CHECK_CUDNN(cudnnDestroyTensorDescriptor(inputDesc));
  86.     CHECK_CUDNN(cudnnDestroy(cudnn));
  87. }
复制代码
[code]#define _CRT_SECURE_NO_WARNINGS#include "rscudaalgo.h"#include #include #include #include #include #include "fundamental.h"#include "segy.h"#include "alloc.h"#include "qdebug.h"//测试三维卷积void test_conv3d() {        const char* filename_input = "smallCube.sgy";   //输入文件名        const char* filename_output = "conv_small.sgy";            //输出文件名        FILE* fp_input = nullptr;                                 //输入文件的文件指针        FILE* fp_output = nullptr;                                 //输出文件的文件指针        bhed fileheader;                                           //定义文件头        segy* traceheader_array = nullptr;                                 //道头数组指针        float* datacube_input = nullptr;                        //输入地震数据的三维指针        float* datacube_output = nullptr;                       //输出地震数据的三维指针        unsigned int size_fileheader = sizeof(fileheader);        unsigned int size_traceheader = sizeof(segy);        unsigned int nline = 0;                                        //三维地震数据的测线数        unsigned int ncdp = 0;                                        //三维地震数据的CDP数        unsigned int nt = 0;                                         //三维地震数据的时间采样数        long long ntrace = 0;        long long size_inputcube = 0;        long long size_trace = 0;                nline = 100;        ncdp = 500;        fp_input = fopen(filename_input, "rb");        if (fp_input == nullptr) {                printf("Cannot open this input file!!!\n");                qDebug()

相关推荐

您需要登录后才可以回帖 登录 | 立即注册