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

使用 oneMKL 和 OpenMP Target Offloading 求解线性系统

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2023年5月11日

CPOL

7分钟阅读

viewsIcon

3094

使用标准、开放的方法加速线性代数计算

上一篇文章《使用 Fortran、oneMKL 和 OpenMP 加速 LU 分解*》展示了如何将 LU 分解及其后续的逆运算卸载到加速器上。文章给出了一些关于通过合并 OpenMP 卸载区域和最小化主机-设备数据传输来减少开销的技巧,但并未涉及求解线性方程组。这留给了读者作为练习,但我们现在就来完成它,以说明《oneAPI GPU 优化指南》中关于“最小化数据传输和内存分配”一章中的一些技术。

我们将展示如何使用 oneMKL 中的批量 LU 分解和求解器函数来求解一组线性方程组,并通过 OpenMP 卸载到加速器上。在实际应用中,我们很少使用逆矩阵因子,因此在本演示中我们将省略该步骤,直接从分解到求解。由于之前的示例代码是用 Fortran 编写的,而且我们也很喜欢它,所以这里将继续使用 Fortran。

让我们从几个小型线性方程组开始,以确认我们能够正确地加载和求解它们。我们在一本旧的线性代数教科书中找到了几个合适的例子(**图 1**)。矩阵是方阵,因此我们可以使用 LU 分解来求解它们。

图 1. 我们将使用 oneMKL 和 OpenMP* 目标卸载来批量求解这两个线性方程组。像这样的小问题适合测试,但远不足以证明加速的必要性。

本文的演示程序与上一篇文章中的程序类似。它首先包含 OpenMP 卸载支持的 Fortran 接口,然后决定是使用 32 位还是 64 位整数类型(**图 2**)。(有关更多信息,请参阅使用 ILP64 接口与 LP64 接口。)这个决定将在编译时做出(参见下面的编译器命令)。**图 1** 中的测试问题包含两个 3x3 的线性方程组,每个方程组都有一个右侧项 (RHS)。相应地设置了 batch_sizennrhsstride 变量,并分配了 abipiv 数组来存储批量方程组。最后四个语句将两个矩阵及其右侧项分别加载到 a 和 b 中。

include "mkl_omp_offtload.t90"
program solve_batched_1inear_systems

! I Decide whether to use 32- or 64-bit integer type
#if defined(MKL_ILP64)
    use onemkl_lapack_omp_offload_ilp64 ! 64-bit
#else
    use onemkl_lapack_omp_offload_1p64  ! 32-bit
#endif

    integer, parameter         :: n = 3, batch_size = 2, nrhs = 1
    integer                    :: lda, stride_a, stride_ipiv
    integer                    :: 1db, stride_b
    real (kind=8), allocatable :: a(:,:), b(:,:)
    integer, allocatable       :: ipiv(:, 1), info(:)
    
    integer allocstat
    character (len = 132) :: allocmsg

    1da         = n
    stride_a    = n * Tda
    stride_ipiv = n
    1db         = n
    stride_b    = n * nrhs

    ! Allocate required memory
    allocate (a(stride_a, batch_size), b(n, batch_size*nrhs), &
        ipiv(stride_ipiv, batch_size),                        &
        info(batch_ size), stat = allocstat, errmsg = allocmsg)
    if (allocstat > 0) stop trim(allocmsg)

    ! Initialize the matrices. Remember that Fortran is a column-major language.
    a(:,1) = (/2.0, 1.0, -6.0, 4.0, -4.0, -9.0, -4.0, 3.0, 10.0/)
    a(:,2) = (/0.0, 2.0, 6.0, 0.0, 4.0, 5.0, 3.0, -1.0, 5.0/)
    b(:,1) = (/12.0, -21.0, -24.0/) ! x = (-4, 2, -3)
    b(:,2) = (/ 6.0, -10.0, -7.0/) ! x = (-2, -1, 2)
图 2. 设置示例程序。提供 OpenMP* 目标卸载支持的 oneMKL 头文件和模块以蓝色高亮显示。

现在我们可以开始计算了。让我们从一个基本实现开始,该实现使用两个 OpenMP 目标区域来分派 LU 分解和线性方程组的求解(**图 3**)。我们按如下方式编译和运行示例,以验证其结果是否正确。

$ ifx -i8 -DMKL_ILP64 -qopenmp -fopenmp-targets=spir64 -fsycl -free \
> lu_solve_omp_offload_ex1_small.F90 -o lu_solve_ex1_small \
> -L${MKLROOT}/lib/intel64 -lmkl_sycl -lmkl_intel_ilp64 -lmkl_intel_thread \
> -lmkl_core -liomp5 -lpthread -ldl
$ ./lu_solve_ex1_small

=================================
 Solutions to the linear systems
=================================
   -4.0000    2.0000   -3.0000
   -2.0000   -1.0000    2.0000
! Compute the LU factorization using OpenMP offload
! On entry, "a" contains the input matrix.
! On exit, it contains the LU factorization.
!$omp target data map(tofrom:a) map(from:ipiv) map(from:info)

    !$omp dispatch
    call dgetrf_batch_strided(n, n, a, lda, stride_a, &
        ipiv, stride_ipiv, batch_size, info)

!$omp end target data

if (any(info .ne. 0)) then
    print *, 'Error: getrf_batch_strided returned with errors'
else
    ! Solving the linear systems. On exit, the solutions are stored in b.
    !$omp target data map(to:a) map(to:ipiv) map(tofrom: b) map(from:info)

        !$omp dispatch

        call dgetrs_batch_strided('N', n, nrhs, a, lda, stride_a, &
            ipiv, stride_ipiv, &
            b, 1db, stride_b, batch_size, info)

    !$omp end target data

    if (any(info .ne. 0)) then
        print *, 'Error: getrs_batch_strided returned with errors'
    else
        print *, 'Computation executed successfully'
    endif
endif
图 3. 使用 oneMKL 和 OpenMP* 目标卸载基本求解批量线性方程组。OpenMP 指令以蓝色高亮显示。LAPACK 调用以绿色高亮显示。

target 结构将控制权转移到加速器设备。第一个区域将输入矩阵 [map(tofrom:a)] 传输到设备内存;将就地 LU 分解 (dgetrf_batch_strided) 分派到设备;并从设备内存中检索 LU 分解后的矩阵(现在存储在 a 中)、枢轴索引 [map(from:ipiv)] 和状态 [map(from:info)]。如果分解成功,程序将继续执行下一个 OpenMP 区域。LU 分解后的矩阵和枢轴索引被传输回设备内存 [map(to:a)map(to:ipiv)],并在设备上求解线性方程组 (dgetrs_batch_strided)。右侧项被覆盖为解向量,并从设备内存 [map(tofrom:b)] 中检索,同时检索计算的状态 [map(from:info)]。主机-设备数据传输在**图 4** 中以示意图的形式展示。

图 4. 图 3 中两个 OpenMP* 目标卸载区域的主机-设备数据传输。每个箭头表示主机和设备内存之间的数据移动。

然而,我们不必依赖对主机-设备数据传输的感性理解。我们可以通过请求 OpenMP 运行时库的调试信息来直接检查数据移动,如最小化数据传输和内存分配中所述。测试问题太小,不足以进行加速,因此我们将问题规模增加到更有趣的程度。让我们(以双精度)求解一批八个 8000 x 8000 的线性方程组,每个方程组有一个右侧项。

我们高亮显示了使用 OpenMP map 构造显式传输的数组,并添加了一个空格来分隔两个 OpenMP 目标区域。未高亮显示的数据移动是正在映射到目标设备之数组的数据描述符。我们可以忽略它们,因为它们通常很小。

在第一个目标区域中,数组 a (4,096,000,000 字节) 从主机传输到目标(hst→tgt)[map(tofrom:a)],然后再从目标传输回主机(tgt→hst)。在第二个目标区域中,它被传输回目标设备 [map(to:a)]。枢轴(ipiv,512,000 字节)在第一个目标区域在设备上计算,被检索 [map(from:ipiv), tgt→hst],然后在第二个目标区域传输回设备 [map(to:ipiv), hst→tgt] 以计算解。存储在数组 b (512,000 字节) 中的右侧项被传输到设备,被解向量覆盖,然后传输回主机 [map(tofrom:b)]。状态数组 info (64 字节) 在两个目标区域结束时从设备检索 [map(from:info), tgt→hst]。

在不连续的内存之间移动数据需要时间和精力,因此我们需要密切关注高亮显示的传输(总计 12,290,048,128 字节)。考虑到这一点,让我们看看如何改进我们的初始实现。

上一篇关于 LU 分解的文章讨论了在可以容纳一个目标区域的情况下使用两个 OpenMP 目标区域的缺点。首先,将控制流转移到目标设备会产生开销。在初始实现(**图 3**)中,这会发生两次。其次,需要冗余的主机-设备数据传输(**图 4**)。在求解线性方程组时,我们只需要将输入数组和右侧项复制到设备,并从设备检索解。我们还需要检索状态数组,但这些相对较小。枢轴仅由设备使用,因此我们将直接在设备内存中分配 ipiv 数组 [map(alloc:ipiv(1:stride_ipiv, 1:batch_size))]。将两个 OpenMP 目标区域合并后,代码会更清晰、更简洁(**图 5**),并且需要更少的数据移动(**图 6**)。

!$omp target data map(to:a) map(tofrom: Db)      &
    map(alloc:ipiv(1:stride_ipiv, l:batch_size)) &
    map(from:info_rf, info_rs)

    !$omp dispatch
    call dgetrf_batch_strided(n, n, a, lda, stride_a, &
        ipiv, stride_ipiv, batch_size, info_rf)

    !$omp dispatch
    call dgetrs_batch_strided('N', n, nrhs, a, lda, stride_a, &
        ipiv, stride_ipiv, &
        b, 1db, stride_b, batch_size, info_rs)
!$omp end target data

if (any(info_rf .ne. 0)) then
    print *, 'Error: getrf_batch_strided returned with errors.'
elseif (any(info_rs .ne. 0)) then
    print *, 'Error: getrs_batch_strided returned with errors.'
else
    print *, 'Computation executed successfully'
endif
图 5. 使用单个 OpenMP* 目标区域通过 oneMKL 求解批量线性方程组。OpenMP 指令以蓝色高亮显示。LAPACK 调用以绿色高亮显示。

图 6. 图 5 中 OpenMP* 目标卸载区域的主机-设备数据传输。每个箭头(设备内的箭头除外)表示主机和设备内存之间的数据移动。

调试信息证实了这一点。同样,我们高亮显示了使用 OpenMP map 构造显式传输的数组。

如果我们忽略数据描述符,**图 5** 中的单个 OpenMP 目标区域比初始实现中的两个目标区域(12,290,048,128 字节)需要显著更少的主机-设备数据传输(4,097,024,128 字节)(**图 3**)。

在异构并行计算中,最小化主机-设备数据传输是首要考虑的问题。第二个示例(**图 5**)中的代码性能明显优于原始代码(**图 3**),尤其是在线性方程组规模增大时(**表 1**)。

矩阵尺寸

CPU 时间

GPU 时间
(示例 1)

GPU 时间
(示例 2)

1,000 x 1,000

0.13

1.72

1.70

2,000 x 2,000

0.60

0.84

0.72

4,000 x 4,000

3.90

2.51

2.10

8,000 x 8,000

19.15

5.98

4.55

12,000 x 12,000

63.11

12.55

9.51

16,000 x 16,000

139.45

21.33

15.71

表 1. 在 Linux* (Ubuntu* 20.04 x64, 5.15.47 内核) 系统上,使用两个 2.0 GHz 第 4 代 Intel® Xeon® Platinum 8480+ 处理器 (CPU)、一个 Intel® Data Center GPU Max 1550 (GPU) 和 528 GB 内存,对八个不同尺寸的批量线性方程组求解的计时。所有时间单位为秒。GPU 测试仅使用一个 tile。每个线性方程组都有一个 RHS。每个实验运行五次。第一次运行被舍弃,因为它包含了 oneMKL 函数的即时编译开销。报告的时间是剩余四次运行的总和。

本文使用的示例程序可在以下存储库中找到。您可以在免费的Intel® Developer Cloud上体验 OpenMP 加速器卸载,该云平台拥有最新的 Intel 硬件和软件。

OpenMP target 构造有许多额外的选项,可让程序员精细控制加速器卸载。我们鼓励您查阅OpenMP 规范示例代码,并查看使用 OpenMP API 进行 GPU 编程入门最小化数据传输和内存分配教程。

© . All rights reserved.