CUDA C BEST PRACTICE 笔记翻译 第二章

之前接触过一段时间的 cuda,但是自从工作之后就很少在碰 cuda 了,cuda 最近的发展可谓是日新月异,这次正准备阅读 cuda c best practice,趁此机会记录和翻译一下,也记录一下自己的见解(见解会用注解的形式标注)

这里是英文原文,想看原文的可以点这个链接
https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/

第一章是序言,搞点客套话,就不翻译了,直接第二章开始

2.异构计算

CUDA 编程 涉及到在两种不同的平台上并行地执行代码:有着一个或多个 CPU 的主机端(host 端,下文统称 host 端)和有着一个或多个NVIDIA-GPU 的设备端(device 端,下文统称 device 端)

注:由于在 cuda 里 host 和 device 是两个常用的概念,翻译成中文总觉得怪怪的,下文也都用 host 代指 CPU 端或者主机端,用 device 代表设备端或 GPU 端。

虽然 NVIDIA 的 GPU 尝尝与图形相关,但是其也是可以并行运行上千轻量级线程的强大的算数引擎,这使得 GPU 可以非常好的适用在一些可以利用并行执行的场景中。

然而,GPU 的设计与 host 端是截然不同的,所以了解他们之间的不同以及他们如何影响到CUDA程序的性能是十分重要的。

2.1 host 和 device 的区别

主要的区别在于线程模型 和 独立内存 。

  • 线程资源

host 端只能支持有限数量的并发线程。比如,一个拥有两个32core 处理器的机子只能同时执行64线程(如果CPU 支持同时多线程则是64的较小的整数倍)。相比之下,CUDA 里最小的并行可执行单元就是32线程(一个 warp),现代的 GPU可以在单个多处理器(multiprocesser)上最多同时执行2048个线程,在有80个 多处理器的 GPU 上可以同时执行超过16W 的线程。

  • 线程

CPU 的线程是重量级的,操作系统需要在 CPU 的执行环境中换进换出线程以提供多线程能力,线程的上下文切换是慢且开销大的。相比之下,GPU 的线程是轻量级的,在一个典型的 GPU 系统中,上千个线程(以 warp 的形式)排队等待执行。如果 GPU 需要等待一组 warp ,他可以继续执行其他 warp。由于活跃线程分配的寄存器是独立的,所以GPU的线程间切换不需要交换寄存器值和其他状态。这些资源会一直分配给每个线程直到它执行完毕。总而言之,CPU的核心被设计来最小化少量线程单次的延迟,GPU设计的目的就是用来同时并行执行大量轻量级线程以最大化吞吐

注:研究生是上课学习时,导师也曾介绍过这个理论,CPU 设计侧重于延迟,而 GPU 侧重于吞吐,GPU 单核的频率也是远低于 CPU 的。

  • RAM

host端与 device 端有着各自独立的内存。正是由于内存相互独立,host 端的内存需要与 device 端的内存进行偶尔的通信。

这里原文用了occasionally偶尔这个词,其意思我认为仅仅在于指出通信次数可以较少,对于一个常规的程序来说也就开始CPU->GPU,算完了再 GPU->CPU,内存拷贝是必须也省不掉的,及时是统一内存寻址也会有隐式的靠背(不过最近看 GTC 好像有个新技术可以不用内存拷贝,这块我就不是很了解了)

这些是CPU主机和GPU设备在并行编程方面的主要硬件差异,还有一些其他差异,会在本文档的其他地方讨论。考虑到这些差异,这可以被视作一个异构系统,其中每个处理单元都被用来做最擅长的事:host 端做序列化的任务,device 做并行化的任务。

2.2 什么任务运行在 CUDA-Enabled 设备

在决定程序的哪一部分运行在device端时,需要考虑以下问题:

  • device端 非常适合可以同时并行运行多个数据元素的计算。比较典型的就是大数据集的算数运算(比如矩阵),同样的操作可以在上千个元素间同时执行。使用 CUDA 获得良好性能的要求是:软件程序必须使用大量的并行线程。CUDA 的轻量级线程模型可以支持同时运行多个线程。
  • 为了使用 CUDA,数据必须从 host 传输到 device 端,这些传输是十分昂贵的,为了性能考虑必须最小化这些传输。这一传输成本会带来一下影响
    • 计算的复杂性必须能证明它值得在host 和 device 端进行数据的传输。只传输数据用来给少量线程简单的使用只会带来少量甚至没有收益。理想的场景是许多线程执行大量的工作。
    • 例如,将两个矩阵传输到device以执行矩阵加法,然后将结果传输回主机将不会实现太多的性能优势。这里的问题是传输每个数据所执行的计算的数目。对于前面提到的运算,假设每个矩阵的大小是NNN * N,计算量是N2N^2而传输量是3N23N^2,所以计算传输比是1:3或 O(1)。这个比值越大,越容易获得性能收益.举例来说,同样大小的矩阵乘法需要N3N^3的计算量,所以计算传输比是 O(N),矩阵规模越大,收益也会越大。操作的类型也重要的考虑因素,加法的计算复杂度与三角函数计算是不同的。在确定操作是应该在host上执行还是在GPU上执行时,考虑向GPU传输数据和从GPU传输数据的开销是很重要的。
    • 数据应该尽可能长时间的保存在 device 端。因为需要尽可能的减少传输,在同一组数据上运行多个内核程序(kernel)应该倾向于把数据一致保存在 device 端,而不是将结果传回 host 端再传回来。所以,之前的矩阵加法的例子,如果这两个矩阵之前已经在 device 端,是前面某些计算的结果,或者是他们加完之后还要进行后续的计算,那这个矩阵加法就应该在 device 端计算。这个方法论即使一系列计算中的某一步在 host 端计算更快也是有用的,如果能减少一次或多次设备间的传输,即使稍微慢点的 kernel 也是有受益的.

注:这个比例是个很好的量化手段,之前考虑工作量大小只是定型评经验来进行考虑,计算传输比可以像复杂度一样定量描述,说白了就是我费了那么大劲传到 GPU 上,总得多干点计算,不然我传个鸡毛。尽可能保存更长的时间这块就有点废话了, 因为计算传输比肯定是要考虑在 gpu 上的全部计算。

  • 为了获得最佳性能,在GPU上运行的相邻线程访问内存时应该有一定的一致性 ,特定的内存访问模式使硬件能够将多个数据项的读或写组合并到一个操作中(也就是常说的合并访存)。数据如果不能合理的布局以合并访存,或者没有足够的局部性以利用 L1 cache和纹理缓存,会导致使用 GPU 时加速效果较小。有一个值得注意的例外是完全随机的内存访问。一般来说,应该避免出现这种情况,因为与峰值能力相比,任何架构处理这些内存访问模式的效率都很低。然而,与基于缓存的架构(如 CPU)相比,延迟隐藏架构(GPU),应对完全随机的内存访问模式更好。

注:完全随机内存访问这是个比较有趣的结论,按照以前的想法来说,GPU 应该尽可能全局内存访问,完全随机更是会严重影响性能,但是看文档的描述完全随机虽然性能差,但是 CPU 性能更差,这块可能需要更多的测试数据的支撑吧,或者有没有类似的加速工作论文应用到了这一点呢