英特尔oneAPI - 使用parallel_for完成高斯消去的并行化

效果

在完成并行程序设计课程作业时,老师向我们介绍了一个平台——英特尔OneAPI。因此简单学习了一下OneAPI中DPC++的使用,并且用它完成了高斯消去并行化的作业,得到了不错的加速比。
英特尔oneAPI - 使用parallel_for完成高斯消去的并行化_第1张图片

英特尔oneAPI - 使用parallel_for完成高斯消去的并行化

    • 效果
    • OneAPI介绍
    • 高斯消去算法
    • 高斯消去算法C++串行实现
    • 并行化分析
    • OneAPI中的并行化
    • GPU
    • 使用到的资源
    • 总结

OneAPI介绍

英特尔OneAPI平台是一个用于让程序员只需编写一份代码,就可以在诸如CPU、GPU、FPGA等多种设备、平台上运行的异构计算平台。
英特尔oneAPI - 使用parallel_for完成高斯消去的并行化_第2张图片

在这次高斯消去作业中,我们使用到的是英特尔OneAPI中的DPC++。

DPC++是英特尔对SYCL编程模型的一个实现,它允许我们编写跨平台的并行代码。

高斯消去算法

介绍下高斯消去算法,这个算法是将任意矩阵化为上三角矩阵的一种算法。

英特尔oneAPI - 使用parallel_for完成高斯消去的并行化_第3张图片

如图所示,高斯消去的典型串行算法具体步骤如下:

  1. 读入一个N*N的矩阵,设置变量i=0代表当前正在处理的行
  2. 通过行变换的方法,用第i行去消除i+1N行的第i个元素。使得矩阵的i列中,i行以下的元素全为0
  3. i自增, 若i>N,则消元结束。

高斯消去算法C++串行实现

跟据这些步骤,我们可以用C++进行串行实现:

int mat[n][n]; // 待处理矩阵
for (int i = 0; i < n; i++) // 当前消去行
{
        for (int j = i + 1; j < n; j++) // 被消去行
        {   
            float div = mat[j][i] / mat[i][i]; // 将k行消除为0需要的比值
            
            for (int k = i; k < n; k++) // 进行行变换
            {
                mat[j][k] -= mat[i][k] * div; 
            }
        }
}

并行化分析

可以看到,代码中有三重循环,其中

  • 第一重循环遍历矩阵所有行,当前选中行(第i行)便被用来消去其他行;
  • 第二重循环被用来遍历第i行“下面”的所有矩阵行(第j行);
  • 而第三重循环则被用来将第j行减去第i行,来实现将位于(j, i)位置的矩阵元素置零的作用。

很显然,这是一个复杂度为O(n^3)的算法。那么如何将这个算法并行化呢?

首先我们需要决定进行并行化的循环层数。

第一重循环肯定不太好做并行化,因为这一循环存在数据依赖——执行顺序在后的循环步需要执行顺序在前的循环步提供数据。

第三重循环很容易并行化,但是矩阵一行也就数千个元素,进行任务划分和数据发送的时间可能都比计算要花的时间长了。

因此,我们的并行化应该做在第二重循环上。

OneAPI中的并行化

在英特尔OneAPI DPC++中,将高斯消去的并行化的代码如下:

queue myQueue{ host_selector{} }; //创建队列
for (int i = 0; i < n; i++)
{
        myQueue.parallel_for(range{(unsigned long)(n - (i + 1))}, [=](id<1> idx)
        {
            int j = idx[0] + i + 1; // 等同于for(int j=i+1; j
            float div = new_mat[j][i] / new_mat[i][i];
            
            for (int k = i; k < n; k++)
            {
                new_mat[j][k] -= new_mat[i][k] * div; 
            }
        }).wait(); // wait() - 等待所有任务执行完成
}

虽然这还不是完整的代码,但是,是不是看上去很简单、优雅?甚至连代码都没多几行。

还有看不懂的写法?没事,我们一个一个来解释。


queue myQueue{ cpu_selector{} }; // 创建队列

首先是这一行,这行代码所做的事情便是创建一个队列(queue),在OneAPI中,队列是用来沟通主机(host)和运算设备(devices)的一种途径,我们可以通过向队列提交任务来让他们在运算设备(devices)上运行。

就比如说这一行中初始化队列使用到的cpu_selector{},便可以指定这个队列的运算设备为CPU,而我们接下来的算法也就自然会在CPU上运行。


myQueue.parallel_for(range{ n }, [=](id<1> idx){ do_something; }).wait();

parallel_for函数正如其名,是并行化的for语句。

parallel_for接受两个参数,第一个参数属于range类,比如这里的range{n}便表示循环范围是[0, n),后面的idx便是循环变量。而第二个参数则是一个lambda匿名函数,函数中就是给我们写for循环体的地方啦~

最后调用的wait()函数是为了让程序阻塞,等待当前所有任务运行完毕后再开始下一次循环,避免出错。


int j = idx[0] + i + 1;

这一行确实写得有点让人疑惑,不过之所以这么写,是因为parallel_for只能支持循环变量初值为0的情况的,所以需要我们手动加上一个偏移量。


GPU

queue myQueue{ gpu_selector{} }; // 创建队列

创建队列时,将cpu_selector{}改为gpu_selector{}可以通过gpu运算,不过我们的代码在gpu上运行的速度不怎样快。

使用到的资源

本次作业全部是在英特尔DevCloud平台上完成的,这个平台不需要手动配置OneAPI的开发环境。

DevCloud平台的注册和使用可以参考这篇文章

作业中使用的完整代码已经上传到GitCode

总结

通过这次对OneAPI的学习,我们深刻体会到了近年来并行程序设计工具的易用化。编写一次代码就能在不同的设备上做到并行运算,这是并行编程的一大进步。相信英特尔走出的这一步可以使得未来的高性能计算更能为人所用。

你可能感兴趣的:(oneapi,矩阵,算法)