A Case Study in Rounding
Studying the float→half conversion operation is a useful
way to learn the details of floating point encodings and rounding.
Because it’s a simple unary operation, we can focus on the encoding and
rounding without getting distracted by the details of floating point
arithmetic and the precision of intermediate representations.
When converting from float to half, the
correct output for any float too large to represent is
half infinity; any float too small to
represent as a half (even a denormal half)
must be clamped to 0.0. The maximum float that
rounds to half 0.0 is 0x32FFFFFF, or
2.98×10-8, while the smallest float that rounds
to half infinity is 65520.0.
float values inside this range can be converted to
half by propagating the sign bit, rebiasing the exponent
(since float has an 8-bit exponent biased by
127 and half has a 5-bit exponent biased by
15) and rounding the float mantissa to the
nearest half mantissa value. Rounding is straightforward in
all cases except when the input value falls exactly between the two
possible output values; when this case applies, the IEEE standard
specifies rounding to the “nearest even” value. In decimal arithmetic,
this would mean rounding 1.5 to 2.0, but also
rounding 2.5 to 2.0 and (for example) rounding
0.5 to 0.0.
Listing B-1 shows a C routine that exactly replicates the
float-to-half conversion operation. It is
built around a set of symbolic constants that name the exponent shifts,
bit counts, biases, and masks of the two formats, so the shifts and
comparisons in the body read in terms of the formats’ fields rather than
hard-coded magic numbers. The routine is adapted from a formulation
Norbert Juffa posted to the NVIDIA
Developer Forums, with the hard-coded constants replaced by the
symbolic ones used here; Juffa’s original is reproduced under its BSD
2-Clause license in the note that follows.1
The macro LG_MAKE_MASK, used in Listing B-1, creates a
mask with a given bit count:
#define LG_MAKE_MASK(bits) ((1<<(bits))-1)
A volatile union is used to treat the same 32-bit value
as float and unsigned int; idioms such as
*((float *) (&u)) are not portable.
The routine copies the input’s sign bit into the output, then
dispatches on the input’s exponent field. If every exponent bit is set,
the input is INF or NaN: infinity is
propagated (preserving the sign already placed in the output), and
anything else becomes a canonical NaN. Otherwise, if the
input’s biased exponent falls below f32MinRNonzero – the
smallest float exponent whose values can round to a nonzero
half, halfway between 0.0 and the smallest
half denormal – the input is too small to represent, and
the result is the signed 0.0 already sitting in the output.
This single threshold replaces the pair of clamps that a more literal
implementation would apply at both ends of the representable range.
For the values that remain, the routine makes the input mantissa’s
implicit leading 1 explicit and computes
shift, the unbiased input exponent. If shift
exceeds f16MaxExp, the value overflows to half
infinity. Otherwise, the normal and denormal cases
must be handled in distinct ways. For a normal result, the mantissa is
shifted right by f32ExpShift − f16ExpShift and the rebiased
exponent (f16ExpBias − 1 + shift) is added into the
exponent field; the bias is reduced by one because the explicit leading
1, shifted up into the exponent field, supplies the missing count. For a
denormal result (shift < f16MinExp), the mantissa is
shifted right by an additional f16MinExp − shift places, so
the implicit 1 lands at the correct denormal position.
In both cases, the bits shifted off the bottom of the mantissa are
retained, MSB-aligned, in the working value. Rounding then reduces to a
single comparison: 0x80000000 is exactly half an ULP of the
result, so if the residual exceeds it, the result is rounded up; if it
equals that value exactly – a true tie – the result is rounded up only
when the output’s least significant bit is already set. This is the IEEE
round-to-nearest-even rule, and expressing it as a comparison against
the half-ULP point sidesteps the separate rounding-mask construction
that earlier versions of this routine used.
// Make mask out of bit count#define LG_MAKE_MASK(bits) ((1<<(bits))-1) const int f16ExpShift = 10;const int f16MantissaBits = 10;const int f16ExpBits = 5; const int f16ExpBias = 15;const int f16MinExp = -14;const int f16MaxExp = 15;const int f16SignMask = 0x8000; const int f32ExpShift = 23;const int f32MantissaBits = 23;const int f32ExpBits = 8;const int f32ExpBias = 127;const int f32SignMask = 0x80000000; // All bits of the exponent field set (for float16, also infinity).const int f16ExpMask = LG_MAKE_MASK(f16ExpBits) << f16ExpShift;const int f32ExpMask = LG_MAKE_MASK(f32ExpBits) << f32ExpShift; // Smallest float32 exponent field whose values may round to a// nonzero float16 (halfway between 0 and the smallest denormal)const int f32MinRNonzero = (f16MinExp-f16MantissaBits-1+f32ExpBias) << f32ExpShift; unsigned shortConvertFloatToHalf( float f ){ // Use a volatile union to portably coerce the 32-bit // float into a 32-bit integer. volatile union { float f; unsigned int u; } uf; uf.f = f; unsigned int ia = uf.u; unsigned short ir; // start by propagating the sign bit ir = (ia >> 16) & f16SignMask; if ( (ia & f32ExpMask) == f32ExpMask ) { // INF or NaN if ( (ia & ~f32SignMask) == f32ExpMask ) { ir |= f16ExpMask; // INF - propagate sign } else { ir = f16ExpMask | LG_MAKE_MASK(f16MantissaBits); // canonical NaN } } else if ( (ia & f32ExpMask) >= (unsigned int) f32MinRNonzero ) { int shift = (int) ((ia >> f32ExpShift) & LG_MAKE_MASK(f32ExpBits)) - f32ExpBias; if ( shift > f16MaxExp ) { ir |= f16ExpMask; // overflow - round to infinity } else { // extract mantissa and make the implicit 1 explicit ia = (ia & LG_MAKE_MASK(f32MantissaBits)) | (1<<f32ExpShift); if ( shift < f16MinExp ) { // Case 1: float16 denormal int RelativeShift = f32ExpShift-f16ExpShift+f16MinExp-shift; ir |= ia >> RelativeShift; ia = ia << (32 - RelativeShift); } else { // Case 2: float16 normal int RelativeShift = f32ExpShift-f16ExpShift; ir |= ia >> RelativeShift; ia = ia << (32 - RelativeShift); // f16ExpBias-1, since the explicit 1 shifted into // the exponent field adds 1 to the exponent ir = ir + ((f16ExpBias-1+shift) << f16ExpShift); } // Round to nearest even: ia holds the shifted-out bits, // MSB-aligned; 0x80000000 is exactly half an ULP of the result. if ( (ia > 0x80000000) || ((ia == 0x80000000) && (ir & 1)) ) { ir++; } } } return ir;}
ConvertFloatToHalf()In practice, developers should convert float to
half by using the __float2half() intrinsic.
This sample routine is provided purely to aid in understanding floating
point layout and rounding; also, examining all the special-case code for
INF, NaN and denormal values helps to
illustrate why these features of the IEEE spec have been controversial
since its inception: they make hardware slower, more costly, or both due
to increased silicon area and engineering effort for validation.
In the code accompanying this book, the ConvertFloatToHalf()
routine of Listing B-1 is incorporated into a program called
float_to_float16.cu that tests its output for every 32-bit floating
point value.
Juffa’s original, from the forum post cited above, is reproduced here under its BSD 2-Clause license:
/*
Copyright (c) 2015, Norbert Juffa
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
__fp16 uint16_as_fp16 (uint16_t a)
{
__fp16 res;
#if defined (__cplusplus)
memcpy (&res, &a, sizeof (res));
#else /* __cplusplus */
volatile union {
__fp16 f;
uint16_t i;
} cvt;
cvt.i = a;
res = cvt.f;
#endif /* __cplusplus */
return res;
}
uint32_t fp32_as_uint32 (float a)
{
uint32_t res;
#if defined (__cplusplus)
memcpy (&res, &a, sizeof (res));
#else /* __cplusplus */
volatile union {
float f;
uint32_t i;
} cvt;
cvt.f = a;
res = cvt.i;
#endif /* __cplusplus */
return res;
}
/* host version of device function __float2half_rn() */
__fp16 float2half_rn (float a)
{
uint32_t ia = fp32_as_uint32 (a);
uint16_t ir;
ir = (ia >> 16) & 0x8000;
if ((ia & 0x7f800000) == 0x7f800000) {
if ((ia & 0x7fffffff) == 0x7f800000) {
ir |= 0x7c00; /* infinity */
} else {
ir = 0x7fff; /* canonical NaN */
}
} else if ((ia & 0x7f800000) >= 0x33000000) {
int shift = (int)((ia >> 23) & 0xff) - 127;
if (shift > 15) {
ir |= 0x7c00; /* infinity */
} else {
ia = (ia & 0x007fffff) | 0x00800000; /* extract mantissa */
if (shift < -14) { /* denormal */
ir |= ia >> (-1 - shift);
ia = ia << (32 - (-1 - shift));
} else { /* normal */
ir |= ia >> (24 - 11);
ia = ia << (32 - (24 - 11));
ir = ir + ((14 + shift) << 10);
}
/* IEEE-754 round to nearest of even */
if ((ia > 0x80000000) || ((ia == 0x80000000) && (ir & 1))) {
ir++;
}
}
}
return uint16_as_fp16 (ir);
}