OneAPI介绍
一种通用的可以适配Intel的多种异构硬件的并行编程库,个人理解可以用于HPC场景以及深度学习的卷积优化、向量传播等场景,并降低适配不同硬件的编译适配成本。
OneAPI提供了云平台DevCloud可以免费注册后使用,即使笔记本不是Intel的也可以使用Intel OneAPI配套硬件的体验,还无需自行安装toolkit。
jupyter.oneapi.devcloud.intel.com/ 这是我在本次实验中使用的云平台链接,本次实验主要基于Jupyter Lab进行,使用简单,用Jupyter Notebook构建了可视化界面,即使不会使用命令行进行编译也可以直接复制官方教程中的脚本进行编译,免去了CPP的编译痛苦。
问题描述
计算矩阵乘法,编写⼀个基于oneAPI的C++/SYCL程序来执行矩阵乘法操作。需要考虑大尺寸矩阵的乘法操作以及不同线程之间的数据依赖关系。通常在实现矩阵乘法时,可以使用块矩阵乘法以及共享内存来提高计算效率。
实验过程
众所周知,朴素的矩阵乘法运算的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3),当然矩阵乘法有一些别的优化方式,例如Strassen矩阵乘法,基于分治进行了十次小矩阵加法和七次小矩阵乘法,时间复杂度优于 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3)。
但是采用递归算法实现的矩阵乘法往往有依赖关系,子问题数量也呈指数级增加,而我们的机器的线程数(或者说核数)是有限的,所以不能无限制的增加,将子问题派发给不同线程。
因此,在解决这个问题过程中,依然采用了 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3)复杂度的GEMM矩阵乘法算法,但是利用并行计算,我们可以缩短计算的时间,也就是达成一个加速比。
对矩阵进行分块计算,在这里只考虑了矩阵维度能对齐的情况,如果不能对齐tile(例如大小不是512,而是511,513,那么需要进行zero-padding)。实际上的分块(tile)大小和机器的缓存大小有关,因为需要考虑矩阵乘法的访问序列是一行与一列共同访问的,所以一旦tile太大,访问一列时就会频繁发生cache miss,访问A矩阵和B矩阵的行和列时也会频繁触发switch,影响性能。
代码
24行开始定义并行任务队列q,采用nd-parallel思想,nd_item的维数是2。 31~33行使用的是定义的局部内存,防止每一次计算的临时结果都被写入原来的矩阵C中,而是将结果都在共享内存中写好了之后才写入结果矩阵。
然后使用朴素矩阵乘法实现了normal的矩阵计算,用于验算以及测试并行加速比。
cpp
#include <chrono>
#include <iostream>
#include <sycl/sycl.hpp>
#define random_float() (rand() / double(RAND_MAX))
using namespace sycl;
const int tileX = 8;
const int tileY = 8;
double matrix_multiply_parallel(float *A, float *B, float *C,
int M, int N, int K,
int BLOCK, sycl::queue &q) {
auto grid_rows = M / tileY;
auto grid_cols = N / tileX;
auto local_ndrange = range<2>(BLOCK, BLOCK);
auto global_ndrange = range<2>(grid_rows, grid_cols);
double duration = 0.0f;
auto e = q.submit([&](sycl::handler &h) {
h.parallel_for(
sycl::nd_range<2>(global_ndrange, local_ndrange), [=](sycl::nd_item<2> index) {
int row = tileY * index.get_global_id(0);
int col = tileX * index.get_global_id(1);
float sum[tileY][tileX] = {0.0f};
float subA[tileY] = {0.0f};
float subB[tileX] = {0.0f};
for (int k = 0; k < N; k++) {
for(int m = 0; m < tileY; m++) {
subA[m] = A[(row + m) * N + k];
}
for(int p = 0; p < tileX; p++) {
subB[p] = B[k * N + p + col];
}
for (int m = 0; m < tileY; m++) {
for (int p = 0; p < tileX; p++) {
sum[m][p] += subA[m] * subB[p];
}
}
}
for (int m = 0; m < tileY; m++) {
for (int p = 0; p < tileX; p++) {
C[(row + m) * N + col + p] = sum[m][p];
}
}
});
});
e.wait();
duration += (e.get_profiling_info<info::event_profiling::command_end>() -
e.get_profiling_info<info::event_profiling::command_start>()) /1000.0f/1000.0f;
return(duration);
}
double matrix_multiply_normal(float *cA, float *cB, float *cC, int M, int N, int K) {
double duration = 0.0;
std::chrono::high_resolution_clock::time_point s, e;
s = std::chrono::high_resolution_clock::now();
for(int i = 0; i < M; i++) {
for(int j = 0; j < N; j++) {
float sum = 0.0f;
for(int k = 0; k < K; k++) {
sum += cA[i * K + k] * cB[k * N + j];
}
cC[i * N + j] = sum;
}
}
e = std::chrono::high_resolution_clock::now();
duration = std::chrono::duration<float, std::milli>(e - s).count();
return(duration);
}
int verify(float *normal_res, float *parallel_res, int length){
int err = 0;
for(int i = 0; i < length; i++) {
if( fabs(normal_res[i] - parallel_res[i]) > 1e-3) {
err++;
printf("\n%lf, %lf, %d %lf", normal_res[i], parallel_res[i], i, fabs(normal_res[i]-parallel_res[i]));
}
}
return(err);
}
int gemm(const int M,
const int N,
const int K,
const int block_size,
const int iterations,
sycl::queue &q) {
std::cout << "Problem size: c(" << M << "," << N << ") ="
<< " a(" << M << "," << K << ") *"
<< " b(" << K << "," << N << ")\n";
auto A = malloc_shared<float>(M * K, q);
auto B = malloc_shared<float>(K * N, q);
auto C = malloc_shared<float>(M * N, q);
auto C_host = malloc_host<float>(M * N, q);
for(int i=0; i < M * K; i++) {
A[i] = random_float();
}
for(int i=0; i < K * N; i++) {
B[i] = random_float();
}
for(int i=0; i < M * N; i++) {
C[i] = 0.0f;
C_host[i] = 0.0f;
}
double flopsPerMatrixMul
= 2.0 * static_cast<double>(M) * static_cast<double>(N) * static_cast<double>(K);
double duration_parallel = 0.0f;
double duration_normal = 0.0f;
int warmup = 10;
for (int run = 0; run < iterations + warmup; run++) {
float duration = matrix_multiply_parallel(A, B, C, M, N, K, block_size, q);
if(run >= warmup) duration_parallel += duration;
}
duration_parallel = duration_parallel / iterations;
warmup = 2;
for(int run = 0; run < iterations/2 + warmup; run++) {
float duration = matrix_multiply_normal(A, B, C_host, M, N, K);
if(run >= warmup) duration_normal += duration;
}
duration_normal = duration_normal / iterations/2;
int errCode = 0;
if(errCode > 0) printf("\nThere are %d errors\n", errCode);
printf("\nGEMM size M = %d, N = %d, K = %d", M, N, K);
printf("\nWork-Group size = %d * %d, tile_X = %d, tile_Y = %d", block_size, block_size, tileX, tileY);
printf("\nPerformance Flops = %lf, \n"
"Parallel Computation Time = %lf (ms); \n"
"Normal Computaiton Time = %lf (ms); \n"
"Speedup = %lf\n",
flopsPerMatrixMul, duration_parallel, duration_normal, duration_normal/duration_parallel);
free(A, q);
free(B, q);
free(C, q);
free(C_host, q);
return(errCode);
}
int main() {
auto propList = sycl::property_list {sycl::property::queue::enable_profiling()};
queue my_queue(default_selector_v , propList);
int errCode = gemm(512, 512, 512,
4,
10,
my_queue);
return(errCode);
}
运行结果
运行结果如图。在使用Sycl dpc++编译以后,shell里直接运行,使用的DevCloud没有GPU core,所以并行计算版本和普通版本都是CPU core的计算速度。
总结
这次实验我进行了矩阵乘法的实验,了解到了除了降低算法复杂度以外,使用并行分块的方式降低计算时间的方式,也体验了Intel OneAPI的使用全流程,在DevCloud上把程序写出来并运行起来,获得了预期的结果。这次实验拓宽了我对并行计算的认识,也让我有机会接触到Intel OneAPI这一套全新的工具包和并行编程框架。