我并行生成一系列随机数,但根据我调用的线程数,我会得到不同的结果。由此我得出结论,我在某个地方犯了错误!
注意我使用相同的种子,这与线程数无关 - 所以结果应该相同!
这是MWE,它生成一个介于0..1之间的数字,如果生成的变量大于0.5,则递增一个变量:
#include <iostream>
#include <fstream>
#include <vector>
#include <cmath>
#include "omp.h"
#include <random>
typedef std::uniform_real_distribution<double> distr_uni;
#define max_threads 1
using namespace std;
int main(int argc, char* argv[])
{
int reservoir_counter, accepted_tube=0;
double r;
omp_set_num_threads(max_threads);
#pragma omp parallel
{
mt19937 eng(0);
distr_uni uniform(0, 1);
#pragma omp for private(r, reservoir_counter) reduction(+:accepted_tube)
for(reservoir_counter=0; reservoir_counter<100000; reservoir_counter++)
{
r = uniform(eng);
if(r>0.5)
{
accepted_tube++;
}
}
}
cout << accepted_tube << endl;
return 0;
}
当我设置max_threads=1
时,我得到50027,但是当max_threads=60
(在支持它的机器上......)时,我得到50440。
有人可以发现我的错误吗?敏感的RNG及其引擎我已在并行化区域内声明,所以我不太清楚错误可能在哪里。
答案 0 :(得分:4)
随机数生成器,尤其是Mersenne Twister(您正在使用上面的内容),通常是非常连续的生物。对随机数生成器的每次调用都具有将种子更新为下一个内部状态的副作用。它们生成随机数序列,使得对RNG的第N次调用产生相对于种子的已知值。因此,您对unique(eng)
的调用会产生副作用,我认为这会导致OMP并行循环中出现未指定的行为。 (快速,如果草率,谷歌搜索似乎证实了这一点。)
例如,IBM的文档使用了这个术语:在评估这些表达式时不执行同步,并且评估的副作用可能会导致不确定的值。
您应该能够轻松地测试这个假设。编写一个循环的并行和串行版本,只做这个:
for (i = 0; i < N; i++)
a[i] = uniform(eng);
这记录了uniform(eng)
生成的确切值序列。对于串行循环中的顺序RNG,此顺序非常确定。给定相同的起始种子,第N个项始终具有相同的值。对于并行版本,在eng
周围没有额外的锁定或其他同步,我怀疑你会得到重复的值。如果你添加一个锁,我怀疑你会获得相同的值集,但是处于不确定的顺序。
如果你想在并行for
构造中确定性地使用大量随机数,我能想到的唯一安全的方法就是将它们预先生成一个大型数组,或者根据循环索引计算散列函数。这些方法中的任何一种都缺乏大多数典型RNG引擎带来的排序依赖性。
你可以尝试将顺序RNG包装在一个锁中,但即使你从RNG可靠地生成相同的序列,你仍然会以不确定的顺序访问随机数。这是因为RNG输出序列到循环迭代次数的映射可以根据线程到达RNG的顺序而改变。
对于这个特殊的例子(计算高于0.5的值的数量),顺序并不重要,因为你将进行单一的大幅减少。您可以自由重新排序添加数字序列的顺序。这就是为什么OpenMP要求你减少的原因。对于其他计算,顺序可以很好地产生差异,包括没有明显顺序依赖的计算。例如,考虑随机抖动。这是一个非常简单的版本。 (假设在[0,0.5)上均匀分布。)
for (i = 0; i < N; i++)
a[i] = b[i] + uniform(eng) > 1 ? 1 : 0;
你得到的确切抖动取决于随机数到循环索引的精确映射。如果您使用串行RNG,那么您会引入原始算法缺少的序列依赖性,即使您在RNG周围锁定以使其可靠。
编辑:正如其他人在上面指出的那样,在这种特殊情况下,RNG被声明为并行块的本地,因此从技术上讲,它们不受比赛影响。我错过了那种微妙之处,但它没有改变我的核心观点。基本问题仍然存在:顺序程序中的随机值集与并行程序中的随机值集不匹配,因为顺序RNG没有在串行和并行程序之间以相同方式顺序调用。
如果您将并行块的RNG声明为本地,以便获得T个线程的T个并行实例,则每个并行线程将看到相同的随机数序列。如果线程都迭代相同的次数(很可能),那么你会看到N / T随机数重复T次,而不是N个唯一的随机数。
编辑上方的核心点仍然存在。为了在同一程序的并行和串行版本中获得相同的结果,并行循环中的操作不会产生副作用。从像Mersenne Twister这样的连续RNG中提取随机数本身就会产生副作用。您需要在循环外生成随机数,或使用其他没有副作用的方法。 (散列循环迭代次数和Perlin噪声都是示例。)