无法实现最大共享内存带宽

Cannot achieve max shared memory bandwith

提问人:Saitama10000 提问时间:11/17/2023 更新时间:11/20/2023 访问量:85

问:

我正在使用 RTX 2080 max-q 移动版,计算能力 7.5。我试图理解为什么我无法实现 32 位/周期的共享内存带宽:

从 https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#shared-memory-5-x

共享内存有 32 个存储体,这些存储体的组织方式是连续的 32 位字映射到连续的存储体。每个 bank 的带宽为每时钟周期 32 位

我写了这个测试代码:

#include <iostream>
#include <algorithm>
#include <numeric>

using T = float;
extern __shared__ T bank[];

constexpr int warps = 8;
constexpr int pitch = 32 * warps;
constexpr int size = 32;

__managed__ long long starts[pitch];
__managed__ long long stops[pitch];
__managed__ long long clocks[pitch];

__global__ void kernel()
{
    auto* local_bank = bank + threadIdx.x;
    auto* a = local_bank;
    auto* b = a + size * pitch;

    __syncwarp();
    auto start = clock64();
    __syncwarp();

    for (int i = 0; i < size; i++)
        b[i * pitch] = a[i * pitch];

    __syncwarp();
    auto stop = clock64();
    __syncwarp();

    auto duration = stop - start;
    printf("%5lld %s", duration, threadIdx.x % 32 == 31 ? "\n" : ""); 
    __syncthreads();
    starts[threadIdx.x + blockDim.x *blockIdx.x] = start;
    stops[threadIdx.x + blockDim.x *blockIdx.x] = stop;
    clocks[threadIdx.x + blockDim.x *blockIdx.x] = duration;
}

int main()
{
    cudaDeviceSetLimit(cudaLimitStackSize, 64 * 1024);
    cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, 64*1024);
    cudaFuncSetCacheConfig(kernel, cudaFuncCachePreferShared);
    kernel<<<1, pitch, 2 * size * pitch * sizeof(T)>>>();
    cudaDeviceSynchronize();    

    auto min_clock = *std::min_element(std::begin(clocks), std::end(clocks));
    auto min_start = *std::min_element(std::begin(starts), std::end(starts));
    auto max_stop = *std::max_element(std::begin(stops), std::end(stops));
    auto avg_clock = std::accumulate(std::begin(clocks), std::end(clocks), 0) / (float)(std::end(clocks) - std::begin(clocks));
    auto avg_clock_per_access = avg_clock / (float)(warps * size * 2);
    printf("min = %lli\n", min_clock);
    printf("max = %lli\n", max_stop - min_start);
    printf("avg = %f\n", avg_clock_per_access);
}

它以时钟周期为单位测量每个线程的共享内存复制的持续时间,并输出共享内存访问所花费的平均周期数。我有 2 个周期,应该是 1 个周期。我不明白我做错了什么。

注意:我尝试过使用 float4,得到了相同的结果。您可以将类型 T 更改为 float4 并将大小减小到 8,以便使用不超过 64kb 的共享内存

库达

评论

1赞 Sebastian 11/19/2023
在经过基准测试的代码中使用 printf 可能会影响您的结果。您是否在发布模式下编译?所有的经线是在同一时间开始和停止,还是一个接一个地运行更多?你能尝试将经线的数量从 8 个增加到 32 个吗?您能否增加共享内存上的操作数以使线程保持更长时间的繁忙状态?
0赞 Saitama10000 11/19/2023
@Sebastian我已经尝试了所有方法,除了“增加共享内存上的操作次数以保持线程长时间忙碌”之外。如果我在它们周围循环,访问就会被优化为一次迭代。我必须想办法按照你的建议去做。
0赞 Sebastian 11/19/2023
您可以在每次迭代后尝试 syncwarp 或 syncthreads。你看过 Compute Nsight 吗?它显示了共享内存(以及其他)在一个很好的 GUI 中的使用情况。
1赞 Sebastian 11/20/2023
根据剖析图灵架构论文,共享内存的最大带宽是频率 x SM 数量 x 16 通道 x 32 位。因此,访问 32 位需要两个时钟周期(见表 3.1)。“我们在 T4 GPU 上测量的实际带宽为 58.83 字节/周期/SM”,这相当于 2 个周期的完整 128 字节 = 32*32 位访问。
1赞 Sebastian 11/20/2023
researchgate.net/publication/ 确认...:“然而,每一代SM的SMEM带宽都保持不变,甚至图灵一代减少了一半。因此,与 7.0 相比,7.5 中的共享内存速度较慢。剖析论文中的 TU104 是 7.5。您提到的 Nvidia 编程指南文档专注于每种架构的旗舰产品。

答:

1赞 Sebastian 11/20/2023 #1

在以下论文中记录,在考虑 SM 的频率和数量之前,图灵 GPU (CC 7.5) 的共享内存带宽是 Volta (7.0) 或 Ampere (8.x) 值的一半。

请参阅 https://arxiv.org/abs/1903.07486(通过微基准测试剖析 NVidia Turing T4 GPU)和 https://ieeexplore.ieee.org/abstract/document/9893362(Tensor Core 内存层次结构的未来扩展以及使用扭曲间多播消除冗余共享内存流量)。

使用图灵(计算能力 7.5)时,共享内存带宽为 16 通道 * 32 位/周期/SM。因此,32 位访问需要 2 个周期。在 Volta 和 Ampere 上,它们需要 1 个周期。

请注意,这是每个 SM 而不是每个 SM 分区。因此,计算速度比共享内存带宽快得多。

全局内存带宽要求和计算要求较低,但使用大量共享内存的内核很容易受到共享内存带宽的限制。在这种情况下,尝试在线程之间更好地分配计算,或者使用寄存器在线程中缓存数据,并具有潜在的展开循环,以便能够访问寄存器中的数组,其索引在编译时是已知的。

多年来,通过引入和改进 L1/L2 缓存以及不增加共享内存带宽,共享内存(甚至更多的纹理访问)对高性能和架构的重要性已经降低。

共享内存仍然提供了更好的控制,与L1缓存相比,保留了哪些数据,更好的随机访问,而不需要合并,只要可以通过巧妙的数据布局和访问模式来避免银行冲突。此外,随机播放指令也得到了改进,因为它们使用共享内存路由硬件,并且如果唯一的任务是在 warps 内共享一次性数据,则无需存储 + 加载或保留共享内存。此外,Ampere 引入的异步数据加载指令对于使用共享内存也有很大的改进。