0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何对spmv算法进行优化

perfxlab 来源:澎峰科技PerfXLab 2023-05-25 09:05 次阅读

主要是介绍如何对spmv算法进行优化。Spmv,即稀疏化的矩阵向量乘操作,关于稠密的矩阵向量乘操作,已经在上一篇文章中介绍过了。关于稀疏kernel的优化,是CUDA优化中最难的一部分,其难度在于稀疏特性千差万别,需要针对不同的应用、不同的数据选择不同的数据存储格式,然后再根据不同的数据特点进行特定的并行算法设计。而现实生活中,尤其是科学计算里面,基本上都是稀疏问题。在深度学习领域中,也一直有针对稀疏模型的研究,主要是针对推理方向,将模型进行剪枝之后,直接减少了计算量来达到对模型的加速目的。但实际上,为了保证模型的精度,稀疏度有限,且稀疏问题很难充分地利用硬件性能,导致了这一条路线其实并不好走。唠唠叨叨说了挺多,总之,稀疏kernel的优化是一个非常难的话题。本文会详细地介绍一下spmv。

一、前言

在说spmv之前,说一下稀疏格式。当矩阵中的绝大多数元素都是0时,需要一些特殊的格式来存储非零元素。这些格式也就是稀疏格式,常用的稀疏格式有:COO、CSR、DIA、ELL、HYB。深度学习领域还有blocked CSR、blocked ELL等。具体的稀疏格式总结见以下链接:

稀疏矩阵存储格式总结+存储效率对比:COO,CSR,DIA,ELL,HYB - Bin的专栏 - 博客园www.cnblogs.com/xbinworld/p/4273506.html64e19abc-fa8c-11ed-90ce-dac502259ad0.png

在本文中,使用CSR格式存储稀疏矩阵,后续所说的一系列优化也是针对CSR格式而言。说完了稀疏格式,现在再来说一下spmv,即稀疏矩阵向量乘。稠密的矩阵向量乘,即gemv已经在之前说过了。具体的操作即给定稀疏矩阵A和向量x,需要计算两者的乘积y。示意图如下。

65013de0-fa8c-11ed-90ce-dac502259ad0.jpg

spmv介绍

二、并行算法设计

并行算法设计,主要是block和thread的设计,在这里主要参考了cusp的实现。有一个很重要的考虑是workload的分配。我们需要使用多少个线程来负责A矩阵中一行的计算?需要说明的是,在不同的数据特性下,需要采用不同的取值。如果这一行的元素非常多,那使用一个warp或者一个block,如果这一行只有一个元素,只需要一次乘加指令,那显然只能使用一个线程,毕竟用两个线程处理一个元素,怎么都不像正常人能干出的事。因而,我们假定一个参数,即THREADS_PER_VECTOR,来代表这个值。每THREADS_PER_VECTOR个线程为一组,他们需要负责A矩阵中一行元素的计算。

说完了这个核心思路,接下来看看每个线程需要干的工作。每个线程都要单独地对A矩阵的offset和index等进行读取,然后计算当前行的结果。如果每一行的元素特别少,比如这一行元素有4个,THREADS_PER_VECTOR就设为4,有8个元素就设为8,平均多少个元素,THREADS_PER_VECTOR就设为几,但上限是32。元素比32多的话,就多进行几个迭代即可。总之最多使用一个warp来处理一行元素。

至于如何得到一行元素有多少,row_offset数组长度除以y数组长度即可得。有必要在这里再提一句的是,这些参数的选择,以及对于不均衡的A矩阵元素如何处理,这些都是比较棘手的问题。有一大堆的论文在谈负载均衡自动调参这两个事情,大家可以搜一下相关的论文瞅瞅。

讲完了思路,下面说一下具体的代码,如下:

template 
__device__ __forceinline__ float warpReduceSum(float sum) {
    if (WarpSize >= 32)sum += __shfl_down_sync(0xffffffff, sum, 16); // 0-16, 1-17, 2-18, etc.
    if (WarpSize >= 16)sum += __shfl_down_sync(0xffffffff, sum, 8);// 0-8, 1-9, 2-10, etc.
    if (WarpSize >= 8)sum += __shfl_down_sync(0xffffffff, sum, 4);// 0-4, 1-5, 2-6, etc.
    if (WarpSize >= 4)sum += __shfl_down_sync(0xffffffff, sum, 2);// 0-2, 1-3, 4-6, 5-7, etc.
    if (WarpSize >= 2)sum += __shfl_down_sync(0xffffffff, sum, 1);// 0-1, 2-3, 4-5, etc.
    return sum;
}

template 
__global__ void My_spmv_csr_kernel(const IndexType row_num,
                       const IndexType * A_row_offset,
                       const IndexType * A_col_index,
                       const ValueType * A_value,
                       const ValueType * x,
                       ValueType * y)
{
    const IndexType THREADS_PER_BLOCK = VECTORS_PER_BLOCK * THREADS_PER_VECTOR;
    const IndexType thread_id   = THREADS_PER_BLOCK * blockIdx.x + threadIdx.x;    // global thread index
    const IndexType thread_lane = threadIdx.x & (THREADS_PER_VECTOR - 1);          // thread index within the vector
    const IndexType row_id   = thread_id   /  THREADS_PER_VECTOR;               // global vector index

    if(row_id < row_num){
        const IndexType row_start = A_row_offset[row_id];                  //same as: row_start = Ap[row];
        const IndexType row_end   = A_row_offset[row_id+1];

        // initialize local sum
        ValueType sum = 0;

        // accumulate local sums
        for(IndexType jj = row_start + thread_lane; jj < row_end; jj += THREADS_PER_VECTOR)
            sum += A_value[jj] * x[ A_col_index[jj] ];

        sum = warpReduceSum(sum);
        if (thread_lane == 0){
            y[row_id] = sum;
        }   
    }
}

首先说一下传入的几个模板参数,IndexType代表索引类型,一般用int,如果矩阵十分巨大,可以考虑long long,ValueType代表数值存储的格式,科学计算一般都双精度,深度学习一般用单精度,或者fp16。推理甚至有一些int8这样的需求。THREADS_PER_VECTOR这个参数已经在前面说过了,VECTORS_PER_BLOCK则是代表一个block中有多少vector。我们尽可能地保证一个block有256个线程,所以VECTORS_PER_BLOCK = 256 / THREADS_PER_VECTOR。看完了模板参数,再看函数输入参数,分别是A矩阵的行数,A矩阵的CSR表示,有3个数组,然后是向量x和向量y。

接下来到了具体的kernel逻辑,首先计算了四个参数,需要注意的是thread_lane和row_id参数,thread_lane代表当前元素是当前组里面的第几个元素,row_id代表当前元素负责A矩阵中第几行的计算。接下来的逻辑也比较明了,先计算当前行对应的索引值,即row_start和row_end。定义sum变量来存储该行的计算结果,而后进行多次迭代,将每个线程对应的sum取出来,最后将sum元素进行warp的reduce_sum操作。最后将元素写到y向量中。

三、优化技巧

在上一节中已经把代码说完了,接下来盘点一下具体的优化技巧,以及优化中需要考虑的方方面面。

1、合理的block和thread调整。我一直觉得这个点是优化中最重要的一点。核心就是THREADS_PER_VECTOR需要根据实际的数据进行调整。这一点主要是考虑到如果使用更多的线程处理的话,只有THREADS_PER_VECTOR个线程在工作,其他的线程都被浪费了。

2、Shuffle指令减少访存的latency。在不使用shuffle指令的话,只能通过shared memory完成最后的求和操作。从shared memory中取数比寄存器之间直接访问要花费更多的latency。因而要尽可能地使用shuffle指令。

3、对于global memory的合并访存。对于稀疏问题,由于CSR格式中的col数组和val数组不能保证地址对齐,因而针对global memory的合并访存其实是有一定的困难。我们可以仔细地来进行分析。当A矩阵行数比较多的情况下,spmv主要的访存有3部分,分别是A_value,A_col_index和x。其中,对于A_value和A_col_index的访存是连续的,但是由于地址不能保证对齐,所以访存效率大概率不会太高。而对于x的访存本身就是不连续的,因而cache命中率会显然易见地低。如何解决这些问题呢?对于A_value和A_col_index的访存问题,尚可以尝试对其进行数据填充,强制其地址对齐。而对于x的非连续访存问题,如何提高访存效率,这个问题就非常困难了。

4、关于向量化指令的使用。之前在进行gemm和gemv优化中大量地使用了float4这样的向量化访存结构。如何将向量化带到spmv中,这也是一个非常困难的问题。最大的根源是因为每一行的元素不确定,并且本身A中每行的元素就比较少,根本没有那么多数据去喂到LDS128指令上。

5、关于负载均衡的思考。CUDA上的负载均衡问题可以从两个角度考虑,一个是block之间的负载均衡,另一个是block/warp内,不同线程之间的负载均衡。关于spmv的负载均衡问题,可以参考一下Speculative Segmented Sum for Sparse Matrix-Vector Multiplication on Heterogeneous Processors。

四、实验与总结

最后,我们来说一下实验环节。实验主要用来说明两个问题,第一个是THREADS_PER_VECTOR参数对性能的影响,第二个是与cusparse的对比,用以观察不同数据下的性能表现。

实验一,从佛罗里达矩阵库里面选了一个稀疏矩阵,shyy41。平均一行有4.2个元素。我们在不同的参数下进行了实验,其结果如下:

THREADS_PER_VECTOR spmv kernel耗时(ns)
2 4093
4 3969
8 4066
16 4368
32 4976

这个结果和预期的差不多,因为平均一行元素个数是4.2,所以THREADS_PER_VECTOR参数取4或8会有更好的性能表现。

实验二,从佛罗里达矩阵库里面随机选取了一些矩阵,其稀疏特性如下,矩阵旁边有x-y-z标识。x和y代表矩阵的行数和列数,z代表矩阵中的非零元个数。

6528dc88-fa8c-11ed-90ce-dac502259ad0.jpg

稀疏矩阵

性能表现如下,

654964da-fa8c-11ed-90ce-dac502259ad0.jpg

与cusparse的性能对比

结论:

1、先单独看cusparse的表现,库里面会调用两个kernel,分别是binary_seach和load_balance。这个名称简写了。总之,就是cusparse不管来的数据是啥,都会进行负载均衡,在数据量比较多的时候,额外的开销比较少,能够取到足够的效益。

2、如果是结构化的网格,即元素聚集在对角线附近,且每行的非零元都差不了太多的时候,我写的spmv会比cusparse快一些。如果每行的非零元方差特别大,cusparse中的负载均衡工作就发挥了威力,在web网络这种矩阵上能够比我的spmv快2-3倍。总之,在sparse问题中,负载均衡非常重要,我会在下一篇博文中说明如何在spmm中进行负载均衡。

总之,我们实现了spmv kernel,并对主要的优化技巧进行了解析和说明,然后大概地说了一下在spmv上需要注意的问题。通过实验评估了不同参数对性能的影响以及在不同的稀疏矩阵下同cusparse进行了比较,在部分矩阵上性能能够超越cusparse。但由于没有考虑负载均衡,在非均匀网格上,与cusparse有一定的差距。

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据
    +关注

    关注

    8

    文章

    6511

    浏览量

    87600
  • 存储
    +关注

    关注

    12

    文章

    3856

    浏览量

    84660
  • 模型
    +关注

    关注

    1

    文章

    2704

    浏览量

    47685
  • 澎峰科技
    +关注

    关注

    0

    文章

    35

    浏览量

    3070

原文标题:深入浅出GPU优化系列:spmv优化

文章出处:【微信号:perfxlab,微信公众号:perfxlab】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何对MD5加密算法优化

    有人针对程序安全启动过程,进行MD5算法优化嘛。目前采用标准算法,时间稍长,如果有人做过优化的话,可以分享一下,谢谢。
    发表于 02-18 08:20

    源码供应基于DAVINCI DSP的视频压缩算法算法优化算法裁减

    ,可以大大加快产品的研发进度。产品特性:基于TI 最新 C64X架构,充分利用CPU最新指令集进行优化算法底层函数 DCT,量化,预测,运动估计,运动补偿,插值等底层函数完全汇编优化
    发表于 11-13 15:09

    《MATLAB优化算法案例分析与应用》

    《MATLAB优化算法案例分析与应用》清华大学出版社《MATLAB优化算法案例分析与应用》这本书,给大家推荐一下这本书清华大学出版社《MATLAB
    发表于 10-10 12:34

    SMO的序列最小优化算法算法推导

    11 SVM - SMO - 序列最小优化算法
    发表于 05-21 06:44

    基于遗传算法优化EKF算法的SOC估算

    采用遗传算法对 EKF 中的系统噪声矩阵和测量矩阵的协方差进行在线优化,以实现在模型误差最小时对 SOC 进行在线估计
    发表于 03-12 12:27

    如何对CCSDS图像压缩算法编码进行优化

    如何对CCSDS图像压缩算法编码进行优化
    发表于 06-02 06:03

    粒子群算法城镇能源优化调度问题

    粒子群算法城镇能源优化调度问题,一、简介1 粒子群算法的概念粒子群优化算法(PSO:Particle swarm optimization)
    发表于 07-07 06:04

    多目标优化算法有哪些

    多目标优化算法有哪些,该文围绕包含柴油发电机、风力发电、光伏发电和铅酸蓄电池的独立微网系统中的容量配置问题,提出了包含微网全寿命周期内的总成本现值、负荷容量缺失率和污染物排放的多目标优化设计模型。该
    发表于 07-12 06:52

    如何改进和优化RSA算法

    第三章 如何改进和优化RSA算法这章呢,我想谈谈在实际应用出现的问题和理解。由于近期要开始各种忙了,所以写完这章后我短时间内也不打算出什么资料了=- =(反正平时就没有出资料的习惯。)在讲第一章
    发表于 07-19 07:12

    果蝇优化算法MATLAB实现

    果蝇优化算法MATLAB实现发布时间:2018-10-12 23:28,浏览次数:1183, 标签:MATLAB果蝇优化算法--Matlab实现1果蝇
    发表于 08-17 07:28

    如何优化控制算法的代码

    如何优化算法,也根据不同的处理器自带的协处理器或者硬件指令进行调整。引言  电机控制应用设计传统上采用微控制器(MCU)或数字信号处理器(DSP)来运行电机控制算法。在研究永磁同步电机
    发表于 08-30 07:57

    manual中rtk算法如何优化

    RTK算法原理是什么?manual中rtk算法如何优化
    发表于 09-27 06:36

    蚁群算法参数优化

    针对蚁群算法运行参数选取问题,提出一种利用粒子群优化算法对蚁群算法的运行参数进行优化选择的方法。
    发表于 04-22 08:42 28次下载

    智能优化算法及其应用_王凌著

    本书系统地叙述模拟退火算法、遗传算法、禁忌搜索、神经网络优化算法、混沌优化、混合优化策略等智能
    发表于 10-10 16:23 0次下载

    一种优化的SIFT配准算法

    针对SIFT算法在生成特征向量和进行特征匹配过程中存在的计算量较大、容易产生误匹配等不足,提出一种优化的SIFT配准算法优化
    发表于 12-05 13:46 0次下载
    一种<b class='flag-5'>优化</b>的SIFT配准<b class='flag-5'>算法</b>