CUDA探查器报告低效的全局内存访问

时间:2017-02-25 04:06:16

标签: caching memory cuda profiler

我有一个简单的CUDA内核,我认为它可以有效地访问全局内存。然而,Nvidia分析器报告我正在执行低效的全局内存访问。我的内核代码是:

__global__ void update_particles_kernel
(
    float4 *pos, 
    float4 *vel, 
    float4 *acc, 
    float dt, 
    int numParticles
)
{
int index = threadIdx.x + blockIdx.x * blockDim.x;
int offset = 0;

while(index + offset < numParticles)
{
    vel[index + offset].x += dt*acc[index + offset].x;   // line 247
    vel[index + offset].y += dt*acc[index + offset].y;
    vel[index + offset].z += dt*acc[index + offset].z;

    pos[index + offset].x += dt*vel[index + offset].x;   // line 251
    pos[index + offset].y += dt*vel[index + offset].y;
    pos[index + offset].z += dt*vel[index + offset].z;

    offset += blockDim.x * gridDim.x;
}

特别是分析器报告以下内容:

enter image description here

来自CUDA best practices guide它说:

&#34;对于计算能力2.x的设备,可以非常容易地总结需求:warp线程的并发访问将合并到等于缓存行数的多个事务中必须为warp的所有线程提供服务。默认情况下,所有访问都通过L1进行缓存,L1为128字节行。对于分散的访问模式,为了减少过度捕获,有时仅在L2中缓存有用,缓存较短的32字节段(参见CUDA C编程指南)。

对于计算能力3.x的设备,仅在L2中缓存对全局内存的访问; L1保留用于本地存储器访问。某些具有计算能力的设备3.5,3.7或5.2也允许在L1中选择性地缓存全局变量。&#34;

现在在我的内核基于这些信息,我预计将需要16次访问来服务32线程warp,因为float4是16字节,而我的卡(770m计算能力3.0)从L2缓存读取是在32执行字节块(16字节* 32线程/ 32字节高速缓存行= 16次访问)。事实上,您可以看到探查器报告我正在进行16访问。我不明白的是,为什么分析器报告理想的访问将涉及247行的每次访问8个L2事务,而剩余行的每次访问仅涉及4个L2事务。有人可以解释我在这里缺少的东西吗?

1 个答案:

答案 0 :(得分:4)

  

我有一个简单的CUDA内核,我认为它可以有效地访问全局内存。然而,Nvidia分析器报告我正在执行低效的全局内存访问。

举一个例子,您的float4 vel数组存储在内存中,如下所示:

0.x 0.y 0.z 0.w 1.x 1.y 1.z 1.w 2.x 2.y 2.z 2.w 3.x 3.y 3.z 3.w ...
  ^               ^               ^               ^             ...
  thread0         thread1         thread2         thread3

所以当你这样做时:

vel[index + offset].x += ...;   // line 247

您正在访问(存储)我上面标记的位置(.x)。每个^标记之间的间隙表示分析器指出的低效访问模式。 (在下一行代码中,您存储到.y位置并不重要。)

至少有两种解决方案,其中一种是经典的AoS - &gt;通过适当的代码调整,重新组织您的数据。这有很好的文档记录(例如cuda标签上的here和其他地方)的含义,以及如何操作,所以我会让你看一下。

另一种典型的解决方案是在需要时为每个线程加载float4数量,并在需要时为每个线程存储float4数量。您可以通过简单的方式重新编写代码,这样可以提高分析结果:

//preceding code need not change
while(index + offset < numParticles)
{
    float4 my_vel = vel[index + offset];
    float4 my_acc = acc[index + offset];
    my_vel.x += dt*my_acc.x;   
    my_vel.y += dt*my_acc.y;
    my_vel.z += dt*my_acc.z;
    vel[index + offset] = my_vel;

    float4 my_pos = pos[index + offset];
    my_pos.x += dt*my_vel.x; 
    my_pos.y += dt*my_vel.y;
    my_pos.z += dt*my_vel.z;
    pos[index + offset] = my_pos;

    offset += blockDim.x * gridDim.x;
}

即使您可能认为此代码的效率较低,并且#34;比你的代码,因为你的代码&#34;出现&#34;仅加载和存储.x.y.z,而我的&#34;出现&#34;另外,加载和存储.w,由于GPU加载和存储到全局内存或从全局内存存储的方式,基本上没有区别。虽然您的代码似乎没有触及.w,但在访问相邻元素的过程中,GPU将从全局内存加载.w元素,并且(最终)存储.w元素回到全球记忆。

  

我不明白为什么分析器报告理想的访问将涉及247行的每次访问8个L2事务

对于原始代码中的第247行,您为float的加载操作每个线程访问一个acc.x数量,并为{} 1的加载操作每个线程获取一个float数量{1}}。每个线程的vel.x数量本身应该需要128个字节用于warp,这是4个32字节的L2缓存行。两个负载一起需要8个L2高速缓存线负载。这是理想的情况,假设数量很好地包装在一起(SoA)。但这不是你拥有的(你有AoS)。