您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
AMD OpenCL大学课程(7)
 
译者:迈克老狼2012,火龙果软件 发布于:2014-11-11
   次浏览      
 

性能优化

1、线程映射

所谓线程映射是指某个线程访问哪一部分数据,其实就是线程id和访问数据之间的对应关系。

合适的线程映射可以充分利用硬件特性,从而提高程序的性能,反之,则会降低performance。

请参考Static Memory Access Pattern Analysis on a Massively Parallel GPU这篇paper,文中讲述线程如何在算法中充分利用线程映射。这是我在google中搜索到的下载地址:http://www.ece.neu.edu/~bjang/patternAnalysis.pdf

使用不同的线程映射,同一个线程可能访问不同位置的数据。下面是几个线程映射的例子:

我们考虑一个简单的串行矩阵乘法:这个算法比较适合输出数据降维操作,通过创建N*M个线程,我们移去两层外循环,这样每个线程执行P个加法乘法操作。现在需要我们考虑的问题是,线程索引空间究竟应该是M*N还是N*M?

当我们使用M*N线程索引空间时候,Kernel如下图所示:

而使用N*M线程索引空间时候,Kernel如下图所示:

使用两种映射关系,程序执行结果是一样的。下面是在nv的卡GeForce 285 and 8800 GPUs上的执行结果。可以看到映射2(及N*M线程索引空间),程序的performance更高。

performance差异主要是因为在两种映射方式下,对global memory访问的方式有所不同。在行主序的buffer中,数据都是按行逐个存储,为了保证合并访问,我们应该把一个wave中连续的线程映射到矩阵的列(第二维),这样在A*B=C的情况下,会把矩阵B和C的内存读写实现合并访问,而两种映射方式对A没有影响(A又i3决定顺序)。

完整的源代码请从:http://code.google.com/p/imagefilter-opencl/downloads/detail?name=amduniCourseCode4.zip&can=2&q=#makechanges下载,程序中我实现了两种方式的比较。结果确实第二种方式要快一些。

下面我们再看一个矩阵转置的例子,在例子中,通过改变映射方式,提高了global memory访问的效率。

矩阵转置的公式是:Out(x,y) = In(y,x)

从上图可以看出,无论才去那种映射方式,总有一个buffer是非合并访问方式(注:在矩阵转置时,必须要把输入矩阵的某个元素拷贝到临时位置,比如寄存器,然后才能拷贝到输出矩阵)。我们可以改变线程映射方式,用local memory作为中间元素,从而实现输入,输出矩阵都是global memory合并访问。

下面是AMD 5870显卡上,两种线程映射方式实现的矩阵转置性能比较:

完整代码:http://code.google.com/p/imagefilter-opencl/downloads/detail?name=amduniCourseCode5.zip&can=2&q=#makechanges

2、Occupancy

前面的教程中,我们提到过Occupancy的概念,它主要用来描述CU中资源的利用率。

OpenCL中workgroup被映射到硬件的CU中执行,在一个workgroup中的所有线程执行完之后,这个workgroup才算执行结束。对一个特定的cu来说,它的资源(比如寄存器数量,local memory大小,最大线程数量等)是固定的,这些资源都会限制cu中同时处于调度状态的workgroup数量。如果cu中的资源数量足够的的话,映射到同一个cu的多个workgroup能同时处于调度状态,其中一个workgroup的wave处于执行状态,当处于执行状态的workgroup所有wave因为等待资源而切换到等待状态的话,不同workgroup能够从就绪状态切换到ALU执行,这样隐藏memory访问时延。这有点类似操作系统中进程之间的调度状态。我简单画个图,以供参考:

1.对于一个比较长的kernel,寄存器是主要的资源瓶颈。假设kernel需要的最大寄存器数目为35,则workgroup中的所有线程都会使用35个寄存器,而一个CU(假设为5870)的最大寄存器数目为16384,则cu中最多可有16384/35=468线程,此时,一个workgroup中的线程数目(workitem)不可能超过468,

2.考虑另一个问题,一个cu共16384个寄存器,而workgroup固定为256个线程,则使用的寄存器数量可达到64个。

每个CU的local memory也是有限的,对于AMD HD 5XXX显卡,local memory是32K,NV的显卡local memory是32-48K(具体看型号)。和使用寄存器的情况相似,如果kernel使用过多的local memory,则workgroup中的线程数目也会有限制。

GPU硬件还有一个CU内的最大线程数目限制:AMD显卡256,nv显卡512。

NV的显卡对于每个CU内的激活线程有数量限制,每个cu 8个或16个warp,768或者1024个线程。

AMD显卡对每个CU内的wave数量有限制,对于5870,最多496个wave。

这些限制都是因为有限的资源竞争引起的,在nv cuda中,可以通过可视化的方式查看资源的限制情况。

3、向量化

向量化允许一个线程同时执行多个操作。我们可以在kernel代码中,使用向量数据类型,比如float4来获得加速。向量化在AMD的GPU上效果更为明显,这是因为AMD的显卡的stream core是(x,y,z,w)这样的向量运算单元。

下图是在简单的向量赋值运算中,使用float和float4的性能比较。

kernel代码为:

本节主要介绍NBody算法的OpenCL性能优化。

1、NBody

NBody系统主要用来通过粒子之间的物理作用力来模拟星系系统。每个粒子表示一个星星,多个粒子之间的相互作用,就呈现出星系的效果。

上图为一个粒子模拟星系的图片:Source: THE GALAXY-CLUSTER-SUPERCLUSTER CONNECTION,http://www.casca.ca/ecass/issues/1997-DS/West/west-bil.html

由于每个粒子之间都有相互作用的引力,所以这个算法的复杂度是N2的。下面我们主要探讨如何优化算法以及在OpenCL基础上优化算法。

2、NBody算法

假设两个粒子之间通过万有引力相互作用,则任意两个粒子之间的相互作用力F公式如下:

最笨的方法就是计算每个粒子和其它粒子的作用力之和,这个方法通常称作N-Pair的NBody模拟。

粒子之间的万有引力和它们之间的距离成反比,对于一个粒子而言(假设粒子质量都一样),远距离粒子的作用力有时候很小,甚至可以忽略。Barnes Hut 把3D空间按八叉树进行分割,只有在相邻cell的粒子才直接计算它们之间的引力,远距离cell中的粒子当作一个整体来计算引力。

3、OpenCL优化Nbody

在本节中,我们不考虑算法本身的优化,只是通过OpenCL机制来优化N-Pair的NBody模拟。

最简单的实施方法就是每个例子的作用力相加,代码如下:

for(i=0; i<n; i++)
{
ax = ay = az = 0;
// Loop over all particles "j”
for (j=0; j<n; j++) {

//Calculate Displacement
dx=x[j]-x[i];
dy=y[j]-y[i];
dz=z[j]-z[i];

// small eps is delta added for dx,dy,dz = 0
invr= 1.0/sqrt(dx*dx+dy*dy+dz*dz +eps);

我们对每个粒子计算作用在它上面的合力,然后求在合力作用下,delta时间内粒子的新位置,并把这个新位置当作下次计算的输入参数。

没有优化的OpenCL kernel代码如下:

__global float4* pos ,
__global float4* vel,
int numBodies,
float deltaTime,
float epsSqr,
__local float4* localPos,
__global float4* newPosition,
__global float4* newVelocity)

{
unsigned int tid = get_local_id(0);
unsigned int gid = get_global_id(0);
unsigned int localSize = get_local_size(0);

在这种实现中,每次都要从global memory中读取其它粒子的位置,速度,内存访问= N reads*N threads= N2

我们可以通过local memory进行优化,一个粒子数据读进来以后,可以被p*p个线程共用,p*p即为workgroup的大小,对于每个粒子,我们通过迭代p*p的tile,累积得到最终结果。

优化后的kernel代码如下:

 int numBodies,

 float deltaTime,

 float epsSqr,

__local float4* localPos,
__global float4* newPosition,
__global float4* newVelocity)

{
 unsigned int tid = get_local_id(0);

下面是在AMD, NV两个平台上性能测试结果:

AMD GPU = 5870 Stream SDK 2.2

Nvidia GPU = GTX 480 with CUDA 3.1

另外,在程序中,也尝试了循环展开,通过展开内循环,从而减少GPU执行分支指令,我的测试中,使用展开四次,得到的FPS比没展开前快了30%。(AMD 5670显卡)。具体实现可以看kernel代码中的__kernel void nbody_sim_unroll函数。在AMD平台上,使用向量化也可以提高10%左右的性能。

1、OpenCL扩展

OpenCL扩展是指device支持某种特性,但这中特性并不是OpenCL标准的一部分。通过扩展,厂商可以给device增加一些新的功能,而不用考虑兼容性问题。现在各个厂商在OpenCL的实现中或多或少的使用了自己的扩展。

扩展的类型分为三种:

1.Khronos OpenCL工作组批准的扩展,这种要经过一致性测试,可能会被增加到新版本的OpenCL规范中。这种扩展都以cl_khr作为扩展名。

2.外部扩展, 以cl_ext为扩展名。这种扩展是由2个或2个以上的厂商发起,并不需要进行一致性测试。比如cl_ext_device_fission扩展。

3.某个厂商自己的扩展,比如AMD的扩展printf

2、使用扩展

OpenCL中,要使用扩展,我们必须打开扩展,在默认状态下,所有的扩展都是禁止的。

#pragma OPENCL EXTENSION extension_name : enable

对于OpenCL,一个函数只有在运行时,才知道其是否可用,所以要确定某个扩展是否可用,是程序员的责任,我们必须在使用前查询它的状态。下面是查询扩展是否可用的代码:

3、一些Khronos批准的扩展

原子操作,它可以保证函数只在一个device上实施原子操作,比如:

—cl_khr_{global | local}_int32_base_atomics

—cl_khr_{global | local}_int32_extended_atomics

—cl_khr_int64_base_atomics

—cl_khr_int64_extended_atomics

注意:原子操作能够保证操作结果正确,但不保证操作的顺序。

双精度和half精度扩展cl_khr_fp64,在一些物理模拟或者科学计算中,需要双精度支持。AMD的64位扩展用cl_amd_fp64,对于cl_khr_fp64是部分支持,NV支持cl_khr_fp64扩展。但half精度扩展cl_khr_fp16,这两家厂商现在都还不支持。

在OpenCL中,Byte addressable store 也是一个扩展,对于sub 32的写,比如char,需要该扩展的支持。例如AMD 直方图的例子中,每个bin用一个byte来存储。

3D Image Write Extensions,在OpenCL标准中,支持2D图像的读写,3D图形的写就需要通过扩展来操作。

The extension cl_KHR_gl_sharing 允许应用程序使用OpenGL buffer,纹理等。

4、AMD扩展

cl_ext_device_fission扩展,通过该扩展把一个设备分成多个子设备,每一个设备都有自己的队列,主要是多核cpu以及Cell Broadband Engine使用,该扩展由AMD,Apple,Intel以及IBM四家联合提出。

fission设备可能的用途包括:

1.保留一部分设备处理高优先级、低时延的任务。

2.Control for the assignment of work to individual compute units

3.Subdivide compute devices along some shared hardware feature like a cache

对于每个子设备,都有自己的queue,比如下面的图中,我们把不同任务发送到两个子设备。值得注意的是:要把设备拆分为子设备,首先我们要了解该设备的架构,然后根据任务及device架构进行拆分。

GPU printf 扩展,主要用来debug kernel代码。cl_amd_media_ops扩展,主要用于一些多媒体操作。The AMD device query extension 主要用于查询和事件处理。

5、NV扩展

1.Compiler Options

2.Interoperability Extensions

3.Device Query Extension

6、Cell Broadband Engine Extensions

cell处理器用的不多,就不详细说了,使用的人可以查询其相关手册。

   
次浏览       
相关文章

企业架构、TOGAF与ArchiMate概览
架构师之路-如何做好业务建模?
大型网站电商网站架构案例和技术架构的示例
完整的Archimate视点指南(包括示例)
相关文档

数据中台技术架构方法论与实践
适用ArchiMate、EA 和 iSpace进行企业架构建模
Zachman企业架构框架简介
企业架构让SOA落地
相关课程

云平台与微服务架构设计
中台战略、中台建设与数字商业
亿级用户高并发、高可用系统架构
高可用分布式架构设计与实践
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...