在OpenCL中对偏移邻域构建操作的更快方法

时间:2013-01-27 11:56:40

标签: optimization opencl gpgpu pyopencl

如何构建2D阵列的多个重叠但偏移的块上的操作,以便在OpenCL中更有效地执行?

例如,我有以下OpenCL内核:

__kernel void test_kernel(
    read_only image2d_t src,
    write_only image2d_t dest,
    const int width,
    const int height
)
{
    const sampler_t sampler =  CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
    int2 pos = (int2)(get_global_id(0), get_global_id(1));
    int2 pos0 = (int2)(pos.x - pos.x % 16, pos.y - pos.y % 16);

    uint4 diff = (uint4)(0, 0, 0, 0);

    for (int i=0; i<16; i++)
    {
        for (int j=0; j<16; j++)
        {
            diff += read_imageui(src, sampler, (int2)(pos0.x + i, pos0.y + j)) -
                read_imageui(src, sampler, (int2)(pos.x + i, pos.y + j));
        }
    }
    write_imageui(dest, pos, diff);
}

它产生正确的结果,但速度很慢...... NVS4200M只有~25 GFLOPS,1k×1k输入。 (硬件规格为155 GFLOPS)。我猜这与内存访问模式有关。每个工作项读取一个16x16数据块,该数据块与16x16区域中的所有邻居相同,并且大多数时间的另一个偏移数据块与其直接邻居的数据重叠。所有读取都是通过采样器进行的主机程序是PyOpenCL(我认为实际上没有改变任何东西),工作组大小是16x16。

编辑:以下建议的新版内核,将工作区复制到本地变量:

__kernel __attribute__((reqd_work_group_size(16, 16, 1)))
void test_kernel(
    read_only image2d_t src,
    write_only image2d_t dest,
    const int width,
    const int height
)
{
    const sampler_t sampler =  CLK_NORMALIZED_COORDS_FALSE | CLK_ADDRESS_CLAMP_TO_EDGE | CLK_FILTER_NEAREST;
    int2 pos = (int2)(get_global_id(0), get_global_id(1));

    int dx = pos.x % 16;
    int dy = pos.y % 16;

    __local uint4 local_src[16*16];
    __local uint4 local_src2[32*32];

    local_src[(pos.y % 16) * 16 + (pos.x % 16)] = read_imageui(src, sampler, pos);
    local_src2[(pos.y % 16) * 32 + (pos.x % 16)] = read_imageui(src, sampler, pos);
    local_src2[(pos.y % 16) * 32 + (pos.x % 16) + 16] = read_imageui(src, sampler, (int2)(pos.x + 16, pos.y));
    local_src2[(pos.y % 16 + 16) * 32 + (pos.x % 16)] = read_imageui(src, sampler, (int2)(pos.x, pos.y + 16));
    local_src2[(pos.y % 16 + 16) * 32 + (pos.x % 16) + 16] = read_imageui(src, sampler, (int2)(pos.x + 16, pos.y + 16));
    barrier(CLK_LOCAL_MEM_FENCE);


    uint4 diff = (uint4)(0, 0, 0, 0);

    for (int i=0; i<16; i++)
    {
        for (int j=0; j<16; j++)
        {
            diff += local_src[ j*16 + i ] - local_src2[ (j+dy)*32 + i+dx ];
        }
    }
    write_imageui(dest, pos, diff);
}

结果:输出正确,运行时间 56%。如果仅使用local_src(而不是local_src2),结果会快〜10%。

编辑:基于功能更强大的硬件,AMD Radeon HD 7850获得420GFLOPS,规格为1751GFLOPS。为了公平起见,规范是乘法加法,并且这里没有乘法因此预期为~875GFLOPS,但与理论性能相比,这仍然相当多。

编辑:为了方便任何想要尝试的人运行测试,下面是PyOpenCL中的主机端程序:

import pyopencl as cl
import numpy
import numpy.random
from time import time

CL_SOURCE = ''' 
// kernel goes here
'''

ctx = cl.create_some_context()
queue = cl.CommandQueue(ctx, properties=cl.command_queue_properties.PROFILING_ENABLE)
prg = cl.Program(ctx, CL_SOURCE).build()

h, w = 1024, 1024
src = numpy.zeros((h, w, 4), dtype=numpy.uint8)
src[:,:,:] = numpy.random.rand(h, w, 4) * 255

mf = cl.mem_flags
src_buf = cl.image_from_array(ctx, src, 4)
fmt = cl.ImageFormat(cl.channel_order.RGBA, cl.channel_type.UNSIGNED_INT8)
dest_buf = cl.Image(ctx, mf.WRITE_ONLY, fmt, shape=(w, h))

# warmup
for n in range(10):
    event = prg.test_kernel(queue, (w, h), (16,16), src_buf, dest_buf, numpy.int32(w), numpy.int32(h))
event.wait()

# benchmark
t1 = time()
for n in range(100):
    event = prg.test_kernel(queue, (w, h), (16,16), src_buf, dest_buf, numpy.int32(w), numpy.int32(h))
event.wait()
t2 = time()
print "Duration (host): ", (t2-t1)/100
print "Duration (event): ", (event.profile.end-event.profile.start)*1e-9

编辑:考虑到内存访问模式,原始的天真版本可能相当不错;当调用read_imageui(src, sampler, (int2)(pos0.x + i, pos0.y + j))工作组中的所有工作项读取相同的位置时(所以这只是一个读取??),并且当调用read_imageui(src, sampler, (int2)(pos.x + i, pos.y + j))时,它们正在读取顺序位置(因此读取可以是完美合并??)。

2 个答案:

答案 0 :(得分:6)

这绝对是一个内存访问问题。相邻工作项的像素可以重叠15x16,更糟糕的是,每个工作项至少会重叠225个。

我会使用本地内存并让工作组协同处理许多16x16块。我喜欢为每个工作组使用一个大的方块。矩形块有点复杂,但可以为您提供更好的内存利用率。

如果从源图像中读取n×n像素的块,则边界将重叠nx15(或15xn)。您需要根据可用的本地内存大小(LDS)计算n的最大可能值。如果您使用的是opencl 1.1或更高版本,则LDS至少为32kb。 opencl 1.0承诺每个工作组16kb。

n <= sqrt(32kb / sizeof(uint4))
n <= sqrt(32768 / 16)
n ~ 45

使用n = 45将使用327S字节的LDS中的32400,并允许您每组使用900个工作项(45-15)^ 2 = 900.注意:这是矩形块可以帮助的地方;例如64x32将使用所有LDS,但组大小=(64-15)*(32-15)= 833。

为内核使用LDS的步骤:

  1. 为图像的缓存块分配1D或2D本地数组。我使用#define常量,很少需要更改。
  2. 从您的图片中读取uint值,并在本地存储。
  3. 调整每个工作项的'pos'以与本地内存相关
  4. 执行相同的i,j循环,但使用本地内存读取值。记住,i和j循环停止了n。
  5. 如果您不确定如何实施,可以在线搜索每个步骤,或者您可以询问我是否需要帮忙。

    设备上的LDS优于纹理读取速度的可能性很大。这是违反直觉的,但请记住,您一次只能读取少量数据,因此gpu可能无法有效地缓存像素。 LDS的使用将保证像素可用,并且考虑到每个像素的读取次数,我希望这会产生巨大的差异。

    请告诉我您观察到的结果。

    更新:这是我尝试更好地解释我的解决方案。我使用方格纸作为我的绘图,因为我对图像处理软件并不是那么好。

    How values were originally from 'src'

    上面是如何在第一个代码段中从src读取值的草图。最大的问题是pos0矩形--16x16 uint4值 - 正在为组中的每个工作项(其中256个)完整地读取。我的解决方案涉及读取大面积区域并共享所有256个工作组的数据。

    enter image description here

    如果将图像的31x31区域存储在本地存储器中,则所有256个工作项的数据都将可用。

    步骤进行:

    • 使用工作组维度:(16,16)
    • 将src的值读入一个大的本地缓冲区,即:uint4 buff [31] [31];需要翻译缓冲区,使'pos0'处于buff [0] [0]
    • 屏障(CLK_LOCAL_MEM_FENCE)等待内存复制操作
    • 为你原来的循环做同样的i,j,除了你省略了pos和pos0值。只使用i和j作为位置。以与原来相同的方式累积'diff'。
    • 将解决方案写入'dest'

    这与我对您的问题的第一次回复相同,除了我使用n = 16。此值不会完全利用本地内存,但可能适用于大多数平台。 256往往是一个共同的最大工作组大小。

    我希望这能为你解决问题。

答案 1 :(得分:1)

一些建议:

  • 计算每个工作项中的多个输出像素。它将增加数据重用。
  • 对不同的工作组大小进行基准测试,以最大限度地利用纹理缓存。
  • 也许有办法将内核分成两个通道(水平和垂直)。

更新:更多建议

不是将所有内容都加载到本地内存中,而是尝试仅加载local_src值,并将read_image用于另一个值。

由于您几乎不进行任何计算,因此您应该以GB / s为单位测量读取速度,并与峰值内存速度进行比较。