配置:
- 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
| 项目 | 设置 |
|---|---|
| GPU | NVIDIA GeForce RTX 5060 Laptop GPU |
| 显存理论峰值带宽 | 384 GB/s(本实验采用的标称参考值) |
| 构建与运行 | Windows,Release |
| 数据类型与布局 | float,行优先 |
| 主要矩阵尺寸 | 输入 4096 × 4096,输出同尺寸 |
| 输入初始化 | cudaMemset 为零 |
| Tiled 共享数组 | float tile[32][33] |
2.1 带宽口径#
有效带宽的计算如下,均按有效读写各一次计算:
1N = 4096 × 4096 = 16,777,2162单个矩阵 = N × sizeof(float) = 67,108,864 byte = 64 MiB3有效数据量 = 2 × N × sizeof(float) = 134,217,728 byte4有效带宽 GB/s = 有效数据量 / (time_ms / 1000) / 10^93. 分析#
3.1 用线程访问模式预测 sectors/request#
对本实验满 warp、每线程访问一个 float 的情况,32 个线程需要 128 byte。连续且对齐的访问(理想访存)覆盖 4 个 32-byte sector;朴素转置中,读取地址跨行分散,一个 warp 覆盖 32 个 sector,下面是 4 次测量的结果
| 指标 | Copy | 朴素 | 32×32 tiled | 32×8 循环 |
|---|---|---|---|---|
| 全局读 requests | 524,288 | 524,288 | 524,288 | 524,288 |
| 全局读 sectors | 2,097,152 | 16,777,216 | 2,097,152 | 2,097,152 |
| 全局读 sectors/request | 4 | 32 | 4 | 4 |
| 全局写 requests | 524,288 | 524,288 | 524,288 | 524,288 |
| 全局写 sectors | 2,097,152 | 2,097,152 | 2,097,152 | 2,097,152 |
| 全局写 sectors/request | 4 | 4 | 4 | 4 |
已证实:朴素版的读访问不合并,写访问已合并;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 tiled | 32×8 循环 |
|---|---|---|---|---|
| DRAM 读取 MB | 67.203 | 69.594 | 67.623 | 67.126 |
| DRAM 写入 MB | 60.673 | 60.878 | 59.581 | 50.951 |
| L1 sector 命中率 % | 0.000 | 4.577 | 0.000 | 0.000 |
| L2 sector 命中率 % | 0.010 | 76.470 | 0.014 | 0.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 tiled | 32×8 循环 |
|---|---|---|---|---|
| effective_bandwidth GB/s | 306.91 | 176.68 | 209.57 | 302.1 |
以下为 10 次 ncu 算术平均:
| 指标 | Copy | 朴素 | 32×32 tiled | 32×8 循环 | 无判断指令 |
|---|---|---|---|---|---|
| L1TEX throughput % | 29.894 | 94.260 | 20.252 | 39.272 | 37.931 |
| L2 throughput % | 24.771 | 69.672 | 13.639 | 30.771 | 30.449 |
| DRAM throughput % | 82.182 | 35.595 | 42.807 | 89.857 | 87.835 |
| Memory throughput % | 82.182 | 69.672 | 42.807 | 89.857 | 87.835 |
| SM throughput % | 32.638 | 18.523 | 27.004 | 35.347 | 34.159 |
| Achieved occupancy % | 76.889 | 83.510 | 63.933 | 93.688 | 93.467 |
| Eligible warps | 0.158 | 0.310 | 0.842 | 0.640 | 0.399 |
| Issue active % | 13.053 | 18.523 | 19.831 | 29.949 | 18.030 |
| 总 warp 指令数 | 8,388,608 | 28,311,552 | 24,641,536 | 15,990,889.1 | 9961472 |
| Long Scoreboard ratio | 56.084 | 27.200 | 13.802 | 23.543 | 39.555 |
| Barrier ratio | 0.000 | 0.000 | 4.473 | 6.173 | 9.603 |
| Short Scoreboard ratio | 4.112 | 5.308 | 2.179 | 0.711 | 1.275 |
主要解释:
- 朴素版读 sectors/request 为 32,同时 L1TEX throughput 达 94.26%,强烈支持分散读取对 L1TEX 造成很大压力。
- 32×32 tiled 消除了这类低效率全局读取,但 DRAM throughput 只有约 42.81%;改善合并访问后,仍有其他执行限制。
- 循环版保持相同的合并读写,DRAM throughput 提高到约 89.86%,普通运行有效带宽也接近 copy。这里可能暂且没有相关指标能推出 DRAM 吞吐率差异原因。
- 循环版 Eligible warps 较高,Issue active 达到约 29.949%,这些数据与循环版更好地维持指令发射、隐藏内存访问延迟的解释相符。
- 相较于朴素版与 Tiling 版,循环版的 Warp 级指令数大幅降低为 15.99M,无边界条件判断的循环版本,指令数减少为大约 9.96M(确切减少了哪些指令,仍需 SASS 对照)。由于两个版本都要处理 16,384 个 tile,因此总全局 request 数不变。
- 注意到,对于循环版本和无判断 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 tiled | 32×8 循环 |
|---|---|---|
| threads/block | 1,024 | 256 |
| warps/block | 32 | 8 |
| registers/thread | 18 | 26 |
| shared memory 配置 byte | 8,192 | 65,536 |
| shared memory 分配 byte/block | 5,248 | 5,248 |
| occupancy limit:blocks | 24 | 24 |
| occupancy limit:registers | 2 | 8 |
| occupancy limit:shared memory | 1 | 12 |
| occupancy limit:warps | 1 | 6 |
| 综合理论驻留上限 block/SM | 1 | 6 |
由于本机每 SM 最多 48 warp,理论 occpancy 为:
132×32:1 block × 32 warp / 48 = 66.67%232×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 tiled | 32×8 循环 |
|---|---|---|
| 读 wavefront | 546,537.8 | 526,170.8 |
| 写 wavefront | 612,599.3 | 524,288 |
| 读 bank-conflict 计数 | 22,249.8 | 1,882.8 |
| 写 bank-conflict 计数 | 88,311.3 | 0 |
按本内核满 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_ROWS | threads/block | 每线程元素数 | 平均耗时 ms | 相对 8 的耗时增幅 |
|---|---|---|---|---|
| 4 | 128 | 8 | 0.451870 | 2.58% |
| 8 | 256 | 4 | 0.440510 | — |
| 16 | 512 | 2 | 0.446190 | 1.29% |
结论: 8 在本组测试中效果最好。由于更多每线程工作可能减少重复指令,也可能增加寄存器需求;更多线程也不必然更快,因此 BLOCK_ROWS 的选择是这些因素的折中。
以上就是对合并访存与转置矩阵性能的实验数据记录与分析
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
