Skip to content

Commit

Permalink
update exp spmm
Browse files Browse the repository at this point in the history
  • Loading branch information
zqh-wz committed May 15, 2024
1 parent 271c208 commit 8c431c3
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 88 deletions.
150 changes: 64 additions & 86 deletions docs/exp/3.spmm.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 实验三:稀疏矩阵-矩阵乘

负责助教:黄可钊 hkz20@mails.tsinghua.edu.cn
负责助教:张齐颢 zqh23@mails.tsinghua.edu.cn

在本实验中,你将通过实现 多线程 CPU 或 GPU 加速的稀疏矩阵-矩阵乘法(SpMM)进一步熟悉 OpenMP, CUDA 编程以及 CPU, GPU 体系结构。
在本实验中,你将通过实现 GPU 加速的稀疏矩阵-矩阵乘法(SpMM)进一步熟悉 OpenMP, CUDA 编程以及 CPU, GPU 体系结构。

## 实验任务

Expand Down Expand Up @@ -35,26 +35,22 @@ VAL = [ 10 20 30 40 50 60 70 80 ]
```
.
|-- CMakeLists.txt
|-- cmake
|-- data
|-- include
| |-- args.hxx
| |-- data.h
| |-- dbg.h
| |-- spmm_base.h
| |-- spmm_cpu_opt.h
| |-- spmm_cpu_ref.h
| |-- spmm_cusparse.h
| |-- spmm_opt.h
| |-- spmm_ref.h
| |-- util.h
| `-- valid.h
|-- script
| |-- run_all_CPU.sh
| `-- run_all.sh
|-- src
| |-- data.cu
| |-- spmm_cpu_opt.cpp
| |-- spmm_cpu_ref.cpp
| |-- spmm_cusparse.cu
| |-- spmm_opt.cu
| |-- spmm_ref.cu
Expand All @@ -63,85 +59,46 @@ VAL = [ 10 20 30 40 50 60 70 80 ]
|-- test
| |-- CMakeLists.txt
| |-- main.cpp
| |-- test_spmm_cpu.cpp
| `-- test_spmm.cu
`-- third_party
`-- googletest
```

其中 `spmm_base.h` 是SpMM实现的基类,`spmm_ref.*` `spmm_cpu_ref.*` 是效率很低的 GPU, CPU 参考实现,`spmm_cusparse.*` 是利用 NVIDIA 的稀疏计算库的 GPU 实现,`spmm_opt.*` `spmm_cpu_opt.*` 是你需要实现的地方(请只修改 `spmm_opt.h`, `spmm_opt.cu`, `spmm_cpu_opt.h`, `spmm_cpu_opt.cpp`)。你需要实现的是 `preprocess``run` 函数。
其中 `spmm_base.h` 是SpMM实现的基类,`spmm_ref.*` 是效率很低的 GPU 参考实现,`spmm_cusparse.*` 是利用 NVIDIA 的稀疏计算库的 GPU 实现,`spmm_opt.*` 是你需要实现的地方(请只修改 `spmm_opt.h`, `spmm_opt.cu`)。你需要实现的是 `preprocess``run` 函数。

`test_spmm.cu` `test_spmm_cpu.cpp` 中,使用 Googletest 实现了多个测试项,其中有验证正确性的以及测试性能的。请在先过了正确性验证之后再运行性能测试。
`test_spmm.cu` 中,使用 Googletest 实现了多个测试项,其中有验证正确性的以及测试性能的。请在先过了正确性验证之后再运行性能测试。

## 实验步骤

```bash
spack load [email protected]
spack load cuda
spack load [email protected]
cp -R /home/course/hpc/assignments/2024/PA3/ ~
cd ~/PA3/
mkdir build
cd build
cmake ..
make -j4
# 运行单个数据点
srun -gpus 1 ./test/unit_tests --dataset <datasetname> --len 32 --datadir ~/PA3/data/ # 所有的数据集在 ~/PA3/data/ 中
srun -N 1 --gres=gpu:1 ./test/unit_tests --dataset <datasetname> --len 32 --datadir ~/PA3/data/ # 所有的数据集在 ~/PA3/data/ 中
# 运行全部 GPU 数据点
srun -gpus 1 ~/PA3/scripts/run_all.sh # 在 PA3/scripts 目录下
# 运行全部 CPU 数据点
srun -gpus 1 ~/PA3/scripts/run_all_CPU.sh # 在 PA3/scripts 目录下
srun -N 1 --gres=gpu:1 ~/PA3/script/run_all.sh # 在 PA3/scripts 目录下
# 改变环境变量,仅仅运行单个测试,例如验证正确性(Validation)
GTEST_FILTER="SpMMTest.validation" srun --gpus 1 ./test/unit_tests --dataset <datasetname> --len 32 --datadir ~/PA3/data/
# 例如只运行 CPU 的相关测试
GTEST_FILTER="SpMMCPUTest.*" srun --gpus 1 ./test/unit_tests --dataset <datasetname> --len 32 --datadir ~/PA3/data/
GTEST_FILTER="SpMMTest.validation" # 运行全部 GPU 数据点
srun -N 1 --gres=gpu:1 ./test/unit_tests --dataset <datasetname> --len 32 --datadir ~/PA3/data/
```

其中 `dataset` 包含许多真实图数据(稀疏矩阵),稀疏矩阵的形状($M$,在代码中是 `kNumV`)和非零元($nnz$,在代码中是 `kNumE`)也被其决定。例如对于图数据 `a`,我们有两个文件,`a.config` 在同一行内存储了 $M$ 和 $nnz$,`a.graph` 第一行存储了 `PTR` 数组,第二行存储了 `IDX` 数组,`VAL` 数组在运行时随机初始化。

`--len` 决定了 $B$ 和 $C$ 的 $K$。数据在 `PA3/data` 中。可以自己造一个小的数据集来 debug,如下:

```bash
srun -gpus 1 ./test/unit_tests --dataset toy_graph --len 32 --datadir /path/to/your/data
srun -N 1 --gres=gpu:1 ./test/unit_tests --dataset toy_graph --len 32 --datadir /path/to/your/data
```

在测试时会测试 `len = 32, 256` 的情况。

## 实验提交

1. 实验代码:
* 在截止日期之前将完成后的整个实验框架置于自己 home 目录下的 `PA3` 目录,如 `/home/course/hpc/users/2020000000/PA3`
2. 实验报告:
***PDF 文件** 提交至网络学堂。
* 包含以下内容:
1. 介绍你的实现方法,可以包括如何解决 ref 实现中的 warp divergence 的问题,如何利用各级缓存,如何改善数据的局部性,如何解决 load imbalance 的问题等;
2. 展示不同优化对性能产生的影响,可以以单个数据集为例子详细分析;
3. 在 $len = 32, 256$ 时的运行时间,及相对于 cuSparse 实现的加速比。

## 优化 Hint

* **GPU 访存**:在 SpMM 中,稀疏矩阵的一个元素代表了对于稠密矩阵的一行的访问,所以访存量很大,需要考虑到 GPU 的访存行为(coalesce memory access)来优化
* **Warp divergence**:稀疏矩阵的元素分布不规则,会导致 reference 实现中同一个 warp 内部的线程工作量差距很大,因为 warp divergence 导致一个 warp 的执行时间由最慢的线程决定。而在 reference 实现中,每个线程负责一整行稀疏矩阵相关计算;每行非零元相差大,所以有严重的 warp divergence 问题。可以通过改变并行维度,让同一个 warp 内线程处理相同的工作量;
* **局部性**:稀疏结构带来不规则的数据访问,导致局部性很差,可以通过对图数据做预处理(在 `preprocess` 函数中处理,不占计算性能的运行时间;可以使用现有工具如 [METIS](https://github.com/KarypisLab/METIS),也可以自己实现),改变图数据结构,增加局部性;
* **负载不均衡**:可以自己预处理图结构,来减少它的不规则性,优化计算过程中的 load imbalance 的问题。

## GPU 优化技巧

可以通过 GPU profiling 工具 `nvprof` 对程序进行 profile:

```bash
# 得到程序运行的总结,包括整个程序运行过程中,各个 kernel 以及 CUDA API 的执行时间和次数
srun --gpus 1 nvprof ./test/unit_tests xxxxxxxxxx
# profile 单个 kernel 的执行情况,通过 --kernels 指定要 profile 的 kernel 的名称;通过 --metrics 指定要 profile 的是什么 metric,如 dram_read_bytes, achieved_occupancy 等,也可以指定为 all 来得到所有的 metric
srun --gpus 1 nvprof --kernels "KERNEL1|KERNEL2" --metrics "METRIC1|METRIC2" ./test/unit_tests xxxxxxxxxx
```

关于可以 profile 得到的性能指标以及 `nvprof` 更高级的使用方法可以参考 <https://docs.nvidia.com/cuda/profiler-users-guide/index.html>

## 往届实现

![稀疏矩阵例子](./fig/pa3/performance.svg)

图中显示了不同的实现在不同数据集上相对 cuSparse 的加速比($K = 32$):同学 A 通过改变线程映射的方式,基本消除了 warp divergence 的问题,其代码量相比于 naive 实现仅有小于 20 行不同;同学 B 除此之外,在 CPU 上预处理了稀疏矩阵,在一些不规则的数据集上,如 arxiv,取得了更多的提升。

## 评分

### 正确性
Expand All @@ -151,55 +108,38 @@ srun --gpus 1 nvprof --kernels "KERNEL1|KERNEL2" --metrics "METRIC1|METRIC2" ./t

为了避免直接提交下发的参考实现得分,我们还设置了加速比得分。GPU 的加速比得分为需要超过 cusparse 的性能;

只要在 **≥20** 个数据集超过 cusparse 的性能且结果正确, 就可以获得 **全部** 加速比分;
只要在 **≥20** 个测试中超过 cusparse 的性能且结果正确, 就可以获得 **全部** 加速比分;


### 性能

性能得分共占 $30\%$, 针对 GPU 测试 13 个数据集 (`scripts/run_all.sh`中指定), 两种 $K$ 的长度; 针对 CPU 测试 4 个数据集 (`scripts/run_all_CPU.sh`中指定), 两种 $K$ 的长度;
性能得分共占 $30\%$, 针对 GPU 测试 13 个数据集 (`scripts/run_all.sh`中指定), 两种 $K$ 的长度;

* 对于每组测试用例,只有当你获得了正确性基础分后,才能得到性能分。每组测试用例的性能分数相同。
* 每组测试用例有一个性能线 (作业开始一周后公布),超过性能线的同学将得到满分。
* 每组测试用例有一个性能线,超过性能线的同学将得到满分。
* 未达到性能线的同学,根据测试性能在 **未达性能线同学** 的排名给出每组测试用例的分数:每组测试用例各自排名,性能排名前 $10 \%$ 的同学得到 $100 \%$ 的分数,排名 $10 \%$ - $20 \%$ 的同学得到 $90 \%$ 的分数,依此类推。对于任何测试用例,获得正确性分数的同学将至少获得 $10 \%$ 的性能分数。

### 更新 (2023.05.25)

原要求:

为了避免直接提交下发的参考实现得分,我们还设置了加速比得分。GPU 的加速比得分为需要超过 cusparse 的性能;

对于每个测试用例,只有当你 **获得了正确性基础分且加速比合格**,方可获得该测试用例的加速比得分。

更新后的要求:

为了避免直接提交下发的参考实现得分,我们还设置了加速比得分。GPU 的加速比得分为需要超过 cusparse 的性能;

只要在 **≥20** 个数据集超过 cusparse 的性能且结果正确, 就可以获得 **全部** 加速比分;
#### 性能线



### 性能线

为避免针对对于每个数据集进行微调导致不必要的工作量, 发布两种性能线, 一个是整体的, 一个是针对每个数据集的, 达到整体 **或者** 针对单个数据集的性能线, 即可获得全部性能分.
为避免针对对于每个数据集进行微调导致不必要的工作量, 发布两种性能线:整体性能线和针对每个数据集的性能线, 达到整体 **或者** 针对单个数据集的性能线, 即可获得全部性能分.

* 如果你满足了整体性能线, 则得到当前setting下对应的所有数据点的满分(即使有数据集没达线)
* 如果你满足了某个数据集的性能线, 则得到这个数据集的所有性能分
* 性能线仅针对GPU测例, CPU不设性能线
<!-- * 性能线仅针对GPU测例, CPU不设性能线 -->

#### 整体
#### 整体性能线

计算方法是 `avg(nnz/t)`, K不同, 分别计算
以吞吐量作为性能指标,其计算方法是 `avg(nnz/t)`

对于 K=32, 平均吞吐量为 5e9 nnz/s
对于 K=32, 平均吞吐量达到 5e9 nnz/s

对于 K=256, 平均吞吐量为 6.5e8 nnz/s
对于 K=256, 平均吞吐量达到 6.5e8 nnz/s


#### 单个数据集性能线


#### 单个数据集

单位为us
以运行时间作为性能指标,单位为us

| dataset | k=32 | k=256 |
|-----------------|----------|-----------|
Expand All @@ -222,7 +162,45 @@ srun --gpus 1 nvprof --kernels "KERNEL1|KERNEL2" --metrics "METRIC1|METRIC2" ./t

### 实验报告

实验报告共占 $10\%$。
实验报告占 $10\%$。

## 实验提交

1. 实验代码:
* 在截止日期之前将完成后的整个实验框架置于自己 home 目录下的 `PA3` 目录,如 `/home/course/hpc/users/2020000000/PA3`
2. 实验报告:
***PDF 文件** 提交至网络学堂。
* 包含以下内容:
1. 介绍你的实现方法,可以包括如何解决 ref 实现中的 warp divergence 的问题,如何利用各级缓存,如何改善数据的局部性,如何解决 load imbalance 的问题等;
2. 展示不同优化对性能产生的影响,可以以单个数据集为例子详细分析;
3. 在 $len = 32, 256$ 时的运行时间,及相对于 cuSparse 实现的加速比。

## 优化 Hint

* **GPU 访存**:在 SpMM 中,稀疏矩阵的一个元素代表了对于稠密矩阵的一行的访问,所以访存量很大,需要考虑到 GPU 的访存行为(coalesce memory access)来优化
* **Warp divergence**:稀疏矩阵的元素分布不规则,会导致 reference 实现中同一个 warp 内部的线程工作量差距很大,因为 warp divergence 导致一个 warp 的执行时间由最慢的线程决定。而在 reference 实现中,每个线程负责一整行稀疏矩阵相关计算;每行非零元相差大,所以有严重的 warp divergence 问题。可以通过改变并行维度,让同一个 warp 内线程处理相同的工作量;
* **局部性**:稀疏结构带来不规则的数据访问,导致局部性很差,可以通过对图数据做预处理(在 `preprocess` 函数中处理,不占计算性能的运行时间;可以使用现有工具如 [METIS](https://github.com/KarypisLab/METIS),也可以自己实现),改变图数据结构,增加局部性;
* **负载不均衡**:可以自己预处理图结构,来减少它的不规则性,优化计算过程中的 load imbalance 的问题。

## GPU 优化技巧

可以通过 GPU profiling 工具 `nvprof` 对程序进行 profile:

```bash
# 得到程序运行的总结,包括整个程序运行过程中,各个 kernel 以及 CUDA API 的执行时间和次数
srun --gpus 1 nvprof ./test/unit_tests xxxxxxxxxx
# profile 单个 kernel 的执行情况,通过 --kernels 指定要 profile 的 kernel 的名称;通过 --metrics 指定要 profile 的是什么 metric,如 dram_read_bytes, achieved_occupancy 等,也可以指定为 all 来得到所有的 metric
srun --gpus 1 nvprof --kernels "KERNEL1|KERNEL2" --metrics "METRIC1|METRIC2" ./test/unit_tests xxxxxxxxxx
```

关于可以 profile 得到的性能指标以及 `nvprof` 更高级的使用方法可以参考 <https://docs.nvidia.com/cuda/profiler-users-guide/index.html>

## 往届实现

![稀疏矩阵例子](./fig/pa3/performance.svg)

图中显示了不同的实现在不同数据集上相对 cuSparse 的加速比($K = 32$):同学 A 通过改变线程映射的方式,基本消除了 warp divergence 的问题,其代码量相比于 naive 实现仅有小于 20 行不同;同学 B 除此之外,在 CPU 上预处理了稀疏矩阵,在一些不规则的数据集上,如 arxiv,取得了更多的提升。


## 注意事项

Expand All @@ -237,4 +215,4 @@ srun --gpus 1 nvprof --kernels "KERNEL1|KERNEL2" --metrics "METRIC1|METRIC2" ./t

关于本作业的问题可以在共享文档中反馈,助教会 check ,热心的同学也可以帮忙回答,共创良好讨论氛围。

【腾讯文档】高性能作业 PA3 反馈:<https://docs.qq.com/doc/DQURXTEF3dldBV0pC>
<!-- 【腾讯文档】高性能作业 PA3 反馈:<https://docs.qq.com/doc/DQURXTEF3dldBV0pC> -->
3 changes: 1 addition & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@

* [实验一](exp/1.odd_even_sort.md)(奇偶排序,$20\%$):基于消息传递的并行编程(MPI)
* [实验二](exp/2.apsp.md)(全源最短路,$20\%$):CUDA 编程
<!--
* [实验三](exp/3.spmm.md)(稀疏矩阵-矩阵乘,$30\%$):CPU 多线程 与 CUDA 编程 -->
<!-- * [实验三](exp/3.spmm.md)(稀疏矩阵-矩阵乘,$30\%$):CUDA 编程 -->
<!--
* [实验零](exp/0.pow_a.md)(pow_a,$5\%$):熟悉环境配置、集群使用、作业提交
Expand Down

0 comments on commit 8c431c3

Please sign in to comment.