2290 字
7 分钟
CUDA 合并访存与转置矩阵性能指标分析
2026-10-01

实验源码与本地测量数据

配置:

  • RTX 5060 Laptop GPU,-26 SM,L2=32 MiB,
  • 驱动 592.00,
  • CUDA 12.8.61,
  • Nsight Compute 2025.2.0;
  • Windows,
  • Release、原配置 native 架构

1. 前言#

对一个 4096 × 4096 的 float 矩阵进行转置一般有三种算法:朴素方法,Tiling 做法以及线程循环 Tiling。本实验主体包括但不限于上述三种算法,同时对他们分别进行了 ncu 指标测量。

阶段结论: 带边界判断的 32 × 8 循环版在完整对比批次中达到 303.98 GB/s,约为同批次 copy 基线的 99.05%,相对朴素版加速约 1.72 倍。删除边界判断进一步减少了指令数,但没有观察到明确的性能收益。

2. 实验环境#

本次实验的测量方法与数据来源可大致概括为:普通运行与 ncu 指标单独测量。普通运行测量有效带宽与平均耗时,先预热再反复求平均。同理 ncu 测量。详细请见仓库 README

项目设置
GPUNVIDIA GeForce RTX 5060 Laptop GPU
显存理论峰值带宽384 GB/s(本实验采用的标称参考值)
构建与运行Windows,Release
数据类型与布局float,行优先
主要矩阵尺寸输入 4096 × 4096,输出同尺寸
输入初始化cudaMemset 为零
Tiled 共享数组float tile[32][33]

2.1 带宽口径#

有效带宽的计算如下,均按有效读写各一次计算:

N = 4096 × 4096 = 16,777,216
单个矩阵 = N × sizeof(float) = 67,108,864 byte = 64 MiB
有效数据量 = 2 × N × sizeof(float) = 134,217,728 byte
有效带宽 GB/s = 有效数据量 / (time_ms / 1000) / 10^9

3. 分析#

3.1 用线程访问模式预测 sectors/request#

对本实验满 warp、每线程访问一个 float 的情况,32 个线程需要 128 byte。连续且对齐的访问(理想访存)覆盖 4 个 32-byte sector;朴素转置中,读取地址跨行分散,一个 warp 覆盖 32 个 sector,下面是 4 次测量的结果

指标Copy朴素32×32 tiled32×8 循环
全局读 requests524,288524,288524,288524,288
全局读 sectors2,097,15216,777,2162,097,1522,097,152
全局读 sectors/request43244
全局写 requests524,288524,288524,288524,288
全局写 sectors2,097,1522,097,1522,097,1522,097,152
全局写 sectors/request4444

已证实:朴素版的读访问不合并,写访问已合并;tiling 改善了读取。

这里的 request/sectors 使用 l1tex__t_requests_pipe_lsu_mem_global_op_{ld,st}.sum 和 l1tex__t_sectors_pipe_lsu_mem_global_op_{ld,st}.sum。

3.2 DRAM 流量分析#

批次 A 的指标Copy朴素32×32 tiled32×8 循环
DRAM 读取 MB67.20369.59467.62367.126
DRAM 写入 MB60.67360.87859.58150.951
L1 sector 命中率 %0.0004.5770.0000.000
L2 sector 命中率 %0.01076.4700.0140.010
注意到:各版 DRAM 读取量接近一个矩阵的大小,没有相差八倍。
可能原因:朴素版较高的 L2 命中率,说明不同请求重复触达相同 sector、部分流量被缓存接住,所以实际 DRAM 流量没有 8 倍。

值得一提的是,实际 DRAM 写入的数据小于一个矩阵的大小 67.109 MB 原因:GPU 上的全局内存访问需要经过缓存体系。L2 Cache 是 GPU 设备内存的一致性点;数据写入 L2 后,就可以被后续设备访问正确地观察到,即使对应的脏数据(Dirty 即缓存中已更新,但与底层存储尚未同步的数据)还没有写回物理 DRAM

这意味着,Kernel 执行完成,不等于所有数据都必须已经写回物理 DRAM;但也绝不意味着后续程序可以看到过期数据。NVIDIA 开发者论坛经典案例

3.3 瓶颈随优化迁移#

3.3.1 有效带宽、吞吐、调度与指令指标#

以下为4次实验程序计算的有效带宽:

指标Copy朴素32×32 tiled32×8 循环
effective_bandwidth GB/s306.91176.68209.57302.1

以下为 10 次 ncu 算术平均:

指标Copy朴素32×32 tiled32×8 循环无判断指令
L1TEX throughput %29.89494.26020.25239.27237.931
L2 throughput %24.77169.67213.63930.77130.449
DRAM throughput %82.18235.59542.80789.85787.835
Memory throughput %82.18269.67242.80789.85787.835
SM throughput %32.63818.52327.00435.34734.159
Achieved occupancy %76.88983.51063.93393.68893.467
Eligible warps0.1580.3100.8420.6400.399
Issue active %13.05318.52319.83129.94918.030
总 warp 指令数8,388,60828,311,55224,641,53615,990,889.19961472
Long Scoreboard ratio56.08427.20013.80223.54339.555
Barrier ratio0.0000.0004.4736.1739.603
Short Scoreboard ratio4.1125.3082.1790.7111.275

主要解释:

  1. 朴素版读 sectors/request 为 32,同时 L1TEX throughput 达 94.26%,强烈支持分散读取对 L1TEX 造成很大压力。
  2. 32×32 tiled 消除了这类低效率全局读取,但 DRAM throughput 只有约 42.81%;改善合并访问后,仍有其他执行限制。
  3. 循环版保持相同的合并读写,DRAM throughput 提高到约 89.86%,普通运行有效带宽也接近 copy。这里可能暂且没有相关指标能推出 DRAM 吞吐率差异原因。
  4. 循环版 Eligible warps 较高,Issue active 达到约 29.949%,这些数据与循环版更好地维持指令发射、隐藏内存访问延迟的解释相符。
  5. 相较于朴素版与 Tiling 版,循环版的 Warp 级指令数大幅降低为 15.99M,无边界条件判断的循环版本,指令数减少为大约 9.96M(确切减少了哪些指令,仍需 SASS 对照)。由于两个版本都要处理 16,384 个 tile,因此总全局 request 数不变。
  6. 注意到,对于循环版本和无判断 Long Scoreboard ratio,Barrier ratio,无判断的指标反而更大,可能的原因在于分母更小。具体解释为:指令数减少到原来的 62.3%,如果等待负担基本不变,按发射指令归一化的等待 ratio 就可能放大约 1 / 0.623 = 1.605 倍,Long Scoreboard 放大约 1.68 倍、Barrier 放大约 1.56 倍,与这个量级接近。足以说明:这些 ratio 上升,不能直接解释为实际内存延迟增加了 68%,或同步开销增加了 56%

据此推断,循环版主要接近 DRAM 侧吞吐限制;尚未证明这是唯一限制。

Throughput 表示相关硬件单元相对于持续峰值的利用程度,不是缓存命中率或总时间占比。汇总项由其组成指标中的最大百分比主导,并非各层百分比相加;Memory throughput 与 DRAM throughput 相等,本身不足以证明显存带宽受限。

3.3.2 Tiling 的 occupancy 分析#

指标32×32 tiled32×8 循环
threads/block1,024256
warps/block328
registers/thread1826
shared memory 配置 byte8,19265,536
shared memory 分配 byte/block5,2485,248
occupancy limit:blocks2424
occupancy limit:registers28
occupancy limit:shared memory112
occupancy limit:warps16
综合理论驻留上限 block/SM16

由于本机每 SM 最多 48 warp,理论 occpancy 为:

32×32:1 block × 32 warp / 48 = 66.67%
32×8 :6 block × 8 warp / 48 = 100%

这与实际约 63.93%、93.69% 相符。

原版 32×32 tiled 同时受到 warps 和当前 shared 配置的 1 block 限制,解释:

  • 由于一个 block 最多 48 warp,原版 1 个 block 包含 32 个 warp,循环版 1 个 block 包含 8 个 warp,在 warp 限制条件下,原版的一个 SM 最多容纳 1 个 block,循环版为 6 个。
  • 1 个 Block 需要 5248 B,但在启动时原版 shared memory 配置 1 个 Block 只给了 8192 B,经限制一个 SM 只能存在一个 block,同理循环版本。

循环版寄存器从 18 增至 26,并未成为最终驻留限制。因为实际上原版需要寄存器总数多余循环版。 这里不需要为降低寄存器数而强行限制分配。Occupancy 衡量驻留情况,eligible 衡量就绪情况,二者不能互相替代;高 occupancy 也不保证高性能。

3.3.3 Padding 与 shared wavefront#

批次 A 的 shared 指标32×32 tiled32×8 循环
读 wavefront546,537.8526,170.8
写 wavefront612,599.3524,288
读 bank-conflict 计数22,249.81,882.8
写 bank-conflict 计数88,311.30

按本内核满 warp、标量 shared 访问的预期,每个方向的 wavefront 基数为 524,288。循环版读计数比基数多约 0.36%,写计数达到基数。

当前 tile[32][33] 的索引按传统 bank 映射可避免规则的列访问冲突;计数中的剩余增量没有进一步定位。现有结果不支持把 residual bank conflict 认定为主要瓶颈,也不能把 wavefront 减少比例直接换算成总耗时收益。这里先不继续深挖 padding。

3.3.4 BLOCK_ROWS 扫描#

保持 TILE=32,分别调整启动 block 和内核循环步长;这组实验只记录普通平均耗时:

BLOCK_ROWSthreads/block每线程元素数平均耗时 ms相对 8 的耗时增幅
412880.4518702.58%
825640.440510—
1651220.4461901.29%

结论: 8 在本组测试中效果最好。由于更多每线程工作可能减少重复指令,也可能增加寄存器需求;更多线程也不必然更快,因此 BLOCK_ROWS 的选择是这些因素的折中。

以上就是对合并访存与转置矩阵性能的实验数据记录与分析

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CUDA 合并访存与转置矩阵性能指标分析
https://www.naie-char.cc/posts/cuda_合并访存/
作者
萘Naie_Char
发布于
2026-10-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录