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

使用 Fortran 和 OpenMP 解决异构编程挑战

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2023 年 5 月 8 日

CPOL

5分钟阅读

viewsIcon

2711

使用开放、标准的编程语言表达异构并行性

技术的快速创新正在推动异构计算的新时代。硬件的多样性和日益增长的计算需求要求能够利用异构并行性的编程模型。这些模型还必须是开放且可移植的,以便程序能够运行在不同供应商的硬件上。尽管 Fortran 已经有几十年的历史,但它在科学和工程领域仍然是一种活跃且重要的编程语言。同样,OpenMP*,这个于 1997 年发布的用于编译器指令并行化的开放标准,也已发展到支持异构计算。它现在包含将计算卸载到加速器设备和在独立内存之间移动数据的指令。主机和设备内存的概念,以及其他更微妙的内存类型,如纹理/表面和常量内存,都通过 OpenMP 指令暴露给开发者。

将任务卸载到加速器可以使一些计算更有效率。例如,高度数据并行的计算可以利用 GPU 中的大量处理单元。本文将展示 Fortran + OpenMP 如何解决三个主要的异构计算挑战:将计算卸载到加速器、管理独立内存以及在目标设备上调用现有 API。

将计算卸载到加速器

让我们从一个例子开始。图 1 展示了 OpenMP 的 targetteamsdistribute parallel do 结构如何执行一个嵌套循环。target 结构在目标设备上创建一个并行区域。teams 结构创建了一个团队联盟(即线程组)。在示例中,团队的数量小于或等于 num_blocks 参数。每个团队拥有的线程数小于或等于变量 block_threads。每个团队的主线程执行 teams 区域中的代码。外层循环的迭代被分配给每个团队的主线程。当一个团队的主线程遇到 distribute parallel do 结构时,其团队中的其他线程将被激活。该团队执行并行区域,然后工作共享内层循环的执行。这在图 2 中进行了示意说明。

program target_teams_distribute
    external saxpy

    integer, parameter :: n = 2048, num_blocks = 64
    real, allocatable  :: A(:), B(:), C(:)
    real               :: d_sum = 0.0
    integer            :: i, block_size = n / num_blocks
    integer            :: block_threads = 128

    allocate(A(n), B(n), C(n))
    A = 1.0
    B = 2.0
    C = 0.0

    call saxpy(A, B, C, n, block_size, num_blocks, block_threads)

    do i = 1, n
        d_sum = d_sum + C(i)
    enddo

    print '("sum = 2048 x 2 saxpy sum:"(f))', d_sum

    deallocate(A, B, C)
end program target_teams_distribute

subroutine saxpy(B, C, D, n, block_size, num_teams, block_threads)
    real    :: B(n), C(n), D(n)
    integer :: n, block_size, num_teams, block_threads, i, i0

    !$omp target map(to: B, C) map(tofrom: D)
    !$omp teams num_teams(num_teams) thread_limit(block_threads)
    do i0 = 1, n, block_size
        !$omp distribute parallel do
        do i = i0, min(i0 + block_size - 1, n)
            D(i) = D(i) + B(i) * C(i)
        enddo
    enddo
    !$omp end teams
    !$omp end target
end subroutine
图 1. 使用 OpenMP* 指令(蓝色显示)将嵌套循环卸载到加速器

图 2. OpenMP* target、teams 和 distribute parallel do 区域的概念图

主机-设备数据传输

现在,我们将注意力转向内存管理以及主机和设备之间的数据移动。OpenMP 提供了两种方法。第一种方法使用 data 结构来映射独立内存之间的数据。例如,在图 1 中,target 指令上的 map(to: B, C)map(tofrom: D) 子句将数组 BCD 复制到设备,并从设备检索 D 中的最终值。第二种方法是调用设备内存分配器,这是一个 OpenMP 运行时库例程。本文将不涵盖后一种方法。

图 3 中,target data 结构创建了一个新的设备数据环境(也称为目标数据区域),并将数组 ABC 映射到其中。target data 区域包含两个 target 区域。第一个创建了一个新的设备数据环境,该环境根据 map(to: A, B)map(from: C) 数据移动子句,从其包围的设备数据环境中继承了 ABC。主机等待第一个目标区域完成,然后在数据环境中为 A 和 B 赋值。target update 结构会更新设备数据环境中的 AB。当第二个目标区域完成时,当退出设备数据环境时,C 中的结果将从设备复制到主机内存。这一切都在图 4 中进行了示意说明。

program target_data_update
    integer           :: i, n = 2048
    real, allocatable :: A(:), B(:) ,C(:)
    real              :: d_sum = 0.0

    allocate(A(n), B(n), C(n))
    A = 1.0
    B = 2.0
    C = 0.0

    !$omp target data map(to: A, B) map(from: C)
    !$omp target
    !$omp parallel do
    do i = 1, n
        C(i) = A(i) * B(i)
    enddo
    !$omp end target

    A = 2.0
    B = 4.0

    !$omp target update to (A, B) map
    !$omp target
    !$omp parallel do
    do i = 1, n
        C(i) = C(i) + A(i) * B(i)
    enddo
   !$omp end target
   !$omp end target data

    do i = 1, n
        d_sum = d_sum + C(i)
    enddo

    print '("sum = 2048 x (2 + 8) sum:"(f))', d_sum

    deallocate(A, B, C)
end program target_data_update
图 3. 创建设备数据环境。

图 4. 图 3 中 OpenMP* 程序的宿主-设备数据传输。每个箭头表示主机和设备内存之间的数据移动。

使用 Intel® Fortran 编译器和 OpenMP 目标卸载在 Linux* 上编译前面示例程序的命令是

$ ifx -xhost -qopenmp -fopenmp-targets=spir64 source_file.f90

从 OpenMP 目标区域使用现有 API

使用 Fortran、oneMKL 和 OpenMP 加速 LU 分解* 中涵盖了从 OpenMP 目标区域调用外部函数。简而言之,dispatch 指令告诉编译器在关联的子程序或函数调用周围生成条件分派代码。

    !$omp target data
    !$omp dispatch
    call external_subroutine_on_device
    !$omp end target data

如果目标设备可用,则在设备上调用结构化块的变体版本。

Intel Fortran 支持

Intel® Fortran 编译器 (ifx) 是一个新编译器,基于 Intel Fortran 编译器 Classic (ifort) 的前端和运行时库,但它使用 LLVM(低级虚拟机)后端。有关更多信息,请参阅 Intel Fortran 编译器 Classic 和 Intel Fortran 编译器开发指南和参考。它与二进制文件 (.o/.obj) 和模块 (.mod) 兼容,支持最新的 Fortran 标准(95、2003、2018)以及通过 OpenMP(v5.0 和 v5.1)进行的异构计算。 Fortran 进行异构并行性的另一种方法 是标准的 do concurrent 循环。

program test_auto_oft load
    integer, parameter :: N = 100000
    real               :: a(N), b(N), c(N), sumc

    a = 1.0
    b = 2.0
    c = 0.0
    sumc = 0.0
    
    call add_vec

    do i = 1, N
        sumc = sumc + c(i)
    enddo

    print *,' sumc = 300,000 =', sumc

    contains
        subroutine add_vec
            do concurrent (i = 1:N)
                c(i) = a(i) + b(i)
            enddo
        end subroutine add_vec

end program test_auto_offload

按如下方式编译此代码,OpenMP 运行时库将生成设备内核代码

$ ifx -xhost -qopenmp -fopenmp-targets=spir64 \
> -fopenmp-target-do-concurrent source_file.f90

‑fopenmp‑target‑do‑concurrent 标志指示编译器自动为 do concurrent 循环生成设备内核。

通过设置以下环境变量,OpenMP 运行时可以提供内核活动配置文件

$ export LIBOMPTARGET_PLUGIN_PROFILE=T

运行可执行文件将产生输出

当程序执行时,请查找输出中的子程序名称“add vec”,例如。

Kernel 0 : 
__omp_offloading_3b_dd004710_test_auto_offload_IP_add
_vec__l10

Fortran 语言委员会正在就一项提案进行工作,旨在将归约操作添加到 2023 标准的 do concurrent 中,即。

do concurrent(i = 1:N) reduce(+:sum)

结束语

我们概述了使用 Fortran 和 OpenMP 进行异构并行编程。正如我们在上面的代码示例中所见,OpenMP 是一种描述性的并行性表达方法,通常是非侵入性的。换句话说,如果未启用 OpenMP 指令,底层的顺序程序仍然是完整的。代码在没有加速器设备的同构平台上仍然可以正常工作。Fortran + OpenMP 是实现异构并行性的一种强大、开放且标准的方法。

© . All rights reserved.