🤗QiuckLearnFromPicture

GPU 加速应用程序与 CPU 应用程序对比

CUDA 线程层次结构

CUDA 提供的线程层次结构变量

协调并行线程

通过这些变量,公式 threadIdx.x + blockIdx.x * blockDim.x 可将每个线程映射到向量的元素中

必须使用代码检查并确保经由公式 threadIdx.x + blockIdx.x * blockDim.x 计算出的 dataIndex 小于 N(数据元素数量)。

网格跨度循环

并发 CUDA 流

默认流与非默认流中的操作不会发生重叠。

默认流较为特殊。默认流中执行任何操作期间,任何非默认流中皆不可同时执行任何操作,默认流将等待非默认流全部执行完毕后再开始运行,而且在其执行完毕后,其他非默认流才能开始执行。

使用流将复制和计算进行重叠

通过使用默认流,典型的三步式 CUDA 程序会顺次执行 HtoD 复制、计算和 DtoH 复制,如下图:

但是这样明显是不可行的。回忆一下,非默认流中的操作顺序不固定,因此可能会出现这种情况:

在其所需的数据传输到 GPU 之前,计算可能便会开始,

我们还可采用另一种初级做法:将所有操作全部发布在同一个非默认流中,以确保数据和计算的顺序,

但这样做与使用默认流没有区别,结果是依然没有重叠。思考一下,如果采用现有程序并将数据分为 2 块

如果现将针对每个数据块的所有操作移至其各自独立的非默认流,数据和计算顺序得以保持,同时能够实现部分重叠。

根据假设,通过增加数据块数量,重叠效果可能更好。要获得理想的分块数量,最好的途径是观察程序性能。

将数据分块以在多个流中使用时,索引可能较为棘手。让我们通过几个示例了解一下如何进行索引。首先为所有数据块分配所需数据,为使示例更加清晰,我们使用了较小规模的数据。

  1. 为CPU和GPU分配内存(N表示总的数据量);

  1. 接下来,定义流的数量,并通过执行循环代码以数组形式创建和收集流(此处定义的流的数量为2);

  1. 每个数据块的大小取决于数据条目的数量以及流的数量;

  1. 每个流需要处理一个数据块,我们需要计算其在整个数据集中的索引。为此,我们将遍历流的数量,从 0 开始然后乘以数据块大小。从 lower 索引开始,并使用一个数据块大小的数据,如此即可从全部数据中获得流数据,此方法将会应用至每一个 stream_i;

  1. 计算完这些值后,我们现在即可执行非默认流 HtoD 内存复制;

上面的示例中,N 被流数量整除。如果不能整除呢?为解决该问题,我们使用向上取整的除法运算来计算数据块大小。但是这还是会有问题,如下图:

我们确实可以访问所有数据,但又产生了新问题:对于最后一个数据块而言,数据块大小过小,存在线程访问越界的问题。

解决方法如下:

  1. 为每个数据块计算 upper 索引(不得超过 N);

  1. 然后使用 upper 和 lower 计算数据块 width;

  1. 现在使用 width 而非数据块大小进行迭代;

这样我们就能完美适配数据,而不受其大小或流数量的影响。

复制与计算重叠的代码示例

完整代码如下:

reference

Last updated

Was this helpful?