intel oneAPI jupyterlab 异构计算实验

团队名称:中南ovo

一、引言

        本次实验报告源于中南大学intel校企合作,我在C++课上对intel oneAPI jupyterlab 进行了学习,在这上面了解了SYCL相关知识,使用英特尔Developer Cloud平台进行矩阵乘法的异构计算,并探索不同统一共享内存(USM)类型对性能的影响。

        进行本次实验需要在C++的基础上,了解SYCL编程知识,建议阅读前先进行SYCL编程学习。

二、oneAPI平台简单介绍

        oneAPI 是一个由英特尔(Intel)推出的跨平台编程模型,旨在简化和统一多种计算架构(如CPU、GPU、FPGA等)的编程。其核心目标是提供一种能够在多种硬件上运行的统一编程方法,以减少开发者在针对不同硬件编写和优化代码时的工作量。

oneAPI 包括一系列的工具和库,如:

  • DPC++(Data Parallel C++):一种基于C++的编程语言,用于编写可以在多种硬件架构上运行的并行代码。
  • Level Zero:一种低级别的硬件抽象层,用于直接与硬件交互。
  • oneDNN、oneTBB等库:提供针对深度学习、线程构建块等的优化。

通过这些工具和库,oneAPI 旨在提高开发效率,同时保持高性能的计算能力,使开发者能够更容易。

三、实验

(1)环境配置:推荐使用英特尔oneAPI Developer Cloud 服务,可免安装额外环境,直接利用Developer Cloud平台中的CPU与GPU硬件完成相应的作业。 相应注册及启用服务可参考相应的使用手册,关于在登录节点上运行的JupyterLab递交代码到计算节点的排队系统的相关操作方式及命令,可以参考任务递交相应命令参考 以及 队列管理参考 。     

如果使用自有硬件,推荐使用基于英特尔酷睿6代或更新版本的处理器与系统中同时可用的基于Xe或Arc的集成显卡,通过完成与安装操作系统配套的相应oneAPI软件工具,通过构建自有开发环境方式完成作业。

相应的工具可参考oneAPI基础工具套件或独立编译器的网页, 硬件要求可参考发布说明中的具体要求。

(2)在开始进行实验之前,建议先阅读intel oneAPI jupyterlab 平台“SYCL_PROGRAM_STRUCTURE”文档进行学习,网址:JupyterHub

我们先来看文档中的 vector add实验

Lab Exercise: Vector Add

Complete the coding excercise below using SYCL Buffer and Accessor concepts:

  • The code has three vector vector1 initialized on host
  • The kernel code increments the vector1 by 1.
  • Create a new second vector2 and initialize to value 20.
  • Create sycl buffers for the above second vector
  • In the kernel code, create a second accessor for the second vector buffer
  • Modify the vector increment to vector add, by adding vector2 to vector1
  1. Edit the code cell below by following the steps and then click run ▶ to save the code to a file.
  2. Next run ▶ the cell in the Build and Run section below the code to compile and execute the code.

#include <sycl/sycl.hpp>
​
using namespace sycl;
​
int main() {
    const int N = 256;
    
    //# Initialize a vector and print values
    std::vector<int> vector1(N, 10);
    std::cout << "\nInput Vector1: ";    
    for (int i = 0; i < N; i++) std::cout << vector1[i] << " ";
​
    //# STEP 1 : Create second vector, initialize to 20 and print values
    std::vector<int> vector2(N, 20);
    std::cout << "\nInput Vector2: ";    
    for (int i = 0; i < N; i++) std::cout << vector2[i] << " ";
    
    //# Create Buffer
    buffer vector1_buffer(vector1);
    
    //# STEP 2 : Create buffer for second vector 
    buffer vector2_buffer(vector2);
​
    //# Submit task to add vector
    queue q;
    q.submit([&](handler &h) {
        //# Create accessor for vector1_buffer
        accessor vector1_accessor(vector1_buffer, h);
      
        //# STEP 3 - add second accessor for second buffer
        accessor vector2_accessor(vector2_buffer, h, read_only);
​
        h.parallel_for(range<1>(N), [=](id<1> index) {
            //# STEP 4 : Modify the code below to add the second vector to first one
            vector1_accessor[index] += vector2_accessor[index];
        });
    });
​
    //# Create a host accessor to copy data from device to host
    host_accessor h_a(vector1_buffer, read_only);
​
    //# Print Output values 
    std::cout << "\nOutput Values: ";
    for (int i = 0; i < N; i++) std::cout << h_a[i] << " ";
    std::cout << "\n";
​
    return 0;
}
​

这个小实验是我们进行sycl编程的开始,它的核心是用sycl编写两个向量相加,并在异构计算环境中运行这个程序。

在刚接触sycl的情况下,我认为在实验中主要有以下难点:

1.理解并行计算的概念

在SYCL中,kernel是在device(如GPU)上并行执行的。这就要求我们思考,如何充分利用device的计算资源,例如,将原来C++程序中的循环结构改成device中并行计算的kernel。

2.熟悉SYCL的缓冲区和访问器

  • 缓冲区(Buffer):缓冲区是SYCL中用于存储数据并在主机和设备之间传输数据的对象。它抽象了内存管理的细节,允许SYCL运行时负责在主机和设备间透明地复制数据。

  • 访问器(Accessor):访问器是用于访问缓冲区中数据的对象。它提供了一种方法来读取或写入缓冲区中的数据,并可以指定访问类型(读、写或读写)和访问范围。

    这里给出代码实例方便理解:
    // 创建缓冲区
    buffer<float, 2> bufferA(A.data(), range<2>(N, N));
    
    // 在内核函数中创建访问器
    q.submit([&](handler& h) {
        auto accA = bufferA.get_access<access::mode::read>(h);
        auto accB = bufferB.get_access<access::mode::read>(h);
        auto accC = bufferC.get_access<access::mode::write>(h);
    
        // 使用访问器进行计算
        h.parallel_for(range<2>(N, N), [=](id<2> id) {
            size_t i = id[0];
            size_t j = id[1];
            accC[i][j] = accA[i][j] + accB[i][j];
        });
    });
    

    在这个例子中,首先为矩阵A、B和C创建了二维缓冲区。然后,在内核函数中创建了这些缓冲区的访问器。这些访问器用于读取A和B的内容,以及写入C的结果。在内核中使用这些访问器来执行实际的向量加法操作。

 理解了上面这两个难点,在熟悉相应的语法后,可以将add vector 实验补充完整:

#include <sycl/sycl.hpp>

using namespace sycl;

int main() {
    const int N = 256;
    
    //# Initialize a vector and print values
    std::vector<int> vector1(N, 10);
    std::cout << "\nInput Vector1: ";    
    for (int i = 0; i < N; i++) std::cout << vector1[i] << " ";

    //# STEP 1 : Create second vector, initialize to 20 and print values
    std::vector<int> vector2(N, 20);
    std::cout << "\nInput Vector2: ";    
    for (int i = 0; i < N; i++) std::cout << vector2[i] << " ";
    
    //# Create Buffer
    buffer vector1_buffer(vector1);
    
    //# STEP 2 : Create buffer for second vector 
    buffer vector2_buffer(vector2);

    //# Submit task to add vector
    queue q;
    q.submit([&](handler &h) {
        //# Create accessor for vector1_buffer
        accessor vector1_accessor(vector1_buffer, h);
      
        //# STEP 3 - add second accessor for second buffer
        accessor vector2_accessor(vector2_buffer, h, read_only);

        h.parallel_for(range<1>(N), [=](id<1> index) {
            //# STEP 4 : Modify the code below to add the second vector to first one
            vector1_accessor[index] += vector2_accessor[index];
        });
    });

    //# Create a host accessor to copy data from device to host
    host_accessor h_a(vector1_buffer, read_only);

    //# Print Output values 
    std::cout << "\nOutput Values: ";
    for (int i = 0; i < N; i++) std::cout << h_a[i] << " ";
    std::cout << "\n";

    return 0;
}

下图为在jupyterlab平台上运行的结果,可知结果正确:

这个小实验帮助我们理解了sycl中并行计算的概念,熟悉了sycl语法,下面让我们正式开始实验任务

(3)实验任务二:

矩阵乘法运算 

       按以下步骤写一个SYCL程序来实现包含两个矩阵的矩阵乘法运算: 

组建2个序列(向量)的浮点数,每个序列的规格是N(如N=1024*1024),构成矩阵输入值。用随机值初始化序列。使用缓存和存储来实现对设备(GPU)上矩阵内存的分配并运行。运行SYCL Kernel实现两个矩阵的并行运算,这里你需要运用SYCL nd_range概念来定义Kernel的运行范围。使用SYCL排队系统来运行设备上的Kernel。 Kernel运行结束后,使用存储将结果从设备(GPU)检索回主机。 

把这个任务分解成小任务,主要要攻克的点有以下部分:

1. 矩阵初始化

2. 缓冲区和访问器的使用

3. 编写SYCL Kernel进行矩阵乘法

4. 使用nd_range定义Kernel的运行范围

5. 使用SYCL排队系统和内核执行

6. 数据回传

接下来我们逐一分析这些关键点:

1.矩阵初始化

这里我们使用 <random> 库生成随机数并填充矩阵;并进行相应措施确保程序能够处理大量数据

void initialize_matrix(vector<float>& matrix) {
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (auto& element : matrix) {
        element = dist(mersenne_engine);
    }
}

2. 缓冲区和访问器的使用

这里的难点在于创建和管理SYCL缓冲区,正确使用访问器来读写缓冲区中的数据。

关键代码:

buffer<float, 2> bufferA(A.data(), range<2>(N, N));

这段代码创建了一个二维缓冲区 bufferA,用于存储矩阵A的数据。类似地,我们也可以按照相同方式为矩阵B和C创建缓冲区。这些缓冲区将在SYCL设备上使用。

3. 编写SYCL Kernel进行矩阵乘法

关键点:(1)处理索引和边界条件 (2)利用并行计算优势,分解矩阵乘法为可并行执行的子任务

关键代码:

h.parallel_for(work_range, [=](nd_item<2> item) {
    size_t i = item.get_global_id(0);
    size_t j = item.get_global_id(1);
    float sum = 0.0f;
    for (size_t k = 0; k < N; ++k) {
        sum += accA[i * N + k] * accB[k * N + j];
    }
    accC[i * N + j] = sum;
});

4. 使用nd_range定义Kernel的运行范围

难点:理解nd_range概念,正确设置工作组大小和形状

关键代码:

nd_range<2> work_range(range<2>(N, N), range<2>(16, 16));

鉴于nd_range理解较难,这里我们具体讲讲每个参数含义:

  1. 二维索引空间nd_range<2> 表示这是一个二维的工作索引空间。这里的“2”代表了工作项的维度,适用于像矩阵乘法这样的二维数据操作。

  2. 全局范围range<2>(N, N) 定义了全局索引空间的大小,即在每个维度上有多少工作项。在这个例子中,每个维度的大小都被设置为了 N,这对应于矩阵的行和列的大小。这意味着相应的kernel将会有 N x N 个工作项。

  3. 局部范围range<2>(16, 16) 定义了每个工作组中的工作项的布局和数量。每个工作组包含 16 x 16 个工作项。

5. 使用SYCL排队系统和内核执行

难点:管理队列,确保Kernel按预期执行;处理执行和同步问题。

关键代码:

queue q{ property::queue::enable_profiling() };
auto event = q.submit([&](handler& h) { /* ... */ });
event.wait();

6. 数据回传

关键代码:

host_accessor h_accC(bufferC, read_only);
for (size_t i = 0; i < N; ++i) {
    for (size_t j = 0; j < N; ++j) {
        C[i * N + j] = h_accC[i][j];
    }
}

这段代码中,使用了一个主机访问器 h_accC 来读取缓冲区 bufferC 的内容。这个访问器允许在主机端访问设备端的计算结果。然后通过一个双重循环,将计算结果从SYCL缓冲区复制回标准向量 C

接下来给出完整的解决方案:

#include <CL/sycl.hpp>
#include <iostream>
#include <random>
#include <vector>
#include <chrono>

using namespace sycl;
using namespace std;

const size_t N = 1024;

// 用随机浮点数初始化矩阵的函数
void initialize_matrix(vector<float>& matrix) {
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (auto& element : matrix) {
        element = dist(mersenne_engine);
    }
}

int main() {
    // 主机内存中的矩阵
    vector<float> A(N * N), B(N * N), C(N * N);

    // 使用随机数初始化矩阵
    initialize_matrix(A);
    initialize_matrix(B);

    // 设备缓冲区用于矩阵
    buffer<float, 2> bufferA(A.data(), range<2>(N, N));
    buffer<float, 2> bufferB(B.data(), range<2>(N, N));
    buffer<float, 2> bufferC(C.data(), range<2>(N, N));

    // 使用默认选择器创建一个启用性能分析的队列
    queue q{ property::queue::enable_profiling() };

    // 提交一个命令组到队列以执行矩阵乘法
    auto event = q.submit([&](handler& h) {
        // 用于矩阵的访问器
        auto accA = bufferA.get_access<access::mode::read>(h);
        auto accB = bufferB.get_access<access::mode::read>(h);
        auto accC = bufferC.get_access<access::mode::write>(h);

        // 使用nd_range定义工作组布局
        nd_range<2> work_range(range<2>(N, N), range<2>(16, 16));

        // 执行矩阵乘法
        h.parallel_for(work_range, [=](nd_item<2> item) {
            size_t i = item.get_global_id(0);
            size_t j = item.get_global_id(1);
            float sum = 0.0f;
            for (size_t k = 0; k < N; ++k) {
                sum += accA[i][k] * accB[k][j]; // 修改索引方式
            }
            accC[i][j] = sum; // 修改索引方式
            });
        });

    // 等待队列中的所有任务完成
    event.wait();

    // 测量内核执行所用的时间
    auto start_time = event.get_profiling_info<info::event_profiling::command_start>();
    auto end_time = event.get_profiling_info<info::event_profiling::command_end>();
    auto execution_time = (end_time - start_time) / 1e6; // 从纳秒转换为毫秒

    // 从缓冲区读回结果到主机内存
    host_accessor h_accC(bufferC, read_only);
    for (size_t i = 0; i < N; ++i) {
        for (size_t j = 0; j < N; ++j) {
            C[i * N + j] = h_accC[i][j];
        }
    }

    // 打印结果矩阵C的第一个元素以进行验证
    cout << "C[0][0] = " << C[0] << "\n";
    // 打印执行时间
    cout << "矩阵乘法执行时间:" << execution_time << " 毫秒\n";

    return 0;
}

可以看出在上面的解决方案中,我还增加一个运行时间作为指标,因为我还想要进行比较,查看仅在CPU上进行计算和把CPU作为host,GPU作为device进行异构计算,在时间上的差异。以下是jupyterlab 上运行的结果

 下面是仅仅在CPU上运行的代码:

%%writefile lab/vector_add.cpp
//==============================================================
// Copyright © Intel Corporation
//
// SPDX-License-Identifier: MIT
// =============================================================
#include <iostream>
#include <random>
#include <vector>
#include <chrono>

using namespace std;

const size_t N = 4;

// 用随机浮点数初始化矩阵的函数
void initialize_matrix(vector<float>& matrix) {
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (auto& element : matrix) {
        element = dist(mersenne_engine);
    }
}

int main() {
    // 主机内存中的矩阵
    vector<float> A(N * N), B(N * N), C(N * N);

    // 使用随机数初始化矩阵
    initialize_matrix(A);
    initialize_matrix(B);

    // 记录矩阵乘法开始时间
    auto start = chrono::high_resolution_clock::now();

    // 执行矩阵乘法
    for (size_t i = 0; i < N; ++i) {
        for (size_t j = 0; j < N; ++j) {
            float sum = 0.0f;
            for (size_t k = 0; k < N; ++k) {
                sum += A[i * N + k] * B[k * N + j];
            }
            C[i * N + j] = sum;
        }
    }

    // 记录矩阵乘法结束时间
    auto end = chrono::high_resolution_clock::now();

    // 计算并打印执行时间
    auto duration = chrono::duration_cast<chrono::microseconds>(end - start).count();
    cout << "矩阵乘法执行时间:" << duration << " 微秒\n";

    // 打印结果矩阵C的第一个元素以进行验证
    cout << "C[0][0] = " << C[0] << "\n";

    return 0;
}

我们可以看到,相比仅在CPU上运行,使用异构计算明显加快了运算速度。

下面我们来看任务三:

(4)任务3:统一共享内存(简称“USM”) 

        统一共享内存在主机和设备(GPU)之间提供了一个统一的存储模式。修改你的程序以使用统一共享内存来实现内存分配和数据转换,从而替代缓存和存储。如果使用不同种类的USM将获得加分。重写代码以使用统一共享内存来实现内存分配和数据转换(如需要),照常运行Kernel来验证程序运行结果。 

在任务三中,我们将使用不同的USM(host、shared、device),并用运行时间作为指标,编写程序,发现他们的差异,并进行原因分析。

a.host_USM

难点分析

  1. 内存分配与释放:使用 malloc_host 在主机上分配内存,并确保在程序结束时正确释放这些内存。
  2. 内存访问与同步:虽然使用主机USM可以简化内存访问,但需要确保内核执行结束后再从主机端访问数据。

关键代码:

        1.内存分配

float* A = malloc_host<float>(N * N, default_selector{});
float* B = malloc_host<float>(N * N, default_selector{});
float* C = malloc_host<float>(N * N, default_selector{});

         2.矩阵乘法执行

q.submit([&](handler& h) {
    h.parallel_for(range<2>(N, N), [=](id<2> id) {
        size_t i = id[0];
        size_t j = id[1];
        float sum = 0.0f;
        for (size_t k = 0; k < N; ++k) {
            sum += A[i * N + k] * B[k * N + j];
        }
        C[i * N + j] = sum;
    });
}).wait();

        3.不要忘了内存释放

free(A, q.get_context());
free(B, q.get_context());
free(C, q.get_context());

完整代码:

#include <CL/sycl.hpp>
#include <iostream>
#include <random>
#include <chrono>

using namespace sycl;
using namespace std;

const size_t N = 1024;

void initialize_matrix(float* matrix, size_t size) {
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (size_t i = 0; i < size; ++i) {
        matrix[i] = dist(mersenne_engine);
    }
}

int main() {
    queue q{};

    // 使用 queue 的 context 来分配内存
    float* A = malloc_host<float>(N * N, q.get_context());
    float* B = malloc_host<float>(N * N, q.get_context());
    float* C = malloc_host<float>(N * N, q.get_context());

    initialize_matrix(A, N * N);
    initialize_matrix(B, N * N);

    auto start = std::chrono::high_resolution_clock::now();

    q.submit([&](handler& h) {
        h.parallel_for(range<2>(N, N), [=](id<2> id) {
            size_t i = id[0];
            size_t j = id[1];
            float sum = 0.0f;
            for (size_t k = 0; k < N; ++k) {
                sum += A[i * N + k] * B[k * N + j];
            }
            C[i * N + j] = sum;
        });
    }).wait();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    cout << "C[0][0] = " << C[0] << "\n";
    cout << "使用主机分配的USM进行矩阵乘法的执行时间:" << duration << " 毫秒\n";

    free(A, q.get_context());
    free(B, q.get_context());
    free(C, q.get_context());

    return 0;
}

运行结果:

b.改成使用shared_USM

#include <CL/sycl.hpp>
#include <iostream>
#include <random>
#include <chrono>

using namespace sycl;
using namespace std;

const size_t N = 1024;

void initialize_matrix(float* matrix, size_t size) {
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (size_t i = 0; i < size; ++i) {
        matrix[i] = dist(mersenne_engine);
    }
}

int main() {
    // 创建队列,使用default_selector_v和性能分析属性
    queue q(default_selector_v, property::queue::enable_profiling());

    float* A = malloc_shared<float>(N * N, q);
    float* B = malloc_shared<float>(N * N, q);
    float* C = malloc_shared<float>(N * N, q);

    initialize_matrix(A, N * N);
    initialize_matrix(B, N * N);

    auto start = std::chrono::high_resolution_clock::now();

    q.submit([&](handler& h) {
        h.parallel_for(range<2>(N, N), [=](id<2> id) {
            size_t i = id[0];
            size_t j = id[1];
            float sum = 0.0f;
            for (size_t k = 0; k < N; ++k) {
                sum += A[i * N + k] * B[k * N + j];
            }
            C[i * N + j] = sum;
        });
    }).wait();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    cout << "C[0][0] = " << C[0] << "\n";
    cout << "使用Shared-USM进行矩阵乘法的执行时间:" << duration << " 毫秒\n";

    free(A, q);
    free(B, q);
    free(C, q);

    return 0;
}

运行结果:

c.device_USM

#include <CL/sycl.hpp>
#include <iostream>
#include <random>
#include <chrono>

using namespace sycl;
using namespace std;

const size_t N = 1024;

void initialize_matrix(float* matrix, size_t size, queue& q) {
    float* temp_matrix = malloc_host<float>(size, q.get_context());
    random_device rd;
    mt19937 mersenne_engine(rd());
    uniform_real_distribution<float> dist(-100.0, 100.0);

    for (size_t i = 0; i < size; ++i) {
        temp_matrix[i] = dist(mersenne_engine);
    }

    q.memcpy(matrix, temp_matrix, sizeof(float) * size).wait();
    free(temp_matrix, q.get_context());
}

int main() {
    queue q(default_selector{}, property::queue::enable_profiling());

    float* A = malloc_device<float>(N * N, q);
    float* B = malloc_device<float>(N * N, q);
    float* C = malloc_device<float>(N * N, q);

    initialize_matrix(A, N * N, q);
    initialize_matrix(B, N * N, q);

    auto start = std::chrono::high_resolution_clock::now();

    q.submit([&](handler& h) {
        h.parallel_for(range<2>(N, N), [=](id<2> id) {
            size_t i = id[0];
            size_t j = id[1];
            float sum = 0.0f;
            for (size_t k = 0; k < N; ++k) {
                sum += A[i * N + k] * B[k * N + j];
            }
            C[i * N + j] = sum;
        });
    }).wait();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    float* C_host = malloc_host<float>(N * N, q.get_context());
    q.memcpy(C_host, C, sizeof(float) * N * N).wait();

    cout << "C[0][0] = " << C_host[0] << "\n";
    cout << "使用Device-Allocated USM进行矩阵乘法的执行时间:" << duration << " 毫秒\n";

    free(A, q);
    free(B, q);
    free(C, q);
    free(C_host, q.get_context());

    return 0;
}

运行结果

差异因素分析:

1. Device-Allocated USM : 时间最短。

原因分析:

    高带宽和并行性:GPU通常具有非常高的内存带宽,可以同时处理大量的操作,特别是对于像矩阵乘法这样的并行计算密集型任务。

    近距离内存访问:在设备上分配的内存可以被GPU直接访问,没有跨设备的数据传输开销。

    优化的内存访问:使用设备内存可能意味着更优化的内存访问模式,减少了内存访问延迟。

2. Host-Allocated USM : 时间最长。

原因分析:

    数据传输开销:虽然主机分配的内存可以被GPU访问,但可能涉及显式的数据传输,会增加额外的延迟。

    可能的同步问题:在某些情况下,主机和设备之间可能需要同步,这会导致额外的性能开销。

    内存访问速度:主机内存的带宽通常低于GPU内存,且访问速度可能慢于设备上直接分配的内存。

3. Shared-Allocated USM: 时间介于两者之间。

原因分析:

    平衡的内存访问:共享内存旨在平衡主机和设备的内存访问需求,提供了一种折中的性能。

    潜在的优化:可能在某些硬件上进行了优化,以提高对共享内存的访问速度。

    减少数据迁移:减少了与主机分配的USM相比的数据迁移需求,但仍可能面临一些内存访问开销。

综合来看,影响运算性能(这里选取运算时间作为指标)的因素有:

内存带宽:内存与计算单元之间数据传输的速度。

内存访问模式:缓存的使用效率,以及访问模式是否能够充分利用硬件的内存访问优化。

数据传输:数据在主机和设备之间传输的需要,包括数据传输的次数和量。

硬件优化:特定硬件架构的优化,如GPU的内存访问优化,可以大大影响性能。

同步机制:主机和设备之间同步数据的机制,可能会增加延迟。

并行计算资源:GPU拥有的并行计算资源远多于CPU,对于并行计算任务能提供更好的性能。

四、实验中的挑战与心得体会

挑战一:理解SYCL编程模型

作为oneAPI的关键部分,SYCL为我提供了一个充满挑战的学习曲线。刚开始接触SYCL时,理解其异构编程概念和抽象级别对我来说非常困难。例如,缓冲区、访问器、队列和内核的概念及其在并行计算中的协同作用,初看起来令人望而却步。为了克服这一挑战,我深入研究了SYCL官方文档和oneAPI相关教程。通过反复练习编写简单程序并逐渐增加复杂度,我逐渐掌握了SYCL的核心原理。此外,参与在线社区的讨论也大大帮助了我解决了理解过程中的疑问。

挑战二:性能优化

在oneAPI的性能优化阶段,我意识到,单纯将计算任务迁移到GPU并不意味着能自动获得最佳性能。SYCL提供的nd_range配置和内存访问模式调整为性能优化提供了更多可能性。我通过实验不同的工作组配置,优化了矩阵乘法内核的性能。通过分析内核的内存访问模式,我学会了如何调整内存访问策略,以减少内存延迟和提高带宽利用率。

挑战三:使用不同的USM类型

在oneAPI的框架下,我被要求使用不同的统一共享内存(USM)类型执行矩阵乘法,并比较它们的性能。每种USM类型(如显式USM、隐式USM)都有其独特的优势和局限性。理解这些类型的高效使用方式是一个挑战。我通过编写多个版本的矩阵乘法程序,每个版本采用不同的USM策略,深入探究了它们的性能差异。通过分析和比较,我不仅理解了不同USM类型对性能的影响,还学会了根据特定的计算任务选择最合适的USM类型。

心得体会

通过这个实验,我不仅学会了使用SYCL进行高效的异构编程,还深刻理解了性能优化在实际应用中的重要性。我发现,即使是看似简单的矩阵乘法,也存在许多优化的空间。这个实验过程让我意识到,深入理解硬件架构和编程模型对于进行有效的并行编程至关重要。

最重要的是,这个实验提高了我的问题分析和解决能力。面对挑战时,我学会了不断尝试不同解决方案,直至找到最佳方法。我不仅提升了自己的编程技能,也增强了自我学习和适应新技术的能力。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值