OpenCL 通用编程与优化(9)

46 篇文章 22 订阅

7.2最佳 memory load/store

在以前的部分中,我们讨论了有关如何使用其他类型内存的一般指南。在本节中,我们将审查有关memory load/store的性能至关重要的一些关键点。

7.2.1 合并 memory load/store

合并加载/存储是指合并来自多个相邻工作项的加载/存储请求的能力,如第 3.2.4 节所述,用于本地内存访问。合并访问对于全局内存加载/存储也至关重要。合并存储的工作方式与读取类似,不同之处在于加载是一个双向进程(请求和响应),而存储是一个单向进程,大多数情况下不会阻止内核执行。对于大多数用例,数据负载远大于数据存储。因此,合并加载通常比存储更重要。 Adreno GPU 支持合并访问全局和本地内存,但不支持私有内存。

7.2.2 矢量加载/存储

矢量加载/存储是指为单个工作项的多个数据加载/存储。这不同于合并的访问,这是针对各种工作项目的。以下是使用矢量化负载/存储的几个关键点:

  • 每个工作项都应将数据加载到多个字节的一部分中,例如64/128位。可以更好地利用带宽。

    • 例如,可以将多个8位数据手动包装到一个元素(例如64位/128位)中,该元素使用VLOADN加载,然后使用AS_TYPEN函数(例如AS_CHAR16)打开包装。
    • 请参阅第10.3.3节中的矢量化操作示例。
  • 为了获得最佳的 SP 到 L2 带宽性能,加载/存储的内存地址应该是 32 位对齐的。

  • 有两种方法可以进行矢量化加载/存储:

    • 使用内置函数 (vload/vstoren)。
    • 或者,指针转换可用于执行矢量化加载/存储,如下所示:
    char *p1; char4 vec;   
    vec = *(char4 *)(p1 + offset); 
    
  • 使用最多包含四个组件的矢量化加载/存储指令。具有四个以上组件的矢量化数据类型加载将分为多个加载/存储指令,每个指令不超过四个组件。

  • 避免在一个工作项中加载太多数据。

    • 加载太多数据可能会导致更高的寄存器占用空间,从而导致更小的工作组大小并​​损害性能。
    • 在最坏的情况下,它可能会导致寄存器溢出,即编译器必须使用系统 RAM 来存储变量。

向量化 ALU 计算也可以提高性能,但通常不如向量化内存加载/存储那样多。

7.2.3 最佳数据类型

数据类型至关重要,因为它不仅影响内存流量,而且影响 ALU 操作。以下是数据类型的一些规则:

  • 检查应用流水线每个阶段的数据类型,并确保使用的数据类型在整个流水线中保持一致。
  • 如果可能,使用较短的数据类型以减少内存提取/带宽并增加可用于执行的 ALU 数量。

7.2.4 16 位与 32 位数据类型

强烈建议在 Adreno GPU 上使用 16 位数据类型而不是 32 位数据类型,原因有二:

  • 16 位 ALU 的计算能力(以 gflops 衡量)操作是 32 位的两倍,这要归功于 Adreno 专用于 16 位 ALU 计算的硬件加速逻辑。
  • 加载/存储 16 位数据与 32 位数据相比可以节省一半的带宽。

特别是 16 位浮点数,也称为半浮点数 (FP16),非常适合某些机器学习和图像处理用例。请注意,16 位 half 的数据范围和精度比 32 位浮点数据 (FP32) 更受限制。例如,它只能在整数值上准确表示 [0, 2048]。开发人员必须意识到精度损失问题。

使用16位的另一种方法是将/存储数据作为16位加载,而如果精确损失不可接受,则计算零件可能是32位。与使用32位数据相比,这将节省一半的内存流量。

7.3 OpenCL 1.x 中的原子函数

OpenCL 1.x 支持局部和全局原子函数,包括atomic_add、atomic_inc、atomic_min、atomic_max 等。注意这里讨论的原子函数与7.5 节中共享虚拟内存(SVM)中的原子函数不同. Adreno GPU 在硬件上支持所有这些。以下是使用原子函数时的一些规则:

  • 避免许多工作项对单个全局/本地内存地址频繁执行原子操作。
    • 原子操作是序列化和不可分离的操作,可能需要对内存地址进行锁定和解锁。
    • 因此,许多工作项在单个地址上的原子操作是不可取的。
  • 尝试首先进行减少,例如,首先使用局部原子原子,并在原子上对全局内存进行一次更新。
    • 在 Adreno GPU 中,每个 SP 都有自己的本地内存原子引擎。如果在地址相同的情况下使用全局内存原子,首先执行本地原子有助于减少访问争用。

7.4 零拷贝

Adreno Opencl提供了一些机制,以避免在主机侧可能发生的昂贵内存副本。根据如何创建内存对象,存在一些选项,以防止过多的副本。本节介绍了一些实现零拷贝的基本方法,第7.5节提出了一种具有共享虚拟内存(SVM)的更高级技术。

7.4.1 使用映射替代复制

假设OpenCL应用程序对数据流有完全控制,即,目标和源内存对象创建均由OpenCL应用程序管理。对于这种简单的情况,可以使用以下步骤来避免内存副本:

  • 创建缓冲区/图像对象时,请使用flag cl_mem_alloc_host_ptr,并按照以下步骤操作:
首先在clCreateBuffer中设置cl_mem_flags输入:
cl_mem Buffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, sizeof(cl_ushort) * size, NULL, &status); 
  • 然后使用 map 函数返回指向主机的指针:
cl_uchar *hostPtr = (cl_uchar *)clEnqueueMapBuffer(commandQueue,Buffer, CL_TRUE,CL_MAP_WRITE,0,sizeof(cl_uchar) * size,0, NULL, NULL, &status);
  • 主机使用Pointer Hostptr更新缓冲区。

    • 例如,主机可以填充摄像头数据或从磁盘读取数据到缓冲区中。
  • 解除对象映射:

status = clEnqueueUnmapMemObject(commandQueue, Buffer,  (void *) hostPtr, 0, NULL, NULL); 
  • OpenCL 内核可以使用该对象。

cl_mem_alloc_host_ptr是避免在这种情况下复制数据的唯一方法。使用其他标志,例如cl_mem_use_host_ptr或cl_mem_copy_host_ptr,驱动程序将必须为GPU执行额外的内存副本才能访问。

7.4.2 避免对非 OpenCL 分配的对象进行内存复制

7.4.2.1 ION/dmabuf 内存扩展

假设一个内存对象最初是在 OpenCL API 的范围之外创建的,并使用 ION/DMA-BUF 进行分配。在这种情况下,开发人员可以使用 cl_qcom_ion_host_ptr 或 cl_qcom_dmabuf_host_ptr 扩展来创建缓冲区/图像对象,这些对象映射到 GPU 可访问的内存中,而不会产生额外的副本。

可应要求提供说明 cl_qcom_ion_host_ptr 和 cl_qcom_dmabuf_host_ptr 用法的示例。

7.4.2.2 QTI Android 原生缓冲区 (ANB) 扩展

ANB(由Gralloc分配)必须在许多相机和视频处理用例中共享。可以共享,因为缓冲区基于ION。但是,开发人员需要从这些缓冲区中提取内部手柄以使用ION路径,这需要访问QTI的内部标头。 cl_qcom_android_native_buffer_host_ptr扩展名提供了一种与OpenCL共享ANB的更简单的方法,而无需访问QTI标头。这使ISV和其他第三方开发人员能够为ANB实施零拷贝技术。

可应要求提供说明 cl_qcom_android_native_buffer_host_ptr 扩展的用法的示例。

7.4.2.3 Android 硬件缓冲区 (AHB) 扩展

与上述 ANB 扩展一样,cl_qcom_android_ahardwarebuffer_host_ptr 扩展提供了一种与 OpenCL 共享 AHB 而无需提取内部 ION 句柄的简单方法,从而实现零拷贝 AHB 应用程序。

可应要求提供说明 cl_qcom_android_ahardwarebuffer_host_ptr 扩展的用法的示例。

7.4.2.4 使用标准 EGL 扩展

cl_khr_egl_image扩展名从EGL图像创建OPENCL图像。随之而来的主要好处是:

  • 这是标准的;使用此技术编写的代码很可能适用于支持它的其他 GPU。
  • EGL/CL 扩展(cl_khr_egl_event 和 EGL_KHR_cl_event)与此扩展一起使用可以实现更高效的同步。
  • 使用 EGL_IMG_image_plane_attribs 扩展,YUV 处理会更容易一些。

7.5 共享虚拟内存(SVM)

作为 OpenCL 2.0 标准引入的一项重要的高级特性,SVM 允许主机和设备共享和访问相同的内存空间,避免过多的数据复制,例如,现在可以访问 OpenCL 设备上的主机指针。

SVM 有几种类型可供 GPU 选择支持。从 Adreno A5x GPU 开始,粗粒度 SVM 和更高级的带有原子的细粒度缓冲区 SVM 都得到支持。

  • 对于粗粒度 SVM,仅在使用 map/unmap 函数(即 clEnqueueSVMMap 和 clEnqueueSVMUnMap)的同步点处保证内存一致性。
    • 因此,粗粒度 SVM 类似于第 7.4.1 节中描述的零拷贝技术,因为它们都需要映射和取消映射操作。
    • 尽管如此,粗粒度 SVM 仍允许应用程序在主机和设备之间使用和共享基于指针的数据结构。
  • 细粒度缓冲 SVM 消除了粗粒度 SVM 中映射/取消映射同步的要求。
    • 细粒度缓冲区SVM是“无MAP” SVM,即主机和设备可以同时修改同一内存区域。
      • 尽管如此,它仍需要一定程度的同步。
    • 根据主机和设备之间的数据访问模式,可能需要不同类型的同步。
      • 如果对主机和设备之间的相同数据没有读取依赖性,例如,主机和设备正在处理SVM内存对象的单独部分,则不需要原子/围栏。
        • 在这种情况下,在OPENCL同步点(例如,在通话后,所有数据都将是最新的,都将确保内存一致性。
    • 如果对内存访问顺序有依赖或要求,比如主机修改了一个数据,设备需要使用新数据,则需要 atomic 或 fence。
      • 创建它时,SVM 缓冲区必须具有标志 CL_MEM_SVM_ATOMICS。
      • 在内核内部,必须使用 memory_scope_all_svm_devices。
      • 一组 C11 风格的原子函数必须与适当的内存范围、顺序和原子标志一起使用。

开发人员需要仔细评估 SVM 的优缺点。作为一项高级功能,用于 GPU 的 SVM 的示例性和高性能实现通常需要复杂的硬件设计。实施所有这些高级数据共享和同步有一个隐藏的成本,开发人员可能没有意识到这一点。在复杂的现实生活用例中观察 SVM 的好处的门槛相对较高。开发人员应谨慎使用 SVM,尤其是在主机和设备之间存在大量数据依赖性的情况下。此类用例中的同步成本可能会破坏共享虚拟内存空间的好处。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值