并行矩阵乘法
1 问题陈述
矩阵乘法是一种将两个矩阵相乘得到第三个矩阵的运算。在计算机科学中,矩阵乘法是一项基本而重要的操作,被广泛应用于图形处理、神经网络训练等领域。了解矩阵乘法的思路对于理解这些应用的实现过程至关重要。
2 项目简介
本项目主要利用oneAPI对矩阵乘法进行优化
矩阵乘法的定义涉及到矩阵中元素的相乘与相加。对于两个矩阵A和B,它们的乘积C的第i行第j列的元素由以下公式给出:
Cij =k=1nAik⋅ Bkj
其中,n为矩阵的维度。
该式的时间复杂度为O(n3)
,利用并行的思路,可以将其的时间复杂度减小。
3 基本思路
3.1 矩阵乘法思路
1. 遍历元素相乘:矩阵乘法的基本思路是遍历矩阵A的每个元素,与矩阵B的对应元素相乘,然后将乘积累加得到矩阵C的相应元素。
2. 行列匹配:在遍历的过程中,需要确保矩阵A的行与矩阵B的列匹配,即矩阵A的列数等于矩阵B的行数。
3. 累加求和:利用累加求和的方式计算乘积的总和,得到矩阵C的元素。
3.2 优化方法
利用基于SYCL的编程模型在GPU上实现矩阵乘法的计算,步骤如下:
1. 分配内存:在主机端分配内存空间用于存储输⼊矩阵和输出矩阵,同时在GPU端分配内存空间用于存储相应的输入和输出数据。
2. 数据传输:将输入矩阵数据从主机端内存传输到GPU端内存中。
3. 核函数调用:在SYCL中,矩阵乘法的计算通常会在GPU上使用核函数来实现并行计算。核函数会分配线程块和线程来处理不同的数据块。
4. 并行计算:在核函数中,每个线程负责计算输出矩阵的⼀个单独的元素。为了最大限度地利用GPU的并行计算能力,通常会使用⼆维线程块和线程网格的方式来处理矩阵的乘法计算。
5. 数据传输:计算完成后,将输出矩阵数据从GPU端内存传输回主机端内存中,以便进⼀步处理或分析。
在并行计算矩阵乘法时,可以利用线程块和线程的层次结构来优化计算。通过合理划分矩阵数据并利用共享内存来减少全局内存访问的次数,可以⼤幅提高计算效率。此外,还可以利用GPU上的多个计算单元并执行行矩阵乘法,进⼀步提高计算速度。
为了将内核执行分组到工作组中。本项目具体使用了nd_range内核的功能,通过nd_range和nd_item类。Nd_range类表示使用全局执行范围和每个工作组的本地执行范围的分组执行范围。Nd_item类表示内核函数的单个实例,并允许查询工作组范围和索引,如下图所示。
本项目利用ND-Range Kernel来计算矩阵乘法。工作组的大小取决于加速器硬件功能,有些硬件需要矩阵大小除以工作组大小,此处默认使用16 × 16(256)的工作组大小,这适用于测试的所有加速器硬件,并且最终将使用不同的工作组大小来查看它如何影响性能,如下图所示。
4 核心代码
#include <CL/sycl.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <sstream>
#include <string>
#include <time.h>
using namespace sycl;
void read_file(std::string file_name, std::vector<std::vector<double>> &matrix)
{
std::ifstream file(file_name);
if (!file.is_open())
{
std::cerr << "can't open file" << std::endl;
}
std::string line;
while (std::getline(file, line))
{
std::vector<double> row;
std::istringstream lineStream(line);
double value;
while (lineStream >> value)
{
row.push_back(value);
}
matrix.push_back(row);
}
file.close();
}
int main()
{
queue q;
device my_device = q.get_device();
std::vector<std::vector<double>> matrix1;
std::vector<std::vector<double>> matrix2;
std::string matrix_file1 = "matrix1.txt";
std::string matrix_file2 = "matrix2.txt";
read_file(matrix_file1, matrix1);
read_file(matrix_file2, matrix2);
// printf("%d %d", matrix1.size(), matrix2.size());
int m = matrix1.size();
int k = matrix1[0].size();
int n = matrix2[0].size();
std::vector<std::vector<double>> matrix3(m, std::vector<double>(n));
int start1 = clock();
// for (int i = 0; i < m; i++)
// {
// for (int j = 0; j < n; j++)
// {
// for (int p = 0; p < k; p++)
// matrix3[i][j] += double(matrix1[i][p] * matrix2[p][j]);
// }
// }
int finish1 = clock();
int start2 = clock();
{
buffer bufV1{matrix1}, bufV2{matrix2}, bufV3{matrix3};
q.submit([&](handler &h)
{
auto matrix1 = bufV1.get_access(h, read_only);
auto matrix2 = bufV2.get_access(h, read_only);
auto matrix3 = bufV3.get_access(h, write_only);
h.parallel_for(range<3>(m, n, k), [=](id<3> index) {
int i = index[0];
int j = index[1];
int p = index[2];
matrix3[i][j] += double(matrix1[i][p] * matrix2[p][j]);
}); });
}
int finish2 = clock();
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
printf("%.4f ", matrix3[i][j]);
}
printf("\n");
}
printf("in parallel compute:%f\n", (double)(finish2 - start2) / CLOCKS_PER_SEC);
printf("in direct compute:%f\n", (double)(finish1 - start1) / CLOCKS_PER_SEC);
return 0;
}
5 总结
Intel SYCL库有着诸多优点,首先是其采用的单一源代码编程模型,使得在同一份代码中可以方便地描述和执行跨多个处理器架构的并行计算。其次,SYCL支持异构计算,能够有效利用不同设备的计算资源,包括CPU、GPU和FPGA等。此外,通过内核函数的使用,可以更灵活地管理并行工作负载,同时SYCL提供了丰富的性能优化技术,使得在异构环境下实现高效的计算成为可能。这使得Intel SYCL成为在科学计算、人工智能和图形处理等领域应用广泛的工具之一。