64位线程分离共享内存最小化bank冲突的策略
Posted
技术标签:
【中文标题】64位线程分离共享内存最小化bank冲突的策略【英文标题】:Strategy for minimizing bank conflicts for 64-bit thread-separate shared memory 【发布时间】:2018-11-20 02:17:25 【问题描述】:假设我在一个 CUDA 块中有一个完整的线程扭曲,并且每个线程都旨在与驻留在共享内存中的 T 类型的 N 个元素一起工作(所以我们总共有 warp_size * N = 32 N 个元素)。不同的线程从不访问彼此的数据。 (嗯,他们这样做,但在稍后阶段,我们在这里不关心)。这种访问将在循环中发生,如下所示:
for(int i = 0; i < big_number; i++)
auto thread_idx = determine_thread_index_into_its_own_array();
T value = calculate_value();
write_to_own_shmem(thread_idx, value);
现在,不同的线程可能各自有不同的索引,或者相同——我不会以这种或那种方式做出任何假设。但我确实想尽量减少共享内存库冲突。
如果sizeof(T) == 4
,那么这很容易:只需将线程 i 的所有数据放在共享内存地址 i、32+i、64+i、96+i 等。这会将 i 的所有数据放在同一家银行,这也与另一条车道的银行不同。太好了。
但是现在 - 如果sizeof(T) == 8
怎么办?我应该如何放置和访问我的数据以最大程度地减少银行冲突(对指数一无所知)?
注意:假设 T 是普通旧数据。如果这能让你的答案更简单,你甚至可以假设它是一个数字。
【问题讨论】:
【参考方案1】:tl;dr:使用与 32 位值相同类型的交织。
在晚于 Kepler 的微架构(高达 Volta)上,理论上我们能得到的最好结果是 2 个共享内存事务,用于读取单个 64 位值的完整 warp(因为单个事务为每个事务提供 32 位最多车道)。
这在实践中可以通过为 32 位数据描述的类似布局模式 OP 来实现。也就是说,对于T* arr
,让通道i
将idx
'th 元素读取为T[idx + i * 32]
。这将编译,以便发生两个事务:
-
较低的 16 个通道从 T 中的前 32*4 字节获取数据(利用所有存储体)
较高的 16 位从 T 中的连续 32*4 字节获取数据(利用所有存储体)
因此,GPU 比尝试分别为每个通道获取 4 个字节更智能/更灵活。这意味着它可以比之前提出的简单的“将 T 分成两半”的想法做得更好。
(此答案基于@RobertCrovella 的 cmets。)
【讨论】:
【参考方案2】:在 Kepler GPU 上,这有一个简单的解决方案:只需更改存储库大小! Kepler 支持动态地将共享内存库大小设置为 8 而不是 4。但可惜的是,该功能在后来的微架构(例如 Maxwell、Pascal)中不可用。
现在,对于最近的 CUDA 微架构,这是一个丑陋且次优的答案:将 64 位大小写为 32 位大小写。
不是每个线程存储 N 个T
类型的值,而是存储 2N 个值,每个连续的对是 T
的低 32 位和高 32 位。
要访问 64 位值,需要进行 2 次半-T
访问,T
由类似 `
uint64_t joined =
reinterpret_cast<uint32_t&>(&upper_half) << 32 +
reinterpret_cast<uint32_t&>(&lower_half);
auto& my_t_value = reinterpret_cast<T&>(&joined);
写的时候反过来也一样。
正如 cmets 建议的那样,最好进行 64 位访问,如 this answer 中所述。
【讨论】:
你关于开普勒的说法是正确的。在其他架构上,只需按照您通常的方式存储和访问数据。您的构造没有任何价值。 Kepler 上存在 8 字节 bank 模式而不是其他模式的原因是 Kepler 为每个 bank 每个周期提供 64 位吞吐量。其他人则不是这样(阅读the programming guide)。每个周期每个存储库 32 位,典型的全扭曲访问将分为 8 个字节数量的 2 个事务;你的构造并没有改进。 @RobertCrovella:但没有“通常”——我可以选择最好的。现在,我意识到我不会在每个周期中获得每个银行 64 位,但是使用我的方法,我应该在每个周期每个银行至少获得 32 位,而在发生冲突时,我担心由于银行冲突,我会得到更少的数据 - 因为每个通道必须访问 2 个 bank(如果我们尊重对齐,访问是 all-even-banks,然后是 all-odd-banks)。 你的构造不会移除任何银行冲突效应。如果两个线程碰巧访问了相同 2 个 bank 中的 8 字节数量(正确对齐的 8 字节数量总是占用两个 32 位 bank),您的代码将无法对其进行排序。此外,它还增加了一些不必要的工作,例如轮班和加班。 warp 中的前 16 个线程将占用所有 32 个 32 位银行,这将是一个事务。 warp 中的第二个 16 个线程也将占用所有 32 个 32 位银行,这将是第二个事务。对于给定的线程,从银行 2*i 和 2*i+1 的读取将发生在在同一个事务中。这也不意味着任何形式的银行冲突。 64 位访问是 1 条指令导致两个事务。在 Maxwell/Pascal 上,这是 2 个周期的延迟损失。 2 个 32 位访问是 2 条指令,导致两个事务,最好的情况是 2 个周期延迟。由于您发出两条指令,因此您正在使用另一个经线可能使用的额外发出周期。在 Maxwell - Volta 上,64 位操作是更好的选择。以上是关于64位线程分离共享内存最小化bank冲突的策略的主要内容,如果未能解决你的问题,请参考以下文章