在OpenCL

时间:2017-11-22 10:16:37

标签: parallel-processing opencl gpu sequences

一维数据集分为几个部分,每个工作项处理一个部分。它从段中读取了许多元素?事先不知道元素的数量,并且每个片段都有所不同。

例如:

+----+----+----+----+----+----+----+----+----+     <-- segments
  A    BCD  E    FG  HIJK   L    M        N        <-- elements in this segment

在完成所有段之后,他们应该在连续输出内存中编写elements,例如

A B C D E F G H I J K L M N

因此,来自一个段的元素的绝对输出位置取决于先前段中的元素数量。 E位于第4位,因为segment包含1个元素(A),而segment 2包含3个元素。

OpenCL内核将每个段的元素数写入本地/共享内存缓冲区,并且这样工作(伪代码)

kernel void k(
    constant uchar* input,
    global int* output,
    local int* segment_element_counts
) {
    int segment = get_local_id(0);
    int count = count_elements(&input[segment * segment_size]);

    segment_element_counts[segment] = count;

    barrier(CLK_LOCAL_MEM_FENCE);

    ptrdiff_t position = 0;
    for(int previous_segment = 0; previous_segment < segment; ++previous_segment)
        position += segment_element_counts[previous_segment];

    global int* output_ptr = &output[position];
    read_elements(&input[segment * segment_size], output_ptr);
}

因此,每个工作项必须使用循环计算部分和,其中具有较大id的工作项执行更多迭代。

在OpenCL 1.2中,是否有更有效的方法来实现这一点(每个工作项计算一个序列的部分和,直到其索引)? OpenCL 2似乎为此提供了work_group_scan_inclusive_add

1 个答案:

答案 0 :(得分:2)

您可以使用以下内容在log2(N)迭代中执行N个部分(前缀)和:

offsets[get_local_id(0)] = count;
barrier(CLK_LOCAL_MEM_FENCE);

for (ushort combine = 1; combine < total_num_segments; combine *= 2)
{
    if (get_local_id(0) & combine)
    {
        offsets[get_local_id(0)] +=
            offsets[(get_local_id(0) & ~(combine * 2u - 1u)) | (combine - 1u)];
    }
    barrier(CLK_LOCAL_MEM_FENCE);
}

给出

的段元素计数
a     b     c        d

连续迭代将产生:

a     b+a   c        d+c

a     b+a   c+(b+a)  (d+c)+(b+a)

我们想要的结果是什么。

所以在第一次迭代中,我们将段元素计数分成2组,并在它们之内求和。然后我们将两组一次合并为4个元素,并将结果从第一组传播到第二组。我们再次将这些组织增加到8,依此类推。

关键的观察是这个模式也匹配每个段的索引的二进制表示:

0: 0b00  1: 0b01  2: 0b10  3: 0b11

索引0不执行任何总和。索引1和3都在第一次迭代中执行求和(位0 / LSB = 1),而索引2和3在第二次迭代中执行求和(位1 = 1)。这解释了这一行:

    if (get_local_id(0) & combine)

真正需要解释的另一个陈述当然是

        offsets[get_local_id(0)] +=
            offsets[(get_local_id(0) & ~(combine * 2u - 1u)) | (combine - 1u)];

计算我们想要累积到工作项总和上的前一个前缀和的索引有点棘手。子表达式(combine * 2u - 1u)在每次迭代时取值(2 n -1)(对于从1开始的n):

1 = 0b001
3 = 0b011
7 = 0b111
…

通过逐位屏蔽这些位后缀(即i & ~x)工作项索引,这将为您提供当前组中第一个项的索引。

然后(combine - 1u)子表达式为您提供上半部分最后一项的当前组内的索引。将两者放在一起可以得到您想要累积到当前细分中的项目的整体索引。

结果中有一个轻微的丑陋:它向左移一个:所以段1需要使用offsets[0],依此类推,而段0的偏移当然是0.你可以覆盖 - 将偏移量数组分配1,并在索引1开始的子阵列上执行前缀和,并将索引0初始化为0,或使用条件。

您可以对上述代码进行分析驱动的微优化。