Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions dace/runtime/include/dace/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <cstdint>
#include <complex>
#include <limits> // std::numeric_limits (specialized for the low-precision structs at the bottom)
#include <type_traits>
#include <bit> // std::bit_cast (C++20)

Expand Down Expand Up @@ -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);
Comment on lines +349 to +352

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add the f suffix when these are cast to the data type anyway?

}
#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<uint16_t>(std::numeric_limits<dace::half>::max()) == 0x7BFF &&
std::bit_cast<uint16_t>(std::numeric_limits<dace::half>::denorm_min()) == 0x0001, "half limits");
static_assert(std::bit_cast<uint16_t>(std::numeric_limits<dace::bfloat16>::max()) == 0x7F7F &&
std::bit_cast<uint16_t>(std::numeric_limits<dace::bfloat16>::denorm_min()) == 0x0001, "bf16 limits");
#endif

#endif // __DACE_TYPES_H
55 changes: 55 additions & 0 deletions tests/codegen/lowp_numeric_limits_test.py
Original file line number Diff line number Diff line change
@@ -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 <typename T>
static bool ok() {
using L = std::numeric_limits<T>;
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<dace::half>() || !ok<dace::bfloat16>()) return 1;
// The exact finite bounds of IEEE binary16 and bfloat16.
if ((float)std::numeric_limits<dace::half>::max() != 65504.0f) return 2;
if ((float)std::numeric_limits<dace::bfloat16>::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'])