问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

CUDA内存访问优化详解

创作时间:
作者:
@小白创作中心

CUDA内存访问优化详解

引用
CSDN
1.
https://m.blog.csdn.net/gg_bruse/article/details/145489656

CUDA内存访问优化是提升GPU计算性能的关键技术之一。本文详细介绍了GPU内存访问模式、对齐与合并访存、AoS与SoA数据结构选择以及性能调优策略,对于从事GPU编程和并行计算的开发者具有重要的参考价值。

一、内存访问模式

GPU对片外DRAM的访问延迟大,带宽低,导致其成为很多应用的性能瓶颈。因此对DRAM访问的进一步优化可以有效改善程序性能。优化之前,首先看一下GPU对内存的访问模式

如上图所示,DRAM内存的读写在物理上是从 片外DRAM -> 片上Cache -> 寄存器。 其中,片外DRAM到片上Cache是主要性能瓶颈。DRAM 到 Cache 之间的一次传输(transaction)设计为32、64或者128字节,并且内存地址按照32、64或者128字节的间隔对齐

注意:后文中 transaction 特指设备内存(DRAM)到片内存储(Cache)的传输

以读数据为例,DRAM 数据首先进入 L2 Cache,之后根据 GPU 架构的不同,有些会继续传输到 L1 Cache,最后进入线程寄存器。L2 cache 是所有 SM 共有,而 L1 cache 是 SM 私有

若使用了 L1+L2 Cache,那么执行一次 DRAM 到 Cache 之间的传输(transaction),是 128 字节。若仅使用 L2 Cache,那么就是 32 字节

是否启用L1 Cache取决于GPU架构与编译选项

关闭L1 cache
-Xptxas -dlcm=cg
打开L1 cache
-Xptxas -dlcm=ca  

由于采用 SIMT 的架构,GPU 对内存的访问指令是由 warp 发起的,即 warp 中每个线程同时执行内存操作指令,不过每个线程所访问的数据地址可以不同,GPU 会根据这些不同的地址发起一次或多次 DRAM -> Cache 的传输(transaction),直到所有线程都拿到各自所需的数据(Cache->Registers)。显然,可以通过减少DRAM->Cache的transaction次数来优化程序性能

warp 执行内存指令时会有很长的延迟,此时 warp 进入 Stalled 状态,warp 调度器调度其他 eligible warp 执行

内存指令延迟的原因:

  • 访问设备内存本身存在大的延迟

  • SIMT 的执行模型,意味着只有当 warp 内所有32个线程都得到了数据后,才会从 Stalled 态转为 Eligible 态

最好的情况下,GPU发起一次 DRAM -> Cache 的transaction就把 warp 中所有线程所需的数据全部获取到 Cache,最坏的情况下,则需要 32 次 transaction

Read-Only cache

Read-Only cache原本用于纹理内存的缓存。GPU 3.5以上的版本可以使用该缓存替代L1,作为Global内存的缓存。此cache采用32字节对齐间隔,因此比原128字节的缓冲区更适合非对齐非合并的情况

二、对齐与合并访存

优化全局内存的访问性能,需要考虑下面两个方面,

  • 内存对齐访问(Aligned memory access):DRAM -> Cache transaction,首地址是32或128字节的倍数

  • 内存合并访问(Coalesced memory access): Warp 内线程访问连续内存块

理想情况

Warp 内每个线程需要 4 个字节的数据,且总共 32 * 4 = 128 个字节是连续的,起始地址为128。此时只需一次 128 字节的 memory transactioin (DRAM -> Cache)

非对齐、非合并的情况

这种情况下需要 3 次 128字节的 memory transaction,一个从内存地址 0 -> 127,为标记为 1 的线程取值,一个从 128 -> 255,为标记 2 的线程取值,一个从 256 -> 383,为标记 3 的线程取值

2.1 Global Memory Read

根据是否使用L1 cache,可分为两种情况讨论,

  • Cached load(使用 L1 cache)

  • Uncached load(不使用 L1 cache)

2.1.1 Cached load

使用 L1 cache,memory transaction 的以 L1 cache line 的大小128字节为间隔访存

合并对齐访问

warp 内线程访问的地址在 128~256 之间,每个线程需要 4 个字节,所有线程所需内存地址按照线程 ID 连续排列。只需执行一个 128 字节的 transaction 即可满足所有线程的需求。此时总线利用率为 100%,即在这次 transaction 中,内存带宽得以充分使用,没有多余的数据

warp 的线程访问的数据在 128~256 之间,每个线程需要 4 个字节,但是内存地址没有按照线程 ID 排序。与之前的例子相同,一次 128 字节的 transaction 即可满足要求,且总线利用率为 100%

合并、不对齐

上图 warp 需要连续的 128 字节,但是内存的首地址没有 128 字节对齐。此时需要两次 128 字节的 transaction,总线利用率为 50% (总共读了 256 个字节,实际使用 128 字节)

Warp内线程访问同一4字节数据

warp 内所有线程访问了相同 4 个字节,需要一次 128 字节的 transaction,总线利用率 4 / 128 = 3.125%

最坏情况

同样是请求32个4 bytes数据,但是每个地址分布的相当不规律,我们只想要需要的那128 bytes数据,但是,实际上下图这样的分布,却需要N∈(0,32)个cache line,也就是N次数据传输消耗

CPU的L1 cache是根据时间和空间局部性做出的优化,但是GPU的L1仅仅被设计成针对空间局部性而不包括时间局部性。频繁获取L1不会导致某些数据驻留在cache中,只要下次用不到,直接删

2.1.2 Uncached load

若不使用 L1 cache,一次内存传输由 1、2或4 个 segments 完成,每个 segment 为 32 字节, 并且按照 32 字节对齐。显然这种情况下数据传输得到了更精细的划分(最小 32 字节),这会带来更高效的非对齐,非合并的内存访问

理想情况

对齐合并的内存访问,只需一个 4 segment 的 transaction,总线利用率 100%

非对齐、合并

上图warp需要连续的128字节数据,但是首地址并未和32字节对齐,此时128字节的数据最多分布在5个segment内,因此总线利用率至少为80%。显然要比使用L1 cached的时候更好

Warp内线程访问同一4字节数据

此时的总线利用率为4/32 = 12.5%,也要比使用L1 cache时(3.125%)要好

最坏情况

32个线程所需的数据散落在至多32个segment里。散落在32个32字节的segment里显然要比散落在32个128字节的情况要好

2.2 Global Memory Write

内存写操作情况要简单的多,Fermi/Kelper 的 L1 cache 并不支持写操作。内存写仅通过 L2 cache 写入设备内存。与 Uncached Load 类似,transaction分为1, 2, 4个segment,每个segment32字节

理想情况

warp写入连续的128个字节,仅需一个4-segments的transaction

对齐,不合并

需要3个1-segment的transaction

对齐且地址在一个连续的64-byte范围内的情况

需要一个2-segment的transaction

三、AoS与SoA

array of structures(AoS,结构阵列)和structure of arrays(SoA,数组结构)

//Array of structures (AoS)
struct innerStruct {
    float x;
    float y;
};
struct innerStruct myAoS[N];
 
//Structure of array
struct innerArray {
    float x[N];
    float y[N];
};
struct innerArray moa;  

上图为AoS,SoA的内存结构。采用AoS的形式,Warp 线程访问 x 时,会将 y 的值也载入 cache。浪费了50%的带宽。而 SoA 就不存在这个问题。因此推荐采用 SoA 的形式组织数据

四、性能调优

化设备内存带宽利用率有两个目标:

  1. 对齐及合并内存访问,以减少带宽的浪费

  2. 足够的并发内存操作,以隐藏内存延迟

影响设备内存操作性能的因素主要有两个:

  1. 有效利用设备DRAM和SM片上内存之间的字节移动:为了避免设备内存带宽的浪费,内存访问模式应该是对齐和合并的

  2. 当前的并发内存操作数

可通过以下两点实现最大化当前存储器操作数

  1. 展开,每个线程产生更多的独立内存访问

  2. 修改核函数启动的执行配置来使每个SM有更多的并行性

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号