STM32
直播中

张旭

7年用户 995经验值
私信 关注
[问答]

如何去使用一种OpenCL应用程序呢

OpenCL应用程序由哪些部分组成?
如何去使用一种OpenCL应用程序呢?

回帖(1)

徐静怡

2021-11-10 15:17:07
  TI OpenCL 用户指南3
  Optimization Tips
  OpenCL应用程序由主机应用程序和一组设备内核组成。主机代码和设备代码都有优化技术。存在跨越主机和设备之间的边界的一些技术。本节提供了编写OpenCL应用程序的技巧,该应用程序执行得很好。它以DSP为加速器设备,以TI SoCs为目标。这些提示被组织成基于尖端适用的部分,即主机或设备。
  Optimization Techniques for Host Code
  使用离线的嵌入式编译模型
  OpenCL允许在主机代码运行时动态编译设备代码。这允许应用程序的可移植性,但显然,当编译发生时,它会减慢宿主应用程序的运行速度。为了加快主机应用程序的速度,应该离线编译设备代码,即在主机应用程序运行之前。编译部分中有两个使用离线编译的编译模型。对于最快的操作,带嵌入式对象模型的离线编译将是最快的。有关为该模型构造代码的详细信息,请参阅从二进制创建带有嵌入式二进制的OpenCL程序。
  避免共享内存SoC平台上的读/写缓冲模型
  在共享存储器SoC平台上,主机和设备具有读取和写入相同存储器区域的能力。但是,Linux系统内存不能与设备共享。因此,快速OpenCL应用程序应避免在Linux系统内存和可共享内存区域之间复制数据。有关Linux/OpenCL内存分区的详细信息,请参见如何将DDR3分区为Linux系统和OpenCL。
  读缓冲区和写缓冲区OpenCL操作执行复制,应避免。或者,FASTOpenCL应用程序将在共享内存中分配OpenCL缓冲区,并允许主机直接读取和写入底层内存。这可以通过两种方式实现。
  通常创建缓冲区,并使用map缓冲区和Unmap缓冲区OpenCLAPI将基础缓冲区内存映射到主机地址空间。有关缓冲区创建信息,请参阅OpenCL缓冲区,并查看缓冲区读/写与map/unmap缓冲区信息的对比。
  使用__malloc_ddr or __malloc_msmc,并使用生成的指针创建具有CL_MEM_USE_HOST_PTR属性的缓冲区。有关__malloc_ddr and __malloc_msmc的详细信息,请参见“Alternate Host malloc/free Extension for Zero Copy OpenCL Kernels for details on __malloc_ddr and __malloc_msmc,并查看CL_MEM_USE_HOST_PTR属性用法的OpenCL缓冲区。
  25.1 Use MSMC Buffers Whenever Possible
  TI SoC通常有一个片上共享内存区域,称为MSMC.MSMC的内存访问延迟比DDR小得多,因此对MSMC缓冲区的操作将比对DDR缓冲区的操作执行得更好。这将是特别真实的计算成本每字节加载是低的,即带宽有限的算法。TI OpenCL实现有一个扩展,允许在MSMC内存中创建全局缓冲区。有关扩展的详细信息,请参阅片内MSMC内存中的快速全局缓冲区。您还可以使用__malloc_msmc内存分配扩展,并将返回的指针传递到缓冲区创建操作,还可以断言CL_MEM_USE_HOST_PTR属性,如上一小节所示。
  25.2 Dispatch Appropriate Compute Loads
  将计算从主机调度到设备自然需要一定的开销。调度个人,小的计算不会导致提高性能。如果你有灵活性来控制计算的大小,那么一个很好的规则,一个拇指将开销保持在总调度往返的10%以下。当然,您需要知道开销,以便计算最小目标计算负载。
  设备派遣的开销有两个方面:根据设备频率和使用SOC平台的原始OpenCL调度开销通常每次调度在60和180微秒之间运行。使用TIOpenCL产品附带的NULL示例可用于测量开销的此组件。在向/从设备通信共享缓冲区时,CPU上的显式缓存操作的成本。该计算具有一些可变性,但公式microseconds = 3 + bytes/8096每个缓冲区的每个调度都是一个合理的近似。
  例如,如果内核K接受两个1MB缓冲区作为输入,那么开销的粗略计算将是:180 + (3+1024/8) + (3+1024/8) = 442us,这意味着建议K的最小计算为10x开销或大约4.5毫秒(Ms)。
  更喜欢每个工作组有一个工作项的内核,以获得更好的性能,
  使用单个工作项创建工作组,并在工作组内使用迭代。
  26. Optimization Techniques for Device (DSP) Code
  26.1Prefer Kernels with 1 work-item per work-group
  为了获得更好的性能,使用单个工作项创建工作组,并在工作组内使用迭代。以这种方式构
  造的内核利用了C66xDSP高效执行循环的能力。
  本地缓冲区不能用于主机和设备之间的直接通信,但它们非常适合在设备代码中存储临时中间值。在TI SoC上,本地缓冲区位于L2 SRAM存储器中,而全局缓冲区位于DDR 3存储器中。对L2的访问时间比DDR快10倍以上。在编写值时,本地而不是全局的影响会进一步放大。对于算法,如果值被写入缓冲区,并且缓冲区随后被另一个内核或CPU主机使用,则通常最好将值写入本地缓冲区,然后使用OpenCL异步_Work_group_Copy函数将该本地缓冲区复制回全局缓冲区。
  下面两个内核执行相同的简单向量加法操作。区别在于,第一个从DDR读取两个输入并将结果写入DDR,其中第二个从DDR读取两个输入并将结果写入本地L2,然后使用异步_Work_GROUP_COMPY将本地缓冲区大容量移动回全局缓冲区。第二个版本比第一个版本快3倍。
  上一节演示了async_work_group_copy调用的用法。OpenCL内置函数async_work_group_copy和async_work_group_copy都使用系统DMA操作来执行数据从一个位置到另一个位置的移动。这可能是有益的原因有几个:
  顾名思义,异步…函数是异步的,这意味着调用启动数据传输,但在返回之前不等待完成。随后的wait_group_events 调用阻塞,直到数据传输完成。这允许在数据传输的同时执行额外的工作。DDR通过系统DMA写入发生在最佳突发大小,而DSP写入DDR内存没有,因为缓存设置为写入模式上的DSP,以避免错误的共享问题,可能导致不正确的结果。
  Avoid DSP writes directly to DDR
  See the previous two subsections.
  Use the reqd_work_group_size attribute on kernels
  如果您按照主机优化技巧“更喜欢每个工作组有一个工作项的内核”,那么您应该用reqd_work_group_size属性对内核进行注释,以通知OpenCL C编译器内核只有一个工作项。这会将信息传递给OpenCL C编译器,否则它将不知道这些信息,并且有许多基于这些知识启用的优化。使用此属性的示例如下所示:
  kernel __attribute__((reqd_work_group_size(1, 1, 1)))void k1wi(global int *p){ 。。。}
  即使内核在每个工作组具有》1个工作项,此属性对于OpenCLC编译器也是有用的。当然,要使用它,您将断言主机代码将以与在属性中指定的数字相同的本地大小对该内核进行排队。如果内核以与属性中指定的本地大小不同的本地大小进行排队,则运行时将给出明确定义的错误。以下内核使用属性来断言维度1的本地大小为640,维度2的本地大小为480,维度3未使用:
  kernel __attribute__((reqd_work_group_size(640, 480, 1)))void img_alg(global int *p){ 。。。} Use the TI OpenCL extension than allows Standard C code to be called from OpenCL C code
  Call existing, optimized, std C code library functions. Or write your own standard C code.
  Avoid OpenCL C Barriers
  Avoid OpenCL C barriers if possible. Particularly prevent private data from being live across barriers. barrier(), async…(), wait…()
  Use the most efficient data type on the DSP
  为应用程序选择最有效的数据类型。例如,如果足够,优选“char”类型来表示使用“float”类型的8位数据。这可能会产生重大影响,因为:
  它更有效地利用可用的数据带宽它提高了C66xDSP的计算效率,单指令SIMD指令操作的SIMD元素的数量通常倾向于与元素宽度成反比。观察到,如果8位存储对于给定的应用程序是足够的,则更有效的使用使用Char和float计算资源和数据带宽。
  不要使用大型向量类型
  不要使用向量类型,其中向量类型的大小为》64位。对于宽向量类型,C66xDSP的指令支持有限,因此它们的使用对性能没有好处。
  Vector types with total size 《= 64 bits may be beneficial, but the benefit is not guaranteed.
  Consecutive memory accesses
  数据访问模式在生成有效代码中起着关键作用。连续的内存访问是最快的方法。此外,数据流可以发生在不同的数据大小,如。
  Single Byte ld/st
  Half Word ld/st
  Single Word ld/st
  Double Word ld/st
  在上述列表中,数据流量为存储器操作的上升速率。
  使用双字ld/st是最有利的,因为它具有最高的数据流速率。
  这可用于在不同包装粒度中传输数据。说双字id可以在不同的包装粒度中带来数据,例如:
  • Single 64-bit data
  • Two 32-bit data
  • Four 16-bit data
  • Eight 8-bit data
  根据应用程序的性质,可以选择不同大小的加载。这里的重点是设法实现更高的数据流速率。例如:
  MXN图像表示为‘char’类型的一维数组。该图像由高斯滤波核组成。为了像前面讨论的那样利用SIMD运算,选择了一个向量长度为4的向量。
  为了有效地输入数据,
  char* image;char4 r1_7654, r1_3210;r1_7654 = vload4(0, image);r1_3210 = vload4(4, image);Prefer the CPU style of writing OpenCL code over the GPU style
  有大量现有的OpenCL代码可用,而且大多数代码都针对GPU或CPU进行了优化。通常,应用程序将为每个应用程序优化不同的内核。通常,当在TI SoC上执行并使用DSP作为设备时,针对CPU的版本将比针对GPU的版本执行得更好。
  27. Typical Steps to Optimize Device Code
  下面的流程图描述了典型的顺序,其中应用各种优化步骤来改善C66xDSP上OpenCL内核的性能:
  Example: Optimizing 1D convolution kernel
  在此conv1d示例附带OpenCL产品中,我们展示了如何按步骤优化OpenCL1D卷积内核步骤。
  通常,我们需要优化三个区域:指令管线:在低II(启动时间间隔)时,回路是否为软件管线?SIMD效率:是否充分利用了可用的SIMD指令(如C66X)?内存层次结构性能:输入和输出数据是否可以通过双重缓冲扩展到更快的内存,以重叠计算和数据移动?
  The example 1D卷积核应用于2D数据的每一行,这些数据可以表示图像、独立通道的集合等。一维卷积核/滤波器尺寸为5x1。我们为非对称过滤器编写了一个通用内核。如果您的滤波器是对称的,欢迎您优化两个乘法。
  我们使用一个简单的直进内核的时间作为基线,并报告优化版本相对于该基线的加速比。执行时间由主机端的1920x1080输入映像测量,并报告为微秒。在AM572x(2个DSP核)和K2H(8个DSP核)EVMS上进行了同样的实验。通过改变代码中的NUMCOMPUNITS,我们获得了将内核分配到1、2、4和8个DSP核的性能。每个实验重复5次,每个类别中的最小值在这里报告。
  在下面的内容中,我们将通过优化来实现性能的提高。
  Driver code setup
  将一维卷积核应用于二维图像。我们编写了一个驱动代码,它用随机数据初始化2D图像,并相应地调用OpenCL内核。我们选择高清图像大小1920x1080作为输入和输出。在主机端进行了相同的内核计算,并根据DSP的内核结果对其结果进行了验证,保证了算法的正确性。性能是根据主机端在内核排队之前和内核完成后经过的时间来衡量的。
  最初,我们将OpenCL全局大小(1920,1080)划分为NUMCOMPUNITS工作组,每个工作组具有本地大小(1920,1080/NUMCOMPUNITS),这样每个DSP核心将得到一个工作组。
  k_baseline: Ensure correct measurements
  TI的OpenCL运行时将延迟地将设备程序加载到程序的内核的第一个队列上,因此从第一个队列到第一个队列的运行时间将更长,以考虑程序的加载。为了从内核性能中删除程序加载开销,我们可以在运行其他内核之前对一个空内核进行排队。
  k_baseline: Check software pipelining
  我们可以查看程序集输出,以查看编译器是否成功地对内核进行了软件流水线操作。对于最初的二维内核,编译器将在内核周围添加两个隐式循环,以创建OpenCL工作组,并尝试将最内部的循环用于软件管道。使用选项-k运行编译器,以保留程序集代码:
  clocl -k ti_kernels.clLook at the k_conv1d_5x1 function in ti_kernels.asm, search for SOFTWARE PIPELINE and we see these two lines:;* Searching for software pipeline schedule at 。。。;* ii = 7 Schedule found with 5 iterations in parallel 因此原始内核已经是软件流水线化的。每7个周期开始在最内侧维度上的循环迭代。
  k_loop: Improve software pipelining
  仔细看一下基线源代码,我们就会发现循环处理的是一些边界条件。如果我们可以将这些边界迭代从主循环中剥离出来,那么主循环可能被安排在较低的II上。为此,我们还需要将OpenCL内核的工作空间从2D减少到1D,以便内核代码中最内部的循环变得显式化。核k_loop是这种转换的结果。从组装文件中,我们可以看到主循环是调度在ii=3,这意味着每3个周期启动一次迭代:
  ;* Searching for software pipeline schedule at 。。。;* ii = 3 Schedule found with 10 iterations in parallel 总结:在内核中显式地定义圆环,将OpenCL内核的工作空间从2D减少到1D,去掉边界条件,或者删除边界检查,这样就可以填充输入数据或减小输出大小,从而使边界条件消失。与基线版本相比,使用精简的II,我们并没有看到执行带来的性能改善。一个可能的原因是,由于缓存错误而导致的软件管道中断已经占据了执行的主导地位。现在是为内存层次结构进行优化的时候了。在此之前,让我们看看是否可以优化C66 DSP上可用的SIMD功能。
  k_loop_simd: Improve software pipelining with SIMDization
  有时,编译器可能无法auto-SIMDize循环。我们可以查看所涉及的内存访问和计算,并执行SIMD手动。由于OpenCL C向量语义,我们必须假设每一行在8字节的边界上正确地对齐,以便使用向量类型Float 2。首先我们对内存访问和计算进行SIMDISE,然后我们寻求在寄存器中流水线加载值的机会。K_loop_SIMD是SIMD化的结果。从程序集中可以看到,每5个周期启动一次展开迭代(对应于两个基线迭代):
  ;* Searching for software pipeline schedule at 。。。;* ii = 5 Schedule found with 5 iterations in parallel 总结:
  Unroll col-loop by a factor of 2 by hand
  Data layout requirement: each row is aligned on 8-byte double word boundary
  SIMDize loads and stores
  SIMDize computation
  Pipeline loaded values in registers if possible
  用手将圆环展开2倍
  数据布局要求:每一行按8字节双对齐
  k_loop_db: EDMA and double buffer k_loop
  TI的OpenCL实现为OpenCL本地内存在每个核心上指定了L2 SRAM的一部分。我们可以使用EDMA将数据从全局缓冲区(DDR)移动到本地缓冲区(L2),在本地缓冲区上执行计算,然后将本地缓冲区(L2)的结果存储回全局缓冲区(DDR)。OpenCL C内核语言内置了我们映射到TI的EDMA例程的异步_Work_group_()函数。为了更好地利用EDMA的异步特性,我们使用了双缓冲(乒乓)来有效地重叠数据移动和计算。
  对于这个特定的内核,每一行都需要COLSsizeof(float) + COLSsizeof(float)字节进行输入和输出。使用双缓冲,每行输入和输出都需要16cols字节。给定我们选择的Cols=1920,我们可以在128 KB的本地内存中容纳最多4行,或者在768 KB的本地内存中容纳最多25行:
  4 * (2 * (1920*4 + 1920*4)) 《= 128 * 102425 * (2 * (1920*4 + 1920*4)) 《= 768 * 1024 为了确保双缓冲管道至少执行几次,比如说8,我们可以将BLOCK_HEIGHT to ROWS / NUMCOMPUNITS / 8 + 1。在内核中,在计算本地存储器中的图像行的当前块之前,我们将下一块行插入到具有EDMA的本地存储器中。另一个转换是内核现在显式地迭代通过行维度,因为需要双缓冲。因此,我们需要将所需的内核工作组大小设置为(1,1,1)。在主机代码中,我们只需要指定工作组的数量,在将ND范围内核排队时,我们使用计算单位的数量。我们在内核中添加了三个附加的参数:块高度、用于输入的本地缓冲区和用于输出的本地缓冲区。OpenCL运行时自动分配本地缓冲区,OpenCL应用程序代码只需要指定大小。在所有这些变换过程中,我们看到,非正弦化的K_LOOP_DB不仅优于基线K_LOOP,而且与K_LOOP_SIMD进行了比较。
  With all these transformation, we see that non-SIMDized k_loop_db outperforms not only baseline k_loop, but also SIMDized k_loop_simd.Summary1. Require 8-byte alignment for each row2. Determine the block height for double buffering3. Set required work group size to (1,1,1) for kernel4. Set OpenCL workspace to (NUMCOMPUNITS, 1, 1), each work group will figure out which rows to work on
  Double buffer with EDMA on input and
  output, computation only loads from and stores to local buffersk_loop_simd_db: EDMA and double buffer k_loop_simdWe apply the same EDMA and double buffering transformation on k_loop_simd as above. Now we see similar performance improvements upon k_loop_simd.k_loop_simd_db_extc: Use external C function for k_loop_simd_db 虽然我们可以在OpenCL C语言中完全处理这个例子,但有时OpenCL C对我们C66DSP的表现力有限制。例如,C66DSP可以执行比异步工作组*()OpenCLC内置函数更多的EDMA传输模式,C66DSP支持非对齐SIMD负载和存储。当这些限制确实影响用户应用程序时,我们可以在标准C函数中使用它们,并从OpenCL C代码中调用它们。
  我们将此版本用作示例如何将标准C函数纳入OpenCL。我们将K环SIMDDB的主体移动到外部C函数中,并将OpenCL函数视为一个简单的包装函数。外部C函数使用C66C编译器编译,您可以使用C66C内部函数。同样,您可以重新利用自己或TI开发的现有优化的C实现和库。当然,这是TI的扩展,不适用于其他供应商的OpenCL平台。
  c_loop_simd_db_extc() in k_ext.c中是重写C函数。注意EDMAMGR函数和C66SIMD内部函数的显式使用。使用此版本,我们获得了稍微更好的性能。
  Summary1. Move kernel body to an external standard C function2. Use EdmaMgr_*() functions directly, cover non-consecutive transfers3. Use C66 C SIMD intrinsic built-in functions, cover non-aligned SIMD loads and stores4. Link separately compiled C object back to kernel executable
  并讨论了OpenCLRTOS软件包开发OpenCL应用程序的过程。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分