英特尔oneAPI——通过DPC++实现异构计算

OneAPI与DPC++简介

OneAPI是英特尔提出的一种开放的跨架构的编程模型,使开发人员可以自由地跨多个架构使用单个代码库,从而帮助加速计算。

在如今的高性能计算领域,有多种硬件架构可用于运行工作负载——CPU,GPU,FPGA和专用加速器。没有一个架构最适合每一个工作负载。因此,使用混合架构可以在大多数情况下获得最佳性能。但是多种架构的使用也带来了一些编程挑战:

  • 开发人员需要使用以数据为中心的不同硬件架构。
  • 每个架构都需要各自的编程模型和工具链。
  • 软件开发的复杂性限制了架构选择的自由。

英特尔oneAPI——通过DPC++实现异构计算_第1张图片

而OneAPI提供了一个不同的编程模型,简化了不同架构的开发。通过oneAPI编程模型,开发人员可以用相同的语言和库瞄准不同的硬件平台,并可以使用同一套调试和性能分析工具在不同的平台上开发和优化代码。

在不同的平台和硬件架构上使用相同的语言,使得源代码更容易被重用;即使在代码被转移到不同的硬件架构上时,仍然需要进行平台特定的优化,也不再需要进行代码翻译。而且,使用共同的语言和工具集会使新的开发人员的培训更快,调试更快,生产率更高。

而DPC++是Intel为了将SYCL引入LLVM和oneAPI所开发的开源项目。SYCL是为了提高各种加速设备上的编程效率而开发的一种高级别的编程模型,简单来说它是一种跨平台的抽象层,用户不需要关心底层的加速器具体是什么,按照标准编写统一的代码就可以在各种平台上运行。可以说SYCL大大提高了编写异构计算代码的可移植性和编程效率,已经成为了异构计算的行业标准。值得一提的是SYCL并不是由多个单词的首字母的缩写。DPC++正是建立在SYCL和现代C++语言之上,具体来说是建立在C++17标准之上的。

实验cross entropy:使用DPC++实现异构异步计算程序设计

本节先简单介绍使用DPC++实现异构计算的相关语法,再通过编写CPU和GPU计算程序计算cross entropy的实验帮助大家入门DPC++异构计算程序的设计。

设备

设备,表示 OneAPI 系统中的各种硬件。设备类是预定义的设备选择和查询的方法,包含用于查询设备信息的成员函数,支持创建不同硬件, CPU/GPU/FPGA/…

device_selector 类支持运行时选择特定设备,如default_selector、cpu_selector、gpu_selector…

// Patric Zhao:  [email protected]

#include 
#include 
using namespace sycl;

int main() {
  queue my_gpu_queue( gpu_selector{} );

  std::cout << "Selected GPU device: " <<
    my_gpu_queue.get_device().get_info() << "\n";

  return 0;
}

可以在oneAPI官网注册账号,进入https://devcloud.intel.com/oneapi/get_started/ 可以获得英特尔提供的oneAPI和DPC++运行环境,在线编译运行源代码。

内存移动(与C++对比)

  • 显式内存申请

    //traditional C++
    void* malloc (size_t size);
    
    //DPC++
    void* malloc_host(size_t size, const sycl::queue& q);
    void* malloc_device(size_t size, const sycl::queue& q);
    void* malloc_shared(size_t size, const sycl::queue& q);
    
  • 显式内存拷贝

    //traditional C++
    void * memcpy ( void * destination, const void * source, size_t num );
    
    //DPC++
    void * queue.memcpy ( void * destination, const void * source, size_t num );
    
  • 内存释放

    //traditional C++
    void free (void* ptr);
    
    //DPC++
    void free(void* ptr, sycl::queue& q);
    

并行计算核心:parallel_for

•并行化 for-loop是并行计算的核心。在该循环中,每个迭代应该是完全独立的,并且不分顺序。

•并行内核使用 parallel_for 函数表示

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

//使用parallel_for
h.parallel_for(range<1>(1024), [=](id<1> i){
    A[i] =  B[i] + C[i];
});

cross entropy 实验

题面

英特尔oneAPI——通过DPC++实现异构计算_第2张图片

源码及实验结果

#include
#include
#include
#include

#define random_float()  (rand() / double(RAND_MAX));
//random_float() return a random float in [0,1]

using namespace std;
using namespace sycl;


// Input size
constexpr int K = 128;
constexpr int M = 32;
constexpr int N = 8192;

constexpr int iterations = 10;

// basic gpu speed
float gpu_kernel(float* X, int* mask, float* weight, float* loss, queue& q) {

   float duration = 0.0;
     //grid_rows is the minimum multiple of block_size  bigger than M
   int block_size=4;
   auto grid_rows = (K + block_size - 1) / block_size * block_size;
   auto grid_cols = (N + block_size - 1) / block_size * block_size;
   auto local_ndrange  = range<2>(block_size, block_size);
   auto global_ndrange = range<2>(grid_rows, grid_cols);

   auto e = q.submit([&](handler& h) {

       // (TODO) File range of computation 
       h.parallel_for(
         sycl::nd_range<2>(global_ndrange, local_ndrange), [=](sycl::nd_item<2> index)  {

           // (TODO): calculate index
           int row = index.get_global_id(0) ;
           int col = index.get_global_id(1) ;

           float exp_sum = 0.0;
           for (int i = 0; i < M; ++i) {
               exp_sum += exp(X[row * M * N + i * N + col]);
           }

           int mask_id = mask[row * N + col];
           loss[row * N + col] = weight[row * N + col] * log(exp(X[row * M * N + mask_id * N + col]) / exp_sum);

           });
       });

   e.wait();

   // (TODO) calculate computation time 
   duration +=(e.get_profiling_info() -
   e.get_profiling_info())  / 1000.0f / 1000.0f;

   return duration;
}


float cpu_kernel(float* X, int* mask, float* weight, float* loss) {
   double duration = 0.0;
   chrono::high_resolution_clock::time_point s, e;

   s = chrono::high_resolution_clock::now();
   for (int i = 0; i < K; ++i) {
       for (int j = 0; j < N; ++j) {
           float exp_sum = 0.0;

           for (int k = 0; k < M; ++k) {
               // (TODO): calculate sum of exp
               exp_sum  += exp(X[i * M * N + k * N + j]);
           }

           int mask_id = mask[i * N + j];

           loss[i * N + j] = weight[i * N + j] * (log(exp(X[i * M * N + mask_id * N + j]) / exp_sum));
       }
   }
   e = chrono::high_resolution_clock::now();
   duration = chrono::duration(e - s).count();

   return duration;
}

int verify(float* data_host, float* data_device) {
   int errCount = 0;
   for (int i = 0; i < K * N; ++i) {
       if (fabs(data_host[i] - data_device[i]) > 0.001) ++errCount;
   }
   return errCount;
}

void run(queue& q) {

   // (TODO): Allocate host memory for input
   float* X_cpu = malloc_host(K*M*N, q);
   int*   mask_cpu = malloc_host(K*N, q);
   float* weight_cpu = malloc_host(K*N,q);

   // (TODO): Allocate device memory for input
   float* X_gpu = malloc_device(K*M*N, q);
   int*   mask_gpu = malloc_device(K*N, q);
   float* weight_gpu = malloc_device(K*N, q);

   // (TOOD): Allocate shared memory for results
   float* loss_gpu = malloc_shared(K * N, q);

   // CPU results for reference
   float* loss_cpu = malloc_host(K * N, q);

   // Init random result for input
   for (int i = 0; i < K * M * N; ++i) {
       X_cpu[i] = random_float();
   }

   for (int i = 0; i < K * N; ++i) {
       loss_gpu[i] = 0.0;
       loss_cpu[i] = 0.0;
       mask_cpu[i] = i % M;
       weight_cpu[i] = random_float();
   }


   // (TODO): explicit memory copy from CPU to GPU for inputs
   q.memcpy(X_gpu , X_cpu,   sizeof(float) *K * M * N).wait();
   q.memcpy(mask_gpu,   mask_cpu,   sizeof(int)*K*N).wait();
   q.memcpy(weight_gpu,weight_cpu,sizeof(float)*K*N).wait();
   

   float duration_cpu = 0.0;
   float duration_gpu = 0.0;

   int warmup = 10;
   for (int i = 0; i < iterations / 2 + warmup / 2; ++i) {
       float duration = cpu_kernel(X_cpu, mask_cpu, weight_cpu, loss_cpu);
       if (i >= warmup / 2) duration_cpu += duration;
   }
   duration_cpu /= (iterations / 2);

   for (int i = 0; i < iterations + warmup; ++i) {
       // (TODO): pass params to function  
       float duration = gpu_kernel(X_gpu,mask_gpu,weight_gpu,loss_gpu ,   q);
       if (i >= warmup) duration_gpu += duration;
   }
   duration_gpu /= iterations;

   printf("Cross Entropy Input Size: K: %d, M: %d, N: %d, Total : %d\n"
       "GPU time: %lf (ms)\n"
       "CPU time: %lf (ms)\n",
       K, M, N, K * M * N, duration_gpu, duration_cpu);

   int errCount = 0;
   errCount = verify(loss_cpu, loss_gpu);
   printf("%d errors in loss_gpu\n", errCount);


   // free all memory in host 
   free(loss_cpu, q);
   free(X_cpu, q);
   free(mask_cpu, q);
   free(weight_cpu, q);

   // (TODO) : free all memory in device 
   free(loss_gpu,q);
   free(X_gpu,q);
   free(mask_gpu,q);
   free(weight_gpu,q);
}

int main() {
   
   auto propList = property_list{ property::queue::enable_profiling() };

   // (TODO) : Select your device, GPU
   queue my_gpu_queue(cl::sycl::gpu_selector{}, propList);

   run(my_gpu_queue);

   return 0;
}

实验结果:

英特尔oneAPI——通过DPC++实现异构计算_第3张图片

参考资料

OneAPI官网

知乎回答:如何看待英特尔 oneAPI 编程语言 DPC++ 功能?

OneAPI快速入门指南

Data Parallel C++程序设计

你可能感兴趣的:(oneapi,c++)