英特尔oneAPI——异构计算学习总结

oneAPI编程模型

oneAPI编程模型提供了一个全面、统一的开发人员工具组合,可用于各种硬件设备,其中包括跨多个工作负载领域的一系列性能库。这些库包括面向各目标架构而定制化代码的函数,因此相同的函数调用可为各种支持的架构提供优化的性能。**DPC++**基于行业标准和开放规范,旨在鼓励生态系统的协作和创新。

多架构编程面临的挑战

在以数据为中心的环境中,专用工作负载的数量不断增长。专用负载通常因为没有通用的编程语言或API而需要使用不同的语言和库进行编程,这就需要维护各自独立的代码库。

由于跨平台的工具支持不一致,因此开发人员必须学习和使用一整套不同的工具。单独投入精力给每种硬件平台开发软件。

oneAPI则可以利用一种统一的编程模型以及支持并行性的库,支持包括CPU、GPU、FPGA等硬件等同于原生高级语言的开发性能,并且可以与现有的HPC编程模型交互。
无oneAPI的程序
有oneAPI的程序架构

SYCL

SYCL支持C++数据并行编程,SYCL和OpenCL一样都是由Khronos Group管理的,SYCL是建立在OpenCL之上的跨平台抽象层,支持用C++用单源语言方式编写用于异构处理器的与设备无关的代码。

DPC++

DPC++(Data Parallel C++)是一种单源语言,可以将主机代码和异构加速器内核写在同一个文件当中,在主机中调用DPC++程序,计算由加速器执行。DPC++代码简洁且效率高,并且是开源的。现有的CUDA应用、Fortran应用、OpenCL应用都可以用不同方式很方便地迁移到DPC++当中。
下图显示了原来使用不同架构的HPC开发人员的一些推荐的转换方法。
工作流

编译和运行DPC++程序

编译和运行DPC++程序主要包括三步:

  1. 初始化环境变量
  2. 编译DPC++源代码
  3. 运行程序
    例如本地运行,在本地系统上安装英特尔基础工具套件,使用以下命令编译和运行DPC++程序。
source /opt/intel/inteloneapi/setvars.sh
dpcpp simple.cpp -o simple
./simple

编程实例

实现矢量加法

以下实例描述了使用DPC++实现矢量加法的过程和源代码。

queue类

queue类用来提交给SYCL执行的命令组,是将作业提交到运算设备的一种机制,多个queue可以映射到同一个设备。

Parallel kernel

Parallel kernel允许代码并行执行,对于一个不具有相关性的循环数据操作,可以用Parallel kernel并行实现
在C++代码中的循环实现

for(int i=0; i < 1024; i++){
    a[i] = b[i] + c[i];
});

在Parallel kernel中的并行实现

h.parallel_for(range<1>(1024), [=](id<1> i){
    A[i] =  B[i] + C[i];
});
通用的并行编程模板
h.parallel_for(range<1>(1024), [=](id<1> i){
// CODE THAT RUNS ON DEVICE 
});

range用来生成一个迭代序列,1为步长,在循环体中,i表示索引。

Host Accessor

Host Accessor是使用主机缓冲区访问目标的访问器,它使访问的数据可以在主机上使用。通过构建Host Accessor可以将数据同步回主机,除此之外还可以通过销毁缓冲区将数据同步回主机。
buf是存储数据的缓冲区。

host_accessor b(buf,read_only);

除此之外还可以将buf设置为局部变量,当系统超出buf生存期,buf被销毁,数据也将转移到主机中。

矢量相加源代码

根据上面的知识,这里展示了利用DPC++实现矢量相加的代码。

//第一行在jupyter中指明了该cpp文件的保存位置
%%writefile lab/vector_add.cpp
#include <CL/sycl.hpp>
using namespace sycl;

int main() {
    const int N = 256;
    //# 初始化两个队列并打印
    std::vector<int> vector1(N, 10);
    std::cout<<"\nInput Vector1: ";    
    for (int i = 0; i < N; i++) std::cout << vector1[i] << " ";
    
    std::vector<int> vector2(N, 20);
    std::cout<<"\nInput Vector2: ";    
    for (int i = 0; i < N; i++) std::cout << vector2[i] << " ";
    
    //# 创建缓存区
    buffer vector1_buffer(vector1);
    buffer vector2_buffer(vector2);
    
    //# 提交矢量相加任务
    queue q;
    q.submit([&](handler &h) {
      //# 为缓存区创建访问器
      accessor vector1_accessor (vector1_buffer,h);
      accessor vector2_accessor (vector2_buffer,h);

      h.parallel_for(range<1>(N), [=](id<1> index) {
        vector1_accessor[index] += vector2_accessor[index];
      });
   });

  //# 创建主机访问器将设备中数据拷贝到主机当中
  host_accessor h_a(vector1_buffer,read_only);

  std::cout<<"\nOutput Values: ";
  for (int i = 0; i < N; i++) std::cout<< vector1[i] << " ";
  std::cout<<"\n";

  return 0;
}

运行结果
在这里插入图片描述

统一共享内存 (Unified Shared Memory USM)

统一共享内存是一种基于指针的方法,是将CPU内存和GPU内存进行统一的虚拟化方法,对于C++来说,指针操作内存是很常规的方式,USM也可以最大限度的减少C++移植到DPC++的代价。
下图显示了非USM(左)和USM(右)的程序员开发视角。
在这里插入图片描述

类型函数调用说明在主机上可访问在设备上可访问
设备malloc_device在设备上分配(显式)
主机malloc_host在主机上分配(隐式)
共享malloc_shared分配可以在主机和设备之间迁移(隐式)

USM语法

  • 初始化:
    int *data = malloc_shared<int>(N, q);
    int *data = static_cast<int *>(malloc_shared(N * sizeof(int), q));
  • 释放
    free(data,q);

使用共享内存之后,程序将自动在主机和运算设备之间隐式移动数据。

数据依赖

使用USM时,要注意数据之间的依赖关系以及事件之间的依赖关系,如果两个线程同时修改同一个内存区,将产生不可预测的结果。

我们可以使用不同的选项管理数据依赖关系:

  • 内核任务中的 wait()
  • 使用 depends_on 方法
  • 使用 in_queue 队列属性
wait()
    q.submit([&](handler &h) {
      h.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 2; });
    }).wait();  // <--- wait() will make sure that task is complete before continuing

    q.submit([&](handler &h) {
      h.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 3; });
    });
depends_on
    auto e = q.submit([&](handler &h) {  // <--- e is event for kernel task
      h.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 2; });
    });

    q.submit([&](handler &h) {
      h.depends_on(e);  // <--- waits until event e is complete
      h.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 3; });
    });
in_order queue property
    queue q(property_list{property::queue::in_order()}); // <--- this will make sure all the task with q are executed sequentially
练习1:事件依赖

以下代码使用 USM,并有三个提交到设备的内核。每个内核修改相同的数据阵列。三个队列之间没有数据依赖关系

  • 为每个队列提交添加 wait()
  • 在第二个和第三个内核任务中实施 depends_on() 方法
  • 使用 in_order 队列属性,而非常规队列: queue q{property::queue::in_order()};
%%writefile lab/usm_data.cpp
#include <CL/sycl.hpp>
using namespace sycl;

static const int N = 256;

int main() {
  queue q{property::queue::in_order()};//用队列限制执行顺序
  std::cout << "Device : " << q.get_device().get_info<info::device::name>() << "\n";

  int *data = static_cast<int *>(malloc_shared(N * sizeof(int), q));
  for (int i = 0; i < N; i++) data[i] = 10;

  q.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 2; });

  q.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 3; });

  q.parallel_for(range<1>(N), [=](id<1> i) { data[i] += 5; });
  q.wait();//wait阻塞进程

  for (int i = 0; i < N; i++) std::cout << data[i] << " ";
  std::cout << "\n";
  free(data, q);
  return 0;
}

执行结果
结果

练习2:事件依赖

以下代码使用 USM,并有三个提交到设备的内核。前两个内核修改了两个不同的内存对象,第三个内核对前两个内核具有依赖性。三个队列之间没有数据依赖关系

%%writefile lab/usm_data2.cpp
#include <CL/sycl.hpp>
using namespace sycl;

static const int N = 1024;

int main() {
  queue q;
  std::cout << "Device : " << q.get_device().get_info<info::device::name>() << "\n";//设备选择

  int *data1 = malloc_shared<int>(N, q);
  int *data2 = malloc_shared<int>(N, q);
  for (int i = 0; i < N; i++) {
    data1[i] = 10;
    data2[i] = 10;
  }

  auto e1 = q.parallel_for(range<1>(N), [=](id<1> i) { data1[i] += 2; });

  auto e2 = q.parallel_for(range<1>(N), [=](id<1> i) { data2[i] += 3; });//e1,e2指向两个事件内核

  q.parallel_for(range<1>(N),{e1,e2}, [=](id<1> i) { data1[i] += data2[i]; }).wait();//depend on e1,e2

  for (int i = 0; i < N; i++) std::cout << data1[i] << " ";
  std::cout << "\n";
  free(data1, q);
  free(data2, q);
  return 0;
}

运行结果
在这里插入图片描述

UMS实验

在主机中初始化两个vector,初始数据为25和49,在设备中初始化两个vector,将主机中的数据拷贝到设备当中,在设备当中并行计算原始数据的根号值,然后将data1_device和data2_device的数值相加,最后将数据拷贝回主机当中,检验最后相加的和是否是12,程序结束前将内存释放。

%%writefile lab/usm_lab.cpp
#include <CL/sycl.hpp>
#include <cmath>
using namespace sycl;

static const int N = 1024;

int main() {
  queue q;
  std::cout << "Device : " << q.get_device().get_info<info::device::name>() << "\n";

  //intialize 2 arrays on host
  int *data1 = static_cast<int *>(malloc(N * sizeof(int)));
  int *data2 = static_cast<int *>(malloc(N * sizeof(int)));
  for (int i = 0; i < N; i++) {
    data1[i] = 25;
    data2[i] = 49;
  }
    
  //# STEP 1 : Create USM device allocation for data1 and data2
  int *data1_device = static_cast<int *>(malloc_device(N * sizeof(int),q));
  int *data2_device = static_cast<int *>(malloc_device(N * sizeof(int),q));

  //# STEP 2 : Copy data1 and data2 to USM device allocation
  q.memcpy(data1_device, data1, sizeof(int) * N).wait();
  q.memcpy(data2_device, data2, sizeof(int) * N).wait();

  //# STEP 3 : Write kernel code to update data1 on device with sqrt of value

  auto e1 = q.parallel_for(range<1>(N), [=](id<1> i) { data1_device[i] = std::sqrt(25); });
  auto e2 = q.parallel_for(range<1>(N), [=](id<1> i) { data2_device[i] = std::sqrt(49); });

  //# STEP 5 : Write kernel code to add data2 on device to data1
  q.parallel_for(range<1>(N),{e1,e2}, [=](id<1> i) { data1_device[i] += data2_device[i]; }).wait();

  //# STEP 6 : Copy data1 on device to host
  q.memcpy(data1, data1_device, sizeof(int) * N).wait();
  q.memcpy(data2, data2_device, sizeof(int) * N).wait();

  //# verify results
  int fail = 0;
  for (int i = 0; i < N; i++) if(data1[i] != 12) {fail = 1; break;}
  if(fail == 1) std::cout << " FAIL"; else std::cout << " PASS";
  std::cout << "\n";

  //# STEP 7 : Free USM device allocations
  free(data1_device, q);
  free(data1);
  free(data2_device, q);
  free(data2);

  //# STEP 8 : Add event based kernel dependency for the Steps 2 - 6
  return 0;
}

运行结果
在这里插入图片描述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值