掌握CUDA的编程思路,利用GPU实现高性能计算就基本上可以应用到实际项目中;本主题简单利用CUDA的工具包提供的cuDNN深度学习库实现一个Sobel- y方向的梯度计算。cuDNN主要实现了深度学习中卷积运算与循环递归运算的GPU封装。这种封装思想与Tensorflow,Torch基本上如出一辙。
本主题主要理顺了计算数据的布局关系与函数参数的理解。
一如既往的我们使用VSCode + Makefile,而没有使用VS IDE,可以有助于理解这些开发环境的工作细节。
Makefile的截图
cuDNN的安装
- 下载地址
https://developer.nvidia.com/rdp/cudnn-download
-
需要注册与调查,使用微信或者QQ登录即可。
选择合适的cuDNN的版本
-
下载的注意事项
- 确保有一块支持CUDA的GPU显卡;
- 确保GPU指出CUDA对应的版本;
-
安装
- Window下载的是zip文件,安装不按照一般的套路,需要手工完成。因为下载的主要是三个文件:
- 头文件h文件;
- 运行库文件dll文件;
- 编译时的链接符号文件lib文件
- 手工把三个文件拷贝到Nvidia CUDA对应的目录即可:
- include目录;
- bin目录;
- lib目录;
- Window下载的是zip文件,安装不按照一般的套路,需要手工完成。因为下载的主要是三个文件:
编程模式
模式说明
-
- 创建cuDNN句柄;
cudnnStatus_t cudnnCreate(cudnnHandle_t *handle)
-
- 以Host方式调用在Device上运行的函数;
- 比如卷积运算:
cudnnConvolutionForward
等
-
- 释放cuDNN句柄;
cudnnStatus_t cudnnDestroy(cudnnHandle_t handle)
-
编程模式的例子
#include <iostream>
#include <cudnn.h>
int main(int argc, const char **argv){
cudnnStatus_t cudnn_re;
cudnnHandle_t h_cudnn;
// 创建cuDNN上下文句柄
cudnn_re = cudnnCreate(&h_cudnn);
if(cudnn_re != CUDNN_STATUS_SUCCESS){
std::cout << "创建cuDNN上下文失败!" << std::endl;
}
std::cout << "cuDD创建成功!" << std::endl;
// =========================================cudnn操作
// 获取版本
size_t version = cudnnGetVersion();
size_t rt_version = cudnnGetCudartVersion();
std::cout << "cuDNN的版本:" << version << std::endl;
std::cout << "CUDA的运行时版本:" << rt_version << std::endl;
// =================================================
// 释放cuDNN
cudnnDestroy(h_cudnn);
}
卷积运算的编程例子
- cudnn是使用C++语法封装,因为cudnn提供了运算符重载与泛型操作,这两个语法都只有C++提供。
Window环境下的编译配置
CPLUS_ARGS = /EHsc
INCLUDES =/I "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.1\include" /source-charset:utf-8 /execution-charset:utf-8
LIBS =/link /LIBPATH:"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.1\lib\x64" /DYNAMICBASE \
"cudnn.lib" \
"cuda.lib" \
"cudart.lib"
# main: nn01_model.cpp
# @cl /nologo $(CPLUS_ARGS) $(INCLUDES) nn01_model.cpp $(LIBS) /out:main.exe
main: nn02_conv.cpp
@cl /nologo $(CPLUS_ARGS) $(INCLUDES) nn02_conv.cpp $(LIBS) /out:main.exe
clean:
@del *.exe *.exp *.obj gpu_out.bmp 2>Nul
cuDNN的卷积函数定义
- cuDNN定义定义的卷积函数为cudnnConvolutionForward
- 该函数第一个参数就需要是cudnn的上下文句柄cudnnHandle_t。
cudnnStatus_t cudnnConvolutionForward(
cudnnHandle_t handle,
const void *alpha,
const cudnnTensorDescriptor_t xDesc,
const void *x,
const cudnnFilterDescriptor_t wDesc,
const void *w,
const cudnnConvolutionDescriptor_t convDesc,
cudnnConvolutionFwdAlgo_t algo,
void *workSpace,
size_t workSpaceSizeInBytes,
const void *beta,
const cudnnTensorDescriptor_t yDesc,
void *y)
-
基于对卷积数学运算的常识,上面的参数比较容易理解。反而是其中的惨数的数据类型需要精确理解与使用。
-
cudnn对一些命名习惯遵循了惯例:
- x输入;
- y输出;
- w权重;
- C,H,W图像的通道,高度、宽度;
- c,C当前与总的输入通道;
- n当前输入批次大小;
- k,K当前与总的输出通道;
- p,q当前输出图像的像素的y坐标与x坐标;
- G分组数量;
- pad补丁值;
- u,v采样的垂直与水平步长;
- h与w方向的偏差;
- r,s当前滤波核的高度、宽度;
- R,S总的滤波核的高度、宽度
- 分别为
-
卷积的定义如下:
卷积的实现过程
加载数据
- 我们使用原来使用过的函数加载BMP图像。
- 图像像素的通道是挨着的RGBA。我们考虑不对A做卷积运算;
- 对前面的读取与保存函数略作修改
- 读取RGB
- 保存为RGB
数据格式布局选择
- 根据上面的描述我们的格式选择NHWC是做合适的。
-
NHWC 布局格式
NHWC数据布局 -
NCHW 布局格式
NCHW布局格式
数据类型的选择
- 由于卷积运算不支持整数计算,所有整个过程全部使用float运算
数据结构与数据定义
#include <iostream>
#include <cuda.h>
#include <cudnn.h>
// 结构体定义
#pragma pack(1)
struct img_header{
// 文件头
char magic[2]; // 魔法字
unsigned int file_size; // 文件大小
unsigned char reserve1[4]; // 跳4字节
unsigned int data_off; // 数据区开始位置
// 信息头
unsigned char reserve2[4]; // 跳4字节
int width; // 图像宽度
int height; // 图像高度
unsigned char reserve3[2]; // 跳2字节
unsigned short int bit_count; // 图像位数1,4,8,16,24,32
unsigned char reserve4[24]; // 跳24字节
};
// 偷懒写一个匿名全局类
// 全局数据
struct img_header header;
uchar3 *img; // 使用gpu的扩展类型(只读取RGB)
float3 *f_img; // 临时存储浮点数的像素的。
float3 *img_gpu; // 输入
float3 *conv_gpu; // 输出
float3 *kernel_gpu; // 卷积核
// 输入/输出文件名
const char *in_filename = "gpu.bmp";
const char *out_filename = "gpu_out.bmp";
// 打开图像
void read_bmp(); // 无参数,采用全局成员
// 保存图像
void save_bmp();
// 内存释放
void free_mem();
// Host <-> Device
void move_to_device();
void move_to_host();
void create_kernel();
// cudnn卷积计算封装
void cudnn_conv();
图像读取与保存
- 其中读取的时候,只读取RGB,并存储为浮点数;
- 可以使用OpenCV的图像读取函数。
- 保存的时候保存为24位图;
- 具体的代码见附录。
GPU内存与Host内存
- 从GPU拷贝的数据,直接转换拷贝到unsigned char的Host缓冲中。然后保存。
void move_to_host(){
// 把选装后的图像拷贝到Host内存,用来保存到磁盘
cudaMemcpy((void*)f_img, (void*)conv_gpu, header.height * header.width * sizeof(float3), cudaMemcpyDeviceToHost);
// 循环把图像转换为uchar3
for(int i = 0; i < header.height * header.width; i++){
img[i].x = (unsigned char)f_img[i].x;
img[i].y = (unsigned char)f_img[i].y;
img[i].z = (unsigned char)f_img[i].z;
}
}
卷积核
- 为了只管,直接使用了Sobel核;
void create_kernel(){
cudaMalloc((void**)&kernel_gpu, 3 * 3 * 3 * sizeof(float3)); // 返回指针,则参数就需要二重指针。
// 卷积核
float3 data_kernel[] = {
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f)
};
// 拷贝到device
cudaMemcpy((void*)kernel_gpu, (void*)data_kernel, 3 * 3 * 3 * sizeof(float3), cudaMemcpyHostToDevice);
}
核心的cudd4nn的卷积运行模式
void cudnn_conv(){
// 返回状态
cudnnStatus_t status;
//cudnn句柄
cudnnHandle_t h_cudnn;
// 创建cuDNN上下文句柄
cudnnCreate(&h_cudnn);
// =================================================输入输出张量
// 1. 定义一个张量对象
cudnnTensorDescriptor_t ts_in, ts_out;
// 2. 创建输入张量
status = cudnnCreateTensorDescriptor(&ts_in);
if(CUDNN_STATUS_SUCCESS == status){
std::cout << "创建输入张量成功!" << std::endl;
}
// 3. 设置输入张量数据
status = cudnnSetTensor4dDescriptor(
ts_in, // 张量对象
CUDNN_TENSOR_NHWC, // 张量的数据布局
CUDNN_DATA_FLOAT, // 张量的数据类型
1, // 图像数量
3, // 图像通道
1080, // 图像高度
1920 // 图像宽度
);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建输出张量成功!" << std::endl;
// 类似创建输出的张量
cudnnCreateTensorDescriptor(&ts_out);
status = cudnnSetTensor4dDescriptor(ts_out, CUDNN_TENSOR_NHWC, CUDNN_DATA_FLOAT, 1, 3, 1080, 1920);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "设置输出张量成功!" << std::endl;
// =================================================滤波核
cudnnFilterDescriptor_t kernel;
cudnnCreateFilterDescriptor(&kernel);
status = cudnnSetFilter4dDescriptor(kernel, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NHWC, 3, 3, 3, 3);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建卷积核张量成功!" << std::endl;
// =================================================卷积描述
cudnnConvolutionDescriptor_t conv;
status = cudnnCreateConvolutionDescriptor(&conv);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建卷积成功!" << std::endl;
status = cudnnSetConvolution2dDescriptor(conv, 1, 1, 1, 1, 1, 1, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "设置卷积成功!" << std::endl;
// =================================================卷积算法
cudnnConvolutionFwdAlgo_t algo;
status = cudnnGetConvolutionForwardAlgorithm(h_cudnn, ts_in, kernel, conv, ts_out, CUDNN_CONVOLUTION_FWD_PREFER_FASTEST, 0, &algo);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "获取算法成功!" << std::endl;
// =================================================工作区
size_t workspace_size = 0;
status = cudnnGetConvolutionForwardWorkspaceSize(h_cudnn, ts_in, kernel, conv, ts_out, algo, &workspace_size);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "获取工作空间大小成功!" << std::endl;
std::cout << "卷积计算空间大小" << workspace_size << std::endl;
// 分配工作区空间
void * workspace;
cudaMalloc(&workspace, workspace_size);
// =================================================线性因子
float alpha = 1.0f;
float beta = -100.0f;
// =================================================卷积执行
status = cudnnConvolutionForward(
h_cudnn,
&alpha,
ts_in,
img_gpu, // 输入
kernel,
kernel_gpu, // 核
conv,
algo,
workspace,
workspace_size,
&beta,
ts_out,
conv_gpu // 输出
);
if (status == CUDNN_STATUS_SUCCESS) {
std::cout << "卷积计算成功!" << std::endl;
}else{
std::cout << "卷积计算失败!" << std::endl;
}
// 释放cuDNN
cudnnDestroy(h_cudnn);
}
运算后的图像效果
-
与我们实现的Sobel滤波一样的效果,贴图如下:
Sobel滤波
附录
- 卷积运算的全部实现代码
#include <iostream>
#include <cuda.h>
#include <cudnn.h>
// 结构体定义
#pragma pack(1)
struct img_header{
// 文件头
char magic[2]; // 魔法字
unsigned int file_size; // 文件大小
unsigned char reserve1[4]; // 跳4字节
unsigned int data_off; // 数据区开始位置
// 信息头
unsigned char reserve2[4]; // 跳4字节
int width; // 图像宽度
int height; // 图像高度
unsigned char reserve3[2]; // 跳2字节
unsigned short int bit_count; // 图像位数1,4,8,16,24,32
unsigned char reserve4[24]; // 跳24字节
};
// 偷懒写一个匿名全局类
// 全局数据
struct img_header header;
uchar3 *img; // 使用gpu的扩展类型(只读取RGB)
float3 *f_img;
float3 *img_gpu; // 输入
float3 *conv_gpu; // 输出
float3 *kernel_gpu; // 卷积核
// 输入/输出文件名
const char *in_filename = "gpu.bmp";
const char *out_filename = "gpu_out.bmp";
// 打开图像
void read_bmp(); // 无参数,采用全局成员
// 保存图像
void save_bmp();
// 内存释放
void free_mem();
// Host <-> Device
void move_to_device();
void move_to_host();
void create_kernel();
// cudnn卷积计算封装
void cudnn_conv();
int main(int argc, const char **argv){
read_bmp();
move_to_device();
create_kernel();
cudnn_conv();
move_to_host();
save_bmp();
free_mem();
}
void cudnn_conv(){
// 返回状态
cudnnStatus_t status;
//cudnn句柄
cudnnHandle_t h_cudnn;
// 创建cuDNN上下文句柄
cudnnCreate(&h_cudnn);
// =================================================输入输出张量
// 1. 定义一个张量对象
cudnnTensorDescriptor_t ts_in, ts_out;
// 2. 创建输入张量
status = cudnnCreateTensorDescriptor(&ts_in);
if(CUDNN_STATUS_SUCCESS == status){
std::cout << "创建输入张量成功!" << std::endl;
}
// 3. 设置输入张量数据
status = cudnnSetTensor4dDescriptor(
ts_in, // 张量对象
CUDNN_TENSOR_NHWC, // 张量的数据布局
CUDNN_DATA_FLOAT, // 张量的数据类型
1, // 图像数量
3, // 图像通道
1080, // 图像高度
1920 // 图像宽度
);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建输出张量成功!" << std::endl;
// 类似创建输出的张量
cudnnCreateTensorDescriptor(&ts_out);
status = cudnnSetTensor4dDescriptor(ts_out, CUDNN_TENSOR_NHWC, CUDNN_DATA_FLOAT, 1, 3, 1080, 1920);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "设置输出张量成功!" << std::endl;
// =================================================滤波核
cudnnFilterDescriptor_t kernel;
cudnnCreateFilterDescriptor(&kernel);
status = cudnnSetFilter4dDescriptor(kernel, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NHWC, 3, 3, 3, 3);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建卷积核张量成功!" << std::endl;
// =================================================卷积描述
cudnnConvolutionDescriptor_t conv;
status = cudnnCreateConvolutionDescriptor(&conv);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "创建卷积成功!" << std::endl;
status = cudnnSetConvolution2dDescriptor(conv, 1, 1, 1, 1, 1, 1, CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "设置卷积成功!" << std::endl;
// =================================================卷积算法
cudnnConvolutionFwdAlgo_t algo;
status = cudnnGetConvolutionForwardAlgorithm(h_cudnn, ts_in, kernel, conv, ts_out, CUDNN_CONVOLUTION_FWD_PREFER_FASTEST, 0, &algo);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "获取算法成功!" << std::endl;
// =================================================工作区
size_t workspace_size = 0;
status = cudnnGetConvolutionForwardWorkspaceSize(h_cudnn, ts_in, kernel, conv, ts_out, algo, &workspace_size);
if(CUDNN_STATUS_SUCCESS == status) std::cout << "获取工作空间大小成功!" << std::endl;
std::cout << "卷积计算空间大小" << workspace_size << std::endl;
// 分配工作区空间
void * workspace;
cudaMalloc(&workspace, workspace_size);
// =================================================线性因子
float alpha = 1.0f;
float beta = -100.0f;
// =================================================数据准备
// 见全局函数的实现。
// =================================================卷积执行
status = cudnnConvolutionForward(
h_cudnn,
&alpha,
ts_in,
img_gpu, // 输入
kernel,
kernel_gpu, // 核
conv,
algo,
workspace,
workspace_size,
&beta,
ts_out,
conv_gpu // 输出
);
if (status == CUDNN_STATUS_SUCCESS) {
std::cout << "卷积计算成功!" << std::endl;
}else{
std::cout << "卷积计算失败!" << std::endl;
}
// 释放cuDNN
cudnnDestroy(h_cudnn);
}
void create_kernel(){
cudaMalloc((void**)&kernel_gpu, 3 * 3 * 3 * sizeof(float3)); // 返回指针,则参数就需要二重指针。
// 卷积核
float3 data_kernel[] = {
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f),
make_float3(-2.0f, -2.0f, -2.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(2.0f, 2.0f, 2.0f),
make_float3(-1.0f, -1.0f, -1.0f), make_float3(0.0f, 0.0f, 0.0f), make_float3(1.0f, 1.0f, 1.0f)
};
// 拷贝到device
cudaMemcpy((void*)kernel_gpu, (void*)data_kernel, 3 * 3 * 3 * sizeof(float3), cudaMemcpyHostToDevice);
}
void move_to_host(){
// 把选装后的图像拷贝到Host内存,用来保存到磁盘
cudaMemcpy((void*)f_img, (void*)conv_gpu, header.height * header.width * sizeof(float3), cudaMemcpyDeviceToHost);
// 循环把图像转换为uchar3
for(int i = 0; i < header.height * header.width; i++){
img[i].x = (unsigned char)f_img[i].x;
img[i].y = (unsigned char)f_img[i].y;
img[i].z = (unsigned char)f_img[i].z;
}
}
void move_to_device(){
// 分配GPU内存
cudaMalloc((void**)&img_gpu, header.height * header.width * sizeof(float3)); // 返回指针,则参数就需要二重指针。
// 拷贝数据
cudaMemcpy((void*)img_gpu, (void*)f_img, header.height * header.width * sizeof(float3), cudaMemcpyHostToDevice);
// 用来存储卷积计算输出
cudaMalloc((void**)&conv_gpu, header.height * header.width * sizeof(float3)); // 返回指针,则参数就需要二重指针。
// 初始化为指定为0
cudaMemset(conv_gpu, 0, header.height * header.width * sizeof(float3));
}
void read_bmp(){
/* 读取头,分配内存,读取数据,这里数据采用了一维数组,使用的时候,需要转换处理下。*/
FILE *file = fopen(in_filename, "rb");
// 读取头
size_t n_bytes = fread(&header, 1, 54, file);
// 计算读取的大大小,并分配空间,并读取。
header.height = header.height >= 0? header.height : -header.height;
img = (uchar3 *)malloc(header.height * header.width * sizeof(uchar3));
f_img = (float3 *)malloc(header.height * header.width * sizeof(float3));
uchar4 buffer;
// 循环只读取RGB
for(int i = 0; i < header.height * header.width; i++){
fread((void*)&buffer, sizeof(uchar4), 1, file);
memcpy(&img[i], &buffer, sizeof(uchar3));
f_img[i].x = (float)buffer.x;
f_img[i].y = (float)buffer.y;
f_img[i].z = (float)buffer.z;
}
fclose(file); // 关闭文件
}
void save_bmp(){
/* 使用与读取一样的头信息保存图像 */
FILE *file = fopen(out_filename, "wb");
// 写头
header.bit_count = 24; //修改图像的位数
header.file_size = 54 + header.height * header.width * sizeof(uchar3); // 修改文件大小
header.height = -header.height;
size_t n_bytes = fwrite(&header, 1, 54, file);
header.height = -header.height;
// 写图像数据
n_bytes = fwrite(img, sizeof(uchar3), header.height * header.width, file);
// 关闭文件
fclose(file);
}
void free_mem(){
/* 释放Host与Device内存 */
free(img); // 直接释放(不需要指定大小,malloc系列函数有内部变量管理分配的内存)
cudaFree(img_gpu);
cudaFree(conv_gpu);
cudaFree(kernel_gpu);
}
网友评论