如果这篇博客帮助到你,可以请我喝一杯咖啡~
CC BY 4.0 (除特别声明或转载文章外)
上次我学习了用 Shuffle 加速 CUDA 上的 Reduce 操作,据说这是目前在 CUDA 上最快的区间规约算法。然而运用在实际的情况中却并没有对代码的性能带来多大提升。本文中我再次整理了自己已知的所有 CUDA 上的快速区间规约方法,并以此对写出高性能且高可扩展的 CUDA 代码提出一些自己的思考。
- 多路规约
- 使用 Shared Memory 和 Warp Shuffle 增加计算带宽
- 使用
<thrust/reduce.h>
- 使用
<cublas_v2.h>
实验环境
使用 v100 集群上一个结点的单张 v100 运行。
实验过程与分析
一些说明
和前一个实验用 Shuffle 加速 CUDA 上的 Reduce 操作不同,这里被规约元素的类型不再是 unsigned
而是 double
,更加贴合实际使用场景。单个 unsigned
内存占用 4 字节,而 double
是 8 字节,这就使得 Warp 间互相访问寄存器的流量增加了一倍,很大程度上降低了 Shuffle 的优化效果(推测)。
thrust::reduce
首先是性能标杆 thrust
库,来看一看目前最流行的实现可以达到怎样的性能。
运行时间 10.561248ms
。
simpleDasum
先来做最基础的算法优化,几乎无需掌握任何 CUDA 内存分布的知识。
首先,由于这里规约的元素数量高达十亿个,如果按照通常习惯的每个线程对应输入的一个元素,那么显卡的调度开销一定程度上无法忽视。这里我们让一个线程可以对应输入中的多个元素,减少所需要的线程数量,从而减少调度开销。
最后,我们用 template <size_t UNROLL_SIZE>
传入 #pragma unroll(UNROLL_SIZE)
循环展开次数,这样编译器可以在代码生成的时候将循环展开,以减少多次循环跳转的开销。
运行时间为10.727680ms
,和thrust
相比还算可以接受。
naiveDasum
在 simpleDasum
基础上,优化存储器的使用。
对于同一个 block 内的所有线程,我们可以借助 Shared Memory 再进行一次树形规约,从而减少对内存的写操作和二次规约。
对于同一个 warp 内的所有线程也是同理,warp 间直接访问寄存器的开销比 Shared Memory 更小。此外 warp 同步的开销也要小于__syncthreads()
,并且同一个 warp 上执行语句是不需要条件分支的,因为不管怎样都会被执行。
运行时间 10.467008ms
,终于比 thrust::reduce
快了一丢丢。
cublasDasum
也可以使用线性代数库 <cublas_v2.h>
中的 asum
系列函数实现。
运行时间为 11.918208ms
,看来cublas
库提供的线性代数抽象有一定的开销。
总结
可以看到,随着硬件技术的发展,显卡上的运行速度已经是相当快了,十亿多的数据只用了十毫秒就完成了规约,并且在这种前提下存储优化的效果越来越有限了。
然而,较之略显复杂的访存优化,一些简单的编程习惯反而能有效提高 CUDA 代码的效率。从可扩展性的角度来说,我也更倾向于写 simpleDasum
这样对硬件的依赖程度更低的代码。毕竟未来显卡到底会怎么发展谁也说不准,也许以后一个 warp 或者一个 block 中会有更多的线程。
最后调库大法好,自己做了半天优化最后也只比库快了一丢丢,从开发成本的角度来说还是不要重复造轮子为妙。
源代码
Dasum.pbs
调度脚本。
Dasum.o18880
运行结果。