HPC-homework-3

稀疏矩阵乘法

任务描述

本任务为在单个GPU上优化稀疏矩阵乘法。

优化代码为spgemm-optimized.cu.

基本算法

在CPU上,稀疏矩阵乘法的基本算法为:

  • 首先估计最后非零元个数
  • 在matA矩阵上按行枚举i,按照CSR格式枚举非零元j,在matB矩阵上按行枚举对应的非零元k
  • 在行内统计非零元k个数
  • 接着计算真正矩阵值
  • 在matA矩阵上按行枚举i,按照CSR格式枚举非零元j,在matB矩阵上按行枚举对应的非零元k,算出对应(i, k)值大小

在GPU上,总体算法流程是类似的,首先通过 calculate_nzz对估算出最终非零元个数,最后在通过 calculate_values计算矩阵值。

优化算法

哈希表统计

在CPU版本的稀疏矩阵乘法中,我们可以利用时间戳的方式复用空间,空间复杂度可以降低到O(N)。

但在GPU上则无法并行这样做,因此为了在同一行上哪些元素已经出现,我们采用哈希表的方式来辅助统计。

1
2
3
4
__shared__ int hash_idx[HASH_COUNT];
for (int i = tid; i < HASH_COUNT; i += 32) {
hash_idx[i] = -1;
}

哈希函数采用基本的取模操作,比较简单:

1
2
int pk = matB->gpu_c_idx[j];
int hash = (pk & (HASH_COUNT - 1));

当出现哈希冲突时,我们将继续往后面遍历直到遇到第一个哈希槽插进去:

1
2
3
4
5
6
7
while (atomicCAS(&hash_idx[hash], -1, pk) != -1) {
if (hash_idx[hash] == pk) {
row_nzz--;
break;
}
hash = ((hash + 1) & (HASH_COUNT - 1));
}

这里用 atmoicCAS是为了防止多个thread对同一个哈希表槽进行操作。

多线程并行

考虑到按照行划分任务会出现负载均衡的问题,这里我们按照在同一行枚举的列对任务进行划分。

1
2
int rid = blockIdx.x;
int tid = threadIdx.x;

变量 tid表示线程编号(范围为0~31)。

哈希表初始化时,每个线程初始化一部分内容:

1
2
3
4
5
6
__shared__ int hash_idx[HASH_COUNT];
__shared__ data_t hash_values[HASH_COUNT];
for (int i = tid; i < HASH_COUNT; i += 32) {
hash_idx[i] = -1;
hash_values[i] = 0;
}

通过 __syncthreads函数同步所有线程。

在计算时,每个线程负责自己的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (int i = matA->gpu_r_pos[rid] + tid; i < matA->gpu_r_pos[rid + 1]; i += 32) {
int pj = matA->gpu_c_idx[i];
data_t vj = matA->gpu_values[i];

for (int j = matB->gpu_r_pos[pj]; j < matB->gpu_r_pos[pj + 1]; j++) {
int pk = matB->gpu_c_idx[j];
int hash = (pk & (HASH_COUNT - 1));
while (atomicCAS(&hash_idx[hash], -1, pk) != -1) {
if (hash_idx[hash] == pk) {
break;
}
hash = ((hash + 1) & (HASH_COUNT - 1));
}
atomicAdd(&hash_values[hash], matB->gpu_values[j] * vj);
}
}

在估算nzz数量时,通过reduction方法规约得到总的行数量:

1
2
3
4
5
6
__syncthreads();
row_nzz += __shfl_xor_sync(int(-1), row_nzz, 16);
row_nzz += __shfl_xor_sync(int(-1), row_nzz, 8);
row_nzz += __shfl_xor_sync(int(-1), row_nzz, 4);
row_nzz += __shfl_xor_sync(int(-1), row_nzz, 2);
row_nzz += __shfl_xor_sync(int(-1), row_nzz, 1);

在计算矩阵真正内容时,遍历哈希表中的非零元变为政正常数组:

1
2
3
4
5
6
7
8
int start = matC->gpu_r_pos[rid];
for (int i = tid; i < HASH_COUNT; i += 32) {
if (hash_idx[i] != -1) {
int pos = atomicAdd(&nzz, 1) + start;
matC->gpu_c_idx[pos] = hash_idx[i];
matC->gpu_values[pos] = hash_values[i];
}
}