Opencl 在AMD Radeon RX580上解决的N体问题中,内存访问的最佳实践是什么?

Opencl 在AMD Radeon RX580上解决的N体问题中,内存访问的最佳实践是什么?,opencl,amd-gcn,Opencl,Amd Gcn,我计算了N个粒子在引力场中运动的轨迹。我编写了以下OpenCL内核: #define G 100.0f #define EPS 1.0f float2 f (float2 r_me, __constant float *m, __global float2 *r, size_t s, size_t n) { size_t i; float2 res = (0.0f, 0.0f); for (i=1; i<n; i++) { size_t idx

我计算了N个粒子在引力场中运动的轨迹。我编写了以下OpenCL内核:

#define G 100.0f
#define EPS 1.0f

float2 f (float2 r_me, __constant float *m, __global float2 *r, size_t s, size_t n)
{
    size_t i;
    float2 res = (0.0f, 0.0f);

    for (i=1; i<n; i++) {
        size_t idx = i;
//        size_t idx = (i + s) % n;
        float2 dir = r[idx] - r_me;
        float dist = length (dir);
        res += G*m[idx]/pown(dist + EPS, 3) * dir;
    }

    return res;
}

__kernel void take_step_rk2 (__constant float *m,
                             __global float2 *r,
                             __global float2 *v,
                             float delta)
{
    size_t n = get_global_size(0);
    size_t s = get_global_id(0);


    float2 mv = f(r[s], m, r, s, n);
    float2 mr = v[s];

    float2 vpred1 = v[s] + mv * delta;
    float2 rpred1 = r[s] + mr * delta;

    float2 nv = f(rpred1, m, r, s, n);
    float2 nr = vpred1;

    barrier (CLK_GLOBAL_MEM_FENCE);

    r[s] += (mr + nr) * delta / 2;
    v[s] += (mv + nv) * delta / 2;
}
这是引用自(2015年):

在某些情况下,通道冲突的一种意外情况是,从同一地址读取数据是一种冲突,即使在快速路径上也是如此。 这不会发生在只读存储器上,例如常量缓冲区, 纹理或着色器资源视图(SRV);但在读/写UAV上是可能的 内存或OpenCL全局内存

“我的队列”中的所有工作项都试图访问此循环中的相同内存,因此必须存在通道冲突:

for (i=1; i<n; i++) {
        size_t idx = i;
//        size_t idx = (i + s) % n;
        float2 dir = r[idx] - r_me;
        float dist = length (dir);
        res += G*m[idx]/pown(dist + EPS, 3) * dir;
    }

因此,第一个工作项(全局id为
0
)首先访问数组
r
中的第一个元素,第二个工作项访问第二个元素,依此类推

我原以为这一变化一定会导致性能的提高,但恰恰相反,它导致了显著的性能下降(大约是2倍)。我错过了什么?为什么在这种情况下,所有人都能更好地访问相同的内存


如果你有其他提高绩效的建议,请与我分享。OpenCL优化指南非常混乱。

f函数的循环没有为合并访问的重新聚合设置障碍。一旦一些项目得到它们的r数据,它们就开始计算,但是那些不能等待它们的数据的项目将因此失去合并完整性。要对它们重新分组,至少每10次迭代或2次迭代或甚至每一次迭代添加1个屏障。但访问全局网络的延迟很高。屏障+延迟对性能不利。这里您需要本地内存,因为它具有低延迟和广播能力,这使得它仅在大于本地线程数(64?)的颗粒上失去聚合性,这对全局内存访问也不坏(您需要在每个第K次迭代中从全局填充本地内存,其中N被划分为K个大小的组)

来源于2013年( ):

因此,有效使用LDS的关键是控制访问 模式,以便在同一周期上生成的访问映射到不同的 LDS中的银行。一个值得注意的例外是访问相同的 地址(即使它们具有相同的位6:2)可以广播到 所有请求者和不生成银行冲突

为此使用LDS(
\u local
)将提供良好的性能。因为LDS很小,所以您应该一次在256个粒子这样的小块中进行

另外,使用i作为idx非常有利于缓存,但模数版本非常有利于缓存。一旦数据可以存在于缓存中,是否完成N个请求就无关紧要了。他们现在从缓存中出来了。但对于模数,你们在重复使用之前销毁缓存成分,这取决于N。对于小N,它应该会更快,正如你们所预见的。对于大N和小GPU缓存,情况会更糟。与每个周期N-cache_大小的全局请求相比,每个周期只有1个全局请求

我猜,有了如此强大的GPU,您的N值很高,比如64k个实体,每个实体需要2个变量,每个变量需要4个字节,总计512kB,这不适合L1。可能只有L2比idx=i到L1慢

答复:

  • “全部到同一个一级缓存”adr比“全部到全局”和“二级缓存”adr快

  • 在“阻塞/修补”算法中使用本地内存以实现高速

for (i=1; i<n; i++) {
        size_t idx = i;
//        size_t idx = (i + s) % n;
        float2 dir = r[idx] - r_me;
        float dist = length (dir);
        res += G*m[idx]/pown(dist + EPS, 3) * dir;
    }
        size_t idx = i;
//        size_t idx = (i + s) % n;
//        size_t idx = i;
        size_t idx = (i + s) % n;