我使用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的图片中说明它。
因此,以下情况属实:
N-1
执行result[i]
次操作(可能的并发写入)
result[i]
将从减法和N-(i+1)
添加内容i
次写入P[i][j]
开始,r[j]
减去r[i]
这是主要问题的出现地点:
每个线程块都有一个unqiue,共享结果向量的想法也是不可能的。 (50k中的N个产生25亿个P元素,因此[假设每个块最多1024个线程],如果每个块都有自己的具有50k双元素的结果数组,则最少240万个块消耗超过900GiB的内存。)
我认为我可以处理更多静态行为的减少,但就潜在的并发内存写访问而言,这个问题相当动态。 (或者是否可以通过一些"基本"减少类型来处理它?)
不幸的是,根据(任意用户)输入,它与初始值无关,需要跳过P的某些元素。 让我们假设我们需要跳过排列P [6],P [14]和P [18]。因此,我们剩下24个组合,需要进行计算。
如何告诉内核需要跳过哪些值? 我提出了三种方法,如果N非常大(如几万个元素),每个方法都有明显的缺点。
...使用各自的行和列索引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的每个元素,这是第二种方法的缺点。
如果我想对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_row
,current_col
和if-branch的其他计算是非常低效。
(请记住,我们仍在讨论数十亿次评估。)
我的另一个想法是独立计算所有有效和无效的组合。 但不幸的是,由于总和错误,以下陈述是正确的:
calc_non_skipped() != calc_all() - calc_skipped()
是否有一种方便,已知,高效的方法可以从初始值中获得所需的结果?
我知道这个问题相当复杂,相关性可能有限。不过,我希望一些启发性的答案能帮助我解决我的问题。
目前,这是作为带有OpenMP的CPU代码实现的。
我首先建立了一个上述combo
的向量,存储了每个需要计算的P并将其传递给并行for循环。
每个线程都有一个私有结果向量,并行区域末尾的临界区用于正确求和。
答案 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;
}
请注意以下事项:
init_values
和函数f
的语义如问题所述。result
数组中的一个条目;具体来说,它会计算result[my_id]
,因此您应该启动N
个实例。result[my_id]
。好吧,上面的功能并没有写入任何东西,但是如果你把它翻译成CUDA,我想你最后都要写那个。但是,没有其他人写入result
的特定元素,因此此写操作不会导致数据争用的任何争用。init_values
和skipped_indices
在此函数的所有正在运行的实例之间共享。 skipped_indices
包含应在每行中跳过的索引列表。它的内容和结构如上所述,只有一个小的优化。由于没有必要,我删除了行号并只留下了列。无论如何,行号将作为my_id
传递给函数,并且使用skipped_indices
和skip_begin
确定每个调用应使用的skip_end
数组的片段。
对于上面的示例,传递到calc_one_result
的所有调用的数组将如下所示:[4, 6, 0, 5, 4, 3]
。
skip_indices[i] < my_id
。虽然我相信这是无害且完全可预测的,但在代码中也可以轻松避免这个分支。我们只需要传入另一个名为skip_middle
的参数,它告诉我们跳过的项目在哪里穿过主对角线(即对于行#my_id
,skipped_indices[skip_middle]
处的索引是第一个更大的比my_id
。)我绝不是CUDA和HPC的专家。但如果我正确地理解了你的问题,我认为这种方法可能会消除对内存的任何争议。此外,我不认为这会导致任何(更多)数值稳定性问题。
实现这个的成本是:
f
两次(并跟踪row < col
的调用时间,以便您可以将结果乘以-1
。)( UPDATE :添加了伪实现部分。)