最小化 64 位线程分离共享内存的存储体冲突的策略
Strategy for minimizing bank conflicts for 64-bit thread-separate shared memory
假设我在 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 等。这将所有我的数据在同一家银行,这也不同于其他车道的银行。太好了。
但是现在 - 如果 sizeof(T) == 8
怎么办?我应该如何放置和访问我的数据以最大程度地减少银行冲突(在不了解索引的情况下)?
注意:假设 T 是普通旧数据。如果这样可以使您的答案更简单,您甚至可以假设它是一个数字。
在开普勒 GPU 上,这有一个简单的解决方案:只需更改组大小! Kepler 支持动态地将共享内存组大小设置为 8 而不是 4。但遗憾的是,该功能在后来的微体系结构(例如 Maxwell、Pascal)中不可用。
现在,这是针对最新 CUDA 微体系结构的丑陋且次优的答案:将 64 位情况减少为 32 位情况。
- 它存储 2N 个值,而不是每个线程存储
T
类型的 N 个值,每个连续对是 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);
反写同理
如评论所述,最好进行64位访问,如中所述。
tl;dr:使用与 32 位值相同的交错。
在晚于开普勒的微架构(直到 Volta)上,我们理论上可以获得的最好结果是 2 个共享内存事务,用于完整的 warp 读取单个 64 位值(因为单个事务提供 32 位最多到每条车道)。
这实际上可以通过为 32 位数据描述的类似放置模式 OP 来实现。也就是说,对于 T* arr
,让泳道 i
读取第 idx
个元素作为 T[idx + i * 32]
。这将编译以便发生两个事务:
- 下 16 条通道从 T 中的前 32*4 个字节获取数据(利用所有组)
- 高16位从T中连续的32*4字节获取数据(利用所有bank)
因此 GPU smarter/more 比尝试分别为每个通道获取 4 个字节更灵活。这意味着它可以比早期答案提出的简单的“将 T 分成两半”的想法做得更好。
(此答案基于@RobertCrovella 的评论。)
假设我在 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 等。这将所有我的数据在同一家银行,这也不同于其他车道的银行。太好了。
但是现在 - 如果 sizeof(T) == 8
怎么办?我应该如何放置和访问我的数据以最大程度地减少银行冲突(在不了解索引的情况下)?
注意:假设 T 是普通旧数据。如果这样可以使您的答案更简单,您甚至可以假设它是一个数字。
在开普勒 GPU 上,这有一个简单的解决方案:只需更改组大小! Kepler 支持动态地将共享内存组大小设置为 8 而不是 4。但遗憾的是,该功能在后来的微体系结构(例如 Maxwell、Pascal)中不可用。
现在,这是针对最新 CUDA 微体系结构的丑陋且次优的答案:将 64 位情况减少为 32 位情况。
- 它存储 2N 个值,而不是每个线程存储
T
类型的 N 个值,每个连续对是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);
反写同理
如评论所述,最好进行64位访问,如
tl;dr:使用与 32 位值相同的交错。
在晚于开普勒的微架构(直到 Volta)上,我们理论上可以获得的最好结果是 2 个共享内存事务,用于完整的 warp 读取单个 64 位值(因为单个事务提供 32 位最多到每条车道)。
这实际上可以通过为 32 位数据描述的类似放置模式 OP 来实现。也就是说,对于 T* arr
,让泳道 i
读取第 idx
个元素作为 T[idx + i * 32]
。这将编译以便发生两个事务:
- 下 16 条通道从 T 中的前 32*4 个字节获取数据(利用所有组)
- 高16位从T中连续的32*4字节获取数据(利用所有bank)
因此 GPU smarter/more 比尝试分别为每个通道获取 4 个字节更灵活。这意味着它可以比早期答案提出的简单的“将 T 分成两半”的想法做得更好。
(此答案基于@RobertCrovella 的评论。)