(一)基于CUDA的异构并行计算

本篇笔记参考如下:
https://baike.baidu.com/item/冯·诺依曼结构/9536784

自本章开始使用配置和版本如下:

1
2
3
4
显卡:NVIDIA GeForce RTX 5060 Ti 16g
CUDA Toolkit 安装版本:13.0.88
架构:-arch=sm_120(Blackwell)
优化级别:O2

1.1 并行计算

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

·计算机架构(硬件方面)

·并行程序设计(软件方面)

书中提及大多数现代处理器都应用了哈佛体系结构

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

实际上,如今在现代高性能计算机(如电脑、手机)中,普遍采用的是一种“混合模式”,通常被称为“改进型哈佛架构”(Modified Harvard Architecture)。

宏观层面(由于成本和灵活性):冯·诺依曼占主导

统一的内存(RAM): 16GB 或 32GB 内存条里,既存放着正在运行的软件代码(指令),也存放着你正在编辑的文档(数据)。

统一的存储(硬盘): 硬盘里也是程序和数据混在一起。

冯·诺依曼结构更便宜、更灵活。如果采用纯哈佛结构,需要把内存物理上分成两块,不仅硬件设计复杂,而且如果你的程序很大但数据很少(或者反过来),另一块内存就会被浪费。

微观层面(为了速度):哈佛结构占主导

把 CPU 拆开,看它的核心,会发现现代处理器(无论是 Intel/AMD 的 x86 还是手机里的 ARM 芯片)在核心内部大多采用了哈佛结构的设计思路。

L1 缓存分离: 现代 CPU 的一级缓存(L1 Cache)通常被严格分为两部分:指令缓存(Instruction Cache)数据缓存(Data Cache)

并行读取: 这意味着 CPU 可以同时读取一条指令和读取一个数据,互不干扰。这正是哈佛结构的核心优势——速度快,吞吐量高。

这就是所谓的“改进型哈佛架构”:

  • CPU 内部: 像哈佛结构一样,指令和数据分开跑,追求极致速度。
  • CPU 外部: 像冯·诺依曼结构一样,共用主内存,追求灵活性和低成本。

1.1.1 串行编程和并行编程

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

有些是有执行次序的,所以必须串行执行;其他的没有执行次序的约束,则可以并发执行。

并行程序:所有包含并发执行任务的程序。

从程序员的角度来看,一个程序应包含两个基本的组成部分:指令和数据

当一个计算问题被划分成许多小的计算单元后,每个计算单元都是一个任务。在一个任务中,单独的指令负责处理输入和调用一个函数并产生输出。当一个指令处理前一个指令产生的数据时,就有了数据相关性的概念。

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

1.1.2 并行性

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

·任务并行

·数据并行

这里举一个例子,是我大三上计算机体系结构课程时罗老师(后来还指导了我的毕设!感恩老师!)举的例子,时间比较久远,大致说一下:三位助教需要改一个班级的卷子,假设一张卷子有三道题,共有30份卷子。如果是每个助教改10份卷子,这就是任务并行。如果是每个助教改30份卷子中的一道题,这就是数据并行。例子大致是这样,接下来就说一下具体的定义吧~
任务并行:当许多任务或函数可以独立地、大规模地并行执行时,这就是任务并行。任务并行的重点在于利用多核系统对任务进行分配。

数据并行:当可以同时处理许多数据时,这就是数据并行。数据并行的重点在于利用多核系统对数据进行分配。

1.1.3 计算机架构

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

·单指令单数据(SISD)

·单指令多数据(SIMD)

·多指令单数据(MISD)

·多指令多数据(MIMD)
这里不多赘述,并行计算入门都会提到

计算机架构也能根据内存组织方式进行进一步划分,一般可以分成下面两种类型。

·分布式内存的多节点系统

·共享内存的多处理器系统

在多节点系统中,大型计算引擎是由许多网络连接的处理器构成

的。每个处理器有自己的本地内存,而且处理器之间可以通过网络进行

通信。

“众核”(many-core)通常是指有很多核心(几十或几百个)的多核

架构。近年来,计算机架构正在从多核转向众核。

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

1.2 异构计算

最初,计算机只包含用来运行编程任务的中央处理器(CPU)。近年来,高性能计算领域中的主流计算机不断添加了其他处理元素,其中最主要的就是GPU。

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

1.2.1 异构架构

一个典型的异构计算节点包括两个多核CPU插槽和两个或更多个的众核GPU。

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

·主机代码

·设备代码

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

以下是描述GPU容量的两个重要特征。

CUDA核心数量

内存大小

相应的,有两种不同的指标来评估GPU的性能。

峰值计算性能

内存带宽

性能具体如何衡量也不再进行详细解释

1.2.2 异构计算范例

对于特定的程序来说,每种计算方法都有它自己的优点。CPU计算适合处理控制密集型任务,GPU计算适合处理包含数据并行的计算密集型任务。

一个问题有较小的数据规模、复杂的控制逻辑和/或很少的并行性,那么最好选择CPU处理该问题,因为它有处理复杂逻辑和指令级并行性的能力。

相反,如果该问题包含较大规模的待处理数据并表现出大量的数据并行性,那么使用GPU是最好的选择。

因为CPU和GPU的功能互补性导致了CPU+GPU的异构并行计算架构的发展,这两种处理器的类型能使应用程序获得最佳的运行效果。
如今超算中大多使用这种异构架构,控制核+加速核的形式(除日本一个超算外,仍使用同构架构),我们国家的超算也正加速发展中!(用了好几个超算服务器的感受…)

CPU线程与GPU线程

CPU上的线程通常是重量级的实体。操作系统必须交替线程使用启用或关闭CPU执行通道以提供多线程处理功能。上下文的切换缓慢且开销大

GPU上的线程是高度轻量级的。在一个典型的系统中会有成千上万的线程排队等待工作。

CPU的核被设计用来尽可能减少一个或两个线程运行时间的延迟,而GPU的核是用来处理大量并发的、轻量级的线程,以最大限度地提高吞吐量。

1.2.3 CUDA:一种异构计算平台

CUDA是一种通用的并行计算平台和编程模型,CUDA C是标准ANSI C语言的一个扩展,它带有的少数语言扩展功能使异构编程成为可能,同时也能通过API来管理设备、内存和其他任务。

CUDA提供了两层API来管理GPU设备和组织线程:

·CUDA驱动API

·CUDA运行时API

运行时API和驱动API之间没有明显的性能差异。在设备端,内核是如何使用内存以及你是如何组织线程的,对性能有更显著的影响。这两种API是相互排斥的,必须使用两者之一,从两者中混合函数调用是不可能的。本书所有内容均使用运行时API。

主机代码是标准的C代码,使用C编译器进行编译。设备代码,也就是核函数,是用扩展的带有标记数据并行函数关键字的CUDA C语言编写的。设备代码通过nvcc进行编译。

1.3 用GPU输出Hello World

使用示例代码hello.cu

1
2
3
4
5
6
7
8
9
10
11
12
13
__global__ void helloFromGPU()
{
printf("Hello World from GPU!\n");
}

int main(int argc, char **argv)
{
printf("Hello World from CPU!\n");

helloFromGPU<<<1, 10>>>();
CHECK(cudaDeviceReset());
return 0;
}

得到结果如下:

对初学者来说要注意:修饰符__global__告诉编译器这个函数将会从CPU中调用,然后在GPU上执行。用下面的代码启动内核函数。

三重尖括号意味着从主线程到设备端代码的调用。里面的参数后面章节会提到

1
helloFromGPU<<<1, 10>>>();

CUDA编程结构

一个典型的CUDA编程结构包括5个主要步骤。

1.分配GPU内存。

2.从CPU内存中拷贝数据到GPU内存。

3.调用CUDA内核函数来完成程序指定的运算。

4.将数据从GPU拷回CPU内存。

5.释放GPU内存空间。