20

vesperW · 2021年03月19日

Mali GPU opencl编程优化方法总结

以下优化方法是翻译自arm\_mali\_bifrost\_and\_valhall\_opencl\_developer\_guide\_101574\_0403\_00\_en.这篇官方文档。链接:

Documentation - Arm Developer​developer.arm.com

针对biforest和valhall架构的GPU进行的优化方法。

1.通用优化方法:

  • 用最好的processor来进行任务处理
  • GPU是用来进行并行处理的
  • CPU是用来高速串行应用
  • 所有的应用包括控制部分或者计算部分,为了最佳性能请选择合适的处理器
  • 控制部分用通用语言,通用处理器
  • 并行计算部分用GPU
  • 只编译一次程序
  • 排列多个work-item
  • 处理大规模数据
  • 最好数据可以align到64B,对cache-line友好
  • 在精度要求不高情况下,用低精度的数据类型
  • image和buffer 在opencl中有区别,尽量用image
  • 如果算法可以向量化,用buffer,将两个buffer,merge成一个buffer是不科学的,没有收益
  • 如果算法需要插值或者自动边缘剔除用image
  • 使用异步操作,CPU和GPU之间用异步
  • 在操作中尽量避免处理器之间的互动
  • 尽量避免在执行过程中使用clWaitForEvent等api,会block住线程,如果非要利用中间数据,使用双缓冲策略进行 -批处理提交kernel:暂时不清楚原理

2.kernel 优化

  • 最小的groupsize是warp size或者是他的整数倍,如果用了barrier,小的groupsize是可以的,如果不确定是何种size比较有效,可以设置成NULL group的shape也是需要实验出来结果的
  • 需要同步的情况下,通常需要特定的group size,如果不需要同步的情况下,就是不实用共享内存的情况下,性能最好的情况是使用了最大的资源数目,如果没有barrier的情况下concat两个kernel是有性能提升的
  • 用大量的线程数目来掩盖指令的执行延迟
  • shader core(ALU?核心?)所执行的线程取决于内核函数中使用的活跃寄存器数目,使用的寄存器越多,同时执行的线程数越少。活跃寄存器数目取决于程序的复杂程度,kernel程序越复杂,用编译器编译出来的变量数目越多。
  • 为了减少寄存器数目,你可以减少你kernel中的活跃变量数目,或者使用大的NDRange,会有很多的work-items,(不清楚这个,可能是减少了每个线程的使用寄存器数目,从而增加了可以并行的线程数目)
  • 使用离线编译器来产生你kernel用的统计信息

3.优化memory

  • 使用线性访问和高局部性的数据结构,这些提升了可缓存性和性能,
  • 使用cl\_arm\_thread\_limit\_hint这个来优化,但是没有看懂啥意思,还只支持bifrostGPU

4.代码优化

  • vector load 和 vector store,每个operation 可以load 128bit得数据,不要超过128bit的数据,这会降低性能,这是为什么呢?
  • 每个load 进行最多的操作,reuse data,用很多的计算指令
  • 尽量避免转换到、来:float和int,会消耗很多
  • 实验多次来确定最优的kernel,包括:使用小的数据type,减少load和store的数据量,
  • 数据排布变换,来最大化cache使用率
  • 尽量使用128bit的vector load
  • 不要用divide这种数学运算
  • 使用vector load 和vector store
  • 利用 \_sat() functions 而不是 min() or max()
  • 太多的live variable可能影响性能和每个core执行的可以并行的threads
  • 不要在kernel 里面计算constants
  • 使用离线编译器来进行统计
  • 使用内置的函数,是快速的硬件指令
  • 小心使用cache,每个线程分配的cache非常少,小心使用,最大化空间局部性和时间局部性(spatial locality and temporal locality)
  • 大规模的顺序的读取、写入数据比较好

5.执行时期的优化

  • cache 住binaries在存储设备上,不用每次都编译

6.减少串行计算的影响

  • 使用memory map 而不是 memory copy 来交换数据,这个api值得好好看一下
  • 让message尽量小
  • 使用memory size 是一个power of two
  • 通用处理器处理串行工作
  • 利用优化过的clEnqueueFillBuffer和clEnqueueFillImage来fill buffer和Image

7.针对Bifrost and Valhall的特殊优化

  • 相邻分支合并访问,在Mali Bifrost和Valhall GPU中,相邻线程组排列在一起。相邻线程中的标量指令可以并行执行,因此GPU可同时对多个数据元素进行操作。标量在线程中执行,并且必须在锁步中运行。如果着色器包含分支,例如if语句或循环,则相邻线程中的分支可以采用不同的方式。算术单元不能同时执行分支的两侧。拆分两个操作,降低了处理速度。为了避免这种性能下降,请尝试确保所有相邻线程都以相同的方式分支。下表显示了每个Mali Bifrost和Valhall GPU的相邻线程数。

image.png

  • 每个线程有64个32bit的寄存器,64位的数据用两个相邻的32位的寄存器来表示,如果一个线程使用超过64个寄存器,编译器将把寄存器数据存储在memory中,减少带宽不好
  • 向量化8bit和16bit数据的操作,使用fp16会比f32增加一倍,因为一个register可以存储两个数据,使用char可以存储4个char。
  • 32位数据的向量化和非向量化运算是性能一样的,8和16bit的数据向量化才有意义,因为寄存器都是32位的
  • 使用128bit的load和store
  • 如果每相邻四线程中的所有线程都从同一缓存行加载,则加载和存储操作会更快,合并访问呗
  • 如果可能使用32位的数学运算代替64bit的算数运算
  • 如果系统允许,使用共享虚拟内存,就不用同步了
  • 寻找计算单元和存储单元之间的平衡
作者:夏津
原文链接:https://zhuanlan.zhihu.com/p/346591412
推荐阅读
关注数
23565
内容数
1018
Arm相关的技术博客,提供最新Arm技术干货,欢迎关注
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息