Skip to content

Commit 81c8faf

Browse files
Merge pull request #109 from JuliaMath/fma_for_windows
Add fma128.jl for Float128 software fma implementation in Windows
2 parents 9f739ce + dc3c8d7 commit 81c8faf

2 files changed

Lines changed: 311 additions & 0 deletions

File tree

src/Quadmath.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ const NaN128 = reinterpret(Float128, UInt128(0x7fff8)<<108)
356356
@assume_effects :foldable fma(x::Float128, y::Float128, z::Float128) =
357357
Float128(@quad_ccall(libquadmath.fmaq(x::Cfloat128, y::Cfloat128, z::Cfloat128)::Cfloat128))
358358
end
359+
include("fma128.jl")
359360

360361
function isinf(x::Float128)
361362
return x===Inf128 || x===-Inf128

src/fma128.jl

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# fma128.jl — software `fma` for Float128
2+
#
3+
# This file is written for inclusion in Quadmath.jl:
4+
#
5+
# include("fma128.jl") # add to src/Quadmath.jl, after the
6+
# # existing `@static if !Sys.iswindows()`
7+
# # fma definition
8+
#
9+
# On Windows, libquadmath's `fmaq` subtly corrupts floating-point state
10+
# (issue #31), so Quadmath deliberately leaves `fma(::Float128, ::Float128,
11+
# ::Float128)` undefined there and callers hit Base's promotion fallback,
12+
# which throws `ErrorException("fma not defined for Float128")`. This file
13+
# provides `fma_emulated`, a pure-Julia soft-float fused multiply-add that
14+
# never calls into libquadmath (bit-level `reinterpret` and integer
15+
# arithmetic only, so the FP-state corruption cannot occur), and binds it to
16+
# `fma` on Windows. It is:
17+
#
18+
# * IEEE 754 correctly rounded (round-to-nearest, ties-to-even, one
19+
# rounding), with signed zeros, gradual underflow, and overflow;
20+
# * bit-for-bit identical to libquadmath's `fmaq` — including NaN payload
21+
# propagation and invalid-operation NaN generation (glibc soft-fp x86
22+
# semantics) — so Windows results match Linux/macOS exactly. Validated
23+
# against native `fmaq` on ~19 million cases (random all-class bit
24+
# patterns; normal, subnormal-heavy, overflow-boundary, cancellation,
25+
# and constructed-tie batteries; NaN/special fuzz; targeted
26+
# subnormal-multiplicand/addend populations) with zero mismatches, and
27+
# against an independent high-precision MPFR nearest/ties-to-even
28+
# oracle;
29+
# * allocation-free, ~30 ns/call on x86-64 (roughly 30x faster than the
30+
# `fmaq` ccall round-trip), with inferred effects consistent /
31+
# effect-free / terminating.
32+
#
33+
# Algorithm: classic soft-float mulAdd on the raw binary128 bit patterns.
34+
# Operands are decomposed into sign / unbiased exponent / 113-bit
35+
# significand (subnormals normalized); the exact 113x113 -> 226-bit product
36+
# is formed with four 64x64 -> 128-bit multiplies; product and addend are
37+
# held in a 256-bit fixed-point accumulator (a pair of UInt128) with the
38+
# leading significand bit pinned at bit 254; alignment uses shift-right-jam
39+
# (any shifted-out bit sets the LSB as a sticky). Jamming preserves correct
40+
# rounding here because a jam can only occur when the exponent gap exceeds
41+
# ~30 (product shifted) or ~142 (addend shifted), in which case catastrophic
42+
# cancellation is impossible and the jam bit stays >= 100 positions below
43+
# the guard bit; conversely, whenever cancellation can occur (gap <= 1) the
44+
# alignment shift loses no bits and the subtraction is exact. A single
45+
# round-to-nearest-even at 113 bits finishes (guard bit 141, sticky bits
46+
# 0..140), with a pre-round jam-shift onto the subnormal grid when the
47+
# result underflows (the minimum-normal rounding carry falls out of the bit
48+
# assembly automatically) and overflow to +-Inf.
49+
#
50+
# NaN semantics match libquadmath (glibc soft-fp `_FP_CHOOSENAN` for x86),
51+
# determined empirically against `fmaq`: among competing NaNs the one with
52+
# the larger raw 112-bit fraction field wins (the quiet bit participates in
53+
# the comparison); ties go to the product-stage NaN; an invalid product
54+
# (0 * Inf) or an invalid sum (Inf - Inf) generates the x86 "indefinite"
55+
# NaN 0xffff8000...0, which competes by the same rule; the product-stage
56+
# survivor is quieted BEFORE competing with the addend (this matters when
57+
# both stages hold signaling NaNs); the winner is returned with its quiet
58+
# bit set, sign and payload preserved. Floating-point exception flags are
59+
# not modeled (Julia does not expose them for Float128).
60+
61+
# ---------------------------------------------------------------------------
62+
# 256-bit helpers on (hi::UInt128, lo::UInt128) pairs
63+
# ---------------------------------------------------------------------------
64+
65+
# Exact 128x128 -> 256-bit product.
66+
@inline function _fma_mul256(x::UInt128, y::UInt128)
67+
m64 = UInt128(typemax(UInt64))
68+
x0 = x & m64; x1 = x >> 64
69+
y0 = y & m64; y1 = y >> 64
70+
p00 = x0 * y0 # each factor < 2^64: exact in UInt128
71+
p01 = x0 * y1
72+
p10 = x1 * y0
73+
p11 = x1 * y1
74+
mid = (p00 >> 64) + (p01 & m64) + (p10 & m64) # < 3*2^64, no overflow
75+
lo = (mid << 64) | (p00 & m64)
76+
hi = p11 + (p01 >> 64) + (p10 >> 64) + (mid >> 64)
77+
return hi, lo
78+
end
79+
80+
@inline function _fma_add256(ah::UInt128, al::UInt128, bh::UInt128, bl::UInt128)
81+
lo = al + bl
82+
hi = ah + bh + (lo < al ? one(UInt128) : zero(UInt128))
83+
return hi, lo
84+
end
85+
86+
# a - b, assuming a >= b.
87+
@inline function _fma_sub256(ah::UInt128, al::UInt128, bh::UInt128, bl::UInt128)
88+
lo = al - bl
89+
hi = ah - bh - (al < bl ? one(UInt128) : zero(UInt128))
90+
return hi, lo
91+
end
92+
93+
# Left shift by 0 <= s < 256 (callers never shift set bits past bit 255).
94+
@inline function _fma_shl256(hi::UInt128, lo::UInt128, s::Int)
95+
if s == 0
96+
return hi, lo
97+
elseif s < 128
98+
return (hi << s) | (lo >> (128 - s)), lo << s
99+
else
100+
return lo << (s - 128), zero(UInt128) # s == 128 gives (lo, 0)
101+
end
102+
end
103+
104+
# Right shift by s >= 0 with "jamming": any shifted-out bit sets the LSB.
105+
@inline function _fma_shr256jam(hi::UInt128, lo::UInt128, s::Int)
106+
if s == 0
107+
return hi, lo
108+
elseif s < 128
109+
sticky = (lo << (128 - s)) != 0
110+
nlo = (lo >> s) | (hi << (128 - s))
111+
return hi >> s, nlo | (sticky ? one(UInt128) : zero(UInt128))
112+
elseif s < 256
113+
t = s - 128 # t == 0: hi << 128 == 0
114+
sticky = (lo != 0) | ((hi << (128 - t)) != 0)
115+
return zero(UInt128), (hi >> t) | (sticky ? one(UInt128) : zero(UInt128))
116+
else
117+
sticky = (hi | lo) != 0
118+
return zero(UInt128), (sticky ? one(UInt128) : zero(UInt128))
119+
end
120+
end
121+
122+
@inline _fma_lz256(hi::UInt128, lo::UInt128) =
123+
hi == 0 ? 128 + leading_zeros(lo) : leading_zeros(hi)
124+
125+
# Decompose |x| (finite, nonzero) into (sig, e) with
126+
# value == sig * 2^(e - 112) and sig in [2^112, 2^113); subnormals normalized.
127+
@inline function _fma_split(a::UInt128)
128+
ef = Int((a >> significand_bits(Float128)) % UInt16)
129+
fr = a & significand_mask(Float128)
130+
if ef == 0 # subnormal (fr != 0)
131+
s = leading_zeros(fr) - exponent_bits(Float128) # bring MSB to bit 112
132+
return fr << s, 1 - exponent_bias(Float128) - s
133+
else
134+
return fr | (one(UInt128) << significand_bits(Float128)),
135+
ef - exponent_bias(Float128)
136+
end
137+
end
138+
139+
# NaN choice, matching libquadmath (glibc soft-fp, x86 _FP_CHOOSENAN):
140+
# the larger raw fraction field wins, ties to the first argument.
141+
@inline _fma_choosenan(a::UInt128, b::UInt128) =
142+
(a & significand_mask(Float128)) >= (b & significand_mask(Float128)) ? a : b
143+
144+
# ---------------------------------------------------------------------------
145+
# the fused multiply-add
146+
# ---------------------------------------------------------------------------
147+
148+
"""
149+
fma_emulated(x::Float128, y::Float128, z::Float128) -> Float128
150+
151+
Software fused multiply-add: the correctly rounded (round-to-nearest,
152+
ties-to-even) `x*y + z` with a single rounding, computed in pure Julia
153+
integer arithmetic; bit-for-bit compatible with libquadmath's `fmaq`,
154+
including NaN payload propagation. Used as the `fma` implementation on
155+
Windows, where calling `fmaq` corrupts floating-point state (issue #31).
156+
"""
157+
function fma_emulated(x::Float128, y::Float128, z::Float128)
158+
quiet = one(UInt128) << (significand_bits(Float128) - 1)
159+
# the x86 "indefinite" NaN produced by invalid operations
160+
default_nan = sign_mask(Float128) | exponent_mask(Float128) | quiet
161+
162+
ux = reinterpret(UInt128, x)
163+
uy = reinterpret(UInt128, y)
164+
uz = reinterpret(UInt128, z)
165+
ax = ux & ~sign_mask(Float128)
166+
ay = uy & ~sign_mask(Float128)
167+
az = uz & ~sign_mask(Float128)
168+
169+
sp = ((ux uy) & sign_mask(Float128)) != 0 # sign of the product
170+
sz = (uz & sign_mask(Float128)) != 0
171+
172+
# ---- specials: NaN propagation and invalid operations ---------------
173+
expmask = exponent_mask(Float128)
174+
xn = ax > expmask; yn = ay > expmask; zn = az > expmask
175+
if xn | yn | zn ||
176+
((ax == expmask) & (ay == 0)) || ((ay == expmask) & (ax == 0))
177+
local t::UInt128
178+
have_t = true
179+
if xn & yn
180+
t = _fma_choosenan(ux, uy)
181+
elseif xn
182+
t = ux
183+
elseif yn
184+
t = uy
185+
elseif !zn # 0 * Inf, z not NaN
186+
return reinterpret(Float128, default_nan)
187+
elseif ((ax == expmask) & (ay == 0)) || ((ay == expmask) & (ax == 0))
188+
t = default_nan # 0 * Inf competes with NaN z
189+
else
190+
have_t = false # only z is NaN
191+
t = zero(UInt128)
192+
end
193+
# soft-fp quiets the product-stage NaN when packing the intermediate
194+
# result, BEFORE it competes with z (whose fraction stays raw)
195+
t |= quiet
196+
r = have_t ? (zn ? _fma_choosenan(t, uz) : t) : uz
197+
return reinterpret(Float128, r | quiet)
198+
end
199+
if ax == expmask || ay == expmask # x or y infinite (no NaN)
200+
if az == expmask && sz != sp
201+
return reinterpret(Float128, default_nan) # Inf - Inf
202+
end
203+
return reinterpret(Float128,
204+
(sp ? sign_mask(Float128) : zero(UInt128)) | expmask)
205+
end
206+
az == expmask && return z # finite*finite + Inf
207+
if ax == 0 || ay == 0 # product is a zero
208+
if az == 0
209+
# (+-0) + (+-0): same signs keep the sign, else +0 (RN)
210+
return sp == sz ?
211+
reinterpret(Float128, sp ? sign_mask(Float128) : zero(UInt128)) :
212+
reinterpret(Float128, zero(UInt128))
213+
end
214+
return z
215+
end
216+
217+
# ---- exact product in 256-bit fixed point ---------------------------
218+
sigx, ex = _fma_split(ax)
219+
sigy, ey = _fma_split(ay)
220+
ph, pl = _fma_mul256(sigx, sigy) # value = P * 2^(ex+ey-224), MSB at 224 or 225
221+
msb = (ph >> 97) != 0 ? 225 : 224 # bit 225 of P == bit 97 of ph
222+
Ep = ex + ey + (msb - 224) # value = (M/2^254) * 2^Ep after the shift
223+
ph, pl = _fma_shl256(ph, pl, 254 - msb) # normalize: leading bit at 254
224+
225+
local Mh::UInt128, Ml::UInt128
226+
local E::Int
227+
local sres::Bool
228+
229+
if az == 0
230+
Mh, Ml, E, sres = ph, pl, Ep, sp
231+
else
232+
sigz, ez = _fma_split(az)
233+
zh, zl = _fma_shl256(zero(UInt128), sigz, 142) # leading bit at 254
234+
d = Ep - ez
235+
if d >= 0
236+
zh, zl = _fma_shr256jam(zh, zl, min(d, 300)) # align z to product
237+
E = Ep
238+
else
239+
ph, pl = _fma_shr256jam(ph, pl, min(-d, 300)) # align product to z
240+
E = ez
241+
end
242+
if sp == sz # effective addition
243+
Mh, Ml = _fma_add256(ph, pl, zh, zl)
244+
sres = sp
245+
if (Mh & sign_mask(Float128)) != 0 # carry into bit 255
246+
Mh, Ml = _fma_shr256jam(Mh, Ml, 1)
247+
E += 1
248+
end
249+
else # effective subtraction
250+
if ph > zh || (ph == zh && pl >= zl)
251+
Mh, Ml = _fma_sub256(ph, pl, zh, zl)
252+
sres = sp
253+
else
254+
Mh, Ml = _fma_sub256(zh, zl, ph, pl)
255+
sres = sz
256+
end
257+
if Mh == 0 && Ml == 0
258+
return reinterpret(Float128, zero(UInt128)) # exact cancel -> +0 (RN)
259+
end
260+
sh = _fma_lz256(Mh, Ml) - 1 # restore leading bit to 254
261+
if sh > 0
262+
Mh, Ml = _fma_shl256(Mh, Ml, sh)
263+
E -= sh
264+
end
265+
end
266+
end
267+
268+
# ---- round to nearest even at 113 bits ------------------------------
269+
be = E + exponent_bias(Float128) # tentative exponent field
270+
if be >= exponent_raw_max(Float128) # certain overflow
271+
return reinterpret(Float128,
272+
(sres ? sign_mask(Float128) : zero(UInt128)) | expmask)
273+
end
274+
if be <= 0 # subnormal range: pre-shift
275+
Mh, Ml = _fma_shr256jam(Mh, Ml, min(1 - be, 300))
276+
be = 0
277+
end
278+
279+
sig = Mh >> 14 # bits 142..254 -> 113 bits
280+
guard = (Mh >> 13) & 1 # bit 141
281+
sticky = ((Mh & ((one(UInt128) << 13) - 1)) | Ml) != 0 # bits 0..140
282+
if guard == 1 && (sticky || (sig & 1) == 1)
283+
sig += 1
284+
end
285+
286+
local r::UInt128
287+
if be == 0
288+
r = sig # a carry to 2^112 is exactly the minimum normal
289+
else
290+
if sig == (one(UInt128) << 113) # carry out of 113 bits
291+
sig >>= 1
292+
be += 1
293+
be >= exponent_raw_max(Float128) && return reinterpret(Float128,
294+
(sres ? sign_mask(Float128) : zero(UInt128)) | expmask)
295+
end
296+
r = (UInt128(be) << significand_bits(Float128)) |
297+
(sig & significand_mask(Float128))
298+
end
299+
return reinterpret(Float128,
300+
(sres ? sign_mask(Float128) : zero(UInt128)) | r)
301+
end
302+
303+
# On Windows the fmaq binding is deliberately absent (issue #31); use the
304+
# software implementation. Elsewhere the native correctly-rounded fmaq
305+
# defined in Quadmath.jl keeps priority, and `fma_emulated` remains
306+
# available for testing.
307+
@static if Sys.iswindows()
308+
@assume_effects :foldable fma(x::Float128, y::Float128, z::Float128) =
309+
fma_emulated(x, y, z)
310+
end

0 commit comments

Comments
 (0)