并行计算的内存架构 SIMD 单指令流多处理器 GPU 并行编程模型 并行算法
小结:
1、
2. 并行计算的内存架构 — python-parallel-programming-cookbook-cn 1.0 文档 https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/chapter1/02_The-parallel-computing-memory-architecture.html
并行的程序也是使用这种方法,用多个处理器同时工作,来完成同一个任务。每一个处理器都做自己的那部分工作(独立的部分)。而且计算过程中处理器之间可能需要交换数据。如果,软件应用要求越来越高的计算能力。提高计算能力有两种思路:提高处理器的时钟速度或增加芯片上的核心数。提高时钟速度就必然会增加散热,然后每瓦特的性能就会降低,甚至可能要求特殊的冷却设备。提高芯片的核心数是更可行的一种方案,因为能源的消耗和散热,第一种方法必然有上限,而且计算能力提高没有特别明显。
为了解决这个问题,计算机硬件供应商的选择是多核心的架构,就是在同一个芯片上放两个或者多个处理器(核心)。GPU制造商也逐渐引进了这种基于多处理器核心的硬件架构。事实上,今天的计算机几乎都是各种多核、异构的计算单元组成的,每一个单元都有多个处理核心。
所以,对我们来说充分利用计算资源就显得至关重要,例如并行计算的程序、技术和工具等。
2. 并行计算的内存架构
根据指令的同时执行和数据的同时执行,计算机系统可以分成以下四类:
- 单处理器,单数据 (SISD)
- 单处理器,多数据 (SIMD)
- 多处理器,单数据 (MISD)
- 多处理器,多数据 (MIMD)
这种分类方法叫做“费林分类”:
2.1. SISD
单处理器单数据就是“单CPU的机器”,它在单一的数据流上执行指令。在SISD中,指令被顺序地执行。
对于每一个“CPU时钟”,CPU按照下面的顺序执行:
- Fetch: CPU 从一片内存区域中(寄存器)获得数据和指令
- Decode: CPU对指令进行解码
- Execute: 该执行在数据上执行,将结果保存在另一个寄存器中
当Execute阶段完成之后,CPU回到步骤1准备执行下一个时钟循环。
![../_images/SISD-schema.png](https://img2023.cnblogs.com/blog/1006507/202304/1006507-20230426050233316-1708761417.png)
运行在这些计算机上的算法是顺序执行的(连续的),不存在任何并行。只有一个CPU的硬件系统就是SISD的例子。
这种架构(冯·诺依曼体系)的主要元素有以下:
- 中心内存单元:存储指令和数据
- CPU:用于从内存单元获得指令/数据,对指令解码并顺序执行它们
- I/O系统:程序的输入和输出流
传统的单处理器计算机都是经典的SISD系统。下图表述了CPU在Fetch、Decode、Execute的步骤中分别用到了哪些单元:
![../_images/cpu-phase.png](https://img2023.cnblogs.com/blog/1006507/202304/1006507-20230426050232860-1375789538.png)
2.2. MISD
这种模型中,有n个处理器,每一个都有自己的控制单元,共享同一个内存单元。在每一个CPU时钟中,从内存获得的数据会被所有的处理器同时处理,每一个处理器按照自己的控制单元发送的指令处理。在这种情况下,并行实际上是指令层面的并行,多个指令在相同的数据上操作。能够合理利用这种架构的问题模型比较特殊,例如数据加密等。因此,MISD在现实中并没有很多用武之地,更多的是作为一个抽象模型的存在。
![../_images/MISD.png](https://img2023.cnblogs.com/blog/1006507/202304/1006507-20230426050233546-1984836666.png)
2.3. SIMD
SIMD计算机包括多个独立的处理器,每一个都有自己的局部内存,可以用来存储数据。所有的处理器都在单一指令流下工作;具体说,就是有n个数据流,每个处理器处理一个。所有的处理器同时处理每一步,在不同的数据上执行相同的指令。这是一个数据并行的例子。SIMD架构比MISD架构要实用的多。很多问题都可以用SIMD计算机的架构来解决。这种架构另一个有趣的特性是,这种架构的算法非常好设计,分析和实现。限制是,只有可以被分解成很多个小问题(小问题之间要独立,可以不分先后顺序被相同的指令执行)的问题才可以用这种架构解决。很多超级计算机就是使用这架构设计出来的。例如Connection Machine(1985年的 Thinking Machine)和MPP(NASA-1983).我们在第六章 GPU Python编程中会接触到高级的现代图形处理器(GPU),这种处理器就是内置了很多个SIMD处理单元,使这种架构在今天应用非常广泛。
2.4. MIMD
在费林分类中,这种计算机是最广泛使用、也是最强大的一个种类。这种架构有n个处理器,n个指令流,n个数据流。每一个处理器都有自己的控制单元和局部内存,让MIMD架构比SIMD架构的计算能力更强。每一个处理器都在独立的控制单元分配的指令流下工作;因此,处理器可以在不同的数据上运行不同的程序,这样可以解决完全不同的子问题甚至是单一的大问题。在MIMD中,架构是通过线程或进程层面的并行来实现的,这也意味着处理器一般是异步工作的。这种类型的计算机通常用来解决那些没有统一结构、无法用SIMD来解决的问题。如今,很多计算机都应用了这中间架构,例如超级计算机,计算机网络等。然而,有一个问题不得不考虑:异步的算法非常难设计、分析和实现。
3. 内存管理 — python-parallel-programming-cookbook-cn 1.0 文档 https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/chapter1/03_Memory_organization.html
3. 内存管理
内存管理是并行架构需要考虑的另一方面,确切来说是获得数据的方式。无论处理单元多快,如果内存提供指令和数据的速度跟不上,系统性能也不会得到提升。制约内存达到处理器速度级别的响应时间的主要因素是内存存取周期。所谓存取周期就是连续启动两次读或写操作所需间隔的最小时间。处理器的周期通常比内存周期短得多。当处理器传送数据到内存或从内存中获取数据时,内存依旧在一个周期中,其他任何设备(I/O控制器,处理器)都不能使用内存,因为内存必须先对上一个请求作出响应。
为了解决 MIMD 架构访问内存的问题,业界提出了两种内存管理系统。第一种就是人们所熟知的共享内存系统,共享内存系统有大量的虚拟内存空间,而且各个处理器对内存中的数据和指令拥有平等的访问权限。另外一种类型是分布式内存模型,在这种内存模型中,每个处理器都有自己专属的内存,其他处理器都不能访问。共享内存和分布式内存的区别以处理器的角度来说就是内存和虚拟内存体系的不同。每个系统的内存都会分为能独立访问的不同部分。共享内存系统和分布式内存系统的处理单元管理内存访问的方式也不相同。 load R0,i
指令意味着将 i
内存单元的内容加载进 R0
寄存器,但内存管理方式的不同,处理器的处理方式也不尽相同。在共享内存的系统中, i
代表的是内存的全局地址,对系统中的所有处理器来说都指向同一块内存空间。如果两个处理器想同时执行该内存中的指令,它们会向 R0
寄存器载入相同的内容。在分布式内存系统中, i
是局部地址。如果两个处理器同时执行向 R0
载入内容的语句,执行结束之后,不同处理器 R0
寄存器中的值一般情况下是不一样的,因为每个处理器对应的内存单元中的 i
代表的全局地址不一样。对于程序员来说,必须准确的区分共享内存和分布式内存,因为在并行编程中需要考量内存管理方式来决定进程或线程间通讯的方式。对于共享内存系统来说,共享内存能够在内存中构建数据结构并在子进程间通过引用直接访问该数据结构。而对于分布式内存系统来说,必须在每个局部内存保存共享数据的副本。一个处理器会向其他处理器发送含有共享数据的消息从而创建数据副本。这使得分布式内存管理有一个显而易见的缺点,那就是,如果要发送的消息太大,发送过程会耗费相对较长的时间。
3.1. 共享内存
下图展示了共享内存多处理器系统的架构,这里只展示了各部件之间简单的物理连接。总线结构允许任意数量的设备共享一个通道。总线协议最初设计是让单处理器,一个或多个磁盘和磁带控制器通过共享内存进行通讯。可以注意到处理器拥有各自的Cache,Cache中保存着局部内存中有可能被处理器使用的指令或数据。可以想象一下,当一个处理器修改了内存中的数据,同时另外一个处理器正在使用这个数据时,就会出现问题。已修改的值会从处理器的Cache传递到共享内存中,接着,新值会传递到其他处理器的Cache中,其它处理器就不可以使用旧值进行计算。这就是人们所熟知的Cache一致性问题,是内存一致性问题的一种特殊情况,要解决这个问题需要硬件能像多进程编程一样实现处理并发问题 和同步控制 。
![../_images/Page-8-Image-1.png](https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/_images/Page-8-Image-1.png)
共享内存系统的主要特性如下:
- 内存对于所有处理器来说是一样的,例如,所有处理器所对应的相同数据结构都存在于相同的逻辑地址,也就是说可以从相同的内存单元中获得该数据结构。
- 通过控制处理器对共享内存的访问权限可以达到同步控制的效果。实际上,每次只有一个处理器拥有对内存资源的访问权限。
- 当一个任务正在访问共享内存的时候,其它所有任务都不能改变内存单元的内容。
- 共享内存很快,两个任务通讯的时间和读取单个内存单元的时间相等(取决于内存的访问速度)
在共享内存系统中访问内存的方式如下
- 均匀内存访问 (Uniform memory access (UMA) ):这类系统的基本特征是无论对于处理器来说访问任意的内存区域速度是相同的。因此,这些系统也成为对称式多处理器 (symmetric multiprocessor (SMP)) 系统。这类系统实现起来相对简单,但是可扩展性较差,程序员需要通过插入适当的控制、信号量、锁等机制来管理同步,进而在程序中管理资源。
- 非均匀内存访问 (Non-uniform memory access (NUMA)):这类架构将内存分为高速访问区域和低速访问区域。高速访问区域是分配给各个处理器的区域,是用于数据交换的公用区域。这类系统也称为分布式共享内存系统 (Distributed Shared Memory Systems (DSM)) ,这类系统的扩展性很好,但开发难度较高。
- 无远程内存访问 (No remote memory access (NORMA)):对于处理器来说内存在物理上是分布式存在的。每个处理器只能访问其局部私有内存。处理器之间通过消息传递协议进行通讯。
- 仅Cache可访问 (Cache only memory access (COMA)):这类系统中仅有Cache内存。分析 NUMA 架构时,需要注意的是这类系统会把数据的副本保存在Cache中供处理器使用,并且在主存中也保留着重复的数据。COMA 架构可以移除重复的主存数据,而只保留Cache内存。对于处理器来说内存在物理上是分布式存在的。每个处理器只能访问其局部私有内存。处理器之间通过消息传递协议进行通讯。
3.2. 分布式内存
在使用分布式内存的系统中,各个处理器都有其各自的内存,而且每个处理器只能处理属于自己的内存。某些学者把这类系统称为“多计算机系统”,这个名字很真实地反映了组成这类系统的元素能够独立作为一个具有内存和处理器的微型系统,如下图所示:
![../_images/Page-10-Image-1.png](https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/_images/Page-10-Image-1.png)
这种内存管理方式有几个好处。第一,总线和开关级别的的通讯不会发生冲突。每个处理器都可以无视其他处理器的干扰而充分利用局部内存的带宽;第二,没有通用总线意味着没有处理器数量的限制,系统的规模只局限于连接处理器的网络带宽;第三,没有Cache一致性问题的困扰。每个处理器只需要处理属于自己的数据而无须关心上传数据副本的问题。但最大的缺点是,很难实现处理器之间的通讯。如果一个处理器需要其他处理器的数据,这两个处理器必须要通过消息传递协议来交换消息。这样进行通讯会导致速度降低,原因有两个,首先,从一个处理器创建和发送消息到另外一个处理器需要时间;其次,任何处理器都需要停止工作,处理来自其他处理器的消息。面向分布式内存机器的程序必须按照尽量相互独立的任务来组织,任务之间通过消息进行通讯。
![../_images/Page-11-Image-1.png](https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/_images/Page-11-Image-1.png)
分布式内存系统的特性如下:
- 内存通常分布在不同的处理器之中,局部内存只能由对应的处理器访问。
- 同步控制通过在处理器之间转移数据 (也可以是消息本身) 来实现, 同理通讯的实现方式也一样。
- 局部内存的数据分支会影响机器的性能——有必要精确地进行数据分割最小化 CPU 间的通讯。另外,协调数据的分解合成操作的处理器必须与处理部分数据的处理器高效地通讯。
- 消息传递协议用于 CPU 间通过交换数据包通讯。消息是信息的分解单元,他们经过良好的定义,所以处理器之间能够准确地识别出消息地内容。
大规模并行处理 (Massively parallel processing (MPP))
MPP 机器由上百个处理器 (在一些机器中达到成千上万个) 通过通讯网络连接而成。世界上最快的计算机几乎都基于这种架构,采用这种架构的计算机系统有:Earth Simulator, Blue Gene, ASCI White, ASCI Red, ASCI Purple 以及 Red Storm 等。
工作站集群
工作站集群是指将传统的计算机通过通讯网络连接起来。在集群架构中,一个节点就是集群中的一个计算单元。对于用户来说,集群是完全透明的,掩盖了软硬件的复杂性,使得数据以及应用仿佛从一个节点中得到的。
在这里,会定义三种集群:
- 故障切换集群 (The fail-over cluster) :在这类集群中,会持续检测节点的活动状态,当一个节点出现故障,另外一台机器会马上接管故障节点的工作。这类集群通过这种冗余架构可以保证系统的可用性。
- 负载均衡集群 (The load balancing cluster) :在这类系统中,会将一个作业请求发送给负载较小的节点上执行。这样做可以减少整个处理过程所耗费的时间。
- 高性能计算集群 (The high-performance cluster) :在这类系统中,每个节点都可以提供极高的性能,一个任务依旧分解为若干个子任务交给各个节点处理。任务是并行化的,分配给不同的机器进行处理。
异构架构
在同构的超级计算机中采用GPU加速器改变了之前超级计算机的使用规则。即使GPU能够提供高性能计算,但是不能把它看作一个独立的处理单元,因为GPU必须在CPU的配合下才能顺利完成工作。因此,异构计算的程序设计方法很简单,首先CPU通过多种方式计算和控制任务,将计算密集型和具有高并行性的任务分配给图形加速卡执行。CPU和GPU之间不仅可以通过高速总线通讯,也可以通过共享一块虚拟内存或物理内存通讯。事实上,在这类设备上GPU和CPU都没有独立的内存区域,一般是通过由各种编程框架(如CUDA,OpenCL)提供的库来操作内存。这类架构被称之为异构架构,在这种架构中,应用程序可以在单一的地址空间中创建数据结构,然后将任务分配给合适的硬件执行。通过原子性操作,多个任务可以安全地操控同一个内存区域同时避免数据一致性问题。所以,尽管CPU和GPU看起来不能高效联合工作,但通过新的架构可以优化它们之间的交互和提高并行程序的性能。
4. 并行编程模型 — python-parallel-programming-cookbook-cn 1.0 文档 https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/chapter1/04_Parallel_programming_models.html
4. 并行编程模型
并行编程模型是作为对硬件和内存架构的抽象而存在的。事实上,这些模式不是特定的,而且和机器的类型或内存的架构无关。他们在理论上能在任何类型的机器上实现。相对于前面的架构细分,这些编程模型会在更高的层面上建立,用于表示软件执行并行计算时必须实现的方式。为了访问内存和分解任务,每一个模型都以它独自的方式和其他处理器共享信息。
需要明白的是没有最好的编程模型,模型的效果如何很大程度上取决于实际的问题。使用范围最广的并行编程模型有:
- 共享内存模型
- 多线程模型
- 分布式内存/消息传递模型
- 数据并行模型
在这节中,会描述这些编程模型的概览。在下一章会更加准确的描述这些编程模型,并会介绍Python中实现这些模型的相应模块。
4.1. 共享内存模型
在这个编程模型中所有任务都共享一个内存空间,对共享资源的读写是 异步的。系统提供一些机制,如锁和信号量,来让程序员控制共享内存的访问权限。使用这个编程模型的优点是,程序员不需要清楚任务之间通讯的细节。但性能方面的一个重要缺点是,了解和管理数据区域变得更加困难;将数据保存在处理器本地才可以节省内存访问,缓存刷新以及多处理器使用相同数据时发生的总线流量。
4.2. 多线程模型
在这个模型中,单个处理器可以有多个执行流程,例如,创建了一个顺序执行任务之后,会创建一系列可以并行执行的任务。通常情况下,这类模型会应用在共享内存架构中。由于多个线程会对共享内存进行操作,所以进行线程间的同步控制是很重要的,作为程序员必须防止多个线程同时修改相同的内存单元。现代的CPU可以在软件和硬件上实现多线程。POSIX 线程就是典型的在软件层面上实现多线程的例子。Intel 的超线程 (Hyper-threading) 技术则在硬件层面上实现多线程,超线程技术是通过当一个线程在停止或等待I/O状态时切换到另外一个线程实现的。使用这个模型即使是非线性的数据对齐也能实现并行性。
4.3. 消息传递模型
消息传递模型通常在分布式内存系统(每一个处理器都有独立的内存空间)中应用。更多的任务可以驻留在一台或多台物理机器上。程序员需要确定并行和通过消息产生的数据交换。实现这个数据模型需要在代码中调用特定的库。于是便出现了大量消息传递模型的实现,最早的实现可以追溯到20世纪80年代,但直到90年代中期才有标准化的模型——实现了名为MPI (the Message Passing Interface, 消息传递接口)的事实标准。MPI 模型是专门为分布式内存设计的,但作为一个并行编程模型,也可以在共享内存机器上跨平台使用。
![../_images/Page-15-Image-1.png](https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/_images/Page-15-Image-1.png)
4.4. 数据并行模型
在这个模型中,有多个任务需要操作同一个数据结构,但每一个任务操作的是数据的不同部分。在共享内存架构中,所有任务都通过共享内存来访问数据;在分布式内存架构中则会将数据分割并且保存到每个任务的局部内存中。为了实现这个模型,程序员必须指定数据的分配方式和对齐方式。现代的GPU在数据已对齐的情况下运行的效率非常高。