【CUDA学习笔记】第三篇:CUDA C并行化编程【下半部分】(附案例代码下载方式)(二)

简介: 【CUDA学习笔记】第三篇:CUDA C并行化编程【下半部分】(附案例代码下载方式)(二)

3、CUDA中的向量运算


3.1、两个向量加法程序

GPU main函数具有本章第一节所述的已知结构:


1、先是定义CPU和GPU上的数组和指针。设备指针指向通过cudaMalloc分配的显存。


2、然后通过cudaMemcpy函数,将前两个数组,从主机内存传输到设备显存。


3、内核启动的时候,将这些设备指针作为参数传递给它。你看到的内核启动符号(<<<>>>)里面的N和1,它们分别是启动N个块,每个块里面只有1个线程。


4、再然后通过cudaMemcpy,将内核的计算结果从设备显存传输到主机内存,注意最后这次传输的方向是反的,设备到主机。


5、最后,用cudaFree释放掉3段在显存上分配的缓冲区。


直接上代码:

#include "stdio.h"
#include<iostream>
#include <cuda.h>
#include <cuda_runtime.h>
//定义数组元素个数
#define N5
//定义GPU设备的Kernel函数
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {
int tid = blockIdx.x;// 获取数据的index
if (tid < N)
d_c[tid] = d_a[tid] + d_b[tid];
}
int main(void) {
//定义主机数组
int h_a[N], h_b[N], h_c[N];
//定义GPU设备指针
int *d_a, *d_b, *d_c;
// 申请内存
cudaMalloc((void**)&d_a, N * sizeof(int));
cudaMalloc((void**)&d_b, N * sizeof(int));
cudaMalloc((void**)&d_c, N * sizeof(int));
//初始化数组
for (int i = 0; i < N; i++) {
h_a[i] = 2 * i*i;
h_b[i] = i;
}
// 把CPU下的数组复制到GPU设备
cudaMemcpy(d_a, h_a, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, N * sizeof(int), cudaMemcpyHostToDevice);
//确定要使用的块和线程数目
gpuAdd << <N, 1 >> > (d_a, d_b, d_c);
//把GPU运输的结果复制回CPU主机上进行打印处理
cudaMemcpy(h_c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);
printf("Vector addition on GPU \n");
//在控制台打印结果
for (int i = 0; i < N; i++) {
printf("The sum of %d element is %d + %d = %d\n", i, h_a[i], h_b[i], h_c[i]);
}
//最后别忘了释放内存
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}

   所有CUDA程序都遵循与前面相同的模式。我们并行启动N个块。这意味着我们同时启动了N个执行该内核代码的线程副本。你可以通过一个现实的例子来理解这一点:假设你想把5个大盒子从一个地方转移到另一个地方。在第一种方法中,你可以通过雇佣一个人将一个盒子从一个地方带到另一个地方,然后重复5次来完成这项任务。这个方式需要时间,它类似于如何在CPU上做向量相加。现在,假设你雇了5个人,每个人都带着一个盒子。他们每个人也知道他们所携带的箱子的ID。这个方法将比前一个快得多。每个人只需要被告知他们必须携带一个带有特定ID的盒子从一个地方到另一个地方。


   从上述例子里我们知道内核代码的编写方式,以及这些线程是如何在GPU上执行的。每个线程可以通过blockIdx.x内置变量来知道自己的ID。然后每个线程通过ID来索引数组,计算每对元素加法。这样,多个线程的并行计算,明显减少了数组整体的处理时间。所以,用这种并行的计算方式,比CPU上的串行计算,提高了吞吐率。

   这个程序还含有额外的一个CUDA函数调用:cudaDeviceSynchronize()。为何要加这句?这是因为启动内核是一个异步操作,只要发布了内核启动命令,不等内核执行完成,控制权就会立刻返回给调用内核的CPU线程。在上述的代码中,CPU线程返回,继续执行的下一句是printf()。而再之后,在内核完成之前,进程就会结束,终止控制台窗口。所以,如果不加上这句同步函数,你就看不到任何的内核执行结果输出。在程序退出后内核生成的输出结果,将没有地方可去,你没法看到它们,因此,如果我们不包含这个指令,你将不会看到任何内核执行的printf语句的输出结果。要能看到内核生成的输出结果,我们必须包含这句同步函数。这样,内核的结果将通过可用的标准输出显示,而应用程序则会在内核执行完成之后才退出。


3.2、对比CPU代码和GPU代码的延迟

#include "stdio.h"
#include<iostream>
#include <cuda.h>
#include <cuda_runtime.h>
//定义数组元素个数
#define N10000000
//定义GPU设备的Kernel函数
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {
int tid = blockIdx.x;// 获取数据的index
if (tid < N)
d_c[tid] = d_a[tid] + d_b[tid];
}
//定义CPU设备的函数
void cpuAdd(int *h_a, int *h_b, int *h_c) {
int tid = 0;
while (tid < N)
{
h_c[tid] = h_a[tid] + h_b[tid];
tid += 1;
}
}
int main(void) {
//定义主机数组
int h_a[N], h_b[N], h_c[N];
//定义GPU设备指针
int *d_a, *d_b, *d_c;
// 申请内存
cudaMalloc((void**)&d_a, N * sizeof(int));
cudaMalloc((void**)&d_b, N * sizeof(int));
cudaMalloc((void**)&d_c, N * sizeof(int));
//初始化数组
for (int i = 0; i < N; i++) {
h_a[i] = 2 * i*i;
h_b[i] = i;
}
/*****************************************CPU运算****************************************/
clock_t start_h = clock();
//Calling CPU function for vector addition
cpuAdd(h_a, h_b, h_c);
clock_t end_h = clock();
double time_h = (double)(end_h - start_h) / CLOCKS_PER_SEC;
//printf("Vector addition on CPU\n");
//for (int i = 0; i < N; i++) {
//printf("The sum of %d element is %d + %d = %d\n", i, h_a[i], h_b[i], h_c[i]);
//}
/*****************************************GPU运算****************************************/
// 把CPU下的数组复制到GPU设备
cudaMemcpy(d_a, h_a, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, N * sizeof(int), cudaMemcpyHostToDevice);
clock_t start_d = clock();
//确定要使用的块和线程数目并在GPU处理运行
gpuAdd << <N, 1 >> > (d_a, d_b, d_c);
cudaThreadSynchronize();
clock_t end_d = clock();
double time_d = (double)(end_d - start_d) / CLOCKS_PER_SEC;
printf("No of Elements in Array:%d\n Device time %f Seconds \n host time %f Seconds\n", N, time_d, time_h);
//把GPU运输的结果复制回CPU主机上进行打印处理
cudaMemcpy(h_c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);
//printf("Vector addition on GPU \n");
//在控制台打印结果
//for (int i = 0; i < N; i++) {
//printf("The sum of %d element is %d + %d = %d\n", i, h_a[i], h_b[i], h_c[i]);
//}
//最后别忘了释放显存
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}

image.png

   我们可以从输出结果看到,执行时间或叫吞吐量,从25ms提高到在GPU上几乎1ms实现相同的功能。这证明了我们之前在理论上提到的GPU并行执行代码有助于提高吞吐量。


3.3、并行通信模式

   当多个线程并行执行时,它们遵循一定的通信模式,指导它们在显存里哪里输入,哪里输出。我们将讨论每个通信模式。它将帮助你识别通信模式相关的应用程序,以及如何编写代码。


1、映射模式(一对一操作:基本运算使用)

在这种通信模式中,每个线程或任务读取单一输入,产生一个输出。基本上,它是一个一对一的操作。前面部分中向量加法程序和向量元素平方的程序,就是Map模式的例子。Map的代码模式看起来如下:


2、收集(多对一操作,移动平均使用)

   在此模式中,每个线程或者任务,具有多个输入,并产生单一输出,保存到存储器的单一位置中。假设你想写一个求3数据的MA操作的程序。这就是一个Gather操作的例子。每个线程读取3个输入数据,产生单一的结果数据保存到显存。因此,在输入端有数据复用。它基本上是一个多对一的操作。Gather模式的代码看起来如下:


3、分散式(一对多操作,按序排列使用)

   在Scatter模式中,线程或者任务读取单一输入,但向存储器产生多个输出。数组排序就是一个Scatter操作的例子。它也可以叫作1对多操作。Scatter模式的代码看起来如下:


4、蒙板(图像使用)

   当线程或者任务要从数组中读取固定形状的相邻元素时,这叫stencil模式。这种模式在图像处理中非常有用,例如当你想用一个3×3或者5×5的(滤波)窗口时(对整个图像进行滑动处理的时候)。它是Gather操作的一种模式,所以代码的语法和Gather很相似。


5、转置(一对一操作,基本使用)

   当原始输入矩阵是行主序的时候,如果需要输出得到一个列主序的矩阵,则应当进行转置操作。如果你有一个结构数组(SoA),而你想把它转换成一个数组结构(AoS),它是特别有用的。这也是一个一对一的操作。Transpose模式的代码看起来如下:

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
存储 并行计算 测试技术
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
171 0
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
|
缓存 并行计算 API
【CUDA学习笔记】第三篇:CUDA C并行化编程【下半部分】(附案例代码下载方式)(一)
【CUDA学习笔记】第三篇:CUDA C并行化编程【下半部分】(附案例代码下载方式)(一)
169 0
|
存储 并行计算 程序员
【CUDA学习笔记】第二篇:CUDA C并行化编程【上半部分】(附案例代码下载方式)
【CUDA学习笔记】第二篇:CUDA C并行化编程【上半部分】(附案例代码下载方式)
414 0
|
存储 并行计算 计算机视觉
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(一)
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(一)
296 0
|
并行计算 异构计算
|
并行计算 Linux 程序员
|
并行计算 异构计算
|
并行计算 异构计算 数据管理
|
并行计算 异构计算
|
并行计算 调度