GPU编程学习入门

国庆假期,赶上疫情,因此只能在家学习了。之前有一些很酷的想法,在CPU上计算效率不够,无法应用到工程中,但在GPU上有对应的解决方案,因此趁此机会,学习一下GPU编程的相关基础知识。

正好,之前大概是19年,为了解决板材缺陷分类问题,自己购置了一张RTX2060显卡,使用开源yolov3框架。但调用的都是别人写好的库,真正的GPU编程还没有接触过。

本文主要是记录一下学习GPU编程的过程,在一到两天的时间里,对GPU编程思想有一个大概的认识,为将来深入学习打下基础,也对GPU编程可以解决的问题边界有一个认知,不被讲PPT的忽悠。

本文使用的显卡是Nvidia,编程语言是CUDA。

历史背景

GPU是计算机游戏发展的产物,为了提高游戏性能,在20世纪90年代产生了加速浮点运算的外插卡。后来板卡厂商将3D图像渲染采用专用硬件实现,用来处理大量的三角片纹理映射等浮点计算,逐渐发展成GPU(Graphics Processing Unit)。再后来研究高性能计算的学者,提出可以将本领域的问题,通过图像学API伪装成三角片映射,以此实现高效求解。GPU制造商发现了这一趋势,转而开发更加通用的GPU产品,称为GPGPU(通用GPU),Nvidia就是这些制造商之一。

GPGPU只是硬件,要想实现功能,还需要有软件体系结构。当前主流的两种GPU语言有CUDA和OpenCL。
CUDA(Compute Unified Device Architecture),由显卡厂商NVIDIA在2007年推出,提供了一套通用的并行计算架构与编程模型。使开发人员可以使用c语言开发在GPU上运行的代码,利用GPU的并行特性,这些代码可以达到极高的运行效率。

基本概念

CPU和主存被称为主机(Host),GPU和显存被称为设备(Device),CPU无法直接读显存,GPU无法直接读主存,两者间需要数据传输。

要为两个完全不同的处理设备编写程序,最好的办法是设计能同时编译CPU和GPU代码的编译器,使一套代码同时在CPU和GPU运行。Nvidia给出的解决方案是nvcc编译器。

hello world

介绍任何语言的开头必定会有一个“hello world”的例子。

#include <stdio.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"

__global__ void hello_world(void)
{
	printf("GPU: Hello world! Thread id : %d\n", threadIdx.x);
}

int main() 
{
	cudaError_t cudaStatus;
	cudaStatus = cudaSetDevice(0);
    //调用核函数,在GPU执行
	hello_world <<<1, 5>> >();
    //必须被调用来供调试工具分析
	cudaDeviceReset();
	system("pause");
	return 0;
}

执行结果如下:

请添加图片描述

两个数组相加

这是CUDA编程自带的一个例子

__global__ void addKernel(int *c, const int *a, const int *b)
{
	int i = threadIdx.x;
	c[i] = a[i] + b[i];
}

int main()
{
	cudaError_t cudaStatus;
    //用于计时
	cudaEvent_t time0, time1;
	cudaEventCreate(&time0); cudaEventCreate(&time1);

	//选择显卡设备,默认是0,多显卡环境下调用
	cudaStatus = cudaSetDevice(0);

	int a[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int b[10] = { 0,11,22,33,44,55,66,77,88,99 };
	int c[10] = { 0 };
	int *dev_a = 0;
	int *dev_b = 0;
	int *dev_c = 0;

	//在显卡上分配内存
	cudaStatus = cudaMalloc((void**)&dev_a, 10 * sizeof(int));
	cudaStatus = cudaMalloc((void**)&dev_b, 10 * sizeof(int));
	cudaStatus = cudaMalloc((void**)&dev_c, 10 * sizeof(int));

	//内存拷贝到显存
	cudaStatus = cudaMemcpy(dev_a, a, 10 * sizeof(int), cudaMemcpyHostToDevice);
	cudaStatus = cudaMemcpy(dev_b, b, 10 * sizeof(int), cudaMemcpyHostToDevice);

	//调用和函数,执行GPU处理
	cudaEventRecord(time0, 0);
	addKernel << <1, 10 >> >(dev_c, dev_a, dev_b);
	cudaEventRecord(time1, 0);

	//告诉Nvidia运行时引擎,对给定的事件执行同步操作,确保时间正确
	cudaEventSynchronize(time0); cudaEventSynchronize(time1);
	//由两个事件计算得到时间
	float ms = 0;
	cudaEventElapsedTime(&ms, time0, time1);
	std::cout << "GPU耗时:" << ms << "ms" << std::endl;

	cudaEventDestroy(time0); cudaEventDestroy(time1);

	//执行过程是否有问题
	cudaStatus = cudaGetLastError();
	if (cudaStatus != cudaSuccess)
	{
		fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus));
		return -1;
	}

	//等待所有kernal执行完
	cudaStatus = cudaDeviceSynchronize();
	if (cudaStatus != cudaSuccess) 
	{
		fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus);
		return -1;
	}

	//从显存拷贝到主存
	cudaStatus = cudaMemcpy(c, dev_c, 10 * sizeof(int), cudaMemcpyDeviceToHost);

	//释放显存
	cudaFree(dev_a);
	cudaFree(dev_b);
	cudaFree(dev_c);

	//必须被调用来供调试工具分析
	cudaDeviceReset();
	system("pause");
	return 0;
}

好的,那么接下来:
请添加图片描述

图像翻转

首先是bmp文件的读写

#pragma pack(2)
typedef struct tagBITMAPFILEHEADER
{
	unsigned short bfType;    // 19778,必须是BM字符串,对应的十六进制为0x4d42,十进制为19778,否则不是bmp格式文件(0-4)
	int bfSize; // 文件大小 以字节为单位(2-5字节)                                                               (4-8)
	unsigned short bfReserved1; // 保留,必须设置为0 (6-7字节)                                                          (8-10)
	unsigned short bfReserved2; // 保留,必须设置为0 (8-9字节)                                                          (10-12)
	int bfOffBits;    // 从文件头到像素数据的偏移  (10-13字节)                                                (12-16)
}BITMAPFILEHEADER;
#pragma pack()

typedef struct tagBITMAPINFOHEADER
{
	unsigned int    biSize;          // 此结构体的大小 (14-17字节)
	long            biWidth;         // 图像的宽  (18-21字节)
	long            biHeight;        // 图像的高  (22-25字节)
	unsigned short  biPlanes;        // 表示bmp图片的平面属,显然显示器只有一个平面,所以恒等于1 (26-27字节)
	unsigned short  biBitCount;      // 一像素所占的位数,一般为24   (28-29字节)
	unsigned int    biCompression;   // 说明图象数据压缩的类型,0为不压缩。 (30-33字节)
	unsigned int    biSizeImage;     // 像素数据所占大小, 这个值应该等于上面文件头结构中bfSize-bfOffBits (34-37字节)
	long            biXPelsPerMeter; // 说明水平分辨率,用象素/米表示。一般为0 (38-41字节)
	long            biYPelsPerMeter; // 说明垂直分辨率,用象素/米表示。一般为0 (42-45字节)
	unsigned int    biClrUsed;       // 说明位图实际使用的彩色表中的颜色索引数(设为0的话,则说明使用所有调色板项)。 (46-49字节)
	unsigned int    biClrImportant;  // 说明对图象显示有重要影响的颜色索引的数目,如果是0,表示都重要。(50-53字节)
} BITMAPINFOHEADER;

核函数定义

__global__ void Hflip(uint8_t* pSrc, uint8_t* pDst, int nW)
{
	int ThrPreBlk = blockDim.x;//每块多少线程   256
	int MYbid = blockIdx.x;	//块索引
	int MYtid = threadIdx.x;//线程索引
	int MYgtid = ThrPreBlk * MYbid + MYtid; //全局索引

	int BlkPerRow = (nW + ThrPreBlk - 1) / ThrPreBlk;//每行使用多少block,因为一个像素使用一个线程
	int RowBytes = (nW * 3 + 3)&(~3);//每行像素数,向上取4的倍数

	int MYRow = MYbid / BlkPerRow;
	int MYCol = MYgtid - MYRow*BlkPerRow*ThrPreBlk;

	if (MYCol >= nW) return;

	int MYMirrorCol = nW - 1 - MYCol;
	int MYOffset = MYRow*RowBytes;
	int MYSrcIndex = MYOffset + 3 * MYCol;
	int MYDstIndex = MYOffset + 3 * MYMirrorCol;

	pDst[MYDstIndex] = pSrc[MYSrcIndex];
	pDst[MYDstIndex+1] = pSrc[MYSrcIndex+1];
	pDst[MYDstIndex+2] = pSrc[MYSrcIndex+2];
}

主流程:

int main()
{
	//bmp文件读取
	std::ifstream ifs;
	ifs.open("D:\\1.bmp", std::ifstream::in | std::ifstream::binary);

	if (!ifs.is_open())
	{
		std::cout << "文件打开失败" << std::endl;
		return;
	}

	char buf[54];
	ifs.read(buf, 54);
	BITMAPFILEHEADER* pHeader = (BITMAPFILEHEADER*)buf;
	BITMAPINFOHEADER* pInfo = (BITMAPINFOHEADER*)(buf + 14);

	if (pInfo->biBitCount != 24)
	{
		std::cout << "必须是24位图像" << std::endl;
		return;
	}

	int lineByte = (pInfo->biWidth * pInfo->biBitCount / 8 + 3) / 4 * 4;  //向上取4的整倍数
	int nSize = lineByte * pInfo->biHeight;
	uint8_t* pSrc = new uint8_t[nSize + 31];
	uint8_t* pDst = new uint8_t[nSize + 31];
	ifs.read((char*)pSrc, nSize);
	ifs.close();

	//开始进行图像翻转处理
	cudaError_t cudaStatus;
	cudaEvent_t time0, time1;
	cudaEventCreate(&time0); cudaEventCreate(&time1);


	uint8_t* pGPUSrc, *pGPUDst;

	//在显卡上分配内存
	cudaStatus = cudaMalloc((void**)&pGPUSrc, nSize * sizeof(uint8_t));
	cudaStatus = cudaMalloc((void**)&pGPUDst, nSize * sizeof(uint8_t));

	//图像数据拷贝到显存
	cudaStatus = cudaMemcpy(pGPUSrc, pSrc, nSize * sizeof(uint8_t), cudaMemcpyHostToDevice);

	//GPU处理
	int ThrPerBlk = 256;
	int BlkPerRow = (pInfo->biWidth + ThrPerBlk - 1) / ThrPerBlk;
	int NumBlks = pInfo->biHeight*BlkPerRow;
	cudaEventRecord(time0, 0);
	Hflip << <NumBlks, ThrPerBlk >> > (pGPUSrc, pGPUDst, pInfo->biWidth);
	cudaEventRecord(time1, 0);
	//告诉Nvidia运行时引擎,对给定的事件执行同步操作,确保时间正确
	cudaEventSynchronize(time0); cudaEventSynchronize(time1);
	//由两个事件计算得到时间
	float ms = 0;
	cudaEventElapsedTime(&ms, time0, time1);
	std::cout << "GPU耗时:" << ms << "ms" << std::endl;

	//等待所有kernal执行完
	cudaStatus = cudaDeviceSynchronize();

	//从显存拷贝到主存
	cudaStatus = cudaMemcpy(pDst, pGPUDst, nSize * sizeof(uint8_t), cudaMemcpyDeviceToHost);

	//释放显存
	cudaFree(pGPUSrc);
	cudaFree(pGPUDst);

	//必须被调用来供调试工具分析
	cudaDeviceReset();

	//将bmp数据写回
	std::ofstream ofs;
	ofs.open("D:\\1-Hflip.bmp", std::ios::out);
	ofs.write(buf, 54); //图像头
	ofs.write((char*)pDst, nSize2);
	ofs.close();

	return 0;
}

执行结果:
请添加图片描述

设备属性获取

	cudaDeviceProp GPUProp;
	cudaGetDeviceProperties(&GPUProp, 0);
	std::cout
		<< "全局内存大小:" << GPUProp.totalGlobalMem/1024/1024/1024.0 << "GB" << std::endl
		<< "gridsize[0]:" << GPUProp.maxGridSize[0] << std::endl	//一个网格中每个维度的block数量
		<< "gridsize[1]:" << GPUProp.maxGridSize[1] << std::endl
		<< "gridsize[2]:" << GPUProp.maxGridSize[2] << std::endl
		<< "sharedMemPerBlock:" << GPUProp.sharedMemPerBlock/1024.0 << "kB" <<std::endl//每block共享内存
		<< "regsPerBlock:" << GPUProp.regsPerBlock << std::endl//每block32为寄存器个数
		<< "ThrePerBlock:" << GPUProp.maxThreadsPerBlock << std::endl//每block最多多少线程
		<< "maxThreadsDim[0]" << GPUProp.maxThreadsDim[0] << std::endl
		<< "maxThreadsDim[1]" << GPUProp.maxThreadsDim[1] << std::endl
		<< "maxThreadsDim[2]" << GPUProp.maxThreadsDim[2] << std::endl
		<< "设备上处理器数量:" << GPUProp.multiProcessorCount << std::endl
		<< "canMapHostMemory:" << GPUProp.canMapHostMemory << std::endl
		<< std::endl;

总结

GPU编程的通用流程:

  1. 初始化, 将必要数据拷贝到GPU设备的显存
  2. CPU调用GPU函数,启动GPU多核同时计算
  3. CPU和GPU异步计算
  4. 将GPU计算结果拷贝回主机,得到计算结果

后续会有:图像颜色空间转换,图像滤波(卷积)等操作在GPU上的实现

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年8月3日
下一篇 2023年8月3日

相关推荐