OpenCL™ Device Fission 助力 CPU 性能

下载 PDF

概要

Device Fission 是 OpenCL™ 规范的一种特性,可为 OpenCL 编程人员提供更强的能力和控制,以更好地管理哪些计算单元运行 OpenCL 命令。 从根本上讲,Device Fission 支持将设备再次划分为一个或多个子设备,如果使用得当,这可以提供出色的性能优势,尤其是在运行 CPU 时。

面向 OpenCL™ 应用的英特尔® 软件开发套件是专为基于英特尔® 架构的平台上的 OpenCL 应用提供的一个全面的软件开发环境。 该软件开发套件支持开发人员在使用 Windows* 和 Linux* 操作系统的英特尔® CPU 上开发 OpenCL 应用并以其为目标。

面向 OpenCL 应用的英特尔软件开发套件可提供一套丰富的 OpenCL 扩展和可选特性,帮助开发人员充分利用英特尔 CPU 上的全部可用资源。 本文主要介绍此软件开发套件中的一个特性 — device fission。

免费下载面向 OpenCL 应用的英特尔软件开发套件: www.intel.com/software/opencl

 

什么是 Device Fission?

OpenCL 规范由多个层级的模型组成,包括平台、运行、内存和编程模型。 最高级别的模型平台模型由与一个或多个 OpenCL 设备的主机处理器组成。 OpenCL 通过主机处理器运行提交而来的命令。 设备可以是 CPU、GPU 或其他加速器设备。 更进一步来讲,一台设备包含一个或多个计算单元。 例如,对于多核 CPU,计算单元是在内核上运行的一条线程。 对于 GPU 而言,计算单元是在一个流处理器或流式多处理器上运行的一条线程。 随着计算单元和线程的数量不断增加,需要开发一些机制来控制这些资源,而非仅将其当做一个同构计算资源。

OpenCL 规范中添加了一个重要的特性,名为 Device Fission,以便支持 OpenCL 编程人员控制哪些计算单元执行 OpenCL 命令。 Device Fission 在 OpenCL 1.2 规范中定义(之前是 OpenCL 1.1 的扩展)。

Device fission 是一款强大的功能,支持将设备再次划分为两个或多个子设备。 Google Dictionary 将 fission 定义为“将某物划分为两个或多个部分的操作”。 从 OpenCL 平台上确认并选择一台设备后,您可以进一步将设备划分一个或多个子设备。

我们可以使用多种方法来确定如何创建子设备。 每台子设备都有自己的环境和工作队列及其自己的程序(如果需要)。 这支持在各工作队列间实现更高级的任务并行化。

子设备的运行方式与设备在 OpenCL API 中的运行方式相同。 使用设备用作参数的 API 调用可以使用子设备用作参数。 换而言之,对于子设备而言,API 与新建没有差别。 与设备相同,环境或命令队列可以针对子设备创建。 使用子设备支持参考原设备内的特定计算单元。

子设备还可进一步再次划分为更多子设备。 每台子设备都有一个从其衍生而来的母设备。 创建子设备不会破会原来的母设备。 母设备和所有派生的子设备可以一起使用(如有需要)。

Device Fission 可以当作一个高级功能,用以提高 OpenCL 代码的性能和/或高效管理计算资源。 使用 Device Fission 需要对底层目标硬件有一定的了解。 Device Fission 需要小心使用,如果使用不当,将会影响代码的可移植性和性能。

为何使用 Device Fission?

通常,Device Fission 可帮助编程人员更好地控制硬件平台,支持他们选择由 OpenCL 运行时使用哪些计算单元执行命令。 Device Fission 的出色之处在于,如果使用得当,将可提供更出色的 OpenCL 性能,或提高平台的总体效率。

以下是几个示例案例:

  • Device Fission 允许使用部分设备。 当设备上有其他非 OpenCL 作业需要资源时,此特性将会非常有帮助。 它可以确保 OpenCL 运行时不会占用整个设备。
  • Device Fission 允许在不同的任务项之间共享特定内容,如共享 NUMA 节点。
  • Device Fission 允许创建一系列子设备,并且支持每台子设备拥有自己的命令队列。 它允许主处理器控制这些队列,并根据需要向子设备分配任务。
  • Device Fission 允许使用特定子设备,以利用数据局部性特性。

在本文的后续内容中,我们将具体讨论使用 Device Fission 的策略,但是首先我们将为大家介绍如何为 Device Fission 编码。

如何使用 Device Fission

本部分将概括介绍如何使用 Device Fission,以及如何在面向 OpenCL 应用的英特尔软件开发套件中创建子设备。 请参考 OpenCL 1.2 规范的第 4.3 节(划分设备)进行进一步了解。

创建子设备时可用的划分类型和选项:

  • 平均划分 — 尽可能多地将设备划分为多个子设备,每台子设备包含指定数量的计算设备。
  • 按数量划分 — 按照每台子设备中的既定计算单元数量划分设备。 可提供每台子设备的目标计算单元数量列表。
  • 按名称划分 — 按照设备名称指定的计算单元划分设备。 它是面向 OpenCL 应用的英特尔软件开发套件支持的英特尔扩展。 参阅 2013 年 8 月 15 日发布的 OpenCL 扩展 20 版本 2。

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: 返回设备根据属性确定的划分方案划分子设备的数量。 如果 num_devices_ret 为控制,请忽略。

划分属性

理解划分属性对于将设备划分为子设备非常关键。 确定划分类型(平均、按数量或按名称)后,对属性列表进行编写,使其作为一个参数在 clCreateSubDevices 调用中传递。 属性列表首先包括要使用的划分类型,其次包括其他进一步定义划分类型和其他信息的属性,最后以 0 值结尾。 下一部分内容中展示了一些属性列表示例,以便帮助大家了解此概念。

属性列表中的起始划分属性包括以下划分类型:

  • CL_DEVICE_PARTITION_EQUALLY
  • CL_DEVICE_PARTITION_BY_COUNTS
  • CL_DEVICE_PARTITION_BY_NAME_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。

属性列表示例

为了解释该示例,我们选择了一个示例目标机器作为我们设备。 目标机器是一个采用 2 枚处理器的 NUMA 平台,每处理器包含 4 个内核。 这台机器中总共包含 8 个物理内核。 机器上启用了英特尔® 超线程技术(英特尔® HT 技术)。 该机器总共有 16 条逻辑线程。 在本示例中,处理器 0 的逻辑线程被操作系统编为 0、1、2、3、4、5、6 和 7,处理器 1 的逻辑线程被编为 8、9、10、11、12、13、14 和 15。

每枚处理器包含一个共享三级高速缓存,以便 4 个内核共享。 每个内核都有自己的一级和二级高速缓存。 启用英特尔 HT 技术后,每个内核包含 2 条线程,因此一级和二级高速缓存在 2 条线程之间共享。 没有四级高速缓存。 参见图 1。


图 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 个计算单元。

Error! 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。

英特尔® HT 技术和计算单元

如果启用英特尔 HT 技术,一个计算单元相当于一条线程。 两条线程共享一个内核。 如果禁用英特尔 HT 技术,一个计算单元相当于一个内核。 一条线程在一个内核上运行。 编写的代码应可处理两种情况。

子设备环境

创建子设备后,您可以使用 clCreateContext 调用为其创建环境。 请注意,如果您使用 clCreateContextFromType 从某种类型的设备创建环境,所创建的环境不会参考从这种类型的设备创建的子设备。

子设备程序

与为设备创建程序一样,也可为不同的子设备创建各种程序。 这是执行任务并行的一种高效方法。 可以针对不同的子设备创建不同的程序。

还可以在设备和子设备之间共享程序。 程序二进制码可以在设备和子设备之间共享。 为一台设备构建的程序二进制码可以用于从该设备创建的所有子设备。 如果子设备没有程序二进制码,将使用母程序。

子设备划分

子设备创建后,还可进一步划分,即通过子设备创建子设备。 设备之间的关系构成了一棵树,原设备是根设备,位于树的顶端。

每台子设备都有一台母设备。 根设备没有母设备。


图 2. 设备划分示例

图 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 可为特定子设备减少参考计数。

其它注意事项

使用 Device Fission 时,您需要确认以下内容:

  • 您的设备支持 Device Fission
  • 可创建的最大子设备数量
  • 支持 Device Fission 划分类型。 这可以使用 GetDeviceInfo 调用进行查看。

创建子设备后,进行查看,确认设备是否真得创建正确。

此外,还应该确保您的代码足够强大,能够应对未来的平台变化。 请思考一下,如何使您的代码应对未来目标硬件架构的变化。 请思考一下,在出现以下情况时,如何在目标机器上运行代码:

  • 新的或不同的高级缓存层级
  • NUMA 或非 NUMA 平台
  • 更多或更少的计算单元
  • 异构计算节点
  • 启用或禁用英特尔 HT 技术

Device Fission 代码示例

本章展示了一些简单的代码示例,以解释 Device Fission。

代码示例一 — 平均划分

在本代码示例中,我们使用平均划分的方法尽可能多地划分子设备,每台子设备包含 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);

使用 Device Fission 的策略

有许多不同的策略可以使用 Device Fission 改进 OpenCL 程序的性能或有效管理计算资源。 这些策略并不互相排斥,可以同时使用一个或多个策略。

利用策略的前提是,必须真正理解您工作负载的特点,以及它如何在目标平台上运行。 您对工作负载的了解越深入,对平台的利用则越充分。

策略 1: 创建高优先级任务

Device Fission 可用来为高优先级任务创建子设备,从而在专用内核上运行。 为了确保高优先级任务在需要时有充分的资源运行,应为该任务保留一个或多个内核。 此做法旨在确保非关键型任务不对高优先级任务产生干扰。 高优先级任务可以充分利用所有的内核资源。

策略: 使用按数量划分的方法创建一台包含一个或多个内核的子设备和一台包含其他内核的子设备。 指定内核可专用于运行在该子设备上的高优先级任务。 其他较低优先级的任务可以分配到另一台子设备。

策略 2: 利用共享高速缓存或通用 NUMA 节点

如果工作负载要求在程序的不同任务项之间进行高级别数据共享,那么创建一个所有的计算单元共享一个高速缓存或位于相同 NUMA 节点的子设备将可改进性能。 没有 Device Fission,将无法保证不同的任务项共享一个高速缓存或共享相同的 NUMA 节点。

策略: 创建共享通用三级高速缓存或同位于相同 NUMA 节点的子设备。 使用按名称划分的方法创建一个共享三级高速缓存或 NUMA 节点的子设备。

策略 3: 利用数据重新使用和关联

如果没有 Device Fission,将任务提交到任务队列可能会将其分配至之前未使用过的内核,即“冷”内核。 “冷”内核的指令和数据高速缓存以及 TLB (用于地址转换的高速缓存) 可能没有任何与 OpenCL 程序相关的数据和指令。 数据和指令将需要一段时间才能够进入内核,并存放至高速缓存和 TLB。 通常情况下这没有问题,但是如果代码过长时间不运行,将会产生问题。 程序可能会在预热完处理器高速缓存后便结束。 通常,这对于中长期运行的程序不是问题。 预热处理器损耗的时间可能会导致运行时间更长。 对于运行时间非常短的程序,这可能会是一个问题。 在这种情况下,您需要使用已经预热好的处理器,方法是确保将程序的后续运行路由至之前使用的处理器。 当从许多较小的程序创建较大的应用时将会出现这种问题。 在当前程序之前运行的程序可访问数据并将其传送至处理器。 后续程序可以利用该成果。

策略: 使用按照数量划分的方法创建子设备,为工作队列指定特定内核。 尝试重新使用内核已预热的高速缓存和 TLB,尤其是运行时间较短的程序。

策略 4: 支持任务并行化

对于某些类型的程序,Device Fission 可以提供更好的环境以支持任务并行化。 并行化支持是 OpenCL 的内在特性,支持为一台设备创建多个工作队列。 借助创建子设备的功能,您可以进一步提升该模型的性能。 创建各有自己工作队列的子设备可实现更复杂的任务并行和运行时控制。 比如,像“流程图”一样运行的应用,其中构成应用的各种任务之间的相关性可以帮助确定程序的运行。 程序内的任务相当于流程图中的节点。 节点边缘或与其他节点之间的连接相当于任务相关性。 对于复杂的相关性,包含多个子设备的多个工作队列可独立分配任务,并可确保向前推进。

您也可创建包含不同特征的不同子设备。 可以根据要运行的任务类型来创建子设备。 也有可能主机想要或需要平衡各工作队列之间的任务,而不是将其留给 OpenCL 运行时。

策略: 使用平均划分的方式创建一系列子设备以支持任务并行化。 为每个子设备创建工作队列。 将任务项分配至工作队列。 然后主机可以跨工作队列管理任务。

策略 5: 高吞吐量

有时候,绝对的吞吐量非常重要,但是数据共享不是。 假设有一些高吞吐量作业在多处理器 NUMA 平台上运行,但是作业之间只有有限或没有数据共享。 每个作业需要最大的吞吐量,比如,它可能会使用全部的可用资源,如片内高速缓存。 在这种情况下,如果作业在不同的 NUMA 节点上运行,您将获得最出色的性能。 这可以确保,作业不是在单个 NUMA 节点上运行,从而无需争夺资源。

策略: 使用按照数量划分的方式创建 N 个子设备,一个子设备在一个 NUMA 节点上运行。 然后子设备可以使用所有 NUMA 节点资源,包括所有可用高速缓存。

结论

Device Fission 是 OpenCL 规范的一种特性,可为 OpenCL 编程人员提供更强的能力和控制,以更好地管理哪些计算单元运行 OpenCL 命令。 通过将设备划分为一个或多个子设备,您可以控制 OpenCL 在哪里运行,如果使用得当,则可提供更出色的性能,并可更高效地使用可用计算资源。

Device Fission 特性可在面向 OpenCL 应用的英特尔软件开发套件支持的 OpenCL CPU 设备上使用。 软件开发套件下载链接:www.intel.com/software/opencl


关于作者

Terry Sych 是英特尔公司平台架构支持事业部的资深软件工程师。 于 1992 年加入英特尔,过去 15 年主要负责企业和云应用的性能分析和软件优化方面的工作。 1981 年和 1988 年他分别获得了密歇根大学计算机工程专业的学士学位和明尼苏达大学的电机工程学硕士学位。 他拥有 3 项美国专利。

 

英特尔和 Intel 标识是英特尔在美国和/或其他国家的商标。
OpenCL 和 OpenCL 标识是苹果公司的商标,需获得 Khronos 的许可方能使用。
英特尔公司 © 2014 年版权所有。 所有权保留。
*其他的名称和品牌可能是其他所有者的资产。

有关编译器优化的更完整信息,请参阅优化通知