OpenCL™ 设备分裂以提高 CPU 性能
Intel OpenCL Applications SDK 提供了丰富的 OpenCL 扩展和可选功能,专为希望利用 Intel CPU 上所有可用资源的开发者而设计。本文重点介绍该 SDK 中提供的设备分区功能。
Intel® Developer Zone 提供跨平台应用程序开发工具和操作指南、平台和技术信息、代码示例以及同行专家的经验,帮助开发者创新并取得成功。加入我们的社区,探索 物联网、Android*、Intel® RealSense™ 技术 和 Windows*,下载工具,访问开发套件,与志同道合的开发者交流想法,并参与黑客马拉松、竞赛、路演和本地活动。
摘要
设备分区是 OpenCL™ 规范中的一项功能,它赋予 OpenCL 程序员更大的能力来控制哪些计算单元执行 OpenCL 命令。从根本上说,设备分区允许您将一个设备细分为一个或多个子设备。如果使用得当,这些子设备可以提供性能优势,尤其是在 CPU 上执行时。
Intel® OpenCL™ Applications SDK 是一个全面的软件开发环境,用于在基于 Intel® 架构的平台上开发 OpenCL 应用程序。该 SDK 使开发者能够在 Windows* 和 Linux* 操作系统上,使用 Intel® CPU 开发和定位 OpenCL 应用程序。
Intel OpenCL Applications SDK 提供了丰富的 OpenCL 扩展和可选功能,专为希望利用 Intel CPU 上所有可用资源的开发者而设计。本文重点介绍该 SDK 中提供的设备分区功能。
在以下链接下载您的 Intel OpenCL Applications SDK 免费副本: www.intel.com/software/opencl
什么是设备分区?
OpenCL 规范由平台、执行、内存和编程模型等几个层级模型组成。最高层模型是平台模型,它包含一个主机处理器,该处理器连接到一个或多个 OpenCL 设备。OpenCL 设备执行主机处理器提交给它们的命令。设备可以是 CPU、GPU 或其他加速器设备。一个设备进一步包含一个或多个计算单元。例如,对于多核 CPU,计算单元是在一个核心上执行的线程。对于 GPU,计算单元是在流处理器或流多处理器上执行的线程。随着计算单元和线程数量的不断增长,开发控制这些资源的机制很有用,而不是将它们视为一个单一的同质计算资源。
OpenCL 规范中增加了一项名为“设备分区”的重要功能,用于让 OpenCL 程序员控制哪些计算单元执行 OpenCL 命令。设备分区在 OpenCL 1.2 规范中定义(之前作为 OpenCL 1.1 扩展可用)。
设备分区是一项有用的功能,它允许将一个设备细分为两个或多个子设备。Google 词典将 fission 定义为“将某物分成两个或多个部分的操作”。在识别并从 OpenCL 平台中选择了一个设备后,您可以进一步将其细分为一个或多个子设备。
有几种方法可用于确定子设备的创建方式。每个子设备都可以拥有自己的上下文、工作队列,如果需要,还可以拥有自己的程序。这使得在工作队列之间实现更高级的任务并行。
子设备的作用与 OpenCL API 中的设备作用完全相同。以设备作为参数的 API 调用,可以以子设备作为参数。换句话说,除了创建子设备本身之外,没有专门针对子设备的 API。与设备一样,可以为子设备创建上下文或命令队列。使用子设备可以让您引用原始设备内的特定计算单元。
子设备还可以进一步细分为更多子设备。每个子设备都有一个从中派生的父设备。创建子设备不会销毁原始父设备。如果需要,父设备及其所有后代子设备都可以一起使用。
设备分区可以被视为一项高级功能,它可以提高 OpenCL 代码的性能和/或有效地管理计算资源。使用设备分区需要对底层目标硬件有一定的了解。如果使用不当,设备分区可能会影响代码的可移植性和性能。
为什么要使用设备分区?
总的来说,设备分区通过选择 OpenCL 运行时用于执行命令的计算单元,让程序员能够更好地控制硬件平台。设备分区之所以有用,是因为如果使用得当,它可以提供更好的 OpenCL 性能或提高整个平台的效率。
以下是一些示例场景
- 设备分区允许使用设备的一部分。当设备上有其他非 OpenCL 工作需要资源时,这一点非常有用。它可以确保 OpenCL 运行时不会占用整个设备。
- 设备分区可以允许工作项之间进行专门的共享,例如共享 NUMA 节点。
- 设备分区可以允许创建一组子设备,每个子设备都有自己的命令队列。这使得主机处理器可以控制这些队列,并根据需要将工作分派给子设备。
- 设备分区允许使用特定的子设备来利用数据局部性。
在本文稍后部分,将更详细地讨论使用设备分区的策略,但在此之前,我们将展示如何为设备分区进行编码。
如何使用设备分区
本节将概述如何在 Intel OpenCL Applications SDK 中使用设备分区和创建子设备。有关更多详细信息,请参阅 OpenCL 1.2 规范的第 4.3 节(设备分区)。
创建子设备时可用的不同分区类型和选项是:
- 均等分区 - 将设备尽可能多地划分为子设备,每个子设备包含指定数量的计算单元。
- 按数量分区 - 根据每个子设备中计算单元的数量来划分设备。可以提供每个子设备所需计算单元数量的列表。
- 按名称分区 - 按设备名称指定的计算单元来划分设备。这是 Intel SDK for OpenCL Applications 支持的 Intel 扩展。请参阅 OpenCL Extension #20, Version 2, August 15, 2013。
可以通过查询(本文稍后将介绍)来查询 OpenCL 实现支持的分区类型。在尝试分区任何设备之前,强烈建议您检查您的实现以了解支持的分区类型。
创建子设备
OpenCL 中的 Get Device ID 调用有助于查找平台中可用的 OpenCL 设备。使用 clGetDeviceIDs 调用找到设备后,您可以使用 clCreateSubDevices 调用创建一个或多个子设备。这通常在选择 OpenCL 设备之后、创建 OpenCL 上下文之前完成。
clCreateSubDevices 调用是:
[code]cl_int clCreateSubDevices ( cl_device_id in_device, const cl_device_partition_property *properties, cl_uint num_devices, cl_device_id *out_devices, cl_uint *num_devices_ret)[/code]
in_device
:要分区的设备的 ID。properties
:指定设备如何分区的属性列表。下面将对此进行更详细的讨论。num_devices
:子设备的数量(用于调整 out_devices 的内存大小)。out_devices
:创建的子设备的缓冲区。num_devices_ret
:根据 properties 中指定的分区方案,返回设备可分区成的子设备数量。如果 num_devices_ret 为 NULL,则忽略。
分区属性
理解分区属性是设备分区成子设备的关键。在确定了分区类型(均等分区、按数量分区或按名称分区)后,开发要作为参数传递给 clCreateSubDevices 调用的属性列表。属性列表以要使用的分区类型开始,后跟进一步定义分区类型和其他信息的附加属性,最后以 0 值结束。下一节将展示属性列表示例,以帮助说明该概念。
开始属性列表的分区属性是分区类型:
- CL_DEVICE_PARTITION_EQUALLY
- CL_DEVICE_PARTITION_BY_COUNTS
- CL_DEVICE_PARTITION_BY_NAME_INTEL (Intel 扩展)
列表中的下一个值取决于分区类型:
- CL_DEVICE_PARTITION_EQUALLY 后面跟着 N,即每个子设备的计算单元数量。设备将被尽可能多地划分为具有 N 个计算单元的子设备。
- CL_DEVICE_PARTITION_BY_COUNTS 后面跟着一个计算单元计数列表。对于列表中的每个数字,都会创建一个具有该数量计算单元的子设备。计算单元计数列表以 CL_DEVICE_PARTITION_BY_COUNTS_LIST_END 终止。
- CL_DEVICE_PARTITION_BY_NAME_INTEL 后面跟着此子设备的计算单元名称列表。计算单元名称列表以 CL_DEVICE_PARTITION_BY_NAMES_LIST_END_INTEL 终止。
属性列表中的最后一个值始终为 0。
属性列表示例
为了说明此示例,我们将一个示例目标机器作为我们的设备。目标机器是一个 NUMA 平台,包含 2 个处理器,每个处理器有 4 个核心。该机器总共有 8 个物理核心。Intel® 超线程技术 (Intel® HT Technology) 已启用。该机器总共有 16 个逻辑线程。在此示例中,操作系统将处理器 0 的逻辑线程编号为 0、1、2、3、4、5、6 和 7,将处理器 1 的逻辑线程编号为 8、9、10、11、12、13、14 和 15。
每个处理器都有一个 L3 缓存,所有 4 个核心共享该缓存。每个核心都有私有的 L1 和 L2 缓存。在启用 Intel HT 技术的情况下,每个核心有 2 个线程,因此每个 L1 和 L2 缓存由 2 个线程共享。没有 L4 缓存。请参见图 1。
下表显示了属性列表的示例,假设 OpenCL 实现支持该特定分区类型。
请注意,属性列表始终以分区类型开始,以 0 结束。
表 1. 属性列表示例
属性列表 |
描述 |
在示例目标机器上的结果 |
---|---|---|
{ CL_DEVICE_PARTITION_EQUALLY, 8, 0 } |
将设备尽可能多地划分为子设备,每个子设备有 8 个计算单元。 |
2 个子设备,每个子设备有 8 个线程。 |
{ CL_DEVICE_PARTITION_EQUALLY, 4, 0 } |
将设备尽可能多地划分为子设备,每个子设备有 4 个计算单元。 |
4 个子设备,每个子设备有 4 个线程。 |
{ CL_DEVICE_PARTITION_EQUALLY, 32, 0 } |
将设备尽可能多地划分为子设备,每个子设备有 32 个计算单元。 |
错误!32 超出了 CL_DEVICE_PARTITION_ MAX_COMPUTE_UNITS。 |
{ CL_DEVICE_PARTITION_BY_COUNTS, 3, 1, CL_DEVICE_PARTITION_BY_COUNTS_LIST_END, 0 } |
将设备划分为 2 个子设备,1 个有 3 个计算单元,1 个有 1 个计算单元。 |
1 个子设备有 3 个线程,1 个子设备有 1 个线程。 |
{ CL_DEVICE_PARTITION_BY_COUNTS, 2, 2, 2, 2 CL_DEVICE_PARTITION_BY_COUNTS_LIST_END, 0 } |
将设备划分为 4 个子设备,每个子设备有 2 个计算单元。 |
4 个子设备,每个子设备有 2 个线程。 |
{ CL_DEVICE_PARTITION_BY_COUNTS, 3, 1, CL_DEVICE_PARTITION_BY_COUNTS_LIST_END, 0 } |
将设备划分为 2 个子设备,1 个有 3 个计算单元,1 个有 1 个计算单元。 |
1 个子设备有 3 个线程,1 个子设备有 1 个线程。 |
{ CL_DEVICE_PARTITION_BY_NAMES_INTEL, 0, 1, 7, CL_DEVICE_PARTITION_BY_NAMES_LIST_END_INTEL, 0 } |
将设备划分为 1 个子设备,使用这些特定的逻辑线程。 |
1 个子设备有 3 个线程:线程 0、线程 1 和线程 7。 |
{ CL_DEVICE_PARTITION_BY_NAMES_INTEL, 0, 8, CL_DEVICE_PARTITION_BY_NAMES_LIST_END_INTEL, 0 } |
将设备划分为 1 个子设备,使用这些特定的逻辑线程。 |
1 个子设备有 2 个线程:线程 0 和线程 8。 |
Intel® HT 技术和计算单元
如果启用了 Intel HT 技术,一个计算单元相当于一个线程。两个线程共享一个核心。如果禁用了 Intel HT 技术,一个计算单元相当于一个核心。一个线程在核心上执行。代码应按这两种情况编写。
子设备的上下文
创建子设备后,您可以使用 clCreateContext 调用为它们创建上下文。请注意,如果您使用 clCreateContextFromType 从给定类型的设备创建上下文,则创建的上下文不会引用从该类型设备创建的任何子设备。
子设备的程序
就像为设备创建程序一样,可以为每个子设备创建不同的程序。这是一种进行任务并行化的有效方法。可以为不同的子设备创建不同的程序。
另一种方法是在设备和子设备之间共享程序。程序二进制文件可以在设备和子设备之间共享。为某一个设备构建的程序二进制文件可以与从该设备创建的所有子设备一起使用。如果子设备没有程序二进制文件,则将使用父程序。
子设备的划分
创建子设备后,可以通过从子设备创建子设备来进一步划分它。设备的关系形成一棵树,原始设备是树顶部的根设备。
每个子设备都会有一个父设备。根设备将没有父设备。
图 2 展示了一个设备首先使用按数量分区进行划分,然后其中一个子设备使用均等分区进行划分的示例。
划分子设备可能存在限制。例如,使用按名称分区创建的子设备无法进一步划分。使用其他分区类型创建的子设备无法通过按名称分区进行进一步划分。
查询子设备
clGetDeviceInfo 调用添加了几个功能来访问与子设备相关的信息。
在创建子设备之前,您可以使用 clGetDeviceInfo 查询设备以查看:
- CL_DEVICE_PARTITION_MAX_SUB_DEVICES:可为该设备创建的最大子设备数量。
- CL_DEVICE_PARTITION_PROPERTIES:该设备支持的分区类型。
当然,我们建议检查您想使用的分区类型是否受支持。某些 OpenCL 实现可能不支持所有类型。
创建子设备后,您可以像查询设备一样查询子设备。通过查询,您可以发现以下内容:
- CL_DEVICE_PARENT_DEVICE:给定子设备的父设备。
- CL_DEVICE_PARTITION_TYPE:该子设备当前使用的分区类型。
对根设备和所有后代子设备进行查询,对于几乎所有查询都应返回相同的值。例如,当查询时,根设备和所有后代子设备应返回相同的 CL_DEVICE_TYPE 或 CL_DEVICE_NAME。例外情况是以下查询:
- CL_DEVICE_GLOBAL_MEM_CACHE_SIZE
- CL_DEVICE_BUILT_IN_KERNELS
- CL_DEVICE_PARENT_DEVICE
- CL_DEVICE_PARTITION_TYPE
- CL_DEVICE_REFERENCE_COUNT
- CL_DEVICE_MAX_COMPUTE_UNITS
- CL_DEVICE_MAX_SUB_DEVICES
释放和保留子设备
两个调用允许您维护子设备的引用计数。您可以像其他 OpenCL 对象一样增加(保留)或减少(释放)引用计数。clRetainDevice 增加给定子设备的引用计数。clReleaseDevice 减少给定子设备的引用计数。
其他注意事项
使用设备分区时,您需要检查:
- 您的设备是否支持设备分区。
- 未超过可创建的最大子设备数量。
- 设备分区类型是否受支持。这可以通过 GetDeviceInfo 调用来检查。
创建子设备后,请检查设备是否确实已正确创建。
另外,使您的代码健壮并能够处理未来的平台变化也很重要。考虑您的代码将如何处理目标硬件架构未来的变化。考虑代码在以下目标机器上的执行方式:
- 新的或不同的缓存层次结构
- NUMA 或非 NUMA 平台
- 更多或更少的计算单元
- 异构计算节点
- 启用或禁用了 Intel HT 技术
设备分区代码示例
本节将展示一些简单的代码示例来演示设备分区。
代码示例 #1 - 均等分区
在此代码示例中,我们使用均等分区将设备划分为尽可能多的子设备,每个子设备包含 4 个计算单元。(OpenCL 调用上的错误检查已省略)。
// Get Device ID from selected platform:
clGetDeviceIDs( platforms[platform], CL_DEVICE_TYPE_CPU, 1, &device_id, &numDevices);
// Create sub-device properties: Equally with 4 compute units each:
cl_device_partition_property props[3];
props[0] = CL_DEVICE_PARTITION_EQUALLY; // Equally
props[1] = 4; // 4 compute units per sub-device
props[2] = 0; // End of the property list
cl_device_id subdevice_id[8];
cl_uint num_entries = 8;
// Create the sub-devices:
clCreateSubDevices(device_id, props, num_entries, subdevice_id, &numDevices);
// Create the context:
context = clCreateContext(cprops, 1, subdevice_id, NULL, NULL, &err);
代码示例 #2 - 按数量分区
在此代码示例中,我们按数量分区设备,其中 1 个子设备有 2 个计算单元,1 个子设备有 4 个计算单元。(OpenCL 调用上的错误检查已省略)。
// Get Device ID from selected platform:
clGetDeviceIDs( platforms[platform], CL_DEVICE_TYPE_CPU, 1, &device_id, &numDevices);
// Create two sub-device properties: Partition By Counts
cl_device_partition_property_ props[5];
props[0] = CL_DEVICE_PARTITION_BY_COUNTS; // Equally
props[1] = 2; // 2 compute units
props[2] = 4; // 4 compute units
props[3] = CL_DEVICE_PARTITION_BY_COUNTS_LIST_END; // End Count list
props[4] = 0; // End of the property list
cl_device_id subdevice_id[2];
cl_uint num_entries = 2;
// Create the sub-devices:
clCreateSubDevices(device_id, props, num_entries, subdevice_id, &numDevices);
// Create the context:
context = clCreateContext(cprops, 1, subdevice_id, NULL, NULL, &err);
使用设备分区的策略
有不同的策略可用于使用设备分区来提高 OpenCL 程序的性能或有效地管理计算资源。这些策略并非相互排斥,可以一起使用一个或多个策略。
利用这些策略的一个先决条件是真正了解您的工作负载的特性以及它在目标平台上的表现。您对工作负载了解得越多,就越能更好地利用平台。
策略 #1:创建高优先级任务
设备分区可用于为高优先级任务创建一个子设备,以在专用核心上执行。为了确保高优先级任务在需要时有足够的资源执行,为该任务预留一个或多个核心是合理的。其思想是避免其他不重要任务干扰高优先级任务。高优先级任务可以利用所有核心的资源。
策略:使用按数量分区创建一个包含一个或多个核心的子设备,以及另一个包含剩余核心的子设备。选定的核心可以专用于在该子设备上运行的高优先级任务。其他低优先级任务可以分派到另一个子设备。
策略 #2:利用共享缓存或公共 NUMA 节点
如果工作负载在程序的工作项之间表现出高度的数据共享,那么创建一个所有计算单元共享缓存或位于同一 NUMA 节点内的子设备可以提高性能。没有设备分区,无法保证工作项会共享缓存或位于同一 NUMA 节点。
策略:创建共享公共 L3 缓存或位于同一 NUMA 节点上的子设备。使用按名称分区来创建用于共享 L3 缓存或 NUMA 节点的子设备。
策略 #3:利用数据重用和亲和性
如果没有设备分区,提交工作到工作队列可能会将其分派到一个之前未使用过的“冷”核心。一个“冷”核心是指其指令和数据缓存以及 TLB(地址转换缓存)可能没有任何与 OpenCL 程序相关的数据和指令。数据和指令需要时间才能加载到核心并放入缓存和 TLB。通常这不成问题,但如果代码运行时间不长,则可能是一个问题。到程序预热处理器缓存时,可能已经结束。通常,对于中长时运行的程序,这不成问题。预热处理器的时延可以通过更长的执行时间来分摊。然而,对于运行时间非常短的程序,这可能是一个问题。在这种情况下,您需要通过确保程序的后续执行被路由到与以前使用的相同的处理器来利用预热的处理器。当由许多小型程序组成的更大应用程序创建时,也可能出现这种情况。在当前程序之前执行的程序会访问数据并将其加载到处理器中。后续程序可以利用该工作。
策略:使用按数量分区创建子设备,以指定工作队列的特定核心。尝试重用核心预热的缓存和 TLB,尤其适用于运行时间短的程序。
策略 #4:实现任务并行
对于某些类型的程序,设备分区可以提供一个改进的环境来实现任务并行。OpenCL 内置了对任务并行性的支持,可以通过为设备创建多个工作队列来实现。通过创建子设备,您可以将该模型提升到更高的水平。创建每个子设备并为其分配自己的工作队列,可以实现更复杂的任务并行和运行时控制。例如,某些应用程序的行为类似于“流图”,其中构成应用程序的各种任务之间的依赖关系有助于确定程序执行。程序中的任务可以建模为图中的节点。节点边或与其他节点的连接模拟任务依赖关系。对于复杂的依赖关系,多个具有多个子设备的工作队列可以独立分派任务,并确保取得进展。
您还可以创建具有不同特性的不同子设备。在创建子设备时,可以考虑到它将执行的任务类型。也可能存在主机想要或需要平衡这些工作队列之间的工作,而不是将其留给 OpenCL 运行时的情况。
策略:通过使用均等分区创建一组子设备来实现任务并行。为每个子设备创建工作队列。将工作项分派到工作队列。然后,主机可以管理多个工作队列上的工作。
策略 #5:高吞吐量
有时绝对吞吐量很重要,但数据共享不重要。假设您要在多处理器 NUMA 平台上执行高吞吐量作业,但作业之间数据共享有限或没有数据共享。每个作业都需要最大吞吐量,例如,它可以利用所有可用资源,如片上缓存。在这种情况下,如果作业在不同的 NUMA 节点上执行,您可能会获得最佳性能。这可以确保作业不在单个 NUMA 节点上执行并争夺资源。
策略:使用按数量分区创建 N 个子设备,每个 NUMA 节点一个子设备。然后,子设备可以使用所有 NUMA 节点的资源,包括所有可用的缓存。
结论
设备分区是 OpenCL 规范中的一项功能,它赋予 OpenCL 程序员更大的能力和控制权来管理哪些计算单元执行 OpenCL 命令。通过将设备细分为一个或多个子设备,您可以控制 OpenCL 程序在哪里执行,如果使用得当,可以提供更好的性能并更有效地利用可用的计算资源。
设备分区功能可在 Intel OpenCL Applications SDK 支持的 OpenCL CPU 设备上使用。SDK 可在 www.intel.com/software/opencl 获取。
Intel 和 Intel 徽标是 Intel Corporation 在美国和/或其他国家/地区的商标。
OpenCL 和 OpenCL 徽标是 Apple Inc. 的商标,已获得 Khronos 许可使用。
版权所有 © 2014 英特尔公司。保留所有权利。
*其他名称和品牌可能被声明为他人的财产。