学习一个东西总要记下来,最近联系了实验室准备做有关等体的计算,浩浩的物理系非资深码农生涯也将进入正轨。万事开头难,当然从构架走起,开始学习CUDA,第一步使用CUDA重构以前做过的建议科学运算算例,诸如有限差分解热流啦,快速傅里叶变换啦,多体运动模拟啦,哈密顿原理模拟啊,以及完成计算物理作业。然后在CFD的实践中全部采用CUDA构架。
CUDA的一些扯淡
新入手的电脑已经达到了睿频4 cores 3.5Ghz的能力,作为八线程出现已经相当的强大,以前也尝试写过一些多线程的代码,代表是
使用Java做刚体力学模拟,然后使用遗传算法用刚体力学模拟结果作为控制器的评价值来演化PIDs的控制器,说白了也就是是一个16线程的大玩具。没有什么实际效果。
不过毕竟是并行的玩意,相比下左图所示的线程池竟然要用效率低下的人脑抢占式c(brain)pu时间分片来完成,而且在线程切换时候效率极其低下,cpu在线程切换时候面临着效率极低的窘境,而且常常因为过热,过冷,能量不足,能量过剩而罢工,甚至会因为视觉系统受到特定人物的刺激而开始大量的宕机(发呆)。不得不喟叹人生之艰难,恩,第一部分因为和颜色相关的理由涉及到个人隐私,作为本期期间最大的支线任务之一就先隐去了。
扯了这么多,几百个线程跑代码确实是一件很爽的事情,是想如果我有分身,一个去做作业一个去复习考试一个去学CFD一个去学CUDA,然后本体去恩(zhui)你(mou)懂(gu)得(niang),那么人生应该如何的美好。不过事情总得一件件来我也没有分身。现在就从CUDA的hello,world学起,讲起。
学习资源:
http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
我这边并不要做一个翻译工作,而是在学习的过程中讲出来,讲出来自己更懂,也能分享知识。
Hello,World以及线程模型
先看官方给出的一个样例:
- // Kernel definition
- __global__ void VecAdd(float* A, float* B, float* C)
- {
- int i = threadIdx.x;
- C[i] = A[i] + B[i];
- }
- int main()
- {
- …
- // Kernel invocation with N threads
- VecAdd<<<1, N>>>(A, B, C);
- …
- }
十分简单,这是一个计算长矢量相加的代码。十分有效的露出了模型的真面目:
根据目前的观察CUDA基于一种非常接近科学运算的模型:块-线程模型,
每个块有若干线程(1024),可以有很多很多块,都可以使用一个三维向量来定位。
而在本例中,长矢量使用threadIdx.x来定位,这也就是cuda模型的漂亮之处,只需要一个三维向量就可以确定每个模块的位置,这个理念虽然和普通意义上的编程相去甚远,但是对于科学运算(大量的矩阵运算)是非常漂亮的,而且在大多数时候矩阵止于二维三维即可,因为用cuda做模拟的化空间也不过三维。
至于具体的线程运行顺序,那是NVIDIA应该考虑的事情,不过根据guide中的介绍,其实和c的多维数组储存没什么区别。
恩,现在我们有了一个模型和一个写好的代码,如何编译它并且运行呢,我们以如下环境为例
Macbook pro retina
System : OS X 10.9
CPU: i7-4850HQ
Memory 16 GB 1600 MHz DDR3
Graphics NVIDIA GeForce GT 750M 2048 MB
编译环境:
gcc (MacPorts gcc49 4.9-20131027_0) 4.9.0 20131027 (experimental),GNU Make 3.81
开发工具:vim
先自己去装toolkit。参照
http://docs.nvidia.com/cuda/cuda-getting-started-guide-for-mac-os-x/index.html
配置~/.bash_profile,加入
- export PATH=/Developer/NVIDIA/CUDA–5.5/bin:$PATH
- export DYLD_LIBRARY_PATH=/Developer/NVIDIA/CUDA–5.5/lib:$DYLD_LIBRARY_PATH
重新打开终端或者重启电脑,然后
- ~$ nvcc —version
- nvcc: NVIDIA (R) Cuda compiler driver
- Copyright (c) 2005–2013 NVIDIA Corporation
- Built on Thu_Sep__5_10:17:14_PDT_2013
Cuda compilation tools, release 5.5, V5.5.0
恩,安装完成了,于是我们来hello个world
把代码写入(vim 有cuda的高亮,赞一个)
- //test.cu
- #include“stdio.h”
- #include<cuda_runtime.h>
- #define N 100
- // Kernel definition
- __global__ void VecAdd(float* A, float* B, float* C)
- {
- int i = threadIdx.x;
- C[i] = A[i] + B[i];
- }
- int main()
- {
- // Kernel invocation with N threads
- printf(“Hello,World\n”);
- float *A=new float[100],*B=new float[100],*C=new float[100];
- for(int i=0;i<N;i++)
- {
- A[i]=i;
- B[i]=i*i;
- }
- VecAdd<<<1, N>>>(A, B, C);
- for(int i=0;i<N;i++)
- {
- printf(“%f “,C[i]);
- }
- printf(“\n”);
- }
以及makefile
- test:test.o
- nvcc –o test test.o
- test.o:test.cu
- nvcc –I /Developer/NVIDIA/CUDA–5.5/include –c test.cu
- clean:
- rm *.o test
make,潇洒通过
运行,
- CUDA$ ./test
- Hello,World
- 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 …………
咦。。完全没有运行。。。似乎是内存管理的问题
注意这样一个问题:刚才的程序完全是把电脑里面的主ram撇给gpu跑,这不是扯淡嘛!
其实cuda的gpu内存分为几种,全局内存,共享内存,线程独占内存,但是无论如何。。。host设备的内存当然是不能跑的,
GPU线性存储器使用 cudaMalloc()分配,通过 cudaFree()释放,使用 cudaMemcpy()在设备和主机间传输
–摘自GUIDE book,风辰翻译
所以,无论如何,先把数据跑到GPU里面吧!
于是修改代码如下
- __global__ void VecAdd(float* A, float* B, float* C)
- {
- int i = threadIdx.x;
- for(int j=0;j<1000;j++)
- C[i] = (A[i] * B[i]);
- }
- void cpu_VecAdd(int i,float* A, float* B, float* C)
- {
- for(int j=0;j<1000;j++)
- C[i] = (A[i] * B[i]);
- }
- int main()
- {
- // Kernel invocation with N threads
- printf(“Hello,World\n”);
- float *A=new float[N],*B=new float[N],*C=new float[N];
- for(int i=0;i<N;i++)
- {
- A[i]=i;
- B[i]=2*i;
- }
- size_t size = N * sizeof(float);
- float* d_A;
- cudaMalloc(>d_A, size);
- float* d_B;
- cudaMalloc(>d_B, size);
- float* d_C;
- cudaMalloc(>d_C, size);
- float *e=new float[N];
- long st=getCurrentTime();
- cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice);
- cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
- VecAdd<<<1, N>>>(d_A, d_B, d_C);
- long ed=getCurrentTime();
- printf(“gpu running time:%ld\n”,ed–st);
- cudaMemcpy(C, d_C, size, cudaMemcpyDeviceToHost);
- st=getCurrentTime();
- for(int i=0;i<N;i++)
- cpu_VecAdd(i,A,B,e);
- ed=getCurrentTime();
- printf(“cpu running time:%ld\n”,ed–st);
- cudaFree(d_A);
- cudaFree(d_B);
- cudaFree(d_C);
- for(int i=0;i<N;i++)
- {
- //printf(“%f “,C[i]);
- }
- printf(“\n”);
- }
于是
- make
- ./test
- gpu running time:102
- cpu running time:2263
- 0.000000 2.000000 8.000000
哇!!GPU好快啊
于是把第十行的j范围增加十倍
- CUDA$ ./test
- Hello,World
- gpu running time:59
- cpu running time:2121
- 0.000000 2.000000 8.000000 .
.
是不是毫不科学?
其实是因为,对于内核启动这件事情,实际上是异步的,即我们启动内核以后控制权就交给了host,所以,我们似乎需要一个函数来等待下内核的执行。
其实倒不需要特别修改,因为cudaMemcpy()就是一个同步函数
修改运行代码如下:
- cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice);
- cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
- VecAdd<<<1, N>>>(d_A, d_B, d_C);
- cudaMemcpy(C, d_C, size, cudaMemcpyDeviceToHost);
- long ed=getCurrentTime();
- printf(“gpu running time:%ld\n”,ed–st);
执行,将那个毫无意义的j循环设到同等大小
- Hello,World
- gpu running time:515
- cpu running time:2674
又或者提高j循环次数分别提高十倍
- CUDA$ ./test
- Hello,World
- gpu running time:4064
- cpu running time:24683
这就比较科学啦~
然而需要注意的是内核启动和数据拷贝并非不需要时间,比如如果修改add为如下代码
- __global__ void VecAdd(float* A, float* B, float* C)
- {
- int i = threadIdx.x;
- C[i] = (A[i] * B[i]);
- }
- void cpu_VecAdd(int i,float* A, float* B, float* C)
- {
- C[i] = (A[i] * B[i]);
- }
反倒是GPU慢了
于是,我们已经对CUDA有了初步的尝试
这个Helloworld完整代码如下
- #include“stdio.h”
- #include<cuda_runtime.h>
- #include <sys/time.h>
- #define N 1000
- // Kernel definition
- __global__ void VecAdd(float* A, float* B, float* C)
- {
- int i = threadIdx.x;
- C[i] = (A[i] * B[i]);
- }
- long getCurrentTime()
- {
- struct timeval tv;
- gettimeofday(>tv,NULL);
- return tv.tv_sec * 1000000 + tv.tv_usec;
- }
- void cpu_VecAdd(int i,float* A, float* B, float* C)
- {
- C[i] = (A[i] * B[i]);
- }
- int main()
- {
- // Kernel invocation with N threads
- printf(“Hello,World\n”);
- float *A=new float[N],*B=new float[N],*C=new float[N];
- for(int i=0;i<N;i++)
- {
- A[i]=i;
- B[i]=2*i;
- }
- size_t size = N * sizeof(float);
- float* d_A;
- cudaMalloc(>d_A, size);
- float* d_B;
- cudaMalloc(>d_B, size);
- float* d_C;
- cudaMalloc(>d_C, size);
- float *e=new float[N];
- long st=getCurrentTime();
- cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice);
- cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
- VecAdd<<<1, N>>>(d_A, d_B, d_C);
- cudaMemcpy(C, d_C, size, cudaMemcpyDeviceToHost);
- long ed=getCurrentTime();
- printf(“gpu running time:%ld\n”,ed–st);
- st=getCurrentTime();
- for(int i=0;i<N;i++)
- cpu_VecAdd(i,A,B,e);
- ed=getCurrentTime();
- printf(“cpu running time:%ld\n”,ed–st);
- cudaFree(d_A);
- cudaFree(d_B);
- cudaFree(d_C);
- for(int i=0;i<N;i++)
- {
- //printf(“%f “,C[i]);
- }
- printf(“\n”);
- }