CUDA学习之第一章基于CUDA的异构并行计算

文章目录

  • 1.基于CUDA的异构并行计算
    • 并行计算
      • 串行编程和并行编程
      • 并行性
      • 计算机架构
        • 弗林分类
        • 内存组织分类
        • GPU
    • 异构计算
      • 异构架构
        • 衡量GPU容量的两个重要指标:
        • 评估GPU性能的两种不同指标:
      • 异构计算范例
        • GPU和CPU线程的特点:
      • CUDA:一种异构计算平台
    • 用GPU输出Hello World
      • CUDA编程模型基础
      • 第一个代码
        • cpu版
        • GPU版
    • CUDA编程注意

1.基于CUDA的异构并行计算

并行计算

并行计算通常涉及两个不同的计算技术领域。

  • 计算机架构(硬件方面)
  • 并行程序设计(软件方面)

计算机架构关注的是在结构级别上支持并行性,而并行编程设计关注的是充分使用计算机架构的计算能力来并发地解决问题。为了在软件中实现并行执行,硬件必须提供一个支持并行执行多进程或多线程的平台。
大多数现代处理器都应用了哈佛体系结构(Harvard architecture),如图1-1所示,
CUDA学习之第一章基于CUDA的异构并行计算_第1张图片

它主要由3个部分组成:

  • 内存(指令内存和数据内存)
  • 中央处理单元(控制单元和算术逻辑单元)
  • 输入/输出接口

高性能计算的关键部分是中央处理单元(CPU),通常被称为计算机的核心。在早期的计算机中,一个芯片上只有一个CPU,这种结构被称为单核处理器。现在,芯片设计的趋势是将多个核心集成到一个单一的处理器上,以在体系结构级别支持并行性,这种形式通常被称为多核处理器。

因此,并行程序设计可以看作是将一个问题的计算分配给可用的核心以实现并行的过程。

串行编程和并行编程

当用计算机程序解决一个问题时,我们会很自然地把这个问题划分成许多的运算块,每一个运算块执行一个指定的任务,运算块依次执行的程序叫作串行程序。

区分两个计算单元之间的关系:

  • 有执行次序的,必须串行执行;
  • 其他的没有执行次序的约束,则可以并发执行。所有包含并发执行任务的程序都是并行程序。一个并行程序中可能会有一些串行部分。
  • 从程序员的角度来看,一个程序应包含两个基本的组成部分:指令和数据。当一个计
    算问题被划分成许多小的计算单元后,每个计算单元都是一个任务。
    在一个任务中,单独的指令负责处理输入和调用一个函数并产生输出。当一个指令处理前一个指令产生的数据时,就有了数据相关性的概念。因此,你可以区分任何两个任务之间的依赖关系,如果一个任务处理的是另一个任务的输出,那么它们就是相关的,否则就是独立的。

分析数据的相关性是最基本的内容,因为相关性是限制并行性的一个主要因素。

并行性

在应用程序中有两种基本的并行类型。

  • 任务并行: 当许多任务或函数可以独立地、大规模地并行执行时,这就是任务并行。任务并行的重点在于利用多核系统对任务进行分配。
  • 数据并行: 当可以同时处理许多数据时,这就是数据并行。数据并行的重点在于利用多核系统对数据进行分配。CUDA编程非常适合解决数据并行计算的问题。

数据并行程序设计的第一步是把数据依据线程进行划分,以使每个线程处理一部分数据。

通常来说,有两种方法可以对数据进行划分:

  • 块划分(block partitioning):
    在块划分中,一组连续的数据被分到一个块内。每个数据块以任意次序被安排给一个线程,线程通常在同一时间只处理一个数据块。

  • 周期划分(cyclic partitioning):
    在周期划分中,更少的数据被分到一个块内。相邻的线程处理相邻的数据块,每个线程可以处理多个数据块。为一个待处理的线程选择一个新的块,就意味着要跳过和现有线程一样多的数据块。

在块划分中,每个线程仅需处理数据的一部分,而在周期划分中,每个线程要处理数据的多个部分。
CUDA学习之第一章基于CUDA的异构并行计算_第2张图片
通常,数据是在一维空间中存储的。即便是多维逻辑数据,仍然要被映射到一维物理地址空间中。如何在线程中分配数据不仅与数据的物理储存方式密切相关,并且与每个线程的执行次序也有很大关系。组织线程的方式对程序的性能有很大的影响。

程序性能通常对块的大小比较敏感。块划分与周期划分中划分方式的选择与计算机架构有密切关系。

计算机架构

弗林分类

弗林分类法(Flynn’s Taxonomy)根据指令和数据进入CPU的方式,将计算机架构分为4种不同的类型

  • 单指令单数据(SISD),是传统的计算机,一种串行架构,只有一个核心,在任何时间点上只有一个指令流在处理一个数据流。

  • 单指令多数据(SIMD),是现在大多计算机,一种并行架构,计算机上有多个核心,在任何时间点所有的核心只有一个指令处理不同的数据流。其最大优势在于编写代码时,程序员可以继续按照串行逻辑思考,但可以实现并加速并行数据操作,这些细节由编译起来负责。

  • 多指令单数据(MISD),比较少见,每个核心通过使用多个指令流来处理同一数据流。

  • 多指令多数据(MIMD),也是一种并行架构,多个核心使用多个指令流来异步处理多个数据流,从而实现空间上的并行性,许多MIMD还包括SIMD执行的子组件。

当前在架构层次已取得了许多进展,包括:

  1. 降低延迟,延迟是一个操作从开始到完成所需要的时间,常用微秒(ms)来表示,用来衡量完成一次操作的时间。

  2. 提高带宽,带宽是单位时间内可以处理的数据量,通常表示为MB/s或GB/s。

  3. 提高吞吐量,吞吐量是单位时间内成功处理的运算数量,通常表示为gflops(即每秒十亿次浮点运算数量),用来衡量在给定单位时间内处理的操作量。

内存组织分类

按照内存组织方式,计算机架构也可以划分成下面两种类型:

  1. 分布式内存的多节点系统,在这种系统中大型计算引擎由许多网络相连的处理器构成,每个处理器都有自己的本地内存,处理器之间通过网络通信,这种系统通常称为集群。
    CUDA学习之第一章基于CUDA的异构并行计算_第3张图片

  2. 共享内存的多处理器系统,由多个处理器组成,这些处理器要么与同一个物理内存相关联(如下图呀所示),要么共享一个低延迟的链路(PCI-Express或PCIe)。尽管共享内存意味着共享地址空间,但着并不意味着就是一个独立的物理内存。
    这样的多处理器不仅包括由多个核心组成的单片机系统,即所谓的多核系统,还包括由多个芯片组成的计算机系统,其中每个芯片都有可能是多核的。多核架构已经永久地取代了单核架构。多核的进一步扩展就是“众核”架构,是由很多(几十到几百)核心组成的系统。
    CUDA学习之第一章基于CUDA的异构并行计算_第4张图片

GPU

GPU代表着一种众核架构,几乎包括了前面所有的并行结构:多线程、MIMD(多指令多数据)、SIMD(单指令多数据),以及指令级并行。NVIDIA公司将这种架构称为SIMT(单指令多线程)。

GPU和CPU的来源并不相同。历史上,GPU是图形加速器。直到最近,GPU才演化成一个强大的、多用途的、完全可编程的,以及任务和数据并行的处理器,它非常适合解决大规模的并行计算问题。

尽管可以用多核和众核来区分CPU和GPU,但这两种核心完全不同:

  • CPU核心比较重,用来处理非常复杂的控制逻辑,以优化串行程序执行;
  • GPU核心比较轻,用于优化具有简单控制逻辑的数据并行任务,注重并行程序的吞吐量。

异构计算

同构是指使用的是同一架构下一个或多个处理器来执行一个应用,而异构计算使用一个处理器架构来执行一个应用,为任务选定合适它的架构,使其最终对性能有所改善。

尽管异构系统比传统的高性能计算系统有更大的优势,但目前对这种系统的有效利用受限于增加应用程序设计的复杂性。

CPU和GPU是两个独立的处理器,它们通过单个计算节点中的PCI-Express总线相连。

异构架构

一个典型的异构计算节点包括两个多核CPU插槽和多个或更多的众核GPU。GPU并非独立运行平台,而是CPU的协处理器,因此必须通过PCIe总线与基于CPU的主机相连来进行操作,如下图所示。因此,CPU所在的位置被称为主机端,而GPU所在的位置则被称为设备端。
CUDA学习之第一章基于CUDA的异构并行计算_第5张图片

一个异构应用包括两个部分。

  • 主机代码:主机代码在CPU上运行
  • 设备代码:设备代码在GPU上运行。

异构平台上执行的应用通常由CPU初始化。在设备端加载计算密集型任务之前,CPU代码负责管理设备端的环境、代码和数据。

在计算密集型应用中,往往有很多并行数据的程序段。GPU就是用来提高这些并行数据的执行速度的。当使用CPU上的一个与其物理上分离开的硬件组件来提高应用中的计算密集部分的执行速度时,这个组件就成为了一个硬件加速器。GPU可以说是最为常见的硬件加速器。

衡量GPU容量的两个重要指标:

  1. CUDA核心数量
  2. GPU内存大小。

评估GPU性能的两种不同指标:

  1. 峰值计算性能,用来评估计算容量,通常定义为每秒能处理的单精度或双精度浮点运算的总量,通常用GFlops(每秒10亿次浮点运算)或TFlops(每秒万亿次浮点运算)来表示。
  2. 内存带宽,是从内存中读取获奖写入数据的比率,常用GB/s表示。

异构计算范例

对于特定的应用程序,CPU和GPU都有自身的优点,如下图所示,两者结合能有效提高大规模计算问题的处理速度与性能:
CPU计算适合处理控制密集型任务,它针对动态工作负载进行了优化,这些动态工作负载由短序列的计算操作和不可预测的控制流程标记。
GPU计算适合处理包括数据并行的计算密集型任务,处理由计算任务主导的且带有简单控制流程的工作负载。
CUDA学习之第一章基于CUDA的异构并行计算_第6张图片
可以从并行级和数据规模两个方面来区分CPU和GPU应用范围:
如果问题的数据规模较小,但有复杂的控制逻辑和/或很少的并行性,则最好选择CPU来处理,因其有处理复杂逻辑和指令级并行性的能力;

如果问题包含大规模的待处理数据,并表现出大量数据并行性,则GPU是最好的选择,因其有大量可编程核心,可支持大规模多线程运算,且有更大的峰值带宽。

GPU和CPU线程的特点:

  1. CPU上的线程通常是重量级实体,操作系统必须打开或关闭执行通道来交替线程,从而提供多线程处理功能,其上下文切换缓慢且开销大。CPU的核被设计来尽可能减少一个或两个运行时间延迟。
  2. GPU上的线程则是高度轻量级的,在一个典型系统中会有成千上万的线程排队等待工作,如果GPU必须等待一组线程执行结束,则只需调用另一组线程执行其他任务即可。GPU的核是用来处理大量并发、轻量线程的,以最大限度提高吞吐量。

CUDA学习之第一章基于CUDA的异构并行计算_第7张图片
这种代码的编写方式能保证GPU与CPU相辅相成,从而使CPU+GPU系统的计算能力得以充分利用。为了支持使用CPU+GPU异构系统架构来执行应用程序,NVIDIA设计了一个被称为CUDA的编程模型。

CUDA:一种异构计算平台

CUDA是一种异构计算平台,通过它程序员可以像在CPU上那样通过GPU进行计算。CUDA可以通过CUDA加速库、编译器指令、应用程序接口和程序语言(C、C++、Fortran、Python)扩展来使用,如下图所示。CUDA C是标准ANSI C语言的一个扩展,它带有的少数语言扩展功能使异构编程成为可能,同时也能通过API来管理设备、内存和其他任务。
CUDA学习之第一章基于CUDA的异构并行计算_第8张图片
CUDA提供了两层API来管理GPU设备和组织线程,如下图所示:

  1. CUDA驱动API,是一种低级API,较难编程,但在GPU设备使用上提供了更多的控制;
  2. CUDA Runtime API,是一个高级API,在驱动API之上实现。每个Runtime API函数
    都被分解为更多传给驱动API的基本运算。

这两种API是相互排斥的,你必须使用两者之一,从两者中混合函数调用是不可能的。本系列中所有例子都使用运行时API。
CUDA学习之第一章基于CUDA的异构并行计算_第9张图片
运行时API和驱动API之间没有明显的性能差异。在设备端,内核是如何使用内存以及你是如何组织线程的,对性能有更显著的影响。

与其他异构代码一样,CUDA程序包含了在CPU上运行的主机代码和在GPU上运行的设备代码。
NVIDIA的CUDA nvcc编译器在编译过程中将设备代码从主机代码中分离出来。

  1. 主机代码是标准的C代码,使用C编译器进行编译。

  2. 设备代码,也就是核函数,是用扩展的带有标记数据并行函数关键字的CUDA C语言编写的。设备代码通过nvcc进行编译。在链接阶段,在内核程序调用和显示GPU设备操作中添加CUDA运行时库。
    CUDA学习之第一章基于CUDA的异构并行计算_第10张图片

用GPU输出Hello World

CUDA编程模型基础

在CUDA中,host和device是两个重要的概念,我们用host指代CPU及其内存,而用device指代GPU及其内存。CUDA程序中既包含host程序,又包含device程序,它们分别在CPU和GPU上运行。同时,host与device之间可以进行通信,这样它们之间可以进行数据拷贝。典型的CUDA程序的执行流程如下:

  1. 分配host内存,并进行数据初始化;
  2. 分配device内存,并从host将数据拷贝到device上;
  3. 调用CUDA的核函数在device上完成指定的运算;
  4. 将device上的运算结果拷贝到host上;
  5. 释放device和host上分配的内存。

第一个代码

写一个CUDA C程序,你需要以下几个步骤:

  1. 用专用扩展名.cu来创建一个源文件。
  2. 使用CUDA nvcc编译器来编译程序。
  3. 从命令行运行可执行文件,这个文件有可在GPU上运行的内核代码。

cpu版

首先,我们编写一个C语言程序来输出“Hello World”,
把代码保存到hello.cu中,
然后使用nvcc编译器来编译。
CUDA nvcc编译器和gcc编译器及其他编译器有相似的语义。
如果你运行可执行文件hello,将会输出:

$ vim hello.cu
$ nvcc hello.cu -o hello
$ ./hello
Hello World from CPU!

GPU版

#include

__global__ void helloFromGPU(void)
{
        printf("Hello World from GPU!\n");
}
int main(void)
{
        printf("Hello World from CPU!\n");
        helloFromGPU <<<1, 10>>>();
        cudaDeviceReset();
        return 0;
}

  1. 编写一个内核函数, 命名为helloFromGPU,用它来输出字符串“Hello World from GPU!”。
    修饰符__global__告诉编译器这个函数将会从CPU中调用,然后在GPU上执行。

  2. 代码启动内核函数:helloFromGPU <<<1, 10>>>();
    三重尖括号意味着从主线程到设备端代码的调用。一个内核函数通过一组线程来执
    行,所有线程执行相同的代码。三重尖括号里面的参数是执行配置,用来说明使用多少线程来执行内核函数。
    在这个例子中,有10个GPU线程被调用,且每个线程调用一次。

  3. 函数cudaDeviceRest()用来显式地释放和清空当前进程中与当前设备有关的所有资
    源。

  4. 在nvcc命令行中使用-arch sm_60进行编译: 开关语句-arch sm_60使编译器为架构生成设备代码。

  5. 运行这个可执行文件,它将输出10条字符串“Hello World from GPU”,每个线程输出1条。
    helloFromGPU <<<2, 10>>>();时,将输出20句Hello World from CPU!

$ nvcc -arch sm_60 hello.cu -o hello
$./hello
Hello World from CPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!
Hello World from GPU!

CUDA编程注意

CPU编程和GPU编程的主要区别是程序员对GPU架构的熟悉程度。用并行思维进行思考并对GPU架构有了基本的了解。

数据局部性在并行编程中是一个非常重要的概念。
数据局部性指的是数据重用,以降低内存访问的延迟。
数据局部性有两种基本类型:

  1. 时间局部性是指在相对较短的时间段内数据和/或资源的重用。

  2. 空间局部性是指在相对较接近的存储空间内数据元素的重用。

现代的CPU架构使用大容量缓存来优化具有良好空间局部性和时间局部性的应用程序。设计高效利用CPU缓存的算法是程序员的工作。程序员必须处理低层的缓存优化,但由于线程在底层架构中的安排是透明的,所以这一点程序员是没有办法优化的。

CUDA中有内存层次和线程层次的概念,使用如下结构,有助于你对线程执行进行更高层次的控制和调度:

  • 内存层次结构
  • 线程层次结构

例如,在CUDA编程模型中使用的共享内存(一个特殊的内存)。共享内存可以视为一个被软件管理的高速缓存,通过为主内存节省带宽来大幅度提高运行速度。有了共享内存,你可以直接控制代码的数据局部性。

当用ANSI C语言编写一个并行程序时,你需要使用pthreads或者OpenMP来显式地组织线程,这两项技术使得在大多数处理器架构以及操作系统中支持并行编程。当用CUDA C编写程序时,实际上你只编写了被单个线程调用的一小段串行代码。GPU处理这个内核函数,然后通过启动成千上万个线程来实现并行化,所有的线程都执行相同的计算。

CUDA编程模型提供了一个层次化地组织线程的方法,它直接影响到线程在GPU上的执行顺序。
因为CUDA C是C语言的扩展,通常可以直接将C程序移植到CUDA C程序中。概念上,剥离代码中的循环后产生CUDA C实现的内核代码。
CUDA抽象了硬件细节,且不需要将应用程序映射到传统图形API上。

CUDA核中有3个关键抽象:线程组的层次结构,内存的层次结构以及障碍同步。这3个抽象是最小的一组语言扩展。随着CUDA版本的更新,NVIDIA正在对并行编程进行不断简化。尽管一些人仍然认为CUDA的概念比较低级,但如果稍稍提高抽象级,对你控制应用程序和平台之间的互动关系来说会增加很大难度。如果那样的话,不管你掌握了多少底层架构的知识,你的应用程序的性能都将超出控制。

因此,你的目标应是学习GPU架构的基础及掌握CUDA开发工具和环境。

NVIDIA为C和C++开发人员提供了综合的开发环境以创建GPU加速应用程序,包括以下几种。

·NVIDIA Nsight集成开发环境
·CUDA-GDB命令行调试器
·用于性能分析的可视化和命令行分析器
·CUDA-MEMCHECK内存分析器
·GPU设备管理工具

你可能感兴趣的:(CUDA,人工智能,CUDA)