diff --git a/dace/runtime/include/dace/types.h b/dace/runtime/include/dace/types.h index a404de9236..47960baafe 100644 --- a/dace/runtime/include/dace/types.h +++ b/dace/runtime/include/dace/types.h @@ -4,6 +4,7 @@ #include #include +#include // std::numeric_limits (specialized for the low-precision structs at the bottom) #include #include // std::bit_cast (C++20) @@ -316,4 +317,48 @@ namespace dace }; } +#if !defined(__CUDACC__) && !defined(__HIPCC__) +// ``std::numeric_limits`` for the 16-bit low-precision structs. Without these the PRIMARY template +// answers ``max() == lowest() == infinity() == T()``, i.e. ZERO -- so a consumer that seeds a +// min/max reduction identity that way (``libraries/torch/dispatchers``) silently folds into zero +// instead of failing. Values are the IEEE binary16 / bfloat16 ones. +#define DACE_LP_LIMITS(TYPE, DIGITS, DIG10, MAXDIG10, MINEXP, MINEXP10, MAXEXP, MAXEXP10, IEC, MAXV, MINV, EPSV, \ + DENV) \ + template <> \ + struct numeric_limits<::dace::TYPE> { \ + static constexpr bool is_specialized = true, is_signed = true, is_integer = false; \ + static constexpr bool is_exact = false, has_infinity = true, has_quiet_NaN = true; \ + static constexpr bool has_signaling_NaN = false, is_bounded = true, is_modulo = false; \ + static constexpr bool is_iec559 = IEC, traps = false, tinyness_before = false; \ + static constexpr int radix = 2, digits = DIGITS, digits10 = DIG10, max_digits10 = MAXDIG10; \ + static constexpr int min_exponent = MINEXP, min_exponent10 = MINEXP10; \ + static constexpr int max_exponent = MAXEXP, max_exponent10 = MAXEXP10; \ + static constexpr float_round_style round_style = round_to_nearest; \ + static constexpr ::dace::TYPE min() noexcept { return ::dace::TYPE(MINV); } \ + static constexpr ::dace::TYPE max() noexcept { return ::dace::TYPE(MAXV); } \ + static constexpr ::dace::TYPE lowest() noexcept { return ::dace::TYPE(-(MAXV)); } \ + static constexpr ::dace::TYPE epsilon() noexcept { return ::dace::TYPE(EPSV); } \ + static constexpr ::dace::TYPE round_error() noexcept { return ::dace::TYPE(0.5f); } \ + static constexpr ::dace::TYPE denorm_min() noexcept { return ::dace::TYPE(DENV); } \ + static constexpr ::dace::TYPE quiet_NaN() noexcept { return ::dace::TYPE(__builtin_nanf("")); } \ + static constexpr ::dace::TYPE signaling_NaN() noexcept { return ::dace::TYPE(__builtin_nanf("")); } \ + static constexpr ::dace::TYPE infinity() noexcept { return ::dace::TYPE(__builtin_huge_valf()); } \ + } +namespace std +{ + DACE_LP_LIMITS(half, 11, 3, 5, -13, -4, 16, 4, true, 6.5504e+4f, 6.103515625e-05f, 9.765625e-04f, + 5.9604644775390625e-08f); + DACE_LP_LIMITS(bfloat16, 8, 2, 4, -125, -37, 128, 38, false, 3.38953139e+38f, 1.17549435e-38f, 7.8125e-03f, + 9.18354962e-41f); +} +#undef DACE_LP_LIMITS + +// The finite bounds are the whole point of the specializations; pin their bit patterns so a typo in +// a literal above cannot pass as a plausible-looking value. +static_assert(std::bit_cast(std::numeric_limits::max()) == 0x7BFF && + std::bit_cast(std::numeric_limits::denorm_min()) == 0x0001, "half limits"); +static_assert(std::bit_cast(std::numeric_limits::max()) == 0x7F7F && + std::bit_cast(std::numeric_limits::denorm_min()) == 0x0001, "bf16 limits"); +#endif + #endif // __DACE_TYPES_H diff --git a/tests/codegen/lowp_numeric_limits_test.py b/tests/codegen/lowp_numeric_limits_test.py new file mode 100644 index 0000000000..37a2c94acf --- /dev/null +++ b/tests/codegen/lowp_numeric_limits_test.py @@ -0,0 +1,55 @@ +# Copyright 2019-2026 ETH Zurich and the DaCe authors. All rights reserved. +"""``std::numeric_limits`` must be specialized for ``dace::half`` and ``dace::bfloat16``. Unspecialized +class types get the primary template, whose ``max()``/``lowest()``/``infinity()`` are all ``T()`` -- +zero -- so identity-seeded min/max reductions silently produce zeros instead of failing to build.""" +import os +import shutil +import subprocess + +import pytest + +import dace +from dace.config import Config + +INCLUDE = os.path.join(os.path.dirname(os.path.abspath(dace.__file__)), 'runtime', 'include') + +#: Every check would pass trivially against the primary template if it compared to zero, so each one +#: is a value the primary template cannot produce. +PROBE_SOURCE = r''' +#include "dace/types.h" +template +static bool ok() { + using L = std::numeric_limits; + return L::is_specialized && (float)L::max() > 0.0f && (float)L::lowest() < 0.0f && + (float)L::infinity() > (float)L::max() && (float)L::denorm_min() > 0.0f; +} +int main() { + if (!ok() || !ok()) return 1; + // The exact finite bounds of IEEE binary16 and bfloat16. + if ((float)std::numeric_limits::max() != 65504.0f) return 2; + if ((float)std::numeric_limits::lowest() != -3.38953139e+38f) return 3; + return 0; +} +''' + + +def test_numeric_limits_is_specialized(tmp_path): + executable = Config.get('compiler', 'cpu', 'executable') or 'c++' + assert shutil.which(executable), f'configured compiler {executable!r} is not on PATH' + source, binary = tmp_path / 'probe.cpp', tmp_path / 'probe' + source.write_text(PROBE_SOURCE) + build = subprocess.run([ + executable, f'-std=c++{Config.get("compiler", "cpp_standard")}', '-I', INCLUDE, + str(source), '-o', + str(binary) + ], + capture_output=True, + text=True, + timeout=300) + assert build.returncode == 0, f'probe did not compile:\n{build.stderr}' + assert subprocess.run([str(binary)], timeout=60).returncode == 0, \ + 'std::numeric_limits is unspecialized for a low-precision type, so its identities are zero' + + +if __name__ == '__main__': + pytest.main([__file__, '-v'])