Skip to content

Commit f9bbc60

Browse files
ViralBShahclaude
andcommitted
Fix fmaf double-rounding via exact 2Sum exactness test (#160)
fmaf(0.9474001f, 4.639901e-7f, -0.24325085f) returned -0x1.f22d44p-3f (-0.2432504f) instead of the correctly single-rounded -0x1.f22d46p-3f (-0.24325041f), a 1-ulp double-rounding error. Root cause: fmaf computes (double)x*y + z and, for halfway-to-float results, applies a correction unless the double result is exact. The exactness check used "result - xy == z". That test is only valid when |xy| >= |z|; when |z| > |xy| the difference result - xy is itself rounded and can equal z even though result already dropped low-order bits of the true sum. The false "exact" verdict skipped the correction and let the inexact double result be rounded a second time to float -> double rounding. Replace it with Knuth's 2Sum, which yields the exact rounding error of xy + z independently of operand magnitude; result is exact iff err == 0. volatile barriers keep the decomposition exact under FP contraction. Verified: the reproducer now returns -0x1.f22d46p-3f, and the rebuilt library matches a hardware (single-rounded) FMA on 150M+ random/edge triples (normal-range and full random bit patterns) with zero mismatches. Full make test passes (test-double, test-float, regression). Adds test/regression/test-160.c pinning the reproducer and several hardware-FMA-verified triples (teeth-checked: fails on the old code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aeef4da commit f9bbc60

2 files changed

Lines changed: 74 additions & 1 deletion

File tree

src/s_fmaf.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,31 @@ fmaf(float x, float y, float z)
4848
xy = (double)x * y;
4949
result = xy + z;
5050
EXTRACT_WORDS(hr, lr, result);
51+
52+
/*
53+
* Compute the exact rounding error of the sum xy + z using Knuth's
54+
* 2Sum. result is the exact value of xy + z if and only if err == 0.
55+
*
56+
* The naive shortcut "result - xy == z" is *not* a valid exactness
57+
* test: when |z| > |xy| the subtraction result - xy is itself rounded
58+
* and can return z even though result lost low-order bits of the true
59+
* sum (e.g. fmaf(0.9474001f, 4.639901e-7f, -0.24325085f)). Trusting
60+
* it skips the correction below and lets the inexact double result be
61+
* rounded a second time to float, producing a 1-ulp double-rounding
62+
* error. 2Sum is order-independent, so it avoids that trap.
63+
*
64+
* The volatile barriers force each step to round in double precision
65+
* so the decomposition stays exact even under FP contraction.
66+
*/
67+
volatile double zz = result - xy;
68+
volatile double rz = result - zz;
69+
volatile double tz = z - zz;
70+
double err = (xy - rz) + tz;
71+
5172
/* Common case: The double precision result is fine. */
5273
if ((lr & 0x1fffffff) != 0x10000000 || /* not a halfway case */
5374
(hr & 0x7ff00000) == 0x7ff00000 || /* NaN */
54-
result - xy == z || /* exact */
75+
err == 0 || /* exact */
5576
fegetround() != FE_TONEAREST) /* not round-to-nearest */
5677
return (result);
5778

test/regression/test-160.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Regression test for issue #160: fmaf double-rounding error.
3+
* https://github.com/JuliaMath/openlibm/issues/160
4+
*
5+
* fmaf must compute x*y + z with a *single* rounding to float. The old
6+
* implementation evaluated (double)x*y + z in double precision and rounded
7+
* to float, using "result - xy == z" as an exactness shortcut to decide
8+
* whether a halfway result needed correction. That test is bogus when
9+
* |z| > |xy|: the subtraction result - xy is itself rounded and can return
10+
* z even though result already lost low-order bits of the true sum. The
11+
* shortcut then skipped the correction and let the inexact double result be
12+
* rounded a second time to float, giving a 1-ulp double-rounding error.
13+
*
14+
* For fmaf(0.9474001f, 4.639901e-7f, -0.24325085f) the correctly single-
15+
* rounded result is -0x1.f22d46p-3f (== -0.24325041f); the buggy code
16+
* returned -0x1.f22d44p-3f (== -0.2432504f).
17+
*
18+
* The fix replaces the shortcut with an exact Knuth 2Sum error term. The
19+
* spot-check values below were verified against a hardware (single-rounded)
20+
* FMA.
21+
*/
22+
#include <math.h>
23+
24+
#include "regress-util.h"
25+
26+
/*
27+
* Call fmaf through volatile operands so the compiler cannot fold the call
28+
* at compile time (which would evaluate it with its own arithmetic and hide
29+
* a bug in the library). This exercises the real openlibm fmaf.
30+
*/
31+
static float
32+
fma3(float a, float b, float c)
33+
{
34+
volatile float va = a, vb = b, vc = c;
35+
return fmaf(va, vb, vc);
36+
}
37+
38+
int
39+
main(void)
40+
{
41+
/* The exact reproducer from issue #160. */
42+
CHECK(fma3(0.9474001f, 4.639901e-7f, -0.24325085f) == -0x1.f22d46p-3f);
43+
44+
/* A few more triples cross-checked against a hardware FMA. */
45+
CHECK(fma3(0x1.000002p+0f, 0x1.000002p+0f, -0x1p+0f) == 0x1p-22f);
46+
CHECK(fma3(0x1.fffffep+0f, 0x1.fffffep+0f, -0x1.fffffcp+1f) == 0x1p-46f);
47+
CHECK(fma3(0x1.cp+1f, 0x1p+1f, 0x1.8p+0f) == 0x1.1p+3f);
48+
CHECK(fma3(-0x1.921fb6p+1f, 0x1.5bf0a8p-1f, 0x1p+0f) == -0x1.228b02p+0f);
49+
CHECK(fma3(0x1.6a09e6p-1f, 0x1.6a09e6p-1f, -0x1p-1f) == -0x1.26055cp-26f);
50+
51+
return 0;
52+
}

0 commit comments

Comments
 (0)