在OpenCL内核中存储小型常量值数组的最佳实践?

时间:2017-03-10 18:10:05

标签: opencl gpu gpgpu

我正在编写一个OpenCL内核,用5x5高斯滤波器对图像进行卷积,并且想知道存储滤波器常量的最佳实践是什么。在内核中,32x32工作组中的每个线程执行以下操作:

  1. 将像素加载到__local内存缓冲区中,
  2. 通过barrier(CLK_LOCAL_MEM_FENCE)
  3. 同步
  4. 然后对其对应的像素执行卷积。
  5. 以下是本地图像数据和过滤器的缓冲区:

     __local float4 localRegion[32][32]; // image region w 2 pixel apron
     .... 
     static const float filter[5][5] = { // __constant vs __private ??
        {1/256.0,  4/256.0,  6/256.0,  4/256.0, 1/256.0},
        {4/256.0, 16/256.0, 24/256.0, 16/256.0, 4/256.0},
        {6/256.0, 24/256.0, 36/256.0, 24/256.0, 6/256.0},
        {4/256.0, 16/256.0, 24/256.0, 16/256.0, 4/256.0},
        {1/256.0,  4/256.0,  6/256.0,  4/256.0, 1/256.0}
      };
    

    哪些内存区域可以容纳filter,这是最好的,以及在每种情况下如何进行初始化?最佳__private最好,但我不确定您是否可以静态初始化私有数组?除非某些线程负责加载__local条目(我认为),否则filter没有意义?另外,根据khronos docs Sec 6.5,我不确定static_private可以合在一起。

    根据the answers hereherefilter可以存储为__private,但不清楚初始化是如何发生的。

1 个答案:

答案 0 :(得分:5)

  

但我不确定你是否可以静态初始化私有数组

Opencl规范说"静态存储类说明符只能用于 非内核函数,在程序范围内声明的全局变量和函数内的变量 在全局或常量地址空间中声明。"。除此之外,编译器(至少是Amd' s)优化了常量数学输出和与简单(常量/指令)存储器访问的交换。即使最重要的是,当空间不足时,私有寄存器溢出到全局内存,内核开始访问那里。因此,当真实数据有时会传递到其他地方时,静态无法获得有意义的描述。

 float filter[5][5] = {  
    {cos(sin(cos(sin(cos(sin(1/256.0f)))))),  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f},
    {cos(sin(cos(sin(cos(sin(4/256.0f)))))), 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {sin(cos(sin(cos(sin(cos(6/256.0f)))))), 24/256.0f, 36/256.0f, 24/256.0f, 6/256.0f},
    {sin(cos(sin(cos(sin(cos(4/256.0f)))))), 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {sin(cos(sin(cos(sin(cos(1/256.0f)))))),  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f}
  };

需要相同的时间(r7_240gpu为0.78ms)

float filter[5][5] = { 
    {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f},
    {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {6/256.0f, 24/256.0f, 36/256.0f, 24/256.0f, 6/256.0f},
    {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f}
  };

和profiler的ISA输出没有任何正弦或余弦函数。在某些内存位置只写了一些数字。这是没有启用任何优化的条件。

  

哪些内存区域可以容纳过滤器,哪个最好

取决于硬件,但通常有多种类型:

// defined before kernel
__constant float filter[5][5] = { 
    {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f},
    {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {6/256.0f, 24/256.0f, 36/256.0f, 24/256.0f, 6/256.0f},
    {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f}
  };

同时为r7_240 gpu执行此操作。请注意,静态索引更适合__constant内存访问(至少在amd gpu中),并且对于同一索引访问并不坏(组中的所有线程都访问相同的索引,就像在此示例中一样(int in nested-loops) )。使用这些寻址模式,常量内存比全局内存更快,但是当使用不同的索引时,它与全局内存访问(甚至命中高速缓存)没有什么不同。 "对于全局范围的常量数组,如果数组的大小低于64 kB,则将其放在硬件常量缓冲区中;否则,它使用全局内存"。 (有与Amd-GCN架构相关但可以预期来自Nvidia和Intel的类似行为)

Amd的opencl规范说" L1和L2启用图像和相同索引的常量。"(对于HD5800系列gpu)所以你也可以使用image2d_t输入获得类似的性能。对于GCN,L1和L2比常量内存更快。

Nvidia的opencl最佳实践说:" p读取靠近的纹理地址将达到最佳效果 性能。纹理存储器也被设计用于具有常量的流读取 潜伏;也就是说,缓存命中会降低DRAM带宽需求,但不会降低读取延迟。 在某些寻址情况下,通过图像对象读取设备内存可以 是从全局或常量读取设备内存的有利替代方案 记忆。 "并且还说"它们被缓存,如果存在2D位置,则可能表现出更高的带宽 在纹理提取。 "(image2d_t再次)

如果在其他地方需要私有内存,您甚至可以拆分过滤器,例如:

// defined before kernel
__constant float filter2[3][5] = {  
    {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f},
    {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
    {6/256.0f, 24/256.0f, 36/256.0f, 24/256.0f, 6/256.0f},
  };

   // no need to write __private, automatically private in function body
   float filter[2][5] = { 
        {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
        {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f}
    };

这与上面两个例子(至少对于r7_240)有相同的时间。所有示例都运行512x512大小的图像,512x512工作项目和16x16本地工作项。

  除非某些线程负责加载过滤条目,否则

__ local没有意义

Amd-GCN上的本地内存速度是恒定内存(相同索引)访问速度的8倍,但在整个GPU上的容量增加了5-20倍(但对于单个计算单元可能更少)。 Nvidia的opencl最佳实践也是如此。但HD5800系列amd gpu比本地内存具有更多恒定的内存带宽。 GCN更新,所以本地内存似乎更好,除非它没有足够的空间。

GCN上的专用寄存器比本地内存快5-6倍,容量是每个计算单元本地内存的8倍。因此,在GCN上的私有内存上有一些东西意味着最终的性能,除非资源消耗停止启动足够的波前(减少延迟隐藏)。

Nvidia也说类似的事情:"通常,访问寄存器会消耗每条指令零额外的时钟周期,但是 由于寄存器读写后依赖性和寄存器存储器,可能会发生延迟 银行冲突。 写后读取依赖性的延迟大约是24个周期,但是这样 延迟在具有至少192个活动线程的多处理器上完全隐藏 (即6次经线)。 "

还有一些鬼墙加载到本地记忆中:

    Test gpu was r7_240 so it can work with only 16x16 local threads
    so 20x20 area is loaded from global memory.
    o: each work item's target pixel
    -: needed ghost wall because of filter going out of bounds
    x: ghost corner handled by single threads (yes,non optimized)

    xx----------------xx  
    xx----------------xx
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    --oooooooooooooooo--
    xx----------------xx  
    xx----------------xx

这个内核用于高级分析:

            __constant float filter2[3][5] = {  
                        {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f},
                        {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
                        {6/256.0f, 24/256.0f, 36/256.0f, 24/256.0f, 6/256.0f},
                      };


            __kernel void test1(__global uchar4 *b2,__global uchar4 *b, __global int * p)
            {
                    int j = get_local_id(0);
                    int g = get_group_id(0);
                    int gx=g%32;
                    int gy=g/32;
                    int lx=j%16;
                    int ly=j/16;
                    int x=gx*16+lx;
                    int y=gy*16+ly;
                    if(gx<2 || gx>29 || gy <2 || gy >29)
                    {
                        b2[((y * 512) + x)] = b[((y * 512) + x)];
                        return;
                    }

                    __local uchar4 localRegion[22][22]; 
                    localRegion[lx+2][ly+2]=b[((y * 512) + x)]; // interior

                    if(lx==0) // left edges
                    {   
                        localRegion[1][ly+2]=b[(( (y) * 512) + x-1)]; // x-1 edge
                        localRegion[0][ly+2]=b[(( (y) * 512) + x-2)]; // x-2 edge
                    }
                    if(lx==15) // right edges
                    {   
                        localRegion[18][ly+2]=b[(( (y) * 512) + x+1)]; // x+1 edge
                        localRegion[19][ly+2]=b[(( (y) * 512) + x+2)]; // x+2 edge
                    }

                    if(ly==0) // top edges
                    {   
                        localRegion[lx+2][1]=b[(( (y-1) * 512) + x)]; // y-1 edge
                        localRegion[lx+2][0]=b[(( (y-2) * 512) + x)]; // y-2 edge
                    }

                    if(ly==15) // bot edges
                    {   
                        localRegion[lx+2][18]=b[(( (y+1) * 512) + x)]; // y+1 edge
                        localRegion[lx+2][19]=b[(( (y+2) * 512) + x)]; // y+2 edge
                    }

                    if(lx==0 && ly==0) // upper-left square
                    {
                        localRegion[0][0]=b[(( (y-2) * 512) + x-2)];
                        localRegion[0][1]=b[(( (y-2) * 512) + x-1)];
                        localRegion[1][0]=b[(( (y-1) * 512) + x-2)];
                        localRegion[1][1]=b[(( (y-1) * 512) + x-1)];
                    }
                    if(lx==15 && ly==0) // upper-right square
                    {
                        localRegion[18][0]=b[(( (y-2) * 512) + x+1)];
                        localRegion[18][1]=b[(( (y-1) * 512) + x+1)];
                        localRegion[19][0]=b[(( (y-2) * 512) + x+2)];
                        localRegion[19][1]=b[(( (y-1) * 512) + x+2)];
                    }
                    if(lx==15 && ly==15) // lower-right square
                    {
                        localRegion[18][18]=b[(( (y+1) * 512) + x+1)];
                        localRegion[18][19]=b[(( (y+2) * 512) + x+1)];
                        localRegion[19][18]=b[(( (y+1) * 512) + x+2)];
                        localRegion[19][19]=b[(( (y+2) * 512) + x+2)];
                    }
                    if(lx==0 && ly==15) // lower-left square
                    {
                        localRegion[0][18]=b[(( (y+1) * 512) + x-2)];
                        localRegion[0][19]=b[(( (y+2) * 512) + x-2)];
                        localRegion[1][18]=b[(( (y+1) * 512) + x-1)];
                        localRegion[1][19]=b[(( (y+2) * 512) + x-1)];
                    }

                    barrier(CLK_LOCAL_MEM_FENCE);



                   float filter[2][5] = { 
                        {4/256.0f, 16/256.0f, 24/256.0f, 16/256.0f, 4/256.0f},
                        {1/256.0f,  4/256.0f,  6/256.0f,  4/256.0f, 1/256.0f}
                    };


                    float4 acc=0;
                    for(int row=-2;row<=0;row++)
                        for(int col=-2;col<=2;col++)
                    {
                        uchar4 tmp=localRegion[lx+col+2][ly+row+2];
                        float tmp2=filter2[row+2][col+2];
                        acc+=((float4)(tmp2,tmp2,tmp2,tmp2)*(float4)((int)tmp.s0,(int)tmp.s1,(int)tmp.s2,(int)tmp.s3));
                    }
                    for(int row=1;row<=2;row++)
                        for(int col=-2;col<=2;col++)
                    {
                        uchar4 tmp=localRegion[lx+col+2][ly+row+2];
                        float tmp2=filter[row-1][col+2];
                        acc+=((float4)(tmp2,tmp2,tmp2,tmp2)*(float4)((int)tmp.s0,(int)tmp.s1,(int)tmp.s2,(int)tmp.s3));
                    }
                    b2[((y * 512) + x)] = (uchar4)(acc.x,acc.y,acc.z,244);
            }

图像为512x512,rgba(每个通道8位)。

源图像(但在作为子步骤过滤之前调整为512x512):

enter image description here

结果图片:

enter image description here

我引用的文件:

http://www.nvidia.com/content/cudazone/cudabrowser/downloads/papers/nvidia_opencl_bestpracticesguide.pdf

http://developer.amd.com/wordpress/media/2013/07/AMD_Accelerated_Parallel_Processing_OpenCL_Programming_Guide-rev-2.7.pdf

编辑:如果你真的需要__private,__ local,__ constant或__image2d_t内存来处理内核中的其他东西,你可以完全展开过滤器循环,删除过滤器数组,自己将这些araray元素放在展开的指令中(我试过,它掉了VGPR使用率为21,SGPR使用率为16)

作为参考,完全消除滤波器计算会使执行时间平均减少0.05毫秒,而所有其他版本的执行时间相同。