高效布局和减少虚拟2d数据(摘要)

时间:2013-05-06 12:54:46

标签: c++ c cuda

我使用C ++和CUDA / C并且想要为特定问题编写代码,我遇到了一个相当棘手的减少问题。

我在并行编程方面的经验不容忽视但非常有限,我无法完全预见到这个问题的特殊性。 我怀疑有一个方便的,甚至是#34; easy"处理我面临的问题的方法,但也许我错了。 如果有任何资源(即文章,书籍,网络链接,......)或关键词来解决此类或类似问题,请告知我们。

我试图尽可能地概括整个案例并保持抽象而不是发布太多代码。

布局......

我有一个N初始元素和N个结果元素的系统。 (例如,我使用N = 8,但N可以是大于3的任何整数值。)

static size_t const N = 8;
double init_values[N], result[N];

我需要计算几乎所有(并非所有我害怕的)init值的唯一排列,而不会产生自干扰。

这意味着计算f(init_values[0],init_values[1])f(init_values[0],init_values[2]),...,f(init_values[0],init_values[N-1])f(init_values[1],init_values[2]),...,f(init_values[1],init_values[N-1]),...等等

这实际上是一个虚拟的三角形矩阵,其形状如下图所示。

 P     0    1    2    3    4    5    6    7
   |---------------------------------------
  0|   x 
   |
  1|   0    x
   |
  2|   1    2    x 
   |
  3|   3    4    5    x
   |
  4|   6    7    8    9    x
   |
  5|  10   11   12   13   14    x
   |
  6|  15   16   17   18   19   20    x
   |
  7|  21   22   23   24   25   26   27    x

每个元素都是init_values中相应列和行元素的函数。

P[i] (= P[row(i)][col(i]) = f(init_values[col(i)], init_values[row(i)])

P[11] (= P[5][1]) = f(init_values[1], init_values[5])

(N*N-N)/2 = 28种可能的唯一组合(注意:P[1][5]==P[5][1],因此我们只使用示例N = 8来获得较低(或较高)的三角矩阵。

基本问题

结果数组从P计算为行元素之和减去各列元素之和。 例如,位置3的结果将计算为第3行减去第3列之和的总和。

result[3] = (P[3]+P[4]+P[5]) - (P[9]+P[13]+P[18]+P[24])
result[3] = sum_elements_row(3) - sum_elements_column(3)

我试图在N = 4的图片中说明它。

Required triangluar reduction scheme

因此,以下情况属实:

    将对每个N-1 执行
  • result[i]次操作(可能的并发写入)
  • result[i]将从减法和N-(i+1)添加内容i次写入
  • P[i][j]开始,r[j]减去r[i]

这是主要问题的出现地点:

  • 使用一个线程计算每个P并直接更新结果将导致多个内核尝试写入相同的结果位置(每个N-1个线程)。
  • 另一方面,存储整个矩阵P用于后续的缩减步骤在内存消耗方面非常昂贵,因此对于非常大的系统来说不可能

每个线程块都有一个unqiue,共享结果向量的想法也是不可能的。 (50k中的N个产生25亿个P元素,因此[假设每个块最多1024个线程],如果每个块都有自己的具有50k双元素的结果数组,则最少240万个块消耗超过900GiB的内存。)

我认为我可以处理更多静态行为的减少,但就潜在的并发内存写访问而言,这个问题相当动态。 (或者是否可以通过一些"基本"减少类型来处理它?)

增加一些并发症......

不幸的是,根据(任意用户)输入,它与初始值无关,需要跳过P的某些元素。 让我们假设我们需要跳过排列P [6],P [14]和P [18]。因此,我们剩下24个组合,需要进行计算。

如何告诉内核需要跳过哪些值? 我提出了三种方法,如果N非常大(如几万个元素),每个方法都有明显的缺点。

1。存储所有组合......

...使用各自的行和列索引struct combo { size_t row,col; };,需要在vector<combo>中计算并对此向量进行操作。 (由当前实施使用)

std::vector<combo> elements;
// somehow fill
size_t const M = elements.size();
for (size_t i=0; i<M; ++i)
{
    // do the necessary computations using elements[i].row and elements[i].col  
}

此解决方案消耗大量内存,因为只有几个&#34; (甚至可能是成千上万的元素,但与总数几十亿相比并不多)但它避免了

  • 指数化计算
  • 找到已删除的元素

对于P的每个元素,这是第二种方法的缺点。

2。对P的所有元素进行操作并找到已删除的元素

如果我想对P的每个元素进行操作并避免嵌套循环(我无法在cuda中很好地再现)我需要做这样的事情:

size_t M = (N*N-N)/2;
for (size_t i=0; i<M; ++i)
{
   // calculate row indices from `i`
   double tmp = sqrt(8.0*double(i+1))/2.0 + 0.5;
   double row_d = floor(tmp);
   size_t current_row = size_t(row_d);
   size_t current_col = size_t(floor(row_d*(ict-row_d)-0.5));
   // check whether the current combo of row and col is not to be removed
   if (!removes[current_row].exists(current_col))
   {
     // do the necessary computations using current_row and current_col
   }
}

与第一个示例中的removes向量相比,向量elements非常小,但获取current_rowcurrent_col和if-branch的其他计算是非常低效。 (请记住,我们仍在讨论数十亿次评估。)

3。操作P的所有元素,然后删除元素

我的另一个想法是独立计算所有有效和无效的组合。 但不幸的是,由于总和错误,以下陈述是正确的:

calc_non_skipped() != calc_all() - calc_skipped()

是否有一种方便,已知,高效的方法可以从初始值中获得所需的结果?

我知道这个问题相当复杂,相关性可能有限。不过,我希望一些启发性的答案能帮助我解决我的问题。


当前的实施

目前,这是作为带有OpenMP的CPU代码实现的。 我首先建立了一个上述combo的向量,存储了每个需要计算的P并将其传递给并行for循环。 每个线程都有一个私有结果向量,并行区域末尾的临界区用于正确求和。

2 个答案:

答案 0 :(得分:6)

首先,我感到疑惑为什么(N**2 - N)/2为N = 7时产生了27 ...但对于索引0-7,N = 8,并且有 28 元素P.不应该在当天晚些时候尝试回答这样的问题。 : - )

但是对于一个潜在的解决方案:你是否需要保持阵列P用于任何其他目的?如果没有,我认为你可以只用两个中间数组得到你想要的结果,每个数组的长度为N:一个用于行的总和,一个用于列的总和。

这是一个快速而肮脏的例子,我认为你正在尝试做什么(子例程direct_approach())以及如何使用中间数组(子例程refined_approach())来实现相同的结果:< / p>

#include <cstdlib>
#include <cstdio>

const int N = 7;
const float input_values[N] = { 3.0F, 5.0F, 7.0F, 11.0F, 13.0F, 17.0F, 23.0F };
float P[N][N];      // Yes, I'm wasting half the array.  This way I don't have to fuss with mapping the indices.
float result1[N] = { 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F };
float result2[N] = { 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F };

float f(float arg1, float arg2)
{
    // Arbitrary computation
    return (arg1 * arg2);
}

float compute_result(int index)
{
    float row_sum = 0.0F;
    float col_sum = 0.0F;
    int row;
    int col;

    // Compute the row sum
    for (col = (index + 1); col < N; col++)
    {
        row_sum += P[index][col];
    }

    // Compute the column sum
    for (row = 0; row < index; row++)
    {
        col_sum += P[row][index];
    }

    return (row_sum - col_sum);
}

void direct_approach()
{
    int row;
    int col;

    for (row = 0; row < N; row++)
    {
        for (col = (row + 1); col < N; col++)
        {
            P[row][col] = f(input_values[row], input_values[col]);
        }
    }

    int index;

    for (index = 0; index < N; index++)
    {
        result1[index] = compute_result(index);
    }
}

void refined_approach()
{
    float row_sums[N];
    float col_sums[N];
    int index;

    // Initialize intermediate arrays
    for (index = 0; index < N; index++)
    {
        row_sums[index] = 0.0F;
        col_sums[index] = 0.0F;
    }

    // Compute the row and column sums
    // This can be parallelized by computing row and column sums
    //  independently, instead of in nested loops.
    int row;
    int col;

    for (row = 0; row < N; row++)
    {
        for (col = (row + 1); col < N; col++)
        {
            float computed = f(input_values[row], input_values[col]);
            row_sums[row] += computed;
            col_sums[col] += computed;
        }
    }

    // Compute the result
    for (index = 0; index < N; index++)
    {
        result2[index] = row_sums[index] - col_sums[index];
    }
}

void print_result(int n, float * result)
{
    int index;

    for (index = 0; index < n; index++)
    {
        printf("  [%d]=%f\n", index, result[index]);
    }
}

int main(int argc, char * * argv)
{
    printf("Data reduction test\n");

    direct_approach();

    printf("Result 1:\n");
    print_result(N, result1);

    refined_approach();

    printf("Result 2:\n");
    print_result(N, result2);

    return (0);
}

并行化计算并不容易,因为每个中间值都是大多数输入的函数。您可以单独计算总和,但这意味着多次执行f(...)。对于非常大的N值,我能想到的最好的建议是使用更多的中间数组,计算结果的子集,然后对部分数组求和以得到最终的总和。当我不那么累的时候,我不得不考虑那个。

要解决跳过问题:如果只是“不要使用输入值x,y和z”,您可以将x,y和z存储在do_not_use数组中并检查这些值循环计算总和。如果要跳过的值是行和列的某个函数,则可以将它们存储为对并检查对。

希望这能为您提供解决方案的想法!

更新,现在我已经醒了:处理“跳过”很大程度上取决于需要跳过哪些数据。第一种情况的另一种可能性 - “不使用输入值x,y和z” - 对于大型数据集来说,更快的解决方案是添加一个间接级别:创建另一个数组,这是整数索引之一,并仅存储 good 输入的索引。例如,如果输入2和5中的无效数据,则有效数组将为:

int valid_indices[] = { 0, 1, 3, 4, 6 };

对数组valid_indices进行交互,并使用这些索引从输入数组中检索数据以计算结果。在另一个爪子上,如果要跳过的值取决于P数组的两个索引,我看不出如何避免某种查找。

回到并行化 - 无论如何,你将处理(N ** 2 - N)/ 2计算 of f()。一种可能性就是接受对这笔款项的争论 数组,如果计算f()需要的时间长得多,那么这不是一个大问题 两个补充。当你到达非常大量的并行路径时,争用将会发生 再次成为一个问题,但应该有一个平衡并行数量的“甜蜜点” 计算f()所需时间的路径。

如果争用仍然存在问题,您可以通过多种方式对问题进行分区。一种方法是 一次计算一行或一列:对于一次一行,每列总和可以 独立计算,每个行总和可以保留一个运行总计。

另一种方法是将数据空间分开,从而将计算分成 子集,其中每个子集都有自己的行和列和数组。每个街区之后 计算后,可以将独立数组相加以生成所需的值 计算结果。

答案 1 :(得分:3)

这可能是那些天真无用的答案之一,但它也可能有所帮助。随意告诉我,我完全错了,我误解了整个事件。

所以......我们走了!

基本问题

在我看来,你可以稍微改变一下你的结果函数,它会至少解除你的中间值的一些争用。我们假设您的P矩阵是低三角形的。如果你(虚拟地)用较低值的负数(以及全部为零的主对角线)填充上三角形,那么你可以将结果的每个元素重新定义为单行的总和:(此处显示为N = 4 ,-i表示标记为i

的单元格中值的负数
 P     0    1    2    3 
   |--------------------
  0|   x   -0   -1   -3
   |
  1|   0    x   -2   -4
   |
  2|   1    2    x   -5
   |
  3|   3    4    5    x

如果启动独立线程(执行相同的内核)来计算此矩阵的每一行的总和,则每个线程将写入一个结果元素。看起来您的问题大小足以使您的硬件线程饱和并使它们保持忙碌。

当然,需要注意的是,您将每次f(x, y)计算两次。我不知道那是多么昂贵,或者记忆争用在多大程度上使你付出了代价,所以我无法判断这是否值得做出权衡取舍。但除非f确实非常昂贵,否则我认为可能会这样。

跳过值

您提到您可能在计算中需要忽略P矩阵的数万个元素(实际上跳过它们。)

为了使用我上面提出的方案,我相信你应该将跳过的元素存储为(row, col)对,并且你必须添加每个坐标对的转置(所以你&#39; ll有两倍的跳过值。)因此,P[6], P[14] and P[18]的示例跳过列表变为P(4,0), P(5,4), P(6,3),然后变为P(4,0), P(5,4), P(6,3), P(0,4), P(4,5), P(3,6)

然后排序此列表,首先基于行然后列。这使我们的列表为P(0,4), P(3,6), P(4,0), P(4,5), P(5,4), P(6,3)

如果虚拟P矩阵的每一行都由一个线程(或内核的单个实例或其他任何一个)处理,您可以传递它需要跳过的值。就个人而言,我会将所有这些存储在一个大的1D数组中,然后传入每个线程需要查看的第一个和最后一个索引(我也不会将行索引存储在我传入的最终数组中,因为它可以隐式推断,但我认为很明显。)在上面的例子中,对于N = 8,传递给每个线程的开始和结束对将是:(注意结束是超过最终值所需的一个处理,就像STL一样,所以空列表用begin == end)

表示
Thread 0: 0..1
Thread 1: 1..1 (or 0..0 or whatever)
Thread 2: 1..1
Thread 3: 1..2
Thread 4: 2..4
Thread 5: 4..5
Thread 6: 5..6
Thread 7: 6..6

现在,每个线程继续计算并汇总一行中的所有中间值。当它逐步遍历列的索引时,它也会逐步浏览此跳过的值列表并跳过列表中出现的任何列号。这显然是一种高效而简单的操作(因为列表也按列排序。它就像合并一样。)

伪实施

我不了解CUDA,但我有一些使用OpenCL的经验,我认为接口是相似的(因为他们所针对的硬件是相同的。)这是内核的一个实现在伪C ++中执行行处理(即计算result的一个条目):

double calc_one_result (
    unsigned my_id, unsigned N, double const init_values [],
    unsigned skip_indices [], unsigned skip_begin, unsigned skip_end
)
{
    double res = 0;
    for (unsigned col = 0; col < my_id; ++col)
        // "f" seems to take init_values[column] as its first arg
        res += f (init_values[col], init_values[my_id]);
    for (unsigned row = my_id + 1; row < N; ++row)
        res -= f (init_values[my_id], init_values[row]);
    // At this point, "res" is holding "result[my_id]",
    // including the values that should have been skipped

    unsigned i = skip_begin;
    // The second condition is to check whether we have reached the
    // middle of the virtual matrix or not
    for (; i < skip_end && skip_indices[i] < my_id; ++i)
    {
        unsigned col = skip_indices[i];
        res -= f (init_values[col], init_values[my_id]);
    }
    for (; i < skip_end; ++i)
    {
        unsigned row = skip_indices[i];
        res += f (init_values[my_id], init_values[row]);
    }

    return res;
}

请注意以下事项:

  1. init_values和函数f的语义如问题所述。
  2. 此函数计算result数组中的一个条目;具体来说,它会计算result[my_id],因此您应该启动N个实例。
  3. 它写入的唯一共享变量是result[my_id]。好吧,上面的功能并没有写入任何东西,但是如果你把它翻译成CUDA,我想你最后都要写那个。但是,没有其他人写入result的特定元素,因此此写操作不会导致数据争用的任何争用。
  4. 两个输入数组init_valuesskipped_indices在此函数的所有正在运行的实例之间共享。
  5. 所有对数据的访问都是线性的和顺序的,除了跳过的值,我认为这是不可避免的。
  6. skipped_indices包含应在每行中跳过的索引列表。它的内容和结构如上所述,只有一个小的优化。由于没有必要,我删除了行号并只留下了列。无论如何,行号将作为my_id传递给函数,并且使用skipped_indicesskip_begin确定每个调用应使用的skip_end数组的片段。

    对于上面的示例,传递到calc_one_result的所有调用的数组将如下所示:[4, 6, 0, 5, 4, 3]

  7. 如您所见,除了循环之外,此代码中唯一的条件分支是第三个for循环中的skip_indices[i] < my_id。虽然我相信这是无害且完全可预测的,但在代码中也可以轻松避免这个分支。我们只需要传入另一个名为skip_middle的参数,它告诉我们跳过的项目在哪里穿过主对角线(即对于行#my_idskipped_indices[skip_middle]处的索引是第一个更大的比my_id。)
  8. 结论

    我绝不是CUDA和HPC的专家。但如果我正确地理解了你的问题,我认为这种方法可能会消除对内存的任何争议。此外,我不认为这会导致任何(更多)数值稳定性问题。

    实现这个的成本是:

    • 总共调用f两次(并跟踪row < col的调用时间,以便您可以将结果乘以-1。)
    • 跳过的值列表中存储两倍的项目。由于这个列表的大小是数千(而不是数十亿!),它不应该是一个大问题。
    • 对跳过的值列表进行排序;再次由于它的大小,应该没问题。

    UPDATE :添加了伪实现部分。)