问题陈述1
编写⼀个基于oneAPI的C++/SYCL程序来执行矩阵乘法操作。需要考虑大尺寸矩阵的乘法操作以及不同线程之间的数据依赖关系。通常在实现矩阵乘法时,可以使用块矩阵乘法以及共享内存来提高计算效率。
分析
利用基于SYCL的编程模型在GPU上实现矩阵乘法的计算,步骤如下:
-
分配内存:在主机端分配内存空间用于存储输⼊矩阵和输出矩阵,同时在GPU端分配内存空间用于存储相应的输入和输出数据。
-
数据传输:将输入矩阵数据从主机端内存传输到GPU端内存中。
-
核函数调用:在SYCL中,矩阵乘法的计算通常会在GPU上使用核函数来实现并行计算。核函数
会分配线程块和线程来处理不同的数据块。
-
并行计算:在核函数中,每个线程负责计算输出矩阵的⼀个单独的元素。为了最大限度地利用
GPU的并行计算能力,通常会使用⼆维线程块和线程网格的方式来处理矩阵的乘法计算。
-
数据传输:计算完成后,将输出矩阵数据从GPU端内存传输回主机端内存中,以便进⼀步处理或分析。
在并行计算矩阵乘法时,可以利用线程块和线程的层次结构来优化计算。通过合理划分矩阵数据并利用共享内 存来减少全局内存访问的次数,可以⼤幅提高计算效率。此外,还可以利用GPU上的多个计算单元并执行行矩阵乘法,进⼀步提高计算速度
项目简介
本项目致力于使用oneAPI技术栈,特别是其Data Parallel C++ (DPC++) 编程模型,来实现并行矩阵乘法,从而显著提升计算效率和性能。通过充分利用现代GPU的并行处理能力,我们可以处理更大规模的数据集并加速计算过程。
技术实现
-
技术栈:
-
oneAPI:一个全新的跨架构编程模型,允许开发者无缝地在不同类型的计算架构上运行代码。
-
DPC++:基于现代C++的高级编程语言,用于编写并行代码并利用异构计算平台的多样性。
-
-
并行矩阵乘法实现:
-
块矩阵乘法:将大矩阵分解为更小的块,这些块可以被独立计算,从而实现真正的并行处理。
-
共享内存优化:使用GPU的共享内存来存储中间数据,减少对全局内存的访问,这样可以降低延迟并提高带宽利用率。
-
实现过程
数据准备
我们的目标是比较在不同计算模型下执行同一计算任务的性能。为此,我们生成了两个大规模矩阵,矩阵 A 和矩阵 B,每个矩阵的尺寸均为 1000x1000,这意味着我们将计算一个包含一百万个浮点数的矩阵乘法。
为了创建这些矩阵,我们编写了一个函数 generateLargeRandomMatrix
,它接受行和列的数量作为参数,并返回一个填充了随机浮点数的 std::vector<float>
。这里,我们使用 C++ 的 rand()
函数来生成随机数,并将其标准化到 0 到 1 之间的浮点数。
std::vector<float> generateLargeRandomMatrix(size_t rows, size_t cols) {
std::vector<float> matrix(rows * cols);
for (size_t i = 0; i < rows * cols; ++i) {
matrix[i] = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
return matrix;
}
非并行矩阵乘法
在 CPU 上实现非并行矩阵乘法,我们简单地使用三层嵌套循环来遍历矩阵 A 的行、矩阵 B 的列,以及它们共同的维度。对于结果矩阵 C 中的每个元素,我们计算 A 的一行与 B 的一列对应元素的乘积和。
void matrixMultiplySerial(const std::vector<float> &A, const std::vector<float> &B, std::vector<float> &C, size_t widthA, size_t heightA, size_t widthB) {
assert(A.size() == widthA * heightA);
assert(B.size() == widthB * widthA);
assert(C.size() == widthB * heightA);
for (size_t i = 0; i < heightA; ++i) {
for (size_t j = 0; j < widthB; ++j) {
float sum = 0.0f;
for (size_t k = 0; k < widthA; ++k) {
sum += A[i * widthA + k] * B[k * widthB + j];
}
C[i * widthB + j] = sum;
}
}
}
并行矩阵乘法
对于并行矩阵乘法,我们使用 SYCL 框架来利用 GPU 的并行处理能力。我们首先创建一个 SYCL 队列来管理并行任务。然后,我们分配共享内存以便 CPU 和 GPU 都能访问矩阵 A、B 和 C。最关键的步骤是定义一个 parallel_for
工作项,它将计算任务分配到 GPU 的多个线程上。
void matrixMultiplyParallel(queue &q, const std::vector<float> &A, const std::vector<float> &B, std::vector<float> &C, size_t widthA, size_t heightA, size_t widthB) {
assert(A.size() == widthA * heightA);
assert(B.size() == widthB * widthA);
assert(C.size() == widthB * heightA);
float *a_data = malloc_shared<float>(A.size(), q);
float *b_data = malloc_shared<float>(B.size(), q);
float *c_data = malloc_shared<float>(C.size(), q);
std::copy(A.begin(), A.end(), a_data);
std::copy(B.begin(), B.end(), b_data);
range<2> global_range((heightA + 15) / 16 * 16, (widthB + 15) / 16 * 16);
range<2> local_range(16, 16);
q.submit([&](handler &h) {
h.parallel_for(nd_range<2>(global_range, local_range), [=](nd_item<2> item) {
size_t row = item.get_global_id(0);
size_t col = item.get_global_id(1);
if (row < heightA && col < widthB) {
float sum = 0;
for (size_t k = 0; k < widthA; ++k) {
sum += a_data[row * widthA + k] * b_data[k * widthB + col];
}
c_data[row * widthB + col] = sum;
}
});
}).wait();
std::copy(c_data, c_data + C.size(), C.data());
free(a_data, q);
free(b_data, q);
free(c_data, q);
}
在这个并行版本中,每个工作项计算结果矩阵 C 中的一个元素。我们定义了一个 2D 范围来表示工作项的全局布局,并将其分为 16x16 的本地工作组,这有助于优化 GPU 上的内存访问模式。
性能测试
在完成算法实现后,我们进行性能测试。我们使用 C++ 的 <chrono>
库来记录并行和非并行矩阵乘法各自的执行时间。我们分别测量并记录这两种方法在处理同样的大型数据集时的耗时。
// 计时并行矩阵乘法
auto start_parallel = high_resolution_clock::now();
matrixMultiplyParallel(q, A, B, C_parallel, widthA, heightA, widthB);
auto stop_parallel = high_resolution_clock::now();
// 计时非并行矩阵乘法
auto start_serial = high_resolution_clock::now();
matrixMultiplySerial(A, B, C_serial, widthA, heightA, widthB);
auto stop_serial = high_resolution_clock::now();
结果分析
我们对比了并行和非并行方法的执行时间,发现并行矩阵乘法在 GPU 上的执行速度要远远快于在 CPU 上执行的非并行矩阵乘法。这一结果强调了并行计算在处理大规模数据时的显著性能优势。
源代码
#include <CL/sycl.hpp>
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <chrono>
using namespace sycl;
using namespace std::chrono;
std::vector<float> generateLargeRandomMatrix(size_t rows, size_t cols) {
std::vector<float> matrix(rows * cols);
for (size_t i = 0; i < rows * cols; ++i) {
matrix[i] = static_cast<float>(rand()) / static_cast<float>(RAND_MAX); // 生成 0 到 1 之间的随机数
}
return matrix;
}
// 并行版本的矩阵乘法
void matrixMultiplyParallel(queue &q, const std::vector<float> &A, const std::vector<float> &B, std::vector<float> &C, size_t widthA, size_t heightA, size_t widthB) {
assert(A.size() == widthA * heightA);
assert(B.size() == widthB * widthA);
assert(C.size() == widthB * heightA);
float *a_data = malloc_shared<float>(A.size(), q);
float *b_data = malloc_shared<float>(B.size(), q);
float *c_data = malloc_shared<float>(C.size(), q);
std::copy(A.begin(), A.end(), a_data);
std::copy(B.begin(), B.end(), b_data);
range<2> global_range((heightA + 15) / 16 * 16, (widthB + 15) / 16 * 16);
range<2> local_range(16, 16);
q.submit([&](handler &h) {
h.parallel_for(nd_range<2>(global_range, local_range), [=](nd_item<2> item) {
size_t row = item.get_global_id(0);
size_t col = item.get_global_id(1);
if (row < heightA && col < widthB) {
float sum = 0;
for (size_t k = 0; k < widthA; ++k) {
sum += a_data[row * widthA + k] * b_data[k * widthB + col];
}
c_data[row * widthB + col] = sum;
}
});
}).wait();
std::copy(c_data, c_data + C.size(), C.data());
free(a_data, q);
free(b_data, q);
free(c_data, q);
}
// 非并行版本的矩阵乘法
void matrixMultiplySerial(const std::vector<float> &A, const std::vector<float> &B, std::vector<float> &C, size_t widthA, size_t heightA, size_t widthB) {
assert(A.size() == widthA * heightA);
assert(B.size() == widthB * widthA);
assert(C.size() == widthB * heightA);
for (size_t i = 0; i < heightA; ++i) {
for (size_t j = 0; j < widthB; ++j) {
float sum = 0.0f;
for (size_t k = 0; k < widthA; ++k) {
sum += A[i * widthA + k] * B[k * widthB + j];
}
C[i * widthB + j] = sum;
}
}
}
int main() {
srand(static_cast<unsigned int>(time(0))); // 初始化随机数生成器
size_t widthA = 1000; // A 的宽度
size_t heightA = 1000; // A 的高度
size_t widthB = 1000; // B 的宽度
auto A = generateLargeRandomMatrix(heightA, widthA);
auto B = generateLargeRandomMatrix(widthA, widthB);
std::vector<float> C_parallel(heightA * widthB, 0.0f);
std::vector<float> C_serial(heightA * widthB, 0.0f);
queue q;
// 计时并行矩阵乘法
auto start_parallel = high_resolution_clock::now();
matrixMultiplyParallel(q, A, B, C_parallel, widthA, heightA, widthB);
auto stop_parallel = high_resolution_clock::now();
auto duration_parallel = duration_cast<milliseconds>(stop_parallel - start_parallel);
std::cout << "Parallel Matrix Multiply Time: " << duration_parallel.count() << " milliseconds" << std::endl;
// 计时非并行矩阵乘法
auto start_serial = high_resolution_clock::now();
matrixMultiplySerial(A, B, C_serial, widthA, heightA, widthB);
auto stop_serial = high_resolution_clock::now();
auto duration_serial = duration_cast<milliseconds>(stop_serial - start_serial);
std::cout << "Serial Matrix Multiply Time: " << duration_serial.count() << " milliseconds" << std::endl;
return 0;
}
项目成果
-
性能提升:通过对比实现前后的性能,可以看出并行矩阵乘法在处理大型数据时的显著效率提升。
-
并行矩阵乘法时间:133毫秒
-
非并行矩阵乘法时间:782毫秒
这一显著的性能差异证明了并行计算在加速大规模数据处理中的有效性。
-
-
扩展性:这种方法提供了良好的扩展性,可以适用于不同规模和类型的计算需求。
问题陈述2
描述
使用基于oneAPI的C++/SYCL实现⼀个高效的并行归并排序。需要考虑数据的分割和合并以及线程之间的协作。
分析&示例
归并排序是⼀种分治算法,其基本原理是将待排序的数组分成两部分,分别对这两部分进行排序,然后将已排
序的子数组合并为⼀个有序数组。可考虑利用了异构并行计算的特点,将排序和合并操作分配给多个线程同时
执行,以提高排序效率。具体实现过程如下:
-
将待排序的数组分割成多个较小的子数组,并将这些⼦数组分配给不同的线程块进行处理。
-
每个线程块内部的线程协作完成子数组的局部排序。
-
通过多次迭代,不断合并相邻的有序⼦数组,直到整个数组有序。
在实际实现中,归并排序可使用共享内存来加速排序过程。具体来说,可以利用共享内存来存储临时数据,减
少对全局内存的访问次数,从而提高排序的效率。另外,在合并操作中,需要考虑同步机制来保证多个线程之
间的数据⼀致性。
需要注意的是,在实际应用中,要考虑到数组大小、线程块大小、数据访问模式等因素,来设计合适的算法和
参数设置,以充分利用目标计算硬件GPU的并行计算能力,提高排序的效率和性能。
项目简介
本项目的核心是通过oneAPI技术栈中的Data Parallel C++ (DPC++) 编程模型,实现了并行归并排序算法。这种方法利用了异构并行计算的特点,将排序和合并操作分配给GPU上的多个线程同时执行,从而显著提高了排序过程的效率。
技术实现
-
技术栈:
-
oneAPI:提供了跨多种计算架构的高性能计算能力,包括CPU和GPU。
-
DPC++:一种基于现代C++的高级编程语言,使开发者能够编写可在异构硬件平台上运行的并行代码。
-
-
并行归并排序的实现:
-
数组分割:将待排序的数组分割成多个较小的子数组,并将这些子数组分配给不同的GPU线程块进行处理。
-
局部排序与合并:每个线程块内部的线程协作完成子数组的局部排序。然后,通过多次迭代,不断合并相邻的有序子数组,直到整个数组有序。
-
共享内存优化:在排序过程中利用共享内存来存储临时数据,减少对全局内存的访问,从而提高效率。
-
实现细节
1. 数据准备
-
代码从 "problem-2.txt" 文件中读取数据,这些数据被用作排序算法的输入。
-
数据被存储在
std::vector<float>
类型的data
数组中,每个元素为一个浮点数。
std::vector<float> data;
std::ifstream file("problem-2.txt");
float value;
while (file >> value) {
data.push_back(value);
}
2. 归并函数(mergeWG
)
-
mergeWG
函数负责在单个工作组内执行归并操作。它合并两个已排序的子数组(由left
、mid
、right
索引定义)到一个临时数组temp
中。 -
函数逐元素比较两个子数组,并将较小的元素依次复制到
temp
中,最后将排序好的元素复制回原数组。
void mergeWG(float *data, size_t left, size_t mid, size_t right, float *temp) {
size_t i = left, j = mid, k = left;
while (i < mid && j < right) {
if (data[i] < data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
while (i < mid) {
temp[k++] = data[i++];
}
while (j < right) {
temp[k++] = data[j++];
}
for (size_t i = left; i < right; ++i) {
data[i] = temp[i];
}
}
3. 并行归并排序(parallelMergeSort
)
-
共享内存分配:使用
malloc_shared
在GPU上为原始数据 (data_array
) 和临时数组 (temp
) 分配共享内存,允许CPU和GPU访问相同的内存空间。 -
工作组大小调整:动态获取设备支持的最大工作组大小(
max_wg_size
),并将工作组大小(local_size
)设置为最大工作组大小和数据大小之间的较小值。 -
全局和局部范围设置:根据工作组大小调整全局范围(
global_size
),确保全局范围是局部范围的整数倍,从而优化工作项的分配。 -
并行归并逻辑:在
parallel_for
中,每个工作项处理一部分数据的归并操作。随着循环的进行,归并的区间大小逐渐增加,直至覆盖整个数组。 -
减少全局同步点:使用
event.wait()
控制每个归并步骤的同步,减少不必要的等待,提高效率。
void parallelMergeSort(queue &q, float *data, size_t n) {
float *temp = malloc_shared<float>(n, q);
// 动态调整工作组大小
size_t max_wg_size = q.get_device().get_info<info::device::max_work_group_size>();
size_t local_size = std::min(max_wg_size, n);
size_t global_size = ((n + local_size - 1) / local_size) * local_size;
for (size_t size = 1; size < n; size *= 2) {
auto event = q.submit([&](handler &h) {
h.parallel_for(nd_range<1>(range<1>(global_size), range<1>(local_size)),
[=](nd_item<1> item) {
size_t id = item.get_global_id(0);
size_t start = id * size * 2;
if (start < n) {
size_t mid = std::min(start + size, n);
size_t end = std::min(start + 2 * size, n);
mergeWG(data, start, mid, end, temp);
}
});
});
// 减少全局同步点
event.wait();
}
free(temp, q);
}
4. 主函数(main
)
-
数据读取:从文件中读取浮点数,存储在
data
数组中。 -
内存分配和复制:使用
malloc_shared
分配内存给data_array
,并将data
中的数据复制到其中。 -
执行排序:调用
parallelMergeSort
函数进行并行归并排序。 -
结果输出:将排序后的数据写入 "sorted_output.txt" 文件。
-
排序结果验证:使用
std::is_sorted
检查data_array
是否已正确排序,并在控制台上输出验证结果。 -
释放为
data_array
分配的共享内存。
int main() {
queue q;
std::vector<float> data;
std::ifstream file("problem-2.txt");
float value;
while (file >> value) {
data.push_back(value);
}
// 获取数据大小
size_t n = data.size();
// 使用 malloc_shared 分配内存
float* data_array = malloc_shared<float>(n, q);
std::copy(data.begin(), data.end(), data_array);
// 执行并行归并排序
parallelMergeSort(q, data_array, n);
std::ofstream outfile("sorted_output.txt");
for (size_t i = 0; i < n; ++i) {
outfile << data_array[i] << '\n';
}
outfile.close(); // 关闭输出文件
// 验证排序结果
bool sorted = std::is_sorted(data_array, data_array + n);
std::cout << "数组是否已排序: " << (sorted ? "是" : "否") << std::endl;
free(data_array, q);
return 0;
}
5.源代码
#include <CL/sycl.hpp>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace sycl;
// 归并函数,执行在单个工作组内
void mergeWG(float *data, size_t left, size_t mid, size_t right, float *temp) {
size_t i = left, j = mid, k = left;
while (i < mid && j < right) {
if (data[i] < data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
while (i < mid) {
temp[k++] = data[i++];
}
while (j < right) {
temp[k++] = data[j++];
}
for (size_t i = left; i < right; ++i) {
data[i] = temp[i];
}
}
// 并行归并排序的主要函数
void parallelMergeSort(queue &q, float *data, size_t n) {
float *temp = malloc_shared<float>(n, q);
// 动态调整工作组大小
size_t max_wg_size = q.get_device().get_info<info::device::max_work_group_size>();
size_t local_size = std::min(max_wg_size, n);
size_t global_size = ((n + local_size - 1) / local_size) * local_size;
for (size_t size = 1; size < n; size *= 2) {
auto event = q.submit([&](handler &h) {
h.parallel_for(nd_range<1>(range<1>(global_size), range<1>(local_size)),
[=](nd_item<1> item) {
size_t id = item.get_global_id(0);
size_t start = id * size * 2;
if (start < n) {
size_t mid = std::min(start + size, n);
size_t end = std::min(start + 2 * size, n);
mergeWG(data, start, mid, end, temp);
}
});
});
// 减少全局同步点
event.wait();
}
free(temp, q);
}
int main() {
queue q;
std::vector<float> data;
std::ifstream file("problem-2.txt");
float value;
while (file >> value) {
data.push_back(value);
}
// 获取数据大小
size_t n = data.size();
// 使用 malloc_shared 分配内存
float* data_array = malloc_shared<float>(n, q);
std::copy(data.begin(), data.end(), data_array);
// 执行并行归并排序
parallelMergeSort(q, data_array, n);
std::ofstream outfile("sorted_output.txt");
for (size_t i = 0; i < n; ++i) {
outfile << data_array[i] << '\n';
}
outfile.close(); // 关闭输出文件
// 验证排序结果
bool sorted = std::is_sorted(data_array, data_array + n);
std::cout << "数组是否已排序: " << (sorted ? "是" : "否") << std::endl;
free(data_array, q);
return 0;
}
项目成果
-
有效性验证:通过验证排序结果的正确性,我们证实了并行归并排序算法的实现是有效的。
-
性能提升:使用GPU并行处理大规模数据集应该能比传统的串行处理方式更加高效。
问题陈述3
描述
使用基于oneAPI的C++/SYCL实现一个用于计算图像的卷积操作。输⼊为一个图像矩阵和一个卷积核矩阵,输出为卷积后的图像。
分析
图像卷积是一种常见的图像处理操作,用于应用各种滤波器和特征检测器。其原理可以简单地描述为在图像的每个像素上应用一个小的矩阵(通常称为卷积核或滤波器),并将卷积核中的元素与图像中对应位置的像素值相乘,然后将所有乘积的和作为结果。这个过程可以看作是对图像进行了平滑、锐化、边缘检测等操作。
假设有⼀个大小为M × N 的输入图像I 和一个大小为m × n 的卷积核 K 。图像卷积操作可以用下面的数学公式来表示:
其中, S(i, j)是卷积操作的结果图像中位置 (i, j) 处的像素值。 I(i + k, j + l) 是图像中位置 (i + k, j + l) 处的像素值, K(k, l) 是卷积核中位置 (k, l) 处的权重。卷积核通常是一个小的⼆维矩阵,用于捕捉图像中的特定特征。在异构计算编程中,可以使用并行计算来加速图像卷积操作。通过将图像分割成小块,然后在GPU上并行处理
这些块,可以实现高效的图像卷积计算。通过合理的块大小和线程组织方式,可以最大限度地利用GPU的并行计算能力来加速图像处理过程。
基于GPU的图像卷积操作的原理基于并行处理和矩阵乘法的基本原理,通过将图像数据和卷积核数据分配给不同的线程块和线程,利用GPU的并行计算能力实现对图像的快速处理。
项目简介
本项目旨在使用oneAPI技术栈,特别是其Data Parallel C++ (DPC++) 编程模型,实现图像卷积的并行计算。通过充分利用现代GPU的并行处理能力,我们能够显著提升图像卷积操作的效率,从而加速图像处理过程。
技术实现
-
技术栈:
-
oneAPI: 一个全新的跨架构编程模型,允许开发者无缝地在不同类型的计算架构上运行代码。
-
DPC++: 基于现代C++的高级编程语言,用于编写并行代码并利用异构计算平台的多样性。
-
-
并行图像卷积实现:
-
读取图像和卷积核数据: 从文本文件中读取图像和卷积核数据。
-
内存分配: 在共享内存中分配空间以存储输入图像、卷积核和输出图像。
-
并行卷积: 使用SYCL队列和核函数对图像执行并行卷积操作。
-
实现过程
数据读取和准备
-
从文本文件中读取图像数据和卷积核数据。
-
确保数据格式和尺寸正确,以适配卷积操作。
size_t image_width = 100; // 图像宽度
size_t image_height = 100; // 图像高度
size_t kernel_width = 5; // 卷积核宽度
size_t kernel_height = 5; // 卷积核高度
// 读取输入图像和卷积核数据
std::vector<float> input_image = readImageDataFromFile("problem-3.txt");
std::vector<float> kernel = readKernelDataFromFile("problem-3.txt");
// 校验读入的数据
assert(input_image.size() == image_width * image_height);
assert(kernel.size() == kernel_width * kernel_height);
并行卷积操作
实现卷积操作的核心是 convolve2D
函数。该函数使用SYCL队列将卷积计算任务提交到GPU。每个工作项处理图像中的一个像素,应用卷积核进行计算。为了处理边界情况,我们采用了镜像边界的策略。
void convolve2D(queue &q, const std::vector<float> &input_image, std::vector<float> &output_image,
const std::vector<float> &kernel, size_t image_width, size_t image_height,
size_t kernel_width, size_t kernel_height) {
// 确保输入图像和输出图像的大小是合适的
assert(input_image.size() == image_width * image_height);
assert(output_image.size() == image_width * image_height);
// 在共享内存中为输入图像、输出图像和卷积核分配空间
float *input_data = malloc_shared<float>(input_image.size(), q);
float *output_data = malloc_shared<float>(output_image.size(), q);
float *kernel_data = malloc_shared<float>(kernel.size(), q);
// 将数据复制到共享内存
std::copy(input_image.begin(), input_image.end(), input_data);
std::copy(kernel.begin(), kernel.end(), kernel_data);
// 定义范围
range<2> global_range(image_height, image_width);
// 提交卷积操作到队列
q.submit([&](handler &h) {
h.parallel_for(global_range, [=](id<2> idx) {
size_t i = idx[0];
size_t j = idx[1];
float sum = 0.0f;
for (size_t k = 0; k < kernel_height; ++k) {
for (size_t l = 0; l < kernel_width; ++l) {
// 对应于图像边界的镜像扩展
size_t img_i = std::min(image_height - 1, std::max(static_cast<size_t>(0), i + k - kernel_height / 2));
size_t img_j = std::min(image_width - 1, std::max(static_cast<size_t>(0), j + l - kernel_width / 2));
sum += input_data[img_i * image_width + img_j] * kernel_data[k * kernel_width + l];
}
}
output_data[i * image_width + j] = sum;
});
}).wait();
// 将计算后的输出数据复制回输出图像
std::copy(output_data, output_data + output_image.size(), output_image.data());
// 释放内存
free(input_data, q);
free(output_data, q);
free(kernel_data, q);
}
结果分析
成功在输出文件中生成了经过卷积处理的图像内容,验证了算法的正确性和高效性。
源代码
%%writefile lab/vector_add.cpp
//==============================================================
// Copyright © Intel Corporation
//
// SPDX-License-Identifier: MIT
// =============================================================
#include <CL/sycl.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <sstream>
#include <cmath>
#include <cassert>
#include <opencv2/opencv.hpp>
using namespace sycl;
std::vector<float> readImageDataFromFile(const std::string& filename) {
std::vector<float> image_data;
std::ifstream file(filename);
std::string line;
bool start_reading = false; // 用于标记开始读取图像数据
while (std::getline(file, line)) {
if (line == "Matrix:") {
start_reading = true;
continue;
}
if (start_reading) {
std::istringstream iss(line);
float num;
while (iss >> num) {
image_data.push_back(num);
}
}
if (line == "Convolution Kernel:") break; // 当到达卷积核部分时停止读取
}
return image_data;
}
std::vector<float> readKernelDataFromFile(const std::string& filename) {
std::vector<float> kernel_data;
std::ifstream file(filename);
std::string line;
bool start_reading = false; // 用于标记开始读取卷积核数据
while (std::getline(file, line)) {
if (line == "Convolution Kernel:") {
start_reading = true;
continue;
}
if (start_reading) {
std::istringstream iss(line);
float num;
while (iss >> num) {
kernel_data.push_back(num);
}
}
}
return kernel_data;
}
void convolve2D(queue &q, const std::vector<float> &input_image, std::vector<float> &output_image,
const std::vector<float> &kernel, size_t image_width, size_t image_height,
size_t kernel_width, size_t kernel_height) {
// 确保输入图像和输出图像的大小是合适的
assert(input_image.size() == image_width * image_height);
assert(output_image.size() == image_width * image_height);
// 在共享内存中为输入图像、输出图像和卷积核分配空间
float *input_data = malloc_shared<float>(input_image.size(), q);
float *output_data = malloc_shared<float>(output_image.size(), q);
float *kernel_data = malloc_shared<float>(kernel.size(), q);
// 将数据复制到共享内存
std::copy(input_image.begin(), input_image.end(), input_data);
std::copy(kernel.begin(), kernel.end(), kernel_data);
// 定义范围
range<2> global_range(image_height, image_width);
// 提交卷积操作到队列
q.submit([&](handler &h) {
h.parallel_for(global_range, [=](id<2> idx) {
size_t i = idx[0];
size_t j = idx[1];
float sum = 0.0f;
for (size_t k = 0; k < kernel_height; ++k) {
for (size_t l = 0; l < kernel_width; ++l) {
// 对应于图像边界的镜像扩展
size_t img_i = std::min(image_height - 1, std::max(static_cast<size_t>(0), i + k - kernel_height / 2));
size_t img_j = std::min(image_width - 1, std::max(static_cast<size_t>(0), j + l - kernel_width / 2));
sum += input_data[img_i * image_width + img_j] * kernel_data[k * kernel_width + l];
}
}
output_data[i * image_width + j] = sum;
});
}).wait();
// 将计算后的输出数据复制回输出图像
std::copy(output_data, output_data + output_image.size(), output_image.data());
// 释放内存
free(input_data, q);
free(output_data, q);
free(kernel_data, q);
}
int main() {
size_t image_width = 100; // 图像宽度
size_t image_height = 100; // 图像高度
size_t kernel_width = 5; // 卷积核宽度
size_t kernel_height = 5; // 卷积核高度
// 读取输入图像和卷积核数据
std::vector<float> input_image = readImageDataFromFile("problem-3.txt");
std::vector<float> kernel = readKernelDataFromFile("problem-3.txt");
// 校验读入的数据
assert(input_image.size() == image_width * image_height);
assert(kernel.size() == kernel_width * kernel_height);
// 创建输出图像容器
std::vector<float> output_image(image_width * image_height, 0);
// 创建SYCL队列
queue q;
// 执行卷积操作
convolve2D(q, input_image, output_image, kernel, image_width, image_height, kernel_width, kernel_height);
// 输出结果到文件
std::ofstream outfile("output_image.txt");
for (size_t i = 0; i < image_height; ++i) {
for (size_t j = 0; j < image_width; ++j) {
outfile << output_image[i * image_width + j] << ' ';
}
outfile << '\n';
}
outfile.close();
return 0;
}
项目成果
-
性能提升: 并行卷积操作相比于传统的单线程方法显示了显著的性能提升。
-
可扩展性: 该方法具有良好的可扩展性,可以适用于更大尺寸的图像和更复杂的卷积核。
学到了什么
通过这个项目,我们深入了解了以下几点:
-
并行计算的深入理解:通过这三个项目,深入理解了并行计算在不同应用中的实现,包括矩阵乘法、排序算法和图像卷积。这增强了对并行处理模式和数据分布策略的认识。
-
oneAPI和SYCL的应用:通过使用oneAPI和SYCL,学习了如何有效地在GPU上编写和执行并行代码,从而提高计算效率。这包括内存管理、核函数编写和性能调优。
-
性能评估技巧:通过比较并行和非并行方法的性能,掌握了性能评估的重要技巧。这不仅涉及编码实现,还包括合理地设置测试基准和解读结果。
-
异构计算的优势:理解了利用GPU和CPU各自优势的重要性。GPU适合处理大规模并行任务,而CPU更适合序列化任务。
-
解决实际问题的能力:这些项目提升了解决实际计算问题的能力,特别是在数据处理和图像处理领域。
结论
通过这三个项目,展示了并行计算在现代数据处理和图像处理中的重要性和效果。利用oneAPI和SYCL等先进工具,我们能够有效地利用GPU的强大计算能力,显著提高任务执行速度,处理更大规模的数据。这些经验和技能对于解决当今世界面临的复杂计算挑战至关重要。未来,随着硬件和软件技术的进一步发展,期待并行计算在多个领域解锁更多的潜力,推动科技和工业的进步。