CUDA编程!深入剖析静态/动态共享内存与Bank Conflict(附源码)

文章探讨了CUDA编程中的共享内存优化,包括静态和动态共享内存的使用,以及如何通过避免银行冲突来提升矩阵乘法性能。作者还通过示例展示了银行冲突的创建与解决方法,以及推荐的行优先访问和数据对齐策略。
部署运行你感兴趣的模型镜像

点击下方卡片,关注“自动驾驶之心”公众号

ADAS巨卷干货,即可获取

>>点击进入→自动驾驶之心【模型部署】技术交流群  

论文作者 | EasonBob

0. 写在前面

共享内存是模型部署和加速很重要的一环,它决定了优化的效率到底能做到什么程度,主要分为动态和静态共享内存,下面将详细为大家介绍了共享内存及其源码,以及可能的冲突!

1. 共享内存

Input size is 4096 x 4096
matmul in gpu(warmup)                                        uses 102.768669 ms
matmul in gpu(without shared memory)<<<256, 16>>>            uses 101.848831 ms
matmul in gpu(with shared memory(static))<<<256, 16>>>       uses 63.545631 ms

在之前的案例中, 我们把M, N两个矩阵通过cudaMalloc()开辟然后cudaMemcpy()把数据从Host搬到Device上, 这里其实用的是Global Memory, 从图上可以看到的是Global Memory其实很慢, 因为在图中离Threads越近, 他会有一个更高的带宽, 所以在CUDA编程中我们需要更多的去使用L1 Cache和Share Memory。共享内存是每个线程块(block)专用的

b47ae55756c91ad5c34eb7151c8235b5.png

1.1 MatmulSharedStaticKernel()

静态共享内存, 这里的设计是给每一个block设置跟线程数同等大小的共享内存, 最后的P_element跟之前一样还是把全部的block里面计算的都加起来, 这里的思想跟之前一样。唯一的区别就是每一个block访问的内存。

每一个block中, 线程先是从Global Memory(M_device, N_device)中拿到对应的内存去填上共享内存, 全部填完了(同步)之后再从共享内存依次取出来去做对应的计算。

__syncthreads();  这个是跟共享内存绑定的, 这里出现两次, 第一次是每个线程块(block)中的线程首先将一小块(tile)的数据从全局内存(M_device 和 N_device)复制到共享内存。第二次是等待全部计算完成。

M的共享内存往右边遍历, 拿的是行, 这里可以想象成是为了拿到每一行, 也就是在y++的情况下怎么拿到每一行的每一个元素, 用tx和y

M_deviceShared[ty][tx] = M_device[y * width + (m * BLOCKSIZE + tx)];

M的共享内存往下边遍历, 拿的是列, 这里可以想象成是为了拿到每一列, 也就是在x++的情况下拿到每一列的元素, 用tx和y

N_deviceShared[ty][tx] = N_device[(m * BLOCKSIZE + ty)* width + x];

ee092a8d62a7a31a891959cf470c1d66.jpeg

__global__ void MatmulSharedStaticKernel(float *M_device, float *N_device, float *P_device, int width){
    __shared__ float M_deviceShared[BLOCKSIZE][BLOCKSIZE];
    __shared__ float N_deviceShared[BLOCKSIZE][BLOCKSIZE];
    /* 
        对于x和y, 根据blockID, tile大小和threadID进行索引
    */
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    float P_element = 0.0;

    // 这里出现的是block里面的索引, 因为共享内存是block专属的东西
    int ty = threadIdx.y;
    int tx = threadIdx.x;
    /* 对于每一个P的元素,我们只需要循环遍历width / tile_width 次就okay了,这里有点绕,画图理解一下*/
    for (int m = 0; m < width / BLOCKSIZE; m ++) {
        M_deviceShared[ty][tx] = M_device[y * width + (m * BLOCKSIZE + tx)];
        N_deviceShared[ty][tx] = N_device[(m * BLOCKSIZE + ty)* width + x];
        __syncthreads();

        for (int k = 0; k < BLOCKSIZE; k ++) {
            P_element += M_deviceShared[ty][k] * N_deviceShared[k][tx];
        }
        __syncthreads();
    }

    P_device[y * width + x] = P_element;
}

P_device的结果是全部m加起来的结果

1.2 动态共享内存

一般没有什么特殊需求就不要用共享动态内存了,也未必见得会快多少 By 韩导

__global__ void MatmulSharedDynamicKernel(float *M_device, float *N_device, float *P_device, int width, int blockSize){
    /* 
        声明动态共享变量的时候需要加extern,同时需要是一维的 
        注意这里有个坑, 不能够像这样定义: 
            __shared__ float M_deviceShared[];
            __shared__ float N_deviceShared[];
        因为在cuda中定义动态共享变量的话,无论定义多少个他们的地址都是一样的。
        所以如果想要像上面这样使用的话,需要用两个指针分别指向shared memory的不同位置才行
    */

    extern __shared__ float deviceShared[];
    int stride = blockSize * blockSize;
    /* 
        对于x和y, 根据blockID, tile大小和threadID进行索引
    */
    int x = blockIdx.x * blockSize + threadIdx.x;
    int y = blockIdx.y * blockSize + threadIdx.y;

    float P_element = 0.0;

    int ty = threadIdx.y;
    int tx = threadIdx.x;
    /* 对于每一个P的元素,我们只需要循环遍历width / tile_width 次就okay了 */
    for (int m = 0; m < width / blockSize; m ++) {
        deviceShared[ty * blockSize + tx] = M_device[y * width + (m * blockSize + tx)];
        deviceShared[stride + (ty * blockSize + tx)] = N_device[(m * blockSize + ty)* width + x];
        __syncthreads();

        for (int k = 0; k < blockSize; k ++) {
            P_element += deviceShared[ty * blockSize + k] * deviceShared[stride + (k * blockSize + tx)];
        }
        __syncthreads();
    }

    if (y < width && x < width) {
        P_device[y * width + x] = P_element;
    }
}

2. Bank Conflict

使用共享内存的时候可能会遇到的问题

2.1 Bank Conflict

  1. 共享内存的Bank组织

共享内存被组织成若干bank(例如,32或64),每个bank可以在一个时钟周期内服务一个内存访问。因此,理想情况下,如果32个线程(一个warp)访问32个不同的bank中的32个不同的字(word),则所有这些访问可以在一个时钟周期内完成。

  1. 什么是Bank Conflict?

当多个线程在同一个时钟周期中访问同一个bank中的不同字时,就会发生bank conflict。这会导致访问被序列化,增加总的访问时间。例如,如果两个线程访问同一个bank中的两个不同字,则需要两个时钟周期来服务这两个访问。

  1. 如何避免Bank

避免bank conflict的一种策略是通过确保线程访问的内存地址分布在不同的bank上。这可以通过合理的数据布局和访问模式来实现。例如,在矩阵乘法中,可以通过使用共享内存的块来重新排列数据访问模式来减少bank conflicts。

总结 理解和避免bank conflicts是优化CUDA程序的一个重要方面,特别是当使用共享内存来存储频繁访问的数据时。你可以通过修改你的数据访问模式和数据结构来尽量减少bank conflicts,从而提高程序的性能。

2.2 案例

最简单的理解就是之前是[ty][tx] =====> [tx][ty] , 左图是bank conflict, 右图是解决bank conflict的分布

ae332ba5db4ab69300cd791be130a882.png

af3ef37ec319ebd6122694b00c21ed61.png

2.2.1 创造bank conflict
/* 
    使用shared memory把计算一个tile所需要的数据分块存储到访问速度快的memory中
*/
__global__ void MatmulSharedStaticConflictKernel(float *M_device, float *N_device, float *P_device, int width){
    __shared__ float M_deviceShared[BLOCKSIZE][BLOCKSIZE];
    __shared__ float N_deviceShared[BLOCKSIZE][BLOCKSIZE];
    /* 
        对于x和y, 根据blockID, tile大小和threadID进行索引
    */
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    float P_element = 0.0;

    int ty = threadIdx.y;
    int tx = threadIdx.x;
    /* 对于每一个P的元素,我们只需要循环遍历width / tile_width 次就okay了,这里有点绕,画图理解一下*/
    for (int m = 0; m < width / BLOCKSIZE; m ++) {
        /* 这里为了实现bank conflict, 把tx与tx的顺序颠倒,同时索引也改变了*/
        M_deviceShared[tx][ty] = M_device[x * width + (m * BLOCKSIZE + ty)];
        N_deviceShared[tx][ty] = M_device[(m * BLOCKSIZE + tx)* width + y];
        __syncthreads();

        for (int k = 0; k < BLOCKSIZE; k ++) {
            P_element += M_deviceShared[tx][k] * N_deviceShared[k][ty];
        }
        __syncthreads();
    }

    /* 列优先 */
    P_device[x * width + y] = P_element;
}
2.2.2 用pad的方式解决bank conflict
__global__ void MatmulSharedStaticConflictPadKernel(float *M_device, float *N_device, float *P_device, int width){
    /* 添加一个padding,可以防止bank conflict发生,结合图理解一下*/
    __shared__ float M_deviceShared[BLOCKSIZE][BLOCKSIZE + 1];
    __shared__ float N_deviceShared[BLOCKSIZE][BLOCKSIZE + 1];
    /* 
        对于x和y, 根据blockID, tile大小和threadID进行索引
    */
    int x = blockIdx.x * BLOCKSIZE + threadIdx.x;
    int y = blockIdx.y * BLOCKSIZE + threadIdx.y;

    float P_element = 0.0;

    int ty = threadIdx.y;
    int tx = threadIdx.x;
    /* 对于每一个P的元素,我们只需要循环遍历width / tile_width 次就okay了,这里有点绕,画图理解一下*/
    for (int m = 0; m < width / BLOCKSIZE; m ++) {
        /* 这里为了实现bank conflict, 把tx与tx的顺序颠倒,同时索引也改变了*/
        M_deviceShared[tx][ty] = M_device[x * width + (m * BLOCKSIZE + ty)];
        N_deviceShared[tx][ty] = M_device[(m * BLOCKSIZE + tx)* width + y];

        __syncthreads();

        for (int k = 0; k < BLOCKSIZE; k ++) {
            P_element += M_deviceShared[tx][k] * N_deviceShared[k][ty];
        }
        __syncthreads();
    }

    /* 列优先 */
    P_device[x * width + y] = P_element;
}

虽然说

Input size is 4096 x 4096
matmul in gpu(warmup)                                        uses 113.364067 ms
matmul in gpu(general)                                       uses 114.303902 ms
matmul in gpu(shared memory(static))                         uses 73.318878 ms
matmul in gpu(shared memory(static, bank conf))              uses 141.755173 ms
matmul in gpu(shared memory(static, pad resolve bank conf))  uses 107.326782 ms
matmul in gpu(shared memory(dynamic))                        uses 90.047234 ms
matmul in gpu(shared memory(dynamic, bank conf)              uses 191.804550 ms
matmul in gpu(shared memory(dynamic, pad resolve bank conf)) uses 108.733856 ms

在设计核函数时候通过选择合适的数据访问模式来避免bank conflicts是一种常用的优化策略。

在CUDA编程中,通常推荐的做法是:

  1. 行优先访问:因为CUDA的内存是按行优先顺序存储的,所以采用行优先访问可以更好地利用内存带宽,减少bank conflicts。

  2. 合适的数据对齐:通过确保数据结构的对齐也可以减少bank conflicts。例如,可以通过padding来确保矩阵的每行都是一个固定数量的word长。

① 全网独家视频课程

BEV感知、毫米波雷达视觉融合多传感器标定多传感器融合多模态3D目标检测点云3D目标检测目标跟踪Occupancy、cuda与TensorRT模型部署协同感知语义分割、自动驾驶仿真、传感器部署、决策规划、轨迹预测等多个方向学习视频(扫码即可学习

a2bb99a463e1db1b7e939ed6a57c9c6e.png 视频官网:www.zdjszx.com

② 国内首个自动驾驶学习社区

近2000人的交流社区,涉及30+自动驾驶技术栈学习路线,想要了解更多自动驾驶感知(2D检测、分割、2D/3D车道线、BEV感知、3D目标检测、Occupancy、多传感器融合、多传感器标定、目标跟踪、光流估计)、自动驾驶定位建图(SLAM、高精地图、局部在线地图)、自动驾驶规划控制/轨迹预测等领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球,这是一个真正有干货的地方,与领域大佬交流入门、学习、工作、跳槽上的各类难题,日常分享论文+代码+视频,期待交流!

a4816c9f8546928b62d272ad4ebf3aad.png

③【自动驾驶之心】技术交流群

自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、BEV感知、多模态感知、Occupancy、多传感器融合、transformer、大模型、点云处理、端到端自动驾驶、SLAM、光流估计、深度估计、轨迹预测、高精地图、NeRF、规划控制、模型部署落地、自动驾驶仿真测试、产品经理、硬件配置、AI求职交流等方向。扫码添加汽车人助理微信邀请入群,备注:学校/公司+方向+昵称(快速入群方式)

f330a994d6935601b0ada2f61d302082.jpeg

④【自动驾驶之心】平台矩阵,欢迎联系我们!

7803970f15feddc407aa556b90f97413.jpeg

您可能感兴趣的与本文相关的镜像

PyTorch 2.5

PyTorch 2.5

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

<think>好的,我现在需要帮助用户了解CUDA中如何查看共享内存Bank配置及访问模式,以及相关的访问机制和冲突解决方法。首先,我得回忆一下自己之前学过的CUDA相关知识,尤其是关于共享内存Bank冲突的内容。 用户提到了几个引用,我需要先仔细看一下这些引用的内容,确保回答准确。引用[1]和[4]提到共享内存被分为32个banks,每个bank的带宽是4字节,这样每个bank可以同时处理一个线程的访问。引用[2]详细解释了bank conflict的发生条件,比如同一warp中的线程访问同一bank的不同地址会导致冲突,而广播和多播机制可以减少冲突。引用[3]则给出了两个例子,说明不同的访问模式是否会导致bank conflict。 用户的问题主要是如何查看Bank配置和访问模式,以及机制和解决方法。我需要分步骤来回答: 首先,关于查看共享内存Bank配置,可能需要使用CUDA提供的工具,比如nsight compute或者nvprof,这些工具可以分析内核的内存访问模式。另外,在编程时,共享内存bank配置可以通过CUDA的API或者文档来确定,比如计算能力不同的设备可能有不同的bank大小。例如,计算能力2.x及以上的设备默认是4字节的bank,而更早的可能有不同配置。 接下来,访问机制方面,每个bank可以独立响应访问请求,理想情况下每个线程访问不同的bank,这样没有冲突。如果多个线程访问同一个bank的不同地址,就会发生冲突,导致串行访问,降低性能。而如果访问同一地址,会触发广播,不会冲突。 解决方法包括调整内存访问模式,比如将二维数组的行主序改为列主序,或者进行内存填充(padding)来改变地址分布,避免同一warp中的线程访问同一bank。例如,在引用[3]的例子中,kernel2的访问间隔是4,导致每个线程访问同一bank的不同地址,从而引发冲突,而kernel1的间隔是1,没有冲突。这时候可能需要调整索引方式或者添加填充来分散访问。 另外,用户可能需要具体的代码示例或工具使用步骤。例如,使用CUDA的nsight工具来分析bank conflict,或者通过代码修改来避免冲突。例如,在代码中,如果访问共享内存时出现跨步较大的情况,可以通过调整数组的维度或添加填充来改变每个线程访问的bank位置。 需要注意的是,不同计算能力的设备可能有不同的bank配置,这一点需要确认。例如,Fermi架构之后的设备默认是4字节bank,而之前的是不同的。所以查看具体设备的计算能力也很重要。 总结下来,回答的结构应该是先解释bank的配置和访问机制,然后讨论如何查看这些配置(工具和文档),接着讲解冲突的原因和解决方法,最后给出示例或优化策略。需要引用用户提供的资料中的内容,比如引用[2]中的广播机制和bank conflict的条件,引用[3]中的例子说明如何通过调整访问模式来避免冲突。 另外,用户可能还希望了解如何在实际编程中检测和调试bank conflict,这时候可以推荐使用Nsight Compute或者nvprof等工具进行分析,并解释工具的输出结果如何反映冲突情况。 最后,确保回答符合用户的要求:结构清晰,分步骤,引用正确,并且生成相关问题。需要检查是否有遗漏的点,比如bank的数量是32个,每个bank的带宽,以及多播的条件等,都需要准确无误地传达。</think>### CUDA共享内存Bank配置访问机制解析 #### 一、共享内存Bank配置规则 1. **Bank数量带宽** CUDA共享内存被划分为$32$个逻辑Bank线程束大小一致),每个Bank的带宽为$4$字节。例如,在计算能力$\geq 2.0$的设备中,每个Bank可独立响应线程的访问请求[^4]。 2. **Bank地址映射规则** 共享内存地址按以下公式映射到Bank: $$\text{Bank Index} = \left( \frac{\text{字节地址}}{4} \right) \mod 32$$ 这意味着每$32 \times 4 = 128$字节的共享内存会循环分配到32个Bank中。 #### 二、查看Bank配置的方法 1. **计算能力查询** 通过`cudaGetDeviceProperties`函数获取设备的计算能力,例如: ```c++ cudaDeviceProp prop; cudaGetDeviceProperties(&prop, 0); printf("Compute Capability: %d.%d\n", prop.major, prop.minor); ``` 不同计算能力的设备可能对Bank配置有细微差异(如Bank带宽)。 2. **工具分析** 使用Nsight Compute或`nvprof`分析内存访问模式: ```bash nvprof --metrics shared_load_transactions_per_request ./your_program ``` 高`shared_load_transactions_per_request`值可能表明存在Bank冲突[^2]。 #### 三、Bank冲突机制解决方法 1. **冲突条件** - 同一线程束中多个线程访问**同一Bank的不同地址**时发生冲突(例如:二维数组列访问)[^2]。 - **不冲突的情况**: - 所有线程访问同一地址(触发广播)[^2]。 - 访问不同Bank的地址(即使随机访问)。 2. **经典冲突案例** ```c++ __global__ void kernel2() { int tid = threadIdx.x; __shared__ int cache[128]; cache[tid * 4] = 1; // 跨步4,导致32线程访问Bank 0,4,8,...(计算后实际Bank相同) } ``` 此时每个线程访问的地址间隔为$4 \times 4=16$字节,按公式$\frac{16}{4} \mod 32 = 4$,所有线程访问Bank 4,导致32-way冲突[^3]。 3. **优化方法** - **内存填充(Padding)**: 在二维数组行尾添加填充字节,改变地址映射。例如将$32 \times 32$数组改为$33 \times 32$: ```c++ __shared__ int cache[33][32]; // 每行多1个元素作为填充 ``` 使得同一列元素分布在不同的Bank中。 - **调整访问模式**: 将列优先访问改为行优先访问,或使用转置操作。 - **利用广播机制**: 让同一线程束内的线程读取相同地址,例如读取共享内存的某个公共参数。 #### 四、验证优化效果 通过Nsight Compute的`Shared Memory Bank Conflicts`指标可直接观察冲突次数: ```bash ncu --metrics smsp__warp_issue_stalled_shared_mem_bank_conflict.per_opcode ./your_program ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值