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

OpenMP 目标卸载的新时代

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 12 月 18 日

CPOL

7分钟阅读

viewsIcon

2277

本文将重点探讨 Fortran 的 DO CONCURRENT 语句和 OpenMP 的 target 构造相对于加速器卸载的优缺点。

我们最近的文章探讨了从 Fortran 程序将计算卸载到加速器:

本文将重点探讨 Fortran 的 DO CONCURRENT 语句和 OpenMP 的 target 构造相对于加速器卸载的优缺点。

DO CONCURRENT 构造于 2008 年在 ISO Fortran 中引入,用于告知或断言编译器 DO CONCURRENT 循环的迭代是独立的,可以按任何顺序执行。DO CONCURRENT 循环可以顺序执行,也可以并行执行,甚至可以使用 OpenMP 后端将 DO CONCURRENT 循环卸载到加速器。加速器卸载功能已于 2013 年添加到 OpenMP 4.0 版本中。OpenMP target 指令允许程序员指定要在加速器上执行的代码区域以及在主处理器和加速器之间传输的数据。 

这两种加速器卸载方法都可以移植到任何具有符合标准的编译器的系统。DO CONCURRENT 具有简洁的 ISO Fortran 语法优势。但是,ISO Fortran 存在与 ISO C++ 相同的一些限制。(编者按:关于 ISO C++ 在异构并行方面的限制,请参阅 SYCL* 的优势。)它没有设备或独立内存的概念,因此没有标准的方法可以将控制流转移到加速器或控制主机-设备之间的数据传输。OpenMP 解决了这些限制。它不是 ISO 标准,而是一个拥有 25 年成熟度的开放行业标准。OpenMP target 指令更加冗长,需要程序员向编译器描述并行性,但它们能够精细控制主机-设备之间的数据传输,并可以聚合并行区域以提高效率,正如我们在下面的代码示例中所看到的。 

我们之前的文章《使用 Fortran DO CONCURRENT 进行加速器卸载》应用了一个简单的滤波器来处理二值图像以说明边缘检测。在这里,我们将使用更真实的边缘检测算法进行比较。我们将使用 Fortran DO CONCURRENT 和 OpenMP target 指令来实现 Sobel 算法。该算法对原始图像的每个像素应用水平和垂直滤波器,然后查找变换图像中像素强度的急剧变化:

图 1 展示了一个前后对比的示例。每个像素上的操作都是独立的,因此该算法具有高度数据并行性。

图 1. Sobel 边缘检测

Sobel 算法通常分三个相关的步骤实现:图像平滑、边缘检测和边缘高亮。(编者按:读者可能从我之前的文章中知道,我偏爱多步算法,因为与更简单的算法相比,它们能更好地突出同步和数据依赖性,例如:使用 ArrayFire 和 oneAPI 加速二维傅里叶相关算法。)这些步骤可以使用三个 Fortran DO CONCURRENT 循环轻松编码(图 2)。每个循环都具有显著的数据并行性,具体取决于图像的大小。我们的示例假定是二维图像,但该算法可以扩展到三维图像。 

DO CONCURRENT 构造只是 DO 构造的另一种形式。即使这是您第一次看到 DO CONCURRENT,对于大多数 Fortran 程序员来说,应该清楚这个示例像熟悉的双重嵌套 DO 循环一样遍历图像的列和行。DO CONCURRENT 循环可以有谓词来屏蔽某些迭代,或者有额外的子句来定义变量的作用域(例如,图 2 中的第二个循环包含一个归约操作)。 

包含像素值的结构化数据具有红色、绿色和蓝色通道(字段),但图像在进行 Sobel 边缘检测之前通常会转换为灰度。这减少了计算量,因为在灰度图像中,通道是等效的。因此,我们只需要在 Sobel 实现的每个步骤中操作一个通道。但这不仅减少了计算量。它还减少了必须在主机和设备内存之间传输的数据量,如下文所示。 

gh     = reshape([-1,  0,  1, -2, 0,  2, -1,   0, 1], [3, 3])
gv     = reshape([-1, -2, -1,  0, 0,  0,  1,   2, 1], [3, 3])
smooth = reshape([ 1,  2,  1,  2, 4,  2,  1,   2, 1], [3, 3])

! Smooth the image to reduce noise
do concurrent (c = 2:img_width - 1, r = 2:img_height - 1)
    image_soa%red(r, c) = sum(image_soa%blue(r-1:r+1, c-1:c+1) * smooth / 16
enddo

! Perform Sobel edge detection
do concurrent (c = 2:img_width - 1, r = 2:img_height - 1) reduce(max: max_gradient)
    image_soa%green(r, c) = abs(sum(image_soa%red(r-1:r+1, c-1:c+1) * gh)) + &
                            abs(sum(image_soa%red(r-1:r+1, c-1:c+1) * gv))
    max_gradient = max(max_gradient, image_soa%green(r, c))
enddo

! Highlight the edges based on gradient threshold
do concurrent (c = 1:img_width, r = 1:img_height)
   if (image_soa%green(r, c) >= 0.5 * max_gradient) then
       image_soa%green(r, c) = 0
   else
       _soa%green(r, c) = 255
   endif
   image_soa%red(r, c) = image_soa%green(r, c)
   image_soa%blue(r, c) = image_soa%green(r, c)
enddo
图 2. 使用 Fortran DO CONCURRENT 循环(蓝色高亮)实现的 Sobel 边缘检测。卸载内核以绿色高亮显示。完整代码可在 sobel_do_concurrent.F90 获取。

Intel® Fortran Compiler 可以使用 OpenMP 后端将 DO CONCURRENT 循环卸载到加速器。我们使用以下命令通过 OpenMP 后端和针对“pvc”设备(即 Intel® Data Center GPU Max)的即时编译来构建我们的示例程序,以实现加速器卸载:

$ ifx ppm_image_io.F90 sobel_do_concurrent.F90 -o sobel_dc_gpu -qopenmp \ 
>     -fopenmp-targets=spir64_gen -fopenmp-target-do-concurrent \ 
>     -Xopenmp-target-backend "-device pvc"

第一个源文件(ppm_image_io.F90)是处理图像 I/O 的实用模块。第二个源文件(sobel_do_concurrent.F90)包含图 2 中的代码。我们将对一个 8K(7,680 x 8,404)分辨率的图像(64,542,720 像素 x 4 字节/像素 = 258,170,880 字节)以 PPM 格式运行可执行文件:

我们对在主机(hst)和目标(tgt)设备内存之间传输的红色、绿色和蓝色图像通道进行了颜色编码。三个部分分隔了图 2 中的三个 DO CONCURRENT 循环。(未显示映射到目标设备的数组的 Fortran 数组描述符或 dope 向量。Dope 向量很小,因此可以忽略此数据移动。同样,也未显示 3 x 3 的滤波器矩阵。)主机-设备数据传输是隐式处理的。这对程序员来说很方便,但不一定总是高效。例如,请注意图 2 中的第一个 DO CONCURRENT 循环仅读取蓝色通道并仅写入红色通道。因此,第一个 DO CONCURRENT 循环应仅需要一次 hst→tgt 和一次 tgt→hst 传输。但是,所有三个通道都被传输到设备并返回到主机。 

这有很多不必要的数据移动。在独立内存之间移动数据需要时间和能量,因此最小化主机-设备数据传输对于异构并行性能至关重要。不幸的是,ISO Fortran 2018 和即将发布的 2023 标准没有提供控制数据移动的语言构造,也没有提供告诉运行时数据是只读还是只写的构造。 

幸运的是,OpenMP target 卸载 API 提供了一种方法,可以将我们 Sobel 实现的三个步骤聚合到一个目标数据映射区域中,从而仅在必要时传输数据(图 3)。 

!$omp target data map(tofrom: image_soa%blue(l:img_height, l:img_width), &
!$omp                         image_soa%green(l:img_height, 1:img_width), &
!$omp                         image_soa%red(1:img_height, l:img_width)) &
!$omp             map(to: gh(1:3, 1:3), gv(1:3, 1:3), smooth(1:3, 1:3))

!$omp target teams distribute parallel do collapse(2)
do c = 2, img_width - 1
    do r = 2, img_height - 1
        ! Smooth the image to reduce noise
        image_soa%red(r, c) = sum(image_soa%blue(r-1:r+l, c-1l:c+1l) * smooth) / 16
    enddo
enddo
!$omp end target teams distribute parallel do

!$omp target teams distribute parallel do reduction(max: max_gradient) collapse(2)
do c = 2, img_width - 1
    do r = 2, img_height - 1
        ! Perform Sobel edge detection
        image_soa%green(r, c) = abs(sum(image_soa%red(r-1:r+l, c-1:c+l) * gh)) + &
                                abs(sum(image_soa%red(r-1:r+1, c-1:c+1) * gv))
        max_gradient = max(max_gradient, image_soa%green(r, c))
    enddo
enddo
!$omp end target teams distribute parallel do

!$omp target update from(max_gradient)

!$omp target teams distribute parallel do collapse(2)
do c = 1, img_width
    do r = 1, img_height
        ! Highlight the edges based on gradient threshold
        if (image_soa%green(r, c) >= 0.5 * max_gradient) then
            image_soa%green(r, c) = 0
        else
            image_soa%green(r, c) = 255
        endif
        image_soa%red(r, c) = image_soa%green(r, c)
        image_soa%blue(r, c) = image_soa%green(r, c)
    enddo
enddo
!$omp end target teams distribute parallel do

!$omp end target data
图 3. 使用 OpenMP* target 卸载指令(蓝色高亮)实现的 Sobel 边缘检测。完整代码可在 sobel_omp_target.F90 获取。

 

我们如下编译和运行图 3 中的 OpenMP 实现:

同样,我们删除了小的 dope 向量和滤波器矩阵,并对在主机(hst)和目标(tgt)设备内存之间传输的红色、绿色和蓝色图像通道进行了颜色编码。请注意,通道仅在每个方向上复制一次,这明显少于 DO CONCURRENT 实现。正如您所看到的,与仅使用 ISO Fortran 相比,OpenMP 提供了更精细的数据移动控制。 

在大型图像上计算 Sobel 边缘检测时,OpenMP target 卸载的性能也优于 DO CONCURRENT(表 1)。DO CONCURRENT(图 2)和 OpenMP target(图 3)在主机 CPU 上的性能相当(0.1 秒),但 OpenMP target 卸载到 GPU 上的总体性能最佳(0.05 秒)。事实上,DO CONCURRENT 示例中的不必要数据移动会损害 GPU 性能(0.18 秒)。 

  OpenMP DO CONCURRENT 
Sequential 0.37  0.37 
并行(CPU)  0.1  0.1 
并行(GPU)  0.05  0.18 

表 1. 比较 OpenMP* target 和 Fortran DO CONCURRENT 对 8K(7,680 x 8,404)分辨率图像的性能(所有时间以秒为单位)。顺序基线是未启用 OpenMP 编译的 OpenMP(sobel_omp_target.F90)和 DO CONCURRENT(sobel_do_concurrent.F90)示例。CPU 和 GPU 分别是 Intel® Xeon® Platinum 8480+ 和 Intel® Data Center GPU Max 1100。 

DO CONCURRENT 构造提供了一种方便的语法来隐式表达并行循环,但直到 ISO Fortran 具有独立内存的概念以及相应的控制主机-设备数据传输的语法之前,最好将 Fortran 和 OpenMP 结合用于异构并行。

我们的源代码以及一个小的测试图像可在 sobel_example 中找到。您可以在免费的 Intel® Developer Cloud 上尝试 Fortran DO CONCURRENT 和 OpenMP 加速器卸载,该平台拥有最新的 Intel® 硬件和软件。 

© . All rights reserved.