CUDA 学习,菜鸟学习入门记录

1.写在前面

因为自己需要实现cuda编程做中心线的提取,实现灰度重心法的实验。所以学习cuda,但是本人从未接触过cuda,所以在这里记录下来学习cuda的过程。

2024.11.26,先学习一篇cuda编程入门极简教程,在csdn搜索一篇教程,先尝试跟着走一遍。教程如下:CUDA编程入门极简教程_cuda编程举例-优快云博客

学习教程画重点:

 2.初识cuda       

GPU并不是一个独立运行的计算平台,而需要与CPU协同工作,可以看成是CPU的协处理器,因此当我们在说GPU并行计算时,其实是指的基于CPU+GPU的异构计算架构。CPU所在位置称为为主机端(host),而GPU所在位置称为设备端(device)。

基于CPU+GPU的异构计算平台可以优势互补,CPU负责处理逻辑复杂的串行程序,而GPU重点处理数据密集型的并行计算程序。

典型的CUDA程序的执行流程如下:

  1. 分配host内存,并进行数据初始化;
  2. 分配device内存,并从host将数据拷贝到device上;
  3. 调用CUDA的核函数在device上完成指定的运算;
  4. 将device上的运算结果拷贝到host上;
  5. 释放device和host上分配的内存。

上面流程中最重要的一个过程是调用CUDA的核函数来执行并行计算,kernel是CUDA中一个重要的概念,kernel是在device上线程中并行执行的函数,核函数用__global__符号声明,在调用时需要用<<<grid, block>>>来指定kernel要执行的线程数量,在CUDA中,每一个线程都要执行核函数,并且每个线程会分配一个唯一的线程号thread ID,这个ID值可以通过核函数的内置变量threadIdx来获得。

__global__:在device上执行,从host中调用(一些特定的GPU也可以从device上调用),返回类型必须是void,不支持可变参数参数,不能成为类成员函数。注意用__global__定义的kernel是异步的,这意味着host不会等待kernel执行完就执行下一步。
__device__:在device上执行,单仅可以从device中调用,不可以和__global__同时用。
__host__:在host上执行,仅可以从host上调用,一般省略不写,不可以和__global__同时用,但可和__device__,此时函数会在device和host都编译。

想要根据博主的代码进行测试,但是发现自己还不知道 在哪里进行cuda编程。于是在vs中新建了一个cuda的源文件,文件后缀名字为*.cu

然后需要配置cuda的环境,因为cuda在安装时就已经自动的加入了系统环境变量,所以这里只需要添加属性配置。

配置如下:

1.包含目录:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\include

库目录:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\lib\x64
                C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\lib

2.附加依赖项:

cublas.lib
cublasLt.lib
cuda.lib
cudadevrt.lib
cudart.lib
cudart_static.lib
cufft.lib
cufftw.lib
curand.lib
cusolver.lib
cusolverMg.lib
cusparse.lib
nppc.lib
nppial.lib
nppicc.lib
nppidei.lib
nppif.lib
nppig.lib
nppim.lib
nppist.lib
nppisu.lib
nppitc.lib
npps.lib
nvblas.lib
nvjpeg.lib
nvml.lib
nvrtc.lib
OpenCL.lib

然后使用测试代码:代码出处:VS2019+CUDA编程(流程)_vs如何运行cu文件-优快云博客

#include "cuda_runtime.h"

#include "device_launch_parameters.h"

#include <stdio.h>
#include <stdlib.h>


void main() {

	int deviceCount;

	cudaGetDeviceCount(&deviceCount);



	int dev;

	for (dev = 0; dev < deviceCount; dev++)

	{

		int driver_version(0), runtime_version(0);

		cudaDeviceProp deviceProp;

		cudaGetDeviceProperties(&deviceProp, dev);

		if (dev == 0)

			if (deviceProp.minor = 9999 && deviceProp.major == 9999)

				printf("\n");

		printf("\nDevice%d:\"%s\"\n", dev, deviceProp.name);

		cudaDriverGetVersion(&driver_version);

		printf("CUDA驱动版本:                                   %d.%d\n", driver_version / 1000, (driver_version % 1000) / 10);

		cudaRuntimeGetVersion(&runtime_version);

		printf("CUDA运行时版本:                                 %d.%d\n", runtime_version / 1000, (runtime_version % 1000) / 10);

		printf("设备计算能力:                                   %d.%d\n", deviceProp.major, deviceProp.minor);

		printf("Total amount of Global Memory:                  %u bytes\n", deviceProp.totalGlobalMem);

		printf("Number of SMs:                                  %d\n", deviceProp.multiProcessorCount);

		printf("Total amount of Constant Memory:                %u bytes\n", deviceProp.totalConstMem);

		printf("Total amount of Shared Memory per block:        %u bytes\n", deviceProp.sharedMemPerBlock);

		printf("Total number of registers available per block:  %d\n", deviceProp.regsPerBlock);

		printf("Warp size:                                      %d\n", deviceProp.warpSize);

		printf("Maximum number of threads per SM:               %d\n", deviceProp.maxThreadsPerMultiProcessor);

		printf("Maximum number of threads per block:            %d\n", deviceProp.maxThreadsPerBlock);

		printf("Maximum size of each dimension of a block:      %d x %d x %d\n", deviceProp.maxThreadsDim[0],

			deviceProp.maxThreadsDim[1],

			deviceProp.maxThreadsDim[2]);

		printf("Maximum size of each dimension of a grid:       %d x %d x %d\n", deviceProp.maxGridSize[0], deviceProp.maxGridSize[1], deviceProp.maxGridSize[2]);

		printf("Maximum memory pitch:                           %u bytes\n", deviceProp.memPitch);

		printf("Texture alignmemt:                              %u bytes\n", deviceProp.texturePitchAlignment);

		printf("Clock rate:                                     %.2f GHz\n", deviceProp.clockRate * 1e-6f);

		printf("Memory Clock rate:                              %.0f MHz\n", deviceProp.memoryClockRate * 1e-3f);

		printf("Memory Bus Width:                               %d-bit\n", deviceProp.memoryBusWidth);

	}
	system("pause");
	//return 0;

}

最终我的运行结果:

到此步骤,顺利输出显卡的参数信息,则证明环境没有问题,并且可以顺利的视线编译。

下一步继续学习如何使用cuda开辟空间和编程。

        2024.11.29继续学习

已经学习了如何在vs中实现cuda文件如何实现函数进行编程,下面进行一个简单的实战,利用cuda实现两个向量的加法。

在实现之前,cuda最重要的内存管理,首先是在device上进行内存分配,使用的函数是cudaMalloc

cudaError_t cudaMalloc(void** devPtr, size_t size);

这个函数和C语言中的malloc类似,但是在device上申请一定字节大小的显存,其中devPtr是指向所分配内存的指针。同时要释放分配的内存使用cudaFree函数,这和C语言中的free函数对应。另外一个重要的函数是负责host和device之间数据通信的cudaMemcpy函数:

cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)

其中src指向数据源,而dst是目标区域,count是复制的字节数,其中kind控制复制的方向:cudaMemcpyHostToHost, cudaMemcpyHostToDevice, cudaMemcpyDeviceToHost及cudaMemcpyDeviceToDevice,如cudaMemcpyHostToDevice将host上数据拷贝到device上。

其中src指向数据源,而dst是目标区域,count是复制的字节数,其中kind控制复制的方向:cudaMemcpyHostToHost, cudaMemcpyHostToDevice, cudaMemcpyDeviceToHost及cudaMemcpyDeviceToDevice,如cudaMemcpyHostToDevice将host上数据拷贝到device上。

加法实例

现在我们来实现一个向量加法的实例,这里grid和block都设计为1-dim,首先定义kernel如下:

// 两个向量加法kernel,grid和block均为一维
__global__ void add(float* x, float * y, float* z, int n)
{
    // 获取全局索引
    int index = threadIdx.x + blockIdx.x * blockDim.x;
    // 步长
    int stride = blockDim.x * gridDim.x;
    for (int i = index; i < n; i += stride)
    {
        z[i] = x[i] + y[i];
    }
}

其中stride是整个grid的线程数,有时候向量的元素数很多,这时候可以将在每个线程实现多个元素(元素总数/线程总数)的加法,相当于使用了多个grid来处理,这是一种grid-stride loop方式,不过下面的例子一个线程只处理一个元素,所以kernel里面的循环是不执行的。下面我们具体实现向量加法:

int main(){
int N = 1 << 20;//N被设置为2的20次方,等于1048576
	int nBytes = N * sizeof(float);//计算所需内存字节数,float类型占用4字节。
	//申请主机内存(Host Memory)
	float *x, *y, *z;//申请3个浮点数数组的指针。
	x = (float*)malloc(nBytes);//为x数组分配内存。
	y= (float*)malloc(nBytes);//为y数组分配内存。
	z = (float*)malloc(nBytes);//为z数组分配内存。
	//初始化数据
	for (int i=0;i<N;++i)
	{
		x[i] = 10.0;//将数组x的每个袁术初始化为10.0
		y[i] = 20.0;//将数组x的每个袁术初始化为20.0
	}
	//申请设备内存(Device Memory)
	float *d_x, *d_y, *d_z;//申明设备端浮点数数组的指针
	cudaMalloc((void**)&d_x, nBytes);//在设备上为d_x分配内存
	cudaMalloc((void**)&d_y, nBytes);//在设备上为d_y分配内存
	cudaMalloc((void**)&d_z, nBytes);//在设备上为d_z分配内存
	//将主机数据拷贝到设备:
	cudaMemcpy((void*)d_x, (void*)x, nBytes, cudaMemcpyHostToDevice);//将x的数据从主机复制到设备。
	cudaMemcpy((void*)d_y,(void*)y, nBytes,cudaMemcpyHostToDevice);//将y的数据从主机复制到设备。
	//定义内核执行配置:
	dim3 blockSize(256);//每个块的线程数量设置为256。
	dim3 gridSize((N+ blockSize.x-1)/ blockSize.x);//计算网格大小,根据线程块的大小确定需要多少个块
	//执行内核
	add <<<gridSize, blockSize >>> (d_x, d_y, d_z,N);
	//将设备计算结果拷贝回主机:
	cudaMemcpy((void*)z,(void*)d_z, nBytes,cudaMemcpyDeviceToHost);//将d_z的数据拷贝回z。
	//执行检查结果:
	float maxError = 0.0;
	for (int i = 0; i < nBytes; i++)
	{
		maxError = fmax(maxError,fabs(z[i]-30.0));//计算结果中最大的误差,期望能够每个结果都为30.0
		std::cout << "最大误差:" << maxError << std::endl;
	}
	//释放设备内存
	cudaFree(d_x);//释放d_x的内存
	cudaFree(d_y);//释放d_y的内存
	cudaFree(d_z);//释放d_z的内存
	//释放主机内存
	free(x);//释放x的内存
	free(y);//释放y的内存
	free(z);//释放z的内存
return 0;
}

这里我们的向量大小为1<<20,而block大小为256,那么grid大小是4096,kernel的线程层级结构如下图所示:

在上面的实现中,我们需要单独在host和device上进行内存分配,并且要进行数据拷贝,这是很容易出错的。好在CUDA 6.0引入统一内存(Unified Memory)来避免这种麻烦,简单来说就是统一内存使用一个托管内存来共同管理host和device中的内存,并且自动在host和device中进行数据传输。CUDA中使用cudaMallocManaged函数分配托管内存:

cudaError_t cudaMallocManaged(void **devPtr, size_t size, unsigned int flag=0);

利用统一内存,可以将上面的程序简化如下:

int main()

{
	//使用cudaMallocManaged函数分配托管内存
	int N1 = 1 << 20;
	int n1Bytes = N1 * sizeof(float);
	//申请托管内存
	float *x1, *y1, *z1;
	cudaMallocManaged((void**)&x1, n1Bytes);
	cudaMallocManaged((void**)&y1, n1Bytes);
	cudaMallocManaged((void**)&z1, n1Bytes);
	//初始化数据
	for (int i = 0; i < N1; i++)
	{
		x1[i] = 10.0;
		y1[i] = 20.0;
	}
	//定义kernel的执行配置

	dim3 blockSize1(256);
	dim3 gridSize1((N1+blockSize.x-1)/blockSize.x);
	//执行kernel
	add <<<gridSize1, blockSize1>> > (x1,y1,z1,N1);
	//同步device保证结果能正确访问。
	cudaDeviceSynchronize();
	//检查执行结果
	float maxError1 = 0.0;
	for (int i = 0; i < N1; i++)
	{
		maxError1 = fmax(maxError1,fabs(z1[i]-30.0));
		std::cout << "最大误差:" << maxError1 << std::endl;
	}
	//释放内存
	cudaFree(x1);
	cudaFree(y1);
	cudaFree(z1);
}

cudaDeviceSynchronize()` 是CUDA编程中用于同步的一个函数,它的作用是确保所有之前发出的、与当前设备(GPU)关联的主机(CPU)线程中的CUDA API调用都已完成执行。在CUDA编程模型中,主机和设备是异步执行的,这意味着当主机代码启动一个核函数后,它不会等待该核函数完成就继续执行后续的主机代码。同样,当主机代码执行一个数据从主机传输到设备的操作时,它也不会等待数据传输完成就继续执行。

由于这种异步性,有时我们需要确保在继续执行主机代码之前,之前的核函数或数据传输操作已经完成。这就是`cudaDeviceSynchronize()`的用处。调用`cudaDeviceSynchronize()`会阻塞调用它的主机线程,直到之前启动的所有核函数和/或数据传输操作都完成。

此外,`cudaDeviceSynchronize()`还可以用于错误检查。如果在之前的核函数执行或数据传输中发生了错误,那么在调用`cudaDeviceSynchronize()`时,这个错误会被捕获并返回给主机。

需要注意的是,虽然`cudaDeviceSynchronize()`很有用,但频繁使用它可能会导致性能下降,因为它会阻塞主机线程。因此,在设计CUDA程序时,应仔细考虑何时需要同步,并尽量减少不必要的同步操作。CUDA还提供了其他同步原语,如流(streams)和事件(events),它们提供了更细粒度的同步控制,但使用起来也更复杂。

矩阵乘法实例

最后我们再实现一个稍微复杂一些的例子,就是2个矩阵的乘法,设输入矩阵为A和B,要得到C=A*B。实现思路是每个线程计算C的一个元素值Cij,对于矩阵运算,应该选用grid和bloc为2-D的。参照CUDA编程入门极简教程_cuda编程举例-优快云博客

2024.11.30继续学习

cuda实现图像二值化

自己不会写代码,进行了AI。搜出来的代码有点问题,我自己改好了。最后实现效果如下:

二值化之前:二值化之后:

还是比较艰辛,能够顺利的视线一个功能,咱也是入门了,给自己点个赞。下面附上二值化的代码,并且每个代码是什么意思都添加了注释,注意这些代码都是在***.cu:文件中编写的

我使用的头函数包括:

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include<opencv2/opencv.hpp>
#include <stdio.h>
#include <stdlib.h>
#include<iostream>

二值化的核函数代码:

//图像二值化核函数
__global__ void binarizeKernel(unsigned char* inputImage, unsigned char* outputImage, int width, int height, unsigned char threshold) {
	int x = blockIdx.x * blockDim.x + threadIdx.x; // 计算当前线程的x坐标  
	int y = blockIdx.y * blockDim.y + threadIdx.y; // 计算当前线程的y坐标  
												   // 确保当前线程在图像范围内  
	if (x < width && y < height) {
		int idx = y * width + x; // 将二维坐标转换为一维索引  
		outputImage[idx] = (inputImage[idx] < threshold) ? 0 : 255; // 应用二值化逻辑  
	}
}

二值化函数:

//使用cuda实现图像二值化
void binary(cv::Mat& image,cv::Mat& binary, unsigned char threshold)
{
	int width = image.cols;
	int height = image.rows;
	//将图像转化为一维数组
	unsigned char *h_image = image.data;
	unsigned char *h_binary = new unsigned char[width*height];
	unsigned char *d_image, *d_binary;
	int size = width*height * sizeof(unsigned char);
	//申请CUDA设备内存
	cudaMalloc((void**)&d_image, size);
	cudaMalloc((void**)&d_binary, size);
	//将输入图像从主机复制到设备
	cudaMemcpy(d_image, h_image, size, cudaMemcpyHostToDevice);
	//定义内核执行配置
	dim3 blockSize(32, 32);
	dim3 gridSize((width+blockSize.x-1)/blockSize.x,(height+blockSize.y-1)/blockSize.y);
	//执行二值化内核
	binarizeKernel <<<gridSize, blockSize >>> (d_image, d_binary, width, height, threshold);
	//同步设备
	cudaDeviceSynchronize();
	//将结果从设备复制回主机
	cudaMemcpy(h_binary, d_binary,size,cudaMemcpyDeviceToHost);

	//创建图像并保存
	cv::Mat outputimage(height,width, CV_8UC1, h_binary);
	 outputimage.copyTo(binary);
	 //释放CUDA内存
	 cudaFree(d_image);
	 cudaFree(d_binary);
	 delete[] h_binary;
	 std::cout << "图像二值化完成,结果已返回。" << std::endl;
}

执行主函数:

int main()
{
	cv::Mat image2 = cv::imread("leftlaser1_27.jpg", cv::IMREAD_GRAYSCALE);
	cv::Mat binaryimage(image2.rows, image2.cols, CV_8UC1, cv::Scalar(0));
	unsigned char a = 180;
 	binary(image2, binaryimage, a);
	
	return 0;
}

最后在使用image watch看看局部放大的二值化效果,即非0就255,即黑白。

下一步计划学习使用cuda实现灰度重心法提取激光中心线。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值