我有一个简单的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;
}
特别是分析器报告以下内容:
来自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事务。有人可以解释我在这里缺少的东西吗?
答案 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)。