首先说明下:
1.我用的是vs2012+cuda7.5+GeForce GTX 760,人蠢,小心。
2.本书的源代码:https://bitbucket.org/mrfright/cuda_by_example/src
3.一般会加上头文件 cuda_runtime.h,贴出来的时候就省了
一、passing parameters
——-cudaMalloc()
& cudaFree()
——
host和device的内存是分开的,在host代码中执行cudaMalloc()
函数在device中为数据分配内存(释放device memory的配套函数为cudaFree()
,cudaMalloc()
返回device上的一个地址,对这个device pointer的使用有以下的限制:
对于host_ptr则是相应的不能在device端进行读写。
——cudaMemcpy()——
device和host内存上数据的交换需通过cudaMemcpy(destnation, source, stat);
其中stat用于表明destination和source分别是哪里的,比如cudaMemcpyDeviceToDevice。
(cuda开头的函数头包含在runtime头文件中)
二、Querying devices
了解运行device code的设备的property。
——cudaGetDeviceCount()
——
HANDLE_ERROR()
是书中自带的book.h中的函数,不加影响也不是很大。cudaGetDeviceCount()
顾名思义就是得到设备的数量,代码如下:
int count;
HANDLE_ERROR( cudaGetDeviceCount( &count ) );
得到设备数量后就可以循环来得到设备的相关信息。
——cudaGetDeviceProperties()
——
#include "../common/book.h"
int main( void ) {
cudaDeviceProp prop;//cudaDevicePro是一个数据结构,其中的内容见下文。
int count;
HANDLE_ERROR( cudaGetDeviceCount( &count ) );
for (int i=0; i< count; i++) {
HANDLE_ERROR( cudaGetDeviceProperties( &prop, i ) );
//Do something with our device's properties
}
}
//cudaDeviceProp
struct cudaDeviceProp {
char name[256]; //name of device,eg:"GeForce GTX 760"
size_t totalGlobalMem; //sizeof global Memory
size_t sharedMemPerBlock; //en...
int regsPerBlock; //en...
int warpSize; //en...
size_t memPitch; //The maximum pitch allowed for memory copies in bytes
int maxThreadsPerBlock; //en...
int maxThreadsDim[3]; //en...
int maxGridSize[3]; //The number of blocks allowed along each dimension of a grid
size_t totalConstMem; //sizeof constant memory
int major; //major revision of the device's compute capability
int minor; //refer to major
...//consult NVIDIA CUDA Programming Guide for more information
};
三、using Device Properties
如果想要运行得更快,则选择processor多的GPU,如果需要与CPU做较多的交互,则选用继承显卡。这些都要通过查询cudaGetDeviceProperties()
。其中的minor和major代表的compute capability可以通过查表得到各个版本的性能,不同的版本号支持不同的性能。
我们可以自己编写程序循环查询每个设备以选择满足我们要求的设备,也可以利用cudaChooseDevice()
自动查找满足条件的设备,它会返回device ID,将deviceID传递给cudaSetDevice()
就可以选定我们想要的设备了。不过反正我只有一个device(摊手)。还是代码最好说话了:
int main( void ) {
cudaDeviceProp prop;
int dev;
HANDLE_ERROR( cudaGetDevice( &dev ) ); //get the id of current device
printf( "ID of current CUDA device: %d\n", dev );
memset( &prop, 0, sizeof( cudaDeviceProp ) ); //初始化
prop.major = 1;
prop.minor = 3; //设置条件
HANDLE_ERROR( cudaChooseDevice( &dev, &prop ) ); //自动查找
printf( "ID of CUDA device closest to revision 1.3: %d\n", dev );
HANDLE_ERROR( cudaSetDevice( dev ) );
}
至此,chapter3过掉了,还没正经用过kernel……= =!
一、矢量求和
这个,过程见下:
这个,如果用serial programming的话,相当容易,就不写了, 加一个循环就好了。很显然,每个c[i] = a[i] + b[i]都是互不干扰的,并行的,由此可以给出GPU版本的代码,首先,我们利用chapter3的知识来给出主函数:
#include <stdio.h>
#include "cuda_runtime.h"
//懒得用book.h了……
#define N 10
int main(void){
int a[N], b[N], c[N];
int *dev_a, *dev_b, *dev_c;
//allocate memory on GPU
cudaMalloc((void**)&dev_a, N * sizeof(int));
cudaMalloc((void**)&dev_b, N * sizeof(int));
cudaMalloc((void**)&dev_c, N * sizeof(int));
//fill the array a & b on CPU
for(int i = 0; i < N; ++i){
a[i] = -i;
b[i] = i * i;
}
//copy the arrays a & b to GPU
cudaMemcpy(dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(dev_c, c, N * sizeof(int), cudaMemcpyHostToDevice);
add<<<N,1>>>(dev_a, dev_b, dev_c);
//copy array c to CPU
cudaMemcpy(c, dev_c, N * sizeof(int), cudaMemcpyDeviceToHost);
//display
for(int i = 0; i < N; ++i) printf("%d + %d = %d\n", a[i], b[i], c[i]);
cudaFree(dev_a);
cudaFree(dev_b);
cudaFree(dev_c);
}
是不是觉得很熟悉!上一章的内容都用到了!啥内存分配辣,啥拷贝辣。这本书章节的安排还是很科学的…
上面的代码中其实给a,b赋值在GPU中进行更快,但是我们专注于分析其中求和过程的并行性,因为在实际的应用中,a,b的值一般是程序其它段产生的值,而不是GPU产生。
加下来来看add()
函数:
__global__ void
add(int *a, int *b, int *c){
int tid = blockIdx.x;
if(tid < N) c[tid] = a[tid] + b[tid];
}
_ global _声明此函数是设备上执行的函数,它的返回类型只能是void。
来看add<<<N, 1>>(dev_a, dev_b, dev_c);
尖括号中第一个parameter表示一个grid中有几个block,第二个parameter表示一个block中有几个线程,这个,具体的我会再写一篇比较基础的文章来说明,blockIdx是cuda的内置类型,用来给block标号的……反正每个block都执行相同的device code:
二、draw slices of the Julia Set
Julia Set是某种函数作用在复数上产生的边界,唔,好难形容,下面引自百度百科:
朱利亚集合是一个在复平面上形成分形的点的集合。
定义
朱利亚集合可以由下式进行反复迭代得到: fc(z) = z^2 + c
对于固定的复数c,取某一z值(如z = z0),可以得到序列z0, fc(z0),fc(fc(z0))…
这一序列可能反散于无穷大或始终处于某一范围之内并收敛于某一值。我们将使其不扩散的z值的集合称为朱利亚集合,这样的z在复数平面上形成下面的图形:
也就是说,对复平面上一个点做Zn+1 = Zn ^ 2 + C得到的一系列的点,若这些点不发散,那么这个点就是集合中的,否则就不是。
》》》CPU Julia Set
这个程序比较复杂,所以split into pieces辣。
int main(){
CPUBitmap bitmap(DIM, DIM); //create the appropriate size bitmap image
unsigned char *ptr = bitmap.get_ptr(); //
kernel(ptr); //处理每个点
Bitmap.display_and_exit(); //显示处理
}
void kernel(unsigned char * ptr){
for(int y = 0; y < DIM; y++){
for(int x = 0; x < DIM; x++){
int offset = x + y * DIM;
int juliaValue = julia(x,y); //julia()返回bool,是集合中的则返回1,否则返回0
ptr[offset*4 + 0] = 255 * juliaValue; //设置颜色
ptr[offset*4 + 1] = 0;
ptr[offset*4 + 2] = 0;
ptr[offset*4 + 3] = 255;
}
}
}
int julia(int x, int y){
//将点坐标换成复数平面上的坐标,复数平面的远点在图的中心(DIM/2, DIM/2)
//并且使其范围在[-1.0, 1.0]
const float scale = 1.5; //这个参数用来调节图形的大小
float jx = scale * (float)(DIM/2 - x)/(DIM/2);
float jy = scale * (float)(DIM/2 - y)/(DIM/2);
cuComplex c(-0.8, 0.156); //this is arbitrary,but it happens to yield an interesting
cuComplex a(jx, jy);
//计算两百次,每次计算后判断有没有超过阈值(这里是1000),超过就发散,返回0
int i = 0; for(i = 0; i < 200; i++){
a = a * a + c;
if(a.magnitude2() > 1000)
return 0;
}
return 1;
}
剩下是cuComplex类的定义:
struct cuComplex {
float r;
float i;
cuComplex( float a, float b ) : r(a), i(b) {}
float magnitude2( void ) { return r * r + i * i; }
cuComplex operator*(const cuComplex& a) {
return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
}
cuComplex operator+(const cuComplex& a) {
return cuComplex(r+a.r, i+a.i);
}
};
……搞了好久才跑出来,基本功不行QAQ,首先是路径什么的不会搞,然后运行的时候出现“丢失glut32.dll”,不过终于跑出来了,有点开心。嗯,文件中的路径要改一下,感觉路径什么的有点概念了,把lib文件夹中的文件放到vc的lib中,头文件复制到当前的工程中,并添加当前项。真漂亮!做出来才发现做出来之前的自己多傻逼哈哈哈。
》》》GPU Julia Set
先贴主函数,其实和CPU Julia Set差不多,只是多了host和device之间拷贝的步骤,是不是好熟悉…
int main( void ) {
CPUBitmap bitmap( DIM, DIM );
unsigned char *dev_bitmap;
HANDLE_ERROR(cudaMalloc((void**)&dev_bitmap, bitmap.image_size()));
dim3 grid(DIM, DIM); //dim3是cuda内置类型,包含三个int型,x,y,z
kernel<<<grid, 1>>>(dev_bitmap);
HANDLE_ERROR(cudaMemcpy(bitmap.get_ptr(), dev_bitmap, bitmap.image_size(), cudaMemcpyDeviceToHost));
bitmap.display_and_exit();
cudaFree(dev_bitmap);
}
这个问题是关于复平面上点的计算,每个点的计算都是独立的,所以我们可以并行之,以及这是个二维的平面,所以很自然的,我们把把平面分成几块,分配给各个block,这里采用的是每个block一个线程,采用DIM*DIM的二维grid。每个线程处理一个点。
然后来看看我们新的kernel函数:
__global__ void kernel( unsigned char *ptr ){
int x = blockIdx.x;
int y = blockIdx.y;
int offset = x + y * DIM;
//循环不见啦!因为都并行了嘛。
int juliaValue = julia( x, y );
ptr[offset*4 + 0] = 255 * juliaValue;
ptr[offset*4 + 1] = 0;
ptr[offset*4 + 2] = 0;
ptr[offset*4 + 3] = 255;
}
然后是判断是否是Julia Set中的点的函数:
//这个函数是kernel函数调用的,在device上执行,所以用__device__声明
__device__ int julia( int x, int y ) {
const float scale = 1.5;
float jx = scale * (float)(DIM/2 - x)/(DIM/2);
float jy = scale * (float)(DIM/2 - y)/(DIM/2);
cuComplex c(-0.8, 0.156);
cuComplex a(jx, jy);
int i = 0;
for (i=0; i<200; i++) {
a = a * a + c;
if (a.magnitude2() > 1000)
return 0;
}
return 1;
}
然后是cuComplex的定义:
struct cuComplex {
float r;
float i;
__device__ cuComplex( float a, float b ) : r(a), i(b) {} //这里一定要记得加__device__!!书里没有加!!
__device__ float magnitude2( void ) { return r * r + i * i; }
__device__ cuComplex operator*(const cuComplex& a) {
return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
}
__device__ cuComplex operator+(const cuComplex& a) {
return cuComplex(r+a.r, i+a.i);
}
};
和CPU Julia Set不同的是,成员函数前面都加上了_device _声明,因为这些函数都是在GPU上执行的。
这确实是一个fun example,尽管目前我的内心是崩溃的,希望以后可以自己写出这些头文件,嘿嘿。不过暂时专注于CUDA吧,做好眼前事。