正如我们在第6.1节中解释的那样,DRAM bursting是一种并行组织形式:多个位置在DRAM核心阵列中并行访问。然而,仅仅依靠bursting是不足以实现现代处理器所需的DRAM访问带宽水平的。DRAM系统通常采用另外两种并行组织形式:banks和channels。在最高级别,一个处理器包含一个或多个通道。每个通道是一个带有总线的内存控制器,连接处理器和一组DRAM bank。图 6.7 展示了一个包含四个通道的处理器,每个通道都有一个总线将四个DRAM bank连接到处理器。在实际系统中,处理器通常有1到8个通道,每个通道连接了大量的bank。
总线的数据传输带宽由其宽度和时钟频率决定。现代双数据速率(double data rate)(DDR)总线每个时钟周期进行两次数据传输:一次在每个时钟周期的上升沿,一次在下降沿。例如,一个64位的DDR总线,时钟频率为1 GHz,其带宽为8B×2×1 GHz=16GB/s。这似乎是一个很大的数字,但对于现代CPU和GPU来说往往还是太小了。一个现代CPU可能需要至少32 GB/s的内存带宽,而现代GPU可能需要256 GB/s。以这个例子为例,CPU将需要2个通道,GPU将需要16个通道。
对于每个通道,连接到它的bank数量由需要完全利用总线数据传输带宽的bank数量确定。这在图 6.8 中有所展示。每个bank包含一个DRAM单元阵列、用于访问这些单元的感应放大器,以及用于向总线提供数据爆发的接口(第6.1节)。
图 6.8(A) 展示了当单个 bank 连接到通道时的数据传输时序。它显示了对 bank 中 DRAM 单元进行的两次连续内存读取访问的时序。回想一下第6.1节的内容,每次访问都涉及到长时间的延迟,这是因为解码器需要激活单元,以及单元需要与感应放大器共享其存储的电荷。这种延迟以时间框架左端的灰色部分显示。一旦感应放大器完成其工作,突发数据就通过总线传输。通过总线传输突发数据的时间显示为图 6.8 中时间框架左边的深色部分。第二次内存读取访问将产生类似的长时间访问延迟(时间框架中深色部分之间的灰色区域),然后其突发数据才能被传输(右边的深色部分)。
实际上,访问延迟(灰色部分)比数据传输时间(深色部分)长得多。很明显,单 bank 组织的访问传输时序将严重浪费通道总线的数据传输带宽。例如,如果DRAM单元阵列访问延迟与数据传输时间的比率是20:1,那么通道总线的最大利用率将是1/21=4.8%;即16 GB/s的通道将以不超过0.76 GB/s的速率向处理器传输数据。这将是完全无法接受的。通过将多个 bank 连接到通道总线解决了这个问题。
当两个 bank 连接到一个通道总线时,可以在第一个 bank 正在服务另一个访问时,在第二个 bank 启动访问。因此,可以重叠访问DRAM单元阵列的延迟。图 6.8(B) 显示了双 bank 组织的时序。我们假设 bank 0 在图 6.8 所示窗口的更早时间开始。在第一个 bank 开始访问其单元阵列后不久,第二个 bank 也开始访问其单元阵列。当 bank 0 完成访问时,它传输突发数据(时间框架最左边的深色部分)。一旦 bank 0 完成数据传输,bank 1 可以传输其突发数据(第二个深色部分)。这种模式对于接下来的访问重复进行。
从图 6.8(B) 可以看出,通过拥有两个 bank,我们可以潜在地将通道总线的数据传输带宽利用率提高一倍。一般来说,如果单元阵列访问延迟与数据传输时间的比率是 R,那么我们需要至少有 R + 1 个 bank,才能完全利用通道总线的数据传输带宽。例如,如果比率是20,我们将需要至少21个 bank 连接到每个通道总线。通常,由于两个原因,每个通道总线连接的 bank 数量需要大于 R。一个原因是拥有更多的 bank 可以降低多个同时访问同一 bank 的可能性,这种现象称为 bank 冲突。由于每个 bank 一次只能服务一个访问,因此这些冲突访问的单元阵列访问延迟无法重叠。拥有更多的 bank 可以增加这些访问在多个 bank 之间分布的可能性。第二个原因是每个单元阵列的大小被设置为实现合理的延迟和可制造性。这限制了每个 bank 可以提供的单元数量。可能需要很多 bank 才能支持所需的内存大小。
线程的并行执行与DRAM系统的并行组织之间有一个重要的联系。为了实现设备指定的内存访问带宽,必须有足够数量的线程进行同时的内存访问。这一观察反映了最大化占用率的另一个好处。
回想一下在第4章“计算架构与调度”中,我们看到最大化占用率确保了足够数量的线程驻留在流式多处理器(SMs)上,以隐藏核心流水线延迟,从而有效利用指令吞吐量。正如我们现在看到的,最大化占用率还具有确保进行足够数量的内存访问请求以隐藏DRAM访问延迟的额外好处,从而有效利用内存带宽。当然,为了实现最佳带宽利用率,这些内存访问必须均匀分布在通道和 banks 上,且每次对 bank 的访问也必须是合并访问。
图 6.9 展示了一个将数组 M 的元素分布到通道和 banks 的玩具示例。我们假设一个小的突发大小为两个元素(8字节)。这种分布是由硬件设计完成的。通道和 banks 的寻址方式是,数组的前8字节(M[0]和M[1])存储在通道0的 bank 0中,接下来的8字节(M[2]和M[3])存储在通道1的 bank 0中,接下来的8字节(M[4]和M[5])存储在通道2的 bank 0中,接下来的8字节(M[6]和M[7])存储在通道3的 bank 0中。此时,分布回到通道0,但将在下一个8字节(M[8]和M[9])中使用 bank 1。因此,元素 M[10] 和 M[11] 将在通道1的 bank 1中,M[12] 和 M[13] 将在通道2的 bank 1中,M[14] 和 M[15] 将在通道3的 bank 1中。尽管图中没有显示,但任何额外的元素都会被包裹起来,并从通道0的 bank 0开始。例如,如果有更多元素,M[16]和M[17]将存储在通道0的 bank 0中,M[18]和M[19]将存储在通道1的 bank 0中,依此类推。
图 6.9 中所示的分布方案通常被称为交错数据分布,它将元素在系统的各个 banks 和通道中分散开来。这种方案确保了即使是相对较小的数组也能很好地分散开来。因此,我们只分配足够的元素以完全利用通道0的 bank 0的DRAM突发,然后再移动到通道1的 bank 0。在我们的玩具示例中,只要我们至少有16个元素,分布就会涉及存储元素的所有通道和 banks。
我们现在将说明并行线程执行与并行内存组织之间的相互作用。我们将使用图 5.5 中的示例,复制为图 6.10。我们假设乘法将使用2×3×2的线程块和2×3×2的tile。
在内核执行的第0阶段,所有四个线程块都将加载它们的第一个瓦片。每个瓦片涉及的 M 元素显示在图 6.11 中。第2行显示了第0阶段访问的 M 元素及其二维索引。第3行以线性化索引显示相同的 M 元素。假设所有线程块都是并行执行的。我们看到每个块都将进行两次合并访问。
根据图 6.9 中的分布,这些合并访问将在通道0的两个 banks 以及通道2的两个 banks 进行。这四个访问将并行进行,以利用两个通道并提高每个通道的数据传输带宽利用率。
我们还看到 Block0,0 和 Block0,1 将加载相同的 M 元素。大多数现代设备都配备了缓存,只要这些块的执行时序足够接近,就会将这些访问合并为一个。实际上,GPU设备中的缓存内存主要设计用于合并此类访问并减少对DRAM系统的访问次数。
第4行和第5行显示了内核执行第1阶段加载的 M 元素。我们看到,现在的访问是对通道1和通道3中的 banks 进行的。再次,这些访问将并行进行。读者应该清楚,线程的并行执行与DRAM系统的并行结构之间存在一种共生关系。一方面,充分利用DRAM系统的潜在访问带宽需要许多线程同时访问DRAM中的数据。另一方面,设备的执行吞吐量依赖于充分利用DRAM系统的并行结构,即 banks 和通道。例如,如果同时执行的线程都在同一个通道中访问数据,内存访问吞吐量和整体设备执行速度将大大降低。
读者可以验证,用相同的2×3×2线程块配置乘以更大的矩阵,例如8×3×8,将利用图 6.9 中的所有四个通道。另一方面,增加DRAM突发大小将需要乘以更大的矩阵,以充分利用所有通道的数据传输带宽。