CUDA kernel 性能的一个最重要因素是访问全局内存中数据的速度,
有限的带宽
很可能成为瓶颈。毕竟CUDA kernel一下子要处理很多数据,数据从哪来,得先有数据才能算,如何高效传输数据当然就比较重要了。
这里聊到的memory coalescing通常与tile技术结合使用,从而高效利用带宽。
全局内存 global memory 的特性
Global memory 是用 DRAM 实现的。数据位存储在小电容中的 DRAM 单元中,其中电荷的存在或缺失区分了 1 和 0 值。从 DRAM 单元读取数据需要小电容使用其微小的电荷来驱动连接到传感器的高电容线路,并触发传感器的检测机制,以确定电容中是否有足够的电荷作为 “1”。这个过程在目前 DRAM 芯片中需要几十纳秒。
下图显示了一个 DRAM 单元及其内容访问路径。decoder(解码器)是一个电子电路,它使用晶体管来驱动连接到成千上万单元的输出门的线路。充电或放电到期望水平可能需要很长时间。更难的是让单元驱动连接到感应放大器的垂直线,并允许感应放大器检测其内容。这是基于电荷共享。门会释放存储在单元中的微小电荷。如果单元内容是“1”,微小电荷必须将长位线的大电容的电势提升到足够高的水平,以便触发感应放大器的检测机制。
一个很好的类比是,有人在长走廊的一端拿着一小杯咖啡,而走廊另一端的人需要通过沿走廊传播的香气来确定咖啡的味道。
每次访问 DRAM 位置时,都会访问包括请求位置在内的
一系列连续位置
。每个 DRAM 芯片中都配备了许多传感器,它们同时并行工作。每个传感器都能感知这些连续位置中一个比特的内容。
一旦由传感器检测到,所有这些连续位置的数据都可以高速传输到处理器
。这些被访问和传递的连续位置被称为 DRAM bursts。如果应用程序集中使用这些突发中的数据,DRAM 可以以远高于访问真正随机位置序列的情况下的速率提供数据。
在任何给定时间点,一个 warp 中的线程执行相同的指令。当一个 warp 中的所有线程执行加载指令时,硬件会检测它们是否访问连续的全局内存位置。换句话说,
最有利的访问模式是当一个 warp 中的所有线程访问连续的全局内存位置时
。在这种情况下,硬件将所有这些访问合并或合并为对连续 DRAM 位置的一次集中访问。例如,对于一个 warp 的给定加载指令,如果线程 0 访问全局内存位置 X,线程 1 访问位置 X + 1,线程 2 访问位置 X + 2,等等,所有这些访问将
被合并或组合为单一请求
,以便在访问 DRAM 时访问连续位置。
这种合并访问允许 DRAM 以 bursts 形式传递数据。
举个栗子
C 和 CUDA 中的多维数组元素是根据行优先约定放置在线性寻址的内存空间中的。
请记住,行优先这个术语表示在内存中连续存放数组的每一行。在行优先存储中,二维数组的第一行元素先连续存储,然后是第二行,依此类推。在 C 和 CUDA 中,这意味着数组元素是按照它们在行中的顺序存储的。
如下图展示:
Placing matrix elements into a linear array based on row-major order
看下图的左上部分的代码,右上部分显示了访问模式的逻辑视图:连续线程遍历连续列。通过检查代码可以发现,对 M 的访问可以被合并。数组 M 的索引是 k×Width+col。变量 k 和 Width 在 warp 中的所有线程中都有相同的值。变量 col 被定义为 blockIdx.x×blockDim.x+threadIdx.x,这意味着连续线程(具有连续 threadIdx.x 值)将具有连续的 col 值,并因此访问 M 的连续元素。
图 6.3 描述了当矩阵以列优先顺序存储时,连续的线程如何遍历连续的列。图 6.3 的左上部分显示了代码,右上部分显示了内存访问的逻辑视图。程序仍然尝试让每个线程访问矩阵 M 的一列。通过检查代码可以看出,对 M 的访问并不利于合并。数组 M 的索引是 col×Width+k。与之前一样,col 被定义为 blockIdx.x×blockDim.x+threadIdx.x,这意味着连续的线程(具有连续 threadIdx.x 值)将具有连续的 col 值。然而,在 M 的索引中,col 乘以 Width,这意味着连续的线程将访问 M 中相隔 Width 的元素。因此,这些访问并不利于合并。
拼车要求希望拼车的工作者在共同的通勤时间表上做出妥协和一致。图 6.6 的上半部分展示了一个适合拼车的良好时间表模式。时间从左到右推移。工作者 A 和工作者 B 有相似的睡眠、工作和晚餐时间表。这使得这两位工作者能够轻松地一起开车上班和回家。他们相似的时间表使他们更容易就共同的出发时间和返回时间达成一致。但在图 6.6 的下半部分所示的时间表中情况并非如此。在这种情况下,工作者 A 和工作者 B 有非常不同的时间表。工作者 A 通宵狂欢至日出,白天睡觉,晚上去上班。工作者 B 夜间睡觉,早上去上班,并在下午 6:00 回家吃晚餐。这些时间表差异如此之大,以至于这两位工作者不可能协调出共同的时间来开车上下班。
Carpooling requires synchronization among people
内存合并与拼车安排非常相似。我们可以将数据视为通勤者,将 DRAM 访问请求视为车辆。当 DRAM 请求的速率超过 DRAM 系统提供的访问带宽时,交通拥堵情况加剧,算术单元变得空闲。如果多个线程从同一 DRAM 位置访问数据,它们可以形成一个“拼车团队”并将它们的访问合并成一个 DRAM 请求。然而,这要求线程具有类似的执行时间表,以便它们的数据访问可以合并为一个。同一 warp 中的线程是完美的候选者,因为它们都同时执行加载指令,这是由SIMD 执行的特性。