SpMV在GPU上的瓶颈(bottleneck)在于负载均衡(load balancing) 和 内存带宽(memory bandwith)。
负载均衡具体指什么?GPU里有很多block,每个block有很多thread,thread之间是并行的。block与block之间、thread和thread之间都有负载均衡的问题。
比如R1、R2两行分配给B1、B2两个block,R1非常稀疏,R2非常稠密,那么由于R1非零元素很少(稀疏矩阵一半只存非零元),R1会很快地完成该行与向量的点乘计算,因此B1就比B2完成任务,B1就空闲了,得等B2完成后才能给出最后的结果。
thread之间也是类似,比方说就t1和t2两个thread,t1处理奇数位置元素,t2处理偶数位置元素,如果奇数位置非零元素非常稀疏,偶数位置非零元素非常稠密,那么t1会先完成任务,t2会后完成任务,t1就得等着t2。
内存带宽又是什么问题呢?是说整个稀疏矩阵很大,把所有数据传到GPU里要花费挺长时间,而且得数据传完了才能得到最后的结果啊。所以如果能减小稀疏矩阵的体积,就能提速。用英文说就是reduce memory footprint。
论文中用到了什么方法来解决这两个问题?
文章的主要贡献在于提出了BCCOO/BCCOO+格式以及基于BCCOO/BCCOO+的GPU算子(如何分配、汇合结果等),它们是由COO格式演变而来的,比起COO多了分块和行索引压缩。
分块的坏处是会可能补很多0,但是行索引和列索引都会变少。
分块有很多规格可以选,后面提到的auto-tuning framework可以自动选择。
行索引压缩相当于记录了前后行索引的差,而且使用bit记录的。原本行索引是升序排列的,大部分情况前后差为0,换行的时候差为1或者极少大于1,大于1的情况就用多个1表示,估计value还要补0,文章没提。最后翻转0和1。
因为只记录非零元素,所以可以给每个thread分配等量的非零元素,以此实现负载均衡。但是会带来最后的结果合并的问题,比如一个block里可能是不同行的,这还需要额外记录一些信息,来保证最后能得出正确的结果。
由于用了bit array,所以行索引的的数据量是减少了很多的,这样就减少了很多memory footprint。
BCCOO+是对稀疏矩阵进行了垂直的切分(竖着切),然后把它们摞在一起。这样每个block可以处理很多行,都是对应向量的同一段,不用往block里加载向量的其他段了,也算是减少了一些memory footprint。
文章里有一个没看明白的点,是在做一个叫做Segmented Sum/Scan的东西,维基百科上说是前缀和的一个变种:
多了一个flag bits,把input分成了好多段,不同段分别做前缀和,不知道在矩阵当中到底对应什么。
刚又看了一下有点懂了,论文中说是跟BCCOO格式的bit flag有关的,因为bit flag将value分段了,每个段对应不同的行,所以分段的前缀和(segmented sum/scan)也就理所当然了,只需要分段前缀和每段最后的结果也就知道什么意思了。这就是矩阵乘法每一行和向量点积,把对应点乘积加起来的过程。
文章里引入了很多超参数,比如分块的大小、BCCOO/BCCOO+,该怎么选?
文章后面给了一个auto-tuning的方法。不过看起来是非常细节、硬件相关的东西,我没怎么看。不过应该还讲了一些跟稀疏矩阵特点有关的东西,我也一同略过了,以后需要这方面资料可以查查。
总结一下,文章最重要的思想是将单调缓慢增加的数据变成了前后之差的bit array,大大减少了内存需要。