0%

CUDA高性能并行计算(3)--CUDA编程初试

CUDA编程初试

第一个调用GPU计算的程序

在前面,我们已经写了一个串行的dist程序计算,现在我们尝试对其进行改进,从而得到一个使用GPU进行计算的程序。

为了调用GPU,所以我们需要写一个核函数,从而在主机端调用GPU。由于核函数是用于不断产生出线程,而线程才是真正用于计算的东西,所以我们在核函数内需要为每个线程分配计算任务,这个就是等价之前for循环内部的东西。

值得注意的是,每个被核函数调用出来的线程执行的计算都是相同的,所以在核函数内需要为每个线程分配数据,这样就可以执行计算。

与之类比,在C语言中fork出的新线程可以执行与原有线程不一样的指令。

每个线程分配计算数据

那如何为每个线程分配数据呢?

在核函数内,有以下的变量可以用于给不同的线程分配数据,这些变量是提供线程块核线程的维度数核索引变量

  • gridDim:声明了网格中的线程块数目

  • blockDim:声明了每个线程块中的线程数目

  • blockIdx:给出了线程块在这个网格中的索引

  • threadIdx:给出了这个线程在这个线程块中的索引

前两个的类型为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;
}

程序执行过程

  1. 在设备上创建数组,这个数组大小和主机上的数组大小一致。
  2. 在内存上分配内存来存储输入的数据。
  3. 将主机端的数组复制到设备中(cudaMemcpy函数)
  4. 启动核函数执行计算并将输出值存储在设备内存上的输出数组中
  5. 将设备上的结果赋值到主机端
  6. 释放内存

注意到,在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这些变量可以用于定位线程的处理数据的索引。