gpu并行编程架构

GPU硬件架构不是独立的概念,它是基于cpu并行架构的发展而演变出来的。即是说理解cpu并行架构是理解gpu并行架构的重要基础。

 

当代处理器都是用冯诺依曼提出的处理器架构为工作基础的

处理单元的电子数字计算机由:一个用于进行二进制运算的算术逻辑单元(ALU),一个用来告诉存储指令和数据的寄存器组(processor registers)(一个cpu时钟,几千个字节),一个用来控制指令读取的控制单元(control unit),一个用于存储所有指令和数据的内存,外加一些大容器存储设备及输入输出设备组成。

gpu并行编程架构

开销=延迟(a)+消息长度(N)/带宽(bandwidth)

 

瓶颈解决方案:

缓存,预取,多线程。

 

缓存分为三级缓存

L1工作速度能达到接近处理器的时钟速度,只有16kb或32kb。

L2空间大些,通常约256KB,速度也慢一些

L3大的多,通常几兆字节,单慢很多。

gpu并行编程架构

现代CPU几乎是多核的,L1和L2缓存都是每个核独享的,而L3是所有核共享的。

建议要保持高性能部分的程序足够小以使其能够存储在L2级指令缓存中,同时保持高性能部分的数据足够小且相邻,使其能够存储在L1级数据缓存中。

缓存的局部性原理:时间局部性和空间局部性。之前访问的数据很可能还要再次访问。刚刚访问过的数据附近的数据,可能马上要被访问,

尽量使用顺序访问的数组。

 

 

预取:

各级缓存都有自己的预取器(prefetcher)。编辑器如GCC通过编译阶段修改源代码,在其中插入一些预取指令以实现软件预取。

对于数据的预取是非常复杂的,因为程序对数据的访问往往不是线性的,所以最常用的数据预取是常数步幅模式。如果你对数据的读取是随机跳跃的,预取器是毫无用处的。

分支预测也不能通过预取来解决。编写程序时候尽量考虑指令连续性,可以大大减少延迟,特别是光线追踪这种大量的运算上。

 

指令级并行

指令数/每秒=指令数/时钟周期x时钟周期数/秒

 

步骤:

IF:获取指令阶段

ID/RF:解码指令以及从寄存器获取操作数

EX:执行

MEM:读取内存

WB:写回寄存器

gpu并行编程架构

当程序中的指令出现依赖时,称为一个障碍依赖就是

E=a+b;

F=c+d

M=e*f

M依赖e和f的值

 

 

通常硬件使用三种主要的方法来处理串行指令的并行障碍。

管线气泡

在解码阶段被识别,同时处理器创建一个棋牌占据解码阶段,使当前管线的解码阶段处于空闲等待状态。

gpu并行编程架构

操作数前移

当出现依赖关系时,后面的指令必须等待前面的指令执行完毕并将数据输出寄存器,后面的执行才能继续往下执行。处理器需要探测依赖性的存在,然后根据探测结果判断是需要从寄存器中获取操作数,还是直接通过相关的电路直接获取前一指令的值。

gpu并行编程架构

 

 

乱序执行

基于一个事实,如果后面的指令不依赖与前面的指令,或者说它此时具备执行指令需要的操作数数据,则它可以先于前面的指令被执行。

 

执行顺序:

  1. 获取指令
  2. 将指令分配到一个指令队列
  3. 指令在指令队列等待,直到其输入操作数可用,此时它可以早于自己前面的指令被执行。
  4. 执行指令
  5. 将指令输出结果保存在一个队列中
  6. 只有当一个指令的之前所有指令被执行完毕,才将指令的结果输出到寄存器,以保证执行结果的顺序一致。

 

 

分支预测对指令管线的影响

gpu并行编程架构

等到四个时钟周期A指令计算完毕之后,在第五个时钟周期开始,处理器将重新将正确的EFG指令载入执行单元执行指令计算,同时销毁之前所有关于BCD在寄存器中的数据及其他相关状态。

浮点数比整数的比较要花费更多的时钟周期。

用跳转转移(conditional move)或者无分支选择(branchless select)。

条件转移类似c++中的三元操作数(f0=(f1>=0?f2:f3))

相当于计算了if条件语句的两个分支语句。

gpu并行编程架构

多线程的技术方案:

块多线程技术,这种方案会一直执行一个线程,直至线程遇到很大的延迟(缓存失效,这种延迟可能需要上百个时钟周期)时切换到另一个处于“可执行状态”的线程。

另一种方案是交叉多线程,在每个时钟周期都使用一个不同于上一个时钟周期的线程。这样理想的情况下指令管线中的每个阶段执行的是不同线程中的指令,从而几乎不会有数据依赖管线。他的不足是每个指令阶段都需要额外的计算和存储成本用来追踪这些线程的ID。

这两种方案都是时分多线程TMT

还有一种技术叫同时多线程技术SMT,他可以同时有来自多个线程的指令在执行。SMT并发线程的数量由芯片设计者决定

gpu并行编程架构

英特尔的超线程技术(Hyper-Threading Technology)是一种SMT架构,一个超线程架构的处理器由两个逻辑处理器组成,每个逻辑处理器拥有自己的如架构状态等,但是两个逻辑处理器共享处理器的一些执行资源。

 

每个逻辑处理器拥有的资源包括

  1. 拥有独立的架构状态
  2. 并发的执行自己的指令
  3. 正在执行的指令可以被独立的打断和停止

两个逻辑处理器共享:

  1. 处理器执行引擎以及L1缓存
  2. 共享系统数据总线。

 

 

 

线程级并行

一个最小单元的,可以独立被处理器调用的指令的集合称为一个线程(thread)

多线程技术是指在单个处理器或者一个多核处理器的其中一个核内部,拥有同时执行多个线程的能力。这些线程在内部共享该处理器的各种资源,包括计算单元,寄存器,缓存等。

多线程并不是真正的同时执行,而是通过处理器的控制交叉执行。当当前正在执行的线程遇到缓存失效或其他事件如一个线程需要等待另一个线程的输出结果时,处理器即自动切换到其他处于等待执行状态(即数据已经加载到缓存)的线程执行指令,通过这样保持处理器的繁忙,避免处理器等待数据从主存读取的延迟。

当发生线程切换时,直接在高速的寄存器之间执行赋值和读取即可,而不需要重新从缓存读取数据。现代处理器的线程切换通常可以在一个时钟周期内完成

 

处理器级并行

多处理器架构是指一个计算机系统拥有多个物理的处理器,或者拥有多个核,两者的主要区别为多核处理器每个核仅拥有的L1缓存及寄存器,同一芯片的核共享L2及主内存,而多个单独的物理处理器往往仅共享主存并拥有自己的缓存系统。

非对等多处理器架构ASMP比较典型的例子是Cell处理器架构,有一个常规处理器作为监管处理器,该处理器与大量的高速协作处理器相连。

每个SPE本身就是一个核,通过EIB,SPE之间,以及SPE与PowerPC核之间可以互相通信。

Cell处理器摒弃了传统的内存缓存结构,而是直接将数据和指令直接发送到每个SPE处理器的一个本地私有内存空间LS,SPE直接从LS存取数据,每个SPE拥有一个直接内存存取DMA引擎,用于将LS的数据告诉同步到其他SPE或者PPE内存。

SPE主要聚焦于数据密集型计算,例如傅里叶变换,他们并行性特征很强,每个SPE是完全基于SIMD的数据结构的。它只有一个128位的SIMD寄存器,并用来存储各种数据类型。

gpu并行编程架构

多处理器架构的通信方式可以分为共享内存以及基于网络的消息传递方式

共享内存会随着处理器中核数量的增多,通知的开销迅速增大,使得缓存一致性成为限制处理器中核数不能太多的一个重要因素。

gpu并行编程架构

另一种多处理器架构称为集群,通过将一些独立的通常是廉价的计算机系统,通过网络等方式联通起来,组成一个多处理器系统。然而网络传输的速度很慢,所以处理器之间的通讯具有很大的延迟,更适合线程之间的耦合相对比较弱的计算。

由于缓存一致性的代价,使得共享内存的架构并不太适合高性能的并行计算,所以GPU架构及Cell处理器架构都在避免使用缓存。

 

弗林分类法

SISD,SIMD,MISD,MIMD

gpu并行编程架构

单指令单数据SISD,一个时刻只执行一个任务。

单指令多数据SIMD,指令流并发的广播到多个处理器上,每个处理器拥有各自的数据流,处理器内存只需要一套逻辑来对这个指令流进行解码和执行,而无需多个指令解码通道。可以数据一次性全部从内存中取出,而不是一次只取一个数据项。

并没有笔记搜知名的系统属于多指令但数据MISD,提出他仅属于完整性的缘故。

多指令多数据MIMD,每个处理单元拥有自己的指令流,并且这些指令流操作自己的数据流。绝大多数现代并行系统属于这个类型。

 

Gpu并行计算架构

串行编程模型已经非常成熟,处理器将很多特征隐藏在硬件级别,使编辑器能够对并行编程进行高度抽象,所以程序员不需要去了解处理器架构的一些知识,然而针对gpu的并行编程仍然依赖于程序员对硬件知识有一定的了解,例如GPU使用受程序员托管的内存模型,而不是完全基于硬件托管的内存模型,还有数据的对齐以及内存合并等概念,才能够充分提高并行计算的效率;另外,gpu并没有独立的编程模型,它通常是和cpu一起形成一个非对等的多处理器架构,开发者需要通过cpu来调度和管理gpu设备,内存,以及管理这些设备内的各种状态等等。

多处理器架构完成能够支持并行计算,通过缓存充分利用局部性原理预取指令或数据,大大减少了对数据读取导致延迟的不必要的等待,但是告诉缓存系统的成本非常高,并且占据芯片很大的空间,操作数从主内存到ALU之间的传输需要耗费大量的电能,难以应付大规模的并行计算。

GPU架构和CPU最大的不同在内存系统上。

 

由于gpu硬件不包含自动完成数据替换的逻辑,因此它也可以减少一部分芯片的面积以及能耗以容纳更多的计算单元。

gpu并行编程架构

流处理器族SM相当于一个cpu核,每个sm内部有多个流处理器sp,每个sp用于执行并行计算中的一个独立的线程。Gpu内存分为四种,寄存器,共享内存,常量/纹理内存以及全局内存。

gpu并行编程架构

PCI-E是全双工总线,数据的传入和传出可以同时进行共享有同样的速率,也就是说我们以8GB/s的速度向gpu卡传送数据的同时,还能够以8GB/s的速度从gpu卡接受数据。并不是说可以以16GB/s的速度单向传递。

 

GPU中的主内存称为全局内存,这是因为gpu和cpu都可以对其进行写操作。

gpu并行编程架构

常量以及纹理内存其实只是全局内存的一种虚拟地址形式,他们都是只读内存。他们可以被缓存到纹理内存缓存存储器,这些存储器通常是L1级缓存,可以提供全局内存更快的访问速度。如果缓存没有命中所需的数据,将导致N次对全局内存的访问,而不单是从常量缓存上获取数据,对于那些数据不太集中或数据利用率不高的内存访问,尽量不要使用常量内存。纹理内存还会提供基于硬件的线性插值功能,硬件会基于给定的纹理坐标值获取该纹素附近的多个纹素值,然后基于这些纹素值进行插值计算。纹理可以根据数组索引自动处理边界条件,可以在数据边界按照环绕方式或夹取方式来对纹理数组进行处理。

 

共享内存实际上是一个位于sm附近的L1高速缓存,它的延迟极地。共享内存使用的是基于存储器切换的架构,它将共享内存平均分成多个相同尺寸的内存模块,称为存储体

gpu并行编程架构

当线程束中所有线程同时访问相同地址的存储体时,会触发一个广播机制到线程束的每个线程,其他情况会导致存储体冲突。

 

GPU每个SM拥有一个巨大的寄存器文件,它包含上千个寄存器。写入到寄存器的数据会一直停留在该寄存器,直到有新的数据写入或者当前线程执行完毕自动退出,寄存器数据被重置。这是因为gpu可能同时处理上千个相同指令的线程,每个线程在执行过程中某些中间计算结果只供自己所在的线程使用,所以它没有必要写入全局内存中。只有当线程计算结束或某些中间过程需要将数据写入到全局的时候,才将寄存器中的数据复制给全局内存变量。

 

图形处理器架构

Nvidia旗舰消费级开普勒架构:

gpu并行编程架构

Cpu负责在gpu上分配内存,并将内核函数(仅在gpu上执行的并行计算程序)以及相关数据发送到gpu内存,gpu从这些内存获取数据并进行大规模的并行计算,最后cpu从gpu内存中取回计算结果。

流处理器族是SMX,它使用SIMD架构模型,他们叫SPMD(单程序多数据)。

 

在gk210/110架构中,虽然每个SMX只有192个sp,但是每个smx可以分配最多高达2048个线程,每个时钟周期有192个线程在执行的同时有2000个线程正在内存中获取数据,这样通过大量的线程就使得大部分线程的内存获取的延迟被隐藏了。

gpu并行编程架构

全局内存访问的合并

当连续的线程向全局内存发起数据请求,并且请求的内存块是连续对齐时,这些线程的多个内存请求会被合并成一个请求,一次性返回数据。

gpu并行编程架构

所谓对齐就是每次指令获取的数据所占内存大小是和内存系统支持的存取单元一致的。

对于分支指令,最有效的方法是尽量保证分支的连续性,对于所有线程组成的条件数组排序,或者以某种方式处理,使得分支能够连续排列。这种优化可以带来一定的性能提升。

gpu并行编程架构

 

参考:全局光照技术:从离线到实时渲染