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

如何从 CUDA 数学库调用迁移到 oneMKL

2023 年 6 月 27 日

CPOL

10分钟阅读

viewsIcon

4083

使用 Intel® oneAPI 数学核心库 SYCL API

科学、金融、企业和通信领域的许多计算工作负载都依赖于高级数学库来高效处理线性代数(BLAS、LAPACK、SPARSE)、向量数学、傅里叶变换、随机数生成,甚至线性方程或分析的求解器。

本文重点介绍如何修改 GPU 目标源代码,使其使用 oneMKL 的 SYCL API 而不是 CUDA*。

在我们深入研究 SYCL 和 CUDA 语法以及 oneMKL 和 cuBLAS 等函数 API 的映射之前,让我们先简单了解一下 oneMKL 软件架构与其在 GPU 上的执行相关的内容。

oneMKL 软件架构

oneMKL 是 Intel® oneAPI Math Kernel Library 的简称,它是一套完整而全面的数学函数和求解器包。(图 1

它不仅针对 Intel® CPU 进行了优化,还支持通过 OpenMP*SYCL 进行计算卸载到 GPU。这使得 oneMKL 能够利用 GPU 架构,该架构有利于小型、高度并行的执行内核。oneMKL 的关键功能已直接为 Intel® GPU 卸载启用。

图 1. Intel® oneAPI Math Kernel Library 函数域

oneMKL 以免费的仅二进制分发形式提供。然而,oneMKL 的一个关键部分不仅是免费的,而且还提供源代码。

oneMKL 的 SYCL API 是开源的,并且是 oneAPI 规范的一部分。这使得它可以在各种计算设备上使用:CPU、GPU、FPGA 和其他加速器。

oneAPI 规范中包含的功能细分为以下域

  • 稠密线性代数
  • 稀疏线性代数
  • 离散傅里叶变换
  • 随机数生成器
  • 向量数学

oneMKL GPU 卸载模型

通用 GPU (GPGPU) 计算模型由连接到一个或多个计算设备的宿主组成。每个计算设备包含许多 GPU 计算引擎 (CE),也称为执行单元 (EU) 或 Xe 向量引擎 (XVE)。宿主程序和一组内核在由宿主设置的上下文中执行。宿主通过命令队列与这些内核进行交互。

图 2. GPU 执行模型概述

当内核入队命令提交一个内核以供执行时,该命令定义了一个 N 维索引空间。内核实例由内核、与内核关联的参数值以及定义索引空间的参数组成。当计算设备执行一个内核实例时,内核函数会针对定义索引空间或 N 维范围中的每个点执行。

同步也可以在命令级别发生,同步可以在宿主命令队列中的命令之间发生。在此模式下,一个命令可以依赖于另一个命令或多个命令中的执行点。

基于内存顺序约束的其他类型的同步包括原子操作和栅栏。这些同步类型控制着任何特定工作项的内存操作如何对另一个工作项可见,这为数据并行计算模型提供了微观级别的同步点。

oneMKL 直接利用了这个基本的执行模型,该模型作为 Intel® Graphics Compute Runtime for oneAPI Level Zero and OpenCL™ Driver 的一部分实现。

oneMKL SYCL API 基础

虽然 oneMKL 支持使用 OpenMP pragma 进行自动 GPU 卸载调度,但**我们将重点关注其对 SYCL 队列的支持**。

SYCL 是一个免版税的跨平台抽象层,它允许使用 ISO C++ 或更高版本编写用于异构和卸载处理器的代码。它提供了 API 和抽象来查找可以在其上执行代码的设备(例如 CPU、GPU、FPGA),并管理这些设备上的数据资源和代码执行。

oneMKL SYCL API 是开源的,并且是 oneAPI 规范的一部分,**它提供了一个完美的载体**,**用于将 CUDA** 专有库函数 API 迁移到开放标准。

oneMKL 使用 C++ 命名空间按数学域组织例程。所有 oneMKL 对象和例程都包含在 `oneapi::mkl` 基命名空间中。各个 oneMKL 域使用如下的二级命名空间层

命名空间 oneMKL 域或内容
oneapi::mkl oneMKL 基命名空间,包含通用的 oneMKL 数据类型、对象、异常和例程。
oneapi::mkl::blas 来自 BLAS 和 BLAS 类扩展的稠密线性代数例程。`oneapi::mkl::blas` 命名空间应包含两个命名空间,column_major 和 row_major,以支持两种矩阵布局。
oneapi::mkl::lapack 来自 LAPACK 和 LAPACK 类扩展的稠密线性代数例程。
oneapi::mkl::sparse 来自 Sparse BLAS 和 Sparse Solvers 的稀疏线性代数例程。
oneapi::mkl::dft 离散和快速傅里叶变换。
oneapi::mkl::rng 随机数生成器例程。
oneapi::mkl::vm 向量数学例程,例如作用于向量元素的三角函数、指数函数。

基于类的 oneMKL API,例如 RNG 和 DFT 域中的 API,需要一个 `sycl::queue` 作为构造函数或其他设置例程的参数。前面章节中计算例程的执行要求也适用于计算类方法。

要分配目标 GPU 设备并控制设备使用,可以分配一个 `sycl::device` 实例。如果底层设备架构支持,则可以进一步将此类设备实例分区为子设备。

oneMKL SYCL API 的设计允许计算例程的异步执行;这有利于系统中多个设备的并发使用。每个计算例程都会将工作入队到所选设备上执行,并且可能(但不要求)在执行完成之前返回。

`sycl::buffer` 对象会自动管理由数据依赖(读后写、写后写或写后读)链接的内核启动之间的同步。oneMKL 例程不需要对 `sycl::buffer` 参数执行任何额外的同步。

当统一共享内存 (USM) 指针用作 oneMKL 例程的输入或输出时,调用应用程序有责任管理可能的异步。为了帮助调用应用程序,所有具有至少一个 USM 指针参数的 oneMKL 例程还会接受一个输入事件列表的可选引用,类型为 `std::vector`,并且返回值为 `sycl::event` 类型,表示计算完成。

sycl::event mkl::domain::routine(..., std::vector<sycl::event> &in_events = {});

所有 oneMKL 函数都是**主机线程安全的**。

oneMKL SYCL API 源代码

oneMKL SYCL API 源代码可在 oneAPI GitHub 存储库中找到。它正在积极开发中。还提供了 BLAS、DFT、LAPACK 和 RND 域的后端映射和等效 CUDA、AMD ROCm* 和 SYCL 实现的包装器。

图 3. 可用的 oneMKL BLAS 兼容性后端

这种开放的后端架构使得 oneMKL SYCL API 能够应用于广泛的卸载设备,包括(但不限于)Intel CPU、GPU 和加速器。

要获取每个域中完整的函数表,您可以参考相应的 function_table.hpp 头文件。

#include <complex>
#include <cstdint>
#if __has_include(<sycl/sycl.hpp>)
#include <sycl/sycl.hpp>
#else
#include <CL/sycl.hpp>
#endif
#include "oneapi/mkl/types.hpp"

typedef struct {
    int version;

    // Buffer APIs

    void (*column_major_scasum_sycl)(sycl::queue &queue, std::int64_t n,
                                     sycl::buffer<std::complex<float>, 1> &x, std::int64_t incx,
                                     sycl::buffer<float, 1> &result);
    void (*column_major_dzasum_sycl)(sycl::queue &queue, std::int64_t n,
                                     sycl::buffer<std::complex<double>, 1> &x, std::int64_t incx,
                                     sycl::buffer<double, 1> &result);
    void (*column_major_sasum_sycl)(sycl::queue &queue, std::int64_t n, sycl::buffer<float, 1> &x,
                                    std::int64_t incx, sycl::buffer<float, 1> &result);
    void (*column_major_dasum_sycl)(sycl::queue &queue, std::int64_t n, sycl::buffer<double, 1> &x,
                                    std::int64_t incx, sycl::buffer<double, 1> &result);
    void (*column_major_saxpy_sycl)(sycl::queue &queue, std::int64_t n, float alpha,
                                    sycl::buffer<float, 1> &x, std::int64_t incx,
                                    sycl::buffer<float, 1> &y, std::int64_t incy);
    void (*column_major_daxpy_sycl)(sycl::queue &queue, std::int64_t n, double alpha,
                                    sycl::buffer<double, 1> &x, std::int64_t incx,
                                    sycl::buffer<double, 1> &y, std::int64_t incy);
    void (*column_major_caxpy_sycl)(sycl::queue &queue, std::int64_t n, std::complex<float> alpha,
                                    sycl::buffer<std::complex<float>, 1> &x, std::int64_t incx,
                                    sycl::buffer<std::complex<float>, 1> &y, std::int64_t incy);
图 4. oneMKL BLAS 函数表头文件
#ifndef _DFT_FUNCTION_TABLE_HPP_
#define _DFT_FUNCTION_TABLE_HPP_

#include <complex>
#include <cstdint>

#if __has_include(<sycl/sycl.hpp>)
#include <sycl/sycl.hpp>
#else
#include <CL/sycl.hpp>
#endif

#include "oneapi/mkl/types.hpp"
#include "oneapi/mkl/dft/types.hpp"
#include "oneapi/mkl/dft/descriptor.hpp"

typedef struct {
    int version;
    oneapi::mkl::dft::detail::commit_impl<oneapi::mkl::dft::precision::SINGLE,
                                          oneapi::mkl::dft::domain::COMPLEX>* (
        *create_commit_sycl_fz)(
        const oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::SINGLE,
                                           oneapi::mkl::dft::domain::COMPLEX>& desc,
        sycl::queue& sycl_queue);
    oneapi::mkl::dft::detail::commit_impl<oneapi::mkl::dft::precision::DOUBLE,
                                          oneapi::mkl::dft::domain::COMPLEX>* (
        *create_commit_sycl_dz)(
        const oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::DOUBLE,
                                           oneapi::mkl::dft::domain::COMPLEX>& desc,
        sycl::queue& sycl_queue);
    oneapi::mkl::dft::detail::commit_impl<oneapi::mkl::dft::precision::SINGLE,
                                          oneapi::mkl::dft::domain::REAL>* (*create_commit_sycl_fr)(
        const oneapi::mkl::dft::descriptor<oneapi::mkl::dft::precision::SINGLE,
                                           oneapi::mkl::dft::domain::REAL>& desc,
图 5. oneMKL FFT 函数表头文件

CUDA 兼容性和库包装器

要了解 oneMKL 功能如何映射到特定后端的详细视角,您只需转到感兴趣的特定后端目录并检查相应的 *{backend}_wrappers.cpp* 文件。

图 6. oneMKL - cuBLAS 兼容性后端文件列表

在**图 6** 的 cuBLAS* 示例中,相关文件是 *cublas_wrappers.cpp*。

您将在相应的头文件中找到的实际映射实现包括名为 *onemkl_{domain}_cu{domain}.hpp* 和 *onemkl_{domain}_cu{domain}.hxx* 的文件。

与之前一样,我们使用 cuBLAS 作为我们存储库源代码屏幕截图的参考,如**图 7** 所示。同样,对于 cuFFT*、cuSolver* / LAPACK 和 cuRAND* 也适用。

// Buffer APIs

void asum(sycl::queue &queue, std::int64_t n, sycl::buffer<std::complex<float>, 1> &x,
          std::int64_t incx, sycl::buffer<float, 1> &result);

void asum(sycl::queue &queue, std::int64_t n, sycl::buffer<std::complex<double>, 1> &x,
          std::int64_t incx, sycl::buffer<double, 1> &result);

void asum(sycl::queue &queue, std::int64_t n, sycl::buffer<float, 1> &x, std::int64_t incx,
          sycl::buffer<float, 1> &result);

void asum(sycl::queue &queue, std::int64_t n, sycl::buffer<double, 1> &x, std::int64_t incx,
          sycl::buffer<double, 1> &result);

void axpy(sycl::queue &queue, std::int64_t n, float alpha, sycl::buffer<float, 1> &x,
          std::int64_t incx, sycl::buffer<float, 1> &y, std::int64_t incy);

void axpy(sycl::queue &queue, std::int64_t n, double alpha, sycl::buffer<double, 1> &x,
          std::int64_t incx, sycl::buffer<double, 1> &y, std::int64_t incy);

void axpy(sycl::queue &queue, std::int64_t n, std::complex<float> alpha,
          sycl::buffer<std::complex<float>, 1> &x, std::int64_t incx,
          sycl::buffer<std::complex<float>, 1> &y, std::int64_t incy);

void axpy(sycl::queue &queue, std::int64_t n, std::complex<double> alpha,
          sycl::buffer<std::complex<double>, 1> &x, std::int64_t incx,
          sycl::buffer<std::complex<double>, 1> &y, std::int64_t incy);

void axpy_batch(sycl::queue &queue, std::int64_t n, float alpha, sycl::buffer<float, 1> &x,
                std::int64_t incx, std::int64_t stridex, sycl::buffer<float, 1> &y,
                std::int64_t incy, std::int64_t stridey, std::int64_t batch_size);

void axpy_batch(sycl::queue &queue, std::int64_t n, double alpha,
                sycl::buffer<double, 1> &x, std::int64_t incx, std::int64_t stridex,
                sycl::buffer<double, 1> &y, std::int64_t incy, std::int64_t stridey,
                std::int64_t batch_size);

void axpy_batch(sycl::queue &queue, std::int64_t n, std::complex<float> alpha,
                sycl::buffer<std::complex<float>, 1> &x, std::int64_t incx,
                std::int64_t stridex, sycl::buffer<std::complex<float>, 1> &y,
                std::int64_t incy, std::int64_t stridey, std::int64_t batch_size);
图 7. oneMKLBLAS - cuBLAS 兼容性函数头文件

在专门的 使用 SYCL* 从 CUDA* 迁移到 C++ 网页以及 oneAPI GitHub 代码示例存储库中,您可以找到一系列源代码示例,这些示例专门介绍了如何将不同类型的应用程序从 CUDA 迁移到 SYCL。

在下一段中,我们将仔细研究其中一个示例。

从 CUDA 迁移 BLAS 例程到基于 Nvidia 自己 cuBLAS 库 - API 示例的 SYCL,是一个很好的起点。

CUDA 到 SYCL 迁移

Intel 和 oneAPI 提供了简化的 CUDA 到 SYCL 迁移工具,从而简化了仍然支持 CUDA 兼容后端和专有 NVIDIA 硬件的数学函数的异构计算,同时使您的代码能够在多供应商硬件上运行。

使用 SYCL* 从 CUDA* 迁移到 C++”页面提供了有关可用自动化迁移工具技术信息的所有通用入口点。

该页面还包含许多指导性的 CUDA 到 SYCL 代码示例

对于深入了解将 CUDA 库代码迁移到 oneMKL,以下内容最有趣:

cuBLAS 迁移代码示例

让我们仔细看看 NVIDIA/CUDA 库 GitHub 存储库中 cuBLAS 库 - API 示例的迁移。

示例源代码 (SYCL) 是从 CUDA 迁移的,用于将计算卸载到 GPU/CPU。该示例演示了如何

  1. 将代码迁移到 SYCL
  2. 优化迁移步骤
  3. 提高处理时间

每个 cuBLAS 示例的源文件都显示了 oneMKL cuBLAS 例程的用法。所有这些都是包含单个函数用法的基本程序。

此示例包含以下文件夹中的两组源代码

文件夹名称 描述
01_sycl_dpct_output 包含 Intel DPC++ 兼容工具生成的输出,用于将 SYCL 兼容代码从 CUDA 代码迁移。
02_sycl_dpct_migrated 包含使用 Intel DPC++ 兼容工具生成的 SYCL 到 CUDA 迁移代码,并进行了手动更改以使代码完全正常运行。

函数分为三个难度级别。共有 **52** 个示例

  • 级别 1 示例 (14 个示例)
  • 级别 2 示例 **(23 个)**
  • 级别 3 示例 (15 个)

构建 cuBLAS 迁移示例

**注意**:如果尚未完成,请通过在 oneAPI 安装根目录中 sourcing `setvars` 脚本来设置您的 CLI 环境。

Linux*

  • 对于系统范围的安装:* . /opt/intel/oneapi/setvars.sh*
  • 对于私有安装:* . ~/intel/oneapi/setvars.sh*
  • 对于非 POSIX shell,如 csh,请使用以下命令:
          bash -c 'source <install-dir>/setvars.sh ; exec csh'

有关配置环境变量的更多信息,请参阅在 Linux* 或 macOS* 上使用 setvars 脚本

  1. 在您的 Linux* shell 中,切换到示例目录。
  2. 构建示例。
    $ mkdir build
    $ cd build
    $ cmake ..
    $ make

    默认情况下,此命令序列将构建 *02_sycl_dpct_migrated* 文件夹中的源代码版本。

  3. 运行 `cuBLAS Migration` 示例。
    在 CPU 或 GPU 上运行程序。每个示例都使用默认设备,在大多数情况下是 GPU。
      •  运行 *02_sycl_dpct_migrated* 文件夹中的示例。
      •  $ make run_amax
  4. 示例输出
    [  0%] Building CXX object 02_sycl_dpct_migrated/Level-1/CMakeFiles/amax.dir/amax.cpp.o
    [100%] Linking CXX executable amax
    [100%] Built target amax
    A
    1.00 2.00 3.00 4.00 
    =====
    result
    4
    =====

代码检查

完成后,此示例将让您对 cuBLAS 函数调用迁移到 oneMKL 的代码流有一个很好的认识。

您可以在此源代码位置(02_sycl_dpct_migrated)仔细查看包含的所有示例的最终迁移代码。

典型的转换将类似于**图 8** 中 SGEMM 的示例。

图 8. cuBLAS 到 oneMKL 调用转换 GEMM

脚本化的 CUDA 到 oneMKL 迁移

我们介绍了 oneMKL API SYCL 架构,包括如何使用相关的 oneMKL 后端头文件来精确识别哪些 cuBLAS、cuFFT、cuRAND、cuSOLVER、cuSparse 或其他 CUDA 数学库函数映射到 oneMKL。

关于功能映射以及 CUDA 库和 oneMKL 之间的语法差异,存在一个清晰可辨的模式。一旦您熟悉了它,即使是专门工作负载的手动代码迁移和迁移代码的调优也将变得常规且可脚本化。

兼容性映射正在快速发展,因此值得频繁查看 GitHub 存储库

SYCLomaticIntel DPC++ Compatibility Tool 是您快速迁移 CUDA 代码到 SYCL 的好帮手。

使用 SYCL* 从 CUDA* 迁移到 C++”页面可让您轻松找到 oneMKL 和性能库之外的所有迁移资源。

额外资源

获取软件

oneMKL 可独立安装,也可作为Intel® oneAPI Base Toolkit 的一部分使用。除了这些下载位置外,它们还可以通过合作伙伴存储库获得。还可以在线获得详细文档

© . All rights reserved.