Skip to content

[BUG]: fpmp2 math functions mishandle ±0, ±inf, NaN and out-of-domain inputs; spec tables echo the input #12550

Open

Description

@rmlarsen

Is this a duplicate?

  • I confirmed there appear to be no duplicate issues for this bug and that I agree to the Code of Conduct

Type of Bug

Silent Failure

Component

libcu++

Describe the bug

Many cuda::fp32mp2 and cuda::fp64mp2 math functions return wrong results for special inputs: signed zeros, infinities, NaN, arguments outside the domain, subnormals and ±max. Often the result is a plausible finite number where C99 Annex F (and CUDA's own double functions) give NaN or ±inf, so nothing signals the error.

A probe of 50 unary functions (plus sincos and sincospi) at 19 special inputs finds 198 wrong results in 63 (type, function) pairs. It covers both types, on the host and on the device. All of them are on current main (89e1909). The open fpmp PRs #12519, #12530 and #12543 leave every one unchanged. pow is fixed separately in #12548.

Finite results where NaN or ±inf is due. These are the most serious, because nothing looks wrong:

  • fp32mp2 log, log2 and log10 ignore the sign, and do not handle 0, inf or NaN:
    • log(x) for x < 0 returns log|x|, e.g. log(−2) = 0.693.
    • log(±0) = −104.7 (ln 2⁻¹⁵¹), log(+inf) = 88.72 and log(NaN) = 89.13.
    • log2(±0) = −151 and log2(+inf) = 128; log10 behaves the same way.
  • fp32mp2 normcdfinv returns ±inf instead of NaN outside [0, 1], e.g. normcdfinv(2) = +inf.
  • fp64mp2 boys_f0 returns 1 for every x < 1e-15, including x < 0 and −inf (F0(−1) = 1.4627).

NaN where ±inf or a finite value is due:

  • fp32mp2 sinh and cosh at ±inf and ±FLT_MAX.
  • fp32mp2 atan(±inf), which should be ±π/2.
  • fp32mp2 boys_f0 at +inf (should be 0), and at −inf and −FLT_MAX (should be +inf).
  • For both types: sqrt(+inf); rsqrt(±0), which should be ±inf; and rsqrt(+inf), which should be +0.
  • For both types: ceil, floor, trunc, round, rint and nearbyint of ±inf.

Subnormal inputs, on the device only:

  • fp32mp2 sqrt and rsqrt return NaN for subnormal inputs; for example sqrt(2⁻¹⁴⁹) should be 3.74e-23. The host is correct.
  • fp64mp2 logb of a subnormal returns INT_MIN instead of −1074 … −1023.

Signed zero. Odd functions return +0 for −0:

  • Both types: sin (and sincos), tan, asin, atan, sinh, tanh, erf, expm1, log1p, sqrt and round. On the device this also applies to the placeholders erfinv, j1, cyl_bessel_i1, rint and nearbyint, and tgamma(−0) returns +inf instead of −inf.
  • fp32mp2 only: trunc does the same for −0.5 and for negative subnormals. On the device, ceil does too, and floor(−0) returns +0.
  • fp64mp2 only: asinh, atanh, cbrt and sinpi (and sincospi) return +0, and rcbrt(−0) returns +inf instead of −inf. On the device, normcdfinv(0.5) returns −0.

Host and device disagree:

  • fp32mp2 cospi(±0.5) returns −0 on the host.
  • On the host, fp64mp2 sinpi and cospi return sin(πx) rounded in double: sinpi(1) = 1.2e-16, cospi(0.5) = 6.1e-17, and NaN at ±DBL_MAX. The device is exact.
  • On the host, fp64mp2 normcdfinv returns ±inf outside [0, 1], and −inf for subnormal inputs.

The specification (docs/libcudacxx/extended_api/fp/fpmp_spec.rst) has 42 unary "Special Values Table"s, and all of them list the input as the result. For example, they give log(−1) = −1, sqrt(−1) = −1, exp(−INF) = −inf and sinh(+maxN) = +maxN, so they document neither the current behavior nor the correct one.

Not covered here:

  • two-argument functions other than pow;
  • arithmetic at the mid and high levels, where an infinite operand gives NaN (−inf + (−1), −inf · (−1)). That arithmetic is a separate design question.
All 198 failing cases

The method:

  • The inputs are ±inf, ±max, ±2, ±1, ±0.5, ±min (smallest normal), ±largest subnormal, ±smallest subnormal, ±0 and NaN, each with a zero low limb.
  • The reference is CUDA's double function on the same input, evaluated on the device and rounded to float for fp32mp2. boys_f0 is integrated numerically for x < 0.
  • A result fails when its class differs from the reference's (NaN, ±inf, ±0 with its sign, or finite), or when a finite result is off by more than 1e-5 (fp32mp2) or 1e-10 (fp64mp2) relative.
  • The host is GCC 15.2 -O2 -march=x86-64-v3 and the device sm_89 with CUDA 13.4.
  • The placeholders erfinv, erfcinv, erfcx, normcdf, lgamma, tgamma, j0, j1, y0, y1, cyl_bessel_i0, cyl_bessel_i1, rint, nearbyint and logb exist on the device only.
  • A note in parentheses means the result is wrong only there.
Type Function Input → result, expected
fp32mp2 asin -0 → +0, expected -0
fp32mp2 atan -INF → nan, expected -1.5708; -0 → +0, expected -0; +INF → nan, expected 1.5708
fp32mp2 boys_f0 -INF → nan, expected +inf; -maxN → nan, expected +inf; +INF → nan, expected +0
fp32mp2 ceil -INF → nan, expected -inf; -0.5 → +0 (device), expected -0; -minN → +0 (device), expected -0; -maxD → +0 (device), expected -0; -minD → +0 (device), expected -0; -0 → +0 (device), expected -0; +INF → nan, expected +inf
fp32mp2 cosh -INF → nan, expected +inf; -maxN → nan, expected +inf; +maxN → nan, expected +inf; +INF → nan, expected +inf
fp32mp2 cospi -0.5 → -0 (host), expected +0; +0.5 → -0 (host), expected +0
fp32mp2 cyl_bessel_i1 -0 → +0, expected -0
fp32mp2 erf -0 → +0, expected -0
fp32mp2 erfinv -0 → +0, expected -0
fp32mp2 expm1 -0 → +0, expected -0
fp32mp2 floor -INF → nan, expected -inf; -0 → +0 (device), expected -0; +INF → nan, expected +inf
fp32mp2 j1 -0 → +0, expected -0
fp32mp2 log -INF → 88.7228, expected nan; -maxN → 88.7228, expected nan; -2 → 0.693147, expected nan; -1 → +0, expected nan; -0.5 → -0.693147, expected nan; -minN → -87.3365, expected nan; -maxD → -87.3365, expected nan; -minD → -103.279, expected nan; -0 → -104.665, expected -inf; +0 → -104.665, expected -inf; +INF → 88.7228, expected +inf; QNAN → 89.1283, expected nan
fp32mp2 log10 -INF → 38.5318, expected nan; -maxN → 38.5318, expected nan; -2 → 0.30103, expected nan; -1 → +0, expected nan; -0.5 → -0.30103, expected nan; -minN → -37.9298, expected nan; -maxD → -37.9298, expected nan; -minD → -44.8535, expected nan; -0 → -45.4555, expected -inf; +0 → -45.4555, expected -inf; +INF → 38.5318, expected +inf; QNAN → 38.7079, expected nan
fp32mp2 log1p -0 → +0, expected -0
fp32mp2 log2 -INF → 128, expected nan; -maxN → 128, expected nan; -2 → 1, expected nan; -1 → +0, expected nan; -0.5 → -1, expected nan; -minN → -126, expected nan; -maxD → -126, expected nan; -minD → -149, expected nan; -0 → -151, expected -inf; +0 → -151, expected -inf; +INF → 128, expected +inf; QNAN → 128.585, expected nan
fp32mp2 nearbyint -INF → nan, expected -inf; -0 → +0, expected -0; +INF → nan, expected +inf
fp32mp2 normcdfinv -INF → -inf, expected nan; -maxN → -inf, expected nan; -2 → -inf, expected nan; -1 → -inf, expected nan; -0.5 → -inf, expected nan; -minN → -inf, expected nan; -maxD → -inf, expected nan; -minD → -inf, expected nan; +2 → +inf, expected nan; +maxN → +inf, expected nan; +INF → +inf, expected nan
fp32mp2 rint -INF → nan, expected -inf; -0 → +0, expected -0; +INF → nan, expected +inf
fp32mp2 round -INF → nan, expected -inf; -minN → +0 (device), expected -0; -maxD → +0 (device), expected -0; -minD → +0 (device), expected -0; -0 → +0, expected -0; +INF → nan, expected +inf
fp32mp2 rsqrt -0 → nan, expected -inf; +0 → nan, expected +inf; +minD → nan (device), expected 2.67137e+22; +maxD → nan (device), expected 9.22337e+18; +INF → nan, expected +0
fp32mp2 sin -0 → +0, expected -0
fp32mp2 sincos_s -0 → +0, expected -0
fp32mp2 sincospi_c -0.5 → -0 (host), expected +0; +0.5 → -0 (host), expected +0
fp32mp2 sinh -INF → nan, expected -inf; -maxN → nan, expected -inf; -0 → +0, expected -0; +maxN → nan, expected +inf; +INF → nan, expected +inf
fp32mp2 sqrt -0 → +0, expected -0; +minD → nan (device), expected 3.74339e-23; +maxD → nan (device), expected 1.0842e-19; +INF → nan, expected +inf
fp32mp2 tan -0 → +0, expected -0
fp32mp2 tanh -0 → +0, expected -0
fp32mp2 tgamma -0 → +inf, expected -inf
fp32mp2 trunc -INF → nan, expected -inf; -0.5 → +0, expected -0; -minN → +0, expected -0; -maxD → +0, expected -0; -minD → +0, expected -0; -0 → +0, expected -0; +INF → nan, expected +inf
fp64mp2 asin -0 → +0, expected -0
fp64mp2 asinh -0 → +0, expected -0
fp64mp2 atan -0 → +0, expected -0
fp64mp2 atanh -0 → +0, expected -0
fp64mp2 boys_f0 -INF → 1, expected +inf; -maxN → 1, expected +inf; -2 → 1, expected 2.36445; -1 → 1, expected 1.46265; -0.5 → 1, expected 1.19496
fp64mp2 cbrt -0 → +0, expected -0
fp64mp2 ceil -INF → nan, expected -inf; +INF → nan, expected +inf
fp64mp2 cospi -maxN → nan (host), expected 1; -0.5 → 6.12323e-17 (host), expected +0; +0.5 → 6.12323e-17 (host), expected +0; +maxN → nan (host), expected 1
fp64mp2 cyl_bessel_i1 -0 → +0, expected -0
fp64mp2 erf -0 → +0, expected -0
fp64mp2 erfinv -0 → +0, expected -0
fp64mp2 expm1 -0 → +0, expected -0
fp64mp2 floor -INF → nan, expected -inf; +INF → nan, expected +inf
fp64mp2 j1 -0 → +0, expected -0
fp64mp2 log1p -0 → +0, expected -0
fp64mp2 logb -maxD → -2.14748e+09, expected -1023; -minD → -2.14748e+09, expected -1074; +minD → -2.14748e+09, expected -1074; +maxD → -2.14748e+09, expected -1023
fp64mp2 nearbyint -INF → nan, expected -inf; +INF → nan, expected +inf
fp64mp2 normcdfinv -INF → -inf (host), expected nan; -maxN → -inf (host), expected nan; -2 → -inf (host), expected nan; -1 → -inf (host), expected nan; -0.5 → -inf (host), expected nan; -minN → -inf (host), expected nan; -maxD → -inf (host), expected nan; -minD → -inf (host), expected nan; +minD → -inf (host), expected -38.4674; +maxD → -inf (host), expected -37.5194; +minN → -inf (host), expected -37.5194; +0.5 → -0 (device), expected +0; +2 → +inf (host), expected nan; +maxN → +inf (host), expected nan; +INF → +inf (host), expected nan
fp64mp2 rcbrt -0 → +inf, expected -inf
fp64mp2 rint -INF → nan, expected -inf; +INF → nan, expected +inf
fp64mp2 round -INF → nan, expected -inf; -0 → +0, expected -0; +INF → nan, expected +inf
fp64mp2 rsqrt -0 → nan, expected -inf; +0 → nan, expected +inf; +INF → nan, expected +0
fp64mp2 sin -0 → +0, expected -0
fp64mp2 sincos_s -0 → +0, expected -0
fp64mp2 sincospi_c -maxN → nan (host), expected 1; -0.5 → 6.12323e-17 (host), expected +0; +0.5 → 6.12323e-17 (host), expected +0; +maxN → nan (host), expected 1
fp64mp2 sincospi_s -maxN → nan (host), expected -0; -2 → 2.44929e-16 (host), expected -0; -1 → -1.22465e-16 (host), expected -0; -0 → +0, expected -0; +1 → 1.22465e-16 (host), expected +0; +2 → -2.44929e-16 (host), expected +0; +maxN → nan (host), expected +0
fp64mp2 sinh -0 → +0, expected -0
fp64mp2 sinpi -maxN → nan (host), expected -0; -2 → 2.44929e-16 (host), expected -0; -1 → -1.22465e-16 (host), expected -0; -0 → +0, expected -0; +1 → 1.22465e-16 (host), expected +0; +2 → -2.44929e-16 (host), expected +0; +maxN → nan (host), expected +0
fp64mp2 sqrt -0 → +0, expected -0; +INF → nan, expected +inf
fp64mp2 tan -0 → +0, expected -0
fp64mp2 tanh -0 → +0, expected -0
fp64mp2 tgamma -0 → +inf, expected -inf
fp64mp2 trunc -INF → nan, expected -inf; +INF → nan, expected +inf

How to Reproduce

#include <cuda/fpmp>

#include <cfloat>
#include <cmath>
#include <cstdio>

using T = cuda::fp32mp2;

__host__ __device__ void show(const char* expr, const char* expected, T r)
{
  printf("  %-22s = %-12g (expected %s)\n", expr, static_cast<double>(r.hi()), expected);
}

__host__ __device__ void probe()
{
  show("log(+0)", "-inf", cuda::log(T(0.0f)));
  show("log(-2)", "nan", cuda::log(T(-2.0f)));
  show("log(+inf)", "+inf", cuda::log(T(INFINITY)));
  show("log(nan)", "nan", cuda::log(T(NAN)));
  show("log2(-1)", "nan", cuda::log2(T(-1.0f)));
  show("sinh(+inf)", "+inf", cuda::sinh(T(INFINITY)));
  show("cosh(FLT_MAX)", "+inf", cuda::cosh(T(FLT_MAX)));
  show("atan(+inf)", "1.5708", cuda::atan(T(INFINITY)));
  show("boys_f0(+inf)", "0", cuda::boys_f0(T(INFINITY)));
  show("sqrt(+inf)", "+inf", cuda::sqrt(T(INFINITY)));
  show("sqrt(2^-149)", "3.74339e-23", cuda::sqrt(T(0x1p-149f)));
  show("rsqrt(+0)", "+inf", cuda::rsqrt(T(0.0f)));
  show("floor(-inf)", "-inf", cuda::floor(T(-INFINITY)));
  show("normcdfinv(2)", "nan", cuda::normcdfinv(T(2.0f)));
  show("sin(-0)", "-0", cuda::sin(T(-0.0f)));
  show("cospi(0.5)", "0", cuda::cospi(T(0.5f)));
}

__global__ void kernel()
{
  printf("device:\n");
  probe();
}

int main()
{
  printf("host:\n");
  probe();
  kernel<<<1, 1>>>();
  cudaDeviceSynchronize();
}

Built with nvcc -std=c++17 -arch=sm_89 -I <cccl>/libcudacxx/include repro.cu against main, it prints:

host:
  log(+0)                = -104.665     (expected -inf)
  log(-2)                = 0.693147     (expected nan)
  log(+inf)              = 88.7228      (expected +inf)
  log(nan)               = 89.1283      (expected nan)
  log2(-1)               = 0            (expected nan)
  sinh(+inf)             = -nan         (expected +inf)
  cosh(FLT_MAX)          = -nan         (expected +inf)
  atan(+inf)             = -nan         (expected 1.5708)
  boys_f0(+inf)          = -nan         (expected 0)
  sqrt(+inf)             = -nan         (expected +inf)
  sqrt(2^-149)           = 3.74339e-23  (expected 3.74339e-23)
  rsqrt(+0)              = -nan         (expected +inf)
  floor(-inf)            = -nan         (expected -inf)
  normcdfinv(2)          = inf          (expected nan)
  sin(-0)                = 0            (expected -0)
  cospi(0.5)             = -0           (expected 0)
device:
  log(+0)                = -104.665     (expected -inf)
  log(-2)                = 0.693147     (expected nan)
  log(+inf)              = 88.7228      (expected +inf)
  log(nan)               = 89.1283      (expected nan)
  log2(-1)               = 0            (expected nan)
  sinh(+inf)             = nan          (expected +inf)
  cosh(FLT_MAX)          = nan          (expected +inf)
  atan(+inf)             = nan          (expected 1.5708)
  boys_f0(+inf)          = nan          (expected 0)
  sqrt(+inf)             = nan          (expected +inf)
  sqrt(2^-149)           = nan          (expected 3.74339e-23)
  rsqrt(+0)              = nan          (expected +inf)
  floor(-inf)            = nan          (expected -inf)
  normcdfinv(2)          = inf          (expected nan)
  sin(-0)                = 0            (expected -0)
  cospi(0.5)             = 0            (expected 0)

Expected behavior

The C99 Annex F / IEEE 754 results, which are also what CUDA's float and double functions return for these inputs:

  • log(x < 0) = NaN, log(±0) = −inf and log(+inf) = +inf;
  • sinh(±inf) = ±inf, cosh(±inf) = +inf and atan(±inf) = ±π/2;
  • sqrt(+inf) = +inf and rsqrt(±0) = ±inf;
  • the rounding functions return ±inf for ±inf;
  • odd functions return −0 for −0;
  • normcdfinv returns NaN outside [0, 1];
  • host and device give the same results.

One way to get there is to handle special inputs explicitly at each function's entry, as #12548 does for pow, instead of relying on the shared kernels to propagate them. The spec's special-value tables should then be regenerated from the implementation.

Reproduction link

No response

Operating System

Ubuntu 26.04.1 LTS (WSL2)

nvidia-smi output

NVIDIA GeForce RTX 4070 Laptop GPU (sm_89), driver 621.14

NVCC version

Cuda compilation tools, release 13.4, V13.4.92; host compiler GCC 15.2

Activity

  1. github-actions commented on Nov 9, 2026

    @github-actions
    Contributor

    Hi @rmlarsen!

    Thanks for submitting this issue - the CCCL team has been notified and we'll get back to you as soon as we can!
    In the mean time, feel free to add any relevant information to this issue.

  2. davebayer commented on Nov 9, 2026

    @davebayer
    Contributor

    @akolesov-nvidia could you please have a look at this one? Thank you

  3. akolesov-nvidia commented on Nov 9, 2026

    @akolesov-nvidia
    Contributor

    We planned math functions set improvements for the next update, WIP. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

needs triageIssues that require the team's attention

Type

No type

Projects

  • Status
    Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions