做过几年GPU高性能计算的人应该都有这种感觉:矩阵乘法是你绕不开的第一道坎,也是最能给你成就感的那个坎。刚开始接触CUDA的时候,我照着教程写了一个最朴素的矩阵乘法kernel,一跑发现确实比CPU快了不少,可等打开Nsight Compute一看性能数据,又觉得自己写了个寂寞——内存吞吐低得可怜,SM利用率更是不忍直视。后来花了几周时间把分块、共享内存、向量化、循环展开这些优化手段逐个试了一遍,性能才真正有了质的变化。这个过程里踩过的坑、调过的参数、用过的工具,我觉得很值得拿出来聊一聊。
这篇文章就从一个完整的实战角度,带你走一遍基于CUDA的并行矩阵乘法优化全流程:从环境搭建到朴素实现,从共享内存分块到性能剖析,每一步我都会讲清楚为什么这么做、数据上有什么变化。如果你刚开始学CUDA编程,或者已经在写kernel但不知道该怎么优化,这篇文章应该能给你一套可以直接照搬的优化思路和排查手段。
1. 为什么选择矩阵乘法切入CUDA优化
1.1 矩阵乘法在高性能计算中的分量
先说个大家都有体感的事实:矩阵乘法(GEMM,General Matrix Multiply)几乎撑起了现代计算生态的半边天。深度学习里的全连接层、卷积操作,底层都要落到GEMM;科学计算里的线性代数求解器、有限元分析、流体仿真,核心热区也全是矩阵乘法;就连图像处理里的各种变换操作,本质上也离不开它。领域里流行一句话:优化好GEMM,你就优化好了半个HPC世界。
从计算本身来看,矩阵乘法天然具备极高的并行潜力。一个 M×K 的矩阵 A 和一个 K×N 的矩阵 B 相乘,结果是 M×N 的矩阵 C,整个过程要做 MNK 次乘加运算,也就是 2MN*K 次浮点操作。而输出元素之间彼此独立——C[i][j] 只依赖 A 的第 i 行和 B 的第 j 列,没有任何写冲突。这种"输出多、依赖少、并行粒度大"的特征,正好是GPU最擅长的场景。
更重要的是,矩阵乘法的计算强度很高。计算强度 = 浮点操作次数 / 访存字节数。以 K 较大的矩阵乘法为例,做一次乘加需要从A读一个float、从B读一个float、往C写一个float(共12字节),但只产生2次浮点操作,计算强度大约只有0.17 FLOP/byte。如果不做任何优化,这个计算强度远低于GPU的理论平衡点,最终性能会被访存卡死。这就逼着你去思考:怎么复用数据、怎么利用片上存储,而这些思考恰恰是CUDA优化最核心的能力训练。
1.2 GPU架构核心概念速览
在动手写代码之前,我强烈建议先把GPU的硬件模型弄明白,不然很多性能问题你根本不知道往哪想。
先看最宏观的对比。CPU芯片上晶体管大量分配给控制逻辑和缓存,核心数量少但单核能力强,适合处理复杂的控制流;GPU则把晶体管几乎全放在了计算单元上,用几千个简单的CUDA Core以量取胜,适合大规模数据并行计算。
在NVIDIA GPU里,最基本的物理计算单元是流多处理器(SM,Streaming Multiprocessor)。SM内部包含若干CUDA Core、共享内存、寄存器文件和调度器。当你启动一个kernel时,线程会被组织成网格(Grid)和线程块(Block),块内的线程会进一步每32个分成一组,叫作一个Warp——这是GPU执行和调度的最小单位。Warp中的所有线程在同一时刻执行同一条指令,如果有线程走了不同分支,就会出现分支发散,多条路径串行执行,白白浪费算力。
内存层次也是理解优化手段的关键。全局内存(Global Memory)容量最大,但延迟通常有几百个时钟周期;共享内存(Shared Memory)位于SM内部,容量只有几十KB,但延迟极低,和寄存器差不多;寄存器(Register)是最快的存储,但数量有限且只属于单个线程。优化矩阵乘法的核心思路之一,就是尽量把数据从慢速的全局内存挪到快速的共享内存或寄存器里,降低重复访存开销。
还有一个基础概念叫合并访问(Coalesced Access)。当同一个Warp内32个线程同时访问全局内存时,如果地址是连续的一段,硬件会把这些访问合并成少数几个内存事务,一次搞定;如果地址是跳着来的,就要拆成多个事务,访存效率急剧下降。这个概念在第4章会反复用到,先记住它。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工具链选型
2.1 CUDA版本与驱动关系的坑
开始写代码之前,得先把开发环境搞利索。这一步看着简单,实际坑不少,尤其是版本对应关系。CUDA有两个关键组件:显卡驱动(Driver)和CUDA Toolkit。Toolkit里包含编译器nvcc、CUDA运行时库、各种开发工具,而驱动是运行CUDA程序的底层支撑。你装的是CUDA 12.x,驱动版本必须不低于该版本要求的最低值,否则程序跑起来会直接报"CUDA driver version is insufficient"之类的错误。
如果你在Linux服务器上工作,还可能会遇到多版本CUDA并存的情况。不同项目可能依赖不同版本,比如旧代码用了CUDA 10的API,新框架又要求CUDA 12。我的做法是安装多个Toolkit到不同目录,通过修改环境变量切换版本:
bash复制export PATH=/usr/local/cuda-12.4/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH
export CUDA_HOME=/usr/local/cuda-12.4
用软链接 /usr/local/cuda 指向当前要用的版本目录,切换的时候只改软链接或环境变量就行,不用反复重装。这个方案我用了很久,很稳。
Windows用户如果有WSL2环境,同样可以装CUDA。WSL2里装CUDA Toolkit时,驱动是Windows侧提供的,Linux里只需要装Toolkit本身。微软和NVIDIA对这条路支持得已经很成熟了,跑PyTorch、TensorFlow的GPU版本都没问题。要注意的是WSL2里nvidia-smi和Windows侧的驱动是同一个,版本以Windows侧为准,而nvcc的版本看Linux里的Toolkit。
2.2 性能剖析工具的选型与配合
优化CUDA程序,光靠肉眼和预感是不行的,必须用profile工具拿数据说话。这里有个重要变化:老牌的nvprof在CUDA 10之后被标记为弃用,CUDA 12中已经完全移除了,现在官方主推的是NVIDIA Nsight Compute(简称ncu)和Nsight Systems(nsys)。
这两个工具分工不一样。Nsight Systems负责系统级剖析,看kernel占用时间占比、CPU和GPU的同步开销、内存拷贝耗时;Nsight Compute则深入kernel内部,给出SM占用率、访存吞吐、warp状态、指令分析等等微观指标。做矩阵乘法优化,ncu才是主力工具。
用ncu对某个kernel做详细分析,命令很简单:
bash复制ncu --set full ./matmul
这条命令会输出大量指标。对初学者来说,我最推荐先看几个关键项:Duration(内核执行时间)、Achieved Occupancy(实际达到的SM占用率)、SM Efficiency(SM利用率)、DRAM Throughput(显存带宽利用情况)、Compute Throughput(计算吞吐)。通过对比"计算吞吐"和"访存吞吐"哪个更接近上限,就能确定你的kernel是计算密集型还是访存密集型,从而决定优化方向。
如果你的CUDA环境里ncu无法正常工作,先用nvidia-smi确认驱动是否认得到GPU和CUDA版本。很多时候排查到最后,问题不是代码,而是驱动和Toolkit版本不匹配。
