CUDA编程初试
第一个调用GPU计算的程序
在前面,我们已经写了一个串行的dist程序计算,现在我们尝试对其进行改进,从而得到一个使用GPU进行计算的程序。
为了调用GPU,所以我们需要写一个核函数,从而在主机端调用GPU。由于核函数是用于不断产生出线程,而线程才是真正用于计算的东西,所以我们在核函数内需要为每个线程分配计算任务,这个就是等价之前for循环内部的东西。
值得注意的是,每个被核函数调用出来的线程执行的计算都是相同的,所以在核函数内需要为每个线程分配数据,这样就可以执行计算。
与之类比,在C语言中fork出的新线程可以执行与原有线程不一样的指令。
每个线程分配计算数据
那如何为每个线程分配数据呢?
在核函数内,有以下的变量可以用于给不同的线程分配数据,这些变量是提供线程块核线程的维度数核索引变量
前两个的类型为dim3
,这是一个1*3的向量,可以使用.x.y.z来访问。
后面两个类型为uint3
,这也是一个1*3向量,遇上一个类似。
核函数调用时的<<<A,B>>>其实是<<<dim3(A,1,1),dim3(B,1,1)>>>,即目前声明的都是1维数组,当需要计算二维、三维的时候,就可以声明高维数组。
每个线程块的线程数不超过1024
主机端与GPU的数据通信
由于GPU不能直接访问主机端中的数据,所以,CUDA提供了一系列可以把数据从设备传回主机的API
位于头文件cuda_runtime.h
cudaMalloc()
:分配设备端内存
cudaMemcpy()
:将数据传入或传出设备
cudaFree()
:释放设备内存
size_
:内存大小的专用变量
cudaError_t
:错误处理专用变量
编写程序
有了上面的内容,我们很容易就将之前的串行程序改成GPU并行程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| #include "device_launch_parameters.h" #include "cuda_runtime.h" #include <stdio.h> #include <math.h>
#define N 64 #define TPB 32
__device__ float scale(int i, int n) { return ((float)i / (n - 1)); }
__device__ float distance(float x1, float x2) { return sqrt((x1 - x2)*(x1 - x2)); }
__global__ void distanceKernel(float *d_out, float ref, int len) { const int i = blockIdx.x*blockDim.x + threadIdx.x; const float x = scale(i, len); d_out[i] = distance(x, ref); printf("i = %2d: dist from %f to %f is %f\n", i, ref, x, d_out[i]); }
int main() { const float ref = 0.5f; float *d_out = 0; cudaMalloc(&d_out, N * sizeof(float));
distanceKernel << <N / TPB, TPB >> > (d_out, ref, N);
cudaFree(d_out);
return 0;
}
|
执行nvcc distcuda.cu
来编译,运行后可以看到,程序的执行顺序是乱的,但是其结果都被打印出来了。
另一个更深入的CUDA程序
我们之前写过两个版本的dist,现在我们把第二个版本的dist并行化。
这是一个更加通用的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| #if DIST == 2
#include <cuda_runtime.h> #include <device_launch_parameters.h> #include <stdlib.h> #include <stdio.h> #include <math.h>
#define N 64 #define TPB 32
float scale(int i, int n) { return ((float)i / (n - 1)); }
__device__ float distance(float x1, float x2) { return sqrt((x2 - x1)*(x2 - x1)); }
__global__ void distanceKernel(float *d_out, float * d_in, float ref) { const int i = blockIdx.x*blockDim.x + threadIdx.x; const float x = d_in[i]; d_out[i] = distance(x, ref); printf("i = %2d: dist from %f to %f is %f\n", i, ref, x, d_out[i]); }
void distanceArray(float * out, float *in, float ref, int len) { float *d_out = 0; float *d_in = 0; cudaMalloc(&d_out, len * sizeof(float)); cudaMalloc(&d_in, len * sizeof(float));
cudaMemcpy(d_in, in, len * sizeof(float), cudaMemcpyHostToDevice);
distanceKernel << <N / TPB, TPB >> > (d_out, d_in, ref);
cudaMemcpy(out, d_out, len * sizeof(float), cudaMemcpyDeviceToHost);
cudaFree(d_in); cudaFree(d_out);
}
int main() { const float ref = 0.5f; float * in = (float *)calloc(N, sizeof(float)); float * out = (float *)calloc(N, sizeof(float));
for (int i = 0; i < N; i++) { in[i] = scale(i, N); }
distanceArray(out, in, ref, N);
free(in); free(out); return 0; }
|
程序执行过程
- 在设备上创建数组,这个数组大小和主机上的数组大小一致。
- 在内存上分配内存来存储输入的数据。
- 将主机端的数组复制到设备中(
cudaMemcpy
函数)
- 启动核函数执行计算并将输出值存储在设备内存上的输出数组中
- 将设备上的结果赋值到主机端
- 释放内存
注意到,在GPU上的计算比在主机端的计算不一样的地方:
- 在设备上创建一个镜像数组,然后需要把数据拷贝过去
- 启动一个核函数并执行大量计算
- 把结果拷贝回主机
很明显,只有当核函数并行计算的收益超过拷贝数据时,使用GPU计算才是合理的。
统一内存与托管数组
在两个设备之间传递已有的很大数组这个步骤是必要且有点冗余的,所以出现了统一内存来简化开发流程。
统一内存就是一个主机和设备可以共同访问的内存,但实际上只是CUDA帮助我们完成了在主机与设备之间拷贝数组的活,这项工作本身没有消失,只是不需要显示地做了。另外,CUDA系统调度内存可能不如手工管理内存那样好。
其实现方式是使用cudaMallocManaged()
即可。
我们对上一个程序使用统一内存来进行简化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <cuda_runtime.h> #include <device_launch_parameters.h> #include <stdlib.h> #include <stdio.h> #include <math.h>
#define N 64 #define TPB 32
float scale(int i, int n) { return ((float)i / (n - 1)); }
__device__ float distance(float x1, float x2) { return sqrt((x2 - x1)*(x2 - x1)); }
__global__ void distanceKernel(float *d_out, float * d_in, float ref) { const int i = blockIdx.x*blockDim.x + threadIdx.x; const float x = d_in[i]; d_out[i] = distance(x, ref); printf("i = %2d: dist from %f to %f is %f\n", i, ref, x, d_out[i]); }
void distanceArray(float * out, float *in, float ref, int len) { distanceKernel << <N / TPB, TPB >> > (out, in, ref); }
int main() { const float ref = 0.5f; float * in = 0; float * out = 0;
cudaMallocManaged(&in, N * sizeof(float)); cudaMallocManaged(&out, N * sizeof(float));
for (int i = 0; i < N; i++) { in[i] = scale(i, N); }
distanceArray(out, in, ref, N);
cudaFree(in); cudaFree(out); return 0; }
|
注意到distanceArray函数中不再需要冗长的复制数据的过程了,这样就非常的方便了。
总结
- 核函数只能在设备上执行,不能向主机端返回变量,其可以访问设备内存,但是不能访问主机内存(这也是为什么需要用
cudaMalloc
而不是malloc
)。
- 可以使用
cudaMallocManaged()
来自动传递内存,而避免使用cudaMemcpy()
来显示传递数据。
- 一般每个线程块的线程数目是32的整倍数时,可以对应SM中的CUDA核心数目,这样性能更好。
- gridDim、blockDim、blockIdx、threadIdx这些变量可以用于定位线程的处理数据的索引。