CUDA 矩阵转置性能优化实战:cuda-samples transpose 示例的合并访问、Bank Conflict 与 Partition Camping 深入剖析
【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples
矩阵转置是线性代数与高性能计算中的经典算子,也是衡量 GPU 访存优化技巧的"试金石"。本文以 NVIDIA cuda-samples 仓库中 transpose 示例 为核心,系统讲解它从朴素实现到高度优化实现的完整演进路径:全局内存合并访问(coalescing)、共享内存 bank conflict 消除、以及 partition camping 规避等关键技术。读完本文,你将掌握这套"以拷贝为性能上界、逐级优化"的转置性能研究方法论,并能直接复现、扩展该示例的基准测试流程。
示例概览:它在仓库中的位置与目标
该示例位于 cpp/6_Performance/transpose/,归属于仓库的6_Performance(性能专题)目录。目录下共包含三个文件:
| 文件 | 作用 |
|---|---|
| transpose.cu | 全部设备端(device)与主机端(host)代码,内含 8 个 kernel 与完整的计时、验证主程序 |
| CMakeLists.txt | 该示例的 CMake 构建脚本 |
| doc/MatrixTranspose.pdf | NVIDIA 官方性能研究白皮书,对该性能研究有详细描述 |
从源码文件头注释可以确认其研究目标(见 transpose.cu):
该文件同时包含矩阵转置的 device 与 host 代码。它执行多个转置 kernel,通过合并访问、消除共享内存 bank conflict、以及规避 partition camping 逐步提升性能。其中若干 kernel 执行的是"拷贝"操作,用于代表转置所能达到的最佳性能上界。
示例默认对 1024×1024 的单精度浮点矩阵进行转置,通过统一的计时框架,在同一 GPU 上横向对比多种转置策略的有效带宽(GB/s),并与参考解做正确性校验。仓库 6_Performance 目录 README 也将其定位为"展示不同性能实现以达到高性能"的性能研究示例。
为什么矩阵转置是性能难题:访存模式分析
一个常规的矩阵转置B[j][i] = A[i][j]意味着:如果按行主序(row-major)存储,那么读取 A 时按行连续访问(天然合并),但写入 B 时却是按列访问——同一 warp 内 32 个线程写出的地址在全局内存中彼此相隔width个元素,形成典型的未合并访问(uncoalesced access)。
未合并访问会被硬件拆分成多次内存事务,严重浪费显存带宽。这正是转置性能优化要解决的核心矛盾。CUDA 的解法是经典的tile(分块)+ 共享内存(shared memory)转置:先把数据块合并读入共享内存,完成块内转置,再合并写回全局内存。本示例在此基础上,进一步把共享内存自身的 bank conflict 和全局内存分区的 partition camping 也逐一消除。
八个 kernel 的性能演进路线
主程序通过一个for (int k = 0; k < 8; k++)循环依次调度 8 个 kernel(见 transpose.cu)。它们可划分为三组:
1. 拷贝基线:性能上界
转置必须读一次、写一次全部数据,因此"能跑多快"的上界就是同规模纯拷贝的带宽。示例用两个拷贝 kernel 作为参考基准:
copy(simple copy):完全不使用共享内存,直接按行连续读取、连续写入,是全局内存带宽的原始表现。copySharedMem(shared memory copy):数据经共享内存中转后再写回,用于量化"引入共享内存本身的代价"。
转置 kernel 若能逼近这两个拷贝 kernel 的带宽,即认为已达到接近硬件极限的水平。
2. 三个完整转置 kernel:逐步优化
三个 kernel 使用相同的分块策略:每块处理TILE_DIM × TILE_DIM的 tile,用TILE_DIM × BLOCK_ROWS个线程,每个线程处理TILE_DIM/BLOCK_ROWS个元素。代码中定义为(见 transpose.cu):
#define TILE_DIM 32 #define BLOCK_ROWS 16即每块 32×32 tile、32×16 = 512 线程、每线程处理 2 个元素。
transposeNaive(naive):朴素的逐元素转置。读入与写出都未做任何合并优化,是性能最差的下限参照。
transposeCoalesced(coalesced):第一步引入共享内存。线程按合并方式读入数据块存入__shared__ float tile[TILE_DIM][TILE_DIM],同步后用转置后的下标(交换threadIdx.x与threadIdx.y)合并写回全局内存(见 transpose.cu)。这一版解决了全局内存的未合并访问问题,但写共享内存和读共享内存两个阶段都存在bank conflict,尚未达到最优。
transposeNoBankConflicts(optimized):把共享内存数组从[TILE_DIM][TILE_DIM]改为[TILE_DIM][TILE_DIM + 1],即每行多出一个元素的padding(填充),从而错开行首地址,消除共享内存 bank conflict(见 transpose.cu)。这是转置优化中最经典也最有效的一招,后文会详述其原理。
transposeDiagonal(diagonal):在前者基础上再叠加对角块重排(diagonal reordering),用于规避全局内存的 partition camping 现象(见 transpose.cu)。它把blockIdx.x重新解释为"沿对角线方向的距离",blockIdx.y对应不同对角线,通过求模运算把对角坐标映射回笛卡尔坐标。该 kernel 既合并访问、又无 bank conflict、还能缓解 partition camping,是完整转置的最终形态。
3. 两个部分转置 kernel:性能剖析工具
transposeCoarseGrained(coarse-grained)与transposeFineGrained(fine-grained)并不执行完整转置,而是分别只完成"合并读入共享内存"或"共享内存中转置后写回"中的一半工作,用于单独剖析转置两个阶段的性能特征。正因如此,源码注释明确说明它们会无法通过参考解校验(见 transpose.cu),主程序在调度它们时也会跳过正确性比较(见 transpose.cu)。
共享内存 Bank Conflict 与 Padding 技巧的底层原理
GPU 共享内存被划分为 32 个 bank,每个 bank 的带宽为每周期 4 字节。当一个 warp 内的多个线程同时访问同一 bank时,访问会被硬件串行化,这就是 bank conflict。
在transposeCoalesced中,线程写共享内存时,threadIdx.y + i相同的行内线程连续写tile[threadIdx.y+i][threadIdx.x],32 个线程刚好覆盖一行 32 个float,正好映射到 32 个 bank,无冲突;但在读转置后数据时,线程访问tile[threadIdx.x][threadIdx.y+i],此时threadIdx.x相同的线程(每隔 16 个线程,因为BLOCK_ROWS = 16)会访问到同一行,即命中同一 bank,产生 2 路 bank conflict(每 16 个线程冲突一次,共 2 组)。反之,写阶段在另一维度也存在类似冲突。
transposeNoBankConflicts的解法是将数组声明为:
__shared__ float tile[TILE_DIM][TILE_DIM + 1];每行多出的 1 个元素使相邻行的起始地址错开一个 bank,于是本应落在同一 bank 的不同行元素被分散到相邻 bank,冲突被彻底消除。代价仅仅是每块多出TILE_DIM个元素的共享内存,性价比极高。这也是transposeDiagonal继续沿用[TILE_DIM][TILE_DIM + 1]声明的原因。
全局内存 Partition Camping 与对角重排
在较新的 GPU 架构上,全局内存的访问还会受partition camping影响:同一时间片内,若多个 block 的访问恰好集中命中 L2 分区的少数几个分区,会造成局部拥塞,拉低有效带宽。
transposeDiagonal的思路是改变 block 的执行调度顺序:让沿矩阵对角线分布的 block 尽量在同一时间运行,从而把对全局内存分区的压力均匀打散。代码中的核心映射逻辑为(见 transpose.cu):
if (width == height) { blockIdx_y = blockIdx.x; blockIdx_x = (blockIdx.x + blockIdx.y) % gridDim.x; } else { int bid = blockIdx.x + gridDim.x * blockIdx.y; blockIdx_y = bid % gridDim.y; blockIdx_x = ((bid / gridDim.y) + blockIdx_y) % gridDim.x; }对于方形矩阵,直接交换逻辑坐标并取模;对于非方形矩阵,则先把二维块 ID 展平为一维bid再做对角映射。映射之后,kernel 其余部分与transposeNoBankConflicts完全一致,仅需把blockIdx.x/blockIdx.y替换为blockIdx_x/blockIdx_y,改动极小、收益显著。
主程序执行流程:计时、缩放与校验
main()的执行逻辑(见 transpose.cu)可归纳为:
- 解析参数:读取
-device、-dimX、-dimY、-help等命令行选项。 - 查询设备:通过
cudaGetDevice/cudaGetDeviceProperties获取 SM 版本与 SM 数量。 - 计算缩放因子:以每 SM 192 核为基线,按 GPU 实际总核心数计算
scale_factor = max(192.0f / (cores_per_sm * sm_count), 1.0f)(见 transpose.cu),用于在小规模 GPU 上自动缩小测试矩阵,保证 tile 数量可比。每 SM 核心数由 helper_cuda.h 中的_ConvertSMVer2Cores按架构查表得出。 - 计算测试矩阵规模:默认 1024×1024,若指定
-dimX/-dimY则以命令行参数为准;非方形矩阵或尺寸不是TILE_DIM整数倍时会报错退出(见 transpose.cu)。 - 分配与初始化数据:host 侧用
h_idata[i] = (float)i填充;device 侧cudaMalloc后cudaMemcpy上传;同时用纯 CPU 的computeTransposeGold生成参考解(见 transpose.cu)。 - 循环调度 8 个 kernel:每个 kernel 先 warmup 一次(避免启动开销计入计时),再用 CUDA event 计时 100 次重复启动(
NUM_REPS = 100),最后回传结果与参考解比对。 - 逐轮清零
d_odata:在进入下一轮 kernel 前,将输出缓冲区显式清零,防止上一个 kernel 的残留数据造成compareData假阳性(见 transpose.cu)。
值得注意的细节是,该示例使用 CUDAcooperative groups的cg::this_thread_block()与cg::sync(cta)替代传统的__syncthreads()做块内同步,这也是较新 CUDA 编程模型的推荐写法(见 transpose.cu)。
有效带宽的计算方法
计时使用 CUDA event 对start/stop打点,测得NUM_REPS次 kernel 启动的总耗时kernelTime(毫秒),单次耗时取kernelTime / NUM_REPS。有效带宽公式见 transpose.cu:
float kernelBandwidth = 2.0f * 1000.0f * mem_size / (1024 * 1024 * 1024) / (kernelTime / NUM_REPS);其中mem_size = sizeof(float) * size_x * size_y(见 transpose.cu)。系数2.0f表示转置需读一次、写一次共两倍数据量;1000.0f将毫秒换算为秒;除以1024³将字节换算为 GB。最终输出格式为:
transpose simple copy , Throughput = 870.1234 GB/s, Time = 0.00921 ms, Size = 1048576 fp32 elements, NumDevsUsed = 1, Workgroup = 512输出中Workgroup = 512即TILE_DIM × BLOCK_ROWS = 32 × 16的线程块规模。通过对比不同 kernel 的 GB/s 数值,可以直观看到从 naive 到 diagonal 的逐级提速幅度。
正确性验证机制
每个完整转置 kernel 的结果都会通过cudaMemcpy回传 host,与computeTransposeGold生成的参考解比对。比对函数是 helper_image.h 中的compareData,采用绝对误差阈值(epsilon = 0.01)逐元素校验;copy 类 kernel 的参考解就是输入数据本身,coarse/fine-grained 因非完整转置而跳过校验(见 transpose.cu)。任一 kernel 校验失败都会打印*** xxx kernel FAILED ***并将success置为 false,最终决定进程以Test passed或Test failed退出。
命令行参数
运行transpose可执行文件时支持以下选项(见 transpose.cu):
| 参数 | 含义 | 默认值 |
|---|---|---|
-device=n | 指定使用的 GPU 设备编号(n = 0, 1, 2…) | 由findCudaDevice自动选择 |
-dimX=row_dim_size | 矩阵行维度(x 方向大小) | 由 GPU 规模自动推导,上限约 1024 |
-dimY=col_dim_size | 矩阵列维度(y 方向大小) | 与dimX相同 |
-help | 打印上述帮助信息后退出 | — |
约束条件:矩阵必须是方形(size_x == size_y)且尺寸必须为TILE_DIM = 32的整数倍,否则程序直接报错退出(见 transpose.cu)。此外,若2 * mem_size超过设备显存也会被拒绝(见 transpose.cu)。
构建与运行
该示例通过 CMake 构建。其 CMakeLists.txt 要求 CMake 3.20 及以上,默认编译架构覆盖75 80 86 87 89 90 100 110 120(对应从 Volta 到 Blackwell 的 SM 架构),默认开启-lineinfo(便于调试工具定位行号),ENABLE_CUDA_DEBUG时可切换为-G以支持 cuda-gdb 调试,并启用了CUDA_SEPARABLE_COMPILATION与 C++17/CUDA 17 标准。
在 Linux 上,可按仓库主 README 的构建说明 从任意子目录构建该示例:
mkdir build && cd build cmake .. make -j$(nproc)编译产物位于build/bin/${TARGET_ARCH}/${TARGET_OS}/${BUILD_TYPE}目录(Linux x86_64 Release 对应build/bin/x64/linux/release,见 README.md)。随后运行:
./transpose # 默认 1024x1024 矩阵 ./transpose -dimX=512 -dimY=512 # 指定矩阵规模 ./transpose -device=0 -dimX=2048 -dimY=2048Windows 下则使用 Visual Studio 的x64 Native Tools Command Prompt for VS,以cmake .. -G "Visual Studio 16 2019" -A x64配置并生成解决方案后编译运行(见 README.md)。
支持环境一览
根据示例 README.md:
- 支持的 SM 架构:SM 5.0 / 5.2 / 5.3 / 6.0 / 6.1 / 7.0 / 7.2 / 7.5 / 8.0 / 8.6 / 8.7 / 8.9 / 9.0(覆盖 Maxwell 至 Hopper/Blackwell 之前的各代架构);
- 支持的操作系统:Linux、Windows;
- 支持的 CPU 架构:x86_64、armv7l;
- 前置条件:安装对应平台的 CUDA Toolkit(示例 README 中的官方指引链接);
- 涉及的 CUDA Runtime API:
cudaMemcpy、cudaMalloc、cudaFree、cudaGetLastError、cudaEventSynchronize、cudaEventRecord、cudaGetDevice、cudaEventDestroy、cudaEventElapsedTime、cudaGetDeviceProperties、cudaEventCreate,全部与 transpose.cu 中的调用一一对应。
进一步阅读
该示例配套的官方白皮书 doc/MatrixTranspose.pdf 对上述性能研究(合并访问、bank conflict、partition camping 及对角重排)有更详尽的数值分析与硬件原理阐述,是深入理解本示例的最佳延伸资料。若想横向了解仓库中其他性能优化主题,可继续阅读 cpp/6_Performance/README.md 中列出的 alignedTypes、UnifiedMemoryPerf、cudaGraphsPerfScaling 等姊妹示例。
【免费下载链接】cuda-samplesSamples for CUDA Developers which demonstrates features in CUDA Toolkit项目地址: https://gitcode.com/GitHub_Trending/cu/cuda-samples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考