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

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

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

<think>好的,我现在需要回答用户关于如何检测CUDA核函数中共享内存bank冲突的问题。首先,我需要回忆一下自己之前学过的CUDA相关知识,尤其是共享内存bank冲突的部分。 共享内存CUDA中每个线程块(block)内线程共享的一块高速内存,访问速度比全局内存快很多。但共享内存被分成多个bank,每个bank在同一时间只能被一个线程访问。如果多个线程同时访问同一个bank的不同地址,就会导致bank冲突,这会显著降低内存访问效率,甚至可能比全局内存还慢。 用户的问题是检测这些bank冲突的方法。根据之前的资料,比如引用[3]和[4],bank冲突通常发生在多个线程同时访问同一bank的情况下。比如,当线程访问共享内存数组的同一列时,如果内存布局没有适当调整,就会导致bank冲突。例如,在二维共享内存数组中,如果数组的宽度是bank数量的整数倍,那么相邻行的同一列会落在同一个bank里,从而导致冲突。解决方法通常是在数组声明时增加一个padding,例如将宽度从TILE_DIM调整为TILE_DIM + 1,这样每一行的起始bank不同,避免了冲突,如引用[1]中提到的。 现在,如何检测这些冲突呢?用户可能想知道具体步骤或工具。首先,我记得CUDA提供了性能分析工具,比如Nsight Compute和Nsight Systems,这些工具可以帮助分析核函数的性能,包括共享内存的访问模式。另外,通过计算线程访问的地址和bank索引,可以手动检查是否存在多个线程同时访问同一bank的情况。 具体来说,每个共享内存地址对应的bank索引可以通过地址计算得到。bank的数量通常是32(在大多数NVIDIA GPU中),所以bank索引的计算公式一般是(字节地址 / 4) % 32,因为每个bank的宽度是4字节。如果多个线程在同一时钟周期内访问同一个bank的不同地址,就会发生bank冲突。比如,如果线程0访问地址0(bank 0),线程1访问地址4(bank 1),依此类推,这样就没有冲突。但如果线程0和线程16都访问bank 0的不同地址,就会产生冲突,如引用[3]中的例子。 此外,使用CUDA的nvprof工具(现在可能被Nsight取代)可以分析共享内存bank冲突次数。在运行性能分析时,可以查看相关指标,比如shared_load_transactions_per_request和shared_store_transactions_per_request,这些指标如果大于1,说明存在bank冲突,因为需要多次事务来处理请求。 总结一下,检测方法可能包括: 1. 使用性能分析工具(Nsight Compute、nvprof)查看bank冲突指标。 2. 手动计算线程访问的bank索引,检查是否存在多个线程访问同一bank。 3. 代码审查,检查共享内存的声明和使用方式,比如是否有适当的padding来避免冲突。 另外,用户可能需要具体的步骤或示例代码来理解如何应用这些方法。例如,可以给出一个简单的核函数,并展示如何调整共享内存的声明来避免冲突,或者如何使用工具输出相关指标。 需要注意的是,不同的GPU架构可能有不同的bank数量和配置,因此需要根据目标GPU的具体情况调整分析方法。例如,某些架构可能有不同的bank大小或数量,这可能影响bank索引的计算方式。 最后,确保回答结构清晰,分点说明方法,并引用相关的资料来支持每个检测方法,比如引用[3]中的例子和引用[4]中提到的bank conflict的影响。</think>在CUDA核函数中检测共享内存bank冲突,可通过以下方法实现: --- ### 1. **使用性能分析工具(Nsight Compute/nvprof)** 通过CUDA官方工具直接获取bank冲突指标: - 使用`nvprof`命令行工具: ```bash nvprof --metrics shared_load_transactions_per_request,shared_store_transactions_per_request ./your_program ``` - 若结果值 **>1**,表示存在bank冲突[^4]。 - 使用Nsight Compute的GUI工具: 在"Memory Workload Analysis"部分查看共享内存事务的bank冲突统计(如`Shared Bank/Channel Conflicts`指标)。 --- ### 2. **手动计算bank索引** 共享内存bank索引公式为: $$ \text{Bank Index} = \left( \frac{\text{字节地址}}{4} \right) \mod 32 $$ (假设GPU为32个bank,每个bank宽度4字节) **步骤**: 1. 分析线程访问共享内存的地址计算逻辑。 2. 检查同一时刻多线程访问的bank索引是否重复。 **示例**: 若线程0访问地址`0`(bank 0),线程1访问地址`128`(bank 0),则触发冲突[^3]。 --- ### 3. **代码审查内存布局优化** - **检查共享内存声明**: 若使用二维数组,需添加**padding**调整列宽: ```cpp __shared__ float tile[TILE_SIZE][TILE_SIZE + 1]; // +1 避免列访问冲突[^1] ``` - **验证访问模式**: 确保线程访问的共享内存地址在连续维度上**跨度不等于bank数量倍数**。例如,避免跨度为32的整数倍(如`threadIdx.x * 32`)。 --- ### 4. **使用调试输出验证** 在核函数中添加打印语句(仅限调试): ```cpp int byte_address = &shared_array[y][x] - &shared_array[0][0]; int bank_index = (byte_address / 4) % 32; printf("Thread %d: Bank %d\n", threadIdx.x, bank_index); ``` 通过输出验证不同线程的bank索引是否重叠。 --- ### 5. **性能对比测试** 对比优化前后的核函数执行时间: - 若添加padding后速度显著提升,说明原代码存在bank冲突[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值