Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utilities for decimal <--> floating conversion #15359

Merged
merged 21 commits into from
May 29, 2024
Merged
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
31aec0e
Utility functions for new decimal/float conversion
pmattione-nvidia Mar 21, 2024
d3332f5
Merge branch 'branch-24.06' into convert_utils
wence- Mar 21, 2024
84d9be8
Switch union to memcpy
pmattione-nvidia Apr 1, 2024
0e65c39
Merge branch 'convert_utils' of https://github.com/pmattione-nvidia/c…
pmattione-nvidia Apr 1, 2024
45ed607
Merge branch 'branch-24.06' into convert_utils
pmattione-nvidia Apr 16, 2024
359a9f7
Move utils to dedicated conversion header, apply feedback, convert fr…
pmattione-nvidia Apr 30, 2024
3529d8d
Merge branch 'branch-24.06' into convert_utils
pmattione-nvidia Apr 30, 2024
45ee1bd
Only allow specific types for counting # sig bits
pmattione-nvidia May 6, 2024
107a86f
Fix typo
pmattione-nvidia May 6, 2024
309566c
Update cpp/include/cudf/fixed_point/floating_conversion.hpp
pmattione-nvidia May 6, 2024
f4f5819
Update cpp/include/cudf/fixed_point/floating_conversion.hpp
pmattione-nvidia May 20, 2024
41b205f
Address feedback
pmattione-nvidia May 20, 2024
ec398c3
Merge branch 'convert_utils' of https://github.com/pmattione-nvidia/c…
pmattione-nvidia May 20, 2024
eeed7bc
Address more feedback
pmattione-nvidia May 21, 2024
9c09519
Merge branch 'branch-24.08' into convert_utils
pmattione-nvidia May 21, 2024
59d6edd
Use enable-if instead of static_assert
pmattione-nvidia May 21, 2024
b2cc813
Merge branch 'branch-24.08' into convert_utils
pmattione-nvidia May 21, 2024
d3348cf
Fix whitespace
pmattione-nvidia May 21, 2024
77c84c3
Handle exponent under/overflow
pmattione-nvidia May 28, 2024
150a115
Fix bias definition
pmattione-nvidia May 28, 2024
dcc4819
Merge branch 'branch-24.08' into convert_utils
pmattione-nvidia May 29, 2024
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
232 changes: 232 additions & 0 deletions cpp/include/cudf/fixed_point/floating_conversion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@

#pragma once

#include <cudf/utilities/traits.hpp>

#include <cuda/std/limits>
#include <cuda/std/type_traits>

#include <cstring>

namespace numeric {

/**
Expand All @@ -29,6 +34,233 @@ namespace numeric {

namespace detail {

/**
* @brief Helper struct for getting and setting the components of a floating-point value
*
* @tparam FloatingType Type of floating-point value
*/
template <typename FloatingType, CUDF_ENABLE_IF(cuda::std::is_floating_point_v<FloatingType>)>
struct floating_converter {
// This struct assumes we're working with IEEE 754 floating-point values.
// Details on the IEEE-754 floating-point format:
// Format: https://learn.microsoft.com/en-us/cpp/build/ieee-floating-point-representation
// Float Visualizer: https://www.h-schmidt.net/FloatConverter/IEEE754.html
static_assert(cuda::std::numeric_limits<FloatingType>::is_iec559, "Assumes IEEE 754");

/// Unsigned int type with same size as floating type
using IntegralType =
cuda::std::conditional_t<cuda::std::is_same_v<FloatingType, float>, uint32_t, uint64_t>;

// The high bit is the sign bit (0 for positive, 1 for negative).
/// How many bits in the floating type
static constexpr int num_floating_bits = sizeof(FloatingType) * CHAR_BIT;
/// The index of the sign bit
static constexpr int sign_bit_index = num_floating_bits - 1;
/// The mask to select the sign bit
static constexpr IntegralType sign_mask = (IntegralType(1) << sign_bit_index);

// The low 23 / 52 bits (for float / double) are the mantissa.
// The mantissa is normalized. There is an understood 1 bit to the left of the binary point.
// The value of the mantissa is in the range [1, 2).
/// # mantissa bits (-1 for understood bit)
static constexpr int num_mantissa_bits = cuda::std::numeric_limits<FloatingType>::digits - 1;
/// The mask for the understood bit
static constexpr IntegralType understood_bit_mask = (IntegralType(1) << num_mantissa_bits);
/// The mask to select the mantissa
static constexpr IntegralType mantissa_mask = understood_bit_mask - 1;

// And in between are the bits used to store the biased power-of-2 exponent.
/// # exponents bits (-1 for sign bit)
hyperbolic2346 marked this conversation as resolved.
Show resolved Hide resolved
static constexpr int num_exponent_bits = num_floating_bits - num_mantissa_bits - 1;
/// The mask for the exponents, unshifted
static constexpr IntegralType unshifted_exponent_mask =
(IntegralType(1) << num_exponent_bits) - 1;
/// The mask to select the exponents
static constexpr IntegralType exponent_mask = unshifted_exponent_mask << num_mantissa_bits;

// To store positive and negative exponents as unsigned values, the stored value for
// the power-of-2 is exponent + bias. The bias is 126 for floats and 1022 for doubles.
/// 126 / 1022 for float / double
static constexpr IntegralType exponent_bias =
cuda::std::numeric_limits<FloatingType>::max_exponent - 2;

/**
* @brief Reinterpret the bits of a floating-point value as an integer
*
* @param floating The floating-point value to cast
* @return An integer with bits identical to the input
*/
CUDF_HOST_DEVICE inline static IntegralType bit_cast_to_integer(FloatingType floating)
{
// Convert floating to integer
IntegralType integer_rep;
memcpy(&integer_rep, &floating, sizeof(floating));
return integer_rep;
}

/**
* @brief Reinterpret the bits of an integer as floating-point value
*
* @param integer The integer to cast
* @return A floating-point value with bits identical to the input
*/
CUDF_HOST_DEVICE inline static FloatingType bit_cast_to_floating(IntegralType integer)
{
// Convert back to float
FloatingType floating;
memcpy(&floating, &integer, sizeof(floating));
return floating;
}

/**
* @brief Extracts the integral significand of a bit-casted floating-point number
*
* @param integer_rep The bit-casted floating value to extract the exponent from
* @return The integral significand, bit-shifted to a (large) whole number
*/
CUDF_HOST_DEVICE inline static IntegralType get_base2_value(IntegralType integer_rep)
{
// Extract the significand, setting the high bit for the understood 1/2
return (integer_rep & mantissa_mask) | understood_bit_mask;
}

/**
* @brief Extracts the sign bit of a bit-casted floating-point number
*
* @param integer_rep The bit-casted floating value to extract the exponent from
* @return The sign bit
*/
CUDF_HOST_DEVICE inline static bool get_is_negative(IntegralType integer_rep)
{
// Extract the sign bit:
return static_cast<bool>(sign_mask & integer_rep);
}

/**
* @brief Extracts the exponent of a bit-casted floating-point number
*
* @note This returns INT_MIN for +/-0, +/-inf, NaN's, and denormals
* For all of these cases, the decimal fixed_point number should be set to zero
*
* @param integer_rep The bit-casted floating value to extract the exponent from
* @return The stored base-2 exponent, or INT_MIN for special values
*/
CUDF_HOST_DEVICE inline static int get_exp2(IntegralType integer_rep)
{
// First extract the exponent bits and handle its special values.
// To minimize branching, all of these special cases will return INT_MIN.
// For all of these cases, the decimal fixed_point number should be set to zero.
auto const exponent_bits = integer_rep & exponent_mask;
if (exponent_bits == 0) {
// Because of the understood set-bit not stored in the mantissa, it is not possible
// to store the value zero directly. Instead both +/-0 and denormals are represented with
// the exponent bits set to zero.
// Thus it's fastest to just floor (generally unwanted) denormals to zero.
return INT_MIN;
} else if (exponent_bits == exponent_mask) {
//+/-inf and NaN values are stored with all of the exponent bits set.
// As none of these are representable by integers, we'll return the same value for all cases.
hyperbolic2346 marked this conversation as resolved.
Show resolved Hide resolved
return INT_MIN;
}

// Extract the exponent value: shift the bits down and subtract the bias.
using SignedIntegralType = cuda::std::make_signed_t<IntegralType>;
SignedIntegralType const shifted_exponent_bits = exponent_bits >> num_mantissa_bits;
return shifted_exponent_bits - static_cast<SignedIntegralType>(exponent_bias);
}

/**
* @brief Sets the sign bit of a positive floating-point number
*
* @param floating The floating-point value to set the sign of. Must be positive.
* @param is_negative The sign bit to set for the floating-point number
* @return The input floating-point value with the chosen sign
*/
CUDF_HOST_DEVICE inline static FloatingType set_is_negative(FloatingType floating,
bool is_negative)
{
// Convert floating to integer
IntegralType integer_rep = bit_cast_to_integer(floating);

// Set the sign bit. Note that the input floating-point number must be positive (bit = 0).
integer_rep |= (IntegralType(is_negative) << sign_bit_index);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this right? If the sign bit is already 1, it's not possible to change it with an OR operator, regardless of is_negative. Are we assuming the sign bit is always 0 when entering this function? If so, that is not documented.

Are there test cases for this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The sign is always zero on input, I'll add that comment. It's not part of this PR, but when I switch from the old decimal/floating conversion code to this, the fixed_point tests test all of this functionality, and all of those tests pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment added

Copy link
Contributor

Choose a reason for hiding this comment

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

Might I suggest a reminder in the comment added that 0 means positive value, so the or will do what we want. Simply saying it must be positive implies the reader knows this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@bdice bdice May 28, 2024

Choose a reason for hiding this comment

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

I don't like the design of this function.

  1. If is_negative is true, this should be a no-op -- we shouldn't even cast back and forth.
  2. Where is this being called? It doesn't appear to be used in this PR, and it doesn't seem like it's designed for public consumption.
  3. Can we avoid bitwise manipulation entirely and just negate the value? i.e. return is_negative ? -floating : floating;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This PR contains utilities for the upcoming primary PR for the decimal <--> floating conversion. I broke the code into several PRs as it is quite large. So nothing is calling this yet.

This specific function is used at the end of decimal --> floating. In the main algorithm we do a lot of bit-shifting so the sign is initially cleared, and here we are setting it at the end. Doing it this way (cast + or) requires no branching, so you don't pay a performance penalty for the branch. A couple months ago I did a bunch of benchmarking trying different methods and this method was the fastest.


// Convert back to float
return bit_cast_to_floating(integer_rep);
}

/**
* @brief Adds to the base-2 exponent of a floating-point number
*
* @param floating The floating value to add to the exponent of
* @param exp2 The power-of-2 to add to the floating-point number
* @return The input floating-point value * 2^exp2
*/
CUDF_HOST_DEVICE inline static FloatingType add_exp2(FloatingType floating, int exp2)
{
// Convert floating to integer
auto integer_rep = bit_cast_to_integer(floating);

// Extract the currently stored (biased) exponent
auto exponent_bits = integer_rep & exponent_mask;
auto stored_exp2 = exponent_bits >> num_mantissa_bits;

// Add the additional power-of-2
stored_exp2 += exp2;
hyperbolic2346 marked this conversation as resolved.
Show resolved Hide resolved
exponent_bits = stored_exp2 << num_mantissa_bits;

// Clear existing exponent bits and set new ones
integer_rep &= (~exponent_mask);
integer_rep |= exponent_bits;

// Convert back to float
return bit_cast_to_floating(integer_rep);
}
};

/**
* @brief Determine the number of significant bits in an integer
*
* @tparam T Type of input integer value. Must be either uint32_t, uint64_t, or __uint128_t
* @param value The integer whose bits are being counted
* @return The number of significant bits: the # of bits - # of leading zeroes
*/
template <typename T,
CUDF_ENABLE_IF(std::is_same_v<T, uint32_t> || std::is_same_v<T, uint64_t> ||
std::is_same_v<T, __uint128_t>)>
CUDF_HOST_DEVICE inline int count_significant_bits(T value)
{
#ifdef __CUDA_ARCH__
if constexpr (std::is_same_v<T, uint64_t>) {
return 64 - __clzll(static_cast<int64_t>(value));
} else if constexpr (std::is_same_v<T, uint32_t>) {
return 32 - __clz(static_cast<int32_t>(value));
} else if constexpr (std::is_same_v<T, __uint128_t>) {
// 128 bit type, must break up into high and low components
auto const high_bits = static_cast<int64_t>(value >> 64);
auto const low_bits = static_cast<int64_t>(value);
return 128 - (__clzll(high_bits) + static_cast<int>(high_bits == 0) * __clzll(low_bits));
}
#else
// Undefined behavior to call __builtin_clzll() with zero in gcc and clang
pmattione-nvidia marked this conversation as resolved.
Show resolved Hide resolved
if (value == 0) { return 0; }

if constexpr (std::is_same_v<T, uint64_t>) {
return 64 - __builtin_clzll(value);
} else if constexpr (std::is_same_v<T, uint32_t>) {
return 32 - __builtin_clz(value);
} else if constexpr (std::is_same_v<T, __uint128_t>) {
// 128 bit type, must break up into high and low components
auto const high_bits = static_cast<uint64_t>(value >> 64);
if (high_bits == 0) {
return 64 - __builtin_clzll(static_cast<uint64_t>(value));
} else {
return 128 - __builtin_clzll(high_bits);
}
}
#endif
}

/**
* @brief Recursively calculate a signed large power of 10 (>= 10^19) that can only be stored in an
* 128bit integer
Expand Down
Loading