AI学习者 · 2020年10月23日

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

来源:AI人工智能初学者
作者:ChaucerG

目录

1、 CUDA并行编程的内容概要

2、 CUDA程序结构

    2.1、内核调用

    2.2、配置内核参数

    2.3、CUDA API函数

3、在GPU设备上执行线程

1、CUDA并行编程的内容概要

    在上一篇推文中,讨论了如何安装CUDA并使用它编写程序。尽管示例并不令人印象深刻,但它证明了使用CUDA是非常容易的。

    在本次推文和下一次推文中,继续以这个概念为基础,讨论一下如何使用CUDA为GPU编写高级程序。从变量加法程序开始,然后逐步构建CUDA C中的复杂向量操作示例,同时也会介绍内核如何工作以及如何在CUDA程序中使用设备属性。本部分还会讨论在CUDA程序中向量是如何运算的,以及与CPU处理相比,CUDA如何能加速向量运算。除此之外,还会介绍与CUDA编程相关的术语。

本部分将讨论以下主题(紫色部分为本次推文的内容):

    1、内核调用的概念

2、在CUDA中创建内核函数并向其传递参数

3、配置CUDA程序的内核参数和内存分配

4、CUDA程序中的线程执行

5、在CUDA程序访问GPU设备属性

6、在CUDA程序中处理向量

7、并行通信模型

2、CUDA程序结构

   上一篇推文中介绍了一个非常简单的“Hello,CUDA!”程序,其中展示了一些与CUDA程序相关的重要概念。CUDA程序是在主机或GPU设备上执行的函数的组合。不显示并行性的函数在CPU上执行,显示数据并行性的函数在GPU上执行。GPU编译器在编译期间隔离这些函数。如前一篇所示,在设备上执行的函数是使用\_\_global\_\_关键字定义的,由NVCC编译器编译,而普通的C主机代码是由C编译器编译的。CUDA代码基本上与ANSI C代码相同,只是添加了一些开发数据并行性所需的关键字。

    因此,本次将用一个简单的双变量加法程序来解释与CUDA编程相关的重要概念,如内核调用、从主机到设备传递参数到内核函数、内核参数的配置、利用数据并行性需要的CUDA API,以及发生在主机和设备上的内存分配。

    话不多说,直接上代码:

#include <iostream>

image.png

2.1、内核调用

    使用ANSI C关键字和CUDA扩展关键字编写的设备代码称为内核。它是主机代码(Host Code)通过内核调用的方式来启动的。简单地说,内核调用的含义是我们从主机代码启动设备代码。内核调用通常会生成大量的块(Block)和线程(Thread)来在GPU上并行地处理数据。内核代码非常类似于普通的C函数,只是这段代码是由多个线程并行执行的。

    它以我们想要启动的内核的名称开始。你应该确保这个内核是使用\_\_global\_\_关键字定义的。然后,它具有<<<>>>内核启动配置,该配置包含内核的配置参数。它可以包含三个用逗号分隔的参数。第一个参数表示希望执行的块数,第二个参数表示每个块将具有的线程数。因此,内核启动所启动的线程总数就是这两个数字的乘积。第三个参数是可选的,它指定内核使用的共享内存的大小。在变量相加程序中,内核启动语法如下:

gpuAdd << <1,1> >> (6, 18, d\_c)

    在这里,gpuAdd是想要启动的内核的名称,<<<1,1>>>表示想用每个块一个线程启动一个块,这意味着只启动一个线程。圆括号中的三个参数是传递给内核的参数。这里,传递了两个常数,6和18。第三个参数是指向d\_c设备显存的指针。它指向设备显存中的位置,内核将在那里存储相加后的结果。

    程序员必须记住的一件事是,作为参数传递给内核的指针应该仅指向设备显存。如果它指向主机内存,会导致程序崩溃。内核执行完成后,设备指针指向的结果可以复制回主机内存,以供进一步使用。只启动一个线程在设备上执行不是设备资源的最佳使用。

2.2、配置内核参数

    为了在设备上并行启动多个线程,必须在内核调用中配置参数,内核调用是在内核启动配置中编写的。它们指定了Grid中块的数量,和每个块中线程的数量。可以并行启动很多个块,而每个块内又有很多个线程。通常,每个块有512或1024个线程。每个块在流多处理器上运行,一个块中的线程可以通过共享内存(Shared Memory)彼此通信。程序员无法选定哪个流多处理器将执行特定的块,也无法选定块和线程以何种顺序执行。

    假设要并行启动500个线程,你可以对前面解释的内核启动语法进行哪些修改?一种选择是通过以下语法启动一个包含500个线程的块:

gpuAdd << <1,500> >> (6, 18, d\_c)

    程序员必须注意,每个块的线程数量不能超过GPU设备所支持的最大限制。

    如果需要要处理一个图像,你可以启动一个16×16的块网格,所有的块都包含16×16个线程。语法如下:

mykernel << < dim3(16, 16), dim3(16, 16) > >> ()

    总之,在启动内核时,块数量和线程数量的配置非常重要。根据正在开发的应用程序和GPU资源的不同,应该谨慎地选择。

2.3、CUDA API函数

    在变量加法程序中,会遇到了一些常规C或C++程序员不熟悉的函数或关键字。这些关键字和函数包括:

    \_\_global\_\_

    cudaMalloc

    cudaMemcpy

    cudaFree

\_\_global\_\_:它与\_\_device\_\_和\_\_host\_\_一起是三个限定符关键字。这个关键字表示一个函数被声明为一个设备函数,当从主机调用时将在设备上执行。应该记住,这个函数只能从主机调用。如果要在设备上执行函数并从设备函数调用函数,则必须使用\_\_device\_\_关键字。\_\_host\_\_关键字用于定义只能从其他主机函数调用的主机函数。这类似于普通的C函数。默认情况下,程序中的所有函数都是主机函数。\_\_host\_\_和\_\_device\_\_都可以同时用于定义任何类型函数。它生成同一个函数的两个副本。一个将在主机上执行,另一个将在设备上执行。

cudaMalloc:它类似于C中用于动态内存分配的Malloc函数。此函数用于在设备上分配特定大小的内存块。

cudaMemcpy:这个函数类似于C中的Memcpy函数,用于将一个内存区域复制到主机或设备上的其他区域。

cudaFree:类似于C中的free函数;

    CUDA除了现有的ANSI C函数之外,还有许多其他的关键字和函数。我们会经常使用这三个函数,因此对它们进行了讨论。要了解更多细节,你可以看一下CUDA的编程指南。

3、在GPU设备上执行线程

#include <iostream>

    从代码中可以看出,正在启动一个内核,它有16个并行块,每个块只有一个线程。在每个执行该段内核代码的线程里,我们打印出来它们各自获取到的块ID。可以认为,并行启动了16个执行相同myfirstkernel代码的线程副本。每个副本线程将拥有一个属于自己的块ID和线程ID。本例中,前者可以通过blockIdx.x的CUDA C的内置变量读取到。后者则可以通过threadIdx.x内置变量读取到。这两个ID将告诉我们正在执行内核的是具体哪个块和其中的哪个线程副本。当你多次运行程序时会发现,每次运行,线程块都是以不同的顺序执行的。一个样本输出如图:

image.png

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

声明:转载请说明出处
推荐专栏文章

更多嵌入式AI算法部署等请关注极术嵌入式AI专栏
推荐阅读
关注数
16565
内容数
1230
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息