OpenCL Memory优化

  Memory的访问效率往往决定着整个kernel的性能,最小化global memory访问次数在优化OpenCL代码时是非常有效的。Memory主要有以下几种:global, constant, local以及private。互联拓扑结构将共享的global内存,constant内存,local内存系统连接到他们的基础内存下,同时互联也包含对memory端口的访问仲裁。

  Memory的访问会争夺共享内存资源,如果kernel中执行大俩个的内存访问,那么必须生成负责的仲裁逻辑处理内存访问请求,会导致fMax最大工作频率的下降,从而降低内核性能。

  最小化global memory的访问是有好处的,因为:

    通常,增加kernel的性能会导致global memory带宽需求的增加;

    global memory最大带宽要远小于local memory的最大带宽;

    FPGA的最大计算贷款远大于global memory最大带宽。注意,尽可能使用local, private, constant memory来增加kernel的内存带宽。

  内存访问优化主要有以下几种方式:

    (1)有一些常用的guideline。

    (2)优化global memory的访问。

    (3)使用local, private, constant memory执行kernel计算。优化内存访问效率,最大程度减少global memory的访问次数。

    (4)通过在local memory存储提高kernel性能。通过指定numbanks(N)以及bankwidth(M)高级kernel属性,配置local memory bank以进行并行的内存访问。

    (5)通过控制memory replication factor内存复制参数优化对local memory的访问。使用max_replicates。

    (6)最小化循环pipeline中的内存依赖性。离线编译器会确保来自同一线程的内存访问符合程序执行的顺序,当编译NDRange kernel时,使用barriers来同步同一个work-group中跨线程的内存访问。

    (7)静态内存合并。静态内存合并时离线编译器的优化步骤之一,旨在减少kernel访问non-private memory的次数。

1、优化内存访问的常用guideline

  如果OpenCL程序中有一对kernel,其中一个用于产生数据,另一个消耗/使用该数据,那么将其转换为执行两个功能的single kernel。同时使用辅助函数在逻辑上分离两个原始kernel的功能。与单独的较小的kernel相比,FPGA更喜欢实现一个较大的kernel,将两个kernel合并为一个消除了将结果从一个kernel写入global memory,再将数据传输到另一个kernel中的需求。

  Intel SDK 中的离线编译器实现local memory的方式与GPU有很大不同,如果opencl的kernel中包含防止GPU特定的local memory冲突的代码,将其删除,因为离线编译器会自动避免冲突。

2、优化global memory访问

  离线编译器将SDRAM作为global memory使用,默认情况下,离线编译器将global memory配置为burst-interleaved突发交错的形式,在每个外部memory bank之间交错global memory。

  大多数情况下,默认的burst-interleaved机制会在memory bank之间产生最佳的负载平衡,然而在一些情况下,我们可能会希望将存储器手动划分为两个非交错且连续的内存区域以实现更好的负载平衡。

 

 (1)连续内存访问

  连续内存访问优化能够静态的分析kernel中全局加载和存储操作的访问模式,对于整个kernel调用操作产生的连续的加载及存储操作,离线编译器会指示kernel访问global memory中的连续位置。

  连续的加载与存储操作可以提高内存的访问效率,因为这样可以提高访问速度并减少硬件资源需求。数据同时进出pipeline的计算部分,允许计算与内存访问之间存在重叠。如果可能,使用为连续内存位置编制索引的work-item id,以进行访问全局内存的加载与存储操作。

(2)手动切分global memory

  可以手动切分global memory,从而使每个缓冲区占用不同的内存bank。Global memory默认的burst-interleaved机制确保内存访问不会偏向于其中一个外部存储bank,以防止负载不平衡问题。但我们可以手动将数据分区来控制一组buffer的带宽。

  离线编译器不能跨不同的memory类型实行burst-interleaved机制,如果要手动对特定类型的global memory进行分区,使用 -no-interleaving=<global_memory_type> 标志编译kernel,从而将特定的内存类型的每个存储体配置为non-interleaved。

  如果kernel访问内存中大小相同的两个缓冲区,那么无论负载之间的动态调度如何,都可以将数据同时分发到两个memory bank中,这么做可能会增加可视的memory带宽。

  如果kernel访问异构的全局内存类型,那么对手动分区的每种内存类型,在aoc命令中使用

-no-interleaving=<global_memory_type>

  在包含多种类型的全局存储器的FPGA板子上执行kernel时,如果板子提供异构的全局存储器类型,则按照以下规则,如:

    使用DDR SDRAM进行长时间顺序访问;

    使用QDR SDRAM进行随机访问;

    使用片上RAM进行随机的低延迟访问等。

(3)针对两个或多个存储区进行优化

  离线编译器会制动创建一个全局存储器互联,从而将大多数可用的全局存储器带宽从BSP传递到kernel系统中。使用只读/只写/混合读写可以使数据传输吞吐量达到饱和,默认情况下,所有可用的bank中的数据传输都是interleaved。如果我们发现吞吐量不足,可以使用 -no-interleaving 选项。

3、使用constant/local/private memory进行kernel计算

  为了最大减少global memory的访问次数,需要先将一组计算中的数据从global memory中预加载到constant/local/private memory中,然后对预加载的数据执行kernel计算,然后将结果写回到global memory中。

(1)Constant Cache Memory

  Constant memory驻存在global memory中,但kernel在运行时将其加载到所有的work-group共享的片上cache中。例如,如果存在所有的work-group都使用的只读数据,并且数据大小适合存在constant memory,那么可以将数据分配给Constant memory。Constant cache最适合在kernel多次调用中保持恒定的高带宽表的查找。Constant cache针对高速cache命中性能做了优化。

  默认情况下,constant cache的大小时16kB,可以通过在aoc命令中包含 -const-cache-bytes=<N> 的选项指定恒定大小的constant cachre,其中N以bytes为单位。

  与具有额外硬件,能够容忍较长时间内存延迟的global memory不同,constant cache会因为高速缓存未命中而导致较大的性能损失。如果_constant参数不能很好的适应cache,那么可以使用_global const来获得更好的性能。如果host的应用程序写入已加载到constant cache的constant memory中,那么将从contant cache中丢弃缓存的数据。

(2)将数据预加载到local memory中

  Local memory要比global memory小得多,但具有更高的吞吐量和更低的延迟。与global memory不同,kernel可以随机访问local memory而不降低性能。在构建kernel代码时,尝试顺序访问global memory,并在kernel使用数据进行计算之前将数据缓冲到片上的local memory中。

  离线编译器在FPGA板子的片上存储模块中实现local memory,片商存储模块有两个读写端口,时钟频率是kernel时钟的2倍。这种两倍的时钟频率允许内存被double-pumped,从而使同意内存的带宽增大一倍,这样,每个片上存储模块最多支持4个同时访问。

  在理想情况下,对每个bank的访问在bank的片上存储块之间均匀分布。由于在一个时钟周期内一个片上只能同时存在四个对存储体的访问,因此对访问进行分配有利于避免存储体之间的争用。这种存储机制通常是有效的,但离线编译器必须创建一个复杂的内存系统来容纳大量的bank,大量的bank可能会导致仲裁网络的复杂化,并可能降低整体系统的性能。由于离线编译器实现了驻留在FPGA片上存储模块中的local memory,离线编译器必须在编译时选择local memory系统的大小,确定系统大小的方法取决于OpenCL代码总使用的local数据的类型。

优化local memory访问方法:

  使用某些优化方法(例如循环展开)可能会导致更多并发内存访问。CAUTION:增加内存访问次数会是内存系统复杂化并降低性能。

  通过尽可能将kernel中local memory访问数量限制在4个或更少,简化local memory的子系统。4个或更少的对local memory的内存访问,可以将local memory的性能最大化。如果对一个特定的memory系统的访问数大于4,那么离线编译器会将memory系统的片上内存块安排为bank配置。

  如果代码中有函数作用域内的local数据,那么离线编译器会在编译时静态调整在函数体内定义的local数据的大小。我们应该通过指示离线编译器将内存设置为所需的大小(入到最接近的2的幂次)。

  避免使用local_mem_size属性,使用__local的kernel变量而不是__local的kernel参数。

  当访问local memory时,使用尽可能简单的地址计算并且避免执行非强制性的指针数学运算。

  尽可能避免存储指向内存的指针。当从内存中检索指针时,存储的指针通常会阻止静态编译器分析确定访问的数据集,将指针存储到内存中几乎总是导致次优的区域和性能结果。

  创建大小为2的幂次byte的local的访问效率往往决定着整个kernel的性能,最小化global memory访问次数在优化OpenCL代码时是非常有效的。Memory主要有以下几种:global, constant, local以及private。互联拓扑结构将共享的global内存,constant内存,local内存系统连接到他们的基础内存下,同时互联也包含对memory端口的访问仲裁。

  Memory的访问会争夺共享内存资源,如果kernel中执行大俩个的内存访问,那么必须生成负责的仲裁逻辑处理内存访问请求,会导致fMax最大工作频率的下降,从而降低内核性能。

  最小化global memory的访问是有好处的,因为:

    通常,增加kernel的性能会导致global memory带宽需求的增加;

    global memory最大带宽要远小于local memory的最大带宽;

    FPGA的最大计算贷款远大于global memory最大带宽。注意,尽可能使用local, private, constant memory来增加kernel的内存带宽。

  内存访问优化主要有以下几种方式:

    (1)有一些常用的guideline。

    (2)优化global memory的访问。

    (3)使用local, private, constant memory执行kernel计算。优化内存访问效率,最大程度减少global memory的访问次数。

    (4)通过在local memory存储提高kernel性能。通过指定numbanks(N)以及bankwidth(M)高级kernel属性,配置local memory bank以进行并行的内存访问。

    (5)通过控制memory replication factor内存复制参数优化对local memory的访问。使用max_replicates。

    (6)最小化循环pipeline中的内存依赖性。离线编译器会确保来自同一线程的内存访问符合程序执行的顺序,当编译NDRange kernel时,使用barriers来同步同一个work-group中跨线程的内存访问。

    (7)静态内存合并。静态内存合并时离线编译器的优化步骤之一,旨在减少kernel访问non-private memory的次数。

1、优化内存访问的常用guideline

  如果OpenCL程序中有一对kernel,其中一个用于产生数据,另一个消耗/使用该数据,那么将其转换为执行两个功能的single kernel。同时使用辅助函数在逻辑上分离两个原始kernel的功能。与单独的较小的kernel相比,FPGA更喜欢实现一个较大的kernel,将两个kernel合并为一个消除了将结果从一个kernel写入global memory,再将数据传输到另一个kernel中的需求。

  Intel SDK 中的离线编译器实现local memory的方式与GPU有很大不同,如果opencl的kernel中包含防止GPU特定的local memory冲突的代码,将其删除,因为离线编译器会自动避免冲突。

2、优化global memory访问

  离线编译器将SDRAM作为global memory使用,默认情况下,离线编译器将global memory配置为burst-interleaved突发交错的形式,在每个外部memory bank之间交错global memory。

  大多数情况下,默认的burst-interleaved机制会在memory bank之间产生最佳的负载平衡,然而在一些情况下,我们可能会希望将存储器手动划分为两个非交错且连续的内存区域以实现更好的负载平衡。

 (1)连续内存访问

  连续内存访问优化能够静态的分析kernel中全局加载和存储操作的访问模式,对于整个kernel调用操作产生的连续的加载及存储操作,离线编译器会指示kernel访问global memory中的连续位置。

  连续的加载与存储操作可以提高内存的访问效率,因为这样可以提高访问速度并减少硬件资源需求。数据同时进出pipeline的计算部分,允许计算与内存访问之间存在重叠。如果可能,使用为连续内存位置编制索引的work-item id,以进行访问全局内存的加载与存储操作。

(2)手动切分global memory

  可以手动切分global memory,从而使每个缓冲区占用不同的内存bank。Global memory默认的burst-interleaved机制确保内存访问不会偏向于其中一个外部存储bank,以防止负载不平衡问题。但我们可以手动将数据分区来控制一组buffer的带宽。

  离线编译器不能跨不同的memory类型实行burst-interleaved机制,如果要手动对特定类型的global memory进行分区,使用 -no-interleaving=<global_memory_type> 标志编译kernel,从而将特定的内存类型的每个存储体配置为non-interleaved。

  如果kernel访问内存中大小相同的两个缓冲区,那么无论负载之间的动态调度如何,都可以将数据同时分发到两个memory bank中,这么做可能会增加可视的memory带宽。

  如果kernel访问异构的全局内存类型,那么对手动分区的每种内存类型,在aoc命令中使用-no-interleaving=<global_memory_type>

  在包含多种类型的全局存储器的FPGA板子上执行kernel时,如果板子提供异构的全局存储器类型,则按照以下规则,如:

    使用DDR SDRAM进行长时间顺序访问;

    使用QDR SDRAM进行随机访问;

    使用片上RAM进行随机的低延迟访问等。

(3)针对两个或多个存储区进行优化

  离线编译器会制动创建一个全局存储器互联,从而将大多数可用的全局存储器带宽从BSP传递到kernel系统中。使用只读/只写/混合读写可以使数据传输吞吐量达到饱和,默认情况下,所有可用的bank中的数据传输都是interleaved。如果我们发现吞吐量不足,可以使用 -no-interleaving 选项。

3、使用constant/local/private memory进行kernel计算

  为了最大减少global memory的访问次数,需要先将一组计算中的数据从global memory中预加载到constant/local/private memory中,然后对预加载的数据执行kernel计算,然后将结果写回到global memory中。

(1)Constant Cache Memory

  Constant memory驻存在global memory中,但kernel在运行时将其加载到所有的work-group共享的片上cache中。例如,如果存在所有的work-group都使用的只读数据,并且数据大小适合存在constant memory,那么可以将数据分配给Constant memory。Constant cache最适合在kernel多次调用中保持恒定的高带宽表的查找。Constant cache针对高速cache命中性能做了优化。

  默认情况下,constant cache的大小时16kB,可以通过在aoc命令中包含 -const-cache-bytes=<N> 的选项指定恒定大小的constant cachre,其中N以bytes为单位。

  与具有额外硬件,能够容忍较长时间内存延迟的global memory不同,constant cache会因为高速缓存未命中而导致较大的性能损失。如果_constant参数不能很好的适应cache,那么可以使用_global const来获得更好的性能。如果host的应用程序写入已加载到constant cache的constant memory中,那么将从contant cache中丢弃缓存的数据。

(2)将数据预加载到local memory中

  Local memory要比global memory小得多,但具有更高的吞吐量和更低的延迟。与global memory不同,kernel可以随机访问local memory而不降低性能。在构建kernel代码时,尝试顺序访问global memory,并在kernel使用数据进行计算之前将数据缓冲到片上的local memory中。

  离线编译器在FPGA板子的片上存储模块中实现local memory,片商存储模块有两个读写端口,时钟频率是kernel时钟的2倍。这种两倍的时钟频率允许内存被double-pumped,从而使同意内存的带宽增大一倍,这样,每个片上存储模块最多支持4个同时访问。

  在理想情况下,对每个bank的访问在bank的片上存储块之间均匀分布。由于在一个时钟周期内一个片上只能同时存在四个对存储体的访问,因此对访问进行分配有利于避免存储体之间的争用。这种存储机制通常是有效的,但离线编译器必须创建一个复杂的内存系统来容纳大量的bank,大量的bank可能会导致仲裁网络的复杂化,并可能降低整体系统的性能。由于离线编译器实现了驻留在FPGA片上存储模块中的local memory,离线编译器必须在编译时选择local memory系统的大小,确定系统大小的方法取决于OpenCL代码总使用的local数据的类型。

优化local memory访问方法:

  使用某些优化方法(例如循环展开)可能会导致更多并发内存访问。CAUTION:增加内存访问次数会是内存系统复杂化并降低性能。

  通过尽可能将kernel中local memory访问数量限制在4个或更少,简化local memory的子系统。4个或更少的对local memory的内存访问,可以将local memory的性能最大化。如果对一个特定的memory系统的访问数大于4,那么离线编译器会将memory系统的片上内存块安排为bank配置。

  如果代码中有函数作用域内的local数据,那么离线编译器会在编译时静态调整在函数体内定义的local数据的大小。我们应该通过指示离线编译器将内存设置为所需的大小(入到最接近的2的幂次)。

  避免使用local_mem_size属性,使用__local的kernel变量而不是__local的kernel参数。

  当访问local memory时,使用尽可能简单的地址计算并且避免执行非强制性的指针数学运算。

  尽可能避免存储指向内存的指针。当从内存中检索指针时,存储的指针通常会阻止静态编译器分析确定访问的数据集,将指针存储到内存中几乎总是导致次优的区域和性能结果。

  创建大小为2的幂次byte的local数组元素,能够允许离线编译器提供有效的内存配置。

(3)在private memory中存储变量及数组

  离线编译器使用FPGA的寄存器或block RAM实现private memory,离线编译器分析private memory访问并将其提升为register访问,主要是对float/int/char型标量变量。通常private memory对存储单个变量或小型数组比较有用。寄存器是FPGA中比较充足的资源,所以使用private memory会比较好。kernel可以并行访问private memory,从而可以使private memory具有更大的带宽。

4、通过local memory提高kernel性能

  指定numbanks(N)与bandwidth(M)高级内核属性,可以配置local memory bank进行内存访问。这些高级kernel属性描述的存储几何结构决定了kernel可以并行访问local memory中的哪些元素。

5、通过控制memory replication factor内存复制参数优化对local memory的访问

  内存复制参数是指用于实现local memory的M20K的memory block数量,需要在kernel中使用max_replicates参数。

  M20K memory block有两个物理端口,每个block的逻辑端口的数量取决于pump的程度。Pumping是M20K block相对于其他部分的时钟频率。

6、最小化循环pipeline中的内存依赖性。

  离线编译器会确保来自同一线程的内存访问符合程序执行的顺序,当编译NDRange kernel时,使用barriers来同步同一个work-group中跨线程的内存访问。

  为了最大化的减小内存依赖项对loop pipeline的影响:

    确保离线编译器不会假定错误的依赖。

    如果确定kernel代码中不存在依赖项,在循环代码前添加#pragma ivdep来覆盖静态内存依赖分析。

7、静态内存合并

  静态内存合并时离线编译器的优化步骤之一,旨在减少kernel访问non-private memory的次数。

posted @ 2020-08-04 19:10  ZhuzhuDong  阅读(2474)  评论(0编辑  收藏  举报