Skip to content

Commit 6de39a9

Browse files
authored
Merge pull request #171 from heal-research/fix/fastmath-nan-propagation
Fix FastExp/FastTanh silently laundering NaN into a finite wrong value
2 parents dd2372c + da7d0f9 commit 6de39a9

2 files changed

Lines changed: 80 additions & 3 deletions

File tree

include/operon/interpreter/backend/eve/functions.hpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,17 @@ auto FastExp(eve::wide<T> a) -> eve::wide<T> {
3939
// just deep in the clamped-out region but even near the boundary,
4040
// e.g. a=-88.7 subnormal-adjacent), e.g. FastExp(-128) returned
4141
// -128 instead of ~0.
42-
return eve::max(eve::ldexp(y, eve::convert(m, eve::as<std::int32_t>{})), W{T(0)});
42+
auto result = eve::max(eve::ldexp(y, eve::convert(m, eve::as<std::int32_t>{})), W{T(0)});
43+
// eve::clamp/min/max on this platform return the non-NaN operand
44+
// instead of propagating NaN (x86 minps/maxps semantics, not IEEE
45+
// NaN rules) -- the initial clamp above silently turns a NaN input
46+
// into a finite boundary value, and the max(...,0) floor does the
47+
// same again if that corrupted computation itself produces NaN. Both
48+
// combined made FastExp(NaN) return 0 instead of NaN, e.g. from a
49+
// NaN that arose from an earlier sqrt(negative) elsewhere in the
50+
// tree -- silently laundering it into a plausible-looking fitness
51+
// value instead of correctly propagating "undefined" outward.
52+
return eve::if_else(eve::is_nan(a), eve::nan(eve::as<W>{}), result);
4353
} else {
4454
return eve::exp(a);
4555
}
@@ -185,7 +195,12 @@ auto FastTanh(eve::wide<T> a) -> eve::wide<T> {
185195
auto q = eve::fma(x2, W{T(1.19825839466702e-06f)}, W{T(1.18534705686654e-04f)});
186196
q = eve::fma(x2, q, W{T(2.26843463243900e-03f)});
187197
q = eve::fma(x2, q, W{T(4.89352518554385e-03f)});
188-
return eve::if_else(tiny, x, p / q);
198+
auto result = eve::if_else(tiny, x, p / q);
199+
// Same eve::clamp NaN-swallowing issue as FastExp above: a NaN input
200+
// (e.g. from sqrt(negative) elsewhere in the tree) silently becomes
201+
// the clamp boundary instead of propagating, so FastTanh(NaN) would
202+
// otherwise return -1 or 1 instead of NaN.
203+
return eve::if_else(eve::is_nan(a), eve::nan(eve::as<W>{}), result);
189204
} else {
190205
// 19/18-degree rational minimax, ~2 ULP on [-18.7,18.7].
191206
// Ported from Eigen's ptanh_double (rminimax-optimised coefficients).
@@ -210,7 +225,8 @@ auto FastTanh(eve::wide<T> a) -> eve::wide<T> {
210225
q = eve::fma(x2, q, W{T(3.437448108450402717e-02)});
211226
q = eve::fma(x2, q, W{T(4.851805297361760360e-01)});
212227
q = eve::fma(x2, q, W{T(1.0)});
213-
return eve::if_else(tiny, x, p / q);
228+
auto result = eve::if_else(tiny, x, p / q);
229+
return eve::if_else(eve::is_nan(a), eve::nan(eve::as<W>{}), result);
214230
}
215231
}
216232

test/source/implementation/backend.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ namespace {
8383
}
8484

8585
constexpr T Inf = std::numeric_limits<T>::infinity();
86+
constexpr T NaN = std::numeric_limits<T>::quiet_NaN();
8687
constexpr int N = 10000;
8788
} // namespace
8889

@@ -196,6 +197,66 @@ TEST_CASE("Backend transcendental ULP accuracy", "[backend]")
196197
}
197198
}
198199

200+
// Regression net for a previously-unknown bug: eve::clamp/min/max on this
201+
// platform return the non-NaN operand instead of propagating NaN (x86
202+
// minps/maxps semantics, not IEEE-754 NaN rules), so any Fast* approximation
203+
// that range-limits its input via those primitives can silently turn a NaN
204+
// (e.g. from sqrt(negative) elsewhere in a tree) into a finite,
205+
// plausible-looking wrong value instead of correctly propagating
206+
// "undefined" outward. Found in FastExp and FastTanh (both fixed with an
207+
// explicit is_nan guard, matching FastLog's pre-existing one).
208+
//
209+
// Each lane in every batch here is either NaN or the inert padding value
210+
// 0.5 -- deliberately NOT mixed with other "interesting" edge-case values.
211+
// A NaN placed alongside an unrelated real value in the same batch was
212+
// found, separately, to trigger a genuine third-party bug in eve::sinh
213+
// (a NaN in one SIMD lane corrupting an unrelated lane's result) -- see
214+
// project_eve_sinh_crosslane_nan_bug in memory. That's a different bug in
215+
// eve itself, not something this test is trying to catch, and mixing
216+
// values here would make this test's pass/fail depend on unrelated
217+
// batch-layout coincidences.
218+
TEST_CASE("Backend NaN propagation", "[backend]")
219+
{
220+
auto allNan = [](auto fn) -> bool {
221+
Buf src{};
222+
src.v.fill(T{0.5});
223+
src.v[0] = NaN;
224+
Buf dst{};
225+
fn(dst.v.data(), T{1}, src.v.data());
226+
return std::isnan(dst.v[0]);
227+
};
228+
229+
CHECK(allNan(Backend::Exp<T,S>));
230+
CHECK(allNan(Backend::Log<T,S>));
231+
CHECK(allNan(Backend::Log1p<T,S>));
232+
CHECK(allNan(Backend::Logabs<T,S>));
233+
CHECK(allNan(Backend::Sin<T,S>));
234+
CHECK(allNan(Backend::Cos<T,S>));
235+
CHECK(allNan(Backend::Tan<T,S>));
236+
CHECK(allNan(Backend::Asin<T,S>));
237+
CHECK(allNan(Backend::Acos<T,S>));
238+
CHECK(allNan(Backend::Atan<T,S>));
239+
CHECK(allNan(Backend::Sinh<T,S>));
240+
CHECK(allNan(Backend::Cosh<T,S>));
241+
CHECK(allNan(Backend::Tanh<T,S>));
242+
CHECK(allNan(Backend::Sqrt<T,S>));
243+
CHECK(allNan(Backend::Sqrtabs<T,S>));
244+
CHECK(allNan(Backend::Cbrt<T,S>));
245+
246+
auto allNan2 = [](auto fn, T x, T y) -> bool {
247+
Buf sa{}; sa.v.fill(T{0.5}); sa.v[0] = x;
248+
Buf sb{}; sb.v.fill(T{0.5}); sb.v[0] = y;
249+
Buf dst{};
250+
fn(dst.v.data(), T{1}, sa.v.data(), sb.v.data());
251+
return std::isnan(dst.v[0]);
252+
};
253+
254+
CHECK(allNan2(Backend::Pow<T,S>, NaN, 2.f));
255+
CHECK(allNan2(Backend::Pow<T,S>, 2.f, NaN));
256+
CHECK(allNan2(Backend::Powabs<T,S>, NaN, 2.f));
257+
CHECK(allNan2(Backend::Powabs<T,S>, 2.f, NaN));
258+
}
259+
199260
TEST_CASE("Backend Pow/Powabs ULP accuracy", "[backend]")
200261
{
201262
auto MaxUlpError2 = [](auto fn, auto ref, std::vector<T> const& xs, std::vector<T> const& ys) {

0 commit comments

Comments
 (0)