C++ 两个单精度浮点向量的点积在CUDA内核中产生的结果与在主机上产生的结果不同

C++ 两个单精度浮点向量的点积在CUDA内核中产生的结果与在主机上产生的结果不同,c++,cuda,floating-point,C++,Cuda,Floating Point,在调试一些CUDA代码时,我使用printf语句将其与等效的CPU代码进行比较,并注意到在某些情况下,我的结果不同;它们在两种平台上都不一定是错的,因为它们都在浮点舍入误差范围内,但我仍然有兴趣知道是什么导致了这种差异 我能够追踪到不同点积结果的问题。在CUDA和主机代码中,我都有float4类型的向量a和b。然后,在每个平台上,我使用以下代码计算点积并打印结果: printf("a: %.24f\t%.24f\t%.24f\t%.24f\n",a.x,a.y,a.z,a.w); printf(

在调试一些CUDA代码时,我使用
printf
语句将其与等效的CPU代码进行比较,并注意到在某些情况下,我的结果不同;它们在两种平台上都不一定是错的,因为它们都在浮点舍入误差范围内,但我仍然有兴趣知道是什么导致了这种差异

我能够追踪到不同点积结果的问题。在CUDA和主机代码中,我都有
float4
类型的向量a和b。然后,在每个平台上,我使用以下代码计算点积并打印结果:

printf("a: %.24f\t%.24f\t%.24f\t%.24f\n",a.x,a.y,a.z,a.w);
printf("b: %.24f\t%.24f\t%.24f\t%.24f\n",b.x,b.y,b.z,b.w);
float dot_product = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
printf("a dot b: %.24f\n",dot_product);
CPU的结果打印输出为:

a: 0.999629139900207519531250   -0.024383276700973510742188 -0.012127066962420940399170 0.013238593004643917083740
b: -0.001840781536884605884552  0.033134069293737411499023  0.988499701023101806640625  1.000000000000000000000000
a dot b: -0.001397025771439075469971
对于CUDA内核:

a: 0.999629139900207519531250   -0.024383276700973510742188 -0.012127066962420940399170 0.013238593004643917083740
b: -0.001840781536884605884552  0.033134069293737411499023  0.988499701023101806640625  1.000000000000000000000000
a dot b: -0.001397024840116500854492
正如您所看到的,a和b的值在两种平台上似乎是按位等效的,但完全相同的代码的结果却相差如此之小。据我所知,浮点乘法根据IEEE 754标准定义良好,并且与硬件无关。然而,我确实有两个假设来解释为什么我没有看到相同的结果:

  • 编译器优化是对乘法进行重新排序,它们在GPU/CPU上以不同的顺序发生,从而产生不同的结果
  • CUDA内核使用fused Multiple add(FMA)操作符,如中所述。在这种情况下,CUDA的结果实际上应该更准确一些
    除了将FMUL和FADD合并到FMA中(可以使用nvcc命令行开关
    -fmad=false
    关闭FMA),CUDA编译器遵守C/C++规定的求值顺序。根据CPU代码的编译方式,它可能会使用比单精度更宽的精度来累加点积,从而产生不同的结果

    对于GPU代码,FMUL/FADD与FMA的合并是常见的,因此产生的数值差异也是常见的。出于性能原因,CUDA编译器执行积极的FMA合并。使用FMA通常也会得到更准确的结果,因为舍入步骤的数量减少了,并且由于FMA在内部保持全宽积,所以有一些防止减法消除的保护。我建议阅读以下白皮书及其引用的参考文献:

    要使CPU和GPU结果与健全性检查相匹配,您需要使用
    -fmad=false
    关闭GPU代码中的FMA合并,并在CPU上强制以单精度存储每个中间结果:

       volatile float p0,p1,p2,p3,dot_product; 
       p0=a.x*b.x; 
       p1=a.y*b.y; 
       p2=a.z*b.z; 
       p3=a.w*b.w; 
       dot_product=p0; 
       dot_product+=p1; 
       dot_product+=p2; 
       dot_product+=p3;
    

    除了将FMUL和FADD合并到FMA中(可以使用nvcc命令行开关
    -fmad=false
    关闭FMA),CUDA编译器遵守C/C++规定的求值顺序。根据CPU代码的编译方式,它可能会使用比单精度更宽的精度来累加点积,从而产生不同的结果

    对于GPU代码,FMUL/FADD与FMA的合并是常见的,因此产生的数值差异也是常见的。出于性能原因,CUDA编译器执行积极的FMA合并。使用FMA通常也会得到更准确的结果,因为舍入步骤的数量减少了,并且由于FMA在内部保持全宽积,所以有一些防止减法消除的保护。我建议阅读以下白皮书及其引用的参考文献:

    要使CPU和GPU结果与健全性检查相匹配,您需要使用
    -fmad=false
    关闭GPU代码中的FMA合并,并在CPU上强制以单精度存储每个中间结果:

       volatile float p0,p1,p2,p3,dot_product; 
       p0=a.x*b.x; 
       p1=a.y*b.y; 
       p2=a.z*b.z; 
       p3=a.w*b.w; 
       dot_product=p0; 
       dot_product+=p1; 
       dot_product+=p2; 
       dot_product+=p3;
    

    除了将FMUL和FADD合并到FMA中(可以使用nvcc命令行开关
    -fmad=false
    关闭FMA),CUDA编译器遵守C/C++规定的求值顺序。根据CPU代码的编译方式,它可能会使用比单精度更宽的精度来累加点积,从而产生不同的结果。在CPU上尝试以下操作,并与使用
    -fmad=false
    编译的GPU代码进行比较:
    易失性浮点p0、p1、p2、p3、点积;p0=a.x*b.x;p1=a.y*b.y;p2=a.z*b.z;p3=a.w*b.w;dot_乘积=p0;dot_乘积+=p1;dot_乘积+=p2;dot_乘积+=p3感谢您提供此指针!我只是添加了“-fmad=false”标志,现在我的结果也是按位等效的。FMA运算符一定是差异的来源。这是一种非常常见的情况。CUDA编译器出于性能原因,积极地将FMULs合并到FADD和FMA中。使用FMA通常也会得到更准确的结果,因为舍入步骤的数量减少了,并且由于FMA在内部保持全宽积,所以有一些防止减法消除的保护。我建议阅读以下白皮书及其引用的参考文献:@njuffa如果你将你的评论转化为答案,我会投赞成票。@Robert Crovella:谢谢,我听从了你的建议,将我的评论改写为答案。除了将FMUL和FADD合并为FMA之外(可使用nvcc命令行开关关闭此开关
    -fmad=false
    ),CUDA编译器遵守C/C++规定的求值顺序。根据CPU代码的编译方式,它可能使用比单精度更宽的精度来累加点积,从而产生不同的结果。在CPU上尝试以下操作,并与使用
    -fmad=false
    编译的GPU代码进行比较:
    volatile floatp0,p1,p2,p3,dot_product;p0=a.x*b.x;p1=a.y*b.y;p2=a.z*b.z;p3=a.w*b.w;dot_product=p0;dot_product+=p1;dot_product+=p2;dot_product+=p3;
    感谢这个指针!我只是添加了“-fmad=false”标志,并且我的结果现在也是按位等效的。FMA运算符必须是差异的来源。这是一种非常常见的情况。CUDA编译器将FMULs合并到FADD中,并将FMA积极地用于性能分析