CUDA SHARED MEMORY

在global Memory部分,数据对齐和连续是很重要的话题,当使用L1的时候,对齐问题可以忽略,但是非连续的获取内存依然会降低性能。依赖于算法本质,某些情况下,非连续访问是不可避免的。使用shared memory是另一种提高性能的方式。

GPU上的memory有两种:

  • On-board memory

  • On-chip memory

global memory就是一块很大的on-board memory,并且有很高的latency。而shared memory正好相反,是一块很小,低延迟的on-chip memory,比global memory拥有高得多的带宽。我们可以把他当做可编程的cache,其主要作用有:

- An intra-block thread communication channel 线程间交流通道 - A program-managed cache for global memory data可编程cache · Scratch pad memory for transforming data to improve global memory access patterns

shared memory(SMEM)是GPU的重要组成之一。物理上,每个SM包含一个当前正在执行的block中所有thread共享的低延迟的内存池。SMEM使得同一个block中的thread能够相互合作,重用on-chip数据,并且能够显著减少kernel需要的global memory带宽。由于APP可以直接显式的操作SMEM的内容,所以又被称为可编程缓存。

由于shared memory和L1要比L2和global memory更接近SM,shared memory的延迟比global memory低20到30倍,带宽大约高10倍。
在这里插入图片描述
当一个block开始执行时,GPU会分配其一定数量的shared memory,这个shared memory的地址空间会由block中的所有thread 共享。shared memory是划分给SM中驻留的所有block的,也是GPU的稀缺资源。所以,使用越多的shared memory,能够并行的active就越少。

关于Program-Managed Cache:在C语言编程里,循环(loop transformation)一般都使用cache来优化。在循环遍历的时候使用重新排列的迭代顺序可以很好利用cache局部性。在算法层面上,我们需要手动调节循环来达到令人满意的空间局部性,同时还要考虑cache size。cache对于程序员来说是透明的,编译器会处理所有的数据移动,我们没有能力控制cache的行为。shared memory则是一个可编程可操作的cache,程序员可以完全控制其行为。

Shared Memory Allocation

我们可以动态或者静态的分配shared Memory,其声明即可以在kernel内部也可以作为全局变量。

其标识符为:__ shared__。

下面这句话静态的声明了一个2D的浮点型数组:

__ shared __ float tile[size_y][size_x];

如果在kernel中声明的话,其作用域就是kernel内,否则是对所有kernel有效。如果shared Memory的大小在编译器未知的话,可以使用extern关键字修饰,例如下面声明一个未知大小的1D数组:

extern shared int tile[];

由于其大小在编译器未知,我们需要在每个kernel调用时,动态的分配其shared memory,也就是最开始提及的第三个参数:

kernel<<<grid, block, isize * sizeof(int)>>>(…)

应该注意到,只有1D数组才能这样动态使用。

Shared Memory Banks and Access Mode

之前博文对latency和bandwidth有了充足的研究,而shared memory能够用来隐藏由于latency和bandwidth对性能的影响。下面将解释shared memory的组织方式,以便研究其对性能的影响。

Memory Banks

为了获得高带宽,shared Memory被分成32(对应warp中的thread)个相等大小的内存块,他们可以被同时访问。不同的CC版本,shared memory以不同的模式映射到不同的块(稍后详解)。如果warp访问shared Memory,对于每个bank只访问不多于一个内存地址,那么只需要一次内存传输就可以了,否则需要多次传输,因此会降低内存带宽的使用。

Bank Conflict

当多个地址请求落在同一个bank中就会发生bank conflict,从而导致请求多次执行。硬件会把这类请求分散到尽可能多的没有conflict的那些传输操作 里面,降低有效带宽的因素是被分散到的传输操作个数。

warp有三种典型的获取shared memory的模式:

  • Parallel access:多个地址分散在多个bank。

  • Serial access:多个地址落在同一个bank。

  • Broadcast access:一个地址读操作落在一个bank。
    Parallel access是最通常的模式,这个模式一般暗示,一些(也可能是全部)地址请求能够被一次传输解决。理想情况是,获取无conflict的shared memory的时,每个地址都在落在不同的bank中。

Serial access是最坏的模式,如果warp中的32个thread都访问了同一个bank中的不同位置,那就是32次单独的请求,而不是同时访问了。

Broadcast access也是只执行一次传输,然后传输结果会广播给所有发出请求的thread。这样的话就会导致带宽利用率低。

下图是最优情况的访问图示:

在这里插入图片描述
下图一种随机访问,同样没有conflict:

在这里插入图片描述
下图则是某些thread访问到同一个bank的情况,这种情况有两种行为:

  • Conflict-free broadcast access if threads access the same address within a bank

  • Bank conflict access if threads access different addresses within a bank
    在这里插入图片描述

Synchronization

因为shared Memory可以被同一个block中的不同的thread同时访问,当同一个地址的值被多个thread修改就导致了inter-thread conflict,所以我们需要同步操作。CUDA提供了两类block内部的同步操作,即:

· Barriers

· Memory fences

对于barrier,所有thread会等待其他thread到达barrier point;对于Memory fence,所有thread会阻塞到所有修改Memory的操作对其他thread可见,下面解释下CUDA需要同步的主要原因:weakly-ordered。

Weakly-Ordered Memory Model

现代内存架构有非常宽松的内存模式,也就是意味着,Memory的获取不必按照程序中的顺序来执行。CUDA采用了一种叫做weakly-ordered Memory model来获取更激进的编译器优化。

GPU thread写数据到不同的Memory的顺序(比如shared Memory,global Memory,page-locked host memory或者另一个device上的Memory)同样没必要跟程序里面顺序呢相同。一个thread的读操作的顺序对其他thread可见时也可能与实际上执行写操作的thread顺序不一致。

为了显式的强制程序以一个确切的顺序运行,就需要用到fence和barrier。他们也是唯一能保证kernel对Memory有正确的行为的操作。

Explicit Barrier

同步操作在我们之前的文章中也提到过不少,比如下面这个:

void __syncthreads();

__syncthreads就是作为一个barrier point起作用,block中的thread必须等待所有thread都到达这个point后才能继续下一步。这也保证了所有在这个point之前获取global Memory和shared Memory的操作对同一个block中所有thread可见。__syncthreads被用来协作同一个block中的thread。当一些thread获取Memory相同的地址时,就会导致潜在的问题(读后写,写后读,写后写)从而引起未定义行为状态,此时就可以使用__syncthreads来避免这种情况。
使用__syncthreads要相当小心,只有在所有thread都会到达这个point时才可以调用这个同步,显而易见,如果同一个block中的某些thread永远都到达该点,那么程序将一直等下去,下面代码就是一种错误的使用方式:

if (threadID % 2 == 0) {
    __syncthreads();
    } else {
        __syncthreads();
}        

Memory Fence

这种方式保证了任何在fence之前的Memory写操作对fence之后thread都可见,也就是,fence之前写完了,fence之后其它thread就都知道这块Memory写后的值了。fence的设置范围比较广,分为:block,grid和system。

可以通过下面的API来设置fence:

void __threadfence_block();

看名字就知道,这个函数是对应的block范围,也就是保证同一个block中thread在fence之前写完的值对block中其它的thread可见,不同于barrier,该function不需要所有的thread都执行。

下面是grid范围的API,作用同理block范围,把上面的block换成grid就是了:

void __threadfence();

下面是system的,其范围针对整个系统,包括device和host:

void __threadfence_system();

转自:https://www.cnblogs.com/1024incn/p/4605502.html

猜你喜欢

转载自blog.csdn.net/TH_NUM/article/details/84501123
今日推荐