CUDA学习笔记

1.一些基本概念

CPU及其系统的内存称为主机,GPU及其内存称为设备;函数前的 __global__告诉编译器,函数应该编译为在设备上运行;

kernel<<<M,N>>>,运行时将创建M个线程块(block),如何知道当前核函数运行的是哪个线程块? int tid=blockIdx.x;

以上M个线程块的集合叫线程格(grid),在启动线程块数组时,数组每一维的最大数量不能超过65535

如何操作二维线程格

	int x = blockIdx.x;
	int y = blockIdx.y;
	int offset = x + y*gridDim.x;

最大的线程数量不能超过该设备属性结构中maxThreadsPerBlock域的值(一般是512)

	int tid=threadIdx.x+blockIdx.x*blockDim.x
	if(tid<N)
	...

	add<<<(N+127)/128, 128>>>(...)

GPU上实现任意长度的计算

	int tid=threadIdx.x+blockIdx.x*blockDim.x
	while(tid<N){
		//...
		tid+=blockDim.x+gridDim.x;
	}

使用二维线程块、二维线程

	dim3 blocks(DIM/16, DIM/16);
	dim3 threads(16, 16);
	kernel<<<blocks, threads>>>(...)

__global__ void kernel(...){
	int x=threadIdx.x+blockIdx.x*blockDim.x;
	int y=threadIdx.y+blockIdx.y*blockDim.y;
	int offset=x+y*blockDim.x*gridDim.x;
	//...
}

2.共享内存与同步

__share__ 添加到变量声明中,使这个变量驻留在共享内存中。对于GPU启动的每个线程块,CUDA C编译器都将创建该变量的一个副本,线程块中的每个线程都共享这块内存,但线程却无法看到也不能修改其他线程块的变量副本。

__syncthreads(); 这个函数将确保线程块中的每个线程都执行完__syncthreads()前面的语句后,才会执行下一条语句。

当某些线程需要执行一条指令,而其他线程不需要执行时,这种情况称为线程发散

使用__syncthreads()时一定要注意线程发散问题。CUDA架构确保,除非线程块中的第个线程都执行了__syncthreads(),否则没有任何线程能执行__syncthreads()之后的指令。

3.常量内存

和全局内存的使用只有2处不同:使用__constant__在全局声明变量;当从主机内存复制到GPU上的内存时,使用cudaMemcpyToSymbol()函数,会复制到常量内存,而使用参数为cudaMemcpyHostToDevice的cudaMemcpy()会复制到全局内存。

#define SPHERES 20
__constant__ Sphere s[SPHERES];

//...
cudaMemcpyToSymbol( s, temp_s, sizeof(Sphere) * SPHERES)

4.使用事件来测量性能

	cudaEvent_t     start, stop;
	cudaEventCreate( &start );
	cudaEventCreate( &stop );
	cudaEventRecord( start, 0 );

	// 在GPU上执行一些工作(核函数、设备内存复制)

	cudaEventRecord( stop, 0 );
	cudaEventSynchronize( stop );
	float   elapsedTime;
	cudaEventElapsedTime( &elapsedTime, start, stop );
	printf( "Time to generate:  %3.1f ms\n", elapsedTime );

	cudaEventDestroy( start );
	cudaEventDestroy( stop );

5.纹理内存

一维纹理

	// 1.申明纹理引用
	float* src;
	texture<float>  tex;

	// 2.在设备上分配内存,将内存绑定到之前声明的纹理引用
	cudaMalloc((void**)&src, imageSize);
	cudaBindTexture(NULL, tex, src, imageSize);

	// 3.在核函数中采样纹理
	float c = tex1Dfetch(tex,offset);

	// 4.解绑定并释放内存
	cudaUnbindTexture(tex);
	cudaFree(src);

二维纹理

	// 1.申明纹理引用
	float* src;
	texture<float, 2>  tex;

	// 2.在设备上分配内存,将内存绑定到之前声明的纹理引用
	cudaMalloc((void**)&src, imageSize);
	cudaChannelFormatDesc desc = cudaCreateChannelDesc<float>();
	cudaBindTexture2D(NULL, tex, src, desc, DIM, DIM, sizeof(float)*DIM);

	// 3.在核函数中采样纹理
	float c = tex2D(tex,offset);

	// 4.解绑定并释放内存
	cudaUnbindTexture(tex);
	cudaFree(src);
当使用tex2D()时,不用担心发生溢出问题,当采样坐标小于0时,tex2D()会返回0处的值,当采样坐标大于宽度或高度时,tex2D()将返回位于宽度或高度处的值。

6.图形互操作性

通用计算与渲染模式之间的互操作:能否在一个应用程序中GPU既执行渲染计算,又执行通用计算?如果要渲染的图像依赖通用计算的结果,该如何处理?如果想要在已经渲染的帧上执行某种图像处理或者统计,又该如何实现?这些都是图形互操作性可以解决的问题。

#include <gl/glew.h>
#include <gl/glut.h>
#include "cuda.h"
#include "cuda_gl_interop.h"

#pragma comment(lib, "opengl32.lib")
#pragma comment(lib, "glu32.lib")
#pragma comment(lib, "glew32.lib")


#define DIM 512


// 指向同一个缓冲区的不同句柄,因为OpenGL和CUDA对于这个缓冲区各自有着不同的"命名"
GLuint imgId;
cudaGraphicsResource* resource;

__global__ void kernel(uchar4* ptr)
{
	int x = threadIdx.x + blockIdx.x * blockDim.x;
	int y = threadIdx.y + blockIdx.y * blockDim.y;
	int offset = x + y * blockDim.x * gridDim.x;

	float fx = x/(float)DIM - 0.5f;
	float fy = y/(float)DIM - 0.5f;
	unsigned char green = 128 + 127*sin(abs(fx*100)-abs(fy*100));

	ptr[offset].x = 0;
	ptr[offset].y = green;
	ptr[offset].z = 0;
	ptr[offset].w = 255;
}

void display()
{
	glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, imgId);
	glDrawPixels(DIM, DIM, GL_RGBA, GL_UNSIGNED_BYTE, 0);
	glutSwapBuffers();
}

void key(unsigned char key, int x, int y)
{
	switch(key)
	{
	case 27:
		cudaGraphicsUnregisterResource(resource);
		glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
		glDeleteBuffers(1, &imgId);
		exit(0);
	}
}

int main(int argc, char* argv[])
{
	// 选择执行程序的CUDA设备
	cudaDeviceProp prop;
	int dev;
	memset(&prop, 0, sizeof(cudaDeviceProp));
	prop.major = 1;
	prop.minor = 0;
	cudaChooseDevice(&dev, &prop);

	// 选择执行OpenGL的设备
	cudaGLSetGLDevice(dev);

	// 初始化OpenGL窗口
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowSize(DIM, DIM);
	glutCreateWindow("图形互操作性");
	glewInit();

	// 创建像素缓冲区对象
	glGenBuffers(1, &imgId);
	glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, imgId);
	glBufferData(GL_PIXEL_UNPACK_BUFFER_ARB, DIM*DIM*4, NULL, GL_DYNAMIC_DRAW_ARB);

	// imgId运行时将在CUDA和OpenGL间共享,通过把imgId注册为一个图形资源
	cudaGraphicsGLRegisterBuffer(&resource, imgId, cudaGraphicsMapFlagsNone);

	// 映射该共享资源
	cudaGraphicsMapResources(1, &resource, NULL); 

	// 请求一个指向映射资源的指针
	uchar4* devPtr;
	size_t size;
	cudaGraphicsResourceGetMappedPointer((void**)&devPtr, &size, resource);

	dim3 grids(DIM/16, DIM/16);
	dim3 threads(16, 16);
	kernel<<<grids, threads>>>(devPtr);

	// 取消映射,确保cudaGraphicsUnmapResource()之前的所有CUDA操作完成
	cudaGraphicsUnmapResources(1, &resource, NULL);

	// 设置好GLUT并启动循环
	glutKeyboardFunc(key);
	glutDisplayFunc(display);
	glutMainLoop();

	return 0;
}

7.原子性

GPU支持的各种功能统称为计算功能集。只有1.1或更高版本的计算功能集才支持全局内存上的原子操作,只有1.2或更高版本的计算功能集才支持共享内存上的原子操作。计算功能集的版本为1.3或者更高的显卡才支持双精度浮点数学计算,prop.major, prop.minor。

由于一些操作的执行过程不能分解为更小的部分,因此将满足这种条件限制的操作称为原子操作

计算直方图,atomicAdd()保证在CUDA C中使用原子操作的方式。

__global__ void histo_kernel( unsigned char *buffer,long size,unsigned int *histo ) 
{					
	 int i = threadIdx.x + blockIdx.x * blockDim.x;
	 int stride = blockDim.x * gridDim.x;
	 while(i < size){
		 atomicAdd(&histo[buffer[i]], 1);
		 i += stride;
	 }
}

对100MB的输入数据使用以上进行直方图统计需要1.752秒,与基于CPU的直图计算相比(只需要0.416秒),性能更糟!这是因为核函数中只包含了非常少的计算工作,全局内存上的原子操作导致了性能的降低。

__global__ void histo_kernel( unsigned char *buffer,long size,unsigned int *histo ) 
{
	 __shared__  unsigned int temp[256];
	 temp[threadIdx.x] = 0;
	 __syncthreads();

	 int i = threadIdx.x + blockIdx.x * blockDim.x;
	 int stride = blockDim.x * gridDim.x;
	 while (i < size) {
		 atomicAdd( &temp[buffer[i]], 1 );
		 i += stride;
	 }

	 __syncthreads();
	 atomicAdd( &(histo[threadIdx.x]), temp[threadIdx.x] );
}

首先分配一个共享内存缓冲区并进行初始化,使用__syncthreads()确保每个线程的写入操作在线程继续前进之前完成。下一个步骤与之前的计算非常类似,只是这里使用了共享内存缓冲区temp[]而不是全局内存缓冲区histo[],交调用__syncthreads()确保提交最后的写入操作。最后将每个线程块的临时直方图合并到全局缓冲区中。

通过使用共享内存,程序执行时间为0.057秒。

有时候依赖原子操作会带来性能问题,这些问题只能通过对算法的某些部分进行重构来加以解决。如上直方图示例中,我们使用一种两阶段算法,该算法降低了在全局内存俯冲上竞争程度。通常,这种降低内存竞争程度的策略总能带来不错的效果,因此在使用原子操作时,要记住这种策略。

补充:

cudaMemset(dev_data, 0, size);与标准C函数的memset()是相似的。

当线程块的数量为GPU中处理器数量的2倍时,性能最佳。

cudaDeviceProp  prop;
HANDLE_ERROR( cudaGetDeviceProperties( &prop, 0 ) );
int blocks = prop.multiProcessorCount;
histo_kernel<<<blocks*2,256>>>(...);








  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值