PAYATHON 2026

Fortran 中如何让 IEEE_FMA 保持正确舍入并实现 SIMD 向量化?

数据阿明

结论

向量化报告显示,当前编译配置将 IEEE_FMA 实现为对 __for_ieee_fma_k8_ 的调用,而向量化器无法处理这个调用。elemental 属性不保证编译器生成 SIMD 指令,添加 !$omp simd 也不会自动补齐缺失的向量实现。

可以先调整循环顺序,明确 SIMD 临时变量的私有属性,再检查生成的代码。如果 IEEE_FMA 仍然阻止向量化,就需要换用能够正确降低该调用的编译器配置,或者将批量运算放入使用显式 SIMD FMA 的内核。这些方案都可以原地更新矩阵,无需复制整个矩阵。

为什么 elemental 仍然报告依赖?

elemental 描述逐元素调用语义。例如,对满足形状要求的数组参数,同一个过程可以逐元素应用。它没有承诺底层一定存在向量入口,也没有要求编译器必须把标量调用转换成硬件 SIMD。

Intel 对 IEEE_FMA(a,b,c) 的定义是:当结果处于范围内时,对数学上的 a*b+c 按相应舍入模式执行一次最终舍入,中间运算不会单独触发相应的溢出、下溢或不精确异常。因此,普通乘加表达式不能直接替代它。Intel IEEE_FMA 文档

报告中指出的直接限制是:

call to function __for_ieee_fma_k8_ cannot be vectorized

后面的 assumed FLOW dependence 和 assumed ANTI dependence 表示编译器在当前分析中假定存在依赖。结合这段代码,它们可能与编译器对运行库调用及其参数的保守分析有关,不能据此认定算法存在真实的跨迭代依赖。仅凭这份报告,无法确定编译器内部的具体原因,也不能确认 ifx 必然有相同问题。

原代码中的 parallel do private(a,c,d,i,j) 解决的是线程之间的私有数据问题,不能据此推断编译器已完成 SIMD lane 之间的数据分析。

先调整循环布局并添加 SIMD 指令

Fortran 数组按列优先存储。对于 X(m,n),让第一个下标 i 在内层变化,可以连续访问内存。原代码让 j 在内层变化,访问通常带有跨步。

下面的改写保留 IEEE_FMA,外层按列分配线程,内层尝试 SIMD。这可以用来验证编译器的处理能力,但不能保证目标编译器一定生成向量 FMA。

subroutine fma_mod(X, p, m, n)
  use, intrinsic :: ieee_arithmetic, only: ieee_fma
  use, intrinsic :: iso_fortran_env, only: int64
  implicit none

  integer, intent(in) :: m, n
  double precision, intent(in) :: p
  double precision, intent(inout) :: X(m,n)

  integer :: i, j
  double precision :: a, c, d, u

  u = 1.d0 / p

  !$omp parallel do private(i,a,c,d)
  do j = 1, n
    !$omp simd private(a,c,d)
    do i = 1, m
      a = X(i,j)
      c = floor(a*u, kind=int64)
      d = ieee_fma(-c, p, a)
      if (d >= p) d = d - p
      X(i,j) = d
    end do
    !$omp end simd
  end do
  !$omp end parallel do
end subroutine fma_mod

代码将原来的整数 kind 字面值 8 写成具名常量 int64,明确要求 FLOOR 返回 64 位整数;赋给变量 c 时,结果仍会转换为浮点类型。

每次迭代只读写当前元素,临时变量都是标量,因此不需要矩阵副本。外层只有 n 个任务,列数较少时,线程利用率仍可能受限。

OpenMP 的 SIMD 循环和函数向量版本是不同的机制。对于循环中的函数调用,编译器需要能够内联处理它,或者调用适用的向量版本。OpenMP 官方 SIMD 示例

如果 IEEE_FMA 仍然无法向量化

不要把 declare simd 包装当成确定的修复

declare simd 可以用于请求为自定义过程创建 SIMD 版本。但如果包装函数内部仍然调用不能向量化的 IEEE_FMA,包装本身就没有消除障碍。OpenMP declare simd 规范

可以尝试包装方案,但必须检查实际生成的代码,不能仅凭指令存在就认为问题已经解决。

对必须融合的运算使用批量 SIMD 内核

如果目标固定为支持 AVX2 和 FMA 的 x86 环境,可以通过 ISO_C_BINDING 调用 C/C++ 批量内核,在内核里使用显式 FMA intrinsic。例如,四个 double 的 a-c*p 可以使用:

#include <immintrin.h>

static inline __m256d residual4(__m256d a,
                               __m256d c,
                               __m256d p)
{
    return _mm256_fnmadd_pd(c, p, a);
}

这个片段只展示融合运算。完整内核还需要完成商估计、修正、加载与存储,以及不足四个元素的尾部处理。接口应一次接收一批连续元素,并写回原地址,避免逐元素跨语言调用。

显式 SIMD FMA 能明确表达硬件融合运算,但不能据此宣称整个内核与 IEEE_FMA 的所有可观察行为完全等价。如果应用依赖动态舍入模式、异常陷阱或特殊值处理,还需要核对浮点环境与这些行为。

仅在已有正确性依据的模约减中考虑普通乘加

题目说明,当前模约减移除 IEEE_FMA 后仍保持正确。如果这一结论已覆盖实际使用的数据范围,可以为这一步单独考虑:

d = a - c*p

这个选择不能推广到题目中依赖融合舍入才能正确的矩阵乘法步骤。允许编译器生成 FMA,与要求某个运算必须具有 FMA 语义,是两件不同的事。

如何确认修改有效?

保留原有向量化报告与 -S 输出,检查以下三方面:

  1. 循环报告:目标内层循环是否向量化,是否仍出现不可向量化的函数调用。处理完 IEEE_FMA 后,FLOOR 的整数转换也可能成为新的限制。
  2. 汇编:对 AVX2 批量 double 运算,检查是否出现使用 ymm 寄存器的 packed-double FMA,例如 vfmadd...pd 或 vfnmadd...pd。仅出现标量 FMA 不能证明循环已实现 SIMD。
  3. 数值结果:在算法允许的输入范围内,与可信的整数模运算或高精度结果比较,重点覆盖商估计边界、接近模数倍数的输入,以及 SIMD 尾部。

不要把 -fp-model fast 当作可靠的正确性修复。Intel 文档说明,浮点优化选项会影响值安全性、乘加融合和浮点环境处理;应按所用编译器版本核对配置。Intel 浮点优化说明

这段代码的模约减正确性还依赖 p、输入大小、浮点整数表示范围及商估计误差界。现有材料没有给出这些界限,不能证明一次 if (d >= p) 修正对任意输入都足够。调整循环顺序可以改善访问布局,但不会补齐算法的适用条件。

备注:内容仅供参考。