最小化 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]。这将编译以便发生两个事务:

  1. 下 16 条通道从 T 中的前 32*4 个字节获取数据(利用所有组)
  2. 高16位从T中连续的32*4字节获取数据(利用所有bank)

因此 GPU smarter/more 比尝试分别为每个通道获取 4 个字节更灵活。这意味着它可以比早期答案提出的简单的“将 T 分成两半”的想法做得更好。

(此答案基于@RobertCrovella 的评论。)