为什么在Cuda/OpenCL的全局内存中不会出现银行冲突?

19

我尚未理解且谷歌也没有帮助我,为什么在共享内存中可能会出现银行冲突,但是在全局内存中不会? 寄存器是否可能存在银行冲突?

更新: 哇,我真的很感谢Tibbit和Grizzly给出的两个答案。看起来我只能给一个答案打勾号。我对Stack Overflow还比较新。我想我必须选择一个答案作为最佳答案。那么我可以采取一些行动向没被选为最佳答案的答案表示感谢吗?


您可以随时为您喜欢的任何问题或答案点赞。 - Grizzly
银行冲突不仅可能发生在寄存器文件中,还可能发生在内存层次结构的其他级别上。共享内存银行冲突会严重影响内核性能,并且完全可以由开发人员进行控制。其他类型的银行冲突对性能影响较小,无法由开发人员解决,因此不会向开发人员通报。 - Greg Smith
4个回答

38

简短回答:在全局内存和寄存器中都没有银行冲突。

解释:

理解原因的关键是抓住操作的粒度。单个线程不会访问全局内存。全局内存访问是"合并"的。由于全局内存速度很慢,块内线程的任何访问都会被组合在一起,以尽可能少地向全局内存发出请求。

共享内存可同时被线程访问。当两个线程试图访问同一块内存时,这会导致银行冲突。

除了分配给它的线程之外,任何线程都无法访问寄存器。由于您无法读取或写入我的寄存器,因此您无法阻止我访问它们--因此不存在任何银行冲突。

谁可以读取和写入全局内存?

只有块。单个线程可以进行访问,但该事务将在块级别(实际上是warp / half warp级别,但我试图不要过于复杂)处理。如果两个块访问相同的内存,我认为它不会花费更长时间,并且可能会在最新设备的L1缓存的加速下发生--虽然这不是透明显而易见的。

谁可以读取和写入共享内存?

给定块内的任何线程。如果每个块只有1个线程,您就不会遇到银行冲突,但性能也不会理想。银行冲突是因为分配了带有几个线程(例如512个)的块,并且它们都在争夺同一块内存中的不同地址(并不完全相同)。CUDA C编程指南的结尾有一些关于这些冲突的优秀图片--第167页(实际上是pdf的第177页)的G2图。 链接到3.2版本

谁可以读取和写入寄存器?

仅仅指定线程被分配访问。因此,一次只有一个线程在访问它。


请注意,我对 L1 缓存的评论实际上是我自己的问题——在 L1 缓存中是否会发生 bank 冲突。由于这完全是由硬件处理的,我不认为在最新的文档中我们已经被告知了。(但 L1 仅存在于最新的 2.* 硬件中——因此,如果您没有 Fermi GPU,则此点无效)。 - M. Tibbits

23

该类型内存是否存在银行冲突显然取决于内存结构及其用途。

那么,为什么共享内存的设计允许出现银行冲突呢?

相对来说,这比较简单,很难设计一个可以同时处理对同一内存的独立访问的内存控制器(大部分无法实现)。因此,为了让半warp中的每个线程都能够访问单独寻址的字,内存被划分成多个bank,每个bank有一个独立的控制器(至少可以这样认为,不确定实际硬件如何)。这些bank是交错的,以便让顺序执行的线程快速访问顺序存储器。因此,这些bank中的每个都可以一次处理一个请求,理想情况下允许半warp中所有请求并行执行(显然,由于这些bank的独立性,该模型在理论上可以支持更高的带宽,这也是一个优点)。

那么寄存器呢?

寄存器被设计为作为ALU指令的操作数进行访问,这意味着必须使用非常低的延迟进行访问。因此,它们获得更多的晶体管/位以实现这一点。我不确定现代处理器如何访问寄存器(这不是你经常需要的信息,也不是那么容易找到的信息)。但是,显然将寄存器组织成bank是非常不切实际的(对于更简单的架构,您通常会看到所有寄存器都挂在一个大的多路复用器上)。所以,寄存器不会出现银行冲突。

全局内存

首先,全局内存的工作粒度不同于共享内存。内存以32、64或128字节块的形式访问(至少对于GT200而言,对于fermi,它总是128字节,但使用缓存,AMD略有不同),每次从块中获取数据时都要访问/传输整个块。这就是为什么需要协调访问,因为如果每个线程都从不同的块访问内存,就必须传输所有块。

但是,谁说没有银行冲突呢?我并不完全确定这一点,因为我没有找到任何关于NVIDIA硬件的实际来源来支持这一点,但它似乎是合乎逻辑的:

全局内存通常分布在多个内存芯片上(可以通过查看显卡轻松验证)。如果每个芯片像一个本地内存的银行,那么如果同一银行有多个同时请求,就会出现银行冲突。然而,这种影响会小得多(因为大多数时间消耗在内存访问的延迟上),并且不会在一个工作组“内部”产生明显的效果(因为只有一个半批次执行,如果该半批次发出多个请求,则会出现未对齐的内存访问,所以已经受到影响,很难测量此冲突的影响)。因此,只有多个工作组尝试访问同一银行时才会出现冲突。在典型的GPGPU情况下,您的大型数据集位于顺序内存中,因此影响应该不会真正可见,因为有足够的其他工作组同时访问其他银行,但应该可以构造一些仅集中在少数几个银行上的数据集的情况,这将导致带宽下降(由于最大带宽来自均匀分配所有银行的访问,因此每个银行只有该带宽的一部分)。同样,我没有阅读任何东西来证明这个理论对于Nvidia硬件的情况(大多数都关注数据对齐,当然对于自然数据集来说这更重要),但根据ATI Stream计算指南,这是Radeon卡的情况(对于5xxx:银行相距2kb,您需要确保您的访问(来自所有同时活动的工作组)均匀分布在所有银行上),因此我想NVIDIA卡的行为应该类似。

当然,在大多数情况下,全局内存中的银行冲突可能并不是一个问题,因此实际上您可以说:

  • 访问全局内存时注意数据对齐
  • 访问本地内存时注意银行冲突
  • 访问寄存器没有问题

1
寻找有关分区露营的信息时,我偶然发现了这个答案。你是对的,全局内存在物理上被划分为分区,一些访问模式可能会产生冲突,即使访问已经合并(请参阅CUDA SDK中矩阵转置示例的文档)。然而,在Fermi架构和一般计算能力为2.x的设备中,全局内存访问是被缓存的,宽度为32字节,并且地址是哈希的,因此理论上分区露营不应该成为问题。 - Auron
这些在计算能力>=2.0中的内存架构改进是否也减少了全局内存写入时分区竞争的影响? - Ahmed Fasih
1
运行CUDA 5矩阵转置示例时,分区露营似乎不会影响C2050 Tesla(计算能力2.0),因为粗粒度和细粒度的伪转置之间几乎没有什么区别。但我想要官方确认。 - Ahmed Fasih
Fermi架构中的地址哈希是否有官方文档? - Benedikt Waldvogel
@SakshamJain 抱歉,这是5年前的事情了,我恐怕记不起来了。可能是在CUDA文档中,但我不能确定。 - Auron
显示剩余4条评论

4

多个线程同时访问同一银行并不一定意味着存在银行冲突。只有当线程想要同时从同一银行的不同行读取时才会发生冲突。


0
为什么在共享内存中可能会出现银行冲突,而在全局内存中却不会呢?
实际上,全局内存访问也存在银行冲突和通道冲突。只有当内存通道和银行以轮询方式均匀访问时,才能达到最大的全局内存带宽。对于对单个一维数组进行线性内存访问的情况,内存控制器通常会自动交错地将内存请求分配给每个银行和通道。然而,当同时访问多个一维数组(或多维数组的不同行)时,并且它们的基地址是内存通道或银行大小的倍数时,可能会出现不完美的内存交错。在这种情况下,一个通道或银行的负载比另一个通道或银行更重,导致内存访问串行化并降低可用的全局内存带宽。
由于缺乏文档,我并不完全了解它的工作原理,但它肯定存在。在我的实验中,我观察到由于不幸的内存基地址导致了20%的性能下降。这个问题可能相当隐匿 - 根据内存分配大小的不同,性能下降可能会随机发生。有时,内存分配器的默认对齐大小也可能过于聪明自作聪明 - 当每个数组的基地址都对齐到较大的大小时,可能会增加通道/银行冲突的几率,有时会使其100%发生。我还发现,分配一个大内存池,然后通过手动偏移将较小的数组"错位"到不同的通道/银行中,可以帮助减轻这个问题。

内存交错模式有时可能很棘手。例如,AMD的手册指出Radeon HD 79XX系列GPU具有12个内存通道 - 这不是2的幂,因此通道映射远非直观,没有文档无法仅从内存地址位推断。不幸的是,我发现GPU供应商对此通常提供的文档不够详细,可能需要一些试验和错误。例如,AMD的OpenCL优化手册仅适用于GCN硬件,并且不提供任何关于更新于Radeon HD 7970之后的硬件的信息 - 关于Vega中使用的新型GCN GPU和带有HBM VRAM的新型RDNA/CDNA架构完全缺失。然而,AMD提供了OpenCL扩展来报告硬件的通道和银行大小,这可能有助于实验。在我的Radeon VII / Instinct MI50上,它们是:

Global memory channels (AMD)                    128
Global memory banks per channel (AMD)           4
Global memory bank width (AMD)                  256 bytes

庞大的通道数量可能是4096位HBM2内存的结果。

AMD的优化手册

AMD的旧版 AMD APP SDK OpenCL优化指南提供了以下解释:

2.1 Global Memory Optimization

[...] If two memory access requests are directed to the same controller, the hardware serializes the access. This is called a channel conflict. Similarly, if two memory access requests go to the same memory bank, hardware serializes the access. This is called a bank conflict. From a developer’s point of view, there is not much difference between channel and bank conflicts. Often, a large power of two stride results in a channel conflict. The size of the power of two stride that causes a specific type of conflict depends on the chip. A stride that results in a channel conflict on a machine with eight channels might result in a bank conflict on a machine with four. In this document, the term bank conflict is used to refer to either kind of conflict.

2.1.1 Channel Conflicts

The important concept is memory stride: the increment in memory address, measured in elements, between successive elements fetched or stored by consecutive work-items in a kernel. Many important kernels do not exclusively use simple stride one accessing patterns; instead, they feature large non-unit strides. For instance, many codes perform similar operations on each dimension of a two- or three-dimensional array. Performing computations on the low dimension can often be done with unit stride, but the strides of the computations in the other dimensions are typically large values. This can result in significantly degraded performance when the codes are ported unchanged to GPU systems. A CPU with caches presents the same problem, large power-of-two strides force data into only a few cache lines.

One solution is to rewrite the code to employ array transpositions between the kernels. This allows all computations to be done at unit stride. Ensure that the time required for the transposition is re latively small compared to the time to perform the kernel calculation.

For many kernels, the reduction in performance is sufficiently large that it is worthwhile to try to understand and solve this problem.

In GPU programming, it is best to have adjacent work-items read or write adjacent memory addresses. This is one way to avoid channel conflicts. When the application has complete control of the access pattern and address generation, the developer must arrange the data structures to minimize bank conflicts. Accesses that differ in the lower bits can run in parallel; those that differ only in the upper bits can be serialized.

In this example:

for (ptr=base; ptr<max; ptr += 16KB)
    R0 = *ptr ;

where the lower bits are all the same, the memory requests all access the same bank on the same channel and are processed serially. This is a low-performance pattern to be avoided. When the stride is a power of 2 (and larger than the channel interleave), the loop above only accesses one channel of memory.

值得注意的是,将内存访问分布在所有通道上并不总是有助于性能,反而可能降低性能。AMD警告说,在同一个工作组中访问相同的内存通道/银行可能更好 - 因为GPU同时运行多个工作组,理想的内存交错就可以实现。另一方面,在同一个工作组中访问多个内存通道/银行会降低性能。
如果工作组中的每个工作项引用连续的内存地址,并且工作项0的地址对齐到256字节,并且每个工作项获取32位数据,整个波前将访问一个通道。虽然这看起来很慢,但实际上它是一种快速模式,因为需要考虑整个设备上的内存访问,而不仅仅是单个波前。
在任何时候,每个计算单元都在执行来自单个波前的指令。在内存密集型内核中,该指令很可能是内存访问。由于AMD Radeon HD 7970 GPU上有12个通道,最多可以有12个计算单元在一个周期内发出内存访问操作。如果来自12个波前的访问分别发送到不同的通道,那么效率最高。实现这一点的一种方法是让每个波前访问连续的256 = 64 * 4字节的数据组。请注意,如图2.1所示,连续获取256 * 12字节的数据并不总是会遍历所有通道。
一种低效的访问模式是每个波前都访问所有通道。如果连续的工作项访问具有大的二次幂步长的数据,很可能会发生这种情况。

阅读原始手册以获取更多硬件实现细节,这里省略了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接