65.9K
CodeProject 正在变化。 阅读更多。
Home

Intel® 处理器图形的 SGEMM。

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015年5月26日

CPOL

16分钟阅读

viewsIcon

19123

在本文中,我们将演示如何为 Intel® 酷睿™ 处理器上的 Intel® 处理器显卡优化单精度浮点通用矩阵乘法 (SGEMM) 内核,以获得最佳性能。

Intel® Developer Zone 提供跨平台应用程序开发工具和操作指南、平台和技术信息、代码示例以及同行专业知识,以帮助开发人员创新并取得成功。加入我们针对 物联网Android*Intel® RealSense™ 技术Windows* 的社区,下载工具、获取开发套件、与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和本地活动。

引言

在本文中,我们将演示如何为 Intel® 酷睿™ 处理器上的 Intel® 处理器显卡优化单精度浮点通用矩阵乘法 (SGEMM) 内核,以获得最佳性能。我们使用 OpenCL 实现此示例,并大量依赖 Intel 的 cl_intel_subgroups OpenCL 扩展。在简要概述通用矩阵乘法和 cl_intel_subgroups 扩展后,我们将介绍我们的实现,并总结其在第四代和第五代 Intel® 酷睿™ 处理器上的 Intel® 处理器显卡上的性能。

我们要感谢 Brijender Bharti、Tom Craver、Ben Ashbaugh、Girish Ravunnikutty、Allen Hux 和 Qually Jiang 在审阅本文及配套代码方面提供的帮助。

通用矩阵乘法

来自 维基百科矩阵乘法页面:“在数学中,矩阵乘法是一种二元运算,它接受一对矩阵并产生另一个矩阵”。矩阵乘法是一项非常普遍的操作,具有各种实际应用,因此已被实现为多种编程语言。自 1979 年以来,Basic Linear Algebra Subprograms (BLAS) 规范规定了一组通用的例程,用于执行线性代数运算,包括矩阵乘法。有关更多详细信息,请参阅 BLAS 维基百科页面。BLAS 功能有三个级别,在这里我们将考虑 Level 3,它包含形式为

我们在此展示的单浮点精度通用矩阵乘法 (SGEMM) 示例说明了如何高效地利用 OpenCL 对两个稠密方阵执行通用矩阵乘法运算。我们开发的示例目标是 Intel® 第 4 代和第 5 代处理器上的 Intel® 处理器显卡。我们的实现依赖于 Intel 的 cl_intel_subgroups OpenCL 扩展来优化矩阵乘法,以实现更有效的数据共享。

cl_intel_subgroups 扩展

来自 cl_intel_subgroups 扩展规范页面:“本扩展的目的是通过利用工作组中的某些工作项共同执行(“子组”),并且子组中的工作项可以利用工作组中不可用的硬件功能,从而使程序员能够提高应用程序的性能。具体来说,本扩展旨在允许子组中的工作项在不使用本地内存和工作组屏障的情况下共享数据,并利用专用硬件加载和存储数据块。

子组的大小等于 SIMD 宽度(请注意,针对 Intel® 处理器显卡的代码可以编译为 SIMD-8、SIMD-16 或 SIMD-32,具体取决于内核大小,这意味着 8、16 或 32 个工作项分别可以适应执行单元 (EU) 的硬件线程。有关更深入的介绍,请参阅 Stephen Junkins 的优秀论文《Intel® 处理器显卡 Gen8 的计算架构》的第 5.3.5 节 SPMD 编程模型的 SIMD 代码生成)。例如,如果内核编译为 SIMD-8,则子组由 8 个工作项组成,这些工作项共享一个硬件线程的 4 KB 寄存器空间并一起执行。程序员可以使用内核函数 get_sub_group_size 来确定子组的大小。

此示例中我们主要使用两个内核函数:intel_sub_group_shuffleintel_sub_group_block_read。我们使用 intel_sub_group_shuffle 在子组中的工作项之间共享数据;我们使用 intel_sub_group_block_read 从源图像的特定位置读取每个子组成员工作项的数据块。

让我们看下面的代码。假设子组大小为 8。块读取函数 intel_sub_group_block_read4行主序读取源图像中的四个 uint 数据,并将其转换为四个浮点数后存储到子组中每个工作项的私有变量 blockA 中。第一个工作项的数据显示为第一列的四个蓝色块(参见下图)。然后,我们通过第二个参数为 intel_sub_group_shuffle 提供的子组本地 ID,将私有变量 blockA 的值读取到变量 acol0-7 中。执行八次子组 shuffle 后,每个工作项都获得了 4x8 块的完整数据。有关 intel_sub_group_shuffleintel_sub_group_block_read 函数及其操作的详细说明,请参阅 cl_intel_subgroups 扩展规范页面

OpenCL 实现

示例中提供的gemm.cl 文件包含几个不同的实现,演示了如何为 Intel® 处理器显卡优化 SGEMM 内核。我们从一个朴素内核开始,然后是使用本地内存的内核和使用 cl_intel_subgroups 扩展的内核。由于使用本地内存是优化 OpenCL 内核的常见做法,因此我们重点关注使用 cl_intel_subgroups 扩展的内核。同时,我们也使用了分块(或阻塞)这一著名实践,即将矩阵分成块并单独相乘以保持更好的数据局部性。我们在 4 代 Intel® 酷睿™ 处理器(具有 40 个 EU,运行频率 1.3GHz,理论峰值计算能力1 为 832 Gflops)和 5 代 Intel® 酷睿™ 处理器(具有 23 个 EU,运行频率 900MHz,理论峰值计算能力为 331 Gflops)上测试了内核性能,操作系统为 SUSE Linux Enterprise Server* (SLES) 12 GM,Intel® Media Server Studio 版本为 16.4.2。

内核的命名约定如下:optimizationMethod_blockHeight x blockWidth_groupHeight x groupWidth。内核中的矩阵是按列主序排列的。

朴素内核

让我们先来看朴素实现,它与原始 C 版本非常相似,只是增加了一些 OpenCL C 限定符,并将最外层循环替换为并行内核。在我们的测试环境中,朴素内核的计算效率仅约为 2%~3%。

朴素代码存在两个主要问题。

  1. 全局内存被反复访问,没有任何数据重用/共享;

  2. 每个工作项仅计算一个输出,未能明智地利用寄存器空间;

使用本地内存的内核

L3_SLM_xx_xx 内核将矩阵 A 加载到本地内存,使用屏障同步工作项,然后继续计算。使用本地内存是避免重复访问全局内存的常见优化。在我们的测试环境中,这些内核的计算效率约为 50%。

使用 cl_intel_subgroups 扩展的内核

我们开发了两种使用 cl_intel_subgroups 扩展的内核类型。一种是 L3_SIMD_xx_xx,它从常规 OpenCL 缓冲区加载数据;另一种是 block_read_xx_xx,它使用 intel_sub_group_block_read 从 OpenCL image2D 读取数据块。内核因输入数据访问方式不同而异,但子组内数据共享的基本思想是相似的。以 block_read_4x1_1x8 内核为例。请注意,在此示例中,分块大小太小,效率不高,仅用于说明目的。根据上述命名约定,该内核每个工作项处理 4 * 1 个浮点数,工作组大小为 (1, 8)。下图显示了该内核在子组中的工作方式。“cl_intel_subgroups 扩展”部分也显示了部分内核代码。

此内核编译为 SIMD-8,因此子组大小为 8。在矩阵 A 中,首先使用 intel_sub_group_block_read4 以行主序读取一个 float4(参见矩阵 A 中的 4 个蓝色块),然后调用 intel_sub_group_shuffle 将第一个子组中工作项 1~7 的相邻 7 列 float4 共享(参见矩阵 A 中的 28 个红色块)。在矩阵 B 中,也以行主序读取一个 float8(参见矩阵 B 中的 8 个蓝色块)。我们需要从矩阵 B 读取一个 float8,因为通过 shuffle 函数可以获得矩阵 A 中的 8 列数据。之后,我们可以执行子矩阵乘法 (4 * 8) * (8 * 1) 并获得子矩阵 C (4 * 1) 的部分结果。这是第一个工作项中的第一次读取和计算。

然后,我们将从矩阵 A 中按列主序移动到下一个 (4 * 8) 块,从矩阵 B 中按行主序移动到下一个 (8 * 1) 块(参见矩阵 A 和 B 中的白色块)。换句话说,在第一个工作项中,我们遍历矩阵 A 的前 4 行,遍历矩阵 B 的第一列,最终得到矩阵 C 的第一个 (4 * 1) 块(参见矩阵 C 中的 4 个蓝色块)。在接下来的工作项中,我们将移动到矩阵 A 的下一个 4 行或矩阵 B 的下一列。

分块参数 TILE_M、TILE_K 和 TILE_N 决定了工作组中三个大矩阵的部分矩阵大小。在当前实现中,工作组大小为 (1 * 8),工作项子矩阵 C 的大小为 TILE_M/1 x TILE_N/8 个元素,子矩阵 A 的大小为 TILE_M/1 x (TILE_K/8 * 8) 个元素,子矩阵 B 的大小为 TILE_K/1 x TILE_N/8 个元素。对于子矩阵 A,我们需要乘以 8,因为我们通过 shuffle 函数共享了子组中八个工作项的八列 float4。因此,在此内核中 TILE_M = 4,TILE_K = 8,TILE_N = 8。

当我们应用 cl_intel_subgroups 扩展时,内核性能会进一步提高。在我们的测试环境中,L3_SIMD_xx_xx 的计算效率约为 60%。在第五代 Intel® 处理器上,block_read_xx_xx 的计算效率约为 80%,在第四代 Intel® 处理器上为 65%~70%。我们将在下一节讨论这些内核的性能。

性能

以下是第四代 Intel® 酷睿™ 处理器(具有 40 个 EU,运行频率 1.3GHz)和第五代 Intel® 酷睿™ 处理器(具有 23 个 EU,运行频率 900MHz)上不同 SGEMM 实现的内核性能比较,操作系统为 SLES 12 GM,MSS 版本为 16.4.2。block_read_32x2_8x1 和 block_read_32x1_8x1 在第五代 Intel® 酷睿™ 处理器上显示出约 90% 的计算效率。

优化技巧

屏障和工作组大小对非本地内存内核性能的影响

内置函数 barrier(CLK_LOCAL_MEM_FENCE) 通常在具有本地内存的内核中使用,但在非本地内存内核中,当矩阵大小过大无法放入缓存时,它也可能提供性能优势。让我们看看 L3_SIMD_8x4_1x8、L3_SIMD_8x4_8x8 和 L3_SIMD_8x4_8x8_barrier。L3_SIMD_8x4_1x8 是基本实现,工作组大小从 (1 * 8) 扩大到 (8 * 8) 在 L3_SIMD_8x4_8x8 中。L3_SIMD_8x4_8x8_barrier 在加载矩阵 B 后添加了一个屏障,使工作组的工作项保持同步,以更好地利用 L3 缓存。让我们比较矩阵大小达到 1K 时的性能。性能提升可以在以下图中看到。

分块参数对性能的影响

分块技术通常能提供加速,但我们需要避免因过度使用私有内存而导致的性能下降,这会耗尽寄存器空间。在第四代和第五代 Intel® 酷睿™ 处理器上,每个 EU 线程都有 128 个通用寄存器。每个寄存器存储 32 字节,可作为 32 位数据元素的 8 元素向量访问,总共 4KB。因此,OpenCL 内核中的每个工作项最多可以访问 128、256 或 512(SIMD32 / SIMD-16 / SIMD-8)字节的寄存器空间。如果大的分块参数超过了私有内存阈值,性能将会下降。

很难找到能显示出对不同矩阵大小都具有良好性能的最佳分块参数。某些分块参数可能在某个矩阵大小上运行更快,而在其他矩阵大小上运行更慢。您也可以使用一些自动调优代码生成器来尝试其他分块参数,并在您的设备上获得最佳性能。大分块参数的另一个限制是矩阵大小必须与大分块大小对齐。请比较不同矩阵大小上 L3_SIMD_32x2_1x8、L3_SIMD_16x2_1x8 和 L3_SIMD_8x4_1x8 等内核的性能。

使用 Intel® VTune Amplifier XE 进行 SGEMM 内核优化

我们强烈建议使用 Intel® VTune Amplifier XE 来更深入地了解应用程序在 Intel® 处理器显卡上的性能。在此,我们重点关注 SGEMM 内核在 Intel® 处理器显卡上各种优化的性能分析。有关 Intel® 处理器显卡上 OpenCL 性能分析的整体介绍,请参阅 Julia Fedorova 在参考文献部分的文章。在此示例中,我们选择了 Intel® VTune Amplifier XE 的高级热点分析,启用了分析 GPU 使用情况选项,在分析处理器图形事件中选择了概述,启用了 GPU 上的 OpenCL 性能分析,并选择了在处理器图形上跟踪 OpenCL 内核选项。

测试矩阵大小为 (1024 * 1024),所有内核都在 Windows 8.1 上运行一次,平台为第四代 Intel® 酷睿™ 处理器,配备 Intel HD4400,拥有 20 个 EU,运行频率为 600MHz。以下是按执行时间排序的各种 SGEMM 内核运行的 VTune 屏幕截图。

以 gemm_naive 为例。首先,查看启动的计算线程数(硬件线程),约为 65536,公式为 Global_size / SIMD_width。其次,EU 阵列停顿率以红色突出显示,L3 缺失次数很多,这意味着 EU 因内存数据等待而停顿了约 30% 的时间。根据这些信息,我们可以推断,在内核执行期间,65536 个软件线程中有 20% 的线程没有执行有效工作。由于软件线程数量众多且 EU 停顿率高,内核性能不佳。

由于 SGEMM 不应受内存带宽限制,我们将尝试优化内存访问和布局。我们使用诸如合并内存访问和利用共享本地内存 (SLM) 等通用优化技术。cl_intel_subgroups 扩展提供了另一种优化途径。基本思想是共享数据,让每个工作项执行更多工作,这样数据加载和计算的比例就更均衡。同时,使用向量数据类型和块读取也能提高内存访问效率。

如上表所示,block_read_32x2_1x8 内核性能最佳。EU 阵列停顿率仅为 7.3%,启动了 2048 个软件线程。尽管每个工作项需要一些时间来计算 32*2 个浮点数的块,但这很可能隐藏了内存读取停顿。块读取和 shuffle 函数提供了高效的内存访问,同时内核的分块大小也不会耗尽寄存器空间。我们还可以比较 L3_SIMD_8x4_1x8、L3_SIMD_8x4_8x8 和 L3_SIMD_8x4_8x8_barrier 之间的数据。第 1 个优化技巧中提到的优化提供了更好的缓存性能。L3_SIMD_8x4_8x8 及其屏障版本由于工作组中的同步,L3 缺失次数减少,EU 阵列停顿率降低,从而受益。

控制示例

选项

描述

-h, --help

显示此帮助文本并退出。

-p, --platform <number-or-string>

选择使用其设备的平台。(默认值:Intel)

-t, --type all | cpu | gpu | acc | default | <OpenCL constant for device type>

选择执行 OpenCL 内核的设备类型。(默认值:gpu)

 

-d, --device <number-or-string>

选择执行所有操作的设备。(默认值:0)

-M, --size1 <integer>

 

 

 

 

第一个矩阵的行数(元素)。(默认值:0)

 

-K, --size2 <integer>

 

第一个矩阵的列数(元素),第二个矩阵的行数(元素)(默认值:0)

 

-N, --size3 <integer>

 

第二个矩阵的列数(元素)(默认值:0)

-i, --iterations <integer>

 

内核调用次数。每次调用都会打印性能信息。允许为零:在这种情况下,不执行内核调用,但所有其他主机组件都会被创建。(默认值:10)

 

-k --kernel naive | L3_SIMD_32x2_1x8 | L3_SIMD_16x2_1x8 | L3_SIMD_16x2_4x8 | L3_SIMD_8x4_1x8 | L3_SIMD_8x4_8x8 | L3_SIMD_8x4_8x8_barrier | L3_SLM_8x8_8x16 | L3_SLM_8x8_4x16 | L3_SLM_8x8_16x16 | block_read_32x2_1x8 | block_read_32x1_1x8

 

确定乘法所涉及的矩阵的格式。有几种支持的内核,具有朴素实现和 Intel GPU 上的优化;矩阵 A 和 B 均为列主序;(默认值:NULL)

 

-v --validation

 

启用主机上的验证过程(对于大矩阵速度较慢)。(默认禁用)

 

  1. 每个产品 SKU 的峰值计算能力不同,计算方法如下:(MUL + ADD)xPhysical SIMDxNum FPUsxNum EUsxClock Speed,其中 Physical SIMD 对于带有 Intel® 处理器显卡的 Intel® 处理器为 4。

 

结论

在本文中,我们演示了如何为 Intel® 酷睿™ 处理器上的 Intel® 处理器显卡优化单精度浮点通用矩阵乘法 (SGEMM) 算法,以获得最佳性能。我们使用 OpenCL 实现此示例,并大量依赖 Intel 的 cl_intel_subgroups OpenCL 扩展。正确使用时,cl_intel_subgroups OpenCL 扩展为 SGEMM 内核提供了出色的性能提升。

参考文献

  1. 维基百科矩阵乘法页面

  2. BLAS 维基百科页面

  3. Stephen Junkins 的《Intel® 处理器显卡 Gen8 的计算架构

  4. cl_intel_subgroups 扩展规范页面

  5. Julia Fedorova 的《Intel® VTune™ Amplifier XE:Intel® HD Graphics OpenCL* 性能分析入门

  6. Julia Fedorova 的《使用 Intel® VTune™ Amplifier 2015 XE 分析 OpenCL 应用程序》网络研讨会(请使用 Internet Explorer 查看视频)

  7. Intel® VTune™ Amplifier 2015

  8. Robert Ioffe 的《优化简单的 OpenCL 内核:Modulate 内核优化

  9. Robert Ioffe 的《优化简单的 OpenCL 内核:Sobel 内核优化

关于作者

Lingyi Kong 是 Intel IT Flex Services Group 的软件工程师。他是 GPU 编程和优化的专家,并且在 Intel® Iris 和 Intel® Iris Pro Graphics 上拥有图形驱动/运行时开发经验。

Robert Ioffe 是 Intel 软件与解决方案事业部的技术咨询工程师。他是 OpenCL 编程和 Intel Iris 和 Intel Iris Pro Graphics 上的 OpenCL 工作负载优化的专家,对 Intel 图形硬件有深入了解。他深度参与了 Khronos 标准工作,专注于对最新功能进行原型开发,并确保它们能在 Intel 架构上良好运行。最近,他一直在原型开发 OpenCL 2.0 的嵌套并行性(enqueue_kernel 函数)功能,并编写了许多演示嵌套并行性功能的示例,包括 OpenCL 2.0 的 GPU-Quicksort。他还录制并发布了两个“优化简单 OpenCL 内核”视频和一个关于嵌套并行性的第三个视频。

您可能还对以下文章感兴趣

优化简单的 OpenCL 内核:Modulate 内核优化

优化简单的 OpenCL 内核:Sobel 内核优化

OpenCL 2.0 中的谢尔宾斯基地毯

OpenCL 2.0 中的 GPU-Quicksort:嵌套并行性和工作组扫描函数

© . All rights reserved.