像其他专业行话一样,并行计算也有自己的行话。行话就像个大坑,坑中的人需要在其中浸淫很久,才能逐渐适应其语境,然而很多行话的使用常常是草率与不精确的。有时候把鬼都听不懂的行话理解了,再跟别人说鬼话,大概也是开启第二天性的方法。(按照农历来算,这个月是鬼月)
一、并行计算内存与CPU结构
统一内存访问 (UMA)
- 目前最常由对称多处理器 (SMP) 机器表示
- 相同的处理器
- 对内存的访问和访问时间相等
- 有时称为 CC-UMA - 缓存相干 UMA。缓存一致性意味着如果一个处理器更新了共享内存中的位置,则所有其他处理器都知道该更新,缓存一致性是在硬件级别实现的。
非一致性内存访问 (NUMA)
- 通常通过物理链接两个或多个 SMP 来实现
- 一个 SMP 可以直接访问另一个 SMP 的内存
- 并非所有处理器对所有内存的访问时间都相等
- 跨链路的内存访问速度较慢
- 如果保持了缓存一致性,那么也可以称为 CC-NUMA - 缓存一致性 NUMA
分布式内存
- 与共享内存系统一样,分布式内存系统差异很大,但有一个共同的特性。分布式内存系统需要一个通信网络来连接处理器间内存。
- 处理器有自己的本地内存。一个处理器中的内存地址不会映射到另一个处理器,因此所有处理器之间没有全局地址空间的概念。
- 由于每个处理器都有自己的本地内存,因此它可以独立运行。它对其本地内存所做的更改对其他处理器的内存没有影响。因此,缓存一致性的概念不适用。
- 当一个处理器需要访问另一个处理器中的数据时,程序员的任务通常是明确定义数据的通信方式和时间。任务之间的同步同样是程序员的责任。
- 用于数据传输的网络“结构”差异很大,尽管它可以像以太网一样简单。
混合分布式共享内存
- 共享内存组件可以是共享内存机和/或图形处理单元 (GPU)。
- 分布式内存组件是多个共享内存/GPU 机器的联网,这些机器只知道自己的内存,而不知道另一台机器上的内存。因此,需要网络通信才能将数据从一台机器移动到另一台机器。
- 目前的趋势似乎表明,在可预见的未来,这种类型的内存架构将继续盛行,并在计算的高端增加。
- 当今世界上最大、最快的计算机采用共享和分布式内存架构。
二、并行计算经典分类方法
- 自 1966 年以来一直使用的更广泛使用的分类法之一称为弗林分类法。
- 弗林分类法根据多处理器计算机架构如何沿着指令流和数据流这两个独立的维度进行分类来区分多处理器计算机架构。这些维度中的每一个都只能具有两种可能状态中的一种:单个或多个。
- 根据弗林说法,下面的矩阵定义了 4 种可能的分类:
单指令,单数据 (SISD)
- 串行(非并行)计算机
- 单条指令:在任何一个时钟周期内,CPU 只执行一个指令流
- 单一数据:在任何一个时钟周期中,只有一个数据流被用作输入
- 确定性执行
- 这是最古老的计算机类型
- 示例:老一代大型机、小型计算机、工作站和单处理器/核心 PC。
单指令多数据 (SIMD)
- 一种并行计算机
- 单条指令:所有处理单元在任何给定的时钟周期内执行相同的指令
- 多个数据:每个处理单元可以对不同的数据元素进行操作
- 最适合于具有高度规律性的特殊问题,例如图形/图像处理。
- 同步(锁步)和确定性执行
- 两种类型:处理器阵列和矢量管道
多指令、单数据 (MISD)
- 一种并行计算机
- 多指令:每个处理单元通过单独的指令流独立地对数据进行操作。
- 单一数据:单个数据流被馈送到多个处理单元。
- 这类并行计算机的实际例子很少(如果有的话)。
- 一些可以想象的用途可能是:
- 在单个信号流上运行多个频率滤波器
- 多种加密算法试图破解单个编码消息。
多指令、多数据 (MIMD)
- 一种并行计算机
- 多指令: 每个处理器可能正在执行不同的指令流
- 多个数据:每个处理器可能正在处理不同的数据流
- 执行可以是同步的或异步的,确定性的或非确定性的
- 目前,最常见的并行计算机类型 - 大多数现代超级计算机都属于这一类。
- 示例:大多数当前的超级计算机、联网的并行计算机集群和“网格”、多处理器 SMP 计算机、多核 PC。
- 注意许多 MIMD 架构还包括 SIMD 执行子组件
三、并行编程模型
并行编程模型作为硬件和内存架构之上的抽象而存在。
- 通常使用几种并行编程模型:
- 共享内存(无线程)
- 线程
- 分布式内存/消息传递
- 数据并行
- 混合
- 单程序多数据 (SPMD)
- 多程序多数据 (MPMD)
共享内存模型
- 在此编程模型中,进程/任务共享一个公共地址空间,它们异步读取和写入该地址空间。
- 各种机制(如锁/信号量)用于控制对共享内存的访问、解决争用以及防止竞争条件和死锁。
- 这可能是最简单的并行编程模型。
- 从程序员的角度来看,这个模型的一个优点是缺乏数据“所有权”的概念,因此不需要明确指定任务之间的数据通信。所有进程都能看到共享内存,并具有对共享内存的平等访问权限。程序开发通常可以简化。
- 就性能而言,一个重要的缺点是,理解和管理数据局部性变得更加困难:
- 将数据保留在处理它的进程的本地,可以节省内存访问、缓存刷新和当多个进程使用相同数据时发生的总线流量。
- 不幸的是,控制数据局部性很难理解,并且可能超出普通用户的控制范围。
在独立的共享内存计算机上,本机操作系统、编译器和/或硬件为共享内存编程提供支持。例如,POSIX标准提供了一个使用共享内存的API,UNIX提供了共享内存段(shmget、shmat、shmctl等)。
线程模型
- 此编程模型是一种共享内存编程。
- 在并行编程的线程模型中,单个“重量级”进程可以有多个“重量级”并发执行路径。
- 例如:
- 主程序 a.out 计划由本地操作系统运行。a.out 加载并获取运行所需的所有系统和用户资源。这就是“重量级”的过程。
- a.out执行一些串行工作,然后创建一些任务(线程),这些任务(线程)可以由操作系统同时调度和运行。
- 每个线程都有本地数据,但也共享 a.out 的全部资源。这样可以节省与为每个线程复制程序资源相关的开销(“轻量级”)。每个线程还受益于全局内存视图,因为它共享 a.out 的内存空间。
- 线程的工作最好描述为主程序中的子程序。任何线程都可以与其他线程同时执行任何子程序。
- 线程通过全局内存(更新地址位置)相互通信。这需要同步构造,以确保多个线程不会在任何时候更新相同的全局地址。
- 线程可以来来去去,但 a.out 仍然存在,以提供必要的共享资源,直到应用程序完成。
实现
- 从编程的角度来看,线程实现通常包括:
- 从并行源代码中调用的子例程库
- 嵌入在串行或并行源代码中的一组编译器指令
在这两种情况下,程序员都负责确定并行度(尽管编译器有时可以提供帮助)。
- 线程实现在计算领域并不新鲜。从历史上看,硬件供应商已经实现了自己的专有线程版本。这些实现彼此之间有很大的不同,使得程序员很难开发可移植的线程应用程序。
- 不相关的标准化工作导致了两种截然不同的线程实现:POSIX 线程和 OpenMP。
POSIX
- 由 IEEE POSIX 1003.1c 标准 (1995) 指定。仅限 C 语言。
- Unix/Linux 操作系统的一部分
- 基于库
- 通常称为 Pthreads。
- 非常明确的并行性;要求程序员非常注意细节。
OpenMP
- 行业标准,由一组主要的计算机硬件和软件供应商、组织和个人共同定义和认可。
- 基于编译器指令
- 可移植/多平台,包括 Unix 和 Windows 平台
- 在 C/C++ 和 Fortran 实现中可用
- 可以非常容易和简单地使用 - 提供“增量并行性”。可以从序列号开始。
- 其他线程实现很常见,但此处不讨论:
- Microsoft 线程
- Java、Python 线程
- GPU 的 CUDA 线程
分布式内存/消息传递模型
- 此模型演示了以下特征:
- 在计算过程中使用自己的本地内存的一组任务。多个任务可以驻留在同一台物理机器上和/或跨任意数量的机器。
- 任务通过发送和接收消息来通过通信交换数据。
- 数据传输通常需要每个进程执行协同操作。例如,发送操作必须具有匹配的接收操作。
实现:
- 从编程的角度来看,消息传递实现通常包含一个子例程库。对这些子例程的调用嵌入在源代码中。程序员负责确定所有并行度。
- 从历史上看,自 1980 年代以来,已经有各种消息传递库可用。这些实现方式彼此之间有很大的不同,使得程序员难以开发可移植的应用程序。
- 1992 年,MPI 论坛成立,其主要目标是为消息传递实现建立标准接口。
- 消息传递接口 (MPI) 的第 1 部分于 1994 年发布。第 2 部分 (MPI-2) 于 1996 年发布,MPI-3 于 2012 年发布。所有 MPI 规范均可在 MPI Documents 的网站上找到。
- MPI 是消息传递的“事实上”的行业标准,几乎取代了用于生产工作的所有其他消息传递实现。几乎所有流行的并行计算平台都存在 MPI 实现。并非所有实现都包含 MPI-1、MPI-2 或 MPI-3 中的所有内容。
数据并行模型
- 也可以称为分区全局地址空间 (PGAS) 模型。
- 数据并行模型演示了以下特征:
- 地址空间是全局处理的
- 大多数并行工作都侧重于对数据集执行操作。数据集通常被组织成一个通用的结构,如数组或多维数据集。
- 一组任务共同处理相同的数据结构,但是,每个任务都处理同一数据结构的不同分区。
- 任务对其工作分区执行相同的操作,例如,“向每个数组元素添加 4”。
- 在共享内存架构上,所有任务都可以通过全局内存访问数据结构。
- 在分布式内存架构上,全局数据结构可以在逻辑上和/或物理上跨任务进行拆分。
实现:
- 目前,基于数据并行/PGAS模型,在不同的开发阶段有几种并行编程实现。
- Coarray Fortran:Fortran 95 的一小部分扩展,用于 SPMD 并行编程。依赖于编译器。详细信息:https://en.wikipedia.org/wiki/Coarray_Fortran
- 统一并行 C (UPC):对 C 编程语言的扩展,用于 SPMD 并行编程。依赖于编译器。详细信息:https://upc.lbl.gov/
- Global Arrays:在分布式数组数据结构的上下文中提供共享内存样式的编程环境。具有 C 和 Fortran77 绑定的公共域库。详细信息:https://en.wikipedia.org/wiki/Global_Arrays
- X10:一种基于PGAS的并行编程语言,由IBM在Thomas J. Watson研究中心开发。详细信息:The X10 Programming Language
- Chapel:一个由 Cray 领导的开源并行编程语言项目。详细信息:http://chapel.cray.com/
混合模型
- 混合模型结合了多个前面描述的编程模型。
- 目前,混合模型的一个常见示例是消息传递模型 (MPI) 与线程模型 (OpenMP) 的组合。
- 线程使用本地节点数据执行计算密集型内核
- 不同节点上的进程之间的通信使用 MPI 通过网络进行
- 这种混合模型非常适合最流行的(当前)集群多核/众核计算机的硬件环境。
- 另一个类似且越来越流行的混合模型示例是将 MPI 与 CPU-GPU(图形处理单元)编程结合使用。
- MPI 任务在使用本地内存的 CPU 上运行,并通过网络相互通信。
- 计算密集型内核被卸载到节点上的 GPU。
- 节点本地内存和 GPU 之间的数据交换使用 CUDA(或等效工具)。
- 其他混合模型很常见:
- 带 Pthreads 的 MPI
- 具有非 GPU 加速器的 MPI