并行程序设计大作业——稀疏神经网络推理

本文详述了一次并行程序设计大作业,任务是优化稀疏神经网络的推理过程,特别是矩阵乘法和bias+relu操作。通过对cublasSgemm函数的参数配置和矩阵存储方式的优化,以及对bias和relu操作的并行化处理,成功将计算时间从16.5s减少到6.7s,显著提升了效率。
摘要由CSDN通过智能技术生成

作业背景与要求

本次作业是 2019 的一个并行竞赛题,也不记得是那个竞赛了,反正题目大致要求下述所示:将一个 60000 * 1024 的矩阵 A 当做左乘矩阵,120 个 1024 * 1024 的稀疏矩阵 B [i], 0 ≤ i ≤ 119 0\le i\le 119 0i119 作为右乘矩阵。A 与 B[i] 相乘得到矩阵 C,C 的维度为 60000 * 1024,对 C 矩阵加 偏置项 bias,并用激活函数 relu 激活,激活后的 C 矩阵作为下一次相乘的矩阵 A 与 B[i+1] 相乘。即神经网络前向传播一次。运算时间通过 gettimeofday(&time,NULL); 获取。

原始代码是通过传统的矩阵乘法,利用三层循环,每个内部循环计算所求矩阵的一个元素,时间复杂度为 O(n3),计算一次矩阵相乘的时间需要 10 分钟左右。120个计算下来不得了。所以本次作业的目的就是尽量优化代码,缩短矩阵相乘和 bias+relu 的时间。老师给上一届优化最快的一个小组奖励了樱花键盘,我们这一届也不知道会不会有,反正我不是最快的。

优化思路来源

本学期一共有 5 次家庭作业,4 次都是关于优化矩阵乘法的,我就是筛选出性能最好的一种算法应用于大作业。

  • 第一次作业是两个方阵相乘,方阵大小在 100 ~ 2000 之间,并100 为步长共 20 组数据测试。改变循环顺序和使用 openmp 还有 openblas。
    在这里插入图片描述
    上图为改变循环顺序的结果。
    在这里插入图片描述
    上图为改变循环顺序后加openmp的结果,有两个顺序加openmp语句后输出结果不正确,就没有画出来。

在这里插入图片描述
上图为 openblas 的结果。

void gemm_OpenBlas(double *A, double *B, double *C, int m, int k, int n)
{
   
    enum CBLAS_ORDER order = CblasRowMajor;
    enum CBLAS_TRANSPOSE transposeA = CblasNoTrans;
    enum CBLAS_TRANSPOSE transposeB = CblasNoTrans;
    double alpha = 1;
    double beta = 1;
    cblas_dgemm(order,transposeA,transposeB,m,n,k,alpha,A,k,B,n,beta,C,n);
}
gemm_OpenBlas(A, B, C_yours, m, k, n);

很快啊! 2000 × 2000 2000\times2000 2000×2000 的两个矩阵相乘只需要 0.25s。

  • 作业三是使用 CUDA 函数继续作业一。
    在这里插入图片描述
//cublasSgemm
cublasHandle_t s;
cublasCreate(&s);
cublasSgemm('N', 'N', m, n, k, 1.0f, d_A, m, d_B, k, 0, d_C, m);
cublasDestroy(s);
//cublasSgemm_v2
cublasHandle_t s;
cublasCreate_v2(&s);
cublasSgemm_v2(s,CUBLAS_OP_T,CUBLAS_OP_T,m,n,k,&al,d_A,k,d_B,n,&ve,d_C,n);
cublasDestroy_v2(s);
//cblas_sgemm
void gemm_OpenBlas(float *A, float *B, float *C, int m, int k, int n) {
   
    enum CBLAS_ORDER order = CblasRowMajor;
    enum CBLAS_TRANSPOSE transposeA = CblasNoTrans;
    enum CBLAS_TRANSPOSE transposeB = CblasNoTrans;
    float alpha = 1;
    float beta = 1;
    cblas_sgemm(order,transposeA,transposeB,m,n,k,alpha,A,k,B,n,beta,C,n);
}
//cuda_shared
typedef struct {
   
    int width;
    int height;
    int stride;
    float *elements;
} Matrix;

__device__ float GetElement(const Matrix A, int row, int col) {
   
    return A.elements[row * A.stride + col];
}

__device__ void SetElement(Matrix A, int row, int col,
                           float value) {
   
    A.elements[row * A.stride + col] = value;
}

__device__ Matrix GetSubMatrix(Matrix A, int row, int col) {
   
    Matrix Asub;
    Asub.width
            = BLOCK_SIZE;
    Asub.height
            = BLOCK_SIZE;
    Asub.stride
            = A.stride;
    Asub.elements = &A.elements[A.stride * BLOCK_SIZE * row
                                + BLOCK_SIZE * col];
    return Asub;
}

#define ifin(r, c, rb, cb, mat_A) \
((c + cb*BLOCK_SIZE < mat_A.width) && (r+rb*BLOCK_SIZE < mat_A.height))

__global__ void MatMulKernel_SharedMemory(Matrix A, Matrix B, Matrix C) {
   
    int blockRow = blockIdx.y;
    int blockCol = blockIdx.x;
    int row = threadIdx.y;
    int col = threadIdx.x;
    __shared__ float As[BLOCK_SIZE][BLOCK_SIZE];
    __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE];

    Matrix subC = GetSubMatrix(C, blockRow, blockCol);
    int tot = A.width / BLOCK_SIZE + (A.width % BLOCK_SIZE != 0);
    float CValue = 0;
    for (int i = 0; i < tot; ++i) {
   
        Matrix Asub = GetSubMatrix(A, blockRow, i);

        Matrix Bsub = GetSubMatrix(B, i, blockCol);
        //if (i * BLOCK_SIZE + col < A.width)
        if(ifin(row,col,blockRow,i,A)) {
   
            As[row][col] = GetElement(Asub, row, col);
        } else As[row][col] = 0;

        if(ifin(row,col,i,blockCol,B)) {
   
            Bs[row][col] = GetElement(Bsub, row, col);
        }else Bs[row][col] = 0;
        __syncthreads();

        for (int e = 0; e < BLOCK_SIZE; ++e)
            CValue += As[row][e] * Bs[e][col];

        __syncthreads();
        if (ifin(row, col, blockRow, blockCol, C))
            SetElement(subC, row, col, CValue);
    }

}

void gemm_cuda_shared(float *A, float *B, float *C, int m, int k, int n,double *time_value) {
   
    Matrix d_A;
    d_A.width = d_A.stride = k;
    d_A.height = m;
    size_t size = k * m * sizeof(float);
    cudaMalloc(&d_A.elements, size);
    cudaMemcpy(d_A.elements, A, size, cudaMemcpyHostToDevice);
    Matrix d_B;
    d_B.width = d_B.stride = n;
    d_B.height = k;
    size = n * k * sizeof(float);
    cudaMalloc(&d_B.elements, size);
    cudaMemcpy(d_B.elements, B, size, cudaMemcpyHostToDevice);

    Matrix d_C;
    d_C.width = d_C.stride = n;
    d_C.height = m;
    size = n * m * sizeof(float);
    cudaMalloc(&d_C.elements, size);

    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
    dim3 dimGrid((n / dimBlock.x) + (n % dimBlock.x != 0),  (m / dimBlock.y) + (m % dimBlock.y != 0));
                 
    timeval t1,t2;
    *time_value = 0;
    cudaDeviceSynchronize();

    gettimeofday(&t1, nullptr);
    MatMulKernel_SharedMemory<<<dimGrid, dimBlock>>>(d_A, d_B, d_C);
    cudaDeviceSynchronize();
    gettimeofday(&t2, nullptr);
    *time_value+=(t2.tv_sec - t1.tv_sec) + (t2.tv_usec - t1.tv_usec) / 1000000.0;
    
    cudaMemcpy(C, d_C.elements, size, cudaMemcpyDeviceToHost);

    cudaFree(d_A.elements);
    cudaFree(d_B.elements);
    cudaFree(d_C.elements);
}
//cuda_global
__global__ void MatrixMulKernel(float *A, float *B, float *C, int m, int k, int n) {
   
// Each thread computes one element of C
// by accumulating results into Cvalue
    float Cvalue = 0;
    int row = blockIdx.y * blockDim.y + threadIdx.y;
    int col = blockIdx.x * blockDim.x + threadIdx.x;
    if (row < m && col < n) {
   
        for (int e = 0; e < k; ++e)
            Cvalue += A[row * k + e]
                      * B[e * n + col];
        C[row * n + col] = Cvalue;
    }
}
void gemm_cuda(float *A, float *B, float *C, int m, int k, int n,double *time_value) {
   
    float *d_A, *d_B, *d_C;
    size_t size = m * k * sizeof(float);
    cudaMalloc(&d_A, size);
    cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice);

    size = k * n * sizeof(float);
    cudaMalloc(&d_B, size);
    cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);


    size = m * n * sizeof(float);
    cudaMalloc(&d_C, size);

    dim3 dimBlock(BLOCK_SIZE, BLOCK_SIZE);
    dim3 dimGrid((n / dimBlock.x) + (n % dimBlock.x != 0), (m / dimBlock.y) + (m % dimBlock.y != 0));

    timeval t1,t2;
    *time_value = 0;
    cudaDeviceSynchronize();
    
    gettimeofday(&t1, nullptr);
    MatrixMulKernel<<<dimGrid, dimBlock>>>(d_A, d_B, d_C, m, k, n);
    cudaDeviceSynchronize();
    gettimeofday(&t2, nullptr);
    *time_value += (t2.tv_sec - t1.tv_sec) + (t2.tv_usec - t1.tv_usec) / 1000000.0;
    
    cudaMemcpy(C, d_C, size, cudaMemcpyDeviceToHost);

    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);

}

最快的是 cublas_sgemm 2000 × 2000 2000 \times 2000 2000×2000 的矩阵只用了大约 0.0075s。其次就是 cublas_sgemm_v2 耗时 0.0175s 左右。

  • 作业四,是稀疏方阵乘瘦矩阵,就是将方阵转化为 csr 的存储格式。然后再按照csr的方式将矩阵相乘。虽然使用csr方式进行计算耗费时间较少,但是需要的前期准备太多,而且在多个矩阵相乘过程中需要多次转换矩阵形式与分配内存,每次参与运算矩阵的 csr 格式表示都不相同,csr 的三个数组长度也不同,所以每次都需重新分配内存、释放内存,会带来较多的时间消耗。

    虽然利用 csr 格式的稀疏矩阵相乘有以上的缺点,但还是对此方法进行了实验,下面是在网上拼凑的代码,原理不是很明白,只是粘取了函数部分,然后在调用函数部分做了改动。将矩阵转化为 csr 存储格式后利用cusparseSpMM:
 #include <stdio.h>
#include <stdlib.h
1. 设计目的、意义(功能描述) 蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是二十世纪四十年代中期由于科学技术的发展和电子计算机的发明,而被提出的一种以概率统计理论为指导的一类非常重要的数值计算方法。本次大作业主要是对蒙特·卡罗方法进行并行处理,通过OpenMP、MPI、.NET、Java、Win32API等一系列并行技术和并行机制对该算法进行并行处理,从而也进一步熟悉了蒙特·卡罗方法的串行算法和并行算法,实现了用蒙特·卡罗方法计算出半径为1单位的球体的体积,体会到了并行技术在实际生活中的应用。 2. 方案分析(解决方案) 蒙特·卡罗方法(Monte Carlo method)是指使用随机数(或更常见的伪随机数)来解决很多计算问题的方法。球的体积可以估算为:位于点模型内随机点个数与全体随机点个数的比值乘以包围盒的体积算的。 3. 设计分析 3.1 串行算法设计 假定球体用B表示,半径r=1单位,B1是包含B的参考立方体(在本例中是边长为2的正方体),在B1中产生N个均匀分布的伪随机点。对每个随机点检测其是否在B内,假设位于B内的随机点个数为N(in)(<=N),应用蒙特卡洛算法,则B的体积为 V=V1(N(in)/N) 其中V1是B1的体积。如果产生足够多的随机点,理论上可以获得任意逼近精度。 算法描述如下: BEGIN N=_MAX; FOR I=0;I<_MAX;I++ X=RANDOM(); Y=RANDOM(); Z=RANDOM(); IF (X*X+Y*Y+Z*Z)<=1 COUNT++; END IF; END FOR; BULK=V1*(COUNT/_MAX); END; 本算法主要是在参考立方体的选取上和定义的_MAX的值对结果影响较大,所以应该选择合适的数。 3.2 并行算法设计 对FOR循环进行划分使用两个处理器完成计算。例如对一个长为n的序列,首先划分得到两个长为n/2的序列,将其交给两个处理器分别处理;而后进一步划分得到四个长为n/4的序列,再分别交给四个处理器处理;如此递归下去最终得到结果。当然这是理想的划分情况,如果划分步骤不能达到平均分配的目的,那么结果的效率会相对较差。 伪代码如下: BEGIN N=_MAX; FOR1 I=0;I<_MAX/2;I++ X1=RANDOM(); Y1=RANDOM(); Z1=RANDOM(); IF (X1*X1+Y1*Y1+Z1*Z1)<=1 COUNT1++; END IF; END FOR1; FOR2 I=_MAX/2+1;I<_MAX;I++ X2=RANDOM(); Y2=RANDOM(); Z2=RANDOM(); IF (X2*X2+Y2*Y2+Z2*Z2)<=1 COUNT2++; END IF; END FOR2; BULK=V1*((COUNT1+ COUNT2)/_MAX); END; 3.3 理论加速比分析 实验中大量数据所产生的加速比比小量数据所产生的加速比要体现得更明显,并且数据生成的并行加速比随着处理器核的增加而增加。设处理器个数为p,数据量为n,由于正常情况下该快速排序算法的复杂度为O(nlogn),并行处理的时间复杂度为O(klogk),其中k=n/p,所以并行算法的时间复杂度为O((n/p)log(n/p)),理论加速比为nlogn/((n/p)log(n/p))=p+logp. 4. 功能模块实现与最终结果分析 4.1 基于OpenMP的并行算法实现 4.1.1 主要功能模块与实现方法 利用了OpenMP里面的#omp parallel sections将对两个for循环用两个线程并行化执行,以多线程方式并行运行程序,并行的算法步骤如下: (1)初始化_max = 10000000; (2)创建两个线程; (3)由OpenMP编译指导语句控制产生并行执行代码区段; (4)将数据存放到tianqing_count; (5)各线程调用算法得出结果; 并行算法的部分代码如下: #pragma omp parallel for private(tianqing_x,tianqing_y,tianqing_z) reduction(+:tianqing_count2) for (tianqing_i = 0; tianqing_i<tianqing_max; tianqing_i++) { tianqing_x = rand(); tianqing_x = tianqing_x / 32767; tianqing_y = rand(); tianqing_y = tianqing_y / 32767; tianqing_z = rand(); tianqing_z = tianqing_z / 32767; if ((tianqing_x*tianqing_x + tianqing_y*tianqing_y + tianqing_z*tianqing_z) work1.pSumto(b, 0, MAXN - 1)); Thread newthread1 = new Thread(thread1); 创建Work类的对象work2; ThreadStart thread2 = new ThreadStart(() => work2.pSumto(c, 0, MAXN - 1)); Thread newthread2 = new Thread(thread2); stopwatch.Start(); 启动线程1和线程2; 等待进程结束; stopwatch.Stop(); 得到结果; 4.5.2 实验加速比分析 实验中创建了两个线程,通过多次测试,得出实验结果:由上面的理论加速比分析可知,当线程数为2时,理论加速比为2+log2=3.但由于实际操作中硬件设备以及内存分配的影响,实验加速比达不到理论值3.实验加速比在2.6~2.7左右。 4.6 并行计算技术在实际系统中的应用 4.6.1 主要功能模块与实现方法 该飞机订票系统主要实现了对机票的一些基本信息进行存储和管理的功能。在系统中实现了对机票信息的增删改查,考虑到查询的方便性,对机票按照航班号进行排序,而此排序方法用并行快速排序运用进来。利用OpenMP的并行技术,对机票信息按顺序排列好,并分析了实验过程中的加速比。 4.6.2 实验加速比分析 实验中创建了两个线程,通过多次测试,得出实验结果:当数据量比较大时,加速比理论在1.9左右。数据量较大时体现出来的加速比更准确。由上面的理论加速比分析可知,当线程数为2时,理论加速比为2+log2=3.但由于实际操作中硬件设备以及内存分配的影响,实验加速比达不到理论值3.实验加速比在2.2~2.4左右。 5. 设计体会 虽然没有按时完成作业,但这份报告花了我好几天的时间,从开始的搭建并行计算平台到最后的程序运行成功可以说是对我的一个锻炼。每一次的遇到问题与每一次的解决问题都是一个成长。每一次遇到问题和解决问题都是一种锻炼,一种尝试,从我们上并行计算课我懂得了很多电脑硬件和软件的知识,这些可能对于我们这个专业以后都是没有机会接触的,所以我觉得选择了并行计算与多核多线程技术这门课是非常正确的。对OpenMP、MPI、WIN32API、Java、.NET的并行技术有了一定的了解。在搭建MPI并行程序这块,学习的知识尤为增加,这些都是在不断的摸索、学习中学会的。 这次的大作业虽然是对以前实验的整合,但它加深了我对并行计算的印象,也使我对并行计算知识的理解更加深刻,也使我认识到了自己很多不足之处。学习并行计算的历程不会因为完成本次大作业而停止,我们是为了用知识武装大脑而学习,通过学习充实自己的生活,要努力学习,争取以后能够完成规模更大的程序。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的博仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值