Source code for ecotyper.rcompat.fma
"""Fused multiply-add for numba kernels.
R and its packages are compiled with ``-O2`` by clang/gfortran, which contract
``a*b + c`` inside one expression into a single fused multiply-add on arm64
(verified by disassembling ``NMF.so`` and ``stats.so``: e.g. NMF's
``divergence_update_H``, ``stats::corcov`` and ``hclust_``). Reproducing R
bit-for-bit therefore requires FMAs at exactly those places; plain numba code
never contracts, so kernels call :func:`fma` explicitly where R's binaries do.
On platforms where R is not compiled with FMA contraction (e.g. x86-64 builds
without ``-mfma``) set ``ECOTYPER_NO_FMA=1`` to use ``a*b + c`` instead.
"""
from __future__ import annotations
import os
import numba as nb
from llvmlite import ir
from numba.core import types
from numba.extending import intrinsic
USE_FMA = os.environ.get("ECOTYPER_NO_FMA", "0") != "1"
if not USE_FMA:
# numba's on-disk cache keys kernels by their own source only, so a kernel compiled with
# FMAs would be reused after switching ECOTYPER_NO_FMA on. Keep the two builds apart.
# (Kernels are defined after this module is imported, so the cache dir is picked up.)
import tempfile
import numba.core.config as _nbcfg
_nbcfg.CACHE_DIR = os.path.join(_nbcfg.CACHE_DIR or os.path.join(tempfile.gettempdir(), "ecotyper_numba"),
"nofma")
@intrinsic
def _fma_intrinsic(typingctx, a, b, c):
sig = types.float64(types.float64, types.float64, types.float64)
def codegen(context, builder, signature, args):
fnty = ir.FunctionType(ir.DoubleType(), [ir.DoubleType()] * 3)
fn = builder.module.declare_intrinsic("llvm.fma", [ir.DoubleType()], fnty)
return builder.call(fn, args)
return sig, codegen
if USE_FMA:
@nb.njit(inline="always")
def fma(a, b, c):
"""``a*b + c`` with a single rounding."""
return _fma_intrinsic(a, b, c)
else:
[docs]
@nb.njit(inline="always")
def fma(a, b, c):
return a * b + c