5.1 引言
在共享内存环境中,独立的控制线程可以竞相修改单个位置。为程序以可预测的方式运行,程序员必须用同步来控制这些竞争。
“内存一致性模型”或“内存模型”定义了并行代理之间通信的基本规则。当这些规则含糊不清地定义或者更糟的是完全不存在的时候,困难的任务变得更加困难。事实上,如果没有明确定义的内存模型,编写正确的、可移植的代码通常是不可能的。
直到2011年,C和C++才有定义明确的内存模型。在此之前,实现依赖于编译器的特定操作或者甚至特定于目标的操作来对内存进行排序。因此,编写可移植代码非常困难。
早期的异构编程模型也采取了类似的路径;第一个可用的可用的平台没有指定一个内存模型。早期的采用者不得不依靠语言和硬件实现的知识来创建代理间通信的代码。
5.2 HSA内存结构
所有编写其他异构编程模型的人都希望HSA的内存组织能够高效地支持GPU。它旨在使高性能算法能够以最少的通信量高效运行,而不会产生不必要的硬件成本或复杂性,同时为更苛刻的算法提供强大的内存排序保证。
HSA内存组织可以被看作在单个地址空间内排列的一组内存区域。每个区域的重要性能和性质略有不同。硬件和工具相结合使得有效地将应用程序映射到可用的目标硬件范围。另外,多个段可以使用相同的地址范围但是地址完全分开的物理内存位置。编译器根据它生成的访问位置的指令区分这些段和物理地址。
5.2.1 分段
HSA内存的概念分布在7个不同的段。HSA中可用的段是:全局(单个网格上运行的所有工作项)、组(工作组)、私有(工作项)、只读、溢出、内核参数和参数。前4者通常由程序员直接控制。只读区域是因为一些设备能够使用专门的常量缓冲区以比一般缓存机制更高的性能或更低的功耗访问只读内存。通过将这个地址空间作为核心HSA特性公开,编译器可以生成特殊的指令来有效地完成这个任务。
后3者内存段支持定义编译为HSAIL的HSA程序的底层API。内核参数内存代表了内核参数的集合,执行时视其为只读,并可能由运行时复制到单独的常量内存,以使它们高效可见地调度所有工作项。溢出内存是高级编译器可以通过它分配到HSAIL寄存器并与终止器通信的任何寄存器,它不能或不想分配为HSAIL寄存器,这样内存不会泄露。终止器可以区分表示注册表溢出(即不在工作项之间共享的暂态内存操作)以及可能共享或具有较长生命周期的其他内存操作,以优化结果代码。溢出内存通过HSA运行时不可见。出于内存分配的目的,它被分类为私有内存。参数内存表示用于将函数参数传递到调用函数的位置。参数内存与终止器通信,该位置的生命周期特定于调用/返回过程过程,因此允许以各种方式进行分配,这取决于设备的基础ABI。这与内核参数内存不同,在内核执行过程中,位置是活的。
5.2.2 平面寻址
为了便于使用,并且使高级语言的编译过程变得更加简单,HSAIL的输入不会不会使地址空间显式化,HSA内存模型支持平面地址空间。平面地址空间允许通过地址空间中的实现定义的划分区域来寻址全局、组和私有地址空间中的所有位置。高级语言中的指针可以重用以映射到不同的段。
HSAIL代码中任何给定的操作都明确指出它的源地址空间是什么。如果操作从平面地址空间获取数据,则终止器生成转换操作或硬件必须检查在那个子范围内以及是否是组或私有位置。如果值非空,操作或硬件必须通过减去值不为null的段偏移量重定向到该段,或者如果该值为空,则转换为特定的空值。
5.2.3 共享虚拟寻址
HSA设计的核心功能是共享虚拟内存。共享虚拟内存最基本的部分是在设备之间共享虚拟地址的能力。所有HSA设备和主机进程都需要能够通过内存传递地址。这些地址必须由系统中的所有设备以相同的方式解释。这使得应用程序的可移植性、灵活性和数据结构实现的简单些成为可能。通过避免副本和布局变化,它也提高了效率。
共享虚拟地址允许包含指针的数据结构在HSA设备之间传递。它还允许将指针存储在内存中,而不需要将其缓冲区偏移量。
共享虚拟地址不一定需要内存一致性。然而,从包含指针的数据结结构到包含可以直接更新指针的数据结构的转换是下一节的一小步。这一步使修改立即对其他设备可见。
即使没有一致性,共享虚拟地址也会导致性能上的好处。在较早的系统中,如果不能在设备之间使用相同的虚拟地址,内存可能会在传递到设备时改变地址。主CPU上的内存分配将被复制到GPU的分立内存中。物理位置不仅会改变,而且所指的虚拟地址范围也会改变。指向这个分配中的位置的任何存储的指针将不再有效。
要解决这个问题,指针在单个内存分配中被转换为偏移量。由此产生的基地+偏移地址方案只能轻易地指向一个单一的分配。这意味着应用程序的重大重构以及可能大量的运行时数据重排对于数据结构按照在主机和加速器设备上有效的形式管理是必要的。共享虚拟地址允许代码变得更简单,需要更少的数据转换。
HSA设备的地址空间与主机的地址空间相匹配。另外,虚拟地址空间的一部分由HSA系统软件划分出来,以将平面地址表示成HSA设备上的私有或组内存。虚拟内存还必须满足通常的保护机制,以确保数据与不同进程的分离。
5.2.4 所有权
HSA运行时控制的分配中的一些可以以粗粒度的形式分配。粗粒度分配时共享虚拟寻址的内存分配,但不要求一致性。它们的存在主要有2个原因:
1. 即使在共享虚拟内存和完全控制内存一致性的世界中,离散加速器依然存在。在这样的设备上,使每一个设备一致的操作可能会非常昂贵,因为设备可以通过非常低的带宽或高延迟互联连接到主机。
2. 其次时降低功耗。预计HSA将得到众多设备的支持。在功耗非常低的设备上,即使是片上加速器,内存一致性也变得非常昂贵。细粒度内存一致性系统的额外探测或缓存刷新流量会消耗功率。
为了支持这些优化,粗粒度内存允许在一段时间内将一致性限制在特定的设备上。我们称之为边界所有权。HSA运行时API调用为驱动程序控制提供了更新虚拟内存系统、关闭页面级别的一致性、将数据迁移到新的物理位置以及更新虚拟内存映射或其组合的机会。当地址范围由给定设备拥有时,其内存一致性属性会改变。
5.3 HSA内存一致性基础
程序和系统之间的约定显示了如何编写正确的HSA程序,如何实现正确的终止器优化,以及如何构建正确的符合HSA标准的硬件。
HSA内存一致性模型由2个同样有效的视图。首先,有一个简单的观点,即只考虑HSA程序的“顺序一致”执行。其次,还可以将内存一致性模型视为执行中的部分命令的复杂组合。HSA松弛原子操作可以在有限的情况下提高性能或减少执行能量。
5.3.1 背景:顺序一致性
大多数内存一致性模型(包括HSA)在单个执行单元、线程或代理内保留顺序语义。如果内存操作O1(加载、存储或读取-修改-写入)发生在内存操作O2之前,则O1的作用对O2是可见的。然而,内存模型在它们如何允许来自不同执行单元的内存操作相互交错方面差别很大。最直观的模型是顺序一致性。
顺序一致性保证了在任何有效的执行过程中都有一个全局可观察的加载、内存和读取-修改-写入的交错。得此名称是因为整个程序看起来好像是一个简单的顺序处理器,可以在多个可用线程之间进行多任务处理。
但是顺序一致性模型可能会限制硬件或编译器可以执行的优化。例如,合并存储操作的硬件写入缓冲区(图形处理器中常见的组件)可能会导致不连续一致性的结果。由于局限性,大多数平台(包括罗=HSA)都指定了一个比顺序一致性弱的模型。
5.3.2 背景:冲突和竞争
当读写冲突已经同步时(例如,它们出现在受锁定保护的关键部分中),就会发生“内存竞争”。内存竞争通常是良性的,因为同步将确保它们始终以正确的顺序执行。当读写冲突没有被同步,并且不是同步原语的一部分时,就会发生数据竞争。数据竞争在程序中通常是有害且无意的,因为它们导致不可预知的行为。
5.3.3 单一内存范围的HSA内存模型
任何无数据竞争的HSA程序的执行将出现顺序一致。此外,任何HSA程序的执行都是不确定的。
由于内存模型只定义了无数据竞争的HSA程序的行为,因此我们必须能够精确地定义数据竞争来编写编写正确的程序。为了避免数据竞争,我们必须确保一个程序有足够的同步。为了基本理解,如果在观察到的顺序一致的操作顺序中的冲突操作之间出现HSA同步,则可以说顺序一致的执行中的冲突是同步的。
1. HSA同步操作
在HSA中,通过原子内存操作进行同步。HSA原子是加载、存储或读取-修改-写入,被定义为具有特定语义的、内存范围和段的原子类型。
HSA原子可以释放、获取或获取-释放语义。所有的原子都有副作用,影响与原子相同的指令流中的其他指令如何出现在其他线程中。
通常,具有释放语义的原子可以确保在原子完成之前,原子之前的任何操作对其他线程都是可见的。换句话说,释放可以防止程序顺序中的原子之前的早期操作在原子之后重新排序。
获取与释放相反--具有获取语义的原子确保在原子完成前,原子之后的任何操作对其他线程都可见。获取阻止在程序顺序中的原子在原子之前重新排序之后的后续操作。
为了同步来自两个代理的普通操作,具有释放语义的原子必须被具有获取语义的原子观察到。
2. 通过不同的地址进行传递式同步
HSA中的同步是传递式的。如果A与B同步并且B与C同步,则A被认为与C同步。
3. 寻找竞争
通过确定发生冲突的程序区域来管理问题,然后确保这些冲突始终保持同步。
5.3.4 多个内存范围的HSA内存模型
每个HSA原子操作都指定一个范围。该范围将原子操作及其副作用的可见性限制为系统中工作项或主机线程的子集。
1. 范围动机
传统上,共享内存CPU系统专为低延迟、全通信而设计。这样的系统实现一致性协议,确保来自执行单元的更新自动传播到系统中的所有其他工作项或主机线程。因此,同步是轻量级的。
但是,HSA瞄准的GPU和其他设备都是为了吞吐量而设计的。GPU的许多实现都没有CPU风格的一致性协议,因为它相信这会降低吞吐量。相反,这些设备通过重量级缓存维护操作(如刷新和无效)进行同步。
如果系统不知道那些角色正在进行同步,则必须假定最坏的情况,并刷新无效所有可能持有陈旧数据的缓存。但是,如果同步实体在系统中执行的位置,系统可以减少维护操作的数量。
内存范围的具体实现将有所不同,但一般来说,同步HSA执行层次结构中彼此较为接近的工作项或主机线程的成本较低。同一工作组的两个工作项的同步要比不同工作组的工作项的要快。前者可能只涉及小内部缓冲区的刷新,类似于CPU同步,而后者可能至少涉及L1缓存的刷新和无效。
2. HSA范围
原子的范围是程序员的一个指示,原子只能被执行中的工作项或主机线程的子集观察到。为了获得良好的性能和功耗,HSA程序应该规定最小的可能范围。如果程序员指定的范围小于执行期间实际观察原子或副作用的工作项或线程集合,则该程序将包含数据竞争。
HSA定义了五个范围:工作项、波前、工作组、代理和系统。
3. 使用较小的范围
随着范围的增加,程序员面临着与原子操作同步使用的是哪个范围的附加选择。为了获得最佳性能,应该努力使用仍然导致无竞争执行的最小范围。
为了避免直接范围同步的竞争(同步的所有原子都指定相同的范围),原子的指定范围实例应包含参与同步的所有工作项和主机线程。
由于所有原子指定相同的范围实例的限制,在HSA目标的前瞻性系统中可能会受到限制。因此,HSA提供了两种无竞争方式来使用指定不同范围实例的原子。第一个称为“包含范围式同步”,第二个称为“传递式范围同步”。
(1)范围包含:只要每个原子指定包含两个同步操作的范围实例,工作项或线程就可以通过原子进行同步,而不会导致竞争。当两个范围实例不相同时,称之为范围包含。它命名的原因是:如果两个范围实例不是同一个,那么由于HSA定义的范围实例的严格层次结构,必须包含另一个范围实例。
(2)范围传递
即使工作项或线程在非包含范围实例中使用原子,也可以在不引起竞争的情况下进行同步。
5.3.5 内存段
原子释放-获取将对同步共享虚拟内存中的所有内存段,而不管原子位于何处。
5.3.6 汇总:HSA竞争自由
1. HSA竞争自由的简化定义
有两条经验法则可以帮助简化HSA竞争自由分析:
1. 始终将相同的范围 应用于原子变量(例如,不要在原子A上执行工作组范围释放后再A上执行系统范围内获取)。
2. 从包含所有直接涉及同步的执行单元(即忽略传递性)的最小范围实例中获取/释放。
2. HSA竞争自由的一般定义
在HSA中,冲突既可以是普通的,也可以是原子的:
1. 普通冲突描述了对同一位置的两个操作。至少有一个是写或读取-修改-写入,且至少有一个是普通操作。
2. 原子冲突描述了从不同执行单元到相同位置的两个原子操作。至少有一个是写或读取-修改-写入,它们指定非包含范围实例。
当且仅当所有的冲突都被包含原子同步的传递链分开,HSA程序的顺序一致的执行是无竞争的。当且仅当程序的所有顺序一致性的执行都是无竞争的,HSA程序总体上是无竞争的。
5.3.7 附加观察和注意事项
在HSA中,原子操作不能部分重叠。例如,如果32位原子操作在一个执行单元中写入地址A,而在另一个执行单元中从地址A读取64位原子,则这是HSA中的竞争。对于普通的内存操作,允许部分重叠。
请注意,与同构CPU无竞争模型不同的是,如果冲突由原子分离,则简单地说执行没有竞争是不够的。相反,这些原子也必须正确使用范围(即指定包含范围实例)。
5.4 HSA内存模型中的高级一致性
HSA内存模型进一步扩展了基本的顺序一致的无竞争模型。首先是添加松弛原子和栅栏。它支持粗粒度的内存区域,可能会限制一致性。
5.4.1 松弛原子
HSA内存模型还定义松弛原子核栅栏。松弛原子在两个方面不同于其他原子。首先,松弛原子没有同步的副作用。换句话说,它们不能强迫系统中普通加载或内存的可见性。其次,松弛原子不能保证顺序一致,即使相互之间也是如此,使得松弛原子特别难以推理。
在程序需要读取-修改-写入语义但不尝试同步工作项或线程的情况下,松弛原子是非常有用的。
松弛原子也可以来建立线程之间的因果关系。导致HSA的基本顺序一致的视图不再有效。优点在于可以以相对较低的成本重复执行松弛加载,而对系统上所有内存访问进行排序可能成本显著较高。
为了确保正确性,用户必须通过一系列必须存在任何无竞争HSA执行中的操作命令来推理程序的行为。有两个特别值得注意的命令:
1. 所有加载、存储以及读取-修改-写入更新的总体一致顺序均位于单个位置。
2. 释放、获取和获取-释放原子的顺序总是一致的。
从这些命令中,用户必须推断正式的“发生之前”的关系,以确定在HSA执行竞争中是否有两个冲突的操作。
松弛原子不用步普通的加载和存储。在使用松弛原子的程序中,必须添加栅栏操作来建立松弛原子操作和普通的加载和存储之间的排序关系。事实上,当把一个松弛原子和一个栅栏当做一个简单的对使用时,可以把它们看做一个标准原子的两个部分。松弛原子对应于在单个变量上云霄的标准原子的部分。栅栏对应于同步普通加载和存储的标准原子的副作用。与原子不同,栅栏不与任何特定位置相关联。
通过查分松弛原子和栅栏,程序员可以独立确定因果关系并强制同步。
虽然正常的操作只能阻止在一个方向上穿过栅栏,但它们可以朝另一个方向移动。例如,它们可以通过释放栅栏向上移动,但不能向下移动。松弛原子无法通过栅栏在任何方向移动。
5.4.2 所有权和范围界限
所有权是一种标记为粗粒度的内存区域可以在所有权期限内传递给给定代理人供该组件使用的方法。
这在两个方面与模型的其余部分一起使用:
1. 所属区域内的系统范围原子操作隐式降级为设备范围;
2. 访问不属于自己的可寻址范围是一次竞争。
然而,由于范围传递性,单独限制原子操作的范围并不能保证所需的语义。还必须限制原子排序的可见性副作用。
任何对无主内存区域的访问都不是由所有者访问,而是由所有权传输本身进行。任何访问该区域内未被释放或获取同步的位置都是一次竞争。