为什么你的 CUDA kernel 写对了,但 GPU 还是跑不满?|Kerminal工程笔记

📅 2026/7/3 11:59:30 👁️ 阅读次数 📝 编程学习
为什么你的 CUDA kernel 写对了,但 GPU 还是跑不满?|Kerminal工程笔记

写在前面

一个 kernel 为什么慢,优化前应该先看什么,以及一次 kernel 改动怎样才算在工程里真正成立?

对 Kerminal 来说,kernel 优化不是生成一段代码,而是进入真实工程上下文,理解代码、定位瓶颈、修改实现,并通过编译、测试和性能验证完成闭环。

今天,我们先从最基础、也最容易被忽略的问题开始聊聊:数据到底是怎么搬的。

很多 CUDA kernel 在功能上看起来都没有问题。

代码能跑,结果正确,也没有报错。

但一旦开始看性能,就会发现一个很典型的现象:GPU 的带宽没有被真正打满

这类问题在 CUDA 开发里非常常见。它往往不是因为计算太复杂,也不是因为 GPU 算不动,而是因为数据在 global memory 里的访问方式不够友好。

换句话说,问题不一定出在“算”,而可能出在“搬”。

01 一个典型例子:矩阵转置

我们看一个最经典的例子:矩阵转置。

假设输入矩阵是 height × width,输出矩阵是 width × height。一个直接的 CUDA 写法可能长这样:

这段代码逻辑没有问题。输入位置是 (y, x),输出位置是 (x, y),确实完成了转置。

但性能问题也正藏在这里。

02读是顺的,写是不顺的

先看读取:

在同一个 warp 里,线程的 x 通常是连续变化的,而 y 基本保持不变。也就是说,这些线程会去读取一段连续的 global memory 地址。

这种访问模式对 GPU 来说是比较友好的。

内存请求可以更容易合并,带宽利用率也比较高。

再看写入:

问题出现了。

同一个 warp 内,x 在变化,y 基本不变。代入地址计算后,相邻线程写入的位置不再是连续地址,而是相隔 height 个元素。

如果 height 很大,那么同一个 warp 里的线程会把数据写到相距很远的位置上。这会导致 global memory 写入无法很好地合并,memory transaction 被拆散,最终带宽利用率下降。

于是就出现了一种很常见的情况:

这个 kernel 不是算得慢,而是写得“不顺”。

03GPU 不只关心你算什么,也关心你怎么访问内存

对 GPU 来说,访存模式非常关键。

一个 warp 里的线程如果访问连续地址,硬件可以更高效地组织内存请求。

但如果线程访问的是分散地址,即使每个线程只读写一个很简单的 float,实际产生的内存 transaction 也可能变多。

这就是为什么很多 kernel 从代码上看很简单,但性能并不好。

它没有做复杂计算,也没有大量分支,但只要 global memory 访问方式不合理,就很容易跑不满带宽。

04常见优化:用 shared memory 做 tiling

矩阵转置的经典优化方法是 shared memory tiling。

思路并不复杂:

先让线程块从 global memory 中连续读取一块 tile 到 shared memory;

然后在 shared memory 内完成转置;

最后再把转置后的结果连续写回 global memory。

也就是说,我们用 shared memory 做了一次“中转”,把原本不友好的跨步访问,尽量变成 global memory 上更连续、更容易合并的访问。

一个常见写法是:

这里的 +1 不是随手多开一个元素。它的作用是避免 shared memory bank conflict。

因为 shared memory 也有自己的访问组织方式。如果多个线程在同一时刻访问落在同一个 bank 上的数据,就会产生冲突,影响性能。对转置这种访问模式来说,给 tile 的第二维加一列 padding,是一种常见的避免 bank conflict 的手段。

05这类问题的本质

矩阵转置只是一个很小的例子,但它反映的是 CUDA kernel 优化里非常基础也非常重要的一类问题:

代码正确,不代表访问模式合理;
计算量不大,不代表 kernel 一定会快;
GPU 没跑满,也不一定是算力不够。

很多时候,真正拖慢 kernel 的,是数据在内存里的移动方式。

所以在优化 CUDA kernel 时,一个很重要的判断是:

这个 kernel 到底是在“算”,还是在“等数据”?

如果瓶颈来自 global memory 访问,那么优化方向就不应该先放在增加计算指令、展开循环,或者盲目调整 block size 上,而应该先回到 memory layout、coalesced access、shared memory tiling 这些问题上。

很多 GPU kernel 的性能问题,本质上不是“算不动”,而是数据“走得不顺”。

下篇预告

下一篇,我们继续看另一个更容易被忽略的问题:为什么有些 kernel 优化之后,性能反而更差?

#计算加速 #kernel #内存访问 #开发