BLAS 实现是否保证给出完全相同的结果?

计算科学 浮点 布拉斯
2021-12-21 22:31:45

给定两个不同的 BLAS 实现,我们可以期望它们进行完全相同的浮点计算并返回相同的结果吗?或者它是否会发生,例如,一个计算标量积为 和一个计算为 因此可能在 IEEE 浮点数中给出不同的结果算术?

((x1y1+x2y2)+x3y3)+x4y4
(x1y1+x2y2)+(x3y3+x4y4),

3个回答

不,这不能保证。如果您使用的是没有任何优化的 NETLIB BLAS,那么结果几乎是相同的。但是对于 BLAS 和 LAPACK 的任何实际使用,使用高度优化的并行 BLAS。并行化导致,即使它仅在 CPU 的向量寄存器内并行工作,单个项的评估顺序也会发生变化,并且求和的顺序也会发生变化。现在从 IEEE 标准中缺少的关联属性得出结果是不一样的。所以你提到的事情可能会发生。

在 NETLIB BLAS 中,标量积只是一个被因子 5 展开的 for 循环:

DO I = MP1,N,5
          DTEMP = DTEMP + DX(I)*DY(I) + DX(I+1)*DY(I+1) +
     $            DX(I+2)*DY(I+2) + DX(I+3)*DY(I+3) + DX(I+4)*DY(I+4)
END DO

如果每个乘法立即添加到 DTEMP,或者是否首先将所有 5 个分量相加然后添加到 DTEMP,则取决于编译器。在 OpenBLAS 中,它依赖于架构一个更复杂的内核:

 __asm__  __volatile__
    (
    "vxorpd     %%ymm4, %%ymm4, %%ymm4               \n\t"
    "vxorpd     %%ymm5, %%ymm5, %%ymm5               \n\t"
    "vxorpd     %%ymm6, %%ymm6, %%ymm6               \n\t"
    "vxorpd     %%ymm7, %%ymm7, %%ymm7               \n\t"

    ".align 16                           \n\t"
    "1:                          \n\t"
        "vmovups                  (%2,%0,8), %%ymm12         \n\t"  // 2 * x
        "vmovups                32(%2,%0,8), %%ymm13         \n\t"  // 2 * x
        "vmovups                64(%2,%0,8), %%ymm14         \n\t"  // 2 * x
        "vmovups                96(%2,%0,8), %%ymm15         \n\t"  // 2 * x

    "vmulpd      (%3,%0,8), %%ymm12, %%ymm12 \n\t"  // 2 * y
    "vmulpd    32(%3,%0,8), %%ymm13, %%ymm13 \n\t"  // 2 * y
    "vmulpd    64(%3,%0,8), %%ymm14, %%ymm14 \n\t"  // 2 * y
    "vmulpd    96(%3,%0,8), %%ymm15, %%ymm15 \n\t"  // 2 * y

    "vaddpd      %%ymm4 , %%ymm12, %%ymm4 \n\t"  // 2 * y
    "vaddpd      %%ymm5 , %%ymm13, %%ymm5 \n\t"  // 2 * y
    "vaddpd      %%ymm6 , %%ymm14, %%ymm6 \n\t"  // 2 * y
    "vaddpd      %%ymm7 , %%ymm15, %%ymm7 \n\t"  // 2 * y

    "addq       $16 , %0	  	     \n\t"
	"subq	        $16 , %1            \n\t"      
    "jnz        1b                   \n\t"
...

它将标量积拆分为长度为 4 的小标量积并将它们相加。

使用其他典型的 BLAS 实现,如 ATLAS、MKL、ESSL,......这个问题保持不变,因为每个 BLAS 实现都使用不同的优化来获得快速代码。但据我所知,需要一个人为的例子来导致真正错误的结果。

如果需要 BLAS 库返回相同的结果(按位相同),则必须使用可重现的 BLAS 库,例如:

简短的回答

如果编写两个 BLAS 实现以完全相同的顺序执行操作,并且库是使用相同的编译器标志和相同的编译器编译的,那么它们会给你相同的结果。浮点运算不是随机的,因此两个相同的实现将给出相同的结果。

但是,为了性能,有很多事情可以打破这种行为......

更长的答案

IEEE 还指定了执行这些操作的顺序,以及每个操作的行为方式。但是,如果您使用“-ffast-math”等选项编译 BLAS 实现,编译器可以执行在精确算术中为真但在 IEEE 浮点中“不正确”的转换。正如您所指出的,典型的例子是浮点加法的非关联性。使用更积极的优化设置,将假定关联性,并且处理器将通过重新排序操作尽可能多地并行执行。

另一个打破标准的行为是通过使用 FMA(融合乘加)指令来实现的。这些在矩阵乘法等运算中很突出,它们有可能使您的例程的吞吐量翻倍。但是,它们在单个操作中操作,并且只产生一个浮点舍入步骤。这偏离了 IEEE 标准,该标准要求此操作具有两个舍入步骤。这使得 FMA 结果实际上比 IEEE 的结果准确,但它在技术上是违反标准的行为。a+bc

一般来说,没有。撇开关联性不谈,编译器标志(例如,启用 SIMD 指令、使用融合乘法加法等)或硬件(例如,是否使用扩展精度)可能会产生不同的结果。

为了获得可重现的 BLAS 实现,我们付出了一些努力。有关详细信息,请参阅ReproBLASExBLAS