确定性模型运行中的小而不可预测的结果

计算科学 浮点 精确 计算机算术
2021-12-16 07:26:21

我有一个用 C 编写的相当大的模型(约 5000 行)。它是一个串行程序,在任何地方都没有随机数生成。它使用 FFTW 库来处理使用 FFT 的函数——我不知道 FFTW 实现的细节,但我认为其中的函数也是确定性的(如果我出错了,请纠正我)。

我无法理解的问题是,在同一台机器(相同的编译器,相同的库)上运行相同的结果我得到了微小的差异。

我使用双精度变量,并将结果输出到变量value中,例如,我发出: fprintf(outFID, "%.15e\n", value);
fwrite(&value, 1, sizeof(double), outFID);

而且我会不断得到差异,例如:
2.07843469652206 4 e-16 vs. 2.07843469652206 3 e-16

我花了很多时间试图弄清楚为什么会这样。我最初以为我的一个内存芯片坏了,我已经订购并更换了它们,但无济于事。随后我还尝试在同事的 Linux 机器上运行我的代码,我得到了相同性质的差异。

这可能是什么原因造成的?现在这是一个小问题,但我想知道它是否是“冰山一角”(一个严重的问题)。

我想我会在这里而不是 StackOverflow 发帖,以防使用数值模型的人可能遇到这个问题。如果有人能阐明这一点,我将不胜感激。

评论跟进:
Christian Clason 和 Vikram:首先,感谢您关注我的问题。您链接到的文章表明:1.舍入误差限制了准确性,2.不同的代码(例如引入看似无害的打印语句)可能会影响机器epsilon的结果。我应该澄清一下,我不是在比较效果fwritefprintf功能。我正在使用一个或另一个。特别是,两次运行都使用相同的可执行文件。我只是说明无论我使用fprintfOR都会出现问题fwrite

所以代码路径(和可执行文件)是一样的,硬件也是一样的。在所有这些外部因素保持不变的情况下,从根本上说,随机性从何而来?我怀疑位翻转的发生是由于内存故障没有正确保留位,这就是我更换内存芯片的原因,但这似乎不是问题,我验证了,你指出了。我的程序在一次运行中输出了数千个这样的双精度数字,并且总是有少数随机位翻转。

跟进 Christian Clason 的第一条评论:为什么在机器精度内与 0 相同?双精度的最小正数是 2.22e-308,所以它不应该等于 0 吗?我的程序输出了 10^-16 范围内的数千个值(范围从 1e-15 到 8e-17),我们在研究项目中看到了有意义的变化,所以我希望我们没有在看荒谬的数字。21016

后续#2
这是模型输出的时间序列图,以帮助评论中的分支讨论。 在此处输入图像描述

4个回答

现代计算系统的某些方面本质上是不确定的,可能会导致这些差异。只要与解决方案所需的精度相比差异非常小,就可能没有任何理由担心这一点。

根据我自己的经验,一个可能出错的例子。考虑计算两个向量 x 和 y 的点积的问题。

d=i=1nxiyi

计算这个点积需要计算乘积,然后将结果相加。浮点乘法每次都应该产生完全相同的结果。如果每次都以相同的顺序计算浮点加法,则总和应该相同。但是,由于浮点加法不是关联的,如果以不同顺序执行加法的方式计算两个向量的乘积,您可能会得到不同的结果。 xiyi

例如,您可能首先计算两个向量的乘积为

d=((x1y1)+(x2y2))+(x3y3)

然后作为

d=(x1y1)+((x2y2)+(x3y3))

这怎么可能发生?这里有两种可能性。

  1. 并行内核上的多线程计算。现代计算机通常具有 2、4、8 甚至更多可以并行工作的处理器内核。如果您的代码使用并行线程在多个处理器上计算点积,那么系统的任何随机扰动(例如,用户移动他的鼠标并且其中一个处理器内核必须在返回到点积之前处理该鼠标移动)都可能导致添加顺序发生变化。

  2. 数据和向量指令的对齐。现代英特尔处理器具有一组特殊的指令,可以一次操作(例如)浮点数。如果数据在 16 字节边界上对齐,则这些向量指令效果最佳。通常,点积循环会将数据分成 16 个字节的部分(一次 4 个浮点数)。如果再次运行代码,则数据可能与 16 字节的内存块对齐方式不同,因此添加的内容是以不同的顺序执行,导致不同的答案。

您可以通过使您的代码作为单线程运行并禁用所有并行处理来解决第 1 点。您可以通过要求内存分配来对齐内存块来解决第 2 点(通常,您可以通过使用 -align 之类的开关编译代码来做到这一点。)如果您的代码仍然给出不同的结果,那么还有其他可能性可以查看在。

来自英特尔的这份文档讨论了可能导致使用英特尔数学核心函数库的结果不可重现的问题。 英特尔的另一份文档讨论了与英特尔编译器一起使用的编译器开关。

提到的 FFTW 库可能以非确定性模式运行。

如果您使用 FFTW_MEASURE 或 FFTW_PATIENT 模式,程序会在运行时检查哪些参数值工作最快,然后将在整个程序中使用这些参数。因为运行时间明显会有一点波动,所以参数会有所不同,傅里叶变换的结果将是不确定的。如果您想要确定性 FFTW,请使用模式 FFTW_ESTIMATE。

虽然由于多核/多线程处理场景很可能会发生表达式术语评估顺序的更改,但不要忘记可能存在(尽管这是一个很长的镜头)某种硬件设计缺陷在起作用。还记得 Pentium FDIV 问题吗?(参见https://en.wikipedia.org/wiki/Pentium_FDIV_bug)。前段时间,我从事基于 pc 的模拟电路仿真软件。我们的方法的一部分涉及开发回归测试套件,我们将针对软件的夜间构建运行这些套件。对于我们开发的许多模型,迭代方法(例如 Newton-Raphson ( https://en.wikipedia.org/wiki/Newton%27s_method) 和 Runge-Kutta) 被广泛用于仿真算法。对于模拟设备,通常会出现具有极小数值的内部伪影,例如电压、电流等。作为模拟过程的一部分,这些值随着(模拟的)时间递增地变化。这些变化的幅度可能非常小,我们经常观察到的是,后续 FPU 对此类 delta 值的操作接近于 FPU 精度的“噪声”阈值(64 位浮点数具有 53 位尾数,IIRC)。再加上我们经常不得不在模型中引入“PrintF”日志代码以允许调试(啊,好日子!),实际上保证了每天都有零星的结果!所以呢' 这一切意味着什么?您必须期望在这种情况下看到差异,最好的办法是定义和实施一种方法来决定(幅度、频率、趋势等)何时/如何忽略它们。

虽然异步操作的浮点舍入可能是问题,但我怀疑它更平庸。使用未初始化的变量会为您的其他确定性代码添加随机性。这是一个经常被开发人员忽略的常见问题,因为当您在调试模式下运行时,所有变量在声明时都被初始化为 0。当不在调试模式下运行时,分配给变量的内存具有分配之前内存的任何值。作为优化,内存不会在分配时归零。如果这发生在您的代码中,则很容易修复,而在库代码中则更少。