Skip to content

Classic Floating-Point (cfloat): Configurable IEEE-754 Compatible Arithmetic

IEEE-754 defines a handful of fixed formats: half (16-bit), single (32-bit), double (64-bit), and quad (128-bit). But modern workloads – especially deep learning, DSP, and mixed-precision HPC – need floating-point formats that don’t exist in the standard: 8-bit floats for inference, 24-bit floats for AMD GPUs, formats with max-exponent value numbers for gradual overflow, or saturating arithmetic instead of infinity.

The Universal cfloat type is a fully parameterized floating-point that can emulate any IEEE-754 format and extend beyond it. You configure the total bit width, exponent size, subnormal behavior, max-exponent value behavior, and overflow semantics at compile time. The result is a type that behaves exactly like hardware floating-point but with the precision and range you choose.

cfloat<nbits, es, bt, hasSubnormals, hasMaxExpValues, isSaturating> is a configurable floating-point:

Parameter Type Default Description
nbits unsigned – Total bits (4 to 256+)
es unsigned – Exponent bits
bt typename uint8_t Storage block type
hasSubnormals bool – Enable gradual underflow
hasMaxExpValues bool – Reclaim max-exponent encodings as numeric values
isSaturating bool – Overflow saturates instead of producing infinity

Standard sign-exponent-fraction layout:

[sign : 1 bit] [exponent : es bits] [fraction : nbits - 1 - es bits]
  • Exponent bias: 2^(es-1) - 1
  • Hidden bit: 1 for normal numbers, 0 for subnormals
  • Infinity: exponent all-1s, fraction all-0s (when hasMaxExpValues is false), as in IEEE-754
  • NaN: exponent all-1s, fraction non-zero (when hasMaxExpValues is false), as in IEEE-754
  • When hasMaxExpValues: the all-1s exponent binade encodes numeric values; only 4 encodings are reserved: +-inf is s.11...11.11...10 and NaN is s.11...11.11...11
  • When isSaturating: overflow produces maxpos/maxneg instead of infinity. With hasMaxExpValues as well there is no infinity at all: s.11...11.11...10 is maxpos/maxneg (the OCP E4M3FN layout)
  • Quiet and signalling NaN: without hasMaxExpValues the fraction MSB is the quiet bit, as in IEEE-754 (#1896): set is quiet, clear is signalling, whatever the sign. setnan writes 0.11...11.10...00 (quiet) and 0.11...11.01...00 (signalling). With hasMaxExpValues, and in the corner case of a one-bit fraction, there are only two NaN patterns and the sign is the type: 0.11...11.11...11 is quiet, 1.11...11.11...11 is signalling
  • Invalid operations (inf - inf, 0 * inf, 0 / 0, sqrt(-x)) deliver a quiet NaN

One rule covers every configuration: infinity is the encoding right after maxpos, and every NaN sits above it. Subnormals do not affect the all-1s exponent binade, so the eight configurations have three layouts there. For cfloat<8,4>, positive half of the all-1s exponent binade:

configuration 0x78 .. 0x7D 0x7E 0x7F maxpos
hasMaxExpValues false 0x78 inf, 0x79..0x7B sNaN, 0x7C..0x7D qNaN qNaN qNaN 0x77 = 240
hasMaxExpValues 256 .. 416 inf NaN (quiet) 0x7D = 416
hasMaxExpValues, isSaturating 256 .. 416 448 NaN (quiet) 0x7E = 448

So half, single, duble, quad and the other configurations without max-exponent values are bit-compatible with IEEE-754 for values and infinities (#1780), and classify every IEEE-754 NaN pattern as NaN of the same type, quiet or signalling (#1896). The NaN payload and sign are not carried: converting a native NaN, or setting one with setnan, writes the canonical pattern of its type. A cfloat quiet NaN is the native quiet_NaN() pattern.

using fp8e2m5 = cfloat<8, 2, uint8_t, true, false, false>; // quarter precision
using fp8e3m4 = cfloat<8, 3, uint8_t, true, false, false>; // ML training
using fp8e4m3 = cfloat<8, 4, uint8_t, true, false, false>; // NVIDIA FP8
using fp8e5m2 = cfloat<8, 5, uint8_t, true, false, false>; // NVIDIA FP8
using half = cfloat<16, 5, uint16_t, true, false, false>; // IEEE-754 binary16
using single = cfloat<32, 8, uint32_t, true, false, false>; // IEEE-754 binary32
using duble = cfloat<64, 11, uint32_t, true, false, false>; // IEEE-754 binary64
using quad = cfloat<128, 15, uint32_t, true, false, false>; // IEEE-754 binary128
using bfloat_t = cfloat<16, 8, uint16_t, true, false, false>; // Google Brain float
using msfp8 = cfloat<8, 2, uint8_t, false, false, false>; // Microsoft FP8
using amd24 = cfloat<24, 8, uint32_t, true, false, false>; // AMD 24-bit

Arithmetic is performed using a blocktriple intermediate representation that carries sufficient precision to compute exact products and sums before rounding. The pipeline is:

  1. Decode operands into blocktriple (sign, scale, significant)
  2. Compute in extended precision (the blocktriple is wider than the target)
  3. Round using round-to-nearest-even (or configurable rounding mode)
  4. Encode back into cfloat format

This mirrors how hardware FPUs work internally but is fully parameterized at the template level.

#include <universal/number/cfloat/cfloat.hpp>
using namespace sw::universal;
// FP8 with 4 exponent bits, 3 fraction bits, saturating
using fp8 = cfloat<8, 4, uint8_t, true, false, true>;
fp8 weight(0.5f);
fp8 activation(0.75f);
fp8 result = weight * activation; // Saturates on overflow, no infinity
// Explore the encoding
std::cout << to_binary(result) << " = " << result << std::endl;
std::cout << "maxpos: " << fp8::maxpos() << std::endl;
std::cout << "minpos: " << fp8::minpos() << std::endl;
template<typename HighPrec, typename LowPrec>
HighPrec mixed_precision_dot(const std::vector<LowPrec>& a,
const std::vector<LowPrec>& b) {
HighPrec sum(0);
for (size_t i = 0; i < a.size(); ++i) {
sum += HighPrec(a[i]) * HighPrec(b[i]); // accumulate in high precision
}
return sum;
}
using FP8 = cfloat<8, 4, uint8_t, true, false, false>;
using FP32 = cfloat<32, 8, uint32_t, true, false, false>;
std::vector<FP8> weights = { FP8(0.5), FP8(0.25), FP8(0.125) };
std::vector<FP8> inputs = { FP8(1.0), FP8(2.0), FP8(3.0) };
FP32 result = mixed_precision_dot<FP32>(weights, inputs);

Full encoding efficiency with hasMaxExpValues

Section titled “Full encoding efficiency with hasMaxExpValues”
// IEEE-754 configuration
constexpr bool hasSubnormals = true;
constexpr bool hasMaxExpValues = false;
constexpr bool isSaturating = false;
// cfloat<nbits, es, BlockType, hasSubnormals, hasMaxExpValues, isSaturating>;
// Standard: subnormals, overflow -> infinity, last binade used for special encodings: inf and NaN
using Standard = cfloat<8, 4>; // equivalent to cfloat<8, 4, uint8_t, true, false, false>;
// Expanded: subnormals, overflow -> infinity, last binade encodes values extending normal range
using Expanded = cfloat<8, 4, uint8_t, true, true, false>;
// Saturating: subnormals, no overflow, last binade encodes values extending normal range
using Saturated = cfloat<8, 4, uint8_t, true, true, true>;
Standard s(200.0f); // may produce infinity
Expanded e(200.0f); // uses max-exponent value encoding, can still produce infinite
Saturated c(200.0f); // uses max-exponent value encoding, stays finite
Problem How cfloat Solves It
Need 8-bit float for ML inference but IEEE has no 8-bit format cfloat<8, es> with configurable exponent width
Infinity corrupts neural network training isSaturating=true clamps to maxpos
Exploring precision/range trade-offs for new hardware Any combination of nbits and es
Need IEEE-754 semantics in software for testing Exact emulation of half/single/double/quad
Custom float for FPGA/ASIC design exploration Parameterized at compile time, matches hardware encoding
Subnormal handling is too slow on some hardware hasSubnormals=false to disable gradual underflow
Need wider-than-quad precision (128-bit+) cfloat<256, 19> for octuple precision