现在的位置: 首页 > 综合 > 正文

ATI 流计算介绍

2013年02月23日 ⁄ 综合 ⁄ 共 11060字 ⁄ 字号 评论关闭

引言:
      随着GPU的并行处理能力的不断提升,GPU的特性被不断的应用于图形无关的应用中,并获得了非常大的速度提升。大量的应用开始工作在GPU上,而不是利用多核CPU进行加速。类似于光线跟踪,光子映射等开销很大的计算都可以在GPU上达到静实时的性能。

      本文介绍了ATI流计算的一些基础知识,对于CAL,Brook+或者OpenCL有兴趣的朋友,可以看看。

正文:

ATI流计算模型:

      ATI流计算模型包含了一套软件系统以及ATI流处理器。下图展示了ATI流处理模块之间的关系:

           
1
 

      ATI流计算软件为客户端用户提供了一个灵活完整的接口,从而使开发人员充分利用ATI的硬件特性进行流计算。
      软件主要分为下面几个模块:

           编译器: 类似于Brook+的编译器,把Brook+内核的代码编译成为独立的C++文件以及IL代码。

           流处理器的设备驱动: ATI流计算抽象模型(CAL)。

           性能分析工具: Stream KernelAnalyzer,可以及时编译Brook+,IL代码,并且分析程序性能。

           性能库: AMD核心数学库(ACML),这部分是专用于特定领域的。

     在最新一代的ATI流处理器中,编程模型应用一种通用的Shader语言。可编程的流处理器可以执行用户指定的各种程序,这些程序被称为内核函数(Kernel)。这些流处理器可以以一种单指令多数据(SIMD)的形式执行一些与图形完全无关的任务。这种编程模型被称为流计算,内存中存储的大量相同类型的数据可以被分配到不同的SIMD引擎中进行处理,从而生成输出数据。

     每一个在SIMD引擎中被处理的实例被称作为线程(Thread)。在每个Pass里,可以有大量的线程被映射到一个矩形区域中,这个矩形区域被称为执行区域(Domain of Execution)。

     流计算处理器为每组线程都分配到一组线程处理器(Thread Processor)中,直到所有的线程都被处理。只有之前的线程完成计算之后,后面的线程才可以得到处理。下图为一个简化的ATI流计算模型:

         
2

Brook+简介:

      Brook+为开发者提供了一个简便快捷的流计算开发接口。用户可以通过Brook+在ATI的硬件进行流计算开发。Brook+的内核程序是由一种类C语言编写的,所有非常适合传统的C以及C++程序员。Brook+语言中两个最关键的概念:

      1. 流。流就是一些类型相同的数据,他们可以被分配到不同的流处理器中进行处理。

      2. 内核。内核程序其实就是GPU硬件的行为程序,它指定了硬件的行为特性。

      Brook+的内核函数是被Brcc(Brook Code Compiler)编译的,编译后生成三个文件,其中两个是.h和.cpp文件,就是定义了一些和函数相关的类,还有一个最关键的就是IL代码。有了这些文件,这三个文件再和用户的其他源文件一起编译,链接,从而生成可执行性文件。Brook+的程序在运行时,是需要Brook runtime的。Brook+的内核程序也可以被CPU执行,还可以进行调试。

      目前,Brook+的版本是1.4版本。已经被提交到了SourceForge上,应该是没有太多的维护了。由于Brook+还有一些限制,所以在灵活性方面并不如CUDA好。而且由于过多的封装,效率也并不很高。唯一的好处就在于用Brook+开发比较简便,程序员需要管理的事情不是特别多,因为Brook+已经把很多复杂的内容都封装起来了。对于想做一些GPGPU测试程序的朋友,这个接口还不错。但是如果开发复杂的项目,Brook+会有一定的限制。

CAL简介:

     ATI CAL(Compute Abstraction Layer)是一个硬件驱动接口,程序员通过CAL可以访问硬件所提供的几乎所有特性,可以控制硬件非常底层的东西。相对于Brook+来说,要底层一些,没有什么限制,灵活的多。事实上Brook+就是基于CAL实现的。CAL具有以下特性:

       可以生成设备相关代码(ISA)。

       设备管理。

       资源管理。

       内核的读取与执行。

       多核GPU的支持。

       与3D图形接口的交互(Interoperability)。(关于这种交互,我专门有介绍过:http://blog.csdn.net/codeboycjy/archive/2009/11/28/4896835.aspx

       CAL可以支持用IL编写的内核函数,也同样可以接受用硬件高度相关的代码(ISA)来编写Kernel。

     CAL的优点在于,它是很底层的接口,用户可以非常灵活的控制GPU的很多模块。但是CAL也有一些缺点,CAL的内核编写是非常繁琐的,因为IL是一种类似于汇编的语言,所以在开发过程中,是很难调试的。而且也不适合快速开发,对于C程序员来说,可能上手也不是特别快。但是其实如果适应了IL的开发,这些困难也可能并不很大。

OpenCL简介:

     目前来说,如果想用A卡进行流计算,无非就是用Brook+,CAL或者OpenCL了。Brook+实际上没有后续支持了,所以并不是最理想的接口。而CAL虽然AMD一直都在更新,但是由于开发难度相对来说大一些,可能也不适合初学者。如果开发者不想用pixel shader进行流计算的话,就只能用OpenCL了。好在这个接口是有Khronos提出的,是一种开放的接口,可以跨平台工作,具有很大潜力。AMD还是比较重视这个接口的,早在9月份初,就已经实现了CPU的solution了。在10月中旬左右,AMD再次发布了基于GPU的OpenCL。

     用户可以通过下载ATI Stream 2.0 Bate 4.0来体验一下AMD的流计算功能。当然,必须要有一块显卡支持才可以。如果有HD Radeon 5000系列最好,但是如果没有的话,4000系列也是同样支持的。

     OpenCL的接口的优点在于,代码不仅可以运行在A卡上,还可以运行在N卡上,Nvidia也发布了OpenCL的solution了。而且还可以在x86架构上运行。由于接口的开放性,以后可能会有更多的处理器支持OpenCL接口。那么用户的OpenCL代码理论上来说,同样是可以跑在新的平台下的。OpenCL的内核编写也是用类C语言进行开发的,所以也非常简单易用。

Stream Kernel Analyzer:

     对于用IL或者Brook+开发的内核程序,用户可以用这个软件进行性能测试。可以在这里下载到:http://developer.amd.com/gpu/ska/Pages/default.aspx

     对于用IL开发程序的朋友来说,可能这个软件就更实用一些了。因为他可以检查一些语法错误,而且报告出的性能更接近与实际的。下图就是这个软件了:

         
3

流处理器的硬件功能:

       
4

      上图为ATI流处理器的简易模型。不同型号的GPU会有不同的性能参数(例如SIMD引擎的数量),但是基本都是一样的模型。

      一个流处理器里面包含很多SIMD引擎。每个SIMD引擎又包含了很多Thread处理器,每个线程处理器可以对于独立的数据进行内核所规定的操作。线程处理器还不算是最小的处理单元,一个线程处理器里面还包含了一定数量的流计算核心,这些核心才是最基本的进行处理的单元,他们可以进行整数,单精度浮点数,双精度浮点数等操作。在同一时间内,一个SIMD引擎中的所有的线程处理器都执行相同的指令集,不同的SIMD引擎是可以执行不同的指令的。

      
5

      一个线程处理器中可以同时最多处理五条指令。我们看到上图中,一个线程处理器中有五个Stream core。其中一个是可以计算超越函数的,剩下四个可以同时计算当精度浮点数。双精度浮点数的处理是通过把四个stream core合起来才可以处理的,所以相对来说要慢一些。除了stream core,每个线程处理器实际还包含一个流控制器,他可以处理一些条件分支的情况。

     不同型号的GPU的细节参数都是不同的。例如,在ATI Radeon 3870 GPU(RV670)这款GPU里面一共包含了4个SIMD引擎,每个SIMD引擎里面有16个线程处理器,并且每个处理器里面有5个stream core。一共是320个物理处理核心单元。

线程处理:

     但同一个cycle中,每个SIMD引擎中的所有线程处理器都必须执行同一指令。为了隐藏内存访问所带来的延迟,线程在发送了内存访问命令之后会被立刻切换。GPU的流处理器里面的Cache并没有CPU多,对于内存访问的优化是通过线程之间的切换来进行的。

     在一个线程处理器中,每4个cycle中,实际可以对于线程处理器指定四条指令。例如,还是刚才那个3870的例子中,16个线程处理器执行同样的指令,每个线程处理器中可以一次执行四条指令(因为每个线程处理器中有4个stream core)。实际上,从外部看,3870的SIMD引擎可以同时处理64条指令的。被同时执行的所有线程的集合被称为wavefront。这里面我们可以理解为3870的wavefront的大小是64的,注意,不是16。

     wavefront的大小是根据GPU的型号不同而可变的。例如,HD 2600 和 HD 2400 的 wavefront的大小分别是32和16,而AMD FireStream 9170的wave front的大小就是64了。

流控制:

     流控制实际上是通过把所有的可能涉及的指令都执行结合起来实现的。举个例子,看下面伪代码:

      if( condition )
      {
          // cost T1
          perform operation 1
      }else
      {
          // cost T2
          perform operation 2
      }

     在一个wavefront里面,如果所有的线程都执行行为1,那么行为2的指令将不会被执行。反之亦然。但是如果有一部分执行了行为1,有一部分执行了行为2,即使是1个线程,那么硬件的做法实际上是让所有线程执行行为1,然后在让所有线程执行行为2。那么总的时间就是T1+T2。

     再举个例子,在一个内核函数的循环里面,每次循环cost T,一个wavefront的线程都只循环一两次,除了一个例外线程循环了100次。那么实际的执行时间就是100*T。这个道理和竹筒装水是一样的。

     所以我们在进行内核程序开发的时候,一定要对于这种分支很明显的情况保持高度敏感。

内存模型:

     在ATI流计算模型中,有三种内存模型:

        host端的内存:这部分内存就是host程序的数据内存等。他可以被host端访问,但是不能被GPU kernel访问。

        PCIe内存:这部分的内存可以被host端或者GPU端访问,但是要做好同步工作。在CAL里面要用calCtxIsEventDone函数,Brook+和OpenCL都已经把这些内容透明了,用户可以无视。

        Local内存:这里的局部是相对于GPU来说的,那么很显然,这种内存是可以被GPU访问的,但是不能被host端访问。

     有三种方式可以拷贝内存到流处理器内存(局部内存)中:

       通过 calResMap

       通过 calCtxMemCopy

       通过一些自定义内核函数从PCIe内存中拷贝。

流处理器的分配:

      高效的流处理器分配机制可以很好的隐藏内存访问所带来的延迟。上面提到GPU是通过线程间切换隐藏内存访问的,这里我们具体举个例子来看一下。

     
7

     这里我们假设有四个线程。在最开始的时候,T0在运行,然后在第20个cycle的时候,该线程申请内存读取。其实,线程没有自己去读取内存,而是发送一条指令给DMA。然后T0就被suspend起了,这个thread processor就去执行T1。然后T1也会被suspend,切换T2。以此类推。在第70个cycle的时候,线程T0请求的内存被返回,所以这是T0就可以继续进行操作了。那么在T3结束后,T0会继续。从thread processor角度讲,在任何一个时间内,都在工作,没有idle状态,所以利用率很高。而从线程角度讲,由于线程的相互切换,内存访问的延迟就被隐藏起来了。

     访问全局的内存的cycle的数量级在200左右,而访问shared memory或者register就会在几个cycle内搞定。这个差距是非常大的。所以为了能够有效的隐藏全局访问的延迟,我们需要让计算更密集。就是说在尽可能少访问内存的情况下,多进行计算操作。另外还要有足够的线程数量,上面的例子是个最简单的例子,访问全局的延迟远远要大于四个指令读取的命令。一定要尽可能的保证线程数量的足够,这样才能最好的利用GPU的硬件特性。

     当然,这里绝对不是说为了更好的利用硬件,要增加一些无关的计算,以及一些无用的线程。线程当然是越少越快,但是如果少到一定的程度,甚至比stream core的数量还要少,GPU的利用率是非常低的。

     OK。就介绍这么多吧。希望能够对于想学习Stream computing的朋友有一点点的帮助。

引言:
      随着GPU的并行处理能力的不断提升,GPU的特性被不断的应用于图形无关的应用中,并获得了非常大的速度提升。大量的应用开始工作在GPU上,而不是利用多核CPU进行加速。类似于光线跟踪,光子映射等开销很大的计算都可以在GPU上达到静实时的性能。

      本文介绍了ATI流计算的一些基础知识,对于CAL,Brook+或者OpenCL有兴趣的朋友,可以看看。

正文:

ATI流计算模型:

      ATI流计算模型包含了一套软件系统以及ATI流处理器。下图展示了ATI流处理模块之间的关系:

           
1
 

      ATI流计算软件为客户端用户提供了一个灵活完整的接口,从而使开发人员充分利用ATI的硬件特性进行流计算。
      软件主要分为下面几个模块:

           编译器: 类似于Brook+的编译器,把Brook+内核的代码编译成为独立的C++文件以及IL代码。

           流处理器的设备驱动: ATI流计算抽象模型(CAL)。

           性能分析工具: Stream KernelAnalyzer,可以及时编译Brook+,IL代码,并且分析程序性能。

           性能库: AMD核心数学库(ACML),这部分是专用于特定领域的。

     在最新一代的ATI流处理器中,编程模型应用一种通用的Shader语言。可编程的流处理器可以执行用户指定的各种程序,这些程序被称为内核函数(Kernel)。这些流处理器可以以一种单指令多数据(SIMD)的形式执行一些与图形完全无关的任务。这种编程模型被称为流计算,内存中存储的大量相同类型的数据可以被分配到不同的SIMD引擎中进行处理,从而生成输出数据。

     每一个在SIMD引擎中被处理的实例被称作为线程(Thread)。在每个Pass里,可以有大量的线程被映射到一个矩形区域中,这个矩形区域被称为执行区域(Domain of Execution)。

     流计算处理器为每组线程都分配到一组线程处理器(Thread Processor)中,直到所有的线程都被处理。只有之前的线程完成计算之后,后面的线程才可以得到处理。下图为一个简化的ATI流计算模型:

         
2

Brook+简介:

      Brook+为开发者提供了一个简便快捷的流计算开发接口。用户可以通过Brook+在ATI的硬件进行流计算开发。Brook+的内核程序是由一种类C语言编写的,所有非常适合传统的C以及C++程序员。Brook+语言中两个最关键的概念:

      1. 流。流就是一些类型相同的数据,他们可以被分配到不同的流处理器中进行处理。

      2. 内核。内核程序其实就是GPU硬件的行为程序,它指定了硬件的行为特性。

      Brook+的内核函数是被Brcc(Brook Code Compiler)编译的,编译后生成三个文件,其中两个是.h和.cpp文件,就是定义了一些和函数相关的类,还有一个最关键的就是IL代码。有了这些文件,这三个文件再和用户的其他源文件一起编译,链接,从而生成可执行性文件。Brook+的程序在运行时,是需要Brook runtime的。Brook+的内核程序也可以被CPU执行,还可以进行调试。

      目前,Brook+的版本是1.4版本。已经被提交到了SourceForge上,应该是没有太多的维护了。由于Brook+还有一些限制,所以在灵活性方面并不如CUDA好。而且由于过多的封装,效率也并不很高。唯一的好处就在于用Brook+开发比较简便,程序员需要管理的事情不是特别多,因为Brook+已经把很多复杂的内容都封装起来了。对于想做一些GPGPU测试程序的朋友,这个接口还不错。但是如果开发复杂的项目,Brook+会有一定的限制。

CAL简介:

     ATI CAL(Compute Abstraction Layer)是一个硬件驱动接口,程序员通过CAL可以访问硬件所提供的几乎所有特性,可以控制硬件非常底层的东西。相对于Brook+来说,要底层一些,没有什么限制,灵活的多。事实上Brook+就是基于CAL实现的。CAL具有以下特性:

       可以生成设备相关代码(ISA)。

       设备管理。

       资源管理。

       内核的读取与执行。

       多核GPU的支持。

       与3D图形接口的交互(Interoperability)。(关于这种交互,我专门有介绍过:http://blog.csdn.net/codeboycjy/archive/2009/11/28/4896835.aspx

       CAL可以支持用IL编写的内核函数,也同样可以接受用硬件高度相关的代码(ISA)来编写Kernel。

     CAL的优点在于,它是很底层的接口,用户可以非常灵活的控制GPU的很多模块。但是CAL也有一些缺点,CAL的内核编写是非常繁琐的,因为IL是一种类似于汇编的语言,所以在开发过程中,是很难调试的。而且也不适合快速开发,对于C程序员来说,可能上手也不是特别快。但是其实如果适应了IL的开发,这些困难也可能并不很大。

OpenCL简介:

     目前来说,如果想用A卡进行流计算,无非就是用Brook+,CAL或者OpenCL了。Brook+实际上没有后续支持了,所以并不是最理想的接口。而CAL虽然AMD一直都在更新,但是由于开发难度相对来说大一些,可能也不适合初学者。如果开发者不想用pixel shader进行流计算的话,就只能用OpenCL了。好在这个接口是有Khronos提出的,是一种开放的接口,可以跨平台工作,具有很大潜力。AMD还是比较重视这个接口的,早在9月份初,就已经实现了CPU的solution了。在10月中旬左右,AMD再次发布了基于GPU的OpenCL。

     用户可以通过下载ATI Stream 2.0 Bate 4.0来体验一下AMD的流计算功能。当然,必须要有一块显卡支持才可以。如果有HD Radeon 5000系列最好,但是如果没有的话,4000系列也是同样支持的。

     OpenCL的接口的优点在于,代码不仅可以运行在A卡上,还可以运行在N卡上,Nvidia也发布了OpenCL的solution了。而且还可以在x86架构上运行。由于接口的开放性,以后可能会有更多的处理器支持OpenCL接口。那么用户的OpenCL代码理论上来说,同样是可以跑在新的平台下的。OpenCL的内核编写也是用类C语言进行开发的,所以也非常简单易用。

Stream Kernel Analyzer:

     对于用IL或者Brook+开发的内核程序,用户可以用这个软件进行性能测试。可以在这里下载到:http://developer.amd.com/gpu/ska/Pages/default.aspx

     对于用IL开发程序的朋友来说,可能这个软件就更实用一些了。因为他可以检查一些语法错误,而且报告出的性能更接近与实际的。下图就是这个软件了:

         
3

流处理器的硬件功能:

       
4

      上图为ATI流处理器的简易模型。不同型号的GPU会有不同的性能参数(例如SIMD引擎的数量),但是基本都是一样的模型。

      一个流处理器里面包含很多SIMD引擎。每个SIMD引擎又包含了很多Thread处理器,每个线程处理器可以对于独立的数据进行内核所规定的操作。线程处理器还不算是最小的处理单元,一个线程处理器里面还包含了一定数量的流计算核心,这些核心才是最基本的进行处理的单元,他们可以进行整数,单精度浮点数,双精度浮点数等操作。在同一时间内,一个SIMD引擎中的所有的线程处理器都执行相同的指令集,不同的SIMD引擎是可以执行不同的指令的。

      
5

      一个线程处理器中可以同时最多处理五条指令。我们看到上图中,一个线程处理器中有五个Stream core。其中一个是可以计算超越函数的,剩下四个可以同时计算当精度浮点数。双精度浮点数的处理是通过把四个stream core合起来才可以处理的,所以相对来说要慢一些。除了stream core,每个线程处理器实际还包含一个流控制器,他可以处理一些条件分支的情况。

     不同型号的GPU的细节参数都是不同的。例如,在ATI Radeon 3870 GPU(RV670)这款GPU里面一共包含了4个SIMD引擎,每个SIMD引擎里面有16个线程处理器,并且每个处理器里面有5个stream core。一共是320个物理处理核心单元。

线程处理:

     但同一个cycle中,每个SIMD引擎中的所有线程处理器都必须执行同一指令。为了隐藏内存访问所带来的延迟,线程在发送了内存访问命令之后会被立刻切换。GPU的流处理器里面的Cache并没有CPU多,对于内存访问的优化是通过线程之间的切换来进行的。

     在一个线程处理器中,每4个cycle中,实际可以对于线程处理器指定四条指令。例如,还是刚才那个3870的例子中,16个线程处理器执行同样的指令,每个线程处理器中可以一次执行四条指令(因为每个线程处理器中有4个stream core)。实际上,从外部看,3870的SIMD引擎可以同时处理64条指令的。被同时执行的所有线程的集合被称为wavefront。这里面我们可以理解为3870的wavefront的大小是64的,注意,不是16。

     wavefront的大小是根据GPU的型号不同而可变的。例如,HD 2600 和 HD 2400 的 wavefront的大小分别是32和16,而AMD FireStream 9170的wave front的大小就是64了。

流控制:

     流控制实际上是通过把所有的可能涉及的指令都执行结合起来实现的。举个例子,看下面伪代码:

      if( condition )
      {
          // cost T1
          perform operation 1
      }else
      {
          // cost T2
          perform operation 2
      }

     在一个wavefront里面,如果所有的线程都执行行为1,那么行为2的指令将不会被执行。反之亦然。但是如果有一部分执行了行为1,有一部分执行了行为2,即使是1个线程,那么硬件的做法实际上是让所有线程执行行为1,然后在让所有线程执行行为2。那么总的时间就是T1+T2。

     再举个例子,在一个内核函数的循环里面,每次循环cost T,一个wavefront的线程都只循环一两次,除了一个例外线程循环了100次。那么实际的执行时间就是100*T。这个道理和竹筒装水是一样的。

     所以我们在进行内核程序开发的时候,一定要对于这种分支很明显的情况保持高度敏感。

内存模型:

     在ATI流计算模型中,有三种内存模型:

        host端的内存:这部分内存就是host程序的数据内存等。他可以被host端访问,但是不能被GPU kernel访问。

        PCIe内存:这部分的内存可以被host端或者GPU端访问,但是要做好同步工作。在CAL里面要用calCtxIsEventDone函数,Brook+和OpenCL都已经把这些内容透明了,用户可以无视。

        Local内存:这里的局部是相对于GPU来说的,那么很显然,这种内存是可以被GPU访问的,但是不能被host端访问。

     有三种方式可以拷贝内存到流处理器内存(局部内存)中:

       通过 calResMap

       通过 calCtxMemCopy

       通过一些自定义内核函数从PCIe内存中拷贝。

流处理器的分配:

      高效的流处理器分配机制可以很好的隐藏内存访问所带来的延迟。上面提到GPU是通过线程间切换隐藏内存访问的,这里我们具体举个例子来看一下。

     
7

     这里我们假设有四个线程。在最开始的时候,T0在运行,然后在第20个cycle的时候,该线程申请内存读取。其实,线程没有自己去读取内存,而是发送一条指令给DMA。然后T0就被suspend起了,这个thread processor就去执行T1。然后T1也会被suspend,切换T2。以此类推。在第70个cycle的时候,线程T0请求的内存被返回,所以这是T0就可以继续进行操作了。那么在T3结束后,T0会继续。从thread processor角度讲,在任何一个时间内,都在工作,没有idle状态,所以利用率很高。而从线程角度讲,由于线程的相互切换,内存访问的延迟就被隐藏起来了。

     访问全局的内存的cycle的数量级在200左右,而访问shared memory或者register就会在几个cycle内搞定。这个差距是非常大的。所以为了能够有效的隐藏全局访问的延迟,我们需要让计算更密集。就是说在尽可能少访问内存的情况下,多进行计算操作。另外还要有足够的线程数量,上面的例子是个最简单的例子,访问全局的延迟远远要大于四个指令读取的命令。一定要尽可能的保证线程数量的足够,这样才能最好的利用GPU的硬件特性。

     当然,这里绝对不是说为了更好的利用硬件,要增加一些无关的计算,以及一些无用的线程。线程当然是越少越快,但是如果少到一定的程度,甚至比stream core的数量还要少,GPU的利用率是非常低的。

     OK。就介绍这么多吧。希望能够对于想学习Stream computing的朋友有一点点的帮助。

抱歉!评论已关闭.